Skip to content

Commit 4f24e9d

Browse files
os-zhuangclaude
andauthored
feat(spec,auth)!: one declared audience posture for self-registration — invite_only|email_domain|open, default invite_only (#11767)
* feat(spec): audience posture vocabulary on AuthConfig — invite_only|email_domain|open, default invite_only Closed vocabulary + runtime value list + type guard (MembershipPolicy precedent), completeness predicates in the schema (email_domain requires non-empty domains; self-registration-permitting postures require the declared permission set; inert declarations refused per ADR-0078), two registered error codes (SELF_REGISTRATION_CLOSED, EMAIL_DOMAIN_NOT_ALLOWED) and the audiencePosture non-flag public-config key. Artifacts regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(plugin-auth): enforce the audience posture at better-auth's admission seam One decision owner (decideAudienceAdmission) wired at user.validateUserInfo for every creation path, plus the sign-up before-hook raise that the vendor's anti-enumeration shield forces (a 403 from the creation seam is converted to a synthetic 200 when requireEmailVerification is on — measured on dist/api/routes/sign-up.mjs:163,235). Pending invitations admit under every posture; enterprise-IdP JIT is provider-class; self-registrants receive the DECLARED permission set via sys_user_permission_set or are not admitted at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(plugin-auth): audience-posture matrix + fixture triage for the invite_only default New audience-posture suite (pure decision matrix, entry refusals, wire outcomes incl. the landed permission-set grant). Existing harness suites create their non-first fixture users through the invitation carve-out (audience-gate-test-support) — the real admission lane under the new default, chosen over an open posture because open forces email verification and would stop sign-up from minting the sessions these suites exercise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: ADR-0087 semantic entry + minor changeset for the audience-posture default flip Registered audience-posture-default-invite-only (the rest-requireauth-default-flip class: a secure-default flip no transform can decide); pinned ledger regenerated for the new engine double. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(spec): regenerate os-regen artifacts on the merged tree Discharges the os-regen deferral recorded by the merge commit. The two driver-deferred artifacts (content/docs/references/index.mdx and the strictness-ledger counts) are regenerated from the merged sources; the remaining eight os-regen paths were reset to origin/main and regenerated back to byte-identical content, which is the evidence the textual merge dropped nothing. * fix(plugin-auth): make the audience bootstrap bypass answerable at its seam The #11739 bootstrap bypass never fired on a real deployment. Its probe was `ctx.context.adapter.findOne({ model: 'user', where: [] })`; `where: []` lowers to an empty filter and the real ObjectQL engine's `requireFindOnePredicate` (#4419) REFUSES a findOne that selects no particular record. The surrounding `catch { return false; }` read that refusal as "users exist", so every bootstrap creation was judged non-bootstrap and the new `invite_only` default refused the operator's own first account — including the dev-admin seed's. Fix, without touching the posture's ruled semantics or its fail-closed floor: - `isBootstrapCreation` asks the question in a form that can be ANSWERED — a bounded `find` through `withSystemReadContext`, the same ctx-independent data path `hasPendingInvitationFor` uses one frame away. An unanswerable probe still reads as NOT bootstrap. - The population predicate is HUMANS, not rows: `isHumanUserRow` is now the one owner, shared by the gate and by the dev-admin seed's own precondition, which had spelled the same filter by hand. They must agree — a DB still carrying the legacy `usr_system` row is exactly where they would have diverged, with plugin-security announcing "no human users yet" while the gate refused that very sign-up. - The sibling `disableSignUp` bootstrap bypass carried the identical dead probe and now routes through the same helper. Pinned by `audience-bootstrap-seam.test.ts` over a REAL ObjectQL engine, and through the server-side `api.signUpEmail` lane the dev seed actually uses — the two things no existing suite exercised. Case 0 asserts the #4419 refusal itself, so the reason a fake engine cannot pin this is under test rather than asserted in prose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(verify): fixture sign-ups enter through the invitation carve-out #11739 made `invite_only` the default audience posture. The verify harness's `signUp` helper exists to mint the SECOND, THIRD… fixture identity — the very population the wall now closes — so every fixture that needs a plain member died with 403 SELF_REGISTRATION_CLOSED once the dev-admin seed was unblocked. Seed a pending `sys_invitation` for the address first, which is the same lane plugin-auth's own suites take (`audience-gate-test-support.ts`) and for the same two reasons: `open`/`email_domain` would force email verification on and stop sign-up minting the token this helper returns, and the invitation lane keeps the audience gate honestly ON the path — the carve-out is a real admission verdict, not a bypass. Best-effort: an unreachable engine just signs up and lets the gate answer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changeset): record the bootstrap-bypass population rule and the verify harness lane Two facts the audience-posture changeset did not carry: the bootstrap bypass counts non-system HUMANS rather than sys_user rows (so a legacy usr_system row is still a fresh install), and @objectstack/verify's stack.signUp now enters through the invitation carve-out — which a consumer counting their own pending invitations needs to know about. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1e79aa4 commit 4f24e9d

42 files changed

Lines changed: 2507 additions & 51 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/plugin-auth': minor
4+
'@objectstack/verify': patch
5+
---
6+
7+
feat(spec,plugin-auth)!: one declared audience posture — `invite_only | email_domain | open`, default `invite_only`
8+
9+
**BREAKING CHANGE (ships as `minor` under the launch-window rule; every publishable package rides the fixed group).** "Who may become a user of an environment's apps" is now ONE declaration instead of an emergent property of five switches — and its default flips to the safe end.
10+
11+
- New authorable surface `auth.audience` on `AuthConfig` (`@objectstack/spec/system`): `posture` (`invite_only` | `email_domain` | `open`), `allowedEmailDomains` (required non-empty for `email_domain`), `selfRegistrationPermissionSet` (required whenever the posture permits self-registration; `admin_full_access` refused). Off-vocabulary postures and inert declarations (domains outside `email_domain`, a permission set under `invite_only`) are refused at parse AND at plugin-auth's config entry — never coerced.
12+
- **FROM:** an undeclared audience meant open email/password self-registration with no email verification, and self-registrants implicitly fell back to the `member_default` permission set. **TO:** an undeclared audience IS `invite_only` — self-serve sign-up (email/password, social-provider OAuth JIT, magic-link/OTP/phone/anonymous, and any unclassified creation method) is refused `403 SELF_REGISTRATION_CLOSED` unless the address holds a pending `sys_invitation` (the first account on a fresh install is exempt — the bootstrap bypass). One-line fix for deployments that mean to stay open: declare `auth: { audience: { posture: 'open', selfRegistrationPermissionSet: 'member_default' } }`.
13+
- `email_domain` admits only allowlisted domains (`403 EMAIL_DOMAIN_NOT_ALLOWED` otherwise; exact case-insensitive match, subdomains not implied, `+tag` local parts irrelevant). Any self-registration-permitting posture FORCES `requireEmailVerification` on (an explicit `false` beside it is refused at boot) and grants each self-registrant the DECLARED permission set (`sys_user_permission_set`); a declaration that cannot be resolved refuses admission (`403 AUTH_CONFIG_ERROR`) rather than admitting ungranted.
14+
- Operator-driven creation is never posture-gated: admin create-user / bulk import, SCIM provisioning, and JIT through operator-registered identity providers (`oidcProviders`, `@better-auth/sso`) keep working under every posture.
15+
- `/api/v1/auth/config` now serves `features.audiencePosture` and mirrors the forced verification flag; `SELF_REGISTRATION_CLOSED` and `EMAIL_DOMAIN_NOT_ALLOWED` are registered in the ADR-0112 ledger.
16+
- The BOOTSTRAP bypass counts non-system HUMANS, not `sys_user` rows, so a database still carrying the legacy `usr_system` service row is still a fresh install; the same predicate now backs the dev-admin seed's own precondition. The `emailAndPassword.disableSignUp` bootstrap bypass reads it too.
17+
- `@objectstack/verify`: `stack.signUp(...)` seeds a pending `sys_invitation` for the address before signing up, so harness fixtures that mint a second/third identity enter through the invitation carve-out under the new default. Fixtures asserting on their environment's pending invitations should filter by their own `organization_id` (the harness rows carry `org_verify_audience_gate`).
18+
19+
<!-- adr-0087: registered audience-posture-default-invite-only -->

content/docs/references/api/contract.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const result = ApiErrorSchema.parse(data);
2727

2828
| Property | Type | Required | Description |
2929
| :--- | :--- | :--- | :--- |
30-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +285 more>` || Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
30+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +287 more>` || Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3131
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) |
3232
| **message** | `string` || Readable error message |
3333
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -140,6 +140,7 @@ const result = ApiErrorSchema.parse(data);
140140
* `DRIVER_UNAVAILABLE`
141141
* `DUPLICATE_REQUEST`
142142
* `ELIGIBILITY_UNEVALUABLE`
143+
* `EMAIL_DOMAIN_NOT_ALLOWED`
143144
* `EMAIL_SEND_FAILED`
144145
* `EMAIL_SERVICE_REQUIRED`
145146
* `ENQUEUE_FAILED`
@@ -288,6 +289,7 @@ const result = ApiErrorSchema.parse(data);
288289
* `SAML_REGISTER_FAILED`
289290
* `SCHEDULES_LIST_FAILED`
290291
* `SCHEDULE_DELETE_FAILED`
292+
* `SELF_REGISTRATION_CLOSED`
291293
* `SETTINGS_ACTION_FAILED`
292294
* `SETTINGS_CRYPTO_UNAVAILABLE`
293295
* `SETTINGS_ENGINE_NOT_BOUND`

