@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.
- package/dist/react/useSignIn.d.ts +1 -1
- package/dist/react/useSignIn.d.ts.map +1 -1
- package/dist/react/useSignIn.js +7 -3
- package/dist/server/audit-storage.d.ts +5 -2
- package/dist/server/audit-storage.d.ts.map +1 -1
- package/dist/server/audit-storage.js +5 -2
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +68 -10
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/platform-roles.d.ts +42 -0
- package/dist/server/platform-roles.d.ts.map +1 -0
- package/dist/server/platform-roles.js +65 -0
- package/dist/server/session.js +1 -1
- package/dist/server/sso/__tests__/helpers/mock-oidc-idp.d.ts +31 -0
- package/dist/server/sso/__tests__/helpers/mock-oidc-idp.d.ts.map +1 -0
- package/dist/server/sso/__tests__/helpers/mock-oidc-idp.js +115 -0
- package/dist/server/sso/__tests__/helpers/mock-saml-idp.d.ts +28 -0
- package/dist/server/sso/__tests__/helpers/mock-saml-idp.d.ts.map +1 -0
- package/dist/server/sso/__tests__/helpers/mock-saml-idp.js +150 -0
- package/dist/server/sso/index.d.ts +13 -0
- package/dist/server/sso/index.d.ts.map +1 -0
- package/dist/server/sso/index.js +12 -0
- package/dist/server/sso/jit.d.ts +39 -0
- package/dist/server/sso/jit.d.ts.map +1 -0
- package/dist/server/sso/jit.js +141 -0
- package/dist/server/sso/oidc.d.ts +137 -0
- package/dist/server/sso/oidc.d.ts.map +1 -0
- package/dist/server/sso/oidc.js +345 -0
- package/dist/server/sso/roles.d.ts +48 -0
- package/dist/server/sso/roles.d.ts.map +1 -0
- package/dist/server/sso/roles.js +109 -0
- package/dist/server/sso/saml.d.ts +99 -0
- package/dist/server/sso/saml.d.ts.map +1 -0
- package/dist/server/sso/saml.js +392 -0
- package/dist/server/sso/state.d.ts +46 -0
- package/dist/server/sso/state.d.ts.map +1 -0
- package/dist/server/sso/state.js +101 -0
- package/package.json +9 -5
|
@@ -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,
|
|
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"}
|
package/dist/react/useSignIn.js
CHANGED
|
@@ -94,13 +94,17 @@ export function useSignIn() {
|
|
|
94
94
|
code: errorData.code,
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
-
//
|
|
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
|
|
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,
|
|
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;
|
|
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"}
|
package/dist/server/auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
//
|
|
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 });
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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":"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,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;
|
|
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"}
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/server/session.js
CHANGED
|
@@ -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.
|
|
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"}
|