From f1517dca26a1f8b0c0d9d8b3689b9e1a58040b14 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 13:16:57 +0000 Subject: [PATCH] docs(plugin-auth): re-measure the version-stamped vendor attestations against the installed 1.7.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `^1.7.1` bump (#3002) moved the better-auth family past the versions 20 comment attestations in `plugin-auth/src` named, turning "verified against X" into claims nobody had checked. Each site below was re-measured against the INSTALLED packages (better-auth 1.7.1, @better-auth/sso 1.7.1, @better-auth/oauth-provider 1.7.1, @better-auth/core 1.7.1) on 2026-08-20 and re-stamped with the version, the date, and the shipped file:line it was read out of, so the next reader can falsify it with one grep instead of trusting it. Not a find-and-replace: 12 further hits are left exactly as they are, because they are deliberately historical (`admin-user-endpoints.ts:70`'s rc.2/stable `accountId` contrast, the `auth-schema-config.ts` rename narratives) or are already-corrected #10072 sites naming the stamp they superseded. Two sites needed more than a new number: - `objectql-adapter.ts` called the dependency a **prerelease**. 1.7.1 is stable, so that half of the reason is deleted rather than re-stamped; the half that still carries the argument (a vendor `dist` internal, invisible to any published type, not covering the raw-adapter path) is kept and re-anchored at the corrected lines `:127,146,602,615`. - `auth-manager.ts` cited `@better-auth/core/src/types/init-options.ts:946-971` — a `src/` path the package does not publish, so no reader could check it. Re-pointed at the shipped `dist/types/init-options.d.mts:835-857`. `auth-schema-config.ts`'s `ssoProvider` mapping carried a field-surface claim about `@better-auth/sso@1.6.20` that had never been measured. Measured now by resolving the model the way the adapter does over `getAuthTables({ plugins: [sso()] })`: all 7 declared fields (8 with `domainVerification`) resolve to columns `sys_sso_provider` declares. Recorded with the gap that makes it worth re-checking — this mapping has no parity gate, because `better-auth-schema-parity.test.ts` deliberately passes no `sso` plugin. Comment-only: no assertion, pin or gate input is touched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx --- .../plugin-auth/src/adopt-membership.ts | 8 ++- .../plugins/plugin-auth/src/auth-manager.ts | 35 +++++++++---- .../plugin-auth/src/auth-schema-config.ts | 26 +++++++++- .../change-email-delete-user-wiring.test.ts | 6 ++- .../src/impersonation-bearer-rotation.ts | 3 +- .../src/managed-extension-fields.test.ts | 14 +++-- .../src/member-role-canonical.test.ts | 14 ++--- .../plugin-auth/src/member-role-canonical.ts | 8 +-- .../plugin-auth/src/objectql-adapter.ts | 16 ++++-- .../src/register-sso-provider.test.ts | 5 +- .../plugin-auth/src/register-sso-provider.ts | 15 ++++-- .../remove-member-permission-guard.test.ts | 8 +-- .../src/remove-member-permission-guard.ts | 9 +++- .../plugin-auth/src/session-tombstone.test.ts | 13 +++-- .../plugin-auth/src/session-tombstone.ts | 52 +++++++++++++------ .../src/sso-client-secret-at-rest.test.ts | 5 +- .../plugin-auth/src/sso-client-secret.ts | 15 ++++-- 17 files changed, 184 insertions(+), 68 deletions(-) diff --git a/packages/plugins/plugin-auth/src/adopt-membership.ts b/packages/plugins/plugin-auth/src/adopt-membership.ts index 569abbc76a..93eb05eb08 100644 --- a/packages/plugins/plugin-auth/src/adopt-membership.ts +++ b/packages/plugins/plugin-auth/src/adopt-membership.ts @@ -36,8 +36,12 @@ * ## Why the seam is HERE, at the better-auth → ObjectQL adapter * * The three hook seams the framework already owns on this route all run at the - * wrong moment or with the wrong reach, verified against better-auth - * `1.7.0-rc.2`: + * wrong moment or with the wrong reach. Measured 2026-08-20 against the + * installed better-auth `1.7.1`, whose + * `dist/plugins/organization/routes/crud-invites.mjs` still calls + * `adapter.createMember(...)` unconditionally at `:324` and, in the `.catch` + * at `:332-339`, rolls the invitation back to `"pending"` and rethrows — + * `beforeAcceptInvitation` fires ahead of all of it at `:280`: * * - `organizationHooks.beforeAcceptInvitation` fires *before* `createMember` * and can only throw or mutate. The one mutation that would make the insert diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index 8cffac5b87..29ecdc4224 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -1081,15 +1081,20 @@ export class AuthManager { // one adds only the notice. So `sendChangeEmailConfirmation` stays // absent above — and that is a measured decision, not an omission: // - // `user.changeEmail` in better-auth 1.7.0-rc.2 declares EXACTLY three - // members (`@better-auth/core/src/types/init-options.ts:946-971`): - // `enabled`, `sendChangeEmailConfirmation`, and - // `updateEmailWithoutVerification`. There is NO notify-only hook, and - // `sendChangeEmailConfirmation` is not one: in `update-user.mjs:457` - // it becomes `canSendConfirmation`, and the branch at :495 RETURNS - // right after invoking it — the new address is never mailed until the - // old one clicks. Setting it is therefore structurally the approval - // gate the ruling refuses, not a way to notify. + // `user.changeEmail` declares EXACTLY three members: `enabled`, + // `sendChangeEmailConfirmation`, and `updateEmailWithoutVerification`. + // Measured 2026-08-20 against the installed + // `@better-auth/core@1.7.1`, in the file the package actually SHIPS — + // `dist/types/init-options.d.mts:835-857`. (The previous stamp cited + // `src/types/init-options.ts:946-971`; no `src/` is published, so + // that reference could not be checked by anyone reading it.) + // There is NO notify-only hook, and + // `sendChangeEmailConfirmation` is not one: in better-auth 1.7.1's + // `dist/api/routes/update-user.mjs:457` it becomes + // `canSendConfirmation`, and the branch opening at `:496` RETURNS at + // `:505` right after invoking it — the new address is never mailed + // until the old one clicks. Setting it is therefore structurally the + // approval gate the ruling refuses, not a way to notify. // // The notice is consequently sent by the framework, from the global // `after` hook on `/change-email` (search `__osChangeEmailFrom`), using @@ -3603,8 +3608,16 @@ export class AuthManager { // [#7724] A subject-erasure request is ONE unit of work, and better-auth // does not treat it as one: `internalAdapter.deleteUser` deletes the // sessions, then the accounts, then the user, in three unrelated adapter - // calls with no transaction (verified in better-auth 1.7.0-rc.2 — - // `dist/db/internal-adapter.mjs` mentions no transaction at all). Anything + // calls with no transaction. Measured 2026-08-20 against the installed + // better-auth 1.7.1: `deleteUser` at `dist/db/internal-adapter.mjs:233-247` + // is three separate `deleteManyWithHooks` / `deleteWithHooks` calls + // (session `:235`, account `:239`, user `:243`), and the string + // "transaction" does not occur anywhere in that file — a zero result that + // means something because "deleteManyWithHooks" occurs 8 times in it, so + // the search does reach the text. (The session call is now conditional on + // `!secondaryStorage || storeSessionInDatabase`; ObjectStack wires no + // `secondaryStorage` — deliberately, see `session-tombstone.ts` — so all + // three fire here.) Anything // that refuses the LAST of those three leaves the first two committed: the // credential rows are gone, the `sys_user` row is not, and the deployment // is left with an identity that still occupies the org roster and can no diff --git a/packages/plugins/plugin-auth/src/auth-schema-config.ts b/packages/plugins/plugin-auth/src/auth-schema-config.ts index 6f3b954b99..06cba71d54 100644 --- a/packages/plugins/plugin-auth/src/auth-schema-config.ts +++ b/packages/plugins/plugin-auth/src/auth-schema-config.ts @@ -914,7 +914,7 @@ export const buildOidcProviderPluginSchema = buildOauthProviderPluginSchema; * Each row is an external OIDC/SAML IdP this environment federates login to * (the relying-party side — ADR-0024's OPEN per-env SSO mechanism). The * protocol detail lives in JSON blobs (`oidcConfig` / `samlConfig`); the model - * itself is thin. Mirrors @better-auth/sso@1.6.20's `BaseSSOProvider`. + * itself is thin. * * | camelCase (better-auth) | snake_case (ObjectStack) | * |:------------------------|:-------------------------| @@ -924,6 +924,30 @@ export const buildOidcProviderPluginSchema = buildOauthProviderPluginSchema; * | userId | user_id | * | organizationId | organization_id | * | issuer / domain | (same name — no remap) | + * | domainVerified | domain_verified | + * + * ## Coverage, measured 2026-08-20 against `@better-auth/sso@1.7.1` + * + * The previous note here said only "Mirrors `@better-auth/sso@1.6.20`'s + * `BaseSSOProvider`" — a field-surface claim about a version two minors behind + * the installed one, which nobody had re-checked. Re-measured by resolving the + * plugin's real model the way the adapter does (`field.fieldName ?? key`) over + * `getAuthTables({ plugins: [sso()] }).ssoProvider.fields`: + * + * - `sso()` declares 7 fields — `issuer`, `oidcConfig`, `samlConfig`, + * `userId`, `providerId`, `organizationId`, `domain` — exactly the members + * of the shipped `BaseSSOProvider` type + * (`dist/index-CZytzKv6.d.mts:189-197`). + * - `sso({ domainVerification: { enabled: true } })` — the shape + * `OS_SSO_DOMAIN_VERIFICATION` turns on — adds an 8th, `domainVerified`. + * - Every one of those 8 resolves to a column `sys_sso_provider` declares. + * Nothing in the map is orphaned, and nothing in the model is unmapped. + * + * ⚠️ Unlike the core models, this mapping has **no parity gate**: + * `better-auth-schema-parity.test.ts` deliberately passes `getAuthTables()` no + * `sso` plugin, so an upstream field added to `ssoProvider` would land here + * silently. Until that changes, re-run the resolution above by hand when the + * `@better-auth/sso` pin moves — the check is one `getAuthTables` call. */ export const AUTH_SSO_PROVIDER_SCHEMA = { modelName: 'sys_sso_provider', diff --git a/packages/plugins/plugin-auth/src/change-email-delete-user-wiring.test.ts b/packages/plugins/plugin-auth/src/change-email-delete-user-wiring.test.ts index 1e557ff298..7a472519b6 100644 --- a/packages/plugins/plugin-auth/src/change-email-delete-user-wiring.test.ts +++ b/packages/plugins/plugin-auth/src/change-email-delete-user-wiring.test.ts @@ -504,8 +504,10 @@ describe('#8019 — change-email notifies the previous address without gating on it('keeps `sendChangeEmailConfirmation` OFF — the notice is not the gate in disguise', async () => { // ⛔ Ruling edge 1: #7735's 「策略按 better-auth 常规」 still governs the - // CONFIRMATION option, and in better-auth 1.7.0-rc.2 that option is not a - // notifier — `update-user.mjs` returns immediately after invoking it, so + // CONFIRMATION option, and that option is not a notifier — measured + // 2026-08-20 against the installed better-auth 1.7.1, whose + // `dist/api/routes/update-user.mjs` opens the branch at `:496` and returns + // `ctx.json({ status: true })` at `:505`, immediately after invoking it, so // the NEW address is never mailed until the OLD one clicks. Setting it // would silently convert this card's notification into the approval gate // the ruling refuses, and every assertion above would still pass. Read off diff --git a/packages/plugins/plugin-auth/src/impersonation-bearer-rotation.ts b/packages/plugins/plugin-auth/src/impersonation-bearer-rotation.ts index 7446dc0643..7dacb982bb 100644 --- a/packages/plugins/plugin-auth/src/impersonation-bearer-rotation.ts +++ b/packages/plugins/plugin-auth/src/impersonation-bearer-rotation.ts @@ -8,7 +8,8 @@ * better-auth's `bearer()` plugin authenticates a request by OVERWRITING the * request's session cookie with the bearer token (a before-hook calling * `setRequestCookie(headers, authCookies.sessionToken.name, decodedToken)` — - * `dist/plugins/bearer/index.mjs`, verified in 1.7.0-rc.2). The admin plugin's + * `dist/plugins/bearer/index.mjs:46` — measured 2026-08-20 against the + * installed better-auth `1.7.1`). The admin plugin's * `POST /admin/impersonate-user` does the opposite: it mints an impersonation * session and hands it over as a *cookie* (`setSessionCookie`), parking the * admin's own session token in a signed `admin_session` cookie for the exit diff --git a/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts b/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts index 72bd821f66..82224cbf70 100644 --- a/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts +++ b/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts @@ -183,8 +183,10 @@ const UNMAPPED_MANAGED_OBJECTS: Record = { reason: 'Hand-rolled ObjectStack table, not a better-auth model at all. ' + '`packages/core/src/security/api-key.ts` mints and verifies the key and POST /api/v1/keys ' - + 'writes the row, and better-auth 1.7.0-rc.2 ships NO apiKey plugin: there is no ' - + '"better-auth/plugins/api-key" export subpath and "better-auth/plugins" exports no apiKey. ' + + 'writes the row, and better-auth ships NO apiKey plugin — measured 2026-08-20 against ' + + 'the installed 1.7.1: package.json declares no "./plugins/api-key" export subpath and ' + + 'importing "better-auth/plugins" yields apiKey === undefined (bearer and admin are ' + + 'functions in the same import, so the read is not a silent miss). ' + 'So no model exists to derive and no column on this table can change hands. ' + 'Pinned by the premise test at the bottom of this file (#7770).', noBetterAuthColumns: true, @@ -915,9 +917,11 @@ describe('sys_api_key exemption premise (#7770)', () => { // This is the whole warrant for `noBetterAuthColumns: true` on // sys_api_key: the columns the issue worried about (`name`, `prefix`, // `key`, `userId`, `expiresAt`, `permissions`, `metadata`) belong to a - // plugin that does not exist at the pinned version — better-auth 1.7.0-rc.2 - // publishes no `./plugins/api-key` subpath and `better-auth/plugins` - // exports no `apiKey`. + // plugin that does not exist at the installed version — measured + // 2026-08-20 against better-auth 1.7.1, which publishes no + // `./plugins/api-key` subpath and whose `better-auth/plugins` exports no + // `apiKey`. The assertion below re-measures this on every run, so the + // stamp is a reading aid and the test is the actual check. // // Going red here is CORRECT and is the point: a bump that (re)introduces // the plugin makes the exemption's premise expire BEFORE anyone can enable diff --git a/packages/plugins/plugin-auth/src/member-role-canonical.test.ts b/packages/plugins/plugin-auth/src/member-role-canonical.test.ts index 62893b9d72..736bb7d75e 100644 --- a/packages/plugins/plugin-auth/src/member-role-canonical.test.ts +++ b/packages/plugins/plugin-auth/src/member-role-canonical.test.ts @@ -22,14 +22,16 @@ * branches fails the extraction and reddens the suite, instead of silently * leaving a pin that verifies nothing. * - * The three branches (better-auth 1.7.0-rc.2, - * `dist/plugins/organization/routes/crud-members.mjs`): + * The three branches, re-read 2026-08-20 out of the installed + * better-auth 1.7.1, `dist/plugins/organization/routes/crud-members.mjs` + * (the line numbers are an aid — the extraction below keys on the text, so a + * shift moves them without breaking the pin, while a rewrite reddens it): * - * 1. `removeMember` — `const roles = toBeRemovedMember.role.split(",");` - * … `if (roles.includes(creatorRole))` + * 1. `removeMember` — `const roles = toBeRemovedMember.role.split(",");` (`:193`) + * … `if (roles.includes(creatorRole))` (`:195`) * 2. `updateMemberRole` — `const isUpdatingCreator = - * toBeUpdatedMember.role.split(",").includes(creatorRole);` - * 3. `organization/leave` — `if (member.role.split(",").includes(creatorRole))` + * toBeUpdatedMember.role.split(",").includes(creatorRole);` (`:288`) + * 3. `organization/leave` — `if (member.role.split(",").includes(creatorRole))` (`:420`) */ import { describe, it, expect, beforeEach } from 'vitest'; diff --git a/packages/plugins/plugin-auth/src/member-role-canonical.ts b/packages/plugins/plugin-auth/src/member-role-canonical.ts index 4e5067b2f1..2015eadee5 100644 --- a/packages/plugins/plugin-auth/src/member-role-canonical.ts +++ b/packages/plugins/plugin-auth/src/member-role-canonical.ts @@ -11,11 +11,13 @@ * 1. the #5942 grade ladder (`orgRoleGrade` / `isOrgAdminGrade`, * `invitation-role-cap.ts`) — `split(',')` then `trim().toLowerCase()`; * 2. `mapMembershipRole` (`@objectstack/spec/identity`) — `trim().toLowerCase()`; - * 3. **better-auth itself** — `better-auth@1.7.0-rc.2`, + * 3. **better-auth itself** — measured 2026-08-20 against the installed + * `better-auth@1.7.1`, * `dist/plugins/organization/routes/crud-members.mjs`, a raw * `role.split(",")` with NO trim and NO lower-casing, in three branches: - * `removeMember`'s "only an owner may remove an owner", `updateMemberRole`'s - * creator protection, and `organization/leave`'s last-owner count. + * `removeMember`'s "only an owner may remove an owner" (`:193`), + * `updateMemberRole`'s creator protection (`:288`), and + * `organization/leave`'s last-owner count (`:420`). * * For a row stored as `Owner` (or `' owner'`), (1) and (2) say owner and (3) * says plain member. The vendor therefore skips its owner branch entirely and diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts index 877b93e6d8..54d3c257a2 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -239,12 +239,20 @@ function normaliseComparand(value: unknown): unknown { * * It is deliberately NOT a redundant belt over better-auth's own lower-casing. * better-auth's `internalAdapter` does lower-case `user.email` on - * `createUser` / `createOAuthUser` / `updateUser` / `updateUserByEmail` - * (`better-auth@1.7.0-rc.2/dist/db/internal-adapter.mjs:120,139,594,607`), but - * that is an *internal* of a **prerelease** dependency, invisible to any + * `createOAuthUser` / `createUser` / `updateUser` / `updateUserByEmail` — + * measured 2026-08-20 against the installed `better-auth@1.7.1`, at + * `dist/db/internal-adapter.mjs:127,146,602,615`. + * + * ⚠️ Two things about that sentence changed under it, and only one of them was + * the version number. The previous stamp read `1.7.0-rc.2/…:120,139,594,607` + * and called the dependency a **prerelease**; `1.7.1` is a stable release, so + * that half of the reason is simply dead and is not restamped — it is removed. + * What survives is the half that still holds and still carries the argument: + * the lower-casing is an *internal* of the vendor's `dist`, invisible to any * published type, and it does not cover the raw {@link createObjectQLAdapter} * path (hand-built calls that never pass through better-auth at all). The - * invariant the read half depends on has to be owned where it is relied upon. + * invariant the read half depends on has to be owned where it is relied upon — + * which is why this function exists whether or not the vendor keeps doing it. * * Idempotent by construction, so a payload better-auth already normalised is * unchanged — which is why this adds no behaviour to any existing write. diff --git a/packages/plugins/plugin-auth/src/register-sso-provider.test.ts b/packages/plugins/plugin-auth/src/register-sso-provider.test.ts index 939526d685..6b642880f5 100644 --- a/packages/plugins/plugin-auth/src/register-sso-provider.test.ts +++ b/packages/plugins/plugin-auth/src/register-sso-provider.test.ts @@ -48,7 +48,10 @@ describe('runRegisterSsoProviderFromForm (OIDC) — the emitted body must satisf // Regression pin for the end-to-end break where the bridge always emitted // `oidcConfig.mapping.id`, which `oidcMappingSchema` (a `z.strictObject` with // no `id` member since 1.7.0-rc.2) rejects outright — every OIDC registration - // answered `400 [body.oidcConfig.mapping] Unrecognized key: "id"`. + // answered `400 [body.oidcConfig.mapping] Unrecognized key: "id"`. "since + // 1.7.0-rc.2" is provenance, not a stamp: the member has been absent from + // every release since, the installed `@better-auth/sso@1.7.1` included + // (`dist/index.mjs:1852`, re-read 2026-08-20). // // These cases drive the REAL `/sso/register` endpoint. `@better-auth/sso` // validates the request body BEFORE the endpoint's session gate, so an diff --git a/packages/plugins/plugin-auth/src/register-sso-provider.ts b/packages/plugins/plugin-auth/src/register-sso-provider.ts index 725986d228..a30734337b 100644 --- a/packages/plugins/plugin-auth/src/register-sso-provider.ts +++ b/packages/plugins/plugin-auth/src/register-sso-provider.ts @@ -127,9 +127,10 @@ export async function runRegisterSsoProviderFromForm( if (discoveryEndpoint) oidcConfig.discoveryEndpoint = discoveryEndpoint; oidcConfig.scopes = scopesRaw ? scopesRaw.split(/[\s,]+/).filter(Boolean) : ['openid', 'email', 'profile']; - // `oidcConfig.mapping` is a `z.strictObject` in `@better-auth/sso@1.7.0-rc.2` - // (dist/index.mjs, `oidcMappingSchema`): members { email, emailVerified?, - // name, image?, extraFields? }, with `email` and `name` REQUIRED and NO `id` + // `oidcConfig.mapping` is a `z.strictObject`. Measured 2026-08-20 against the + // installed `@better-auth/sso@1.7.1` (`dist/index.mjs:1852`, + // `oidcMappingSchema`): members { email, emailVerified?, name, image?, + // extraFields? }, with `email` and `name` REQUIRED and NO `id` // member. Emitting `id` is therefore a hard 400 on EVERY registration: // [body.oidcConfig.mapping] Unrecognized key: "id" // @@ -145,6 +146,14 @@ export async function runRegisterSsoProviderFromForm( // `extraFields.id` is silently overwritten by `sub` — a no-op that reads as // configured. The subject claim is simply not configurable any more, so a // caller that asks for a different one is told so instead of being ignored. + // + // The two sentences above name rc.2 because that is WHEN the member was + // retired; the state they describe is still the installed one. Re-measured + // 2026-08-20 against `@better-auth/sso@1.7.1`, `dist/index.mjs`: the + // `extraFields` spread at `:3921` precedes `id: readStringClaim(rawUserInfo, + // "sub")` at `:3922` (and `:3932` precedes `id: idToken.sub` at `:3933`), + // with the cross-checks at `:3907` (`id_token_subject_missing`) and `:3918` + // (`id_token_userinfo_subject_mismatch`). const mapId = str(body?.mapId); if (mapId && mapId !== 'sub') { return { diff --git a/packages/plugins/plugin-auth/src/remove-member-permission-guard.test.ts b/packages/plugins/plugin-auth/src/remove-member-permission-guard.test.ts index ecf92c229a..d2cac55830 100644 --- a/packages/plugins/plugin-auth/src/remove-member-permission-guard.test.ts +++ b/packages/plugins/plugin-auth/src/remove-member-permission-guard.test.ts @@ -5,9 +5,11 @@ // // ## Where the wrong answer is minted (measured, not assumed) // -// NOT in our packages. better-auth `1.7.0-rc.2`, -// `dist/plugins/organization/routes/crud-members.mjs`, the `removeMember` -// handler, runs its checks in this order: +// NOT in our packages. Measured 2026-08-20 against the installed better-auth +// `1.7.1`, `dist/plugins/organization/routes/crud-members.mjs`, the +// `removeMember` handler (declared `:139`) runs its checks in this order — +// the branch at `:195`, its two throws at `:196` and `:197-200`, and +// `hasPermission` at `:202-207`: // // 1. resolve the caller's own member row → 400 MEMBER_NOT_FOUND // 2. resolve the target member row → 400 MEMBER_NOT_FOUND diff --git a/packages/plugins/plugin-auth/src/remove-member-permission-guard.ts b/packages/plugins/plugin-auth/src/remove-member-permission-guard.ts index 8c65ed3c3b..fa26e767a6 100644 --- a/packages/plugins/plugin-auth/src/remove-member-permission-guard.ts +++ b/packages/plugins/plugin-auth/src/remove-member-permission-guard.ts @@ -7,8 +7,13 @@ * ## The defect, and where it is minted * * Not here, and not anywhere in our packages: the wrong answer comes out of the - * pinned vendor. better-auth `1.7.0-rc.2`, - * `dist/plugins/organization/routes/crud-members.mjs`, `removeMember` runs: + * pinned vendor. Measured 2026-08-20 against the installed better-auth `1.7.1` + * (what `^1.7.1` resolves to today), in + * `dist/plugins/organization/routes/crud-members.mjs`: `removeMember` + * (declared `:139`) still runs the four checks below in this order — (3a) at + * `:196`, (3b) at `:197-200`, and the real permission check at `:202-207`, + * i.e. AFTER both. Re-check by reading those line numbers; if the order has + * changed upstream, this guard is what should be deleted, not adjusted: * * ```js * const roles = toBeRemovedMember.role.split(","); diff --git a/packages/plugins/plugin-auth/src/session-tombstone.test.ts b/packages/plugins/plugin-auth/src/session-tombstone.test.ts index bfe22c8eb8..a394240aa1 100644 --- a/packages/plugins/plugin-auth/src/session-tombstone.test.ts +++ b/packages/plugins/plugin-auth/src/session-tombstone.test.ts @@ -293,11 +293,14 @@ describe('#7732 — an interactive revoke tombstones the row instead of deleting // ─────────────────────────────────────────────────────────────────────────── describe('#7732 — the tombstone survives the one thing that used to collect it', () => { it('the revoked client polling /get-session does not garbage-collect its own record', async () => { - // The measured GC: better-auth 1.7.0-rc.2 has no sweeper at all, and its - // ONE expiry-driven collection is inside `/get-session` — a row whose - // `expiresAt` has passed is deleted "to clean up the session". That is why - // even the automatic path's stamps were best-effort. It only fires on a row - // `findSession` returned, so a hidden tombstone is never presented to it. + // The measured GC, re-read 2026-08-20 off the installed better-auth 1.7.1: + // no sweeper at all (`setInterval` appears only in two client-side dist + // files), and its ONE expiry-driven collection is inside `/get-session` + // (`dist/api/routes/session.mjs:146-157`) — a row whose `expiresAt` has + // passed is deleted "to clean up the session", and at 1.7.1 only when + // `!deferSessionRefresh || isPostRequest`. That is why even the automatic + // path's stamps were best-effort. It only fires on a row `findSession` + // returned, so a hidden tombstone is never presented to it. const engine = createMemoryEngine(); const manager = makeManager(engine); const { first, second, secondRow } = await twoSessions(manager, engine, 'poll@example.com'); diff --git a/packages/plugins/plugin-auth/src/session-tombstone.ts b/packages/plugins/plugin-auth/src/session-tombstone.ts index dcad128b57..32288cdbaa 100644 --- a/packages/plugins/plugin-auth/src/session-tombstone.ts +++ b/packages/plugins/plugin-auth/src/session-tombstone.ts @@ -31,13 +31,18 @@ * * ## Why the seam is HERE, at the adapter * - * Verified against better-auth `1.7.0-rc.2`, the version this package pins: + * Measured 2026-08-20 against the installed better-auth `1.7.1` — the version + * `^1.7.1` resolves to today, read from `node_modules/better-auth/package.json` + * and never from the range. Each bullet names the shipped file and symbol it + * was read out of, so a later bump is re-checkable one grep at a time: * * - **better-auth already implements this exact substitution, one layer up, - * and we cannot reach it.** `internalAdapter.endPreservedSessions` replaces - * the physical delete with `updateMany({ expiresAt: now })` while keeping - * the delete hooks running (`deleteManyWithHooks(..., { fn, executeMainFn: - * false })`). It is gated on `secondaryStorage` being configured + * and we cannot reach it.** `internalAdapter.endPreservedSessions` + * (`dist/db/internal-adapter.mjs:41`) replaces the physical delete with + * `updateMany({ model: 'session', update: { expiresAt: now } })` while + * keeping the delete hooks running (`deleteManyWithHooks(liveSessions, + * 'session', { fn, executeMainFn: false })`). It is gated on + * `secondaryStorage` being configured * (`deleteSession`: `if (secondaryStorage) { … if (preserveSessionInDatabase) * … }`), and ObjectStack deliberately does not wire one — handing better-auth * a `secondaryStorage` moves the session OF RECORD into the cache and makes @@ -46,8 +51,12 @@ * unreachable; this module is that shape at the only layer we own. * - **`databaseHooks.session.delete.before` returning `false` was refused.** * It aborts the delete, which is what we want — but `getWithHooks` then skips - * every `delete.after` hook, and `@better-auth/oauth-provider` (enabled by - * default here) registers `session.delete.before`/`after` to prepare and + * every `delete.after` hook (`dist/db/with-hooks.mjs:132`: a `before` hook + * answering `false` does a bare `return null`, ahead of the `delete.after` + * loop at `:140`), and `@better-auth/oauth-provider@1.7.1` (enabled by + * default here) registers `session.delete.before`/`after` + * (`dist/authorize-Crqw4_bR.mjs:4413`, `prepareBackchannelLogoutPlan` in + * `before` and the dispatch in `after`) to prepare and * dispatch **OIDC back-channel logout**. Suppressing back-channel logout on * an admin revoke — the single revocation that most needs downstream relying * parties told — trades an audit row for a security hole. Upstream's own @@ -80,23 +89,36 @@ * * ## Retention — why hiding, and not just stamping * - * Measured, not assumed. better-auth 1.7.0-rc.2 has **no scheduled sweeper** of - * session rows: the only expiry-driven collection in the whole library is - * inside `GET /get-session`, which on finding a row whose `expiresAt` has passed - * calls `internalAdapter.deleteSession(token)` to "clean up the session" - * (`api/routes/session.mjs`). That single line is what makes the automatic + * Measured, not assumed — 2026-08-20, against the installed better-auth + * `1.7.1`. It has **no scheduled sweeper** of session rows: grepping the whole + * shipped `dist/` for `setInterval` finds only two client-side files + * (`dist/client/session-refresh.mjs`, `dist/plugins/oauth-popup/client.mjs`), + * neither of which collects rows. The only expiry-driven collection in the + * library is inside `GET /get-session` + * (`dist/api/routes/session.mjs:146-157`), which on finding a row whose + * `expiresAt` has passed calls `internalAdapter.deleteSession(token)` to + * "clean up the session". 1.7.1 narrows even that: the call is now conditional + * (`if (!deferSessionRefresh || isPostRequest)`, `:155`), so it fires in + * strictly fewer cases than the rc line this note was first written against. + * That single line is what makes the automatic * path's stamps best-effort today — and it would eat an interactive tombstone * the moment the revoked client polled once, which for a browser session is * seconds. Stamping alone therefore satisfies the letter of D4 and leaves the * trail as inert as it was. * - * The collector only fires on a row it can see: + * The collector only fires on a row it can see — quoted from the installed + * `dist/api/routes/session.mjs:146-158` as it reads at `1.7.1`: * * ```js - * const session = await ctx.context.internalAdapter.findSession(token); + * const session = await ctx.context.internalAdapter.findSession(sessionCookieToken); + * ctx.context.session = session; * if (!session || session.session.expiresAt < new Date()) { * deleteSessionCookie(ctx); - * if (session) { … await ctx.context.internalAdapter.deleteSession(…); } + * if (session) { + * // Only delete on POST when deferSessionRefresh is enabled + * if (!deferSessionRefresh || isPostRequest) + * await ctx.context.internalAdapter.deleteSession(session.session.token); + * } * return ctx.json(null); * } * ``` diff --git a/packages/plugins/plugin-auth/src/sso-client-secret-at-rest.test.ts b/packages/plugins/plugin-auth/src/sso-client-secret-at-rest.test.ts index 92fba37429..3d5ea0b28c 100644 --- a/packages/plugins/plugin-auth/src/sso-client-secret-at-rest.test.ts +++ b/packages/plugins/plugin-auth/src/sso-client-secret-at-rest.test.ts @@ -22,7 +22,10 @@ * ⚠️ Registration is driven against `/sso/register` directly rather than through * the `register_sso_provider` UI action, because that bridge currently 400s on * an unrelated defect (#8193: it always sends `oidcConfig.mapping.id`, which - * `@better-auth/sso@1.7.0-rc.2` rejects with a `z.strictObject`). Same adapter, + * the installed `@better-auth/sso@1.7.1` rejects — `oidcMappingSchema` at + * `dist/index.mjs:1852` is a `z.strictObject` whose members are + * `{ email, emailVerified?, name, image?, extraFields? }`, with no `id`; + * measured 2026-08-20). Same adapter, * same write door; #8193 is filed separately and is not this card's scope. */ import { describe, it, expect, afterEach } from 'vitest'; diff --git a/packages/plugins/plugin-auth/src/sso-client-secret.ts b/packages/plugins/plugin-auth/src/sso-client-secret.ts index a62601938a..90bd85d940 100644 --- a/packages/plugins/plugin-auth/src/sso-client-secret.ts +++ b/packages/plugins/plugin-auth/src/sso-client-secret.ts @@ -21,9 +21,18 @@ * at rest plus our own generic read path. * * ## The seam - * `@better-auth/sso` at the pinned 1.7.0-rc.2 has NO secret-at-rest option, so - * there is no upstream switch to flip: `SSOOptions` has no equivalent of - * `scim({ storeSCIMToken: 'hashed' })`. better-auth owns the writes, so the seam + * `@better-auth/sso` has NO secret-at-rest option, so there is no upstream + * switch to flip: `SSOOptions` has no equivalent of + * `scim({ storeSCIMToken: 'hashed' })`. Measured 2026-08-20 against the + * installed `@better-auth/sso@1.7.1` by enumerating the top-level members of + * `SSOOptions` in the shipped `dist/index-CZytzKv6.d.mts` — `resolveUser`, + * `guardProviderMutation`, `provisionUser`, `provisionUserOnEveryLogin`, + * `organizationProvisioning`, `defaultSSO`, `defaultOverrideUserInfo`, + * `disableImplicitSignUp`, `modelName`, `fields`, `schema`, `providersLimit`, + * `trustEmailVerified`, `domainVerification`. None of them concerns storage of + * `clientSecret`; the only `encrypt`-shaped strings in the package are SAML + * assertion algorithms. Re-check by re-listing those members: a new one named + * for secret storage is what would retire this whole file. better-auth owns the writes, so the seam * sits between better-auth and its adapter — here — and never in a route handler. * * register / update-provider