content/docs/references/api/error-code-ledger.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ const result = ErrorCode.parse(data);
244244
* `DRIVER_UNAVAILABLE`
245245
* `DUPLICATE_REQUEST`
246246
* `ELIGIBILITY_UNEVALUABLE`
247+
* `EMAIL_DOMAIN_NOT_ALLOWED`
247248
* `EMAIL_SEND_FAILED`
248249
* `EMAIL_SERVICE_REQUIRED`
249250
* `ENQUEUE_FAILED`
@@ -392,6 +393,7 @@ const result = ErrorCode.parse(data);
392393
* `SAML_REGISTER_FAILED`
393394
* `SCHEDULES_LIST_FAILED`
394395
* `SCHEDULE_DELETE_FAILED`
396+
* `SELF_REGISTRATION_CLOSED`
395397
* `SETTINGS_ACTION_FAILED`
396398
* `SETTINGS_CRYPTO_UNAVAILABLE`
397399
* `SETTINGS_ENGINE_NOT_BOUND`

content/docs/references/index.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1582 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1583 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -31,9 +31,9 @@ counts are sums of the rows they head. Regenerate with
3131
| [Security Protocol](/docs/references/security) | 5 | 27 | Permission sets, row-level security, sharing rules, tenancy posture. |
3232
| [Shared Protocol](/docs/references/shared) | 8 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
34-
| [System Protocol](/docs/references/system) | 36 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
34+
| [System Protocol](/docs/references/system) | 36 | 288 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 152 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **199** | **1582** | 14 protocol modules |
36+
| **Total** | **199** | **1583** | 14 protocol modules |
3737

