Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #7987 (+ #8676) — `sys_account`'s credential columns must not come back on
* #7987 (+ commit d6e80b28b) — `sys_account`'s credential columns must not come back on
* the generic data path.
*
* [#8676] The file was #7987's three OAuth columns; it now covers the object's
* [commit d6e80b28b] The file was #7987's three OAuth columns; it now covers the object's
* other two credential columns as well — `password` and
* `previous_password_hashes`, the one-way hashes of ADR-0100's third channel.
* They belong here rather than in a fixture of their own because they are the
Expand Down Expand Up @@ -88,9 +88,9 @@ const PLANTED = {
const TOKEN_COLUMNS = ['access_token', 'refresh_token', 'id_token'] as const;

/**
* [#8676] The two one-way password hashes on the same object — ADR-0100's third
* [commit d6e80b28b] The two one-way password hashes on the same object — ADR-0100's third
* channel. They serialized on this very read path alongside the OAuth columns
* (the #8676 key list was captured on this fixture's own ablation run), through
* (commit d6e80b28b's key list was captured on this fixture's own ablation run), through
* the same two barriers that miss them: `collectMaskedReadFields` keys on the
* field TYPE and exempts `managedBy: 'better-auth'`, while these are
* `text` / `textarea`. Asserted through the SAME persona matrix below, because
Expand Down Expand Up @@ -277,7 +277,7 @@ describe('#7987: sys_account OAuth tokens never serialize on the generic data pa
assertNoCredentialColumns(row);
}

// [#8676] The same spelling attack aimed at the password hashes, from both
// [commit d6e80b28b] The same spelling attack aimed at the password hashes, from both
// personas. The member's own row is the one that matters most here: the
// `sys_account_self` policy grants the read, so this is a LEGAL request for
// their own record that must still come back without the hash.
Expand Down Expand Up @@ -334,11 +334,11 @@ describe('#7987: sys_account OAuth tokens never serialize on the generic data pa
// exactly one predicate — `internal === true` — so a column without the
// flag is refused (ADR-0112 code + status).
//
// ⚠️ Its instance changed with #8676, and only its instance. This test used
// ⚠️ Its instance changed with commit d6e80b28b, and only its instance. This test used
// to spell the predicate with `password`, because #7987 deliberately left
// that column unflagged and the test marked THAT card's scope boundary
// ("a column that is **not flagged** … are deliberately NOT `internal`").
// #8676 flags it, so the premise of `password`-as-example disappears while
// Commit d6e80b28b flags it, so the premise of `password`-as-example disappears while
// the proposition itself is untouched: `scope` is an ordinary unflagged
// `sys_account` column and stands in as the example. What is NOT weakened
// is the guard — no ADR-0100 carve-out was added, and the positive arm
Expand All @@ -361,7 +361,7 @@ describe('#7987: sys_account OAuth tokens never serialize on the generic data pa
it('[#8676] `password` and `previous_password_hashes` are stripped, and reachable only through the accessor', async () => {
// The card's own assertions, both directions. These are one-way password
// hashes — ADR-0100's third channel — and they serialized on the generic
// data API before #8676: to an admin for every user's row, and to a member
// data API before commit d6e80b28b: to an admin for every user's row, and to a member
// for their own.
const rows: any[] = await ql.find('sys_account', {
where: { id: memberAccountId },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
// hand-roll better-auth's signed-cookie contract with
// `/admin/stop-impersonating` and silently detach the #8243 bearer-rotation
// hook. It is a better-auth PLUGIN endpoint with only the authorization
// predicate replaced, and since #11686 that predicate is the consolidated
// predicate replaced, and since commit 7131f12bf that predicate is the consolidated
// authority `hasPlatformAdminStanding`. `has-permission` (#11900, ruled
// 2026-08-25) is a third shape: a permission QUERY, raw-mounted WITHOUT
// the refusing judge — the platform admin's query is answered from the
Expand Down Expand Up @@ -59,7 +59,7 @@
// then folds back into `positions[]`. A working "Set Platform Role"
// button would be a supported, gated, one-user-at-a-time channel for
// resurrecting the dual identity representation the 2026-08-18 Option-3
// veto killed. So the maintainer retired the CONSOLE ACTION (PR #11530)
// veto killed. So the maintainer retired the CONSOLE ACTION (commit 033a34c7c)
// and left the vendor ROUTE mounted and vendor-gated, byte for byte.
//
// ⛔ THEREFORE: a `403` from any of those eight is the system working. Do not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
// gate verdict and not a payload the server rejects for everyone.
//
// `shaded-vendor-gate` (1 route: `remove-user`) — the two halves belong to
// DIFFERENT layers, which is why it is neither of its neighbours. #11477
// DIFFERENT layers, which is why it is neither of its neighbours. Commit 6dd3e6968
// gave the route the raw-mount shading `ban-user` already had, so an
// ObjectStack gate answers the refusal (member 403 PERMISSION_DENIED, anon
// 401 UNAUTHENTICATED) — but the mount DELEGATES rather than
Expand All @@ -88,7 +88,7 @@
// The both-sides contrast is therefore not a 2xx but a DIFFERENCE: the
// member and the admin hear two different refusals, which is what proves
// the member's 403 is an authorization verdict and not a blanket refusal.
// The bucket also carries the #11477 negative — a member must never again
// The bucket also carries commit 6dd3e6968's negative — a member must never again
// see the break-glass guard's `409 LAST_LOCAL_CREDENTIAL`, which before the
// shading was answered ahead of every authorization layer and VARIED WITH
// THE TARGET, disclosing per-record state to a caller entitled to none.
Expand Down Expand Up @@ -336,7 +336,7 @@ function expectationsFor(targetUserId: string): Record<string, RouteExpectation>
body: { userId: targetUserId },
},

// ── #11477 — shaded for ORDERING, still admitted by the vendor ─────────
// ── Commit 6dd3e6968 — shaded for ORDERING, still admitted by the vendor ─
//
// The only member of its bucket, and the bucket exists because this route
// genuinely has a third shape rather than because the other two did not
Expand All @@ -345,7 +345,7 @@ function expectationsFor(targetUserId: string): Record<string, RouteExpectation>
// owned by different layers:
//
// refusal → ObjectStack's gate (403 PERMISSION_DENIED), because the
// mount answers first. That is #11477's whole point: the
// mount answers first. That is commit 6dd3e6968's whole point: the
// break-glass `hooks.before` guard used to answer an
// authenticated non-admin BEFORE any authorization ran, and
// its 409 differed per target — a per-record disclosure.
Expand Down Expand Up @@ -634,7 +634,7 @@ describe('#9482 C9: every derived /admin/ route refuses a non-admin', () => {
}, 600_000);

it('the shaded vendor route refuses a non-admin from the ObjectStack gate, before the break-glass guard', async () => {
// #11477. The both-sides contrast here is NOT a 2xx — it is that the two
// Commit 6dd3e6968. The both-sides contrast here is NOT a 2xx — it is that the two
// callers hear DIFFERENT refusals. A member is turned away by ObjectStack's
// gate (`PERMISSION_DENIED`) and a platform admin gets past it only to be
// turned away by the vendor's (`YOU_ARE_NOT_ALLOWED_*`, #9969). Two
Expand All @@ -653,7 +653,7 @@ describe('#9482 C9: every derived /admin/ route refuses a non-admin', () => {
expect(member.status, `${route} member: ${member.body}`).toBe(403);
expect(member.code, `${route} member code: ${member.body}`).toBe('PERMISSION_DENIED');

// ⛔ The load-bearing negative. Before #11477 the break-glass
// ⛔ The load-bearing negative. Before commit 6dd3e6968 the break-glass
// `hooks.before` guard answered an authenticated non-admin ahead of every
// authorization layer, and its answer varied with the TARGET — a
// per-record disclosure to a caller entitled to nothing. A member must
Expand Down Expand Up @@ -720,7 +720,7 @@ describe('#9482 C9: every derived /admin/ route refuses a non-admin', () => {
// transaction, so this route answers the authorization question like
// every other member of the bucket and needs no exception.
//
// ⚠️ #11477 moved `remove-user` OUT of this bucket entirely — its raw
// ⚠️ Commit 6dd3e6968 moved `remove-user` OUT of this bucket entirely — its raw
// mount now answers a member from ObjectStack's gate
// (`403 PERMISSION_DENIED`) before better-auth is reached at all, so the
// vendor-vocabulary rule below no longer describes it. It lives in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// "refused outright (#4757)", on the reasoning that "nothing was ever queried"
// must not read as "nothing to authorize". `attachment-access-hooks.ts` carries
// exactly that refusal, and `attachment-access-hooks.test.ts` pins it against a
// wired engine (#9797). This file pins it END TO END, on the real stack, where
// wired engine (commit 1258dcaee). This file pins it END TO END, on the real stack, where
// RBAC and plugin-sharing are in the path and the session is a real one.
//
// ## History — this file's original verdict has been OVERTAKEN, twice
Expand All @@ -20,7 +20,7 @@
// the `where === undefined` check. That was a PRODUCT gap (#9719), and this
// file deliberately declined to pin the behaviour of the day.
//
// It has since been fixed. #9719/PR #9797 added an opt-in whole-operation
// It has since been fixed. #9719/commit 1258dcaee added an opt-in whole-operation
// dispatch to the engine, which #9974 renamed `dispatchUnscopedMultiWrite` when
// it was ruled onto `beforeUpdate` as well. `attachment-access-hooks.ts`
// declares it on both sys_attachment write registrations, so the #4757 refusal
Expand All @@ -35,7 +35,7 @@
// properties that look identical on a fixture whose rows split entitled/not —
// which is exactly the fixture the first block below uses. Measured on this
// suite: with `dispatchUnscopedMultiWrite` removed from BOTH registrations and
// service-storage rebuilt (the pre-#9797 world), the first block stays 5/5
// service-storage rebuilt (the world before commit 1258dcaee), the first block stays 5/5
// GREEN. It cannot see the refusal it is named for.
//
// So the second block seeds the ONE fixture that separates them: a caller who
Expand Down Expand Up @@ -163,7 +163,7 @@ describe('sys_attachment delete gate under an unscoped multi-delete (#9483)', ()

it('an unscoped multi-delete is refused OUTRIGHT — on its shape — and deletes NOTHING', async () => {
// `{ multi: true }` with neither id nor where composes an AST over the whole
// table. Since #9797 the refusal that answers is #4757's whole-operation
// table. Since commit 1258dcaee the refusal that answers is #4757's whole-operation
// one, dispatched BEFORE any row is resolved — not the per-row gate, which
// on this fixture would also have refused (the member is the uploader of
// one row and neither uploader nor parent-editor of the other).
Expand Down Expand Up @@ -193,7 +193,7 @@ describe('sys_attachment delete gate under an unscoped multi-delete (#9483)', ()

it('an empty `where: {}` reaches the same verdict by a DIFFERENT rule — the per-row gate', async () => {
// ⚠️ Same outcome, deliberately different mechanism, and the difference is
// load-bearing. #9797 scoped the whole-operation dispatch to a delete with
// load-bearing. Commit 1258dcaee scoped the whole-operation dispatch to a delete with
// NO `where` at all; a match-all `where: {}` is a real query, so it is NOT
// refused on shape — it is refused here only because this caller cannot
// have the foreign row. Asserting the per-row message is what keeps that
Expand Down Expand Up @@ -259,7 +259,7 @@ describe('sys_attachment delete gate under an unscoped multi-delete (#9483)', ()
// whole-operation rule or from the per-row gate, because that fixture holds one
// row the caller may not touch. Here the caller uploaded BOTH rows, so the
// per-row gate has nothing to refuse — anything that still refuses is refusing
// the SHAPE. Pre-#9797 this exact call resolved and emptied the table.
// the SHAPE. Before commit 1258dcaee this exact call resolved and emptied the table.
// ─────────────────────────────────────────────────────────────────────────────

describe('sys_attachment unscoped multi-delete is refused on its SHAPE, not on entitlement (#9483)', () => {
Expand Down Expand Up @@ -352,7 +352,7 @@ describe('sys_attachment unscoped multi-delete is refused on its SHAPE, not on e
});

it('refuses `{ multi: true }` with no id and no where — even though the caller may delete every matched row', async () => {
// #9719's measured wipe, end to end: before PR #9797 this call RESOLVED and
// #9719's measured wipe, end to end: before commit 1258dcaee this call RESOLVED and
// took both rows (2 -> 0). The per-row gate licenses each row individually,
// so nothing but the whole-operation #4757 rule can refuse here — which is
// what makes this the case that detects its removal.
Expand Down
8 changes: 4 additions & 4 deletions packages/qa/dogfood/test/authz-conformance.matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
// silencing that particular red costs an enforcement site rather than a
// `covers` append on a row that records an absence.
//
// [#8711] That completeness is over ROUTES, not over primitives: a primitive
// [commit 2ce1eb41b] That completeness is over ROUTES, not over primitives: a primitive
// enforced by a predicate inside an existing resolver adds no entry point, so
// it can be neither UNCLASSIFIED nor STALE. Measured against the rows below:
// 44 of 51 carry no `covers` key at all (7 rows, 15 keys, every one an
Expand Down Expand Up @@ -390,12 +390,12 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
// mass-revoked) and the 0/1 storage shape the primary driver returns is
// judged as well as a literal `false`.
//
// [#8711] Both rows carry NO `covers`, and that is a statement about the
// [commit 60ade586e] Both rows carry NO `covers`, and that is a statement about the
// RATCHET, not an omission: `discover()` enumerates HTTP entry points from a
// curated per-file probe table, and a predicate inside an existing resolver
// adds no entry point — so neither flag could ever have surfaced as
// UNCLASSIFIED during the whole period it was inert. These two rows restore
// the ledger's stated invariant. [Resolved — maintainer ruling on #8711,
// the ledger's stated invariant. [Resolved — maintainer ruling (commit 2ce1eb41b),
// 2026-08-15] The invariant's advertised SCOPE is narrowed to what the
// ratchet can check, not the ratchet widened to reach in-resolver
// predicates like this one — widening was measured unachievable in general
Expand All @@ -408,7 +408,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
enforcement: 'core/security/resolve-authz-context.ts step 6a — isRowActive gates BOTH halves, and only both hold it: (i) only ACTIVE position ids collect their `sys_position_permission_set` linkage, so a deactivated position carries no bound set; (ii) the deactivated NAME is dropped from `grants.positions`, because resolvePermissionSetsForContext requests positions as permission-set NAMES and a name left standing resolves the same grant one layer down',
note: 'Only a name whose `sys_position` row is EXPLICITLY deactivated is dropped — a name with no row at all (`org_owner`, a membership-derived role, the built-in `everyone` audience anchor) has no flag to read and is untouched. Deliberately NOT a blanket revocation of the sets themselves: a set held via BOTH a deactivated position AND a direct user grant still resolves, since the direct grant is a different grant (resolve-authz-context.test.ts pins exactly that case). Symmetrically, the WRITE gates and blast-radius reads in plugin-security (assertAudienceAnchorBindingGate, setsBoundToPosition, the delegated-admin surfaces) stay UNFILTERED on purpose — dropping a deactivated row there would make a refused binding permitted, narrow a delegate\'s boundary, and make a deactivated position unmanageable. Unit-proven in core/security/resolve-authz-context.test.ts (a deactivated position stops granting its sets; an active one still grants; an absent column grants; the 0/1 shape deactivates; deactivating ONE position leaves the others granting) + core/security/row-active.test.ts. Not HIGH_RISK for the same reason as `permission-set-active`.' },

// ── ADR-0091 D1/D2 — grant validity windows (#8811) ───────────────────
// ── ADR-0091 D1/D2 — grant validity windows (commit d6e793507) ────────
//
// The sibling of the `active` switch above, and enforced at the same seam
// for the same stated reason: a grant that is supposed to lapse on a date,
Expand Down
2 changes: 1 addition & 1 deletion packages/qa/dogfood/test/authz-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// ADR-0056 D10 — the authorization conformance matrix is a CHECKED artifact,
// within the scope the mechanism can see: routes are ratcheted, primitives are
// hand-maintained (see the matrix's own header for the narrowed claim and the
// measured numbers — #8711). Refactored onto the reusable ADR-0060
// measured numbers — commit 2ce1eb41b). Refactored onto the reusable ADR-0060
// `checkLedger` helper: one call asserts every shared invariant (valid state,
// enforced-has-site, experimental/removed-has-note, proof-file-exists,
// high-risk-has-proof). A row that regresses one of THOSE invariants, or a
Expand Down
8 changes: 4 additions & 4 deletions packages/qa/dogfood/test/authz-probe-blind-spot.census.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,9 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
// `this.routeManager.register(` reads 73 because the helper's forwarder is
// one of them, and it is sliced out before counting.
//
// [#13214] `enforceAuth` 61 -> 64. ⛔ RE-ANCHORED, not relaxed: the control
// [commit cc837dbfe] `enforceAuth` 61 -> 64. ⛔ RE-ANCHORED, not relaxed: the control
// exists to prove this census is still reading the file it thinks it is, and
// a rising `enforceAuth` is precisely what the 2026-08-30 ruling on #13214
// a rising `enforceAuth` is precisely what the 2026-08-30 ruling (commit cc837dbfe)
// was supposed to cause — `registerUiEndpoints` was the ONE route in this
// file that resolved no identity, and it is now guarded. The move is +3 over
// the whole file (`occurrences` counts the bare term, comments included):
Expand All @@ -445,9 +445,9 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
// ⚠️ The three sibling numbers were re-derived and did NOT move, which is
// what says this is a guard change and not a surface change: `population`
// 80, `reachable` 19, `private register*Endpoints(` 17 and
// `this.routeManager.register(` 80 are all unchanged — #13214 added no route
// `this.routeManager.register(` 80 are all unchanged — commit cc837dbfe added no route
// and no registrar. `blindSpot` therefore stays 61 as well.
// ⚠️ That last figure is the reading AS OF #13214 and is left as written:
// ⚠️ That last figure is the reading AS OF commit cc837dbfe and is left as written:
// the control is 73 today for the spelling reason recorded above, and the
// population it feeds is still 80. Do not "correct" the paragraph — it is a
// dated measurement, not a live claim.
Expand Down
Loading
Loading