Skip to content

Commit f192123

Browse files
refactor(plugin-auth): move the organization plugin factory into auth-manager-organization-plugin.ts (#25224 cut 15) (#25594)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuMjbPDrsigsyqoKsV4WZQMz)_ Part of #25224 Clause-②: no Metadata-protocol: none Ruling-ref: 6348891160, 6364006498 The basis for the `Clause-②: no` line: this is a move plus reader migrations and one anchor entry. No accept set, route, refusal, code or answer changes, and the published surface is measured unchanged: - `@objectstack/plugin-auth`'s `dist/index.d.ts` and `dist/index.d.mts` are raw byte-identical to base (0 diff lines, so the union-order normalisation has nothing to do). The positive control is the one ruling `6348891160` asks for. - Its ESM and CJS entries export the same 159 names at base and at head. The eight declarations this cut exports in place inside the package, `buildOrganizationPlugin` and `OrganizationPluginHost` are not among them. The `.d.ts` bar is ruling `6348891160` item 1 (on #25222, covering this card). Ruling `6364006498` is Sales's ruling A on this card. This is the seventh cut it applies to. Cut 15 of the `auth-manager.ts` split: **the `organization` plugin's factory inside `buildPluginList`**. It is the first of the two `buildPluginList` cuts the seat set at 02:10Z (this factory, then the rest of the method); the series ends after cut 16, and #25224 stays open for it. This follows the amended claim `6379187398`, which amends `6377759458` on the file cut 14 left (#25566, landed note `6378561924`). The shape is the closure move cuts 12 and 13 used (a factory arrow's body into a module function called on a host), with cut 10's get/set pair (#25508) for the one field the body writes. 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 card relation is this body's first line. `auth-manager.ts` goes from 7,122 to 6,556 lines. The cut is +734 / −608 over 7 files: 1,342 changed lines, under the 3,000-line line. Four of the seven files are path 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: NOT governed (0 of 7 paths hit the register), and `--branch HEAD` agrees. One commit: `361581a546` `refactor(plugin-auth): move the organization plugin factory into auth-manager-organization-plugin.ts`. ## The move | moved | base lines | proof lines | tokens | members it reads | |---|---|---:|---:|---:| | the body of the `organization` factory arrow in `buildPluginList` | `:1991`–`:2559` | 571 | 1,854 | 8 methods, 2 fields | The lines are base `ba9e0c3749`. The 571 proof lines are the body with its braces (the arrow's `{` on `:1990` to its `}` on `:2560`). - **What it is:** `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)`. - **What the body does, in order** (all of it moves, with its comments): 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. - **Where it goes:** a new module, `packages/plugins/plugin-auth/src/auth-manager-organization-plugin.ts` (683 lines). The body becomes the body of `export async function buildOrganizationPlugin(this: OrganizationPluginHost)`, re-indented four spaces (two levels) and otherwise unedited. The module header gives the reasoning, in the form the fourteen earlier `auth-manager-*.ts` modules use. - **What stays:** `buildPluginList` keeps its place, its docblock, its signature and every other line. The `organization` call keeps its place in it, and its arrow is now: ```ts // [#25224 cut 15] The body of this factory moved to // `auth-manager-organization-plugin.ts`; this arrow hands it the members // it reads (`OrganizationPluginHost`). await this.addOptionalPlugin(plugins, 'organization', () => { const readConfig = () => this.config; const readOrgRolesMap = () => this.orgRolesMap; const writeOrgRolesMap = (v: AuthManager['orgRolesMap']) => { this.orgRolesMap = v; }; const host: OrganizationPluginHost = { get config() { return readConfig(); }, emailLocaleArg: (...a) => this.emailLocaleArg(...a), // … one closure per method, alphabetical, 8 in all, and: get orgRolesMap() { return readOrgRolesMap(); }, set orgRolesMap(v) { writeOrgRolesMap(v); }, }; return buildOrganizationPlugin.call(host); }); ``` ## The seat's bars **Body.** - The body is the same TypeScript token sequence at base and in the module function: 571 lines with its braces, 1,854 tokens. Each of the 569 lines between the braces is the base line minus exactly four spaces. The closing brace is the one line that moves further: at base the body sat at the arrow line's own column, so its `}` was at column 6, and in the module it is at column 0. - **`async` moves from the arrow to the function.** The arrow is not `async`, and 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. - **No parameter, no annotation, no captured local.** 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). **Receiver.** - The body's `this` was the `AuthManager` instance, the arrow's lexical receiver. It is now the host. - `OrganizationPluginHost` names exactly the 10 members the body reads, no more, each typed as the class member (`AuthManager['x']`). - 8 are methods, so each goes as a late-bound closure, `(...a) => this.x(...a)`: `emailLocaleArg`, `getAppName`, `getConsolePageUrl`, `getDataEngine`, `getEmailService`, `multiOrgPostureEffective`, `resolveMembershipRole` and `storedRecipientLocale`. The method runs on the manager and is looked up when the body calls it, 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. - Two are fields: - `config`, which the class reassigns after construction (`setRuntimeBaseUrl` and `applyConfigPatch`), goes as a getter over a reader closure, `readonly` on the interface. The body reads it 9 times and never writes it. - **`orgRolesMap` goes as a get/set pair**, the shape the seat set for a moved body that writes a field (cut 10, #25508, `routeOwnership` and `routeOwnershipFor`). The body writes it once (`:2050`, `this.orgRolesMap = customOrgRoles`) and never reads it. The setter writes the manager's own field through the writer closure, so the field the actor guards read back (`assertRemoveMemberPermitted` and `assertOrganizationDeletable`, through their own getters) is the same field, written at the same moment, as before. It is not `readonly` on the interface. - **Function-valued reads, disclosed** (the way #25516 disclosed `resolveKernelService`): the body reads five function-valued members off `this.config`. It calls `this.config.invitationPlacement?.()` (twice, `:2245` and `:2290`) and `this.config.logger.warn(msg, meta)` (`:2487`) on their own objects; it reads `onOrganizationCreated` (`:2178`) and `onInvitationAccepted` (`:2307`) into a local `cb` and calls that; and it hands `getI18n` on as a value (`:2485`). Each is read off the object the getter returns, the manager's current `config`, at the same moment as before, so every call has the same callee and the same `this`. - The body reads no `AuthManager.X`, so the module imports the class as a type only. **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` and `ORGANIZATION_SLUG_LOCKED_CODE`; - the slug guard's and the placement check's read-fault answers, `slugGuardReadFaultApiError` and `invitationPlacementReadFaultApiError`; - the placement gate's refusal test, `isInvitationPlacementGateRefusal`; - `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 new is published. `readInvitationPlacementIntent` was exported and published at base, and is in both export lists and in `dist/index.d.ts` at both ends, unchanged. **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 are read on the merge base and on the head below: none drop, and the new module is anchored by the entry under Path readers. Tenant census and symbol-anchor output are byte-identical at both ends, and the full plugin-auth suite passes at both ends. The four path readers whose text or lists name where the moved lines live are migrated in this commit. ## Imports and evaluation order 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`); - `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. esbuild drops a type-only import, so a module whose last value import leaves `auth-manager.ts` can now be evaluated later. The order was measured rather than argued: - **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. ## The decision-letter layer: 0 drop `check-adr-anchors`, measured on the merge base and on the head. The gate exits 0 at both. | point | anchored files | citations | files | decision letters | |---|---:|---:|---:|---:| | merge base `ba9e0c3749` | 84 | 52,305 | 6,698 | 1,939 | | head `361581a546` | 85 | 52,307 | 6,700 | 1,939 | - The body's letters leave `auth-manager.ts`, which is anchored, for the new module, which the new entry below anchors, so the count does not move. - The two new citations are the entry's two `ADR-0108` mentions; the two new files are the module and the entry. ## Path readers Four readers outside the move follow it, in this commit, and one anchor entry is added: - **`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 tenant-audit 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. It passes at head. - **`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. - **Every symbol anchor into the file.** Before the cut, every `auth-manager.ts#symbol` anchor in the tracked tree outside changelogs was mapped 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. ## Proof Builds and suites ran through `os-verify-lock`, and every exit code was captured before any pipe. Base `ba9e0c3749`. A full `turbo run build` ran first. Then `plugin-auth` was built and its suite run at base, and built and run again at head, on the same dependency dists. **1. The moved text.** - **Token identity:** the base arrow body and the module function body are the same TypeScript token sequence (scanner tokens, kind and text, trivia skipped, each template literal re-scanned at its substitutions as the parser does): 571 lines and 1,854 tokens, each line between the braces the base line minus four spaces. - **The stub's shape**, checked by the same run: the arrow is not `async` and takes no parameter; it holds the two reader closures and the writer closure, one host literal whose keys are exactly `OrganizationPluginHost`'s, alphabetical, whose 8 method values are each `(...a) => this.<key>(...a)`, whose `config` is a getter over its reader and whose `orgRolesMap` is a getter over its reader and a setter over the writer, then `return buildOrganizationPlugin.call(host)`, and nothing else. The interface types every member `AuthManager['x']`, and exactly the getter-only field is `readonly`. `buildPluginList` outside the arrow is byte-identical apart from the three comment lines. - **Binding identity:** a second proof walks all 496 identifiers of the body at base and in the module, in order, and resolves each with the TypeScript checker (the module's `./auth-manager.js` resolved to the head file, where the in-place exports live). All 496 resolve the same way at both ends: - 324 to a declaration inside the body, the same declaring identifier by position; - 18 to receiver members (`AuthManager` at base, `OrganizationPluginHost` at head); - 31 reads of 29 imported names, with the same specifier and imported name, the nine `./auth-manager.js` names 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; - 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:** each proof reads the module text, and each control changes it in memory only; the file is untouched. - Token proof: `organization(organizationOptions)` changed to `organization?.(organizationOptions)` fails the token and line checks, and the run exits 1. - Binding proof: the module's `membership-limit` import pointed at another specifier gives 1 difference, and the run exits 1. The `placeholder-email` import shadowed by a module-level `const` gives 1 difference and reports the module top-level read, and exits 1. Run without being told about the in-place exports, it reports the base reads of the eight unexported declarations, and exits 1. - **An independent reading:** `git diff --color-moved=plain --color-moved-ws=allow-indentation-change ba9e0c3 361581a` over the two source files marks 577 of the 600 deleted lines and 580 of the 717 added lines as moved. - The 23 unmoved deletions are 14 import lines (the import names and block closers listed under Imports, and the `./objectql-adapter.js` line, rewritten without `withSystemContext`), the first lines of the eight declarations (now with `export`) and the arrow's own line (now without `async`). - The other added lines are the module header, its import lines, the host interface and its docblock, the function signature and docblock, the stub's comment, readers, writer, host literal and `return`, the eight `export` lines, the rewritten `./objectql-adapter.js` line, and the new import comment and line. **2. `.d.ts` and exports (ruling `6348891160` items 1 and 2).** | file | base and head | raw diff lines | |---|---|---:| | `dist/index.d.ts` raw | `1b3a48357d137155de33104277667a922aab62df73b418b03894c56faaacd0ba` | 0 | | `dist/index.d.ts` normalised | `cee7303ccdfe72794efd588c85b628419d4253d730b5a0c5a3f1cdb8dddb4443` | | | `dist/index.d.mts` raw | `b5e8dc982b634f0a5108b1b533626a68767f2da5a56f72dba801059b149e2872` | 0 | | `dist/index.d.mts` normalised | `562d0a4ce656f97aeb937541482cdf531b61d7c75a005918784bf0a52d032dfc` | | - The normaliser, verbatim (the ruling's one-liner): `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`. - The raw files are byte-identical, so no union-order swap occurs: items 1(b) and 1(d) hold with nothing to account for. - **Positive control:** the head `dist/index.d.ts` with one private member made public (`private buildPluginList;` → `buildPluginList;`, line 1829, a one-line change) hashes raw `f8ff1470cbc34cd34a441c67b8763c5adde86fa408a4bcecf1590c60d30bce57` and normalised `fe48a6a7f320e626e941a422a27571c28fbdc47a73bbbdaa0d5048d7307cae55`. Both differ from the readings above. - **Export lists:** ESM `dist/index.mjs` and CJS `dist/index.js` each have 159 names at base and at head, the same list (the sha256 of the sorted names is `3abb126f5babb701…` on all four). None of the eight declarations exported in place, `buildOrganizationPlugin` or `OrganizationPluginHost` is among them, and `dist/index.d.ts` names none of them. - **The bundle:** `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. **3. Tests and lint.** - `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` (both `tsc` projects and `check:test-typecheck`): exit 0. `pnpm --filter @objectstack/runtime typecheck`: exit 0. - `eslint` over the 6 TypeScript files: exit 0. **4. The censuses, the measurement and the anchors, at base and at head.** - The tenant-audit census, as above: byte-identical gate output, 238 sites at both, 0 per-site differences line-blind, 0 unresolved and 0 unledgered at both. - The durability measurement, as above, with its one moved statistic traced. - These checkers were run by hand at base and at head, because `dispatch-gates` does not derive the ADR one. All six exit 0 at both. Five print byte-identical output at both (`cmp` clean): - `check-adr-symbol-anchors`: 2,384 anchors across 142 records, 466 symbol (440 declaration, 26 literal); - `check-scripts-symbol-anchors`: 4,070 anchors; - `check-spec-docblock-symbol-anchors`: 5,076 anchors; - `check-system-context-census`: 149 elevation read sites; - `check-platform-checklist`: 16 areas, 329 items. The sixth, `check-adr-anchors`, differs only in the anchored-file, citation and file counts, as in the table above, with the decision letters equal. **5. Gates.** - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 71 commands for the 7 paths. All 71 ran at head after the full build, and all 71 exited 0. - `--ran` over the record with each exit code: 71 derived, 71 run, 0 NOT-MEASURED, a derived zero. `--tier`: no path-derived mandate. - `check-governed-merges.mjs`: on the trial file list before the cut and `--branch HEAD` after the commit, both read NOT governed (0 of 7 paths hit the register), 1,342 changed lines. - **Docs drift** (`node scripts/docs-audit/affected-docs.mjs --json ba9e0c3`, 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 the 41 are release-owned (the release notes and `releases/implementation-status.mdx`) and not edited. - **Serial constraints** (in the claim, and re-read at 03:13Z before this PR): 8 PRs are open, and none touches `auth-manager.ts`, the new module's path, a reader edited above or `scripts/adr-anchors/`. The Version Packages PR #24876 touches `plugin-auth`'s `package.json`, its `CHANGELOG.md` and changesets only. `origin/main` has moved 8 merges past this base, to `e073455929`; none touches `plugin-auth`, `scripts/adr-anchors/` or a reader named above. ## Changeset None: `skip-changeset`, per ruling `6348891160` item 4. The published declarations and export lists are unchanged, and the bundle's bytes move only because code moved inside it. --- _Generated by [Claude Code](https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr Co-authored-by: Claude <noreply@anthropic.com>
1 parent e57a4c3 commit f192123

7 files changed

Lines changed: 734 additions & 608 deletions

‎packages/plugins/plugin-auth/src/api-error-code.pin.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ describe('every new APIError( in plugin-auth/src carries an ADR-0112 code (#2424
182182
'auth-manager-password-session-policy.ts',
183183
'auth-manager-email-sms-delivery.ts',
184184
'auth-manager-before-hook.ts',
185+
'auth-manager-organization-plugin.ts',
185186
];
186187
expect(sites.length).toBeGreaterThanOrEqual(30);
187188
expect(new Set(sites.map((s) => s.file)).size).toBeGreaterThanOrEqual(4);

‎packages/plugins/plugin-auth/src/auth-email-locale.test.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -859,8 +859,9 @@ describe("#14641 — an invitation reads the INVITEE's own sys_user.locale", ()
859859
* ⚠️ POPULATION of the class claim these blocks close, stated rather than
860860
* implied: the auth mail surface is the five `sendTemplate` call sites in
861861
* `auth-manager.ts`, `auth-manager-auth-instance.ts` (the body of
862-
* `createAuthInstance`) and `auth-manager-email-sms-delivery.ts` (the body of
863-
* the change-email notice) — `IEmailService` declares exactly two delivering
862+
* `createAuthInstance`), `auth-manager-organization-plugin.ts` (the body of
863+
* the organization plugin's factory) and `auth-manager-email-sms-delivery.ts`
864+
* (the body of the change-email notice) — `IEmailService` declares exactly two delivering
864865
* members (`send`, `sendTemplate`), plugin-auth's only `.send(` calls are the
865866
* SMS service's, and no other file under `plugin-auth/src` calls either.
866867
* Those five are the same five `driveAllFive` above drives and `TEMPLATES`
@@ -1124,8 +1125,8 @@ describe('#15106 — the CLASS claim: all five auth sends read the recipient col
11241125

11251126
it('every one of the five sends is written in the stored language of its own recipient', async () => {
11261127
// ⚠️ POPULATION: the five `sendTemplate` sites in `auth-manager.ts`,
1127-
// `auth-manager-auth-instance.ts` and `auth-manager-email-sms-delivery.ts`
1128-
// — the same five `driveAllFive` drives and `TEMPLATES` names, and the whole of
1128+
// `auth-manager-auth-instance.ts`, `auth-manager-organization-plugin.ts` and
1129+
// `auth-manager-email-sms-delivery.ts` — the same five `driveAllFive` drives and `TEMPLATES` names, and the whole of
11291130
// plugin-auth's mail surface (see this section's header). This is the pin
11301131
// that closes the class; the per-send pins above only cover one member.
11311132
//

‎packages/plugins/plugin-auth/src/auth-manager-organization-plugin.ts‎

Lines changed: 683 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/plugins/plugin-auth/src/auth-manager.ts‎

Lines changed: 34 additions & 600 deletions
Large diffs are not rendered by default.

‎packages/plugins/plugin-auth/src/better-auth-schema-parity.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,8 @@ function betterAuthPluginSet(): unknown[] {
320320
*/
321321
function authManagerPluginFactories(): string[] {
322322
// [#25224] The auth manager's source: `auth-manager.ts` and the method
323-
// bodies moved out of it whole. A factory imported in either is one it loads.
324-
const source = ['auth-manager.ts', 'auth-manager-actor-guards.ts']
323+
// bodies moved out of it whole. A factory imported in any of them is one it loads.
324+
const source = ['auth-manager.ts', 'auth-manager-actor-guards.ts', 'auth-manager-organization-plugin.ts']
325325
.map((file) => readFileSync(join(PKG, 'src', file), 'utf8'))
326326
.join('\n');
327327
const found = new Set<string>();

‎packages/plugins/plugin-auth/src/managed-extension-fields.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,8 +630,8 @@ function findCollisions(
630630
*/
631631
function authManagerPluginFactories(): string[] {
632632
// [#25224] The auth manager's source: `auth-manager.ts` and the method
633-
// bodies moved out of it whole. A factory imported in either is one it loads.
634-
const source = ['auth-manager.ts', 'auth-manager-actor-guards.ts']
633+
// bodies moved out of it whole. A factory imported in any of them is one it loads.
634+
const source = ['auth-manager.ts', 'auth-manager-actor-guards.ts', 'auth-manager-organization-plugin.ts']
635635
.map((file) => readFileSync(join(HERE, file), 'utf8'))
636636
.join('\n');
637637
const found = new Set<string>();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"file": "packages/plugins/plugin-auth/src/auth-manager-organization-plugin.ts",
3+
"adrs": [
4+
"ADR-0108"
5+
],
6+
"invariant": "ADR-0108 — better-auth's organization roles map registers the closed framework vocabulary ONLY. App-declared `position` / `permission` names are not organization roles — registering one makes it storable in `sys_member.role`, which `resolve-authz-context` projects into `current_user.positions`."
7+
}

0 commit comments

Comments
 (0)