3838
---
3939

@@ -318,14 +318,14 @@ Studio designer metadata — the authoring surfaces for the protocols above.
318318

319319
## System Protocol
320320

321-
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **36 pages, 287 schemas**
321+
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **36 pages, 288 schemas**
322322

323323
The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance.
324324

325325
| File | Schemas |
326326
| :--- | :--- |
327327
| [`app-install.zod.ts`](/docs/references/system/app-install) | `AppCompatibilityCheck`, `AppInstallRequest`, `AppInstallResult`, `AppManifest` |
328-
| [`auth-config.zod.ts`](/docs/references/system/auth-config) | `AdvancedAuthConfig`, `AuthConfig`, `AuthPluginConfig`, `AuthProviderConfig`, `EmailAndPasswordConfig`, `EmailVerificationConfig`, `MutualTLSConfig`, `OidcProviderConfig`, `OidcProvidersConfig`, `SocialProviderConfig` |
328+
| [`auth-config.zod.ts`](/docs/references/system/auth-config) | `AdvancedAuthConfig`, `AudienceConfig`, `AuthConfig`, `AuthPluginConfig`, `AuthProviderConfig`, `EmailAndPasswordConfig`, `EmailVerificationConfig`, `MutualTLSConfig`, `OidcProviderConfig`, `OidcProvidersConfig`, `SocialProviderConfig` |
329329
| [`book.zod.ts`](/docs/references/system/book) | `Book`, `BookAudience`, `BookGroup`, `BookInclude`, `BookNode` |
330330
| [`cache.zod.ts`](/docs/references/system/cache) | `CacheAvalanchePrevention`, `CacheConfig`, `CacheConsistency`, `CacheInvalidation`, `CacheStrategy`, `CacheTier`, `CacheWarmup`, `DistributedCacheConfig` |
331331
| [`change-management.zod.ts`](/docs/references/system/change-management) | `ChangeImpact`, `ChangePriority`, `ChangeRequest`, `ChangeStatus`, `ChangeType`, `RollbackPlan` |

