@iqauth/sdk 2.6.4 → 2.8.1
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/README.md +173 -1
- package/dist/browser-session.d.mts +4 -4
- package/dist/browser-session.d.ts +4 -4
- package/dist/browser-session.js +212 -46
- package/dist/browser-session.mjs +3 -3
- package/dist/browser.d.mts +5 -5
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +293 -34
- package/dist/browser.mjs +5 -5
- package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
- package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
- package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/chunk-VYQ3ETCK.mjs +244 -0
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/chunk-WHT6WKTY.mjs +3180 -0
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/chunk-WSH4SW7F.mjs +490 -0
- package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
- package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
- package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
- package/dist/errors-Jl1Jtm-6.d.mts +107 -0
- package/dist/errors-Jl1Jtm-6.d.ts +107 -0
- package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +563 -85
- package/dist/express.mjs +73 -34
- package/dist/fastify.d.mts +10 -0
- package/dist/fastify.d.ts +10 -0
- package/dist/fastify.js +589 -65
- package/dist/fastify.mjs +101 -11
- package/dist/hono.d.mts +10 -0
- package/dist/hono.d.ts +10 -0
- package/dist/hono.js +566 -65
- package/dist/hono.mjs +78 -11
- package/dist/index-Cko-d5po.d.mts +1848 -0
- package/dist/index-RNqwEcmY.d.ts +1848 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +694 -75
- package/dist/index.mjs +30 -10
- package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/locales.js +36 -0
- package/dist/locales.mjs +1 -1
- package/dist/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +307 -46
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +10 -1
- package/dist/next.d.ts +10 -1
- package/dist/next.js +596 -205
- package/dist/next.mjs +83 -10
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
- package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
- package/dist/react-permissions.d.mts +52 -0
- package/dist/react-permissions.d.ts +52 -0
- package/dist/react-permissions.js +239 -0
- package/dist/react-permissions.mjs +98 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +882 -73
- package/dist/react.mjs +71 -2631
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +200 -4
- package/dist/server/handlers.d.ts +200 -4
- package/dist/server/handlers.js +530 -16
- package/dist/server/handlers.mjs +14 -3
- package/dist/server.d.mts +171 -8
- package/dist/server.d.ts +171 -8
- package/dist/server.js +579 -61
- package/dist/server.mjs +99 -12
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +212 -46
- package/dist/service.mjs +3 -3
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
- package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
- package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
- package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
- package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
- package/dist/webhooks.d.mts +113 -17
- package/dist/webhooks.d.ts +113 -17
- package/dist/webhooks.js +179 -15
- package/dist/webhooks.mjs +7 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/dist/ws.js +80 -30
- package/dist/ws.mjs +4 -4
- package/docs/error-handling.md +101 -0
- package/docs/guides/effective-permissions.md +171 -0
- package/docs/guides/invitations.md +65 -0
- package/package.json +19 -4
- package/dist/chunk-6TDJJER7.mjs +0 -217
- package/dist/chunk-UKZLOHZG.mjs +0 -83
- package/dist/errors-CDdl24MP.d.mts +0 -52
- package/dist/errors-CDdl24MP.d.ts +0 -52
package/dist/index.js
CHANGED
|
@@ -42,10 +42,13 @@ __export(index_exports, {
|
|
|
42
42
|
ErrorCodes: () => ErrorCodes,
|
|
43
43
|
GdprModule: () => GdprModule,
|
|
44
44
|
HierarchyModule: () => HierarchyModule,
|
|
45
|
+
IQAUTH_SIGNATURE_HEADER: () => IQAUTH_SIGNATURE_HEADER,
|
|
45
46
|
IQAuthClient: () => IQAuthClient,
|
|
46
47
|
IQAuthError: () => IQAuthError,
|
|
48
|
+
IQ_AUTH_ERROR_CODES: () => IQ_AUTH_ERROR_CODES,
|
|
47
49
|
InMemoryOidcStateStore: () => InMemoryOidcStateStore,
|
|
48
50
|
InvitesModule: () => InvitesModule,
|
|
51
|
+
LEGACY_SIGNATURE_HEADERS: () => LEGACY_SIGNATURE_HEADERS,
|
|
49
52
|
MembershipsModule: () => MembershipsModule,
|
|
50
53
|
MfaModule: () => MfaModule,
|
|
51
54
|
OidcModule: () => OidcModule,
|
|
@@ -63,27 +66,61 @@ __export(index_exports, {
|
|
|
63
66
|
WebhookSignatureError: () => WebhookSignatureError,
|
|
64
67
|
WebhooksModule: () => WebhooksModule,
|
|
65
68
|
assertPublishableKey: () => assertPublishableKey,
|
|
69
|
+
buildUserinfoResponse: () => buildUserinfoResponse,
|
|
66
70
|
createProvisioningBridge: () => createProvisioningBridge,
|
|
67
71
|
createTestIssuer: () => createTestIssuer,
|
|
68
72
|
encodePublishableKey: () => encodePublishableKey,
|
|
73
|
+
expandPermissions: () => expandPermissions,
|
|
74
|
+
handleUserinfo: () => handleUserinfo,
|
|
75
|
+
hasPermission: () => hasPermission,
|
|
69
76
|
iqAuthMiddleware: () => iqAuthMiddleware,
|
|
70
77
|
isPublishableKey: () => isPublishableKey,
|
|
71
78
|
isSecretKey: () => isSecretKey,
|
|
72
79
|
isValidWebhookSignature: () => isValidWebhookSignature,
|
|
73
80
|
parsePublishableKey: () => parsePublishableKey,
|
|
81
|
+
parseWebhookEvent: () => parseWebhookEvent,
|
|
74
82
|
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
75
83
|
verifyWsUpgrade: () => verifyWsUpgrade
|
|
76
84
|
});
|
|
77
85
|
module.exports = __toCommonJS(index_exports);
|
|
78
86
|
|
|
79
87
|
// src/errors.ts
|
|
80
|
-
var
|
|
81
|
-
|
|
88
|
+
var IQ_AUTH_ERROR_CODES = [
|
|
89
|
+
"token_expired",
|
|
90
|
+
"token_invalid",
|
|
91
|
+
"jwks_unavailable",
|
|
92
|
+
"jwks_fetch_failed",
|
|
93
|
+
"rate_limited",
|
|
94
|
+
"network",
|
|
95
|
+
"config_invalid",
|
|
96
|
+
"app_not_found",
|
|
97
|
+
"permission_denied",
|
|
98
|
+
"unknown"
|
|
99
|
+
];
|
|
100
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
101
|
+
constructor(code, message, status, cause) {
|
|
82
102
|
super(message);
|
|
83
103
|
this.name = "IQAuthError";
|
|
84
104
|
this.code = code;
|
|
85
105
|
this.status = status;
|
|
86
|
-
this.
|
|
106
|
+
this.cause = cause;
|
|
107
|
+
this.raw = cause;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
111
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
112
|
+
*/
|
|
113
|
+
static isIQAuthError(value) {
|
|
114
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Type-narrowed code check. Lets callers write
|
|
118
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
119
|
+
* taxonomy without losing the ability to handle server codes via
|
|
120
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
121
|
+
*/
|
|
122
|
+
is(code) {
|
|
123
|
+
return this.code === code;
|
|
87
124
|
}
|
|
88
125
|
};
|
|
89
126
|
var ErrorCodes = {
|
|
@@ -138,7 +175,7 @@ function resolveRetry(cfg) {
|
|
|
138
175
|
}
|
|
139
176
|
function sleep(ms) {
|
|
140
177
|
if (ms <= 0) return Promise.resolve();
|
|
141
|
-
return new Promise((
|
|
178
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
142
179
|
}
|
|
143
180
|
var HttpClient = class {
|
|
144
181
|
constructor(config) {
|
|
@@ -234,7 +271,7 @@ var HttpClient = class {
|
|
|
234
271
|
headers: this.buildHeaders(),
|
|
235
272
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
236
273
|
const refreshToken = this.config.getRefreshToken();
|
|
237
|
-
if (!refreshToken) throw new IQAuthError("
|
|
274
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
238
275
|
return { body: JSON.stringify({ refreshToken }) };
|
|
239
276
|
})()
|
|
240
277
|
});
|
|
@@ -251,7 +288,7 @@ var HttpClient = class {
|
|
|
251
288
|
return;
|
|
252
289
|
}
|
|
253
290
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
254
|
-
throw new IQAuthError("
|
|
291
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
255
292
|
}
|
|
256
293
|
const tokens = {
|
|
257
294
|
accessToken: body.data.accessToken,
|
|
@@ -269,7 +306,7 @@ var HttpClient = class {
|
|
|
269
306
|
return this.requestWithRetry(method, path, body, options, false);
|
|
270
307
|
}
|
|
271
308
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
272
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
309
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
273
310
|
await this.attemptRefresh();
|
|
274
311
|
}
|
|
275
312
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -356,17 +393,27 @@ function parseLoginResponse(data, browserSessionMode) {
|
|
|
356
393
|
tenants: data.tenants
|
|
357
394
|
};
|
|
358
395
|
}
|
|
396
|
+
if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
|
|
397
|
+
return {
|
|
398
|
+
status: "scope_selection",
|
|
399
|
+
scopeSelectionToken: data.scopeSelectionToken,
|
|
400
|
+
tenantId: data.tenantId,
|
|
401
|
+
scopes: data.scopes
|
|
402
|
+
};
|
|
403
|
+
}
|
|
359
404
|
throw new Error("Unexpected login response shape");
|
|
360
405
|
}
|
|
361
406
|
var AuthModule = class {
|
|
362
407
|
constructor(http) {
|
|
363
408
|
this.http = http;
|
|
364
409
|
}
|
|
365
|
-
async login(email, password) {
|
|
410
|
+
async login(email, password, opts) {
|
|
411
|
+
const body = { email, password };
|
|
412
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
366
413
|
const data = await this.http.request(
|
|
367
414
|
"POST",
|
|
368
415
|
"/api/v1/auth/login",
|
|
369
|
-
|
|
416
|
+
body,
|
|
370
417
|
{ skipAutoRefresh: true }
|
|
371
418
|
);
|
|
372
419
|
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
@@ -404,13 +451,29 @@ var AuthModule = class {
|
|
|
404
451
|
method
|
|
405
452
|
}, { skipAutoRefresh: true });
|
|
406
453
|
}
|
|
407
|
-
async selectTenant(tenantSelectionToken, tenantId) {
|
|
454
|
+
async selectTenant(tenantSelectionToken, tenantId, opts) {
|
|
455
|
+
const body = { tenantSelectionToken, tenantId };
|
|
456
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
408
457
|
const data = await this.http.request(
|
|
409
458
|
"POST",
|
|
410
459
|
"/api/v1/auth/select-tenant",
|
|
460
|
+
body,
|
|
461
|
+
{ skipAutoRefresh: true }
|
|
462
|
+
);
|
|
463
|
+
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Task #171 — redeem a scope-selection token + chosen membership for a
|
|
467
|
+
* real authenticated session. `membershipId` must be one of the scopes
|
|
468
|
+
* returned in the prior `scope_selection` envelope.
|
|
469
|
+
*/
|
|
470
|
+
async selectScope(scopeSelectionToken, membershipId) {
|
|
471
|
+
const data = await this.http.request(
|
|
472
|
+
"POST",
|
|
473
|
+
"/api/v1/auth/select-scope",
|
|
411
474
|
{
|
|
412
|
-
|
|
413
|
-
|
|
475
|
+
scopeSelectionToken,
|
|
476
|
+
membershipId
|
|
414
477
|
},
|
|
415
478
|
{ skipAutoRefresh: true }
|
|
416
479
|
);
|
|
@@ -497,6 +560,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
497
560
|
"iqvalidate"
|
|
498
561
|
];
|
|
499
562
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
563
|
+
function classifyJoseError(err) {
|
|
564
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
565
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
566
|
+
}
|
|
567
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
568
|
+
return { code: "token_invalid", message: err.message };
|
|
569
|
+
}
|
|
570
|
+
if (err instanceof Error) {
|
|
571
|
+
return { code: "token_invalid", message: err.message };
|
|
572
|
+
}
|
|
573
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
574
|
+
}
|
|
500
575
|
function decodeProtectedHeader(token) {
|
|
501
576
|
const parts = token.split(".");
|
|
502
577
|
if (parts.length < 2) return null;
|
|
@@ -533,11 +608,11 @@ var TokensModule = class {
|
|
|
533
608
|
async verify(token, options = {}) {
|
|
534
609
|
const header = decodeProtectedHeader(token);
|
|
535
610
|
if (!header) {
|
|
536
|
-
throw new IQAuthError("
|
|
611
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
537
612
|
}
|
|
538
613
|
const kid = header.kid;
|
|
539
614
|
if (!kid) {
|
|
540
|
-
throw new IQAuthError("
|
|
615
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
541
616
|
}
|
|
542
617
|
let cache = await this.ensureCache();
|
|
543
618
|
if (!cache.byKid.has(kid)) {
|
|
@@ -545,7 +620,7 @@ var TokensModule = class {
|
|
|
545
620
|
cache = await this.ensureCache();
|
|
546
621
|
}
|
|
547
622
|
if (!cache.byKid.has(kid)) {
|
|
548
|
-
throw new IQAuthError("
|
|
623
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
549
624
|
}
|
|
550
625
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
551
626
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -561,16 +636,8 @@ var TokensModule = class {
|
|
|
561
636
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
562
637
|
return payload;
|
|
563
638
|
} catch (err) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
568
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
569
|
-
}
|
|
570
|
-
if (err instanceof Error) {
|
|
571
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
572
|
-
}
|
|
573
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
639
|
+
const classified = classifyJoseError(err);
|
|
640
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
574
641
|
}
|
|
575
642
|
}
|
|
576
643
|
/**
|
|
@@ -612,7 +679,7 @@ var TokensModule = class {
|
|
|
612
679
|
getClaims(token) {
|
|
613
680
|
const claims = this.decode(token);
|
|
614
681
|
if (!claims) {
|
|
615
|
-
throw new IQAuthError("
|
|
682
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
616
683
|
}
|
|
617
684
|
return claims;
|
|
618
685
|
}
|
|
@@ -622,7 +689,7 @@ var TokensModule = class {
|
|
|
622
689
|
}
|
|
623
690
|
await this.refreshJwks();
|
|
624
691
|
if (!this.jwksCache) {
|
|
625
|
-
throw new IQAuthError("
|
|
692
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
626
693
|
}
|
|
627
694
|
return this.jwksCache;
|
|
628
695
|
}
|
|
@@ -632,22 +699,38 @@ var TokensModule = class {
|
|
|
632
699
|
}
|
|
633
700
|
this.inFlightRefresh = (async () => {
|
|
634
701
|
try {
|
|
635
|
-
|
|
702
|
+
let res;
|
|
703
|
+
try {
|
|
704
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
throw new IQAuthError(
|
|
707
|
+
"network",
|
|
708
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
709
|
+
void 0,
|
|
710
|
+
err
|
|
711
|
+
);
|
|
712
|
+
}
|
|
636
713
|
if (!res.ok) {
|
|
637
714
|
throw new IQAuthError(
|
|
638
|
-
"
|
|
639
|
-
`Failed to fetch JWKS: ${res.status}
|
|
715
|
+
"jwks_fetch_failed",
|
|
716
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
717
|
+
res.status
|
|
640
718
|
);
|
|
641
719
|
}
|
|
642
720
|
let jwks;
|
|
643
721
|
try {
|
|
644
722
|
jwks = await res.json();
|
|
645
|
-
} catch {
|
|
646
|
-
throw new IQAuthError(
|
|
723
|
+
} catch (err) {
|
|
724
|
+
throw new IQAuthError(
|
|
725
|
+
"jwks_fetch_failed",
|
|
726
|
+
"Malformed JWKS response: invalid JSON",
|
|
727
|
+
res.status,
|
|
728
|
+
err
|
|
729
|
+
);
|
|
647
730
|
}
|
|
648
731
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
649
732
|
throw new IQAuthError(
|
|
650
|
-
"
|
|
733
|
+
"jwks_fetch_failed",
|
|
651
734
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
652
735
|
);
|
|
653
736
|
}
|
|
@@ -655,7 +738,7 @@ var TokensModule = class {
|
|
|
655
738
|
for (const key of jwks.keys) {
|
|
656
739
|
if (!key || typeof key.kid !== "string" || typeof key.n !== "string" && typeof key.x !== "string" || key.kty === "RSA" && (typeof key.n !== "string" || typeof key.e !== "string")) {
|
|
657
740
|
throw new IQAuthError(
|
|
658
|
-
"
|
|
741
|
+
"jwks_fetch_failed",
|
|
659
742
|
"Malformed JWKS response: key missing required fields"
|
|
660
743
|
);
|
|
661
744
|
}
|
|
@@ -673,6 +756,19 @@ var TokensModule = class {
|
|
|
673
756
|
clearCache() {
|
|
674
757
|
this.jwksCache = null;
|
|
675
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
761
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
762
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
763
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
764
|
+
*/
|
|
765
|
+
async prewarm() {
|
|
766
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
767
|
+
try {
|
|
768
|
+
await this.refreshJwks();
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
}
|
|
676
772
|
};
|
|
677
773
|
|
|
678
774
|
// src/modules/sessions.ts
|
|
@@ -996,14 +1092,14 @@ var OidcModule = class {
|
|
|
996
1092
|
*/
|
|
997
1093
|
async handleCallback(params) {
|
|
998
1094
|
if (!params.state) {
|
|
999
|
-
throw new IQAuthError("
|
|
1095
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
1000
1096
|
}
|
|
1001
1097
|
if (!params.code) {
|
|
1002
|
-
throw new IQAuthError("
|
|
1098
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
1003
1099
|
}
|
|
1004
1100
|
const stored = await this.stateStore.get(params.state);
|
|
1005
1101
|
if (!stored) {
|
|
1006
|
-
throw new IQAuthError("
|
|
1102
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
1007
1103
|
}
|
|
1008
1104
|
let tokens;
|
|
1009
1105
|
try {
|
|
@@ -1021,7 +1117,7 @@ var OidcModule = class {
|
|
|
1021
1117
|
if (tokens.id_token) {
|
|
1022
1118
|
if (!this.tokensModule) {
|
|
1023
1119
|
throw new IQAuthError(
|
|
1024
|
-
"
|
|
1120
|
+
"config_invalid",
|
|
1025
1121
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
1026
1122
|
);
|
|
1027
1123
|
}
|
|
@@ -1032,7 +1128,7 @@ var OidcModule = class {
|
|
|
1032
1128
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
1033
1129
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
1034
1130
|
throw new IQAuthError(
|
|
1035
|
-
"
|
|
1131
|
+
"token_invalid",
|
|
1036
1132
|
"OIDC id_token nonce did not match the stored value"
|
|
1037
1133
|
);
|
|
1038
1134
|
}
|
|
@@ -1233,6 +1329,9 @@ var AppsModule = class {
|
|
|
1233
1329
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1234
1330
|
*/
|
|
1235
1331
|
async get(appKey) {
|
|
1332
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1333
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1334
|
+
}
|
|
1236
1335
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1237
1336
|
}
|
|
1238
1337
|
/**
|
|
@@ -1252,6 +1351,16 @@ var AppsModule = class {
|
|
|
1252
1351
|
401
|
|
1253
1352
|
);
|
|
1254
1353
|
}
|
|
1354
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1355
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1356
|
+
}
|
|
1357
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1358
|
+
throw new IQAuthError(
|
|
1359
|
+
"ENVIRONMENT_REQUIRED",
|
|
1360
|
+
"manifest.environment is required and must be 'production', 'staging', or 'development'. This guards against a dev workstation silently overwriting a production app's permission tree.",
|
|
1361
|
+
400
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1255
1364
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1256
1365
|
}
|
|
1257
1366
|
/**
|
|
@@ -1261,11 +1370,14 @@ var AppsModule = class {
|
|
|
1261
1370
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1262
1371
|
*/
|
|
1263
1372
|
async isRegistered(appKey) {
|
|
1373
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1374
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1375
|
+
}
|
|
1264
1376
|
try {
|
|
1265
1377
|
await this.get(appKey);
|
|
1266
1378
|
return true;
|
|
1267
1379
|
} catch (err) {
|
|
1268
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1380
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1269
1381
|
return false;
|
|
1270
1382
|
}
|
|
1271
1383
|
throw err;
|
|
@@ -1302,6 +1414,20 @@ var RolesModule = class {
|
|
|
1302
1414
|
};
|
|
1303
1415
|
|
|
1304
1416
|
// src/modules/permissionGroups.ts
|
|
1417
|
+
function assertAppKey(appKey, callsite) {
|
|
1418
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1419
|
+
throw new IQAuthError(
|
|
1420
|
+
"VALIDATION_ERROR",
|
|
1421
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1422
|
+
400
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1427
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1428
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1305
1431
|
var PermissionGroupsModule = class {
|
|
1306
1432
|
constructor(http) {
|
|
1307
1433
|
this.http = http;
|
|
@@ -1322,7 +1448,14 @@ var PermissionGroupsModule = class {
|
|
|
1322
1448
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1323
1449
|
}
|
|
1324
1450
|
async addPermission(tenantId, groupId, data) {
|
|
1325
|
-
|
|
1451
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1452
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1453
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1454
|
+
appKey: data.appKey,
|
|
1455
|
+
nodeKey: data.nodeKey,
|
|
1456
|
+
effect: data.effect,
|
|
1457
|
+
weight: data.weight
|
|
1458
|
+
});
|
|
1326
1459
|
}
|
|
1327
1460
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1328
1461
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1346,21 +1479,51 @@ var PermissionGroupsModule = class {
|
|
|
1346
1479
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1347
1480
|
}
|
|
1348
1481
|
async addUserOverride(tenantId, userId, data) {
|
|
1349
|
-
|
|
1482
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1483
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1484
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1485
|
+
appKey: data.appKey,
|
|
1486
|
+
nodeKey: data.nodeKey,
|
|
1487
|
+
effect: data.effect,
|
|
1488
|
+
weight: data.weight,
|
|
1489
|
+
expiresAt: data.expiresAt
|
|
1490
|
+
});
|
|
1350
1491
|
}
|
|
1351
1492
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1352
1493
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1353
1494
|
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1497
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1498
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1499
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1500
|
+
*/
|
|
1354
1501
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
const qs = query.toString();
|
|
1359
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1502
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1503
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1504
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1360
1505
|
}
|
|
1361
1506
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1507
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1508
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1362
1509
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1363
1510
|
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1513
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1514
|
+
* single misconfigured entry can't slip through and silently report
|
|
1515
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1516
|
+
*/
|
|
1517
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1518
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1519
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1520
|
+
}
|
|
1521
|
+
checks.forEach((c, i) => {
|
|
1522
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1523
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1524
|
+
});
|
|
1525
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1526
|
+
}
|
|
1364
1527
|
};
|
|
1365
1528
|
|
|
1366
1529
|
// src/modules/apiKeys.ts
|
|
@@ -1785,6 +1948,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1785
1948
|
this._refreshToken = tokens.refreshToken;
|
|
1786
1949
|
},
|
|
1787
1950
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1951
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1952
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1953
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1954
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1788
1955
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1789
1956
|
sessionHeaderName: config.sessionHeaderName,
|
|
1790
1957
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1825,6 +1992,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1825
1992
|
static forServer(config) {
|
|
1826
1993
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1827
1994
|
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1997
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1998
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1999
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
2000
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
2001
|
+
*/
|
|
1828
2002
|
static forMobile(config) {
|
|
1829
2003
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1830
2004
|
}
|
|
@@ -1841,6 +2015,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1841
2015
|
getRefreshToken() {
|
|
1842
2016
|
return this._refreshToken;
|
|
1843
2017
|
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
2020
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
2021
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
2022
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
2023
|
+
*/
|
|
2024
|
+
async prewarm() {
|
|
2025
|
+
await Promise.all([
|
|
2026
|
+
this.tokens.prewarm(),
|
|
2027
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
2028
|
+
]);
|
|
2029
|
+
}
|
|
1844
2030
|
getCurrentClaims() {
|
|
1845
2031
|
if (!this._accessToken) return null;
|
|
1846
2032
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1911,14 +2097,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1911
2097
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1912
2098
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1913
2099
|
throw new IQAuthError(
|
|
1914
|
-
"
|
|
2100
|
+
"config_invalid",
|
|
1915
2101
|
`${ctx}IQAuth publishable key is missing. Set IQAUTH_PUBLISHABLE_KEY (or pass publishableKey) to a pk_test_\u2026 or pk_live_\u2026 value from the IQAuth admin console.`
|
|
1916
2102
|
);
|
|
1917
2103
|
}
|
|
1918
2104
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1919
2105
|
if (!shapeMatch) {
|
|
1920
2106
|
throw new IQAuthError(
|
|
1921
|
-
"
|
|
2107
|
+
"config_invalid",
|
|
1922
2108
|
`${ctx}IQAuth publishable key is malformed (got ${raw.slice(0, 12)}\u2026). Expected pk_test_\u2026 or pk_live_\u2026; regenerate the key from the IQAuth admin console.`
|
|
1923
2109
|
);
|
|
1924
2110
|
}
|
|
@@ -1927,19 +2113,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1927
2113
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1928
2114
|
} catch {
|
|
1929
2115
|
throw new IQAuthError(
|
|
1930
|
-
"
|
|
2116
|
+
"config_invalid",
|
|
1931
2117
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1932
2118
|
);
|
|
1933
2119
|
}
|
|
1934
2120
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1935
2121
|
throw new IQAuthError(
|
|
1936
|
-
"
|
|
2122
|
+
"config_invalid",
|
|
1937
2123
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1938
2124
|
);
|
|
1939
2125
|
}
|
|
1940
2126
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1941
2127
|
throw new IQAuthError(
|
|
1942
|
-
"
|
|
2128
|
+
"config_invalid",
|
|
1943
2129
|
`${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console \u2014 the new key will encode a valid issuer URL.`
|
|
1944
2130
|
);
|
|
1945
2131
|
}
|
|
@@ -1959,12 +2145,18 @@ function isSecretKey(raw) {
|
|
|
1959
2145
|
|
|
1960
2146
|
// src/middleware/express.ts
|
|
1961
2147
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2148
|
+
// Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
|
|
1962
2149
|
"TOKEN_INVALID",
|
|
1963
2150
|
"TOKEN_EXPIRED",
|
|
1964
2151
|
"TOKEN_REVOKED",
|
|
1965
2152
|
"SESSION_EXPIRED",
|
|
1966
2153
|
"SESSION_INVALID",
|
|
1967
|
-
"AUTH_REQUIRED"
|
|
2154
|
+
"AUTH_REQUIRED",
|
|
2155
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
|
|
2156
|
+
// Mapped to 401 here so framework consumers don't have to learn the new
|
|
2157
|
+
// codes to keep their auth-failure handling working.
|
|
2158
|
+
"token_invalid",
|
|
2159
|
+
"token_expired"
|
|
1968
2160
|
]);
|
|
1969
2161
|
var DEFAULT_ACCESS_COOKIE = "iqauth_at";
|
|
1970
2162
|
function getAuthorizationHeader(req) {
|
|
@@ -2146,6 +2338,246 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2146
2338
|
};
|
|
2147
2339
|
}
|
|
2148
2340
|
|
|
2341
|
+
// src/server/handlers.ts
|
|
2342
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2343
|
+
const baseUser = {
|
|
2344
|
+
sub: claims.sub,
|
|
2345
|
+
email: claims.email,
|
|
2346
|
+
name: claims.name,
|
|
2347
|
+
tenantId: claims.tenantId,
|
|
2348
|
+
vendorId: claims.vendorId,
|
|
2349
|
+
roles: claims.roles ?? [],
|
|
2350
|
+
entitlements: claims.entitlements ?? [],
|
|
2351
|
+
// Task #171 — project the active source/client scope onto the userinfo
|
|
2352
|
+
// payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
|
|
2353
|
+
// expose it without consumers having to re-decode the JWT.
|
|
2354
|
+
...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
|
|
2355
|
+
};
|
|
2356
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2357
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2358
|
+
return {
|
|
2359
|
+
success: true,
|
|
2360
|
+
data: {
|
|
2361
|
+
user,
|
|
2362
|
+
claims,
|
|
2363
|
+
tenantId: claims.tenantId ?? null
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
2368
|
+
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2369
|
+
function assertCookiePrefixInvariants(name, secure, path, domain) {
|
|
2370
|
+
if (name.startsWith("__Host-")) {
|
|
2371
|
+
if (!secure) {
|
|
2372
|
+
throw new IQAuthError(
|
|
2373
|
+
"config_invalid",
|
|
2374
|
+
`Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
if (path !== "/") {
|
|
2378
|
+
throw new IQAuthError(
|
|
2379
|
+
"config_invalid",
|
|
2380
|
+
`Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
if (domain) {
|
|
2384
|
+
throw new IQAuthError(
|
|
2385
|
+
"config_invalid",
|
|
2386
|
+
`Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
} else if (name.startsWith("__Secure-") && !secure) {
|
|
2390
|
+
throw new IQAuthError(
|
|
2391
|
+
"config_invalid",
|
|
2392
|
+
`Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function resolve(config) {
|
|
2397
|
+
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
2398
|
+
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2399
|
+
maybeWarnDefaultSignoutRegistry(config);
|
|
2400
|
+
const secure = config.secure ?? true;
|
|
2401
|
+
if (config.secure === false && config.allowInsecureCookies !== true) {
|
|
2402
|
+
throw new IQAuthError(
|
|
2403
|
+
"config_invalid",
|
|
2404
|
+
"Refusing to issue auth cookies with secure:false \u2014 this exposes session cookies over plaintext HTTP. For local HTTP development, set allowInsecureCookies:true to acknowledge the risk. Production MUST use HTTPS with secure cookies."
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
|
|
2408
|
+
const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
|
|
2409
|
+
const stateCookieName = config.stateCookieName ?? "iqauth_state";
|
|
2410
|
+
const cookiePath = config.cookiePath ?? "/";
|
|
2411
|
+
const cookieDomain = config.cookieDomain;
|
|
2412
|
+
for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
|
|
2413
|
+
assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
|
|
2414
|
+
}
|
|
2415
|
+
return {
|
|
2416
|
+
publishableKey: config.publishableKey,
|
|
2417
|
+
secretKey: config.secretKey,
|
|
2418
|
+
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
2419
|
+
accessCookieName,
|
|
2420
|
+
refreshCookieName,
|
|
2421
|
+
cookieDomain,
|
|
2422
|
+
sameSite: config.sameSite ?? "lax",
|
|
2423
|
+
secure,
|
|
2424
|
+
cookiePath,
|
|
2425
|
+
tokenPath: config.tokenPath ?? "/oidc/token",
|
|
2426
|
+
refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
|
|
2427
|
+
logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
|
|
2428
|
+
fetchImpl: config.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
2429
|
+
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
2430
|
+
})),
|
|
2431
|
+
appId: parsed.appId,
|
|
2432
|
+
tenantId: parsed.tenantId,
|
|
2433
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2434
|
+
debug: config.debug,
|
|
2435
|
+
onTimingEvent: config.onTimingEvent,
|
|
2436
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2437
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
|
|
2438
|
+
requireOAuthState: config.requireOAuthState ?? true,
|
|
2439
|
+
stateCookieName: config.stateCookieName ?? "iqauth_state"
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2443
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2444
|
+
function pruneInMemoryMarkers(now) {
|
|
2445
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2446
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2447
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
var defaultSignoutRegistry = {
|
|
2451
|
+
mark(token, ttlMs) {
|
|
2452
|
+
const now = Date.now();
|
|
2453
|
+
pruneInMemoryMarkers(now);
|
|
2454
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2455
|
+
},
|
|
2456
|
+
has(token) {
|
|
2457
|
+
const now = Date.now();
|
|
2458
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2459
|
+
if (!exp) return false;
|
|
2460
|
+
if (exp <= now) {
|
|
2461
|
+
inMemorySignoutMarkers.delete(token);
|
|
2462
|
+
return false;
|
|
2463
|
+
}
|
|
2464
|
+
return true;
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
var warnedDefaultSignoutRegistry = false;
|
|
2468
|
+
function maybeWarnDefaultSignoutRegistry(config) {
|
|
2469
|
+
if (warnedDefaultSignoutRegistry) return;
|
|
2470
|
+
if (config.signoutRegistry) return;
|
|
2471
|
+
warnedDefaultSignoutRegistry = true;
|
|
2472
|
+
console.warn(
|
|
2473
|
+
"[IQAuth] Using the in-memory signout registry (process-local). Signout idempotency is NOT shared across instances \u2014 in a multi-replica deployment a /refresh racing a /signout on another replica can reissue cookies after sign-out. Plug a shared backend (e.g. Redis) into IQAuthHelperConfig.signoutRegistry to fix this and silence this warning."
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2477
|
+
function getTokensFor(issuer) {
|
|
2478
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2479
|
+
if (!m) {
|
|
2480
|
+
m = new TokensModule(issuer);
|
|
2481
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2482
|
+
}
|
|
2483
|
+
return m;
|
|
2484
|
+
}
|
|
2485
|
+
async function handleUserinfo(config, input) {
|
|
2486
|
+
const cfg = resolve(config);
|
|
2487
|
+
if (!input.accessToken) {
|
|
2488
|
+
return {
|
|
2489
|
+
status: 401,
|
|
2490
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2491
|
+
cookies: []
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
let claims;
|
|
2495
|
+
try {
|
|
2496
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
|
|
2497
|
+
issuer: cfg.issuer,
|
|
2498
|
+
...config.verify
|
|
2499
|
+
});
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2502
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2503
|
+
return {
|
|
2504
|
+
status: 401,
|
|
2505
|
+
body: { success: false, error: { code, message } },
|
|
2506
|
+
cookies: []
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2510
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2511
|
+
});
|
|
2512
|
+
return {
|
|
2513
|
+
status: 200,
|
|
2514
|
+
body: envelope,
|
|
2515
|
+
cookies: []
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// src/permissions/wildcard.ts
|
|
2520
|
+
var SUFFIX = ".*";
|
|
2521
|
+
function wildcardPrefix(pattern) {
|
|
2522
|
+
return pattern.slice(0, -SUFFIX.length);
|
|
2523
|
+
}
|
|
2524
|
+
function hasPermission(set, id) {
|
|
2525
|
+
if (!id) return false;
|
|
2526
|
+
if (!set) return false;
|
|
2527
|
+
if (id === "*") {
|
|
2528
|
+
for (const entry of set) if (entry === "*") return true;
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
const queryIsWildcard = id.endsWith(SUFFIX);
|
|
2532
|
+
const queryPrefix = queryIsWildcard ? wildcardPrefix(id) : null;
|
|
2533
|
+
for (const entry of set) {
|
|
2534
|
+
if (!entry) continue;
|
|
2535
|
+
if (entry === "*") return true;
|
|
2536
|
+
if (entry === id) return true;
|
|
2537
|
+
if (entry.endsWith(SUFFIX)) {
|
|
2538
|
+
const prefix = wildcardPrefix(entry);
|
|
2539
|
+
if (!queryIsWildcard) {
|
|
2540
|
+
if (id === prefix) return true;
|
|
2541
|
+
if (id.startsWith(prefix + ".")) return true;
|
|
2542
|
+
} else {
|
|
2543
|
+
if (queryPrefix === prefix) return true;
|
|
2544
|
+
if (queryPrefix !== null && queryPrefix.startsWith(prefix + ".")) return true;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return false;
|
|
2549
|
+
}
|
|
2550
|
+
function expandPermissions(set) {
|
|
2551
|
+
if (!set) return [];
|
|
2552
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2553
|
+
for (const raw of set) {
|
|
2554
|
+
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
2555
|
+
seen.add(raw);
|
|
2556
|
+
}
|
|
2557
|
+
if (seen.has("*")) return ["*"];
|
|
2558
|
+
const wildcards = [];
|
|
2559
|
+
for (const entry of seen) if (entry.endsWith(SUFFIX)) wildcards.push(entry);
|
|
2560
|
+
const out = [];
|
|
2561
|
+
for (const entry of seen) {
|
|
2562
|
+
let covered = false;
|
|
2563
|
+
for (const w of wildcards) {
|
|
2564
|
+
if (w === entry) continue;
|
|
2565
|
+
const prefix = wildcardPrefix(w);
|
|
2566
|
+
if (entry === prefix) {
|
|
2567
|
+
covered = true;
|
|
2568
|
+
break;
|
|
2569
|
+
}
|
|
2570
|
+
if (entry.startsWith(prefix + ".")) {
|
|
2571
|
+
covered = true;
|
|
2572
|
+
break;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
if (!covered) out.push(entry);
|
|
2576
|
+
}
|
|
2577
|
+
out.sort();
|
|
2578
|
+
return out;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2149
2581
|
// src/ws.ts
|
|
2150
2582
|
var DEFAULT_COOKIE = "iqauth_at";
|
|
2151
2583
|
var DEFAULT_SUBPROTOCOL_PREFIX = "iqauth.bearer.";
|
|
@@ -2244,10 +2676,10 @@ function jwkFromPublicKey(publicKey, kid) {
|
|
|
2244
2676
|
return { kty: "RSA", use: "sig", alg: "RS256", kid, n: jwk.n, e: jwk.e };
|
|
2245
2677
|
}
|
|
2246
2678
|
function readBody(req) {
|
|
2247
|
-
return new Promise((
|
|
2679
|
+
return new Promise((resolve2, reject) => {
|
|
2248
2680
|
const chunks = [];
|
|
2249
2681
|
req.on("data", (c) => chunks.push(c));
|
|
2250
|
-
req.on("end", () =>
|
|
2682
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2251
2683
|
req.on("error", reject);
|
|
2252
2684
|
});
|
|
2253
2685
|
}
|
|
@@ -2435,11 +2867,11 @@ async function createTestIssuer(options = {}) {
|
|
|
2435
2867
|
const server = (0, import_http2.createServer)((req, res) => {
|
|
2436
2868
|
void handler(req, res);
|
|
2437
2869
|
});
|
|
2438
|
-
await new Promise((
|
|
2870
|
+
await new Promise((resolve2, reject) => {
|
|
2439
2871
|
server.once("error", reject);
|
|
2440
2872
|
server.listen(port, host, () => {
|
|
2441
2873
|
server.off("error", reject);
|
|
2442
|
-
|
|
2874
|
+
resolve2();
|
|
2443
2875
|
});
|
|
2444
2876
|
});
|
|
2445
2877
|
const addr = server.address();
|
|
@@ -2463,8 +2895,8 @@ async function createTestIssuer(options = {}) {
|
|
|
2463
2895
|
pendingCodes.set(code, { claims: opts, refreshToken });
|
|
2464
2896
|
return code;
|
|
2465
2897
|
},
|
|
2466
|
-
close: () => new Promise((
|
|
2467
|
-
server.close((err) => err ? reject(err) :
|
|
2898
|
+
close: () => new Promise((resolve2, reject) => {
|
|
2899
|
+
server.close((err) => err ? reject(err) : resolve2());
|
|
2468
2900
|
})
|
|
2469
2901
|
};
|
|
2470
2902
|
}
|
|
@@ -2478,6 +2910,12 @@ var WebhookSignatureError = class extends Error {
|
|
|
2478
2910
|
this.code = code;
|
|
2479
2911
|
}
|
|
2480
2912
|
};
|
|
2913
|
+
var IQAUTH_SIGNATURE_HEADER = "x-iqauth-signature";
|
|
2914
|
+
var LEGACY_SIGNATURE_HEADERS = [
|
|
2915
|
+
"x-webhook-signature",
|
|
2916
|
+
"x-iq-auth-signature",
|
|
2917
|
+
"x-signature"
|
|
2918
|
+
];
|
|
2481
2919
|
function toBuffer(p) {
|
|
2482
2920
|
if (typeof p === "string") return Buffer.from(p, "utf8");
|
|
2483
2921
|
if (Buffer.isBuffer(p)) return p;
|
|
@@ -2486,13 +2924,19 @@ function toBuffer(p) {
|
|
|
2486
2924
|
function parseHeader(header) {
|
|
2487
2925
|
let t = NaN;
|
|
2488
2926
|
const v1 = [];
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2927
|
+
const trimmed = header.trim();
|
|
2928
|
+
if (/^[0-9a-f]+$/i.test(trimmed)) {
|
|
2929
|
+
v1.push(trimmed.toLowerCase());
|
|
2930
|
+
return { t, v1 };
|
|
2931
|
+
}
|
|
2932
|
+
for (const part of trimmed.split(",")) {
|
|
2933
|
+
const eqIdx = part.indexOf("=");
|
|
2934
|
+
if (eqIdx === -1) continue;
|
|
2935
|
+
const key = part.slice(0, eqIdx).trim().toLowerCase();
|
|
2936
|
+
const value = part.slice(eqIdx + 1).trim();
|
|
2937
|
+
if (!value) continue;
|
|
2494
2938
|
if (key === "t") t = Number(value);
|
|
2495
|
-
else if (key === "v1") v1.push(value);
|
|
2939
|
+
else if (key === "v1") v1.push(value.toLowerCase());
|
|
2496
2940
|
}
|
|
2497
2941
|
return { t, v1 };
|
|
2498
2942
|
}
|
|
@@ -2504,6 +2948,11 @@ function timingSafeEqualHex(a, b) {
|
|
|
2504
2948
|
return false;
|
|
2505
2949
|
}
|
|
2506
2950
|
}
|
|
2951
|
+
function computeSignatures(secret, body, t) {
|
|
2952
|
+
const modern = import_crypto3.default.createHmac("sha256", secret).update(body).digest("hex");
|
|
2953
|
+
const legacy = Number.isFinite(t) ? import_crypto3.default.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex") : null;
|
|
2954
|
+
return { modern, legacy };
|
|
2955
|
+
}
|
|
2507
2956
|
function verifyWebhookSignature(opts) {
|
|
2508
2957
|
const headerRaw = Array.isArray(opts.header) ? opts.header[0] : opts.header;
|
|
2509
2958
|
if (!headerRaw || typeof headerRaw !== "string") {
|
|
@@ -2513,20 +2962,23 @@ function verifyWebhookSignature(opts) {
|
|
|
2513
2962
|
throw new WebhookSignatureError("MISSING_SECRET", "secret is required");
|
|
2514
2963
|
}
|
|
2515
2964
|
const { t, v1 } = parseHeader(headerRaw);
|
|
2516
|
-
if (
|
|
2965
|
+
if (v1.length === 0) {
|
|
2517
2966
|
throw new WebhookSignatureError("MALFORMED_HEADER", `Could not parse signature header: ${headerRaw}`);
|
|
2518
2967
|
}
|
|
2519
2968
|
const tolerance = opts.toleranceSeconds ?? 300;
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2969
|
+
if (Number.isFinite(t)) {
|
|
2970
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
2971
|
+
if (Math.abs(now - t) > tolerance) {
|
|
2972
|
+
throw new WebhookSignatureError(
|
|
2973
|
+
"TIMESTAMP_OUT_OF_TOLERANCE",
|
|
2974
|
+
`Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2526
2977
|
}
|
|
2527
2978
|
const body = toBuffer(opts.payload);
|
|
2528
|
-
const
|
|
2529
|
-
const
|
|
2979
|
+
const { modern, legacy } = computeSignatures(opts.secret, body, t);
|
|
2980
|
+
const expected = Number.isFinite(t) ? legacy : modern;
|
|
2981
|
+
const matched = expected !== null && v1.some((sig) => timingSafeEqualHex(sig.toLowerCase(), expected));
|
|
2530
2982
|
if (!matched) {
|
|
2531
2983
|
throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
|
|
2532
2984
|
}
|
|
@@ -2536,6 +2988,29 @@ function verifyWebhookSignature(opts) {
|
|
|
2536
2988
|
} catch {
|
|
2537
2989
|
throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
|
|
2538
2990
|
}
|
|
2991
|
+
if (!Number.isFinite(t) && !opts.allowMissingTimestamp) {
|
|
2992
|
+
const rawTime = parsed.time;
|
|
2993
|
+
if (typeof rawTime !== "string" || !rawTime) {
|
|
2994
|
+
throw new WebhookSignatureError(
|
|
2995
|
+
"MISSING_TIMESTAMP",
|
|
2996
|
+
"Modern webhook delivery has no header `t=` and no envelope `time`; cannot enforce replay protection. Use parseWebhookEvent, or set allowMissingTimestamp:true (insecure)."
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
const eventMs = Date.parse(rawTime);
|
|
3000
|
+
if (!Number.isFinite(eventMs)) {
|
|
3001
|
+
throw new WebhookSignatureError(
|
|
3002
|
+
"MALFORMED_TIMESTAMP",
|
|
3003
|
+
`Envelope \`time\` is not a valid ISO timestamp: ${rawTime}`
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
const nowMs = (opts.nowSeconds ?? Math.floor(Date.now() / 1e3)) * 1e3;
|
|
3007
|
+
if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
|
|
3008
|
+
throw new WebhookSignatureError(
|
|
3009
|
+
"TIMESTAMP_OUT_OF_TOLERANCE",
|
|
3010
|
+
`Envelope time ${rawTime} is outside the ${tolerance}s tolerance window`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
2539
3014
|
return parsed;
|
|
2540
3015
|
}
|
|
2541
3016
|
function isValidWebhookSignature(opts) {
|
|
@@ -2546,8 +3021,130 @@ function isValidWebhookSignature(opts) {
|
|
|
2546
3021
|
return false;
|
|
2547
3022
|
}
|
|
2548
3023
|
}
|
|
3024
|
+
function readHeader(headers, name) {
|
|
3025
|
+
if (typeof headers.get === "function") {
|
|
3026
|
+
return headers.get(name);
|
|
3027
|
+
}
|
|
3028
|
+
const lower = name.toLowerCase();
|
|
3029
|
+
const obj = headers;
|
|
3030
|
+
if (lower in obj) return obj[lower];
|
|
3031
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
3032
|
+
if (k.toLowerCase() === lower) return v;
|
|
3033
|
+
}
|
|
3034
|
+
return void 0;
|
|
3035
|
+
}
|
|
3036
|
+
function pickHeaderValue(value) {
|
|
3037
|
+
if (value == null) return null;
|
|
3038
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
3039
|
+
return value;
|
|
3040
|
+
}
|
|
3041
|
+
function envelopeError(message) {
|
|
3042
|
+
throw new WebhookSignatureError("MALFORMED_ENVELOPE", message);
|
|
3043
|
+
}
|
|
3044
|
+
function parseWebhookEvent(rawBody, headers, secrets, opts = {}) {
|
|
3045
|
+
if (!Array.isArray(secrets) || secrets.length === 0 || secrets.every((s) => !s)) {
|
|
3046
|
+
throw new WebhookSignatureError("MISSING_SECRET", "At least one signing secret is required");
|
|
3047
|
+
}
|
|
3048
|
+
let headerValue = pickHeaderValue(readHeader(headers, IQAUTH_SIGNATURE_HEADER));
|
|
3049
|
+
let usedHeader = IQAUTH_SIGNATURE_HEADER;
|
|
3050
|
+
if (!headerValue) {
|
|
3051
|
+
for (const legacy of LEGACY_SIGNATURE_HEADERS) {
|
|
3052
|
+
const v = pickHeaderValue(readHeader(headers, legacy));
|
|
3053
|
+
if (v) {
|
|
3054
|
+
headerValue = v;
|
|
3055
|
+
usedHeader = legacy;
|
|
3056
|
+
const log = opts.onDeprecation ?? ((m) => console.warn(m));
|
|
3057
|
+
log(
|
|
3058
|
+
`[iqauth] deprecation: webhook delivery used legacy header "${legacy}"; migrate sender to "X-IQAuth-Signature" (back-compat removed in next minor).`
|
|
3059
|
+
);
|
|
3060
|
+
break;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
if (!headerValue) {
|
|
3065
|
+
throw new WebhookSignatureError(
|
|
3066
|
+
"MISSING_HEADER",
|
|
3067
|
+
`Missing webhook signature header. Expected "X-IQAuth-Signature" (or one of: ${LEGACY_SIGNATURE_HEADERS.join(", ")}).`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
const { t, v1 } = parseHeader(headerValue);
|
|
3071
|
+
if (v1.length === 0) {
|
|
3072
|
+
throw new WebhookSignatureError(
|
|
3073
|
+
"MALFORMED_HEADER",
|
|
3074
|
+
`Could not parse "${usedHeader}" header value: ${headerValue}`
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
const body = toBuffer(rawBody);
|
|
3078
|
+
let verifiedIdx = -1;
|
|
3079
|
+
for (let i = 0; i < secrets.length; i++) {
|
|
3080
|
+
const secret = secrets[i];
|
|
3081
|
+
if (!secret) continue;
|
|
3082
|
+
const { modern, legacy } = computeSignatures(secret, body, t);
|
|
3083
|
+
const expected = Number.isFinite(t) ? legacy : modern;
|
|
3084
|
+
const ok = expected !== null && v1.some((sig) => timingSafeEqualHex(sig.toLowerCase(), expected));
|
|
3085
|
+
if (ok) {
|
|
3086
|
+
verifiedIdx = i;
|
|
3087
|
+
break;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
if (verifiedIdx === -1) {
|
|
3091
|
+
throw new WebhookSignatureError(
|
|
3092
|
+
"SIGNATURE_MISMATCH",
|
|
3093
|
+
"Webhook signature does not match any provided secret"
|
|
3094
|
+
);
|
|
3095
|
+
}
|
|
3096
|
+
let parsed;
|
|
3097
|
+
try {
|
|
3098
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
3099
|
+
} catch {
|
|
3100
|
+
throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
|
|
3101
|
+
}
|
|
3102
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3103
|
+
envelopeError("Webhook body must be a JSON object");
|
|
3104
|
+
}
|
|
3105
|
+
const { id, type, subject, time, data, tenantId, specversion } = parsed;
|
|
3106
|
+
if (specversion !== "1.0") {
|
|
3107
|
+
envelopeError(`Envelope \`specversion\` must be "1.0" (got: ${JSON.stringify(specversion)})`);
|
|
3108
|
+
}
|
|
3109
|
+
if (typeof id !== "string" || !id) envelopeError("Envelope missing required string `id`");
|
|
3110
|
+
if (typeof type !== "string" || !type) envelopeError("Envelope missing required string `type`");
|
|
3111
|
+
if (typeof subject !== "string" || !subject) envelopeError("Envelope missing required string `subject`");
|
|
3112
|
+
if (typeof time !== "string" || !time) envelopeError("Envelope missing required string `time`");
|
|
3113
|
+
if (typeof tenantId !== "string" || !tenantId) envelopeError("Envelope missing required string `tenantId`");
|
|
3114
|
+
if (data === void 0 || data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
3115
|
+
envelopeError("Envelope `data` must be an object");
|
|
3116
|
+
}
|
|
3117
|
+
const tolerance = opts.toleranceSeconds ?? 300;
|
|
3118
|
+
const eventMs = Date.parse(time);
|
|
3119
|
+
if (!Number.isFinite(eventMs)) envelopeError(`Envelope \`time\` is not a valid ISO timestamp: ${time}`);
|
|
3120
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
3121
|
+
if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
|
|
3122
|
+
throw new WebhookSignatureError(
|
|
3123
|
+
"TIMESTAMP_OUT_OF_TOLERANCE",
|
|
3124
|
+
`Envelope time ${time} is outside the ${tolerance}s tolerance window (now=${new Date(nowMs).toISOString()})`
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
return {
|
|
3128
|
+
specversion: "1.0",
|
|
3129
|
+
id,
|
|
3130
|
+
type,
|
|
3131
|
+
subject,
|
|
3132
|
+
time,
|
|
3133
|
+
tenantId,
|
|
3134
|
+
data,
|
|
3135
|
+
idempotencyKey: id,
|
|
3136
|
+
verifiedWithSecretIndex: verifiedIdx
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
2549
3139
|
|
|
2550
3140
|
// src/server/provisioningBridge.ts
|
|
3141
|
+
var ProvisioningError = class extends Error {
|
|
3142
|
+
constructor(code, message) {
|
|
3143
|
+
super(message);
|
|
3144
|
+
this.name = "ProvisioningError";
|
|
3145
|
+
this.code = code;
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
2551
3148
|
function defaultIsUniqueViolation(err) {
|
|
2552
3149
|
if (!err || typeof err !== "object") return false;
|
|
2553
3150
|
const e = err;
|
|
@@ -2559,6 +3156,16 @@ function defaultIsUniqueViolation(err) {
|
|
|
2559
3156
|
function createProvisioningBridge(options) {
|
|
2560
3157
|
const { storage } = options;
|
|
2561
3158
|
const isUniqueViolation = options.isUniqueViolation ?? defaultIsUniqueViolation;
|
|
3159
|
+
const allowUnverifiedEmailAdopt = options.allowUnverifiedEmailAdopt === true;
|
|
3160
|
+
const emailVerified = (claims) => claims.email_verified === true;
|
|
3161
|
+
const assertAdoptAllowed = (claims) => {
|
|
3162
|
+
if (!allowUnverifiedEmailAdopt && !emailVerified(claims)) {
|
|
3163
|
+
throw new ProvisioningError(
|
|
3164
|
+
"UNVERIFIED_EMAIL_ADOPT_REFUSED",
|
|
3165
|
+
"Refusing to adopt a pre-existing local account from an unverified email (claims.email_verified !== true). Set allowUnverifiedEmailAdopt:true only if your issuer is trusted to never emit unverified emails for adoption."
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
2562
3169
|
const roleOf = (claims) => {
|
|
2563
3170
|
try {
|
|
2564
3171
|
return options.roleMapper?.(claims) ?? null;
|
|
@@ -2575,6 +3182,7 @@ function createProvisioningBridge(options) {
|
|
|
2575
3182
|
if (claims.email) {
|
|
2576
3183
|
const byEmail = await storage.findByEmail(claims.email);
|
|
2577
3184
|
if (byEmail) {
|
|
3185
|
+
assertAdoptAllowed(claims);
|
|
2578
3186
|
if (storage.adoptByEmail) {
|
|
2579
3187
|
const adopted = await storage.adoptByEmail(byEmail, claims, roleOf(claims));
|
|
2580
3188
|
return { user: adopted, claims, created: false, adopted: true };
|
|
@@ -2590,7 +3198,10 @@ function createProvisioningBridge(options) {
|
|
|
2590
3198
|
if (after) return { user: after, claims, created: false, adopted: false };
|
|
2591
3199
|
if (claims.email) {
|
|
2592
3200
|
const byEmail = await storage.findByEmail(claims.email);
|
|
2593
|
-
if (byEmail)
|
|
3201
|
+
if (byEmail) {
|
|
3202
|
+
assertAdoptAllowed(claims);
|
|
3203
|
+
return { user: byEmail, claims, created: false, adopted: true };
|
|
3204
|
+
}
|
|
2594
3205
|
}
|
|
2595
3206
|
throw err;
|
|
2596
3207
|
}
|
|
@@ -2611,10 +3222,13 @@ function createProvisioningBridge(options) {
|
|
|
2611
3222
|
ErrorCodes,
|
|
2612
3223
|
GdprModule,
|
|
2613
3224
|
HierarchyModule,
|
|
3225
|
+
IQAUTH_SIGNATURE_HEADER,
|
|
2614
3226
|
IQAuthClient,
|
|
2615
3227
|
IQAuthError,
|
|
3228
|
+
IQ_AUTH_ERROR_CODES,
|
|
2616
3229
|
InMemoryOidcStateStore,
|
|
2617
3230
|
InvitesModule,
|
|
3231
|
+
LEGACY_SIGNATURE_HEADERS,
|
|
2618
3232
|
MembershipsModule,
|
|
2619
3233
|
MfaModule,
|
|
2620
3234
|
OidcModule,
|
|
@@ -2632,14 +3246,19 @@ function createProvisioningBridge(options) {
|
|
|
2632
3246
|
WebhookSignatureError,
|
|
2633
3247
|
WebhooksModule,
|
|
2634
3248
|
assertPublishableKey,
|
|
3249
|
+
buildUserinfoResponse,
|
|
2635
3250
|
createProvisioningBridge,
|
|
2636
3251
|
createTestIssuer,
|
|
2637
3252
|
encodePublishableKey,
|
|
3253
|
+
expandPermissions,
|
|
3254
|
+
handleUserinfo,
|
|
3255
|
+
hasPermission,
|
|
2638
3256
|
iqAuthMiddleware,
|
|
2639
3257
|
isPublishableKey,
|
|
2640
3258
|
isSecretKey,
|
|
2641
3259
|
isValidWebhookSignature,
|
|
2642
3260
|
parsePublishableKey,
|
|
3261
|
+
parseWebhookEvent,
|
|
2643
3262
|
verifyWebhookSignature,
|
|
2644
3263
|
verifyWsUpgrade
|
|
2645
3264
|
});
|