Skip to content

[maintainer] refactor(plugin-auth): split src/auth-manager.ts (10,338 lines, +1,867 in 30 days) along its AuthManager seams, one move-only slice per PR #25224

Closed

Description

@objectstack-fleet

Filing gate ③: a task the maintainer dispatched, quoted verbatim below. Filed by domain:cli seat 1 (seat post #6024) · os-sales · session session_01CxBKycaiUpA1yGx1XyoGqX, as the restart act of #24586. ⛔ Not a claim.

The maintainer's words, verbatim (#24586, recorded in 6270097840): 「24586 同意A,是否建议这波分拆等 v18 发布之后再处理。」 The restart condition (npm @objectstack/spec latest ≥ 18.0.0) was read true by triage in 6291735609. The ruling's restart act: "one [maintainer] card per file over 10,000 lines into its owning lane … Each card starts with a fresh seam measurement taken at that time, in #23050's shape." This file was under the line at the ruling (8,471 lines 30 days ago) and is over it now, so the ruling's per-file rule covers it.

Reader who acts: the domain:services seat (packages/plugins/plugin-auth is that lane's), one dispatch per slice.

Measured (on origin/main 123ff6f4e, 2026-10-22)

History note: this clone's history starts 2026-09-17, so "total" commit counts are lower bounds; the 7- and 30-day windows are complete, and git blame --since=2026-09-22 agrees with a diff against the last pre-window commit fa2980341 to within 7 lines.

  • Size and churn: 10,338 lines (8,471 thirty days ago: +1,867, +22.0%). 24 commits in 7 days, 47 in 30 days. git blame --since=2026-09-22 attributes 2,277 lines (22.0%) to the last 30 days.
  • Structure: a 1,583-line module preamble (AuthManagerOptions alone is 470 lines), then one AuthManager class (8,755 lines, 181 members). Two methods dominate: createAuthInstance (1,424 lines, the better-auth config with about 30 sub-banners in its hook arms) and buildPluginList (1,336). The largest sections:
section lines lines from the last 30 days
module preamble 1,583 341
plugin assembly (resolvePasswordHasher, addOptionalPlugin, buildJwtPlugin, buildPluginList) 1,551 241
createAuthInstance 1,423 324
request routing, base paths, subject erasure, SCIM, MCP URLs, getPublicConfig, SSO 1,376 168
membership, audience admission, self-registration grants, bootstrap staging 1,206 416
password policy, lockout, auth gates, session controls 1,155 330
email/SMS delivery + locale 560 58
actor + organization guards 553 191
  • Shared state a split must pass: config (171 refs), getDataEngine (22), logDurabilityDegradation (12), auth, resolveActor, the request-scoped staging maps (pendingSelfRegistrationGrants, pendingOwnerVerifiedStamps, pendingOperatorProvisioning). createAuthInstance's hook arms call into the membership, guard, password and email sections.
  • What reads it by path: 12 scripts/ files, including the ADR anchor scripts/adr-anchors/packages__plugins__plugin-auth__src__auth-manager.ts.json (ADR-0093, ADR-0108), check-auth-mount-ledger.mjs, check-dispatcher-error-vocabulary.mjs, tenant-audit-census.mjs, publish-smoke.sh and four baselines; 19 files under content/ and docs/; 119 test files.
  • In flight: none of the 10 open PRs touch the file.
  • ⚠️ This is an authentication boundary. Move-only is the whole contract here: a slice that changes any sign-in, session or membership outcome is out of scope.

The ask (from #24586's ruling and #23050's shape)

  1. Split along the existing seams, one area per PR. Each slice is move-only: no logic changes.
  2. Shared state stays explicit: moved code reaches the class's shared members through one named context object. ⛔ Not a second copy of a helper, ⛔ not a re-derivation.
  3. Every slice proves it moved nothing: every gate, baseline, ADR anchor and docs anchor that reads the file by path is migrated in the same PR and stays green. ⛔ A gate whose target moved must not go silently empty.
  4. Serial with the file's in-flight PRs (a slice waits for an open PR on the same region, or the PR merges main after the slice).
  5. ⛔ No new gate.

Not in this card

Any behaviour change. A design change to the shared members beyond giving them an explicit context. Splitting files in other lanes (#24586 hands those to their own seats).

Dedupe

MCP search_issues on this repository, closed included: "split large file move-only service-automation engine.ts security-plugin.ts auth-manager.ts decompose modules" → 2 hits: #24586 (the ruling this card carries out) and #23050 (closed, rest-server.ts). New.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    on Oct 22, 2026
  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial note · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-22T13:23Z. ⛔ Not a claim. Thread-read: none

    packages/plugins/plugin-auth/src/auth-manager.ts is in flight under #25112 (claim 6341542393, the magic-link mount). The first slice of this card is cut only after #25112's PR lands, on a fresh seam reading of the merged file. Open PRs touching the file at this note: none besides that card's coming PR.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · series card, cut 1 (one cut = one PR, move-only, cuts strictly serial) · 2026-10-22T18:01Z
    Session: session_01LSXBc2GCBwiAL7vV4kWkyr
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lg3wrj (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25224 (a linked worktree in this session's container)
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (fresh seam reading on origin/main 0f1eeee, after #25112's PR #25238 landed: auth-manager.ts is 10,349 lines, AuthManager has 176 members; stop on a breach and explain it in the report): cut 1 moves the body of AuthManager#buildPluginList (:3424-:4768, 1,345 lines, the largest single member after createAuthInstance) unedited into one new module, packages/plugins/plugin-auth/src/better-auth-plugin-list.ts, by #23050's pattern (landed note 6136102437, steps 1-6): the class keeps the private method and its docblock, and its body becomes one named host literal plus .call. The body reads 22 class members (AST walk on 0f1eeee): 2 fields read (config, reassigned after construction, so a getter; degradedFeatures, never reassigned), 2 fields written (jwtSigningFailureReported, orgRolesMap, so accessors), and 18 methods (late-bound closures). In the same PR, every reader whose reading includes a moved line is re-pointed with its sum unchanged: in-package source scanners (better-auth-schema-parity.test.ts and managed-extension-fields.test.ts read the plugin factory imports the body carries; the rest found by measurement), any docs/qa/platform-checklist/areas/*.json symbol anchor that resolves inside the moved range, and any scripts/ reader of moved lines (declared cross-lane in the PR). ⛔ Not packages/spec, not any ADR, not createAuthInstance or any other member. Later cuts each take their own amended claim on the merged file.
    Container & model: L (series), card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier: "no path-derived mandate"), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Basis for the Clause-② line: a move-only cut widens no accept set and no public surface; the proof owed is @objectstack/plugin-auth's dist/*.d.ts byte-identical to base and the same export list.
    Responsibility: n/a — not a defect card
    Thread-read: 6341869420
    Serial constraints cleared: none of the 14 open PRs touches packages/plugins/plugin-auth/src/auth-manager.ts (each PR's file list read this act). #25265 touches four other plugin-auth files (last-admin-guard.ts, default-organization-invariant.ts, ensure-default-organization.ts, tenancy-service.ts) and its own test, none of them a reader of the moved range. The serial predecessor named in 6341869420 (#25112) landed as PR #25238.
    Resume-from: none


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · series card, cut 1 (one cut = one PR, move-only, cuts strictly serial) · amends claim 6347670835, whose cut-1 file surface breached on measurement · 2026-10-22T19:06Z
    Session: session_01LSXBc2GCBwiAL7vV4kWkyr
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lg3wrj (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25224 (a linked worktree in this session's container)
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (amended; same base 0f1eeee): the breach. 6347670835 put AuthManager#buildPluginList in cut 1. Measured before any reader was counted, that move does not stand alone: its body needs module-level helpers that only it reads (the organization slug guard, the organization-hook codes block at :1492-:1548, consentOrganizationOf) and two names it shares with the rest of the file (readBooleanEnv, OAUTH_ORGANIZATION_CLAIM), which index.ts's export * from './auth-manager.js' forbids exporting from auth-manager.ts (the published .d.ts would widen), so they need a non-re-exported shared module too. That came to about 3,075 changed lines, over the card's 3,000-line human-merge line, and the range also carries an ADR-0108 anchor. buildPluginList therefore becomes a later cut, landed by hand. Cut 1 is now the actor and organization guards: eight AuthManager methods (resolveActor, isPlatformAdminUserId, assertActorNotHeld, assertOrganizationDeletable, assertRemoveMemberPermitted, assertRevokeSessionIdentifiesRecord, assertAdminRevokeUserSessionIdentifiesRecord, resolveMembershipRole; 358 body lines) and the module-level INVITATION_ROLE_CAP_READ_FAULT_CODE / invitationRoleCapReadFaultApiError (:1464-:1490, read only by those bodies) move unedited into one new module, packages/plugins/plugin-auth/src/auth-manager-actor-guards.ts, by #23050's pattern (landed note 6136102437). The class keeps each method and its docblock; each body becomes a per-method host (Pick<ActorGuardHost, …>) plus .call. The bodies read five class members (AST walk on 0f1eeee): orgRolesMap and organizationDeleteStandingJudge (fields, handed over as getters) and computeAuthGate, getDataEngine, resolveActor (late-bound closures). Readers whose reading includes a moved line, re-pointed in the same PR with their sums unchanged: api-error-code.pin.test.ts (the per-file floor), better-auth-schema-parity.test.ts and managed-extension-fields.test.ts (the plugin factory scan: hasPermission, defaultRoles), organization-delete-read-fault.test.ts (the caller frame), and one row of packages/runtime/src/dispatcher-error-vocabulary.ts (domain:cli, declared cross-lane in the PR). No changeset: skip-changeset, per ruling 6348891160 item 4. No docs/qa/platform-checklist anchor and no ADR anchor names a moved symbol (measured). ⛔ Not packages/spec, not any ADR, not buildPluginList, createAuthInstance or any other member. Later cuts each take their own amended claim on the merged file.
    Container & model: L (series), card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier: "no path-derived mandate"), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Basis for the Clause-② line: a move-only cut widens no accept set and no public surface. The proof owed is ruling 6348891160 item 1 (on #25222, covering this card), read on @objectstack/plugin-auth's dist/index.d.ts and dist/index.d.mts, each: the same declaration set (no private member made public or protected; nothing added, removed, renamed or retyped); byte-identical to base after one normalisation only, sorting the members of each string-literal union in place; the same ESM and CJS export lists; and every raw-diff line a union-member swap. Measured at this cut's head: the raw files are byte-identical to base (0 diff lines), with the ruling's positive control in the PR and the report.
    Responsibility: n/a — not a defect card
    Thread-read: 6347670835
    Ruling-ref: 6348891160
    Serial constraints cleared: re-read this act. None of the 13 open PRs touches any of this cut's seven files (each PR head diffed against its merge base with origin/main; #24467's one file read through the API). New since 6347670835: #25268 (packages/mcp, packages/qa), #25266 (plugin-sharing, plugin-security, cli, spec, docs) and #25270 (service-automation, #25222's slice 1). #25265 still touches five other plugin-auth files, none of them in this cut; the Version Packages PR #24876 touches only plugin-auth's CHANGELOG.md and package.json. origin/main gained three commits since 0f1eeee, none touching these files.
    Resume-from: none


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25224,
    "status": "done",
    "branch": "claude/project-thread-lg3wrj",
    "pr": "#25273",
    "session": "session_01LSXBc2GCBwiAL7vV4kWkyr",
    "premise_still_valid": true,
    "summary": "Cut 1 of the auth-manager.ts split, per the amended claim 6349004758 (Ruling-ref 6348891160): eight AuthManager method bodies (resolveActor, isPlatformAdminUserId, assertActorNotHeld, assertOrganizationDeletable, assertRemoveMemberPermitted, assertRevokeSessionIdentifiesRecord, assertAdminRevokeUserSessionIdentifiesRecord, resolveMembershipRole; 358 lines) and the module-level INVITATION_ROLE_CAP_READ_FAULT_CODE block (base :1464-:1490) move unedited into packages/plugins/plugin-auth/src/auth-manager-actor-guards.ts by the landed #23050 pattern: the class keeps each method, signature and docblock, its body a per-method host of exactly the members it reads (three methods as late-bound closures, two reassigned fields as getters) plus .call, async dropped. Five path readers whose reading included a moved line were re-pointed in the same PR with their sums unchanged, one of them cross-lane (packages/runtime/src/dispatcher-error-vocabulary.ts, domain:cli, one row's file field). The first claim 6347670835 named buildPluginList for cut 1; measured, that move is about 3,075 changed lines (over the 3,000-line human-merge line) and carries an ADR-0108 anchor, so it becomes a later cut landed by hand, as the amended claim records. No sign-in, session or membership outcome changes: same tests, same published declarations, same export lists.",
    "tests": "Base 0f1eeee, head 26c8deb (source unchanged after e55cb6d; 26c8deb only drops the changeset e315a80 added). (1) Move proof, TS scanner token identity per body plus every line = base line minus two spaces plus same params/return plus stub docblock and signature kept with async dropped: 8 of 8 pass, 358 body lines; base :1464-:1490 (27 lines) verbatim in the module and absent from auth-manager.ts. git diff --color-moved=plain --color-moved-ws=allow-indentation-change marks 398 of 407 deleted and 435 of 566 added lines moved; the 9 other deleted lines are the 8 private async signature lines and one import name. (2) Ruling 6348891160 item 2 block, plugin-auth built through turbo run build at base and head: dist/index.d.ts raw base 82729d0115e4a866ed30dde301da18f96bad321e9a78fcd12b2cdd77bca3801f, raw head the same, normalised base 27d278f6825004c0b604eb60bbf9b3cd0fd8f66a89300bbe62bf7304238f6802, normalised head the same; dist/index.d.mts raw base 9665953d56da469ac783292e8406d09544f8cfe219bac7ef892625fe9a73f96a, raw head the same, normalised base 16f1ed01f0391a8bb5d7de138cbd0f9a7915b83faa9675009cc2996d8cbdb841, normalised head the same; raw diff 0 lines on both, so no union swap occurs and item 1 holds outright. Normaliser verbatim: node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]"(?:\s\|\s*"[^"\n]")+/g,r=>r.split(/\s\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum. Positive control: head dist/index.d.ts with private resolveActor; made public (line 3662, one line) hashes raw 35f7e05b837ddc0f4610783c831b6857e7c5ae7b3bafd23d726ad871255ea299 and normalised 284877cf4dad17a7fe392f3ba8600f57ed6e5321c33a5109bd96b98e1bb66a29, both differ. Export lists: ESM dist/index.mjs and CJS dist/index.js 155 names each at base and head, same sorted-name hash ed3ac8b902031d30 on all four. (3) os-verify-lock -c 'pnpm exec vitest run --maxWorkers=3' in packages/plugins/plugin-auth at e55cb6d: Test Files 185 passed (185), Tests 3487 passed | 10 skipped (3497), VERDICT command-exit 0, the same counts as base. Readers red before their edit, at 1863367 (the move alone): full suite Test Files 3 failed | 182 passed (185), 7 tests (organization-delete-read-fault.test.ts 5 of 7; better-auth-schema-parity and managed-extension-fields: AUTH_MANAGER_PLUGINS names plugins auth-manager.ts no longer imports: defaultRoles, hasPermission); api-error-code.pin.test.ts run alone: expected 18 to be greater than or equal to 24, exit 1; pnpm check:dispatcher-error-vocabulary exit 1 (stale-row plus unclassified-site), exit 0 after the row moved (56 found; 56 classified). pnpm --filter @objectstack/plugin-auth typecheck exit 0; pnpm --filter @objectstack/runtime typecheck exit 0 after the full build; eslint --no-inline-config over the 7 files exit 0. No tested behaviour changed, so no ablation is owed.",
    "gates": "Local subset at 26c8deb: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 59 commands; all 59 ran after a full turbo run build and git fetch --unshallow, each exit code captured before any pipe, all 59 exit 0; --ran: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN. node scripts/pm/check-governed-merges.mjs --branch HEAD: NOT governed, 1,010 changed lines (+598/-412, 7 files) vs the 3,000-line human-merge line. Census gates base vs head: same summaries for check-system-context-census, check-tenant-audit-census, check-registry-log-declared, check:durability-log-level, check:auth-mount-ledger, check:cross-package-test-inputs; scanned-file counts move only by the new module. test:repo not run locally (no repo-project test changed and the derived list does not name it); CI runs it. CI on 26c8deb, the PR's check-runs read at report time (46): 34 success, 12 skipped (path-filtered or opt-in jobs), 0 failure, 0 pending. Build Core success; Test Core (1/6) to (6/6) success and Test Core success; TypeScript Type Check success (Type Check · workspace, · source gates, · consumer gates, · debt ledger: success); Lint & Repo Gates success (Lint · ESLint, · repo & tooling gates, · docs & code guards, · CI, release & engine gates, · PM dispatch-gates, · scoped · tooling self-tests, · scoped · query-options erasure, · scoped · slot lookup & corpus: success); Dogfood Regression Gate success (its shard matrix skipped by its filter); Check Changeset success; Governed Surface Queue Guard success; Check PR Size success; Part-of PR must not also close its card success; The card this PR closes must claim this branch success; No other open PR may claim the same issue success; No other open PR may claim the same single-writer path success; Check Documentation Links success; Flag docs affected by code changes success. mergeable_state clean. No Claude Approvals check runs on this PR. Docs Drift Check (advisory, never fails the build) comment 6349355591: re-derived with node scripts/docs-audit/affected-docs.mjs --json 0f1eeee, exit 0: 35 pages, every one listed through a literal the moved bodies carry unchanged (SERVICE_UNAVAILABLE 11, sys_member 14, sys_organization 10, sys_session 9, NOT_FOUND 1) plus one name collision (content/docs/permissions/system-context.mdx:171 names plugin-approvals' approval-service.ts #resolveActor, a different method); no page restates a rule this move changes, so no doc edit is owed.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "17 — all reads, no write tool: issue_read get 2, issue_read get_comments 3, list_pull_requests 1, search_pull_requests 3, pull_request_read get 2, pull_request_read get_files 4, pull_request_read get_check_runs 2. (claude-code-remote subscribe_pr_activity once, not a GitHub write.)",
    "api_writes": "5 relay acts (scripts/pm/ fleet-write, each read back): 1) label-write on #25224, run 39827597533, 3 actions: POST labels pm:dispatched, DELETE label pm:queue, POST assignees os-sales; 2) post-stamped claim comment 6347670835 on #25224, run 39827763009; 3) post-stamped amended claim comment 6349004758 on #25224, run 39837221540; 4) dispatch pr_create #25273 (draft, assignee os-sales, label skip-changeset; body read back identical), run 39839634890; 5) this report comment on #25224 via post-stamped. Plus git push of the branch (not REST). No other write.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — api-error-code.pin.test.ts at 1863367 failed its per-file floor run alone (expected 18 to be greater than or equal to 24) but the full plugin-auth run at the same head did not report it red; not reproduced, the re-pointed floor is green in both shapes; recorded in the PR Proof section"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed cut 1 · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-22T21:00Z

    reading value
    PR #25273 MERGED through the queue (ACCEPT 6350658338)
    origin/main carries 49185cdb8a

    State: the card stays open and dispatched (Part of); the card thread claims cut 2 afresh on the merged file, per 6349004758.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · series card, cut 2 (one cut = one PR, move-only, cuts strictly serial) · amends claim 6349004758 for cut 2, on the merged file · 2026-10-22T21:28Z
    Session: session_01LSXBc2GCBwiAL7vV4kWkyr
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lg3wrj (the harness-pinned branch for this session, restarted from origin/main after cut 1's PR #25273 merged; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25224 (a linked worktree in this session's container)
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (fresh seam reading on origin/main 49185cdb8a, cut 1 landed per 6351216603: auth-manager.ts is 10,004 lines): cut 2 is the sign-in lockout path (ADR-0069 D2) and the last-login stamp (D7). Seven AuthManager methods (assertAccountNotLocked, recordSignInOutcome, auditRefusedSignIn, resolveSignInAccountId, findSignInAccount, resolveTwoFactorAccountLockout, stampLastLogin; 173 body lines) move unedited into one new module, packages/plugins/plugin-auth/src/auth-manager-sign-in-lockout.ts, by #23050's pattern (landed note 6136102437), the shape cut 1 landed. The class keeps each method and its docblock; each body becomes a per-method host plus .call. The bodies read five class members (AST walk on 49185cdb8a): config (a field the class reassigns in setBaseUrl and applyConfigPatch, so a getter) and findSignInAccount, getDataEngine, logDurabilityDegradation, resolveSignInAccountId (late-bound closures). No module-level declaration moves; the imports only these bodies use leave with them (accountLockedEventFor, refusalCodeOf, signInClientFingerprint, signInFailureEventFor, and the EngineQueryOptions type). Readers whose reading includes a moved line, re-pointed in the same PR with their sums unchanged: api-error-code.pin.test.ts (its sum of throws over the files AuthManager's bodies live in; the ACCOUNT_LOCKED throw moved, so the module joins the list and the sum stays 27). Measured as not reading a moved line: the two plugin-factory scans (the module imports no factory), check:dispatcher-error-vocabulary (exit 0), the query-options erasure ratchet (the moved bodies hold 0 sites; auth-manager.ts keeps its 12, so the baseline holds), and no docs/qa/platform-checklist anchor, ADR anchor or docs page names a moved symbol. Why this seam and not the whole ADR-0069 tail: the first reading took the 16 hardening methods at :8852-:9951. Five of them (assertPasswordNotReused, recordPasswordHistory, unlockUser, enforceSessionControls, enforceConcurrentCap) hold 6 of the file's 12 query-options any-erasure sites, and a site moved into a file scripts/query-options-erasure-baseline.json does not list fails pnpm lint and the shrink-only baseline, so a move cannot carry them. They stay, with a needs-decision on how later cuts carry such sites reported to the coordinator; this cut is the largest coherent seam that holds none. No changeset: skip-changeset, per ruling 6348891160 item 4. ⛔ Not packages/spec, not any ADR, not the five methods above, not buildPluginList, createAuthInstance or any other member. Later cuts each take their own amended claim on the merged file.
    Container & model: L (series), card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier: "no path-derived mandate"), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Basis for the Clause-② line: a move-only cut widens no accept set and no public surface. The proof owed is ruling 6348891160 item 1 (on #25222, covering this card), read on @objectstack/plugin-auth's dist/index.d.ts and dist/index.d.mts, each: the same declaration set (no private member made public or protected; nothing added, removed, renamed or retyped); byte-identical to base after one normalisation only, sorting the members of each string-literal union in place; the same ESM and CJS export lists; and every raw-diff line a union-member swap. Measured at this cut's head: both raw files are byte-identical to base (0 diff lines), so item 1 holds with no union swap to account for; the hashes, the normaliser and the ruling's positive control are in the PR and the report.
    Responsibility: n/a — not a defect card
    Thread-read: 6351216603
    Ruling-ref: 6348891160
    Serial constraints cleared: re-read this act. None of the 14 open PRs touches any of this cut's three files (each PR head diffed against its merge base with origin/main; #24467's one file, scripts/test-shard-timings.json, read through the API). #25286 touches plugin-auth's last-admin-guard.ts and a new test of its own, neither in this cut; the Version Packages PR #24876 touches only plugin-auth's CHANGELOG.md and package.json. #25290 (#25223's cut 1) and #25288 are plugin-security.
    Resume-from: none


    Generated by Claude Code

  8. 42 remaining items

  9. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25224,
    "status": "done",
    "branch": "claude/project-thread-lg3wrj",
    "pr": "#25548",
    "session": "session_01LSXBc2GCBwiAL7vV4kWkyr",
    "premise_still_valid": true,
    "summary": "Cut 13 of the auth-manager.ts split, per the amended claim 6376168919 (amends 6374301142 on the file cut 12 left; landed note 6375260860), under Ruling-ref 6348891160 (the .d.ts bar) and 6364006498 (Sales's ruling A on this card). This is the fifth cut under ruling A and the second of the three createAuthInstance slices the seat set (hooks.after, then hooks.before, then the rest), on the closure shape the seat ruled in scope at 19:56Z. Part of #25224, no closing keyword; #25224 stays open. Base origin/main 93a6ba4, one commit 3ec293d, +734/-597 over 4 files (1,331 changed lines). Moved: the body of the arrow createAuthInstance (:1695, private, annotated Promise of Auth) passes to createAuthMiddleware for hooks.before (base :2245-:2828, BODY :2246-:2827), unedited, into the new module packages/plugins/plugin-auth/src/auth-manager-before-hook.ts (696 lines) as export async function globalBeforeHook(this: BeforeHookHost, ctx: any, setShouldSkipSessionRefresh), re-indented eight spaces. BODY is the before-hook every better-auth endpoint's request passes through (actor resolver for the attribution scope, refresh skip for a minted principal session, per-number OTP send guard, password complexity and reuse checks, change-email capture, remove-member, organization-delete and no-record revoke refusals, held-caller refusal on the admin plugin's role-scalar actions, no-record admin revoke refusal, held-caller refusal on rewriting or deleting an identity provider, identity-provider registration gate, cloud-as-IdP authorization gate, break-glass last-local-credential refusal, audience posture and uniqueness refusal on password sign-up, cloud-managed invite-member refusal, account lockout gate, first-user disableSignUp flip), in the same order. hooks.before keeps its place; its arrow is now non-async, builds one host of 18 late-bound closures plus a config getter over a reader closure, and returns globalBeforeHook.call(host, ctx, setShouldSkipSessionRefresh). The seat's bars: (Body) 584 lines / 1,326 tokens token-identical, every line the base line minus eight spaces, with a failing negative control; async moved from the arrow to the function and the arrow returns that promise unchanged, which better-call 1.4.4's createMiddleware awaits inside its try (dist/middleware.mjs:15); no arguments, new.target, super or bare this in BODY (28 this.X reads). (Captured local) one, setShouldSkipSessionRefresh (read once at :2273, bound once at :1697 and never reassigned), is now the function's second parameter under the same name and the arrow passes the same binding on every call; no other createAuthInstance local is read. A binding proof resolves all 356 identifiers the same way at both ends (69 parameter, 119 body-local, 29 class members incl. the one static, 12 imports, 2 to oidcAuthorizeGate on AuthManagerOptions, 125 globals or untyped property names), with two failing negative controls. (Receiver) BeforeHookHost names exactly the 19 members BODY reads, each typed AuthManager[x]: 18 methods as late-bound closures, and config, which the class reassigns (setRuntimeBaseUrl :4549, applyConfigPatch :4630), as a readonly getter. Function-valued member disclosed: BODY reads this.config.oidcAuthorizeGate twice (:2536 presence check, :2546 call), on the object the getter returns at that moment, with the same this. (Static) AuthManager.OPERATOR_PROVISIONING_TICKET_FIELD (public, :4844) read once at call time, via a value import of the class, as cut 8's membership-admission module does; hasExistingUserFor and isBootstrapCreation (private-static readers) stay in the class per ruling A. (Host lifetime) built inside the arrow, once per run of the hook; nothing captured early. (Readers) check-adr-anchors decision letters 1,955 on the merge base 93a6ba4 and 1,945 on the head: the ten letters in BODY's comments (ADR-0095 D3, ADR-0069 D1 twice, D3 three times, D2, ADR-0135 D6, ADR-0068 D4 twice) leave the anchored file; 84 anchored files and 52,274 citations at both. No entry is owed: the auth-manager.ts entry's two paragraphs (ADR-0108, ADR-0093 D2) do not describe hooks.before. Two path readers move in the same commit: api-error-code.pin.test.ts authManagerFiles gains the module (5 of the 30 sites move; without it the floor would pass silently at 25), and check-dispatcher-error-vocabulary.mjs liveMemberFile names it (the APIError.from member, base :2752), with its three prose mentions; no count, pattern or verdict changes. Tenant census and symbol-anchor output byte-identical at base and head; full plugin-auth suite passes at both. Nine names leave auth-manager.ts with BODY (setAuthActorResolver, SIGN_IN_EMAIL_PATH, VENDOR_ROLE_SCALAR_ADMIN_PATHS, carriesPrincipalSession, ORGANIZATION_DELETE_PATH, and the last-local-credential block). Evaluation order measured, not argued: a loader hook logging each ES module as its body runs while dist/index.mjs loads finds 171 modules in the same order at both except @better-auth/core/dist/utils/ip.mjs, now one place later (after platform-objects' identity entry), whose top level is two zod schemas, constants and functions; the CJS entry's 127 first loads differ only in that module. Inside the bundle auth-session-audit.ts and last-local-credential.ts now run later; their top levels are constants and function declarations, and all 24 references to them in the 5 importing files are inside functions. index.ts names neither new symbol, so nothing is published. auth-manager.ts 8,362 to 7,800 lines. No sign-in, session or membership outcome changes.",
    "tests": "Base 93a6ba4, head 3ec293d. Builds and suites ran through os-verify-lock, every exit code captured before any pipe. A full turbo run build ran first; then plugin-auth was built and tested at base, and built and tested at head, on the same dependency dists. (1) Move proof (TS scanner tokens, kind and text, template literals re-scanned at their substitutions): 1 of 1, 584 lines and 1,326 tokens, each line the base line minus eight spaces; the base arrow's parameter list is the head function's first parameter, then the one captured local under its own name. The head arrow is non-async with the same parameter, then the reader closure for config, one host literal whose keys are exactly BeforeHookHost's (18 late-bound closures and the getter over the reader), then return globalBeforeHook.call(host, ctx, setShouldSkipSessionRefresh). Negative control: the module text changed in memory (setShouldSkipSessionRefresh(true) to (false)) fails the token and line checks and the run exits 1; the file is untouched. Binding proof (TypeScript checker over the base file and the module, the module's ./auth-manager.js resolved to the base file): 356 of 356 identifiers resolve the same way in order; none to any other createAuthInstance local or an unexported top-level declaration. Negative controls: the last-local-credential import pointed at another specifier gives 3 differences and exit 1; run without the captured local declared, it reports that local and exits 1. git diff --color-moved=plain --color-moved-ws=allow-indentation-change 93a6ba4 3ec293d over the two source files marks 588 of 593 deleted and 596 of 727 added lines moved; the 5 unmoved deletions are four import-name lines and the async arrow line (now without async). (2) Ruling 6348891160 item 2 block, identical at base and head. dist/index.d.ts: raw 1b3a48357d137155de33104277667a922aab62df73b418b03894c56faaacd0ba, normalised cee7303ccdfe72794efd588c85b628419d4253d730b5a0c5a3f1cdb8dddb4443. dist/index.d.mts: raw b5e8dc982b634f0a5108b1b533626a68767f2da5a56f72dba801059b149e2872, normalised 562d0a4ce656f97aeb937541482cdf531b61d7c75a005918784bf0a52d032dfc. Raw diff 0 lines, so there is no union swap. Normaliser: the ruling's one-liner, verbatim in the PR body. Positive control: head dist/index.d.ts with 'private createAuthInstance;' made public (line 1712) hashes raw 35b18682171cd394898c9e56bc8d5272ccf20aeff0d1966fc568cb5e566a08df and normalised e69c1b0157b45e48bbc16bd64eb939abf9be33ccb31553485df93ef05aac6fc3; both differ. Export lists: ESM and CJS have 159 names each at both points, sorted-name hash 3abb126f5babb701 on all four; globalBeforeHook and BeforeHookHost are not exported and dist/index.d.ts names neither. The same 18 dist files at both, plus the two .build-input-hash stamps; only index.js, index.mjs and their maps differ. Load order: the ES-module evaluation log and the CJS first-load log as in the summary, each at base and head from the built dist copied into the package and removed after. (3) pnpm exec vitest run --maxWorkers=3 in packages/plugins/plugin-auth: base Test Files 191 passed (191), Tests 3607 passed | 10 skipped (3617), exit 0; head the same counts, exit 0. The pin test's authManagerFiles sum is 30 at both. pnpm --filter @objectstack/plugin-auth typecheck exit 0; pnpm --filter @objectstack/runtime typecheck exit 0; eslint --no-inline-config over the 4 files exit 0. No tested behaviour changed, so no ablation is owed. (4) check-tenant-audit-census output byte-identical at base and head (exit 0, no --write); tenant-audit-census --json: 238 sites at both, 0 per-site differences line-blind, 2 in auth-manager.ts at both (each 562 lines earlier at head), 0 in the new module, 0 unresolved and 0 unledgered; only scannedSources (747 to 748) and measuredAt differ. measure-durability-swallow-family --json: 77 members (same with every line number ignored) and 84 quiet rows (identical) at both; only files moves (3,474 to 3,475); tryStatements 3,640 and droppedNotAwaited 235 at both. (5) Anchors at base and head, run by hand, all exit 0, five with byte-identical output: check-adr-symbol-anchors (2,384 anchors, 466 symbol), check-scripts-symbol-anchors (4,070), check-spec-docblock-symbol-anchors (5,076), check-system-context-census (149 elevation read sites), check-platform-checklist (16 areas, 329 items); check-adr-anchors differs only in files (6,681 to 6,682) and letters (1,955 to 1,945). check-dispatcher-error-vocabulary: 56 found and 56 classified at both, scanned files 3,424 to 3,425, the printed live-member path now the new module; --self-test 11 shapes and 396 assertions OK. Anchor map: all 29 auth-manager.ts#symbol anchors outside changelogs are declaration-class at base, with BODY blanked, and at head; two of their symbols (AuthManager, isBootstrapCreation) are tokens on a BODY line and resolve to their declarations, which stay, at all three. (6) Merge check on origin/main 4df174b (4 merges past the base: #25539, #25538, #25528, #25543) with 3ec293d cherry-picked, no conflict. Move proof and binding proof against 4df174b: 1 of 1 (584 lines, 1,326 tokens) and 356 of 356. check-tenant-audit-census, check-adr-anchors, check-adr-symbol-anchors, check-platform-checklist, check-system-context-census, check-auth-mount-ledger, check-dispatcher-error-vocabulary and check-scripts-symbol-anchors all exit 0. check-adr-anchors on 4df174b alone: 84 anchored files, 52,277 citations across 6,683 files, 1,956 letters; with this cut: 84, 52,277 across 6,684, 1,946. Durability: 77 members and 84 quiet rows at both (line-blind equal), files 3,476 to 3,477, tryStatements 3,645 and droppedNotAwaited 235 at both. The full plugin-auth suite on the merged tree: Test Files 191 passed (191), Tests 3607 passed | 10 skipped (3617), exit 0. Since then origin/main has moved two more merges, to 673ebfd (#25533 metadata-protocol and runtime, #25542 service-automation); neither touches plugin-auth, scripts/adr-anchors or a reader named here.",
    "gates": "Local subset at head after the full build: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 80 commands for the 4 paths. All 80 ran, each exit code captured before any pipe, and all 80 exit 0. --ran over that record: 80 derived, 80 run, 0 NOT-MEASURED, a derived zero. dispatch-gates --tier: no path-derived mandate. node scripts/pm/check-governed-merges.mjs: --test on the final file list before the claim and --branch HEAD after the commit both NOT governed (0 of 4 paths), 1,331 changed lines against the 3,000-line human-merge line. CI on 3ec293d, the PR's check-runs read at report time (46): 34 success, 12 skipped, 0 failure, 0 pending; mergeable_state clean against base 4df174b. Skipped: Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Console Pin Gate, Build Docs, the Dogfood Regression Gate shard matrix, the opt-in 18.x upgrade harness and Packed-tarball smoke (twice each), and in the labeled run Auto Label, Check PR Size and Check Changeset. Success: Build Core; Test Core (1/6) to (6/6) and Test Core; TypeScript Type Check (Type Check · workspace, · source gates, · consumer gates, · debt ledger); Lint and Repo Gates (Lint · ESLint, · repo and tooling gates, · docs and code guards, · CI, release and engine gates, · PM dispatch-gates, · scoped · tooling self-tests, · scoped · query-options erasure, · scoped · slot lookup and corpus); Dogfood Regression Gate; Governed Surface Queue Guard; Check PR Size; Check Changeset; Auto Label; filter; Part-of PR must not also close its card; The card this PR closes must claim this branch; No other open PR may claim the same issue; No other open PR may claim the same single-writer path; Check Documentation Links; Flag docs affected by code changes. No Claude Approvals check runs on this PR. No review threads and no reviews. The PR was opened as a draft. Docs Drift Check (advisory), comment 6376211728: 2 pages name something the change touched, computed on the merge with base 4df174b, both name matches only: content/docs/kernel/contracts/auth-service.mdx (isAuthGateActive, the auth service contract's member, which the class implements unchanged; BeforeHookHost only names it) and content/docs/permissions/system-context.mdx (its :172 row cites plugin-approvals' approval-service.ts#resolveActor, another symbol). It matches node scripts/docs-audit/affected-docs.mjs --json 93a6ba4 (exit 0, the same 2 pages, 0 release-owned), so no doc edit is owed and no reply was posted. Serial, re-read at report time: 10 open PRs besides this one; new since the claim #25550 (lint, metadata-protocol, cli tests); #25533 and #25542 merged. #25541 is the only one touching auth-manager.ts, now one hunk in externalDirectoryDoorDeps, outside this cut's lines. None touches the new module, scripts/adr-anchors or a reader named in the summary; origin/main has moved to 673ebfd (#25533, #25542), neither touching plugin-auth.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 GitHub MCP calls since cut 12's report (6374754398). GitHub REST reads through curl, reads only: #25224's comments (to the landed note 6375260860), the open PR list and each open PR's file list for the serial reads (4 times), #25541 and its files (3 times), and this PR's check runs, reviews, comments and state. (claude-code-remote subscribe_pr_activity once on #25548, not a GitHub write.)",
    "api_writes": "3 relay acts since cut 12's report (scripts/pm/, each read back identical): 1) amended claim 6376168919 via post-stamped, run 40035136618, 20437 bytes; 2) pr_create #25548 via fleet-write, run 40035358207, 25965 bytes sent and stored, assignee os-sales, label skip-changeset; 3) this report via post-stamped. Plus git push of the branch (a new branch after the queue deleted the old one on #25536's merge; not REST). No PR body edit. No other write.",
    "open_questions": [],
    "out_of_scope_findings": [
    "#25541 (another account's open PR, now on base 4df174b) already carries its directory-door comment edit in auth-manager-after-hook.ts, where cut 12 moved it, and its one auth-manager.ts hunk is in externalDirectoryDoorDeps (:7326 on that base), outside this cut's lines. No action from this card; noted for the seat's serial reads."
    ]
    }


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed cut 13 (hooks.before) · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-23T23:45Z

    reading value
    PR #25548 MERGED through the queue (ACCEPT 6376660491), head 3ec293d86a
    origin/main carries 19d3720af1

    State: the card stays open and dispatched (Part of). Cut 14, the rest of createAuthInstance (readDisableSignUpEnv exported in place), is claimed afresh on the merged auth-manager.ts.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · series card, cut 14 (one PR, cuts strictly serial: after cut 13, PR #25548, landed as 19d3720af1, per the landed note 6377004954) · the sixth cut under ruling 6364006498 (A), and the last of the three createAuthInstance slices the seat set (hooks.after, then hooks.before, then the rest) · amends claim 6376168919 for this PR, on the merged file · 2026-10-24T00:49Z
    Session: session_01LSXBc2GCBwiAL7vV4kWkyr
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lg3wrj (the harness-pinned branch for this session, restarted from origin/main after #25548 merged; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25224 (a linked worktree in this session's container)
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main 45f996231e, which is cut 13's 19d3720af1 plus #25541; there auth-manager.ts is 7,807 lines). One commit, b2bbcd6cac: +926/−749 over 3 files (1,675 changed lines). One of the three is a reader outside the move, so check-governed-merges.mjs ran on the trial file list before the cut and on the final list after it: NOT governed (0 of 3 paths hit the register), so this is not Tier H.

    (a) The move: the rest of createAuthInstance, which is its whole body. createAuthInstance (:1689, private async, annotated Promise<Auth<any>>) is the method getOrCreateAuth() calls once per build (:1669, this.createAuthInstance().then(…)). Its body (:1690–:2415) moves unedited into a new module, packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts (861 lines), as the body of export async function createAuthInstance(this: AuthInstanceHost): Promise<Auth<any>>, re-indented two spaces (one level).

    • The body, in order: the deferred better-auth and better-auth/api imports, the plugin list and the password hasher, then one config literal (secret, origin and base path, app name, database, the user, account and verification model configs, the social providers, emailAndPassword with its reset-password and sign-up callbacks, the e-mail verification block, session, rate limiting and secondary storage, the plugins, the composed database hooks, the global hooks.before and hooks.after arrows that cuts 12 and 13 left in place, onAPIError, the trusted origins and advanced), the betterAuth() call, and the wait on auth.$context before the instance is returned. All of it moves, in the same order, with its comments.

    • The method keeps its place, its docblock and its private modifier, and its body becomes:

      private createAuthInstance(): Promise<Auth<any>> {
        // [#25224 cut 14] The body moved to `auth-manager-auth-instance.ts` (see `AuthInstanceHost`).
        const readConfig = () => this.config;
        const readIdpLocale = () => this.idpLocale;
        const host: AuthInstanceHost = { /* 45 late-bound closures and 2 getters, alphabetical */ };
        return createAuthInstance.call(host);
      }

    The seat's bars, each measured (cut 13's, plus what a whole method adds):

    • Body. The body is token-identical inside the module function: 728 lines with its braces, 2,616 tokens (TypeScript scanner, template literals re-scanned at their substitutions), and every line is the base line minus two spaces. Negative control: one token changed in the module text the proof reads (betterAuth( to betterAuth?.(, in memory; the file is untouched) fails the token and line checks, and the run exits 1.
      • async moves from the method to the function. The stub is not async; it returns createAuthInstance.call(host), the function's own promise, unchanged, and getOrCreateAuth() chains .then(…) on that promise exactly as before. The two reader closures, the host literal and the .call cannot throw before the promise exists, and a throw inside the body is still a rejection of it.
      • The body uses no arguments, new.target or super (0 of each), and no bare this: its only this uses are member reads this.X, every method read is a direct call, and it writes no member. No this sits inside a non-arrow function, so every callback the body hands better-auth reads the same receiver the body does.
    • Binding identity. A binding proof walks all 672 identifiers of the body at base and in the module, in order, with the TypeScript checker (the module's ./auth-manager.js resolved to the head file, where the in-place export below lives). All 672 resolve the same way at both ends: 402 to declarations inside the body (the same declaring identifier, by position), 108 to receiver members, 19 to imports with the same specifier and imported name (readDisableSignUpEnv among them, below), 14 to the same declaration in auth-manager.ts (seven AuthManagerOptions members read through this.config: databaseHooks, emailService, logger and its info, rateLimit, rateLimitStorage and secondaryStorage), and 129 to no declaration (globals and property names on untyped values). None resolves to an unexported top-level declaration of auth-manager.ts or to a top-level declaration of the module. Negative controls: pointing the module's placeholder-email import at another specifier gives 1 difference and exit 1; shadowing that import with a module-level const gives 1 difference, reports the module top-level read, and exits 1; running without being told about the in-place export reports the base read of an unexported helper, and exits 1.
    • Receiver. The body's this was the AuthManager instance. It is now the host. AuthInstanceHost names exactly the 47 members the body reads, each typed as the class member (AuthManager['x']). 45 are methods, carried as (...a) => this.x(...a), so a spy or double set on the manager is still the one the body reaches, including from the callbacks better-auth runs long after the function returns. Two are fields, and both go as getters over a reader closure, readonly on the interface; the body writes neither:
      • config, which the class reassigns after construction (setRuntimeBaseUrl, :3975, writes it at :3987; applyConfigPatch, :3998, at :4068), the cut 1 rule;
      • idpLocale (:4167, private readonly, an IdpLocaleSync), which is not reassigned, as every field this series carries goes (the same pair composeDatabaseHooks' stub hands its body as getters, :7030).
      • Function-valued reads, disclosed (the refactor(plugin-security): move the everyone anchor, the explanation, the sharing verdicts and the authored-row write verdict out of security-plugin.ts (#25223 cut 10) #25516 way): the body calls this.idpLocale.stage(data, ctx) once (:1839), and this.config.logger?.info?.(m, meta) once (:1843, inside the logInfo arrow it hands on). Each receiver is the object the getter returns, the manager's current one, so each is looked up and called on the same object, at the same moment, with the same this, as before.
    • The two hook arrows. hooks.before and hooks.after move with the literal, unedited. Each builds its own host from this, which is now this host: each of their closures calls this host's closure, which calls the manager's method, and their config getters read this host's getter, which reads the manager's field. So they reach the same member, at the same moment, as before. Their imports (globalBeforeHook, globalAfterHook and the two host types) move to the new module with them.
    • One top-level helper, exported in place. The body reads one top-level declaration of auth-manager.ts: readDisableSignUpEnv (:384, once). Per ruling 6364006498 (A), it gets export in place, in this cut, the first whose body reads it, and the module imports it by value from ./auth-manager.js. The proof checks that the head declaration is the base one with export in front and nothing else changed, and that the base and head reads resolve to it. src/index.ts names its auth-manager.ts exports one by one and does not name it, so nothing is published: the ESM and CJS entries export the same 159 names at base and at head, and readDisableSignUpEnv is not among them, nor in dist/index.d.ts.
    • No static read. The body reads no AuthManager.X, so the module imports the class as a type only. The private-static readers it calls (hasExistingUserFor, isBootstrapCreation) stay in the class, per ruling A, and are reached as members.
    • Host lifetime. The host is built once per call of createAuthInstance, which getOrCreateAuth() makes once per build, as every method stub in this series does. Nothing the body reads is captured early: the closures look each method up on the manager at call time, and the getters read the fields when the body, or a callback it handed better-auth, reads them.
    • Readers. The decision letters on the anchored surface, read by check-adr-anchors on the merge base and on the head: 1,946 on 45f996231e, 1,938 at b2bbcd6cac, so 8 drop (84 anchored files and 52,280 citations at both; files 6,688 → 6,689). The drop is the letters in the body's comments: ADR-0131 D7 (:2000, :2114), ADR-0069 D4 (:2142), ADR-0069 D2 (:2190, :2214), ADR-0093 D2 (:2222) and ADR-0135 D6 (:2347, :2351). They leave auth-manager.ts, which is anchored, for a module that is not.
      • One judgment, disclosed: no entry is owed for ADR-0093 D2. The letter at :2222 is in the comment on the databaseHooks: this.composeDatabaseHooks(this.config.databaseHooks) wiring, which says D2 makes the database-hook seam the single owner of the user-lifecycle invariants. The auth-manager.ts entry's D2 paragraph describes settleMembership, which stays, and its two seams (user.create.after and session.create.before), which are database hooks that cut 4 moved to auth-manager-database-hooks.ts with their own entry. It does not describe the wiring line, and no other entry names it. The ADR-0108 paragraph (the organization roles map) is not in the body. So, per the seat's bar, no entry is added for the new module. If the seat reads the wiring as part of the D2 mechanism, the remedy is an entry for the new module carrying the D2 paragraph verbatim, cut 4's precedent, in this PR.
      • Tenant census and symbol-anchor output: byte-identical at base and head (check-tenant-audit-census, check-adr-symbol-anchors, check-scripts-symbol-anchors, check-spec-docblock-symbol-anchors, check-system-context-census and check-platform-checklist; cmp clean). The full plugin-auth suite passes at both ends: 191 files, 3,620 passed and 10 skipped, at both.

    (b) Imports. Ten imported values leave auth-manager.ts, because the body was their last reader there: PLATFORM_IDP_PROVIDER_ID and refuseImplicitAccountLink (the implicit-account-linking.js block keeps type LinkingAuthContext), readEnvWithDeprecation (the @objectstack/types block keeps its other names), the whole account-invitation-marker.js line (readAccountInvitation, settleImportResetDelivery), the whole client-address-vendor.js line (vendorClientAddressOptions), and AUTH_USER_CONFIG, AUTH_SESSION_CONFIG, AUTH_ACCOUNT_CONFIG and AUTH_VERIFICATION_CONFIG (the auth-schema-config.js block keeps its other names). The cut 12 and cut 13 import lines (globalAfterHook, globalBeforeHook and the two host types) and their comments are replaced by one comment and one line: import { createAuthInstance, type AuthInstanceHost } from './auth-manager-auth-instance.js'. audiencePermitsSelfRegistration, resolveEmailVerificationRequirement, isPlaceholderEmail and the better-auth types have other readers, so both files import them.

    Evaluation order. esbuild drops a type-only import, so a module whose last value import leaves auth-manager.ts can now be evaluated later. I measured the order instead of arguing it:

    • The ES modules, at run time. A loader hook logged each ES module as its body ran while dist/index.mjs loaded, at base and at head. 171 modules evaluate at both, and the first 117 are the same set at both. One block moves: the 17 modules @better-auth/core/context loads (16 files of @better-auth/core's context, async_hooks, error, db, env and utils directories, and better-call's error) ran first at base and now run after the 100 others in that set (zod's v4 modules, @objectstack/spec's security, api and data entries, pg-connection-string and @objectstack/types). They follow the bundle's first @better-auth/core/context import, which is account-invitation-marker.ts's at both, and that module's segment now sits later in the bundle (line 196 at base, 477 at head). From position 118 on, the order is identical. Their top levels are constants, classes and function declarations, plus defineErrorCodes, createLogger() and the env proxy; the only load-time reads are of process.env (NODE_ENV, TEST, the default logger's colour depth), and the better-auth:global slot is touched only when a function is called. None of the 100 writes process.env or that slot. The bundle's own body runs last, after all 170, at both.
    • The CommonJS entry. A Module._load log for dist/index.js gives 127 first loads at both, the same apart from @better-auth/core/dist/context/index.mjs, which now loads last instead of eighth from last.
    • plugin-auth's own modules. A walk of the bundle's top level and its eager init_* calls finds three that run later: implicit-account-linking.ts (one place), client-address-vendor.ts and account-invitation-marker.ts, whose value imports left auth-manager.ts with the body. Their top levels are string constants, one arrow constant (authContextOf), and two defineRequestState(() => null) calls, each of which makes its own frozen key object and touches no shared state until it is read inside a request. No importer reads any of their bindings at load time: all 14 references in the 6 importing files are inside functions.
    • The new module's own top level is imports, one interface and one function declaration. Its value import of auth-manager.ts, for readDisableSignUpEnv, and auth-manager.ts's import of it form a cycle with nothing read at load time.

    (c) Path readers. One reader outside the move follows it, in the same commit:

    • packages/plugins/plugin-auth/src/auth-email-locale.test.ts: the two population comments (the docblock at :861 and the in-test note at :1126, head numbering) that name the files the five sendTemplate sites live in now name auth-manager-auth-instance.ts (the body of createAuthInstance) among them. No assertion, count or pattern changes.
    • Read, and not edited:
      • api-error-code.pin.test.ts: the body carries no new APIError( site, so authManagerFiles is not owed the new module. Its sum stays 30, as at base.
      • scripts/check-dispatcher-error-vocabulary.mjs: the body has no new APIError( or APIError.from( site and no live member, so nothing in the gate moves. It exits 0 at both, 56 sites found and 56 classified at both, and only its scanned-file count moves (3,430 → 3,431).
      • The census: no census site moves: the body has no engine write. check-tenant-audit-census exits 0 at both with no --write, with byte-identical output. Its --json document has 238 sites at both and 0 per-site differences with line numbers ignored. 2 sites are in auth-manager.ts at both (each 685 lines earlier at head, the cut's net), 0 are in the new module, and 0 are unresolved or unledgered. Only scannedSources (751 → 752) and measuredAt move, so neither census document is edited.
      • scripts/measure-durability-swallow-family.mjs (a measurement, not a gate): 77 members and 84 quiet rows at both, identical with line numbers ignored (one member's write line, in observeFirstMemberships, which stays, moves 5,177 → 4,492). files moves 3,480 → 3,481, and tryStatements (3,645) is the same. One statistic moves, and I traced it: droppedNotAwaited goes 233 → 234 (delete 149 → 150). It counts write-named calls that are not awaited inside a try block and the same-file helpers that block calls, walked by name, each name once, at most four levels deep. The try in linkingContext (base :4355) walks getOrCreateAuth, then createAuthInstance. At base the walk entered the body and reached isOperatorProvisioning at depth 4, so prunePendingOperatorProvisioning sat at depth 5, past the limit. At head the walk enters the stub, whose host literal lists the members alphabetically, and reaches isOperatorProvisioning at depth 3. So prunePendingOperatorProvisioning (:4923, which stays) is now walked, and its this.pendingOperatorProvisioning.delete(key) is counted. That call is a synchronous Map delete, not a write, and the code is unchanged. A logged copy of the instrument, run on the base and head trees and then removed, shows this is the only dropped call that differs (233 at base, 234 at head). Membership and the quiet rows, which are what the measurement reports, do not move.
      • check-auth-mount-ledger.mjs reads only the basePath declaration in auth-manager.ts, which stays.
      • auth-manager-base-path.test.ts reads auth-manager.ts for code lines carrying '/api/v1/auth'; the body has one only in a comment (:1814), so its expectation (the one declaration) holds.
      • The plugin-factory scans in better-auth-schema-parity.test.ts and managed-extension-fields.test.ts match const { X } = await import('better-auth/plugins…' | '@better-auth/…'). The body's two dynamic imports are better-auth and better-auth/api, neither a factory. At head the scanned shape matches 16 times in auth-manager.ts, as at base, and 0 times in the module.
      • rate-limit-storage-isolation.test.ts walks the import graph from rate-limit-storage.ts, which reaches neither file.
      • Prose elsewhere that names auth-manager.ts for code now in the module (for example the NODE_ENV-gated trusted-origins substitution named in packages/cli/test/serve-node-env-production-default.e2e.test.ts) reads no file and is not edited, as cuts 12 and 13 left the prose that names the before-hook.
    • tsup bundles the new module into the root entry (splitting: false). The dist has the same 18 files at base and at head, plus the two .build-input-hash stamps the build writes from its inputs. Only index.js, index.mjs and their maps differ.

    ⛔ Nothing else changes:

    • No method, field or top-level declaration moves besides the body. The body is not edited.
    • No stay-behind code is edited apart from the method's body (now the stub), the import lines in (b), and the export on readDisableSignUpEnv.
    • The only edit outside plugin-auth/src's two moved files is the reader in (c). No ADR, docs prose, anchor entry, baseline or eslint.config.mjs is edited.
    • The title goes under refactor(plugin-auth), with no !.
    • No changeset: skip-changeset, per ruling 6348891160 item 4.

    Anchor reading before the cut, on 45f996231e: I mapped every auth-manager.ts#symbol anchor in the tracked tree, outside changelogs, with symbolSegmentResolution from scripts/symbol-anchors.mjs. There are 29, and all 29 are declaration-class at base, at base with the body blanked, and at head. Five of their symbols occur as a token on a line of the body: secondaryStorage (ADR-0127 :114), composeDatabaseHooks (the access-security checklist area :2419), isBootstrapCreation (identity-auth :1810), readDisableSignUpEnv (identity-auth :1901) and requireEmailVerification (identity-auth :2004). Each resolves to its declaration, which stays, at all three readings; readDisableSignUpEnv's declaration (:384) gains only export.

    Container & model: L (series), card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier: "no path-derived mandate"), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Basis for the Clause-② line: a move-only cut plus one comment migration widens no accept set and no public surface. The published surface is measured, not argued. The proof owed:

    • token identity and binding identity for the body against this base, each with a negative control;
    • ruling 6348891160 item 2's block: raw and normalised sha256 of dist/index.d.ts and dist/index.d.mts at base and at head, the normaliser verbatim, the raw diff line count, and the positive control;
    • ESM and CJS export lists at both, and that readDisableSignUpEnv is in neither, nor in dist/index.d.ts;
    • plugin-auth suite counts at base and at head;
    • the census and the durability measurement at base and head;
    • the symbol-anchor checks and check:adr-anchors (with the decision-letter count) on the merge base and on the head;
    • the load order at base and at head;
    • every derived gate.

    Premise (measured on this base and at b2bbcd6cac):

    • 1 of 1 body token-identical (728 lines, 2,616 tokens) and 672 of 672 identifiers bound the same way, each proof with failing negative controls, as in (a).
    • pnpm --filter @objectstack/plugin-auth typecheck exits 0, and so does pnpm --filter @objectstack/runtime typecheck. eslint on the 3 files exits 0.
    • Suite: 191 files, 3,620 passed and 10 skipped at base and at head.
    • dist/index.d.ts and dist/index.d.mts are raw byte-identical at base and at head (0 diff lines). The ESM and CJS entries export the same 159 names at both; the list includes none of readDisableSignUpEnv, createAuthInstance and AuthInstanceHost, and dist/index.d.ts names neither readDisableSignUpEnv nor AuthInstanceHost.
    • The census and the durability measurement read as stated in (c), the one moved statistic traced. The load order reads as stated under (b).
    • 60 derived gates run at head after a full build, and all exit 0 (--ran: a derived zero). check-governed-merges.mjs --branch HEAD: NOT governed, 1,675 changed lines.
    • affected-docs.mjs --json 45f996231e (exit 0): 8 pages, all name matches of names the moved code carries (the OS_CORS_ORIGIN and CORS_ORIGIN literals, isAuthGateActive and resolveActor as host members, and sys_oauth_resource in a comment). None names auth-manager.ts, and the code that reads those names is unchanged, so no doc edit is owed. 3 of them are release notes (17.3, 18.0, 18.1), owned by the release, and not edited.
    • The anchor map and the decision-letter readings above.

    A diff of another kind is a fresh fork on this card.

    Responsibility: n/a — not a defect card
    Thread-read: 6377004954
    Ruling-ref: 6348891160 (the .d.ts bar), 6364006498 (ruling A on this card)
    Serial constraints: re-read after the proof run, at 00:48Z. 8 PRs are open, and none touches auth-manager.ts, the new module's path, auth-email-locale.test.ts or a reader named in (c): #25562 (release notes), #25560 (metadata-protocol, runtime), #25559 (the CI workflows, scripts/), #25557 (ADR records and core), #25552 (.claude/, scripts/pm/), #25545 (plugin-security, docs, scripts/check-system-context-census.mjs) and #25540 (rest). The Version Packages PR #24876 touches plugin-auth's package.json, its CHANGELOG.md and changesets only.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25224,
    "status": "done",
    "branch": "claude/project-thread-lg3wrj",
    "pr": "#25566",
    "session": "session_01LSXBc2GCBwiAL7vV4kWkyr",
    "premise_still_valid": true,
    "summary": "Cut 14 of the auth-manager.ts split, per the amended claim 6377759458 (amends 6376168919 on the file cut 13 left; landed note 6377004954), under Ruling-ref 6348891160 (the .d.ts bar) and 6364006498 (Sales's ruling A on this card). This is the sixth cut under ruling A and the last of the three createAuthInstance slices the seat set (hooks.after, then hooks.before, then the rest), on the #23050 method shape cuts 1 to 11 used. Part of #25224, no closing keyword; #25224 stays open. Base origin/main 45f9962 (cut 13's 19d3720 plus #25541, rebased onto before the proof run), one commit b2bbcd6, +926/-749 over 3 files (1,675 changed lines). Moved: the whole body of createAuthInstance (:1689, private async, annotated Promise of Auth; body :1690-:2415), unedited, into the new module packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts (861 lines) as export async function createAuthInstance(this: AuthInstanceHost), re-indented two spaces. The body, in the same order: the deferred better-auth and better-auth/api imports, the plugin list and the password hasher, the one config literal (secret, origin and base path, app name, database, user/account/verification model configs, social providers, emailAndPassword with its callbacks, e-mail verification, session, rate limiting and secondary storage, plugins, the composed database hooks, the hooks.before and hooks.after arrows cuts 12 and 13 left in place, onAPIError, trusted origins, advanced), the betterAuth() call and the wait on auth.$context. The method keeps its place, docblock and private modifier; its body is now two reader closures, one host literal (45 late-bound method closures and getters for config and idpLocale, alphabetical) and return createAuthInstance.call(host). The seat's bars: (Body) 728 lines / 2,616 tokens token-identical, every line the base line minus two spaces, with a failing negative control; async moved from the method to the function and the stub returns that promise unchanged, which getOrCreateAuth (:1669) chains .then on as before; no arguments, new.target, super or bare this; every method read a direct call; no member write; no this inside a non-arrow function, so every callback handed to better-auth reads the same receiver. (Binding) a checker proof resolves all 672 identifiers the same way at both ends (402 body-local, 108 receiver members, 19 imports, 14 to seven AuthManagerOptions members read through this.config, 129 globals or untyped property names), with three failing negative controls. (Receiver) AuthInstanceHost names exactly the 47 members the body reads, each typed AuthManager[x]: 45 methods as late-bound closures; config (reassigned by setRuntimeBaseUrl :3987 and applyConfigPatch :4068) and idpLocale (:4167, private readonly) as readonly getters. Function-valued reads disclosed: this.idpLocale.stage(data, ctx) at :1839 and this.config.logger?.info?.(m, meta) at :1843, each on the object the getter returns at that moment, with the same this. (Hook arrows) hooks.before and hooks.after move unedited and build their hosts from this host, whose closures and getters reach the manager at call time; their imports move with them. (In-place export) readDisableSignUpEnv (:384), the one top-level helper the body reads, gets export in place per ruling A and is imported by value; the proof checks the head declaration is the base one plus export and that both reads resolve to it; index.ts does not name it, so nothing is published. No static read (the class is a type-only import); hasExistingUserFor and isBootstrapCreation stay in the class per ruling A. (Host lifetime) built once per createAuthInstance call, nothing captured early. (Readers) check-adr-anchors decision letters 1,946 on the merge base 45f9962 and 1,938 on the head: the eight letters in the body's comments (ADR-0131 D7 twice, ADR-0069 D4, ADR-0069 D2 twice, ADR-0093 D2, ADR-0135 D6 twice) leave the anchored file; 84 anchored files and 52,280 citations at both. One judgment disclosed for the seat: no entry is owed for the ADR-0093 D2 letter at :2222, which sits on the databaseHooks wiring comment; the auth-manager.ts entry's D2 paragraph describes settleMembership (stays) and its two seams (moved by cut 4, with their own entry), not the wiring. If the seat reads the wiring as part of the D2 mechanism, the remedy is a new-module entry carrying that paragraph verbatim, in this PR. One path reader moves in the same commit: auth-email-locale.test.ts's two population comments name the new module among the files the five sendTemplate sites live in; no assertion changes. Read and not edited: the api-error-code pin (no APIError site in the body; sum 30 at both), the vocabulary gate (no site, 56/56 at both), the census (238 sites line-blind equal), the plugin-factory scans (16 matches in auth-manager.ts at both, 0 in the module). Durability: members and quiet rows identical; one statistic moves, droppedNotAwaited 233 to 234 (delete 149 to 150), traced to the instrument's name-deduped, four-deep helper walk: from linkingContext's try (:4355) the stub's alphabetical host literal now reaches isOperatorProvisioning one level higher, so prunePendingOperatorProvisioning (:4923, unchanged) is walked and its synchronous Map delete counted; a logged copy of the instrument, run on both trees and removed, shows it is the only dropped call that differs. Tenant census and symbol-anchor output byte-identical at base and head; full plugin-auth suite passes at both. Ten imported values leave auth-manager.ts with the body; the cut 12 and 13 hook imports are replaced by the new module's. Evaluation order measured, not argued: 171 ES modules evaluate at both and the first 117 are the same set; the 17 that @better-auth/core/context loads now run after the 100 others in that set (zod v4, three @objectstack/spec entries, pg-connection-string, @objectstack/types) instead of first, because account-invitation-marker.ts, whose import leads to them, sits later in the bundle. Their top levels are constants, classes and functions; their only load-time reads are of process.env, and none of the 100 writes process.env or the better-auth global slot. From position 118 on the order is identical, and the bundle body runs last at both. The CJS entry's 127 first loads differ only in @better-auth/core/dist/context/index.mjs, now last. Inside the bundle implicit-account-linking.ts, client-address-vendor.ts and account-invitation-marker.ts run later; their top levels are constants and two defineRequestState calls (each its own frozen key), and all 14 references to them in the 6 importing files are inside functions. auth-manager.ts 7,807 to 7,122 lines. No sign-in, session or membership outcome changes.",
    "tests": "Base 45f9962, head b2bbcd6. Builds and suites ran through os-verify-lock, every exit code captured before any pipe. A full turbo run build ran first; then plugin-auth was built and tested at base, and built and tested at head, on the same dependency dists. (1) Move proof (TS scanner tokens, kind and text, template literals re-scanned at their substitutions): 1 of 1, 728 lines and 2,616 tokens, each line the base line minus two spaces; no parameters on any of the three, the same return annotation, the stub keeps the docblock and private. The stub is non-async: the two readers, one host literal whose keys are exactly AuthInstanceHost's (alphabetical; 45 late-bound closures and the two getters over the readers), then return createAuthInstance.call(host); the interface types each member AuthManager[x] and exactly the two getter fields are readonly. Negative control: the module text changed in memory (betterAuth( to betterAuth?.( ) fails the token and line checks and the run exits 1; the file is untouched. Binding proof (TypeScript checker over the base file, the head file and the module, the module's ./auth-manager.js resolved to the head file): 672 of 672 identifiers resolve the same way in order; none to an unexported top-level declaration of auth-manager.ts or a module top-level; readDisableSignUpEnv checked as the base declaration plus export. Negative controls: the placeholder-email import pointed elsewhere gives 1 difference and exit 1; that import shadowed by a module const gives 1 difference and exit 1; run without the in-place export declared, it reports the base read of an unexported helper and exits 1. git diff --color-moved=plain --color-moved-ws=allow-indentation-change 45f9962 b2bbcd6 over the two source files marks 734 of 744 deleted and 768 of 920 added lines moved; the 10 unmoved deletions are two import-name lines, the six comment lines above the cut 12 and 13 imports, the readDisableSignUpEnv declaration line (now with export) and the method line (now without async). (2) Ruling 6348891160 item 2 block, identical at base and head. dist/index.d.ts: raw 1b3a48357d137155de33104277667a922aab62df73b418b03894c56faaacd0ba, normalised cee7303ccdfe72794efd588c85b628419d4253d730b5a0c5a3f1cdb8dddb4443. dist/index.d.mts: raw b5e8dc982b634f0a5108b1b533626a68767f2da5a56f72dba801059b149e2872, normalised 562d0a4ce656f97aeb937541482cdf531b61d7c75a005918784bf0a52d032dfc. Raw diff 0 lines, so there is no union swap. Normaliser: the ruling's one-liner, verbatim in the PR body. Positive control: head dist/index.d.ts with 'private createAuthInstance;' made public (line 1712) hashes raw 35b18682171cd394898c9e56bc8d5272ccf20aeff0d1966fc568cb5e566a08df and normalised e69c1b0157b45e48bbc16bd64eb939abf9be33ccb31553485df93ef05aac6fc3; both differ. Export lists: ESM and CJS have 159 names each at both points, sorted-name hash 3abb126f5babb701 on all four; readDisableSignUpEnv, createAuthInstance and AuthInstanceHost are not exported, and dist/index.d.ts names neither readDisableSignUpEnv nor AuthInstanceHost. The same 18 dist files at both, plus the two .build-input-hash stamps; only index.js, index.mjs and their maps differ. Load order: the ES-module evaluation log, the CJS first-load log and the bundle walk as in the summary, each at base and head from the built dist copied into the package and removed after. (3) pnpm exec vitest run --maxWorkers=3 in packages/plugins/plugin-auth: base Test Files 191 passed (191), Tests 3620 passed | 10 skipped (3630), exit 0; head the same counts, exit 0. pnpm --filter @objectstack/plugin-auth typecheck exit 0; pnpm --filter @objectstack/runtime typecheck exit 0; eslint over the 3 files exit 0. No tested behaviour changed, so no ablation is owed. (4) check-tenant-audit-census output byte-identical at base and head (exit 0, no --write); tenant-audit-census --json: 238 sites at both, 0 per-site differences line-blind, 2 in auth-manager.ts at both (each 685 lines earlier at head), 0 in the new module, 0 unresolved and 0 unledgered; only scannedSources (751 to 752) and measuredAt differ. measure-durability-swallow-family --json: 77 members (same with every line number ignored) and 84 quiet rows (identical) at both; files 3,480 to 3,481; tryStatements 3,645 at both; droppedNotAwaited 233 to 234, traced as in the summary. (5) Anchors at base and head, run by hand, all exit 0, five with byte-identical output: check-adr-symbol-anchors (2,384 anchors, 466 symbol), check-scripts-symbol-anchors (4,070), check-spec-docblock-symbol-anchors (5,076), check-system-context-census (149 elevation read sites), check-platform-checklist (16 areas, 329 items); check-adr-anchors differs only in files (6,688 to 6,689) and letters (1,946 to 1,938). check-dispatcher-error-vocabulary: 56 found and 56 classified at both, scanned files 3,430 to 3,431. Anchor map: all 29 auth-manager.ts#symbol anchors outside changelogs are declaration-class at base, with the body blanked, and at head; five of their symbols (secondaryStorage, composeDatabaseHooks, isBootstrapCreation, readDisableSignUpEnv, requireEmailVerification) are tokens on a body line and resolve to their declarations, which stay, at all three. (6) Merge check on origin/main 9c43e19 (7 merges past the base: #25550, #25551, #25553, #25429, #25523, #25436, #25562) with b2bbcd6 cherry-picked, no conflict. Move proof and binding proof against 9c43e19: 1 of 1 (728 lines, 2,616 tokens) and 672 of 672, readDisableSignUpEnv checked as the base declaration plus export. check-tenant-audit-census, check-adr-anchors, check-adr-symbol-anchors, check-platform-checklist, check-system-context-census, check-auth-mount-ledger, check-dispatcher-error-vocabulary and check-scripts-symbol-anchors all exit 0. check-adr-anchors on 9c43e19 alone: 84 anchored files, 52,285 citations across 6,691 files, 1,947 letters; with this cut: 84, 52,285 across 6,692, 1,939 (the same 8). Durability: 77 members and 84 quiet rows at both (line-blind equal), files 3,482 to 3,483, tryStatements 3,646 at both, droppedNotAwaited 233 to 234 (the same traced artefact). The full plugin-auth suite on the merged tree: Test Files 191 passed (191), Tests 3620 passed | 10 skipped (3630), exit 0. origin/main is still 9c43e19 at the time of this report.",
    "gates": "Local subset at head after the full build: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 60 commands for the 3 paths. All 60 ran, each exit code captured before any pipe, and all 60 exit 0. --ran over that record: 60 derived, 60 run, 0 NOT-MEASURED, a derived zero. dispatch-gates --tier: no path-derived mandate. node scripts/pm/check-governed-merges.mjs: on the trial file list before the cut and --branch HEAD after the commit both NOT governed (0 of 3 paths), 1,675 changed lines against the 3,000-line human-merge line. Docs drift (affected-docs --json 45f9962, exit 0): 8 pages, all name matches of literals and member names the moved code carries; none names auth-manager.ts and the code reading those names is unchanged, so no doc edit is owed; 3 are release notes, not edited. CI on head b2bbcd6 converged: 46 check runs, 34 success and 12 skipped, 0 failed, 0 pending; the PR reads mergeable, clean, against 041a120. No review and no review comment. The one issue comment is the docs-drift bot's, which lists the same 8 pages as the local affected-docs run above (5 hand-written, all name matches, and 3 release-owned, read-only); no doc edit is owed, as stated in the PR body.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 GitHub MCP calls since cut 13's report (6376554601). GitHub REST reads through curl, reads only: #25224's comments (to the landed note 6377004954), the open PR list and each open PR's file list for the serial reads (4 times), #25563's file list, and this PR's check runs, reviews, comments and state. (claude-code-remote subscribe_pr_activity once on #25566, not a GitHub write.)",
    "api_writes": "3 relay acts since cut 13's report (scripts/pm/, each read back identical): 1) amended claim 6377759458 via post-stamped, run 40046899322, 24023 bytes; 2) pr_create #25566 via fleet-write, run 40047151020, 28189 bytes sent and stored, assignee os-sales, label skip-changeset; 3) this report via post-stamped. Plus git push of the branch (a new branch after the queue deleted the old one on #25548's merge; not REST). No PR body edit. No other write.",
    "open_questions": [
    "The ADR-0093 D2 entry judgment above: no new-module entry, unless the seat reads the databaseHooks wiring as part of the D2 mechanism (then one entry, verbatim paragraph, in this PR)."
    ],
    "out_of_scope_findings": [
    "#25563 (opened after the claim) edits plugin-auth's auth-manager-database-secret.ts and auth-manager.test.ts, not a file of this cut, and reads no source by path; noted for the seat's serial reads."
    ]
    }


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed cut 14 (createAuthInstance body) · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-24T02:07Z

    reading value
    PR #25566 MERGED through the queue (ACCEPT 6378239258), head b2bbcd6cac
    origin/main carries 35d5060380

    State: the card stays open and dispatched (Part of). The next cut (the buildPluginList organization closure, then the rest) is claimed afresh on the merged auth-manager.ts.


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · series card, cut 15 (one PR, cuts strictly serial: after cut 14, PR #25566, landed as 35d5060380, per the landed note 6378561924) · the seventh cut under ruling 6364006498 (A), and the first of the two buildPluginList cuts the seat set at 02:10Z (the organization factory, then the rest; the series ends after cut 16) · amends claim 6377759458 for this PR, on the merged file · 2026-10-24T03:14Z
    Session: session_01LSXBc2GCBwiAL7vV4kWkyr
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lg3wrj (the harness-pinned branch for this session, restarted from origin/main after #25566 merged; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25224 (a linked worktree in this session's container)
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main ba9e0c3749, which is cut 14's 35d5060380 plus #25559; there auth-manager.ts is 7,122 lines). One commit, 361581a546: +734/−608 over 7 files (1,342 changed lines). Four of the seven are readers outside the move and one is a new anchor entry, so check-governed-merges.mjs ran on the trial file list before the cut and on the final list after it: NOT governed (0 of 7 paths hit the register), so this is not Tier H.

    (a) The move: the organization plugin's factory. buildPluginList (:1892, private async, annotated Promise<any[]>) builds better-auth's plugin list; for each optional plugin it calls await this.addOptionalPlugin(plugins, name, async () => {…}), whose arrow loads the plugin and builds it. The organization call is at :1990, inside if (enabled.organization). Its arrow's body (:1991–:2559, 569 lines) moves unedited into a new module, packages/plugins/plugin-auth/src/auth-manager-organization-plugin.ts (683 lines), as the body of export async function buildOrganizationPlugin(this: OrganizationPluginHost), re-indented four spaces (two levels).

    • The body, in order: the deferred better-auth/plugins/organization import; the closed organization-role vocabulary and the roles map built from it (better-auth/plugins/organization/access), stored on the manager as orgRolesMap; one options object (the schema, teams, the requireEmailVerificationOnInvitation: false declaration, the organization limit, the membership limit, the organization hooks for creation, invitation, acceptance, slug change and deletion with their refusal codes, and the invitation e-mail); the vendor plugin built from it; and the rebuilt /organization/list-user-invitations listing, with its warning when the vendor no longer exposes it. All of it moves, in the same order, with its comments.

    • The arrow keeps its place as the third argument of the same call, and the call keeps its place in buildPluginList. Three comment lines go above the call, and the arrow becomes:

      await this.addOptionalPlugin(plugins, 'organization', () => {
        const readConfig = () => this.config;
        const readOrgRolesMap = () => this.orgRolesMap;
        const writeOrgRolesMap = (v: AuthManager['orgRolesMap']) => { this.orgRolesMap = v; };
        const host: OrganizationPluginHost = { /* 8 late-bound closures, 1 getter, 1 get/set pair, alphabetical */ };
        return buildOrganizationPlugin.call(host);
      });

    The seat's bars, each measured (cuts 12 and 13's closure bars, plus the get/set call from cut 10):

    • Body. The body is token-identical inside the module function: 571 lines with its braces, 1,854 tokens (TypeScript scanner, template literals re-scanned at their substitutions). Each of the 569 lines between the braces is the base line minus four spaces. The closing brace is the one line that moves further: at base the body's lines sat at the same column as the arrow's own }); line, so its } was at column 6, and in the module it is at column 0. Negative control: one token changed in the module text the proof reads (organization(organizationOptions) to organization?.(organizationOptions), in memory; the file is untouched) fails the token and line checks, and the run exits 1.
      • async moves from the arrow to the function. The arrow is not async; it returns buildOrganizationPlugin.call(host), the function's own promise, so addOptionalPlugin awaits the same promise the body settles. The readers, the writer, the host literal and the .call cannot throw before the promise exists, and a throw inside the body is still a rejection of it.
      • The arrow and the function take no parameter, and neither carries a return annotation. The body uses no arguments, new.target or super (0 of each), no bare this, and no this under a non-arrow function (its one non-arrow function node is a function type). It reads no local of buildPluginList: enabled and plugins appear only in the if and in the call's own arguments, and the body's dataEngine is its own local.
      • buildPluginList is byte-identical at base and at head outside the arrow, apart from the three comment lines above the call (772 lines at base, checked by the same run).
    • Binding identity. A binding proof walks all 496 identifiers of the body at base and in the module, in order, with the TypeScript checker (the module's ./auth-manager.js resolved to the head file, where the in-place exports below live). All 496 resolve the same way at both ends: 324 to declarations inside the body (the same declaring identifier, by position), 18 to receiver members, 31 reads of 29 imported names with the same specifier and imported name (the nine ./auth-manager.js names below among them), 11 to the same declaration in auth-manager.ts (seven AuthManagerOptions members read through this.config: dataEngine, getI18n, invitationPlacement, logger and its warn, onInvitationAccepted and onOrganizationCreated; and the locale of emailLocaleArg's answer), and 112 to no declaration (globals and property names on untyped values). None resolves to a local of buildPluginList, to an unexported top-level declaration of auth-manager.ts or to a top-level declaration of the module. Negative controls: pointing the module's membership-limit import at another specifier gives 1 difference and exit 1; shadowing the placeholder-email import with a module-level const gives 1 difference, reports the module top-level read, and exits 1; running without being told about the in-place exports reports the base reads of the eight unexported declarations, and exits 1.
    • Receiver. The body's this was the AuthManager instance. It is now the host. OrganizationPluginHost names exactly the 10 members the body reads, each typed as the class member (AuthManager['x']). 8 are methods, carried as (...a) => this.x(...a), so a spy or double set on the manager is still the one the body reaches, including from the organization hooks and the invitation e-mail, which better-auth runs long after the function returns (emailLocaleArg, getAppName, getConsolePageUrl, getDataEngine, getEmailService, multiOrgPostureEffective, resolveMembershipRole, storedRecipientLocale). Two are fields:
    • Eight top-level declarations, exported in place, and one already exported. The body reads nine top-level declarations of auth-manager.ts: the five organization hook refusal codes (ORGANIZATION_RESERVED_IDENTIFIER_CODE, ORGANIZATION_CREATION_DISABLED_CODE, INVITE_PLACEMENT_UNSUPPORTED_CODE, INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE_CODE, ORGANIZATION_SLUG_LOCKED_CODE), the slug guard's and the placement check's read-fault answers (slugGuardReadFaultApiError, invitationPlacementReadFaultApiError), the placement gate's refusal test (isInvitationPlacementGateRefusal), and readInvitationPlacementIntent, which is already exported. Per ruling 6364006498 (A), the eight unexported ones get export in place, in this cut, the first whose body reads them, and the module imports all nine by value from ./auth-manager.js. The binding proof checks that each head declaration is the base one with export in front and nothing else changed, and that the base and head reads resolve to it. They stay where they are, so the two read-fault answers and their new APIError( sites stay in auth-manager.ts. src/index.ts names its auth-manager.ts exports one by one and names none of them, so nothing is published: the ESM and CJS entries export the same 159 names at base and at head (the sha256 of the sorted names is 3abb126f5babb701… on all four), none of the eight is among them, and dist/index.d.ts names none of them. readInvitationPlacementIntent was exported and published at base, and is in both lists and in dist/index.d.ts at both ends, unchanged.
    • No static read. The body reads no AuthManager.X, so the module imports the class as a type only.
    • Host lifetime. The host is built each time addOptionalPlugin runs the factory, which buildPluginList does once per build, as every stub in this series builds its host per call. Nothing the body reads is captured early: the closures look each method up on the manager at call time, and the getter and the get/set pair read and write the fields when the body, or a hook it handed better-auth, does.
    • Readers. The decision letters on the anchored surface, read by check-adr-anchors on the merge base and on the head: 1,939 on ba9e0c3749 and 1,939 at 361581a546, so none drop. The body's letters leave auth-manager.ts, which is anchored, for the new module, which the entry in (c) anchors. Anchored files go 84 → 85, citations 52,305 → 52,307 (the entry's two ADR-0108 mentions) and files 6,698 → 6,700 (the module and the entry).
      • Tenant census and symbol-anchor output: byte-identical at base and head (check-tenant-audit-census, check-adr-symbol-anchors, check-scripts-symbol-anchors, check-spec-docblock-symbol-anchors, check-system-context-census and check-platform-checklist; cmp clean). The full plugin-auth suite passes at both ends: 191 files, 3,626 passed and 10 skipped, at both.

    (b) Imports. Fifteen imported names leave auth-manager.ts, because the body was their last reader there: the type OrganizationOptions (its whole better-auth/plugins/organization line), the whole ./cloud-managed.js line (CLOUD_MANAGED_REFUSAL_CODE, CLOUD_MANAGED_REFUSAL_MESSAGES), resolveOrgLimit (the @objectstack/types block keeps resolveTenancyPosture and isMcpServerEnabled), the whole ./membership-limit.js line (resolveMembershipLimitOption), the whole @objectstack/spec block (MEMBERSHIP_ROLE_DELEGATED_ADMIN, MEMBERSHIP_ROLE_EXTERNAL), withSystemContext (the ./objectql-adapter.js line keeps createObjectQLAdapterFactory and withSystemReadContext), the whole ./list-user-invitations-verification.js, ./invitation-role-cap.js, ./invitation-role-label.js and ./organization-delete.js blocks (applyDeclaredInvitationVerificationToListing, invitationRoleCapFailure, isPlainMemberInvitation, invitationRoleLabel, isDefaultOrganizationTarget, removeOrganizationAuthStructure), and buildOrganizationPluginSchema (the ./auth-schema-config.js block keeps its other names). One comment and one line are added after cut 14's: import { buildOrganizationPlugin, type OrganizationPluginHost } from './auth-manager-organization-plugin.js'. isCloudManaged, postureEnforcesWall, resolveTenancyPosture, withSystemReadContext and isPlaceholderEmail have other readers, so both files import them. One prose mention of withSystemContext stays in a docblock (:892 at head, in backticks, not a {@link}), so nothing dangles.

    Evaluation order. esbuild drops a type-only import, so a module whose last value import leaves auth-manager.ts can now be evaluated later. I measured the order instead of arguing it:

    • The ES modules, at run time. A loader hook logged each ES module as its body ran while dist/index.mjs loaded, at base and at head. 171 modules evaluate at both, in the same order apart from one adjacent pair just before the bundle's own body: @objectstack/platform-objects/identity and @better-auth/core/utils/ip swap places (169 and 170). The bundle's first platform-objects/identity import is invitation-role-label.ts's (SysMember), and that module's segment now sits later in the bundle (line 4,828 at base, 8,522 at head), after the first utils/ip import. Neither module imports the other. utils/ip's top level imports @better-auth/core's env module and zod, builds two zod schemas and declares constants and functions; platform-objects/identity imports three @objectstack/spec entries, all evaluated earlier at both, and builds object schemas. Neither writes process.env or a global.
    • The CommonJS entry. A Module._load log for dist/index.js gives 127 first loads at both. Two move earlier: @better-auth/core/dist/utils/ip.mjs and @better-auth/core/dist/context/index.mjs were the last two loads at base and now come right after @objectstack/spec/identity, ahead of platform-objects/identity, rest, observability, metadata-core, node's module and metadata/view-container. Their top levels are the ones above and the inert context chain cut 14 measured (its only load-time reads are of process.env), and none of those five package entries writes process.env or a global anywhere in its file (module is node's own).
    • plugin-auth's own modules. A walk of the bundle's top level and its eager init_* calls finds six that run later at both entries: cloud-managed.ts, membership-limit.ts, list-user-invitations-verification.ts, invitation-role-label.ts, organization-delete.ts and grant-set-name.ts, whose last value import from auth-manager.ts left with the body (grant-set-name.ts moves with organization-delete.ts, which imports it). Their top levels are imports, constants with no call in their initializers, types and function declarations: no load-time call at all. No importer reads any of their bindings at load time: all 35 references in the 11 importing files are inside functions, and index.ts re-exports cloud-managed.ts with export *, which reads nothing.
    • The new module's own top level is imports, one interface and one function declaration. Its value import of auth-manager.ts, for the nine declarations, and auth-manager.ts's import of it form a cycle with nothing read at load time, as cut 14's does.

    (c) Path readers. Four readers outside the move follow it, in the same commit, and one anchor entry is added:

    • packages/plugins/plugin-auth/src/api-error-code.pin.test.ts: authManagerFiles, the files AuthManager's throws are counted over, gains auth-manager-organization-plugin.ts. The body carries 7 new APIError( sites, so the sum over the list is 30 at head, as at base (7 in the module; auth-manager.ts goes 9 → 2, the two read-fault answers that stay). Without the new entry the sum would be 23, under the floor of 24, and the pin would fail; it is a floor over a list, not a silent zero.
    • better-auth-schema-parity.test.ts and managed-extension-fields.test.ts: each one's authManagerPluginFactories() scans a list of files for const { X } = await import('better-auth/plugins…' | '@better-auth/…'), and both lists gain auth-manager-organization-plugin.ts (the comment above each list now says "any of them" for "either"). The body holds one such import, organization. At base the scan finds 16 factories; at head without the new entry it would find 15, missing organization, which each test's stale-entry check fails on; with it the scan finds the same 16 as base.
    • auth-email-locale.test.ts: the two population comments (the docblock at :861 and the in-test note at :1127, head numbering) that name the files the five sendTemplate sites live in now name auth-manager-organization-plugin.ts (the body of the organization plugin's factory) among them; the invitation send moved with the body. No assertion, count or pattern changes.
    • The anchor entry: scripts/adr-anchors/packages__plugins__plugin-auth__src__auth-manager-organization-plugin.ts.json (new), with adrs: ["ADR-0108"] and the auth-manager.ts entry's ADR-0108 paragraph verbatim ("better-auth's organization roles map registers the closed framework vocabulary ONLY…"). The body is that mechanism: it builds the roles map from the closed vocabulary and stores it. This is cut 4's precedent (an entry for the new module carrying the paragraph verbatim; the old entry stays unchanged), and ruling 6353073966 item 1 says scripts/adr-anchors/ is not a governed-register row. The auth-manager.ts entry is not edited: it keeps ADR-0108, which the file still cites (:771, the additionalOrgRoles removal note), and its ADR-0093 D2 paragraph.
    • Read, and not edited:
      • scripts/check-dispatcher-error-vocabulary.mjs: the body's 7 new APIError( sites each carry a registered or standard code, and the gate's only per-file plugin-auth key is liveMemberFile (auth-manager-before-hook.ts), which the cut does not touch. It exits 0 at both, 56 sites found and 56 classified at both, and only its scanned-file count moves (3,434 → 3,435). --self-test is among the derived gates.
      • The census: no census site moves: the body's engine calls are reads. check-tenant-audit-census exits 0 at both with no --write, with byte-identical output. Its --json document has 238 sites at both and 0 per-site differences with line numbers ignored. 2 sites are in auth-manager.ts at both, 0 are in the new module, and 0 are unresolved or unledgered at both. Only scannedSources (753 → 754) and measuredAt move, so neither census document is edited.
      • scripts/measure-durability-swallow-family.mjs (a measurement, not a gate): 77 members and 84 quiet rows at both, identical with line numbers ignored (one member, in observeFirstMemberships, which stays, moves 566 lines up with the cut's net: :4442 → :3876). Every statistic is the same apart from files (3,484 → 3,485): tryStatements 3,646 and droppedNotAwaited 234 at both.
      • check-auth-mount-ledger.mjs reads only the basePath declaration in auth-manager.ts, which stays.
      • auth-manager-base-path.test.ts reads auth-manager.ts for code lines carrying '/api/v1/auth'; the body has none.
      • rate-limit-storage-isolation.test.ts walks the import graph from rate-limit-storage.ts, which reaches neither file.
      • check-system-context-census.mjs tracks reads of ExecutionContext.isSystem; the body names it once, in a comment.
      • scripts/publish-smoke.sh names auth-manager.ts for the admin: pluginConfig.admin ?? scimEffective line, in the enabled literal, which stays.
    • tsup bundles the new module into the root entry (splitting: false). The dist has the same 18 files at base and at head, plus the two .build-input-hash stamps the build writes from its inputs. Only index.js, index.mjs and their maps differ.

    ⛔ Nothing else changes:

    • No method, field or top-level declaration moves besides the body. The body is not edited.
    • No stay-behind code is edited apart from the arrow (now the stub) and the three comment lines above its call, the import lines in (b), and the export on the eight declarations.
    • The only edits outside plugin-auth/src's two moved files are the readers and the anchor entry in (c). No ADR, docs prose, baseline or eslint.config.mjs is edited.
    • The title goes under refactor(plugin-auth), with no !.
    • No changeset: skip-changeset, per ruling 6348891160 item 4.

    Anchor reading, on ba9e0c3749 and at the head: I mapped every auth-manager.ts#symbol anchor in the tracked tree, outside changelogs, with symbolSegmentResolution from scripts/symbol-anchors.mjs. There are 29, and all 29 are declaration-class at base, at base with the body blanked, and at head. Two of their symbols occur as a token on a line of the body: AuthManager (ADR-0135 :104), whose declaration is the class, which stays, and ORGANIZATION_CREATION_DISABLED_CODE (the attachments-storage checklist area :1116), whose declaration (:1403 at base) stays and gains only export. Each resolves to its declaration at all three readings.

    Container & model: L (series), card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier: "no path-derived mandate"), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Basis for the Clause-② line: a move-only cut plus reader migrations and one anchor entry widens no accept set and no public surface. The published surface is measured, not argued. The proof owed:

    • token identity and binding identity for the body against this base, each with a negative control, and buildPluginList byte-identical outside the arrow;
    • ruling 6348891160 item 2's block: raw and normalised sha256 of dist/index.d.ts and dist/index.d.mts at base and at head, the normaliser verbatim, the raw diff line count, and the positive control;
    • ESM and CJS export lists at both, and that none of the nine ./auth-manager.js names the module imports is in either, nor in dist/index.d.ts;
    • plugin-auth suite counts at base and at head;
    • the census and the durability measurement at base and head;
    • the symbol-anchor checks and check:adr-anchors (with the decision-letter count) on the merge base and on the head;
    • the load order at base and at head;
    • every derived gate.

    Premise (measured on this base and at 361581a546):

    • 1 of 1 body token-identical (571 lines, 1,854 tokens) and 496 of 496 identifiers bound the same way, each proof with failing negative controls, and buildPluginList byte-identical outside the arrow, as in (a).
    • pnpm --filter @objectstack/plugin-auth typecheck exits 0, and so does pnpm --filter @objectstack/runtime typecheck. eslint on the 6 TypeScript files exits 0.
    • Suite: 191 files, 3,626 passed and 10 skipped at base and at head.
    • dist/index.d.ts and dist/index.d.mts are raw byte-identical at base and at head (0 diff lines). The ESM and CJS entries export the same 159 names at both; none of the eight declarations exported in place, buildOrganizationPlugin or OrganizationPluginHost is among them or in dist/index.d.ts.
    • The census and the durability measurement read as stated in (c). The load order reads as stated under (b).
    • 71 derived gates run at head after a full build, and all exit 0 (--ran: a derived zero). check-governed-merges.mjs --branch HEAD: NOT governed, 1,342 changed lines.
    • affected-docs.mjs --json ba9e0c3749 (exit 0): 41 pages, all name matches of names the moved code carries (the VALIDATION_ERROR, sys_member, sys_organization, sys_invitation and sys_environment literals, and AuthManager). One names auth-manager.ts: content/docs/kernel/services-checklist.mdx (:314), for the identity provider service as a whole, which still holds. The code that reads those names is unchanged, so no doc edit is owed. 11 of them are release-owned (the release notes and releases/implementation-status.mdx) and not edited.
    • The reader controls in (c), the anchor map and the decision-letter readings above.

    A diff of another kind is a fresh fork on this card.

    Responsibility: n/a — not a defect card
    Thread-read: 6378561924
    Ruling-ref: 6348891160 (the .d.ts bar), 6364006498 (ruling A on this card), 6353073966 item 1 (anchor entries are not a register row)
    Serial constraints: re-read after the proof run, at 03:13Z. 8 PRs are open, and none touches auth-manager.ts, the new module's path, a reader edited in (c) or scripts/adr-anchors/: #25593 and #25588 (.claude/, scripts/pm/), #25592 (plugin-approvals), #25591 (plugin-sharing, lint, docs), #25589 (metadata-protocol, core), #25569 (cli, docs) and #25540 (rest). The Version Packages PR #24876 touches plugin-auth's package.json, its CHANGELOG.md and changesets only.


    Generated by Claude Code

  15. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25224,
    "status": "done",
    "branch": "claude/project-thread-lg3wrj",
    "pr": "#25594",
    "session": "session_01LSXBc2GCBwiAL7vV4kWkyr",
    "premise_still_valid": true,
    "summary": "Cut 15 of the auth-manager.ts split, per the amended claim 6379187398 (amends 6377759458 on the file cut 14 left; landed note 6378561924), under Ruling-ref 6348891160 (the .d.ts bar) and 6364006498 (Sales's ruling A on this card). This is the seventh cut under ruling A and the first of the two buildPluginList cuts the seat set at 02:10Z (the organization factory, then the rest of the method; the series ends after cut 16), on the closure shape cuts 12 and 13 used, with cut 10's get/set pair (#25508) for the one field the body writes. Part of #25224, no closing keyword; #25224 stays open. Base origin/main ba9e0c3 (cut 14's 35d5060 plus #25559), one commit 361581a, +734/-608 over 7 files (1,342 changed lines). Moved: the body of the organization factory arrow buildPluginList (:1892, private async, annotated Promise of any[]) hands addOptionalPlugin at :1990 (body :1991-:2559, 569 lines), unedited, into the new module packages/plugins/plugin-auth/src/auth-manager-organization-plugin.ts (683 lines) as export async function buildOrganizationPlugin(this: OrganizationPluginHost), re-indented four spaces. The body, in the same order: the deferred better-auth/plugins/organization import; the closed organization-role vocabulary and the roles map built from it, stored on the manager as orgRolesMap; one options object (schema, teams, the requireEmailVerificationOnInvitation: false declaration, the organization and membership limits, the organization hooks for creation, invitation, acceptance, slug change and deletion with their refusal codes, the invitation e-mail); the vendor plugin built from it; and the rebuilt /organization/list-user-invitations listing with its warning. The call keeps its place in buildPluginList and its arrow is now non-async: two reader closures and a writer closure, one host literal (8 late-bound method closures, a getter for config, a get/set pair for orgRolesMap, alphabetical) and return buildOrganizationPlugin.call(host); three comment lines go above the call. The seat's bars: (Body) 571 lines / 1,854 tokens token-identical, each line between the braces the base line minus four spaces (the closing brace moves from column 6 to column 0), with a failing negative control; async moved from the arrow to the function and the arrow returns that promise, which addOptionalPlugin awaits as before; no parameter, no return annotation, no arguments, new.target, super or bare this, no this under a non-arrow function; no local of buildPluginList read (enabled and plugins appear only in the if and the call's own arguments; the body's dataEngine is its own local); buildPluginList byte-identical outside the arrow apart from the three comment lines (772 base lines). (Binding) a checker proof resolves all 496 identifiers the same way at both ends (324 body-local, 18 receiver members, 31 reads of 29 imported names, 11 to the same auth-manager.ts declaration: seven AuthManagerOptions members read through this.config and the locale of emailLocaleArg's answer, 112 globals or untyped property names), none to a buildPluginList local, an unexported auth-manager.ts top level or a module top level, with three failing negative controls. (Receiver) OrganizationPluginHost names exactly the 10 members the body reads, each typed AuthManager[x]: 8 methods as late-bound closures (emailLocaleArg, getAppName, getConsolePageUrl, getDataEngine, getEmailService, multiOrgPostureEffective, resolveMembershipRole, storedRecipientLocale); config (reassigned by setRuntimeBaseUrl and applyConfigPatch; read 9 times, never written) as a readonly getter; orgRolesMap as a get/set pair, not readonly, because the body writes it once (:2050) and never reads it, so the field the actor guards read back is the same field, written at the same moment. Function-valued reads disclosed: this.config.invitationPlacement?.() at :2245 and :2290, this.config.logger.warn at :2487, onOrganizationCreated (:2178) and onInvitationAccepted (:2307) read into a local cb and called, getI18n handed on as a value (:2485), each off the manager's current config with the same callee and this. (In-place exports) the body reads nine auth-manager.ts top levels: the five organization hook refusal codes, slugGuardReadFaultApiError, invitationPlacementReadFaultApiError, isInvitationPlacementGateRefusal, and readInvitationPlacementIntent, already exported. Per ruling A the eight unexported ones get export in place and the module imports all nine by value; the proof checks each head declaration is the base one plus export and that both reads resolve to it; index.ts names none of them, so nothing new is published. No static read (the class is a type-only import). (Host lifetime) built each time addOptionalPlugin runs the factory, once per build; nothing captured early. (Readers) check-adr-anchors decision letters 1,939 on the merge base ba9e0c3 and 1,939 on the head: none drop. The body's letters leave auth-manager.ts for the new module, which a new anchor entry anchors: scripts/adr-anchors/packages__plugins__plugin-auth__src__auth-manager-organization-plugin.ts.json carries adrs ADR-0108 and the auth-manager.ts entry's ADR-0108 paragraph verbatim, because the body builds the organization roles map from the closed vocabulary, the mechanism that paragraph describes (cut 4's precedent; ruling 6353073966 item 1: anchor entries are not a register row); the auth-manager.ts entry is not edited and the file still cites ADR-0108 at :771. Anchored files 84 to 85, citations 52,305 to 52,307, files 6,698 to 6,700. Four path readers move in the same commit: api-error-code.pin.test.ts's authManagerFiles gains the module (the body's 7 new APIError sites; sum 30 at both, 23 under the floor of 24 without it); better-auth-schema-parity.test.ts and managed-extension-fields.test.ts's factory-scan lists gain the module (16 factories at base; 15, missing organization, without it; 16 with it); auth-email-locale.test.ts's two population comments name the module. Read and not edited: the vocabulary gate (its only per-file plugin-auth key, liveMemberFile, names auth-manager-before-hook.ts, untouched; 56/56 at both), the census (238 sites line-blind equal), the durability measurement (all statistics equal but files), the mount ledger, the base-path test, the rate-limit isolation walk, check-system-context-census and publish-smoke.sh. Tenant census and symbol-anchor output byte-identical at base and head; full plugin-auth suite passes at both. Fifteen imported names leave auth-manager.ts with the body and one import of the new module is added. Evaluation order measured, not argued: 171 ES modules evaluate at both, in the same order apart from one adjacent pair (platform-objects/identity and @better-auth/core/utils/ip swap at 169 and 170, because invitation-role-label.ts's segment sits later in the bundle); neither imports the other and neither writes process.env or a global. The CJS entry's 127 first loads differ only in utils/ip.mjs and context/index.mjs moving up to right after spec/identity; none of the five package entries they now precede writes process.env or a global (module is node's own). Inside the bundle cloud-managed.ts, membership-limit.ts, list-user-invitations-verification.ts, invitation-role-label.ts, organization-delete.ts and grant-set-name.ts run later; their top levels make no load-time call and all 35 references to them in the 11 importing files are inside functions. auth-manager.ts 7,122 to 6,556 lines. No sign-in, session or membership outcome changes.",
    "tests": "Base ba9e0c3, head 361581a. Builds and suites ran through os-verify-lock, every exit code captured before any pipe. A full turbo run build ran first; then plugin-auth was built and tested at base, and built and tested at head, on the same dependency dists. (1) Move proof (TS scanner tokens, kind and text, template literals re-scanned at their substitutions): 1 of 1, 571 lines and 1,854 tokens, each line between the braces the base line minus four spaces. The arrow is non-async with no parameter: the two readers and the writer, one host literal whose keys are exactly OrganizationPluginHost's (alphabetical; 8 late-bound closures, the config getter, the orgRolesMap getter and setter), then return buildOrganizationPlugin.call(host); the interface types each member AuthManager[x] and exactly the getter-only field is readonly; buildPluginList outside the arrow is byte-identical apart from the three comment lines. Negative control: the module text changed in memory (organization(organizationOptions) to organization?.(organizationOptions)) fails the token and line checks and the run exits 1; the file is untouched. Binding proof (TypeScript checker over the base file, the head file and the module, the module's ./auth-manager.js resolved to the head file): 496 of 496 identifiers resolve the same way in order; the eight in-place exports checked as the base declaration plus export. Negative controls: the membership-limit import pointed elsewhere gives 1 difference and exit 1; the placeholder-email import shadowed by a module const gives 1 difference and exit 1; run without the in-place exports declared, it reports the base reads of the eight unexported declarations and exits 1. git diff --color-moved=plain --color-moved-ws=allow-indentation-change ba9e0c3 361581a over the two source files marks 577 of 600 deleted and 580 of 717 added lines moved; the 23 unmoved deletions are 14 import lines, the first lines of the eight declarations (now with export) and the arrow line (now without async). (2) Ruling 6348891160 item 2 block, identical at base and head. dist/index.d.ts: raw 1b3a48357d137155de33104277667a922aab62df73b418b03894c56faaacd0ba, normalised cee7303ccdfe72794efd588c85b628419d4253d730b5a0c5a3f1cdb8dddb4443. dist/index.d.mts: raw b5e8dc982b634f0a5108b1b533626a68767f2da5a56f72dba801059b149e2872, normalised 562d0a4ce656f97aeb937541482cdf531b61d7c75a005918784bf0a52d032dfc. Raw diff 0 lines, so there is no union swap. Normaliser: the ruling's one-liner, verbatim in the PR body. Positive control: head dist/index.d.ts with 'private buildPluginList;' made public (line 1829) hashes raw f8ff1470cbc34cd34a441c67b8763c5adde86fa408a4bcecf1590c60d30bce57 and normalised fe48a6a7f320e626e941a422a27571c28fbdc47a73bbbdaa0d5048d7307cae55; both differ. Export lists: ESM and CJS have 159 names each at both points, sorted-name hash 3abb126f5babb701 on all four; none of the eight in-place exports, buildOrganizationPlugin or OrganizationPluginHost is exported or named in dist/index.d.ts; readInvitationPlacementIntent is in both lists and in dist/index.d.ts at both, unchanged. The same 18 dist files at both, plus the two .build-input-hash stamps; only index.js, index.mjs and their maps differ. Load order: the ES-module evaluation log, the CJS first-load log and the bundle walk as in the summary, each at base and head from the built dist copied into the package and removed after. (3) pnpm exec vitest run --maxWorkers=3 in packages/plugins/plugin-auth: base Test Files 191 passed (191), Tests 3626 passed | 10 skipped (3636), exit 0; head the same counts, exit 0. pnpm --filter @objectstack/plugin-auth typecheck exit 0; pnpm --filter @objectstack/runtime typecheck exit 0; eslint over the 6 TypeScript files exit 0. No tested behaviour changed, so no ablation is owed; the three reader controls (pin sum 23 and factory scan 15 without the new list entries) are the failing readings the migrations prevent. (4) check-tenant-audit-census output byte-identical at base and head (exit 0, no --write); tenant-audit-census --json: 238 sites at both, 0 per-site differences line-blind, 2 in auth-manager.ts at both, 0 in the new module, 0 unresolved and 0 unledgered; only scannedSources (753 to 754) and measuredAt differ. measure-durability-swallow-family --json: 77 members (same with every line number ignored) and 84 quiet rows at both; files 3,484 to 3,485; tryStatements 3,646 and droppedNotAwaited 234 at both. (5) Anchors at base and head, run by hand, all exit 0, five with byte-identical output: check-adr-symbol-anchors (2,384 anchors, 466 symbol), check-scripts-symbol-anchors (4,070), check-spec-docblock-symbol-anchors (5,076), check-system-context-census (149 elevation read sites), check-platform-checklist (16 areas, 329 items); check-adr-anchors differs only in anchored files (84 to 85), citations (52,305 to 52,307) and files (6,698 to 6,700), letters 1,939 at both. check-dispatcher-error-vocabulary: 56 found and 56 classified at both, scanned files 3,434 to 3,435. Anchor map: all 29 auth-manager.ts#symbol anchors outside changelogs are declaration-class at base, with the body blanked, and at head; two of their symbols (AuthManager, ORGANIZATION_CREATION_DISABLED_CODE) are tokens on a body line and resolve to their declarations, which stay, at all three. (6) Merge check on origin/main ba6912c (9 merges past the base: #25557, #25567, #25585, #25586, #25587, #25590, #25545, #25552, #25589) with 361581a cherry-picked, no conflict. Move proof and binding proof against ba6912c: 1 of 1 (571 lines, 1,854 tokens, buildPluginList byte-identical outside the arrow) and 496 of 496, the eight in-place exports checked as the base declaration plus export. check-tenant-audit-census, check-adr-anchors, check-adr-symbol-anchors, check-platform-checklist, check-system-context-census, check-auth-mount-ledger, check-dispatcher-error-vocabulary and check-scripts-symbol-anchors all exit 0. check-adr-anchors on ba6912c alone: 84 anchored files, 52,315 citations across 6,701 files, 1,883 letters; with this cut: 85, 52,317 across 6,703, 1,883 (none drop; main's own count differs from the base's 1,939 through those nine merges, not through this cut). Durability: 77 members and 84 quiet rows at both, files 3,487 to 3,488, tryStatements 3,646 and droppedNotAwaited 231 at both. The full plugin-auth suite on the merged tree: Test Files 191 passed (191), Tests 3626 passed | 10 skipped (3636), exit 0. origin/main has since moved one merge, to 4a25192 (#25591: plugin-sharing, lint, docs), which touches no plugin-auth file, no anchor entry and no reader named above.",
    "gates": "Local subset at head after the full build: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 71 commands for the 7 paths. All 71 ran, each exit code captured before any pipe, and all 71 exit 0. --ran over that record: 71 derived, 71 run, 0 NOT-MEASURED, a derived zero. dispatch-gates --tier: no path-derived mandate. node scripts/pm/check-governed-merges.mjs: on the trial file list before the cut and --branch HEAD after the commit both NOT governed (0 of 7 paths), 1,342 changed lines against the 3,000-line human-merge line. Docs drift (affected-docs --json ba9e0c3, exit 0): 41 pages, all name matches of literals and names the moved code carries; one names auth-manager.ts (services-checklist.mdx:314, the identity provider service as a whole, which still holds) and the code reading those names is unchanged, so no doc edit is owed; 11 are release-owned, not edited. CI on head 361581a converged: 46 check runs, 34 success and 12 skipped, 0 failed, 0 pending; the PR reads mergeable, clean, against e073455. No review and no review comment. The one issue comment is the docs-drift bot's (6379236911), which counts the same 41 pages as the local affected-docs run above (30 hand-written, all name matches, and 11 release-owned, read-only); no doc edit is owed, as stated in the PR body.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "5 GitHub MCP calls since cut 14's report (6378140559), all reads: issue_read get on #25224 twice and get_comments once (to the landed note 6378561924 and the seat's notes), pull_request_read get on #25566 once, and pull_request_read get_check_runs on #25594 once. GitHub REST reads through curl, reads only: the open PR list and each open PR's file list for the serial reads, and this PR's check runs, reviews, comments and state. (claude-code-remote subscribe_pr_activity once on #25594, not a GitHub write.)",
    "api_writes": "3 relay acts since cut 14's report (scripts/pm/, each read back identical): 1) amended claim 6379187398 via post-stamped, run 40057514078, 26051 bytes; 2) pr_create #25594 via fleet-write, run 40057753236, 30206 bytes sent and stored, assignee os-sales, label skip-changeset; 3) this report via post-stamped. Plus git push of the branch (a new branch after the queue deleted the old one on #25566's merge; not REST). No PR body edit. No other write.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  16. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed cut 15 (organization plugin factory) · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-24T09:50Z

    reading value
    PR #25594 MERGED through the queue (ACCEPT 6379586182, held through the 18.1 release freeze, readied and armed after merges resumed), head 361581a546
    origin/main carries f1921230b2

    State: the card stays open and dispatched (Part of). Cut 16, the series' last, is claimed afresh on the merged auth-manager.ts; its final report closes the card.


    Generated by Claude Code

  17. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · series card, cut 16, the series' last (one PR, cuts strictly serial: after cut 15, PR #25594, landed as f1921230b2, per the landed note 6382750527) · the eighth cut under ruling 6364006498 (A), and the second of the two buildPluginList cuts the seat set at 02:10Z (the organization factory, then the rest of the method; the series ends after this cut) · amends claim 6379187398 for this PR, on the merged file · 2026-10-24T10:43Z
    Session: session_01LSXBc2GCBwiAL7vV4kWkyr
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lg3wrj (the harness-pinned branch for this session, restarted from origin/main after #25594 merged; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25224 (a linked worktree in this session's container)
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main 10b8250ad5, which is cut 15's f1921230b2 plus #25600 and #25604; there auth-manager.ts is 6,556 lines, and its text is byte-identical to cut 15's head). One commit, a2413e05ce: +961/−813 over 5 files (1,774 changed lines). Two of the five are readers outside the move and one carries two comments, so check-governed-merges.mjs ran on the trial file list before the cut and on the final list after it: NOT governed (0 of 5 paths hit the register), so this is not Tier H.

    (a) The move: the rest of buildPluginList, which is its whole body. buildPluginList (:1875, private async, annotated Promise<any[]>) builds better-auth's plugin list; createAuthInstance calls it once per build (through its host's closure, since cut 14). Its body (:1876–:2666, 791 lines) moves unedited into a new module, packages/plugins/plugin-auth/src/auth-manager-plugin-list.ts (913 lines), as the body of export async function buildPluginList(this: PluginListHost): Promise<any[]>, re-indented two spaces (one level). This is the #23050 method shape cuts 1 to 11 and 14 used.

    • The body, in order: the reset of the degraded-feature record and of the JWT signing verdict; the enabled table (the env and config precedence it documents and the SCIM coherence re-check); and every plugin in the same order: bearer, the organization factory's arrow (whose body cut 15 moved), twoFactor, breached-password rejection, admin, phoneNumber, magicLink, the generic OAuth providers, the OIDC provider with its jwt plugin, sso, scim, device authorization and the custom session, then return plugins. All of it moves, in the same order, with its comments.

    • The method keeps its place, its docblock, its private modifier, its (empty) parameter list and its return annotation. Its body becomes:

      private buildPluginList(): Promise<any[]> {
        // [#25224 cut 16] The body moved to `auth-manager-plugin-list.ts` (see `PluginListHost`).
        const readConfig = () => this.config;
        const readDegradedFeatures = () => this.degradedFeatures;
        const readJwtSigningFailureReported = () => this.jwtSigningFailureReported;
        const writeJwtSigningFailureReported = (v: AuthManager['jwtSigningFailureReported']) => { this.jwtSigningFailureReported = v; };
        const readOrgRolesMap = () => this.orgRolesMap;
        const writeOrgRolesMap = (v: AuthManager['orgRolesMap']) => { this.orgRolesMap = v; };
        const host: PluginListHost = { /* 18 late-bound closures, 2 getters, 2 get/set pairs, alphabetical */ };
        return buildPluginList.call(host);
      }

    The seat's bars, each measured (cut 14's method bars, plus the get/set call from cut 10):

    • Body. The body is token-identical inside the module function: 793 lines with its braces, 2,096 tokens (TypeScript scanner, template literals re-scanned at their substitutions). Every line is the base line minus exactly two spaces. Negative control: one token changed in the module text the proof reads (plugins.push(bearer()); to plugins.push(bearer?.());, in memory; the file is untouched) fails the token and line checks, and the run exits 1.
      • async moves from the method to the function. The stub is not async; it returns buildPluginList.call(host), the function's own promise, so the await this.buildPluginList() in createAuthInstance's body awaits the same promise the body settles. The readers, the writers, the host literal and the .call cannot throw before the promise exists, and a throw inside the body is still a rejection of it.
      • The base method, the stub and the function take no parameter and carry the same return annotation, Promise<any[]>. The body uses no arguments, new.target or super (0 of each), no bare this, and no this under a non-arrow function (its only non-arrow function nodes are the getter and the setter in the organization arrow's host literal, which read and write through their closures and name no this).
    • Binding identity. A binding proof walks all 500 identifiers of the body at base and in the module, in order, with the TypeScript checker (the module's ./auth-manager.js resolved to the head file, where the in-place exports below live). All 500 resolve the same way at both ends: 338 to declarations inside the body (the same declaring identifier, by position), 48 to receiver members, 23 reads of 20 imported names with the same specifier and imported name (the eight ./auth-manager.js names below among them), 10 to the same declaration in auth-manager.ts (six AuthManagerOptions members read through this.config: dataEngine, oidcProviders, and phoneOtp with its allowedAttempts, expiresIn and otpLength), and 81 to no declaration (globals and property names on untyped values). None resolves to an unexported top-level declaration of auth-manager.ts or to a top-level declaration of the module. The one read of the class itself is in a type position (AuthManager['orgRolesMap'], :1979, in the organization arrow's writer), and the module imports the class as a type. Negative controls: pointing the module's admin-impersonate-endpoint import at another specifier gives 1 difference and exit 1; shadowing the placeholder-email import with a module-level const gives 1 difference, reports the module top-level read, and exits 1; running without being told about the in-place exports reports the base reads of the two unexported declarations (5 reads), and exits 1.
    • Receiver. The body's this was the AuthManager instance. It is now the host. PluginListHost names exactly the 22 members the body reads, each typed as the class member (AuthManager['x']).
      • 18 are methods, carried as (...a) => this.x(...a), so a spy or double set on the manager is still the one the body reaches, including from the callbacks it hands better-auth (the phone OTP senders, the magic-link sender and the custom session among them), which run long after the function returns: addOptionalPlugin, buildJwtPlugin, computeAuthGate, deliverPhoneOtp, emailLocaleArg, enforceSessionControls, getAppName, getConsolePageUrl, getDataEngine, getEmailService, getMcpResourceUrl, isPlatformAdminUserId, multiOrgPostureEffective, reconcileScimUserLifecycle (which stays in the class, per ruling A), resolveAuthSecret, resolveMembershipRole, resolveTwoFactorAccountLockout and storedRecipientLocale. Every method read is a direct call.
      • config, which the class reassigns after construction (setRuntimeBaseUrl at :2736 and applyConfigPatch at :2817), and degradedFeatures (:1550, not reassigned, as every field this series carries goes) go as getters over reader closures, readonly on the interface. The body reads config 7 times and writes neither.
      • jwtSigningFailureReported and orgRolesMap go as get/set pairs, the shape the seat set for a moved body that writes a field (cut 10, refactor(plugin-auth): move AuthManager's request routing into auth-manager-request-routing.ts (#25224 cut 10) #25508; cut 6's reportJwtSigningFailure stub carries the same field the same way, :1840; cut 15 carried orgRolesMap so). The body resets jwtSigningFailureReported once (:1881, = false) and never reads it. It reads and writes orgRolesMap only through the organization arrow's reader and writer (:1978, :1979). Each setter writes the manager's own field through its writer closure, so reportJwtSigningFailure, which reads the flag back, and the actor guards, which read the roles map back, read the same field, written at the same moment, as before. Neither is readonly on the interface.
      • Function-valued reads, disclosed (the refactor(plugin-security): move the everyone anchor, the explanation, the sharing verdicts and the authored-row write verdict out of security-plugin.ts (#25223 cut 10) #25516 way): the body calls this.degradedFeatures.clear() (:1876) on the object the getter returns, the manager's own Map, at the same moment as before. Its this.config reads are data (plugins, phoneOtp, oidcProviders, dataEngine); none is called.
    • The organization arrow. It moves with the body, unedited, and builds its host from this, which is now this host: each of its eight closures calls this host's closure, which calls the manager's method; its config getter reads this host's getter; its orgRolesMap reader and writer read and write this host's pair, which read and write the manager's field. So it reaches the same members, and the same field, at the same moment as before. Its import (buildOrganizationPlugin and OrganizationPluginHost) moves to the new module with it.
    • Two top-level declarations, exported in place, and five already exported. The body reads seven top-level declarations of auth-manager.ts: readBooleanEnv (:347, 4 reads) and consentOrganizationOf (:1453, once), which were not exported, and resolveScimEnabled, assertScimAdminCoherence, resolveOidcProviderEnabled, resolveDcrEnabled and OAUTH_ORGANIZATION_CLAIM, which were. Per ruling 6364006498 (A), the two get export in place, in this cut, the first whose body reads them, and the module imports all seven by value from ./auth-manager.js, with the class as a type. The binding proof checks that each head declaration is the base one with export in front and nothing else changed, and that the base and head reads resolve to it. src/index.ts names its auth-manager.ts exports one by one and names neither, so nothing new is published: the ESM and CJS entries export the same 159 names at base and at head (the sha256 of the sorted names is 3abb126f5babb701… on all four), and neither in-place export, buildPluginList nor PluginListHost is among them. dist/index.d.ts names neither in-place export nor PluginListHost; its 7 buildPluginList lines are the class's private buildPluginList; and docblock text, the same 7 at both ends.
    • No static read. The body reads no AuthManager.X.
    • Host lifetime. The host is built each time buildPluginList is called, which createAuthInstance does once per build, as every stub in this series builds its host per call. Nothing the body reads is captured early: the closures look each method up on the manager at call time, and the getters and the get/set pairs read and write the fields when the body, or a callback it handed better-auth, does.
    • Readers. The decision letters on the anchored surface, read by check-adr-anchors on the merge base and on the head: 1,872 on 10b8250ad5 and 1,853 at a2413e05ce, so 19 drop: the body's 19 citations, listed in (c), leave auth-manager.ts, which is anchored, for the new module, which is not. The gate exits 0 at both and every citation still resolves. Anchored files stay 85 and citations 52,467; files go 6,589 → 6,590 (the module).
      • Tenant census and symbol-anchor output: byte-identical at base and head (check-tenant-audit-census, check-adr-symbol-anchors, check-scripts-symbol-anchors, check-spec-docblock-symbol-anchors, check-system-context-census and check-platform-checklist; cmp clean). The full plugin-auth suite passes at both ends: 191 files, 3,626 passed and 10 skipped, at both.

    (b) Imports. Nine imported names leave auth-manager.ts, because the body was their last reader there: resolveUserAuthzGrants (the @objectstack/core block keeps its other names), the whole ./admin-impersonate-endpoint.js block (applyPlatformAdminImpersonation), the whole ./auth-schema-config.js block (buildTwoFactorPluginSchema, buildOauthProviderPluginSchema, buildDeviceAuthorizationPluginSchema, buildAdminPluginSchema, buildPhoneNumberPluginSchema), and cut 15's ./auth-manager-organization-plugin.js line (buildOrganizationPlugin, type OrganizationPluginHost) with its three comment lines, which one comment and one line replace: import { buildPluginList, type PluginListHost } from './auth-manager-plugin-list.js'. AuthPluginConfig (a type), MCP_OAUTH_SCOPES and isPlaceholderEmail have other readers, so both files import them.

    Evaluation order. esbuild drops a type-only import, so a module whose last value import leaves auth-manager.ts can now be evaluated later. I measured the order instead of arguing it:

    • The ES modules, at run time. A loader hook logged each ES module as its body ran while dist/index.mjs loaded, at base and at head. 171 modules evaluate at both, in the same order.
    • The CommonJS entry. A Module._load log for dist/index.js gives 127 first loads at both, in the same order.
    • plugin-auth's own modules. A walk of the bundle's top level and its eager init_* calls finds three that run later at both entries, whose last value import from auth-manager.ts left with the body: auth-schema-config.ts, admin-impersonate-endpoint.ts and impersonation-bearer-rotation.ts (which admin-impersonate-endpoint.ts imports). At the CommonJS entry platform-admin-gate.ts also moves, by two places: it ran two modules before principal-session-mint.ts at base and runs right after it at head; principal-session-mint.ts imports judgeHeldSession from it and reads it inside a function only. The top levels of all five are imports, types, constants and function declarations (principal-session-mint.ts adds one extends Error class), with no load-time call, apart from admin-impersonate-endpoint.ts's VENDOR_ROLE_SCALAR_ADMIN_PATHS, a new Set of string literals (:191). No importer reads any of their bindings at load time: all 41 references in the 16 files that import one of these five or the new module (auth-manager.ts among them) are inside functions, and index.ts re-exports platform-admin-gate.ts and auth-schema-config.ts with export *, which reads nothing.
    • The new module's own top level is imports, one interface and one function declaration. Its value import of auth-manager.ts, for the seven declarations, and auth-manager.ts's import of it form a cycle with nothing read at load time, as cut 14's and cut 15's do.

    (c) Path readers. Two readers outside the move follow it, in the same commit, and one test's comments name it:

    • better-auth-schema-parity.test.ts and managed-extension-fields.test.ts: each one's authManagerPluginFactories() scans a list of files for const { X } = await import('better-auth/plugins…' | '@better-auth/…'), and both lists gain auth-manager-plugin-list.ts (:324 and :634). The body holds 13 such imports, all of auth-manager.ts's (13 → 0). At base the scan finds 16 factories; at head without the new entry it would find 3 (organization from the cut 15 module and the two in auth-manager-actor-guards.ts), missing 13, which each test's stale-entry check fails on; with it the scan finds the same 16 as base.
    • auth-email-locale.test.ts: the two population comments (the docblock at :863 and the in-test note at :1130, head numbering) that name the files the five sendTemplate sites live in now name auth-manager-plugin-list.ts (the body of buildPluginList) among them; the magic-link send moved with the body, and auth-manager.ts keeps one. No assertion, count or pattern changes.
    • No anchor entry; 19 letters leave the anchored surface, disclosed. By the gate's count the body carries 19 decision-letter citations, at these sites: ADR-0057 D4 (:2532, :2569, :2594), ADR-0068 D1/D2 (:2526, :2555), ADR-0068 D2 (:2032, :2595, :2603), ADR-0069 D1 (:2007), D2 (:2001), D3 (:2046) and D4 (:2643), ADR-0090 D3 (:2526, :2555) and D5 (:2533), ADR-0131 D7 (:2196) and ADR-0135 D6 (:1913, :2404, :2435). They leave auth-manager.ts, which is anchored, for a module that is not, so the count drops by 19 while the gate stays green and every citation still resolves. The auth-manager.ts entry carries two paragraphs: ADR-0093 D2 (the membership reconciler, which stays) and ADR-0108 (the organization roles map, whose mechanism cut 15 moved to auth-manager-organization-plugin.ts, anchored by its own entry). Neither describes this body, so no entry is owed, per the seat's ruling after cut 5 (an entry would need new invariant text, which a move-only cut must not add; the drop is recorded in the ACCEPT), as cut 14 recorded its 8.
    • Read, and not edited:
      • api-error-code.pin.test.ts: the body carries no new APIError( site, so authManagerFiles is not owed the new module. Its sum stays 30, as at base.
      • scripts/check-dispatcher-error-vocabulary.mjs: the body carries no code-stamping site (no code: and no new APIError(), and the gate's only per-file plugin-auth key is liveMemberFile (auth-manager-before-hook.ts), which the cut does not touch. It exits 0 at both, 56 sites found and 56 classified at both, and its output is identical apart from its scanned-file count (3,444 → 3,445). Its --self-test runs inside the derived pnpm check:dispatcher-error-vocabulary.
      • The census: no census site moves: the body's engine calls are reads. check-tenant-audit-census exits 0 at both with no --write, with byte-identical output. Its --json document has 238 sites at both and 0 per-site differences with line numbers ignored. 2 sites are in auth-manager.ts at both, 0 are in the new module, and 0 are unresolved or unledgered at both. Only scannedSources (762 → 763) and measuredAt move, so neither census document is edited.
      • scripts/measure-durability-swallow-family.mjs (a measurement, not a gate): 77 members and 84 quiet rows at both, identical with line numbers ignored (one member, in observeFirstMemberships, which stays, moves 767 lines up with the cut's net: :3876 → :3109). Every statistic is the same apart from files (3,494 → 3,495): tryStatements 3,647 and droppedNotAwaited 230 at both.
      • check-auth-mount-ledger.mjs reads only the basePath declaration in auth-manager.ts, which stays.
      • auth-manager-base-path.test.ts reads auth-manager.ts for code lines carrying '/api/v1/auth'; the body has none.
      • rate-limit-storage-isolation.test.ts walks the import graph from rate-limit-storage.ts, which reaches neither file.
      • check-system-context-census.mjs tracks reads of ExecutionContext.isSystem; the body has none in code.
      • check-cross-package-test-inputs.mjs names auth-manager.ts in a self-test fixture only.
    • Prose that now points one file early, disclosed, not edited (cut 14's precedent: a comment that reads no file is not a path reader): scripts/publish-smoke.sh:1131 names auth-manager.ts for the admin: pluginConfig.admin ?? scimEffective line, which now sits in the module; the why of the deliverPhoneOtp row in packages/runtime/src/dispatcher-error-vocabulary.ts (:1359) says its two callers are sendOTP hooks in auth-manager.ts, which now sit in the module (the row's file field, which the gate reads, names auth-manager-email-sms-delivery.ts and is unchanged); and auth-manager-organization-plugin.ts's header describes the organization call in buildPluginList, which is now the module function's. If the seat wants any of them re-pointed, it is a comment edit in this PR.
    • tsup bundles the new module into the root entry (splitting: false). The dist has the same 18 files at base and at head, plus the two .build-input-hash stamps the build writes from its inputs. Only index.js, index.mjs and their maps differ.

    ⛔ Nothing else changes:

    • No method, field or top-level declaration moves besides the body. The body is not edited.
    • No stay-behind code is edited apart from the method's body (now the stub), the import lines in (b), and the export on the two declarations.
    • The only edits outside plugin-auth/src's two moved files are the readers in (c). No ADR, docs prose, baseline, anchor entry or eslint.config.mjs is edited.
    • The title goes under refactor(plugin-auth), with no !.
    • No changeset: skip-changeset, per ruling 6348891160 item 4.

    Anchor reading, on 10b8250ad5 and at the head: I mapped every auth-manager.ts#symbol anchor in the tracked tree, outside changelogs, with symbolSegmentResolution from scripts/symbol-anchors.mjs. There are 29, and all 29 are declaration-class at base, at base with the body blanked, and at head. Seven of their symbols occur as a token on a line of the body, and each one's declaration stays where it was: resolveScimEnabled (ADR-0134 :90, :127), reconcileScimUserLifecycle (:158, :263; the method, which stays per ruling A), assertScimAdminCoherence (:171), AuthManager (ADR-0135 :104, the class), resolveOidcProviderEnabled (the ai checklist area :737, identity-auth :1166), computeAuthGate (identity-auth :1718, the method) and twoFactor (identity-auth :1429, which resolves to getPublicConfig's features.twoFactor, :5301 at base, which stays). Each resolves to its declaration at all three readings.

    Container & model: L (series), card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier: "no path-derived mandate"), effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Basis for the Clause-② line: a move-only cut plus reader migrations widens no accept set and no public surface. The published surface is measured, not argued. The proof owed:

    • token identity and binding identity for the body against this base, each with a negative control;
    • ruling 6348891160 item 2's block: raw and normalised sha256 of dist/index.d.ts and dist/index.d.mts at base and at head, the normaliser verbatim, the raw diff line count, and the positive control;
    • ESM and CJS export lists at both, and that neither in-place export, buildPluginList nor PluginListHost is in either, nor in dist/index.d.ts;
    • plugin-auth suite counts at base and at head;
    • the census and the durability measurement at base and head;
    • the symbol-anchor checks and check:adr-anchors (with the decision-letter count) on the merge base and on the head;
    • the load order at base and at head;
    • every derived gate.

    Premise (measured on this base and at a2413e05ce):

    • 1 of 1 body token-identical (793 lines, 2,096 tokens) and 500 of 500 identifiers bound the same way, each proof with failing negative controls, as in (a).
    • pnpm --filter @objectstack/plugin-auth typecheck exits 0, and so does pnpm --filter @objectstack/runtime typecheck. eslint on the 5 changed files exits 0.
    • Suite: 191 files, 3,626 passed and 10 skipped at base and at head.
    • dist/index.d.ts and dist/index.d.mts are raw byte-identical at base and at head (0 diff lines). The ESM and CJS entries export the same 159 names at both; neither in-place export, buildPluginList nor PluginListHost is among them, and dist/index.d.ts names neither in-place export nor PluginListHost.
    • The census and the durability measurement read as stated in (c). The load order reads as stated under (b).
    • 61 derived gates run at head after a full build, and all exit 0 (--ran: a derived zero). check-governed-merges.mjs --branch HEAD: NOT governed, 1,774 changed lines.
    • affected-docs.mjs --json 10b8250ad5 (exit 0): 21 pages, all name matches of names the moved code carries (the PLATFORM_ADMIN, platform_admin, OS_AUTH_TWO_FACTOR and OS_SSO_DOMAIN_VERIFICATION literals, the plugin names phoneNumber, magicLink, oidcProvider and deviceAuthorization, and AuthManager and getMcpResourceUrl). One names auth-manager.ts: content/docs/kernel/services-checklist.mdx (:314), for the identity provider service as a whole, which still holds. The code that reads those names is unchanged, so no doc edit is owed. 7 of them are release notes, release-owned and not edited.
    • The reader controls in (c), the anchor map and the decision-letter readings above.

    A diff of another kind is a fresh fork on this card.

    Responsibility: n/a — not a defect card
    Thread-read: 6382750527
    Ruling-ref: 6348891160 (the .d.ts bar), 6364006498 (ruling A on this card)
    Serial constraints: re-read after the proof run, at 10:40Z. 12 PRs are open. One touches auth-manager.ts: #25621 (plugin-auth, the organization default currency), whose only hunk there is :1320–:1331, the docblock of acceptLanguageHeader and an export in place on it, a region this cut does not touch, and which the body does not read; git merge-tree merges this head with its head (40b9af0e49) cleanly. None of the others touches plugin-auth, the new module's path, a reader edited in (c) or scripts/adr-anchors/: #25623 (metadata-protocol), #25620 (cli, lint, rest, spec, docs), #25619 and #25618 (ADR records), #25617 and #25612 (spec, lint, docs), #25615 (objectql), #25603 (plugin-sharing, spec, docs), #25593 and #25588 (.claude/, scripts/pm/) and #25569 (cli, docs).


    Generated by Claude Code

  18. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25224,
    "status": "done",
    "branch": "claude/project-thread-lg3wrj",
    "pr": "#25624",
    "session": "session_01LSXBc2GCBwiAL7vV4kWkyr",
    "premise_still_valid": true,
    "summary": "Cut 16 of the auth-manager.ts split, the series' last, per the amended claim 6383249105 (amends 6379187398 on the file cut 15 left; landed note 6382750527), under Ruling-ref 6348891160 (the .d.ts bar) and 6364006498 (Sales's ruling A on this card). This is the eighth cut under ruling A and the second of the two buildPluginList cuts the seat set at 02:10Z (the organization factory in cut 15, then the rest of the method; the series ends after this cut), on the #23050 method shape cuts 1 to 11 and 14 used, with cut 10's get/set pair (#25508) for the two fields the body writes. Part of #25224, no closing keyword; the final report follows this PR's merge and the seat closes the card. Base origin/main 10b8250 (cut 15's f192123 plus #25600 and #25604), one commit a2413e0, +961/-813 over 5 files (1,774 changed lines). Moved: the whole body of buildPluginList (:1875, private async, annotated Promise of any[]; body :1876-:2666, 791 lines), unedited, into the new module packages/plugins/plugin-auth/src/auth-manager-plugin-list.ts (913 lines) as export async function buildPluginList(this: PluginListHost) returning Promise of any[], re-indented two spaces. The body, in the same order: the reset of the degraded-feature record and the JWT signing verdict; the enabled table with its env and config precedence and the SCIM coherence re-check; and every plugin in the same order (bearer, the organization factory's arrow whose body cut 15 moved, twoFactor, breached-password rejection, admin, phoneNumber, magicLink, the generic OAuth providers, the OIDC provider with its jwt plugin, sso, scim, device authorization, the custom session), then return plugins. The method keeps its place, docblock, private modifier, empty parameter list and return annotation; its body is now non-async: four reader closures and two writer closures, one host literal (18 late-bound method closures, getters for config and degradedFeatures, get/set pairs for jwtSigningFailureReported and orgRolesMap, alphabetical) and return buildPluginList.call(host). The seat's bars: (Body) 793 lines / 2,096 tokens token-identical, every line the base line minus two spaces, with two failing negative controls; async moved from the method to the function and the stub returns that promise, which createAuthInstance awaits as before; the same empty parameter list and the same return annotation on all three; no arguments, new.target, super or bare this, and no this under a non-arrow function (the only non-arrow function nodes are the organization arrow's getter and setter, which name no this). (Binding) a checker proof resolves all 500 identifiers the same way at both ends (338 body-local, 48 receiver members, 23 reads of 20 imported names, 10 to the same auth-manager.ts declaration: six AuthManagerOptions members read through this.config, 81 globals or untyped property names), none to an unexported auth-manager.ts top level or a module top level; the one read of the class is a type position (:1979); three failing negative controls. (Receiver) PluginListHost names exactly the 22 members the body reads, each typed AuthManager[x]: 18 methods as late-bound closures (reconcileScimUserLifecycle among them, which stays in the class per ruling A); config (reassigned by setRuntimeBaseUrl and applyConfigPatch; read 7 times) and degradedFeatures as readonly getters; jwtSigningFailureReported (reset once at :1881, never read) and orgRolesMap (read and written only through the organization arrow's reader and writer) as get/set pairs, not readonly, so reportJwtSigningFailure and the actor guards read back the same fields, written at the same moment. Function-valued read disclosed: this.degradedFeatures.clear() at :1876, on the manager's own Map. (Organization arrow) moves unedited and builds its host from this host, so each of its closures, its config getter and its orgRolesMap pair reach the same members and field at the same moment (cut 14's precedent for a moved body holding an earlier cut's stub). (In-place exports) the body reads seven auth-manager.ts top levels: readBooleanEnv (4 reads) and consentOrganizationOf (1), unexported, and resolveScimEnabled, assertScimAdminCoherence, resolveOidcProviderEnabled, resolveDcrEnabled and OAUTH_ORGANIZATION_CLAIM, already exported. Per ruling A the two get export in place and the module imports all seven by value; the proof checks each head declaration is the base one plus export and that both reads resolve to it; index.ts names neither, so nothing new is published. No static read (the class is a type-only import). (Host lifetime) built each time buildPluginList is called, once per build; nothing captured early. (Readers) check-adr-anchors decision letters 1,872 on the merge base 10b8250 and 1,853 on the head: 19 drop, disclosed. They are the body's 19 citations (ADR-0057 D4 x3, ADR-0068 D1/D2 x2 and D2 x3, ADR-0069 D1 to D4, ADR-0090 D3 x2 and D5, ADR-0131 D7, ADR-0135 D6 x3), which leave auth-manager.ts, anchored, for the new module, which is not. No entry is owed per the seat's ruling after cut 5: the auth-manager.ts entry's two paragraphs (ADR-0093 D2, the membership reconciler, which stays; ADR-0108, the roles map, which cut 15 moved and anchored) do not describe this body, as cut 14 recorded its 8. The gate exits 0 at both and every citation resolves; anchored files 85 and citations 52,467 at both, files 6,589 to 6,590. Three path readers move in the same commit: better-auth-schema-parity.test.ts and managed-extension-fields.test.ts's factory-scan lists gain the module (the body holds 13 of the 16 factory imports: 16 at base, 3 without the new entry, 16 with it, the same set); auth-email-locale.test.ts's two population comments name the module. Read and not edited: api-error-code.pin.test.ts (the body has no new APIError site; sum 30), the vocabulary gate (56/56 at both; its only per-file plugin-auth key names auth-manager-before-hook.ts), the census (238 sites line-blind equal), the durability measurement (all statistics equal but files), the mount ledger, the base-path test, the rate-limit isolation walk, check-system-context-census and check-cross-package-test-inputs. Prose that now points one file early, disclosed and not edited (cut 14's precedent): publish-smoke.sh:1131, the deliverPhoneOtp row's why in packages/runtime/src/dispatcher-error-vocabulary.ts (:1359; its file field, which the gate reads, is unchanged), and auth-manager-organization-plugin.ts's header; re-pointing any of them is a comment edit in this PR if the seat wants it. Tenant census and symbol-anchor output byte-identical at base and head; full plugin-auth suite passes at both. Nine imported names leave auth-manager.ts with the body and cut 15's module import is replaced by one import of the new module. Evaluation order measured, not argued: 171 ES modules evaluate at both and 127 CJS first loads at both, each in the same order. Inside the bundle auth-schema-config.ts, admin-impersonate-endpoint.ts and impersonation-bearer-rotation.ts run later at both entries, and at the CJS entry platform-admin-gate.ts moves two places, to right after principal-session-mint.ts, which reads its judgeHeldSession inside a function only; their top levels make no load-time call apart from admin-impersonate-endpoint.ts's new Set of string literals (:191), and all 41 references in the 16 files importing them or the new module are inside functions. auth-manager.ts 6,556 to 5,789 lines. No sign-in, session or membership outcome changes.",
    "tests": "Base 10b8250, head a2413e0. Builds and suites ran through os-verify-lock, every exit code captured before any pipe. A full turbo run build ran first; then plugin-auth was built and tested at base, and built and tested at head, on the same dependency dists. (1) Move proof (TS scanner tokens, kind and text, template literals re-scanned at their substitutions): 1 of 1, 793 lines and 2,096 tokens, each line the base line minus two spaces. The stub is non-async with no parameter and keeps the docblock, private and the return annotation: the six readers and writers, one host literal whose keys are exactly PluginListHost's (alphabetical; 18 late-bound closures, the two getters, the two get/set pairs), then return buildPluginList.call(host); the interface types each member AuthManager[x] and exactly the two getter-only fields are readonly; the body writes exactly the two get/set fields. Negative controls: the module text changed in memory (plugins.push(bearer()); to plugins.push(bearer?.());) fails the token and line checks and the run exits 1; jwtSigningFailureReported left out of the declared get/set fields fails 4 checks and exits 1; the file is untouched. Binding proof (TypeScript checker over the base file, the head file and the module, the module's ./auth-manager.js resolved to the head file): 500 of 500 identifiers resolve the same way in order; the two in-place exports checked as the base declaration plus export. Negative controls: the admin-impersonate-endpoint import pointed elsewhere gives 1 difference and exit 1; the placeholder-email import shadowed by a module const gives 1 difference and exit 1; run without the in-place exports declared, it reports the 5 base reads of the two unexported declarations and exits 1. git diff --color-moved=plain --color-moved-ws=allow-indentation-change 10b8250 a2413e0 over the two source files marks 799 of 808 deleted and 820 of 954 added lines moved; the 9 unmoved deletions are 3 import lines, cut 15's 3 import comment lines, the first lines of the two declarations (now with export) and the method line (now without async). (2) Ruling 6348891160 item 2 block, identical at base and head. dist/index.d.ts: raw 1b3a48357d137155de33104277667a922aab62df73b418b03894c56faaacd0ba, normalised cee7303ccdfe72794efd588c85b628419d4253d730b5a0c5a3f1cdb8dddb4443. dist/index.d.mts: raw b5e8dc982b634f0a5108b1b533626a68767f2da5a56f72dba801059b149e2872, normalised 562d0a4ce656f97aeb937541482cdf531b61d7c75a005918784bf0a52d032dfc. Raw diff 0 lines, so there is no union swap (the same bytes cut 15 measured: a private method's body is not in the declarations). Normaliser: the ruling's one-liner, verbatim in the PR body. Positive control: head dist/index.d.ts with 'private buildPluginList;' made public (line 1829) hashes raw f8ff1470cbc34cd34a441c67b8763c5adde86fa408a4bcecf1590c60d30bce57 and normalised fe48a6a7f320e626e941a422a27571c28fbdc47a73bbbdaa0d5048d7307cae55; both differ. Export lists: ESM and CJS have 159 names each at both points, sorted-name hash 3abb126f5babb701 on all four; neither in-place export, buildPluginList nor PluginListHost is exported; dist/index.d.ts names neither in-place export nor PluginListHost, and its 7 buildPluginList lines are the private member and docblock text, the same 7 at both. The same 18 dist files at both, plus the two .build-input-hash stamps; only index.js, index.mjs and their maps differ. Load order: the ES-module evaluation log, the CJS first-load log and the bundle walk as in the summary, each at base and head from the built dist copied into the package and removed after. (3) pnpm exec vitest run --maxWorkers=3 in packages/plugins/plugin-auth: base Test Files 191 passed (191), Tests 3626 passed | 10 skipped (3636), exit 0; head the same counts, exit 0. pnpm --filter @objectstack/plugin-auth typecheck exit 0; pnpm --filter @objectstack/runtime typecheck exit 0; eslint over the 5 changed files exit 0. No tested behaviour changed, so no ablation is owed; the reader control (factory scan 3 without the new list entries) is the failing reading the migration prevents. (4) check-tenant-audit-census output byte-identical at base and head (exit 0, no --write); tenant-audit-census --json: 238 sites at both, 0 per-site differences line-blind, 2 in auth-manager.ts at both, 0 in the new module, 0 unresolved and 0 unledgered; only scannedSources (762 to 763) and measuredAt differ. measure-durability-swallow-family --json: 77 members (same with every line number ignored; the observeFirstMemberships member moves :3876 to :3109) and 84 quiet rows at both; files 3,494 to 3,495; tryStatements 3,647 and droppedNotAwaited 230 at both. (5) Anchors at base and head, run by hand, all exit 0, five with byte-identical output: check-adr-symbol-anchors (2,384 anchors, 466 symbol), check-scripts-symbol-anchors (4,070), check-spec-docblock-symbol-anchors (5,076), check-system-context-census (149 elevation read sites), check-platform-checklist (16 areas, 329 items); check-adr-anchors differs only in files (6,589 to 6,590) and letters (1,872 to 1,853), anchored files 85 and citations 52,467 at both. check-dispatcher-error-vocabulary: 56 found and 56 classified at both, scanned files 3,444 to 3,445, output otherwise identical. Anchor map: all 29 auth-manager.ts#symbol anchors outside changelogs are declaration-class at base, with the body blanked, and at head; seven of their symbols (resolveScimEnabled, reconcileScimUserLifecycle, assertScimAdminCoherence, AuthManager, resolveOidcProviderEnabled, computeAuthGate, twoFactor) are tokens on a body line and resolve to their declarations, which stay, at all three. (6) Merge check on origin/main cc10e74 (8 merges past the base: #25601, #25540, #25595, #25616, #25614, #25612, #25615, #25617; none touches plugin-auth, scripts/adr-anchors/ or a reader named above) with a2413e0 cherry-picked, no conflict. Move proof and binding proof against cc10e74: 1 of 1 (793 lines, 2,096 tokens, 0 failures) and 500 of 500, the two in-place exports checked as the base declaration plus export. check-tenant-audit-census, check-adr-anchors, check-adr-symbol-anchors, check-platform-checklist, check-system-context-census, check-auth-mount-ledger, check-dispatcher-error-vocabulary and check-scripts-symbol-anchors all exit 0. check-adr-anchors on cc10e74 alone: 85 anchored files, 52,481 citations across 6,594 files, 1,871 letters; with this cut: 85, 52,481 across 6,595, 1,852 (the same 19 drop; main's own count differs from the base's 1,872 through those eight merges, not through this cut). Durability: 77 members and 85 quiet rows at both, line-blind equal; files 3,497 to 3,498, tryStatements 3,647 and droppedNotAwaited 236 at both. The vocabulary gate: 56 found and 56 classified, 3,448 files scanned. The full plugin-auth suite on the merged tree: Test Files 191 passed (191), Tests 3626 passed | 10 skipped (3636), exit 0. The one open PR touching auth-manager.ts, #25621, still touches only :1320-:1331 there (acceptLanguageHeader), a region this cut does not touch, and git merge-tree merges the two heads cleanly.",
    "gates": "Local subset at head after the full build: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 61 commands for the 5 paths. All 61 ran, each exit code captured before any pipe, and all 61 exit 0. --ran over that record: 61 derived, 61 run, 0 NOT-MEASURED, a derived zero. dispatch-gates --tier: no path-derived mandate. node scripts/pm/check-governed-merges.mjs: on the trial file list before the cut and --branch HEAD after the commit both NOT governed (0 of 5 paths), 1,774 changed lines against the 3,000-line human-merge line. Docs drift (affected-docs --json 10b8250, exit 0): 21 pages, all name matches of literals and names the moved code carries; one names auth-manager.ts (services-checklist.mdx:314, the identity provider service as a whole, which still holds) and the code reading those names is unchanged, so no doc edit is owed; 7 are release notes, release-owned, not edited. CI on head a2413e0 converged: 46 check runs, 34 success and 12 skipped, 0 failed, 0 pending; the PR reads mergeable, clean, against 481ac62. No review and no review comment. The one issue comment is the docs-drift bot's (6383289667), which counts the same 21 pages as the local affected-docs run above (14 hand-written, all name matches, and 7 release-owned, read-only); no doc edit is owed, as stated in the PR body.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 GitHub MCP calls since cut 15's report (6379514849). GitHub REST reads through curl, reads only: the open PR list and each open PR's file list for the serial reads (twice, 10:40Z and 10:46Z), #25621's state and file patch, the issue and its comments, and this PR's check runs, reviews, comments and state. (claude-code-remote subscribe_pr_activity once on #25624, not a GitHub write.)",
    "api_writes": "3 relay acts since cut 15's report (scripts/pm/, each read back identical): 1) amended claim 6383249105 via post-stamped, run 40087064242, 26209 bytes; 2) pr_create #25624 via fleet-write, run 40087277465, 30658 bytes sent and stored, assignee os-sales, label skip-changeset; 3) this report via post-stamped. Plus git push of the branch (a new branch after the queue deleted the old one on #25594's merge; not REST). No PR body edit. No other write.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  19. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed cut 16 (buildPluginList body), the series' last cut · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-24T11:57Z

    reading value
    PR #25624 MERGED through the queue (ACCEPT 6383667720), head a2413e05ce
    origin/main carries abdc7f22a5

    State: the card stays open until the card thread posts the series' final report on it (auth-manager.ts from 10,338 lines to its final count, every cut PR, and each body that stays with its reason). The seat closes #25224 after that report.


    Generated by Claude Code

  20. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Final report · #25224 (split auth-manager.ts) · domain:services seat 1 (seat post #6021, project run by the Sales account) · 2026-10-24T11:58Z
    Session: session_01LSXBc2GCBwiAL7vV4kWkyr
    Account: os-sales
    Thread-read: 6383915250 (the landed note for cut 16, #25624, merged at 11:56Z as abdc7f22a5)

    The series is done: cut 16 (#25624) was its last cut, per the seat's call at 02:12Z on 2026-10-24 (the series ends after the two buildPluginList cuts; cut 17, applyConfigPatch, dropped so the PRs waiting on the file get it sooner). This report hands the card to the seat to close.

    Size

    point auth-manager.ts lines
    at the card's filing (123ff6f4e, 2026-10-22, as the card measured it) 10,338
    now (origin/main abdc7f22a5, after cut 16) 5,793
    change −4,545 (44%)

    The 16 modules the series split out hold 6,985 lines between them on abdc7f22a5. Each cut was move-only, one area per PR, strictly serial, under 3,000 changed lines, with every path reader migrated in the same PR; no gate went empty and none was added.

    Every cut PR

    cut PR merged as area new module (lines now) auth-manager.ts before → after
    1 #25273 49185cdb8a the actor and organization guards auth-manager-actor-guards.ts (521) 10,349 → 10,004
    2 #25307 9dc1f42d50 the sign-in lockout path auth-manager-sign-in-lockout.ts (265) 10,034 → 9,927
    3 #25362 fb63d396b1 the password, auth-gate and session policy auth-manager-password-session-policy.ts (661) 9,932 → 9,592
    4 #25383 ca381a8a19 the database hooks and the identity-source stamp auth-manager-database-hooks.ts (493) 9,592 → 9,212
    5 #25395 eca6b61922 the email and SMS delivery auth-manager-email-sms-delivery.ts (435) 9,212 → 8,968
    6 #25400 860f002f8b the plugin assembly helpers auth-manager-plugin-assembly.ts (165) 9,023 → 8,971
    7 #25404 f0fdd45bbc the database and auth secret inputs auth-manager-database-secret.ts (176) 8,971 → 8,909
    8 #25423 70ff9983c9 the audience gate and membership admission auth-manager-membership-admission.ts (273) 8,909 → 8,790
    9 #25461 05cc3ee953 the MCP access-token verifier auth-manager-mcp-access-token.ts (194) 8,790 → 8,711
    10 #25508 7d3c0139fb the request routing auth-manager-request-routing.ts (318) 8,711 → 8,521
    11 #25524 794b769ca3 the WebContainer password hasher auth-manager-password-hasher.ts (75) 8,561 → 8,536
    12 #25536 93a6ba4136 better-auth's global after-hook auth-manager-after-hook.ts (256) 8,536 → 8,362
    13 #25548 19d3720af1 better-auth's global before-hook auth-manager-before-hook.ts (696) 8,362 → 7,800
    14 #25566 35d5060380 the createAuthInstance body auth-manager-auth-instance.ts (861) 7,807 → 7,122
    15 #25594 f1921230b2 the organization plugin's factory in buildPluginList auth-manager-organization-plugin.ts (683) 7,122 → 6,556
    16 #25624 abdc7f22a5 the rest of buildPluginList, its whole body auth-manager-plugin-list.ts (913) 6,560 → 5,793

    The before and after counts are each merge's own parent and result. The gaps between rows are the other PRs that changed the file during the series: #25238 (+11), #25303 (+30), #25309 (+5), #25398 (+2), #25399 (+53), #25515 (+40), #25541 (+7), #25621 (+4), +152 lines in all. One more PR on this card is not a cut: #25326 (67ab712bd6) dropped AuthManager's 12 counted query-options any assertions and ratcheted the erasure baseline, with no line moved.

    The rulings the series ran under: 6348891160 (the .d.ts bar, on #25222, covering this card) and, from cut 9 on, Sales's ruling A, 6364006498 (src/index.ts names its auth-manager.ts exports one by one; a helper a moved body reads gets export in place in the cut that first reads it; every cut proves the .d.ts raw bytes and the ESM and CJS export lists identical; the private-static readers and reconcileScimUserLifecycle stay in the class).

    What stays, and why

    Measured on abdc7f22a5 (body lines, braces excluded):

    what size why it stays
    the leading section (types, AuthManagerOptions, exported helpers and constants) 1,451 lines Moving the exported declarations would change the bundled .d.ts bytes, which is outside ruling A's terms. The seat ruled them out of scope for this series at 02:12Z, with no needs_decision. Its unexported functions are scattered among the exported ones.
    getPublicConfig 204 It is public with an inferred return type, so a stub would make the published declaration depend on the moved function's inference. The seat's rule keeps such a method whole.
    the 9 private-static readers (hasPendingInvitationFor, hasExistingUserFor, observeFirstMemberships, persistSelfRegistrationGrantClaim, settleDeferredSelfRegistrationGrant, isBootstrapCreation and the three prune*) 186 They read AuthManager's private static fields, which a module function cannot read (TS2341). Ruling A keeps them in the class.
    reconcileScimUserLifecycle 43 Ruling A names it to stay.
    the constructor 48 It initialises the instance; there is no .call stub for a constructor.
    69 bodies under 20 lines 224 A method-move stub and its host run about 5 + N lines, as long as the body, so moving them would not shrink the file.
    applyConfigPatch 69 Movable; the seat dropped it (cut 17) so #25529 and #25495, which wait for this series to leave the file, get it sooner.
    6 bodies of 20 to 39 lines (sendExistingAccountSignUpNotice, settleSelfRegistrationGrant, ssoDiscoveryTrustedOrigins, runSubjectErasureAtomically, externalDirectoryDoorDeps, getOrCreateAuth) 162 Each sits in a different area, so each would be its own cut for 20 to 30 lines; the series ended at the seat's call.

    The class also holds 38 fields and the 54 method stubs the cuts left (483 body lines), each handing its moved body the members it reads.

    Disclosures carried by the cuts

    • Where a cut's decision letters left the anchored surface for an unanchored module, the cut disclosed the drop and the seat recorded it in its ACCEPT, per its ruling after cut 5. Cut 4 and cut 15 added anchor entries instead, because their bodies are the mechanism an existing entry's paragraph describes.
    • Comments that now point one file early were left as they are, per cut 14's precedent (a comment that reads no file is not a path reader); cut 16's PR lists its three.

    State

    All 16 cut PRs are merged. Nothing on this card is in flight. The card can close.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedenhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions