@revealui/auth 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/react/useSignIn.d.ts +1 -1
  2. package/dist/react/useSignIn.d.ts.map +1 -1
  3. package/dist/react/useSignIn.js +7 -3
  4. package/dist/server/audit-storage.d.ts +5 -2
  5. package/dist/server/audit-storage.d.ts.map +1 -1
  6. package/dist/server/audit-storage.js +5 -2
  7. package/dist/server/auth.d.ts.map +1 -1
  8. package/dist/server/auth.js +68 -10
  9. package/dist/server/index.d.ts +2 -0
  10. package/dist/server/index.d.ts.map +1 -1
  11. package/dist/server/index.js +3 -0
  12. package/dist/server/platform-roles.d.ts +42 -0
  13. package/dist/server/platform-roles.d.ts.map +1 -0
  14. package/dist/server/platform-roles.js +65 -0
  15. package/dist/server/session.js +1 -1
  16. package/dist/server/sso/__tests__/helpers/mock-oidc-idp.d.ts +31 -0
  17. package/dist/server/sso/__tests__/helpers/mock-oidc-idp.d.ts.map +1 -0
  18. package/dist/server/sso/__tests__/helpers/mock-oidc-idp.js +115 -0
  19. package/dist/server/sso/__tests__/helpers/mock-saml-idp.d.ts +28 -0
  20. package/dist/server/sso/__tests__/helpers/mock-saml-idp.d.ts.map +1 -0
  21. package/dist/server/sso/__tests__/helpers/mock-saml-idp.js +150 -0
  22. package/dist/server/sso/index.d.ts +13 -0
  23. package/dist/server/sso/index.d.ts.map +1 -0
  24. package/dist/server/sso/index.js +12 -0
  25. package/dist/server/sso/jit.d.ts +39 -0
  26. package/dist/server/sso/jit.d.ts.map +1 -0
  27. package/dist/server/sso/jit.js +141 -0
  28. package/dist/server/sso/oidc.d.ts +137 -0
  29. package/dist/server/sso/oidc.d.ts.map +1 -0
  30. package/dist/server/sso/oidc.js +345 -0
  31. package/dist/server/sso/roles.d.ts +48 -0
  32. package/dist/server/sso/roles.d.ts.map +1 -0
  33. package/dist/server/sso/roles.js +109 -0
  34. package/dist/server/sso/saml.d.ts +99 -0
  35. package/dist/server/sso/saml.d.ts.map +1 -0
  36. package/dist/server/sso/saml.js +392 -0
  37. package/dist/server/sso/state.d.ts +46 -0
  38. package/dist/server/sso/state.d.ts.map +1 -0
  39. package/dist/server/sso/state.js +101 -0
  40. package/package.json +9 -5
@@ -19,7 +19,7 @@ export interface UseSignInResult {
19
19
  } | {
20
20
  success: false;
21
21
  requiresMfa: true;
22
- mfaUserId: string;
22
+ mfaUserId?: string;
23
23
  }>;
24
24
  isLoading: boolean;
25
25
  error: Error | null;