content/docs/references/system/auth-config.mdx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ Used in server-side configuration injection.
1717
## TypeScript Usage
1818

1919
```typescript
20-
import { AdvancedAuthConfigSchema, AuthConfigSchema, AuthPluginConfigSchema, AuthProviderConfigSchema, EmailAndPasswordConfigSchema, EmailVerificationConfigSchema, MutualTLSConfigSchema, OidcProviderConfigSchema, OidcProvidersConfigSchema, SocialProviderConfigSchema } from '@objectstack/spec/system';
21-
import type { AdvancedAuthConfig, AuthConfig, AuthPluginConfig, AuthProviderConfig, EmailAndPasswordConfig, EmailVerificationConfig, MutualTLSConfig, OidcProviderConfig, OidcProvidersConfig, SocialProviderConfig } from '@objectstack/spec/system';
20+
import { AdvancedAuthConfigSchema, AudienceConfigSchema, AuthConfigSchema, AuthPluginConfigSchema, AuthProviderConfigSchema, EmailAndPasswordConfigSchema, EmailVerificationConfigSchema, MutualTLSConfigSchema, OidcProviderConfigSchema, OidcProvidersConfigSchema, SocialProviderConfigSchema } from '@objectstack/spec/system';
21+
import type { AdvancedAuthConfig, AudienceConfig, AuthConfig, AuthPluginConfig, AuthProviderConfig, EmailAndPasswordConfig, EmailVerificationConfig, MutualTLSConfig, OidcProviderConfig, OidcProvidersConfig, SocialProviderConfig } from '@objectstack/spec/system';
2222

2323
// Validate data
2424
const result = AdvancedAuthConfigSchema.parse(data);
@@ -40,6 +40,19 @@ Advanced / low-level Better-Auth options
4040
| **cookiePrefix** | `string` | optional | Prefix for auth cookie names |
4141

4242

43+
---
44+
45+
## AudienceConfig
46+
47+
### Properties
48+
49+
| Property | Type | Required | Description |
50+
| :--- | :--- | :--- | :--- |
51+
| **posture** | `Enum<'invite_only' \| 'email_domain' \| 'open'>` | optional (default: `"invite_only"`) | Who may self-register into this environment: invite_only (default — operator acts only), email_domain (allowlisted email domains), or open (anyone). Any posture other than invite_only forces email verification on. |
52+
| **allowedEmailDomains** | `string[]` | optional | Email domains admitted to self-register under posture email_domain (exact, case-insensitive match; subdomains need their own entries). Required non-empty for email_domain; refused under other postures. |
53+
| **selfRegistrationPermissionSet** | `string` | optional | sys_permission_set name granted to each self-registrant. Required when posture is email_domain or open; refused for invite_only. admin_full_access is refused. |
54+
55+
4356
---
4457

4558
## AuthConfig
@@ -60,6 +73,7 @@ Advanced / low-level Better-Auth options
6073
| **oidcProviders** | `{ providerId: string; name?: string; discoveryUrl?: string; issuer?: string; … }[]` | optional | List of OIDC/OAuth2 providers for enterprise SSO. Product or enterprise packages can pass this directly or contribute it through auth:configure. |
6174
| **emailAndPassword** | `{ enabled: boolean; disableSignUp?: boolean; requireEmailVerification?: boolean; minPasswordLength?: number; … }` | optional | Email and password authentication options forwarded to better-auth |
6275
| **emailVerification** | `{ sendOnSignUp?: boolean; sendOnSignIn?: boolean; autoSignInAfterVerification?: boolean; expiresIn?: number }` | optional | Email verification options forwarded to better-auth |
76+
| **audience** | `{ posture: Enum<'invite_only' \| 'email_domain' \| 'open'>; allowedEmailDomains?: string[]; selfRegistrationPermissionSet?: string }` | optional | Audience posture: who may self-register into this environment (invite_only — the default — \| email_domain \| open). See AudienceConfigSchema. |
6377
| **advanced** | `{ crossSubDomainCookies?: object; useSecureCookies?: boolean; disableCSRFCheck?: boolean; cookiePrefix?: string }` | optional | Advanced / low-level Better-Auth options |
6478
| **ssoOnlyMode** | `boolean` | optional | SSO-only login: hide the local password form + self-registration (the break-glass password endpoint stays enabled) |
6579
| **mutualTls** | `{ enabled: boolean; clientCertRequired: boolean; trustedCAs: string[]; crlUrl?: string; … }` | optional | Mutual TLS (mTLS) configuration |

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,4 @@ directory rather than per file.
264264
| `kernel/` | 296 |
265265
| `qa/` | 6 |
266266
| `shared/` | 20 |
267-
| `system/` | 360 |
267+
| `system/` | 361 |

packages/plugins/plugin-auth/src/accept-invitation-adopt-membership.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2424
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/objectql';
2525
import { AuthManager } from './auth-manager';
26+
import { inviteForAudienceGate } from './audience-gate-test-support';
2627

2728
const SECRET = 'test-secret-at-least-32-chars-long!!';
2829
const BASE = 'http://localhost:3000';
@@ -178,6 +179,9 @@ const post = (manager: AuthManager, path: string, body: unknown, cookie?: string
178179

179180
/** Sign a user up and return their session cookie + user id. */
180181
const signUp = async (manager: AuthManager, engine: MemoryEngine, email: string) => {
182+
// [#11739] default posture invite_only: fixture users beyond the first
183+
// enter through the invitation carve-out (see audience-gate-test-support).
184+
inviteForAudienceGate(engine, email);
181185
const res = await post(manager, '/sign-up/email', { email, password: PASSWORD, name: email });
182186
expect(res.status, await res.clone().text()).toBe(200);
183187
const user = (engine.tables.get('sys_user') ?? []).find((u) => u.email === email);

packages/plugins/plugin-auth/src/admin-impersonate-endpoint.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { AuthManager } from './auth-manager';
4444
import { createMemoryEngine } from './impersonation-bearer-rotation.test';
4545
import { ADMIN_SESSION_RECOVERY_RESPONSE_HEADER } from './impersonation-bearer-rotation';
4646
import { USER_NOT_FOUND } from './admin-impersonate-endpoint';
47+
import { inviteForAudienceGate } from './audience-gate-test-support';
4748

4849
const SECRET = 'test-secret-at-least-32-chars-long!!';
4950
const PASSWORD = 'S3cure!Passw0rd-9968';
@@ -58,14 +59,18 @@ const makeManager = (engine: any) =>
5859
plugins: { admin: true },
5960
} as any);
6061

61-
const signUp = (manager: AuthManager, email: string, name: string) =>
62-
manager.handleRequest(
62+
const signUp = (manager: AuthManager, email: string, name: string) => {
63+
// [#11739] default posture invite_only: fixture users beyond the first
64+
// enter through the invitation carve-out (see audience-gate-test-support).
65+
inviteForAudienceGate(manager, email);
66+
return manager.handleRequest(
6367
new Request(`${BASE}/sign-up/email`, {
6468
method: 'POST',
6569
headers: { 'Content-Type': 'application/json' },
6670
body: JSON.stringify({ email, password: PASSWORD, name }),
6771
}),
6872
);
73+
};
6974

7075
const signIn = (manager: AuthManager, email: string) =>
7176
manager.handleRequest(

packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
adminMayRevokeUserSessions,
2929
anySessionCarriesToken,
3030
} from './admin-revoke-user-session-match-guard';
31+
import { inviteForAudienceGate } from './audience-gate-test-support';
3132

3233
/**
3334
* In-memory IDataEngine — the `session-tombstone.test.ts` harness, unchanged,
@@ -142,8 +143,12 @@ const post = (manager: AuthManager, path: string, cookie?: string, body?: unknow
142143
}),
143144
);
144145

145-
const signUp = (manager: AuthManager, email: string) =>
146-
post(manager, 'sign-up/email', undefined, { email, password: PASSWORD, name: 'AdminRevoke' });
146+
const signUp = (manager: AuthManager, email: string) => {
147+
// [#11739] default posture invite_only: fixture users beyond the first
148+
// enter through the invitation carve-out (see audience-gate-test-support).
149+
inviteForAudienceGate(manager, email);
150+
return post(manager, 'sign-up/email', undefined, { email, password: PASSWORD, name: 'AdminRevoke' });
151+
};
147152

148153
const signIn = (manager: AuthManager, email: string) =>
149154
post(manager, 'sign-in/email', undefined, { email, password: PASSWORD });

0 commit comments

Comments
 (0)