@@ -1 +1 @@
1
- {"version":3,"file":"useSignIn.d.ts","sourceRoot":"","sources":["../../src/react/useSignIn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAuCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CACN,KAAK,EAAE,WAAW,KACf,OAAO,CACR;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAC3D,CAAC;IACF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CA2E3C"}
1
+ {"version":3,"file":"useSignIn.d.ts","sourceRoot":"","sources":["../../src/react/useSignIn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAuCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CACnC;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAGhD;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAC5D,CAAC;IACF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CA+E3C"}
@@ -94,13 +94,17 @@ export function useSignIn() {
94
94
  code: errorData.code,
95
95
  };
96
96
  }
97
- // Check for MFA challenge before parsing as full success
97
+ // MFA challenge is HTTP 200 with { requiresMfa: true } and no `user`.
98
+ // The server may omit mfaUserId from the body (identity lives in the
99
+ // httpOnly mfa-pending cookie). Requiring mfaUserId here forced the
100
+ // success schema parse, which threw "expected object, received undefined"
101
+ // on `user` and locked MFA-enabled accounts out of the login UI.
98
102
  const jsonObj = json;
99
- if (jsonObj.requiresMfa === true && typeof jsonObj.mfaUserId === 'string') {
103
+ if (jsonObj.requiresMfa === true) {
100
104
  return {
101
105
  success: false,
102
106
  requiresMfa: true,
103
- mfaUserId: jsonObj.mfaUserId,
107
+ ...(typeof jsonObj.mfaUserId === 'string' ? { mfaUserId: jsonObj.mfaUserId } : {}),
104
108
  };
105
109
  }
106
110
  const successData = SignInSuccessResponseSchema.parse(json);
@@ -30,7 +30,7 @@
30
30
  * to one `audit_log`) safe: rows are independent, each signed at its own door
31
31
  * with the same env-derived key. Ruled for GAP-338 — see the gap file.
32
32
  */
33
- import type { AuditRowSignerFn, DrizzleAuditStore as DrizzleAuditStoreType } from '@revealui/db';
33
+ import type { AuditRowSignerFn, DrizzleAuditStoreOptions, DrizzleAuditStore as DrizzleAuditStoreType } from '@revealui/db';
34
34
  import type { Database } from '@revealui/db/client';
35
35
  import type { AuditEvent, AuditQuery, AuditSeverity, AuditStorage, AuditSystem } from '@revealui/security/server';
36
36
  /**
@@ -45,8 +45,11 @@ export declare function getAuditRowSigner(): AuditRowSignerFn | undefined;
45
45
  * Construct a `DrizzleAuditStore` wired to the process-wide signer. Every audit
46
46
  * writer builds its store through this helper so a row written through the one
47
47
  * door on a signing deployment always carries a signature.
48
+ *
49
+ * @param options Optional store options (GAP-417 residual): pass `targetEnv`
50
+ * from CLI writers that resolve a deployment target independent of NODE_ENV.
48
51
  */
49
- export declare function createAuditStore(db: Database): DrizzleAuditStoreType;
52
+ export declare function createAuditStore(db: Database, options?: DrizzleAuditStoreOptions): DrizzleAuditStoreType;
50
53
  /** Test-only reset of the cached signer (re-reads env on next `getAuditRowSigner`). */
51
54
  export declare function __resetAuditSignerForTest(): void;
52
55
  /** DB-side severity vocabulary — matches the `audit_log_severity_check` constraint. */
@@ -1 +1 @@
1
- {"version":3,"file":"audit-storage.d.ts","sourceRoot":"","sources":["../../src/server/audit-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,IAAI,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAEjG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAWnC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,GAAG,SAAS,CAgBhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAEpE;AAED,uFAAuF;AACvF,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED,uFAAuF;AACvF,KAAK,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAe/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,aAAa,GAAG,UAAU,CAEnE;AASD;;;;;GAKG;AACH,qBAAa,yBAA0B,YAAW,YAAY;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;gBAElC,EAAE,EAAE,QAAQ;IAOlB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BvC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IA4B/C,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;CAMhD;AAgCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,IAAI,CA0ChF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CAAC,WAAW,GAAE,WAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB1F"}
1
+ {"version":3,"file":"audit-storage.d.ts","sourceRoot":"","sources":["../../src/server/audit-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAChB,wBAAwB,EACxB,iBAAiB,IAAI,qBAAqB,EAC3C,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAWnC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,GAAG,SAAS,CAgBhE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,EACZ,OAAO,CAAC,EAAE,wBAAwB,GACjC,qBAAqB,CAEvB;AAED,uFAAuF;AACvF,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED,uFAAuF;AACvF,KAAK,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAe/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,aAAa,GAAG,UAAU,CAEnE;AASD;;;;;GAKG;AACH,qBAAa,yBAA0B,YAAW,YAAY;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;gBAElC,EAAE,EAAE,QAAQ;IAOlB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BvC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IA4B/C,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;CAMhD;AAgCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,IAAI,CA0ChF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CAAC,WAAW,GAAE,WAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB1F"}
@@ -63,9 +63,12 @@ export function getAuditRowSigner() {
63
63
  * Construct a `DrizzleAuditStore` wired to the process-wide signer. Every audit
64
64
  * writer builds its store through this helper so a row written through the one
65
65
  * door on a signing deployment always carries a signature.
66
+ *
67
+ * @param options Optional store options (GAP-417 residual): pass `targetEnv`
68
+ * from CLI writers that resolve a deployment target independent of NODE_ENV.
66
69
  */
67
- export function createAuditStore(db) {
68
- return new DrizzleAuditStore(db, getAuditRowSigner());
70
+ export function createAuditStore(db, options) {
71
+ return new DrizzleAuditStore(db, getAuditRowSigner(), options);
69
72
  }
70
73
  /** Test-only reset of the cached signer (re-reads env on next `getAuditRowSigner`). */
71
74
  export function __resetAuditSignerForTest() {
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AAUpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CAyLvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAqOvB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AAWpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CA0PvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAyOvB"}
@@ -12,6 +12,7 @@ import { and, eq, isNull } from 'drizzle-orm';
12
12
  import { auditLoginFailure, auditLoginSuccess } from './audit-bridge.js';
13
13
  import { clearFailedAttempts, isAccountLocked, recordFailedAttempt } from './brute-force.js';
14
14
  import { validatePasswordStrength } from './password-validation.js';
15
+ import { ensureAccountOwnerPlatformAdmin } from './platform-roles.js';
15
16
  import { checkRateLimit } from './rate-limit.js';
16
17
  import { createSession, rotateSession } from './session.js';
17
18
  /** Grace period after signup during which unverified users can still sign in (24 hours) */
@@ -26,9 +27,27 @@ const EMAIL_VERIFICATION_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000;
26
27
  */
27
28
  export async function signIn(email, password, options) {
28
29
  try {
29
- // Rate limiting by IP address
30
+ // Rate limiting by IP address. The storage backend (DatabaseStorage in
31
+ // production) can throw for reasons unrelated to the credential check
32
+ // itself (pool exhaustion, a transaction-pooling quirk, a storage
33
+ // misconfiguration) — guard it like every other DB-touching step below so
34
+ // a throw here fails closed with a diagnosable reason instead of falling
35
+ // through to the generic outer catch.
30
36
  const ipKey = options?.ipAddress || 'unknown';
31
- const rateLimit = await checkRateLimit(`signin:${ipKey}`);
37
+ let rateLimit;
38
+ try {
39
+ rateLimit = await checkRateLimit(`signin:${ipKey}`);
40
+ }
41
+ catch (rateLimitError) {
42
+ logger.error('Error checking rate limit', rateLimitError instanceof Error ? rateLimitError : undefined, {
43
+ message: rateLimitError instanceof Error ? rateLimitError.message : String(rateLimitError),
44
+ });
45
+ return {
46
+ success: false,
47
+ reason: 'database_error',
48
+ error: 'Database error',
49
+ };
50
+ }
32
51
  if (!rateLimit.allowed) {
33
52
  return {
34
53
  success: false,
@@ -36,8 +55,23 @@ export async function signIn(email, password, options) {
36
55
  error: 'Too many login attempts. Please try again later.',
37
56
  };
38
57
  }
39
- // Brute force protection by email
40
- const bruteForceCheck = await isAccountLocked(email);
58
+ // Brute force protection by email. Same rationale as the rate-limit
59
+ // guard above the storage backend can throw independently of whether
60
+ // the account is actually locked.
61
+ let bruteForceCheck;
62
+ try {
63
+ bruteForceCheck = await isAccountLocked(email);
64
+ }
65
+ catch (lockCheckError) {
66
+ logger.error('Error checking account lock status', lockCheckError instanceof Error ? lockCheckError : undefined, {
67
+ message: lockCheckError instanceof Error ? lockCheckError.message : String(lockCheckError),
68
+ });
69
+ return {
70
+ success: false,
71
+ reason: 'database_error',
72
+ error: 'Database error',
73
+ };
74
+ }
41
75
  if (bruteForceCheck.locked) {
42
76
  const lockMinutes = bruteForceCheck.lockUntil
43
77
  ? Math.ceil((bruteForceCheck.lockUntil - Date.now()) / (60 * 1000))
@@ -118,11 +152,27 @@ export async function signIn(email, password, options) {
118
152
  if (!isValid) {
119
153
  return failInvalidCredentials();
120
154
  }
121
- // Successful login - clear failed attempts
122
- await clearFailedAttempts(email);
123
- // Check email verification (with grace period for new accounts)
155
+ // Successful login - clear failed attempts. Best-effort: this is
156
+ // bookkeeping on an already-successful credential check, not a gate, so a
157
+ // storage hiccup here must not fail an otherwise-valid login (it can only
158
+ // make a future lockout trigger slightly sooner, never bypass one).
159
+ try {
160
+ await clearFailedAttempts(email);
161
+ }
162
+ catch (clearAttemptsError) {
163
+ logger.error('Error clearing failed attempts after successful login', clearAttemptsError instanceof Error ? clearAttemptsError : undefined, {
164
+ message: clearAttemptsError instanceof Error
165
+ ? clearAttemptsError.message
166
+ : String(clearAttemptsError),
167
+ });
168
+ }
169
+ // Check email verification (with grace period for new accounts).
170
+ // Coerce createdAt: the Postgres storage path returns timestamps as strings
171
+ // (not Date objects), so calling .getTime() directly threw a TypeError that
172
+ // fell through to the outer catch and turned a valid login into a generic
173
+ // "unexpected error" on real deploys. new Date() accepts a Date or a string.
124
174
  if (!user.emailVerified) {
125
- const accountAge = Date.now() - user.createdAt.getTime();
175
+ const accountAge = Date.now() - new Date(user.createdAt).getTime();
126
176
  if (accountAge > EMAIL_VERIFICATION_GRACE_PERIOD_MS) {
127
177
  return {
128
178
  success: false,
@@ -177,8 +227,12 @@ export async function signIn(email, password, options) {
177
227
  sessionToken: token,
178
228
  };
179
229
  }
180
- catch {
181
- logger.error('Unexpected error in signIn');
230
+ catch (err) {
231
+ logger.error('Unexpected error in signIn', err instanceof Error ? err : undefined, {
232
+ message: err instanceof Error ? err.message : String(err),
233
+ name: err instanceof Error ? err.name : 'unknown',
234
+ stack: err instanceof Error ? err.stack : undefined,
235
+ });
182
236
  return {
183
237
  success: false,
184
238
  reason: 'unexpected_error',
@@ -422,6 +476,10 @@ export async function signUp(email, password, name, options) {
422
476
  createdAt: now,
423
477
  updatedAt: now,
424
478
  });
479
+ // First membership owner of a hosted account gets platform shell
480
+ // admin (users.role), not super-admin. Unlocks /settings + API keys
481
+ // without granting fleet control-plane power.
482
+ await ensureAccountOwnerPlatformAdmin(db, user.id);
425
483
  }
426
484
  catch {
427
485
  logger.error('Failed to provision personal account at signup (billing webhook will backfill)', undefined, { userId: user.id });
@@ -21,10 +21,12 @@ export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenti
21
21
  export type { ChangePasswordResult, PasswordResetResult, PasswordResetToken, } from './password-reset.js';
22
22
  export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
23
23
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
24
+ export { ensureAccountOwnerPlatformAdmin, isPlatformShellAdminRole, PLATFORM_SHELL_ADMIN_COOKIE_ROLES, PLATFORM_SHELL_ADMIN_ROLES, platformRoleForAccountOwner, } from './platform-roles.js';
24
25
  export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
25
26
  export type { RequestContext, SessionBindingConfig, SessionData } from './session.js';
26
27
  export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
27
28
  export { signCookiePayload, verifyCookiePayload } from './signed-cookie.js';
29
+ export { type BuildOidcAuthorizationUrlInput, type BuildSamlAuthorizeUrlFailureReason, type BuildSamlAuthorizeUrlResult, buildOidcAuthorizationUrl, buildSamlAuthorizeUrl, buildSamlSpMetadata, createOidcRemoteJwkSet, type ExchangeOidcCodeFailureReason, type ExchangeOidcCodeInput, type ExchangeOidcCodeResult, type ExchangeOidcCodeSuccess, exchangeOidcCode, extractGroupsFromClaim, type FetchOidcDiscoveryOptions, type FetchOidcDiscoveryResult, fetchIdpMetadata, fetchOidcDiscovery, type GenerateSsoStateInput, type GenerateSsoStateResult, generateSsoState, type JWTPayload, type JWTVerifyGetKey, type KeyLike, type MapSsoGroupsFailureReason, type MapSsoGroupsInput, type MapSsoGroupsResult, mapSsoGroupsToRole, normalizeIdpCertPem, normalizeSsoUserRole, type OidcDiscoveryDocument, type OidcDiscoveryFailureReason, type ParseIdpMetadataFailureReason, type ParseIdpMetadataResult, parseIdpMetadataXml, type SamlSpConfig, type SsoStatePayload, type UpsertSsoUserInput, upsertSsoUser, type ValidatedIdTokenClaims, type ValidatedSamlAssertion, type ValidateIdTokenFailureReason, type ValidateIdTokenResult, type ValidateOidcIdTokenOptions, type ValidateSamlResponseFailureReason, type ValidateSamlResponseResult, type VerifiedSsoState, validateOidcIdToken, validateSamlPostResponse, verifySsoState, } from './sso/index.js';
28
30
  export type { Storage } from './storage/index.js';
29
31
  export { createStorage, DatabaseStorage, getStorage, InMemoryStorage, resetStorage, } from './storage/index.js';
30
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAO3B,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,+BAA+B,EAC/B,6BAA6B,EAC7B,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,+BAA+B,EAC/B,wBAAwB,EACxB,iCAAiC,EACjC,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EACL,KAAK,8BAA8B,EACnC,KAAK,kCAAkC,EACvC,KAAK,2BAA2B,EAChC,yBAAyB,EACzB,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,6BAA6B,EAClC,KAAK,sBAAsB,EAC3B,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,aAAa,EACb,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,EACjC,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACtC,KAAK,0BAA0B,EAC/B,KAAK,gBAAgB,EACrB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAC"}
@@ -22,8 +22,11 @@ export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getL
22
22
  export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenticationChallenge, generateRegistrationChallenge, listPasskeys, renamePasskey, resetPasskeyConfig, storePasskey, verifyAuthentication, verifyRegistration, } from './passkey.js';
23
23
  export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
24
24
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
25
+ export { ensureAccountOwnerPlatformAdmin, isPlatformShellAdminRole, PLATFORM_SHELL_ADMIN_COOKIE_ROLES, PLATFORM_SHELL_ADMIN_ROLES, platformRoleForAccountOwner, } from './platform-roles.js';
25
26
  export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
26
27
  export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
27
28
  // Signed Cookie
28
29
  export { signCookiePayload, verifyCookiePayload } from './signed-cookie.js';
30
+ // Enterprise SSO pure layer (GAP-464) — OIDC + SAML SP helpers
31
+ export { buildOidcAuthorizationUrl, buildSamlAuthorizeUrl, buildSamlSpMetadata, createOidcRemoteJwkSet, exchangeOidcCode, extractGroupsFromClaim, fetchIdpMetadata, fetchOidcDiscovery, generateSsoState, mapSsoGroupsToRole, normalizeIdpCertPem, normalizeSsoUserRole, parseIdpMetadataXml, upsertSsoUser, validateOidcIdToken, validateSamlPostResponse, verifySsoState, } from './sso/index.js';
29
32
  export { createStorage, DatabaseStorage, getStorage, InMemoryStorage, resetStorage, } from './storage/index.js';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Platform role plane (users.role + _json.roles) — durable bounds.
3
+ *
4
+ * Orthogonal to billing:
5
+ * - account_memberships.role (owner|admin|member) — workspace seat
6
+ * - account_entitlements.tier — paid features
7
+ *
8
+ * Super-admin is platform operator only (lives in _json.roles, never as
9
+ * the sole meaning of "first user of a customer account").
10
+ *
11
+ * Account membership owner of a hosted workspace gets CMS shell access
12
+ * via users.role = admin so proxy isAdminRole + /settings (API keys) work.
13
+ * That is NOT super-admin.
14
+ */
15
+ import type { Database } from '@revealui/db/client';
16
+ /** DB column values that already pass isAdminRole / shell admin cookie gate. */
17
+ export declare const PLATFORM_SHELL_ADMIN_ROLES: ReadonlySet<string>;
18
+ /**
19
+ * Cookie/proxy admin set (isAdminRole). Includes super-admin for cookie
20
+ * compatibility; super-admin must not be written to users.role (CHECK forbids it).
21
+ */
22
+ export declare const PLATFORM_SHELL_ADMIN_COOKIE_ROLES: ReadonlySet<string>;
23
+ /**
24
+ * When a user becomes (or is) billing account owner, ensure they can use
25
+ * workspace admin surfaces. Promote non-admin DB roles to `admin`.
26
+ * Never demote owner/admin. Never assign super-admin.
27
+ */
28
+ export declare function platformRoleForAccountOwner(currentRole: string): string;
29
+ export declare function isPlatformShellAdminRole(role: string | null | undefined): boolean;
30
+ /**
31
+ * Promote users.role to admin when the user is (or becomes) an account
32
+ * membership owner and currently lacks shell admin. Idempotent; never demotes.
33
+ *
34
+ * Call sites: personal-account provision at signup, Stripe ensureHostedAccount,
35
+ * grant-entitlement --create-account, OAuth hosted provision.
36
+ */
37
+ export declare function ensureAccountOwnerPlatformAdmin(db: Database, userId: string): Promise<{
38
+ previousRole: string;
39
+ nextRole: string;
40
+ updated: boolean;
41
+ } | null>;
42
+ //# sourceMappingURL=platform-roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-roles.d.ts","sourceRoot":"","sources":["../../src/server/platform-roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAE3F;;;GAGG;AACH,eAAO,MAAM,iCAAiC,EAAE,WAAW,CAAC,MAAM,CAIhE,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKvE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEjF;AAED;;;;;;GAMG;AACH,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAmB9E"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Platform role plane (users.role + _json.roles) — durable bounds.
3
+ *
4
+ * Orthogonal to billing:
5
+ * - account_memberships.role (owner|admin|member) — workspace seat
6
+ * - account_entitlements.tier — paid features
7
+ *
8
+ * Super-admin is platform operator only (lives in _json.roles, never as
9
+ * the sole meaning of "first user of a customer account").
10
+ *
11
+ * Account membership owner of a hosted workspace gets CMS shell access
12
+ * via users.role = admin so proxy isAdminRole + /settings (API keys) work.
13
+ * That is NOT super-admin.
14
+ */
15
+ import { users } from '@revealui/db/schema';
16
+ import { eq } from 'drizzle-orm';
17
+ /** DB column values that already pass isAdminRole / shell admin cookie gate. */
18
+ export const PLATFORM_SHELL_ADMIN_ROLES = new Set(['owner', 'admin']);
19
+ /**
20
+ * Cookie/proxy admin set (isAdminRole). Includes super-admin for cookie
21
+ * compatibility; super-admin must not be written to users.role (CHECK forbids it).
22
+ */
23
+ export const PLATFORM_SHELL_ADMIN_COOKIE_ROLES = new Set([
24
+ 'owner',
25
+ 'admin',
26
+ 'super-admin',
27
+ ]);
28
+ /**
29
+ * When a user becomes (or is) billing account owner, ensure they can use
30
+ * workspace admin surfaces. Promote non-admin DB roles to `admin`.
31
+ * Never demote owner/admin. Never assign super-admin.
32
+ */
33
+ export function platformRoleForAccountOwner(currentRole) {
34
+ if (PLATFORM_SHELL_ADMIN_ROLES.has(currentRole)) {
35
+ return currentRole;
36
+ }
37
+ return 'admin';
38
+ }
39
+ export function isPlatformShellAdminRole(role) {
40
+ return role != null && PLATFORM_SHELL_ADMIN_COOKIE_ROLES.has(role);
41
+ }
42
+ /**
43
+ * Promote users.role to admin when the user is (or becomes) an account
44
+ * membership owner and currently lacks shell admin. Idempotent; never demotes.
45
+ *
46
+ * Call sites: personal-account provision at signup, Stripe ensureHostedAccount,
47
+ * grant-entitlement --create-account, OAuth hosted provision.
48
+ */
49
+ export async function ensureAccountOwnerPlatformAdmin(db, userId) {
50
+ const [row] = await db
51
+ .select({ id: users.id, role: users.role })
52
+ .from(users)
53
+ .where(eq(users.id, userId))
54
+ .limit(1);
55
+ if (!row) {
56
+ return null;
57
+ }
58
+ const previousRole = row.role;
59
+ const nextRole = platformRoleForAccountOwner(previousRole);
60
+ if (nextRole === previousRole) {
61
+ return { previousRole, nextRole, updated: false };
62
+ }
63
+ await db.update(users).set({ role: nextRole }).where(eq(users.id, userId));
64
+ return { previousRole, nextRole, updated: true };
65
+ }
@@ -129,7 +129,7 @@ export async function getSession(headers, requestContext) {
129
129
  if (requestContext) {
130
130
  const bindingError = validateSessionBinding(session, requestContext);
131
131
  if (bindingError) {
132
- logger.warn('Session binding violation - invalidating session', {
132
+ logger.error('Session binding violation - invalidating session', {
133
133
  sessionId: session.id,
134
134
  reason: bindingError,
135
135
  });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Minimal mock OIDC IdP for GAP-464 integration tests.
3
+ * Real RSA keys + jose-signed id_tokens; discovery/token/jwks via fetchImpl.
4
+ */
5
+ import { type KeyLike } from 'jose';
6
+ export interface MockOidcIdpOptions {
7
+ issuer?: string;
8
+ clientId?: string;
9
+ clientSecret?: string;
10
+ }
11
+ export interface MockOidcIdp {
12
+ issuer: string;
13
+ clientId: string;
14
+ clientSecret: string;
15
+ discoveryUrl: string;
16
+ authorizationEndpoint: string;
17
+ tokenEndpoint: string;
18
+ jwksUri: string;
19
+ /** Local KeyLike for direct validateOidcIdToken tests */
20
+ publicKey: KeyLike;
21
+ signIdToken: (claims?: Record<string, unknown>, overrides?: {
22
+ issuer?: string;
23
+ audience?: string;
24
+ expSeconds?: number;
25
+ privateKey?: KeyLike;
26
+ }) => Promise<string>;
27
+ /** fetchImpl that routes discovery, jwks, and token endpoints */
28
+ fetchImpl: typeof fetch;
29
+ }
30
+ export declare function createMockOidcIdp(options?: MockOidcIdpOptions): Promise<MockOidcIdp>;
31
+ //# sourceMappingURL=mock-oidc-idp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-oidc-idp.d.ts","sourceRoot":"","sources":["../../../../../src/server/sso/__tests__/helpers/mock-oidc-idp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAA2C,KAAK,OAAO,EAAW,MAAM,MAAM,CAAC;AAEtF,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,CACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,SAAS,CAAC,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,OAAO,CAAC;KACtB,KACE,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,iEAAiE;IACjE,SAAS,EAAE,OAAO,KAAK,CAAC;CACzB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwI9F"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Minimal mock OIDC IdP for GAP-464 integration tests.
3
+ * Real RSA keys + jose-signed id_tokens; discovery/token/jwks via fetchImpl.
4
+ */
5
+ import { generateKeyPairSync } from 'node:crypto';
6
+ import { exportJWK, importPKCS8, SignJWT } from 'jose';
7
+ export async function createMockOidcIdp(options = {}) {
8
+ const issuer = options.issuer ?? 'https://mock-idp.example.com';
9
+ const clientId = options.clientId ?? 'revealui-sso-client';
10
+ const clientSecret = options.clientSecret ?? 'mock-client-secret';
11
+ const { publicKey: pubPem, privateKey: privPem } = generateKeyPairSync('rsa', {
12
+ modulusLength: 2048,
13
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
14
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
15
+ });
16
+ const privateKey = await importPKCS8(privPem, 'RS256');
17
+ const { createPublicKey } = await import('node:crypto');
18
+ const pub = createPublicKey(pubPem);
19
+ const jwk = await exportJWK(pub);
20
+ jwk.alg = 'RS256';
21
+ jwk.use = 'sig';
22
+ jwk.kid = 'mock-idp-1';
23
+ const publicKey = (await (await import('jose')).importJWK({ ...jwk, alg: 'RS256' }, 'RS256'));
24
+ const discoveryUrl = `${issuer}/.well-known/openid-configuration`;
25
+ const authorizationEndpoint = `${issuer}/authorize`;
26
+ const tokenEndpoint = `${issuer}/token`;
27
+ const jwksUri = `${issuer}/jwks`;
28
+ async function signIdToken(claims = {}, overrides) {
29
+ const now = Math.floor(Date.now() / 1000);
30
+ const exp = overrides?.expSeconds ?? now + 3600;
31
+ const sub = typeof claims.sub === 'string' ? claims.sub : 'user-sub-1';
32
+ const payload = { ...claims, sub };
33
+ return new SignJWT(payload)
34
+ .setProtectedHeader({ alg: 'RS256', kid: 'mock-idp-1' })
35
+ .setIssuer(overrides?.issuer ?? issuer)
36
+ .setAudience(overrides?.audience ?? clientId)
37
+ .setIssuedAt(now)
38
+ .setExpirationTime(exp)
39
+ .setSubject(sub)
40
+ .sign(overrides?.privateKey ?? privateKey);
41
+ }
42
+ const discoveryDoc = {
43
+ issuer,
44
+ authorization_endpoint: authorizationEndpoint,
45
+ token_endpoint: tokenEndpoint,
46
+ jwks_uri: jwksUri,
47
+ response_types_supported: ['code'],
48
+ code_challenge_methods_supported: ['S256'],
49
+ scopes_supported: ['openid', 'email', 'profile'],
50
+ };
51
+ const jwksDoc = { keys: [jwk] };
52
+ const fetchImpl = (async (input, init) => {
53
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
54
+ if (url === discoveryUrl || url.startsWith(`${discoveryUrl}?`)) {
55
+ return new Response(JSON.stringify(discoveryDoc), {
56
+ status: 200,
57
+ headers: { 'Content-Type': 'application/json' },
58
+ });
59
+ }
60
+ if (url === jwksUri || url.startsWith(`${jwksUri}?`)) {
61
+ return new Response(JSON.stringify(jwksDoc), {
62
+ status: 200,
63
+ headers: { 'Content-Type': 'application/json' },
64
+ });
65
+ }
66
+ if (url === tokenEndpoint || url.startsWith(`${tokenEndpoint}?`)) {
67
+ if ((init?.method ?? 'GET').toUpperCase() !== 'POST') {
68
+ return new Response('method not allowed', { status: 405 });
69
+ }
70
+ // undici may pass URLSearchParams; avoid cross-realm instanceof
71
+ const rawBody = init?.body;
72
+ const bodyText = rawBody == null
73
+ ? ''
74
+ : typeof rawBody === 'string'
75
+ ? rawBody
76
+ : typeof rawBody.toString === 'function'
77
+ ? rawBody.toString()
78
+ : '';
79
+ const params = new URLSearchParams(bodyText);
80
+ // Accept any non-empty code; mock IdP does not track issued codes
81
+ if (!params.get('code') || params.get('grant_type') !== 'authorization_code') {
82
+ return new Response(JSON.stringify({ error: 'invalid_request', body: bodyText }), {
83
+ status: 400,
84
+ });
85
+ }
86
+ if (params.get('client_id') !== clientId || params.get('client_secret') !== clientSecret) {
87
+ return new Response(JSON.stringify({ error: 'invalid_client' }), { status: 401 });
88
+ }
89
+ const idToken = await signIdToken({
90
+ email: 'alice@example.com',
91
+ email_verified: true,
92
+ name: 'Alice Example',
93
+ groups: ['Engineering', 'Staff'],
94
+ });
95
+ return new Response(JSON.stringify({
96
+ id_token: idToken,
97
+ access_token: 'mock-access',
98
+ token_type: 'Bearer',
99
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
100
+ }
101
+ return new Response(`not found: ${url}`, { status: 404 });
102
+ });
103
+ return {
104
+ issuer,
105
+ clientId,
106
+ clientSecret,
107
+ discoveryUrl,
108
+ authorizationEndpoint,
109
+ tokenEndpoint,
110
+ jwksUri,
111
+ publicKey,
112
+ signIdToken,
113
+ fetchImpl,
114
+ };
115
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Minimal mock SAML IdP for GAP-464 integration tests.
3
+ * Self-signed X509 + xml-crypto enveloped signatures on Response + Assertion.
4
+ */
5
+ export interface MockSamlIdp {
6
+ entityId: string;
7
+ ssoUrl: string;
8
+ certPem: string;
9
+ privateKeyPem: string;
10
+ /** Build IdP metadata XML consumed by parseIdpMetadataXml */
11
+ metadataXml: string;
12
+ /**
13
+ * Build a base64 SAMLResponse (HTTP-POST) signed by this IdP.
14
+ * When sign is false, returns an unsigned Response for reject tests.
15
+ */
16
+ buildPostResponse: (input: {
17
+ spEntityId: string;
18
+ acsUrl: string;
19
+ nameId: string;
20
+ groups?: string[];
21
+ sign?: boolean;
22
+ notOnOrAfterOffsetMs?: number;
23
+ }) => string;
24
+ }
25
+ export declare function createMockSamlIdp(options?: {
26
+ entityId?: string;
27
+ }): MockSamlIdp;
28
+ //# sourceMappingURL=mock-saml-idp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-saml-idp.d.ts","sourceRoot":"","sources":["../../../../../src/server/sso/__tests__/helpers/mock-saml-idp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,iBAAiB,EAAE,CAAC,KAAK,EAAE;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,oBAAoB,CAAC,EAAE,MAAM,CAAC;KAC/B,KAAK,MAAM,CAAC;CACd;AAsBD,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,WAAW,CA2J9E"}