@iqauth/sdk 2.6.4 → 2.7.0
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 +181 -41
- 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 +271 -32
- package/dist/browser.mjs +5 -5
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- 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-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/chunk-PMAFENVI.mjs +229 -0
- package/dist/chunk-RR2MGPTK.mjs +2724 -0
- package/dist/{chunk-XAWYUPMO.mjs → chunk-RTJAIBXY.mjs} +220 -20
- package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
- package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
- 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-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
- package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +349 -52
- package/dist/express.mjs +39 -12
- package/dist/fastify.d.mts +2 -0
- package/dist/fastify.d.ts +2 -0
- package/dist/fastify.js +332 -52
- package/dist/fastify.mjs +23 -8
- package/dist/hono.d.mts +2 -0
- package/dist/hono.d.ts +2 -0
- package/dist/hono.js +329 -52
- package/dist/hono.mjs +20 -8
- package/dist/index-5KSZEnDe.d.ts +1626 -0
- package/dist/index-CKoZHAoc.d.mts +1626 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +565 -69
- package/dist/index.mjs +29 -9
- 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/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +276 -41
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/next.js +391 -201
- package/dist/next.mjs +22 -7
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
- 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 +97 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +313 -33
- package/dist/react.mjs +58 -2632
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +148 -3
- package/dist/server/handlers.d.ts +148 -3
- package/dist/server/handlers.js +410 -11
- package/dist/server/handlers.mjs +12 -3
- package/dist/server.d.mts +151 -8
- package/dist/server.d.ts +151 -8
- package/dist/server.js +406 -50
- package/dist/server.mjs +93 -11
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +181 -41
- package/dist/service.mjs +3 -3
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-BLFnz8SV.d.ts} +78 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-T-CZ6t6r.d.mts} +78 -3
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
- package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
- package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
- package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
- package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
- package/dist/webhooks.d.mts +100 -17
- package/dist/webhooks.d.ts +100 -17
- package/dist/webhooks.js +164 -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/package.json +13 -3
- 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}`;
|
|
@@ -497,6 +534,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
497
534
|
"iqvalidate"
|
|
498
535
|
];
|
|
499
536
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
537
|
+
function classifyJoseError(err) {
|
|
538
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
539
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
540
|
+
}
|
|
541
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
542
|
+
return { code: "token_invalid", message: err.message };
|
|
543
|
+
}
|
|
544
|
+
if (err instanceof Error) {
|
|
545
|
+
return { code: "token_invalid", message: err.message };
|
|
546
|
+
}
|
|
547
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
548
|
+
}
|
|
500
549
|
function decodeProtectedHeader(token) {
|
|
501
550
|
const parts = token.split(".");
|
|
502
551
|
if (parts.length < 2) return null;
|
|
@@ -533,11 +582,11 @@ var TokensModule = class {
|
|
|
533
582
|
async verify(token, options = {}) {
|
|
534
583
|
const header = decodeProtectedHeader(token);
|
|
535
584
|
if (!header) {
|
|
536
|
-
throw new IQAuthError("
|
|
585
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
537
586
|
}
|
|
538
587
|
const kid = header.kid;
|
|
539
588
|
if (!kid) {
|
|
540
|
-
throw new IQAuthError("
|
|
589
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
541
590
|
}
|
|
542
591
|
let cache = await this.ensureCache();
|
|
543
592
|
if (!cache.byKid.has(kid)) {
|
|
@@ -545,7 +594,7 @@ var TokensModule = class {
|
|
|
545
594
|
cache = await this.ensureCache();
|
|
546
595
|
}
|
|
547
596
|
if (!cache.byKid.has(kid)) {
|
|
548
|
-
throw new IQAuthError("
|
|
597
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
549
598
|
}
|
|
550
599
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
551
600
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -561,16 +610,8 @@ var TokensModule = class {
|
|
|
561
610
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
562
611
|
return payload;
|
|
563
612
|
} 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");
|
|
613
|
+
const classified = classifyJoseError(err);
|
|
614
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
574
615
|
}
|
|
575
616
|
}
|
|
576
617
|
/**
|
|
@@ -612,7 +653,7 @@ var TokensModule = class {
|
|
|
612
653
|
getClaims(token) {
|
|
613
654
|
const claims = this.decode(token);
|
|
614
655
|
if (!claims) {
|
|
615
|
-
throw new IQAuthError("
|
|
656
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
616
657
|
}
|
|
617
658
|
return claims;
|
|
618
659
|
}
|
|
@@ -622,7 +663,7 @@ var TokensModule = class {
|
|
|
622
663
|
}
|
|
623
664
|
await this.refreshJwks();
|
|
624
665
|
if (!this.jwksCache) {
|
|
625
|
-
throw new IQAuthError("
|
|
666
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
626
667
|
}
|
|
627
668
|
return this.jwksCache;
|
|
628
669
|
}
|
|
@@ -632,22 +673,38 @@ var TokensModule = class {
|
|
|
632
673
|
}
|
|
633
674
|
this.inFlightRefresh = (async () => {
|
|
634
675
|
try {
|
|
635
|
-
|
|
676
|
+
let res;
|
|
677
|
+
try {
|
|
678
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
679
|
+
} catch (err) {
|
|
680
|
+
throw new IQAuthError(
|
|
681
|
+
"network",
|
|
682
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
683
|
+
void 0,
|
|
684
|
+
err
|
|
685
|
+
);
|
|
686
|
+
}
|
|
636
687
|
if (!res.ok) {
|
|
637
688
|
throw new IQAuthError(
|
|
638
|
-
"
|
|
639
|
-
`Failed to fetch JWKS: ${res.status}
|
|
689
|
+
"jwks_fetch_failed",
|
|
690
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
691
|
+
res.status
|
|
640
692
|
);
|
|
641
693
|
}
|
|
642
694
|
let jwks;
|
|
643
695
|
try {
|
|
644
696
|
jwks = await res.json();
|
|
645
|
-
} catch {
|
|
646
|
-
throw new IQAuthError(
|
|
697
|
+
} catch (err) {
|
|
698
|
+
throw new IQAuthError(
|
|
699
|
+
"jwks_fetch_failed",
|
|
700
|
+
"Malformed JWKS response: invalid JSON",
|
|
701
|
+
res.status,
|
|
702
|
+
err
|
|
703
|
+
);
|
|
647
704
|
}
|
|
648
705
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
649
706
|
throw new IQAuthError(
|
|
650
|
-
"
|
|
707
|
+
"jwks_fetch_failed",
|
|
651
708
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
652
709
|
);
|
|
653
710
|
}
|
|
@@ -655,7 +712,7 @@ var TokensModule = class {
|
|
|
655
712
|
for (const key of jwks.keys) {
|
|
656
713
|
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
714
|
throw new IQAuthError(
|
|
658
|
-
"
|
|
715
|
+
"jwks_fetch_failed",
|
|
659
716
|
"Malformed JWKS response: key missing required fields"
|
|
660
717
|
);
|
|
661
718
|
}
|
|
@@ -673,6 +730,19 @@ var TokensModule = class {
|
|
|
673
730
|
clearCache() {
|
|
674
731
|
this.jwksCache = null;
|
|
675
732
|
}
|
|
733
|
+
/**
|
|
734
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
735
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
736
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
737
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
738
|
+
*/
|
|
739
|
+
async prewarm() {
|
|
740
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
741
|
+
try {
|
|
742
|
+
await this.refreshJwks();
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}
|
|
676
746
|
};
|
|
677
747
|
|
|
678
748
|
// src/modules/sessions.ts
|
|
@@ -996,14 +1066,14 @@ var OidcModule = class {
|
|
|
996
1066
|
*/
|
|
997
1067
|
async handleCallback(params) {
|
|
998
1068
|
if (!params.state) {
|
|
999
|
-
throw new IQAuthError("
|
|
1069
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
1000
1070
|
}
|
|
1001
1071
|
if (!params.code) {
|
|
1002
|
-
throw new IQAuthError("
|
|
1072
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
1003
1073
|
}
|
|
1004
1074
|
const stored = await this.stateStore.get(params.state);
|
|
1005
1075
|
if (!stored) {
|
|
1006
|
-
throw new IQAuthError("
|
|
1076
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
1007
1077
|
}
|
|
1008
1078
|
let tokens;
|
|
1009
1079
|
try {
|
|
@@ -1021,7 +1091,7 @@ var OidcModule = class {
|
|
|
1021
1091
|
if (tokens.id_token) {
|
|
1022
1092
|
if (!this.tokensModule) {
|
|
1023
1093
|
throw new IQAuthError(
|
|
1024
|
-
"
|
|
1094
|
+
"config_invalid",
|
|
1025
1095
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
1026
1096
|
);
|
|
1027
1097
|
}
|
|
@@ -1032,7 +1102,7 @@ var OidcModule = class {
|
|
|
1032
1102
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
1033
1103
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
1034
1104
|
throw new IQAuthError(
|
|
1035
|
-
"
|
|
1105
|
+
"token_invalid",
|
|
1036
1106
|
"OIDC id_token nonce did not match the stored value"
|
|
1037
1107
|
);
|
|
1038
1108
|
}
|
|
@@ -1233,6 +1303,9 @@ var AppsModule = class {
|
|
|
1233
1303
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1234
1304
|
*/
|
|
1235
1305
|
async get(appKey) {
|
|
1306
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1307
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1308
|
+
}
|
|
1236
1309
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1237
1310
|
}
|
|
1238
1311
|
/**
|
|
@@ -1252,6 +1325,16 @@ var AppsModule = class {
|
|
|
1252
1325
|
401
|
|
1253
1326
|
);
|
|
1254
1327
|
}
|
|
1328
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1329
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1330
|
+
}
|
|
1331
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1332
|
+
throw new IQAuthError(
|
|
1333
|
+
"ENVIRONMENT_REQUIRED",
|
|
1334
|
+
"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.",
|
|
1335
|
+
400
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1255
1338
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1256
1339
|
}
|
|
1257
1340
|
/**
|
|
@@ -1261,11 +1344,14 @@ var AppsModule = class {
|
|
|
1261
1344
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1262
1345
|
*/
|
|
1263
1346
|
async isRegistered(appKey) {
|
|
1347
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1348
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1349
|
+
}
|
|
1264
1350
|
try {
|
|
1265
1351
|
await this.get(appKey);
|
|
1266
1352
|
return true;
|
|
1267
1353
|
} catch (err) {
|
|
1268
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1354
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1269
1355
|
return false;
|
|
1270
1356
|
}
|
|
1271
1357
|
throw err;
|
|
@@ -1302,6 +1388,20 @@ var RolesModule = class {
|
|
|
1302
1388
|
};
|
|
1303
1389
|
|
|
1304
1390
|
// src/modules/permissionGroups.ts
|
|
1391
|
+
function assertAppKey(appKey, callsite) {
|
|
1392
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1393
|
+
throw new IQAuthError(
|
|
1394
|
+
"VALIDATION_ERROR",
|
|
1395
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1396
|
+
400
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1401
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1402
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1305
1405
|
var PermissionGroupsModule = class {
|
|
1306
1406
|
constructor(http) {
|
|
1307
1407
|
this.http = http;
|
|
@@ -1322,7 +1422,14 @@ var PermissionGroupsModule = class {
|
|
|
1322
1422
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1323
1423
|
}
|
|
1324
1424
|
async addPermission(tenantId, groupId, data) {
|
|
1325
|
-
|
|
1425
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1426
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1427
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1428
|
+
appKey: data.appKey,
|
|
1429
|
+
nodeKey: data.nodeKey,
|
|
1430
|
+
effect: data.effect,
|
|
1431
|
+
weight: data.weight
|
|
1432
|
+
});
|
|
1326
1433
|
}
|
|
1327
1434
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1328
1435
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1346,21 +1453,51 @@ var PermissionGroupsModule = class {
|
|
|
1346
1453
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1347
1454
|
}
|
|
1348
1455
|
async addUserOverride(tenantId, userId, data) {
|
|
1349
|
-
|
|
1456
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1457
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1458
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1459
|
+
appKey: data.appKey,
|
|
1460
|
+
nodeKey: data.nodeKey,
|
|
1461
|
+
effect: data.effect,
|
|
1462
|
+
weight: data.weight,
|
|
1463
|
+
expiresAt: data.expiresAt
|
|
1464
|
+
});
|
|
1350
1465
|
}
|
|
1351
1466
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1352
1467
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1353
1468
|
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1471
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1472
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1473
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1474
|
+
*/
|
|
1354
1475
|
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}` : ""}`);
|
|
1476
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1477
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1478
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1360
1479
|
}
|
|
1361
1480
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1481
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1482
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1362
1483
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1363
1484
|
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1487
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1488
|
+
* single misconfigured entry can't slip through and silently report
|
|
1489
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1490
|
+
*/
|
|
1491
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1492
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1493
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1494
|
+
}
|
|
1495
|
+
checks.forEach((c, i) => {
|
|
1496
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1497
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1498
|
+
});
|
|
1499
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1500
|
+
}
|
|
1364
1501
|
};
|
|
1365
1502
|
|
|
1366
1503
|
// src/modules/apiKeys.ts
|
|
@@ -1785,6 +1922,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1785
1922
|
this._refreshToken = tokens.refreshToken;
|
|
1786
1923
|
},
|
|
1787
1924
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1925
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1926
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1927
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1928
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1788
1929
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1789
1930
|
sessionHeaderName: config.sessionHeaderName,
|
|
1790
1931
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1825,6 +1966,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1825
1966
|
static forServer(config) {
|
|
1826
1967
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1827
1968
|
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1971
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1972
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1973
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1974
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1975
|
+
*/
|
|
1828
1976
|
static forMobile(config) {
|
|
1829
1977
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1830
1978
|
}
|
|
@@ -1841,6 +1989,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1841
1989
|
getRefreshToken() {
|
|
1842
1990
|
return this._refreshToken;
|
|
1843
1991
|
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1994
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1995
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1996
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1997
|
+
*/
|
|
1998
|
+
async prewarm() {
|
|
1999
|
+
await Promise.all([
|
|
2000
|
+
this.tokens.prewarm(),
|
|
2001
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
2002
|
+
]);
|
|
2003
|
+
}
|
|
1844
2004
|
getCurrentClaims() {
|
|
1845
2005
|
if (!this._accessToken) return null;
|
|
1846
2006
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1911,14 +2071,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1911
2071
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1912
2072
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1913
2073
|
throw new IQAuthError(
|
|
1914
|
-
"
|
|
2074
|
+
"config_invalid",
|
|
1915
2075
|
`${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
2076
|
);
|
|
1917
2077
|
}
|
|
1918
2078
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1919
2079
|
if (!shapeMatch) {
|
|
1920
2080
|
throw new IQAuthError(
|
|
1921
|
-
"
|
|
2081
|
+
"config_invalid",
|
|
1922
2082
|
`${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
2083
|
);
|
|
1924
2084
|
}
|
|
@@ -1927,19 +2087,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1927
2087
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1928
2088
|
} catch {
|
|
1929
2089
|
throw new IQAuthError(
|
|
1930
|
-
"
|
|
2090
|
+
"config_invalid",
|
|
1931
2091
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1932
2092
|
);
|
|
1933
2093
|
}
|
|
1934
2094
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1935
2095
|
throw new IQAuthError(
|
|
1936
|
-
"
|
|
2096
|
+
"config_invalid",
|
|
1937
2097
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1938
2098
|
);
|
|
1939
2099
|
}
|
|
1940
2100
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1941
2101
|
throw new IQAuthError(
|
|
1942
|
-
"
|
|
2102
|
+
"config_invalid",
|
|
1943
2103
|
`${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
2104
|
);
|
|
1945
2105
|
}
|
|
@@ -1959,12 +2119,18 @@ function isSecretKey(raw) {
|
|
|
1959
2119
|
|
|
1960
2120
|
// src/middleware/express.ts
|
|
1961
2121
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2122
|
+
// Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
|
|
1962
2123
|
"TOKEN_INVALID",
|
|
1963
2124
|
"TOKEN_EXPIRED",
|
|
1964
2125
|
"TOKEN_REVOKED",
|
|
1965
2126
|
"SESSION_EXPIRED",
|
|
1966
2127
|
"SESSION_INVALID",
|
|
1967
|
-
"AUTH_REQUIRED"
|
|
2128
|
+
"AUTH_REQUIRED",
|
|
2129
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
|
|
2130
|
+
// Mapped to 401 here so framework consumers don't have to learn the new
|
|
2131
|
+
// codes to keep their auth-failure handling working.
|
|
2132
|
+
"token_invalid",
|
|
2133
|
+
"token_expired"
|
|
1968
2134
|
]);
|
|
1969
2135
|
var DEFAULT_ACCESS_COOKIE = "iqauth_at";
|
|
1970
2136
|
function getAuthorizationHeader(req) {
|
|
@@ -2146,6 +2312,185 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2146
2312
|
};
|
|
2147
2313
|
}
|
|
2148
2314
|
|
|
2315
|
+
// src/server/handlers.ts
|
|
2316
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2317
|
+
const baseUser = {
|
|
2318
|
+
sub: claims.sub,
|
|
2319
|
+
email: claims.email,
|
|
2320
|
+
name: claims.name,
|
|
2321
|
+
tenantId: claims.tenantId,
|
|
2322
|
+
vendorId: claims.vendorId,
|
|
2323
|
+
roles: claims.roles ?? [],
|
|
2324
|
+
entitlements: claims.entitlements ?? []
|
|
2325
|
+
};
|
|
2326
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2327
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2328
|
+
return {
|
|
2329
|
+
success: true,
|
|
2330
|
+
data: {
|
|
2331
|
+
user,
|
|
2332
|
+
claims,
|
|
2333
|
+
tenantId: claims.tenantId ?? null
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
|
|
2338
|
+
var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
2339
|
+
function resolve(config) {
|
|
2340
|
+
const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
|
|
2341
|
+
const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
|
|
2342
|
+
return {
|
|
2343
|
+
publishableKey: config.publishableKey,
|
|
2344
|
+
secretKey: config.secretKey,
|
|
2345
|
+
issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
|
|
2346
|
+
accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
|
|
2347
|
+
refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
|
|
2348
|
+
cookieDomain: config.cookieDomain,
|
|
2349
|
+
sameSite: config.sameSite ?? "lax",
|
|
2350
|
+
secure: config.secure ?? true,
|
|
2351
|
+
cookiePath: config.cookiePath ?? "/",
|
|
2352
|
+
tokenPath: config.tokenPath ?? "/oidc/token",
|
|
2353
|
+
refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
|
|
2354
|
+
logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
|
|
2355
|
+
fetchImpl: config.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
2356
|
+
throw new Error("global fetch is unavailable; pass fetchImpl");
|
|
2357
|
+
})),
|
|
2358
|
+
appId: parsed.appId,
|
|
2359
|
+
tenantId: parsed.tenantId,
|
|
2360
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2361
|
+
debug: config.debug,
|
|
2362
|
+
onTimingEvent: config.onTimingEvent,
|
|
2363
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2364
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2368
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2369
|
+
function pruneInMemoryMarkers(now) {
|
|
2370
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2371
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2372
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
var defaultSignoutRegistry = {
|
|
2376
|
+
mark(token, ttlMs) {
|
|
2377
|
+
const now = Date.now();
|
|
2378
|
+
pruneInMemoryMarkers(now);
|
|
2379
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2380
|
+
},
|
|
2381
|
+
has(token) {
|
|
2382
|
+
const now = Date.now();
|
|
2383
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2384
|
+
if (!exp) return false;
|
|
2385
|
+
if (exp <= now) {
|
|
2386
|
+
inMemorySignoutMarkers.delete(token);
|
|
2387
|
+
return false;
|
|
2388
|
+
}
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2393
|
+
function getTokensFor(issuer) {
|
|
2394
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2395
|
+
if (!m) {
|
|
2396
|
+
m = new TokensModule(issuer);
|
|
2397
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2398
|
+
}
|
|
2399
|
+
return m;
|
|
2400
|
+
}
|
|
2401
|
+
async function handleUserinfo(config, input) {
|
|
2402
|
+
const cfg = resolve(config);
|
|
2403
|
+
if (!input.accessToken) {
|
|
2404
|
+
return {
|
|
2405
|
+
status: 401,
|
|
2406
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2407
|
+
cookies: []
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
let claims;
|
|
2411
|
+
try {
|
|
2412
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2415
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2416
|
+
return {
|
|
2417
|
+
status: 401,
|
|
2418
|
+
body: { success: false, error: { code, message } },
|
|
2419
|
+
cookies: []
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2423
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2424
|
+
});
|
|
2425
|
+
return {
|
|
2426
|
+
status: 200,
|
|
2427
|
+
body: envelope,
|
|
2428
|
+
cookies: []
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// src/permissions/wildcard.ts
|
|
2433
|
+
var SUFFIX = ".*";
|
|
2434
|
+
function wildcardPrefix(pattern) {
|
|
2435
|
+
return pattern.slice(0, -SUFFIX.length);
|
|
2436
|
+
}
|
|
2437
|
+
function hasPermission(set, id) {
|
|
2438
|
+
if (!id) return false;
|
|
2439
|
+
if (!set) return false;
|
|
2440
|
+
if (id === "*") {
|
|
2441
|
+
for (const entry of set) if (entry === "*") return true;
|
|
2442
|
+
return false;
|
|
2443
|
+
}
|
|
2444
|
+
const queryIsWildcard = id.endsWith(SUFFIX);
|
|
2445
|
+
const queryPrefix = queryIsWildcard ? wildcardPrefix(id) : null;
|
|
2446
|
+
for (const entry of set) {
|
|
2447
|
+
if (!entry) continue;
|
|
2448
|
+
if (entry === "*") return true;
|
|
2449
|
+
if (entry === id) return true;
|
|
2450
|
+
if (entry.endsWith(SUFFIX)) {
|
|
2451
|
+
const prefix = wildcardPrefix(entry);
|
|
2452
|
+
if (!queryIsWildcard) {
|
|
2453
|
+
if (id === prefix) return true;
|
|
2454
|
+
if (id.startsWith(prefix + ".")) return true;
|
|
2455
|
+
} else {
|
|
2456
|
+
if (queryPrefix === prefix) return true;
|
|
2457
|
+
if (queryPrefix !== null && queryPrefix.startsWith(prefix + ".")) return true;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
function expandPermissions(set) {
|
|
2464
|
+
if (!set) return [];
|
|
2465
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2466
|
+
for (const raw of set) {
|
|
2467
|
+
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
2468
|
+
seen.add(raw);
|
|
2469
|
+
}
|
|
2470
|
+
if (seen.has("*")) return ["*"];
|
|
2471
|
+
const wildcards = [];
|
|
2472
|
+
for (const entry of seen) if (entry.endsWith(SUFFIX)) wildcards.push(entry);
|
|
2473
|
+
const out = [];
|
|
2474
|
+
for (const entry of seen) {
|
|
2475
|
+
let covered = false;
|
|
2476
|
+
for (const w of wildcards) {
|
|
2477
|
+
if (w === entry) continue;
|
|
2478
|
+
const prefix = wildcardPrefix(w);
|
|
2479
|
+
if (entry === prefix) {
|
|
2480
|
+
covered = true;
|
|
2481
|
+
break;
|
|
2482
|
+
}
|
|
2483
|
+
if (entry.startsWith(prefix + ".")) {
|
|
2484
|
+
covered = true;
|
|
2485
|
+
break;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (!covered) out.push(entry);
|
|
2489
|
+
}
|
|
2490
|
+
out.sort();
|
|
2491
|
+
return out;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2149
2494
|
// src/ws.ts
|
|
2150
2495
|
var DEFAULT_COOKIE = "iqauth_at";
|
|
2151
2496
|
var DEFAULT_SUBPROTOCOL_PREFIX = "iqauth.bearer.";
|
|
@@ -2244,10 +2589,10 @@ function jwkFromPublicKey(publicKey, kid) {
|
|
|
2244
2589
|
return { kty: "RSA", use: "sig", alg: "RS256", kid, n: jwk.n, e: jwk.e };
|
|
2245
2590
|
}
|
|
2246
2591
|
function readBody(req) {
|
|
2247
|
-
return new Promise((
|
|
2592
|
+
return new Promise((resolve2, reject) => {
|
|
2248
2593
|
const chunks = [];
|
|
2249
2594
|
req.on("data", (c) => chunks.push(c));
|
|
2250
|
-
req.on("end", () =>
|
|
2595
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2251
2596
|
req.on("error", reject);
|
|
2252
2597
|
});
|
|
2253
2598
|
}
|
|
@@ -2435,11 +2780,11 @@ async function createTestIssuer(options = {}) {
|
|
|
2435
2780
|
const server = (0, import_http2.createServer)((req, res) => {
|
|
2436
2781
|
void handler(req, res);
|
|
2437
2782
|
});
|
|
2438
|
-
await new Promise((
|
|
2783
|
+
await new Promise((resolve2, reject) => {
|
|
2439
2784
|
server.once("error", reject);
|
|
2440
2785
|
server.listen(port, host, () => {
|
|
2441
2786
|
server.off("error", reject);
|
|
2442
|
-
|
|
2787
|
+
resolve2();
|
|
2443
2788
|
});
|
|
2444
2789
|
});
|
|
2445
2790
|
const addr = server.address();
|
|
@@ -2463,8 +2808,8 @@ async function createTestIssuer(options = {}) {
|
|
|
2463
2808
|
pendingCodes.set(code, { claims: opts, refreshToken });
|
|
2464
2809
|
return code;
|
|
2465
2810
|
},
|
|
2466
|
-
close: () => new Promise((
|
|
2467
|
-
server.close((err) => err ? reject(err) :
|
|
2811
|
+
close: () => new Promise((resolve2, reject) => {
|
|
2812
|
+
server.close((err) => err ? reject(err) : resolve2());
|
|
2468
2813
|
})
|
|
2469
2814
|
};
|
|
2470
2815
|
}
|
|
@@ -2478,6 +2823,12 @@ var WebhookSignatureError = class extends Error {
|
|
|
2478
2823
|
this.code = code;
|
|
2479
2824
|
}
|
|
2480
2825
|
};
|
|
2826
|
+
var IQAUTH_SIGNATURE_HEADER = "x-iqauth-signature";
|
|
2827
|
+
var LEGACY_SIGNATURE_HEADERS = [
|
|
2828
|
+
"x-webhook-signature",
|
|
2829
|
+
"x-iq-auth-signature",
|
|
2830
|
+
"x-signature"
|
|
2831
|
+
];
|
|
2481
2832
|
function toBuffer(p) {
|
|
2482
2833
|
if (typeof p === "string") return Buffer.from(p, "utf8");
|
|
2483
2834
|
if (Buffer.isBuffer(p)) return p;
|
|
@@ -2486,13 +2837,19 @@ function toBuffer(p) {
|
|
|
2486
2837
|
function parseHeader(header) {
|
|
2487
2838
|
let t = NaN;
|
|
2488
2839
|
const v1 = [];
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2840
|
+
const trimmed = header.trim();
|
|
2841
|
+
if (/^[0-9a-f]+$/i.test(trimmed)) {
|
|
2842
|
+
v1.push(trimmed.toLowerCase());
|
|
2843
|
+
return { t, v1 };
|
|
2844
|
+
}
|
|
2845
|
+
for (const part of trimmed.split(",")) {
|
|
2846
|
+
const eqIdx = part.indexOf("=");
|
|
2847
|
+
if (eqIdx === -1) continue;
|
|
2848
|
+
const key = part.slice(0, eqIdx).trim().toLowerCase();
|
|
2849
|
+
const value = part.slice(eqIdx + 1).trim();
|
|
2850
|
+
if (!value) continue;
|
|
2494
2851
|
if (key === "t") t = Number(value);
|
|
2495
|
-
else if (key === "v1") v1.push(value);
|
|
2852
|
+
else if (key === "v1") v1.push(value.toLowerCase());
|
|
2496
2853
|
}
|
|
2497
2854
|
return { t, v1 };
|
|
2498
2855
|
}
|
|
@@ -2504,6 +2861,11 @@ function timingSafeEqualHex(a, b) {
|
|
|
2504
2861
|
return false;
|
|
2505
2862
|
}
|
|
2506
2863
|
}
|
|
2864
|
+
function computeSignatures(secret, body, t) {
|
|
2865
|
+
const modern = import_crypto3.default.createHmac("sha256", secret).update(body).digest("hex");
|
|
2866
|
+
const legacy = Number.isFinite(t) ? import_crypto3.default.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex") : null;
|
|
2867
|
+
return { modern, legacy };
|
|
2868
|
+
}
|
|
2507
2869
|
function verifyWebhookSignature(opts) {
|
|
2508
2870
|
const headerRaw = Array.isArray(opts.header) ? opts.header[0] : opts.header;
|
|
2509
2871
|
if (!headerRaw || typeof headerRaw !== "string") {
|
|
@@ -2513,20 +2875,27 @@ function verifyWebhookSignature(opts) {
|
|
|
2513
2875
|
throw new WebhookSignatureError("MISSING_SECRET", "secret is required");
|
|
2514
2876
|
}
|
|
2515
2877
|
const { t, v1 } = parseHeader(headerRaw);
|
|
2516
|
-
if (
|
|
2878
|
+
if (v1.length === 0) {
|
|
2517
2879
|
throw new WebhookSignatureError("MALFORMED_HEADER", `Could not parse signature header: ${headerRaw}`);
|
|
2518
2880
|
}
|
|
2519
2881
|
const tolerance = opts.toleranceSeconds ?? 300;
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2882
|
+
if (Number.isFinite(t)) {
|
|
2883
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
2884
|
+
if (Math.abs(now - t) > tolerance) {
|
|
2885
|
+
throw new WebhookSignatureError(
|
|
2886
|
+
"TIMESTAMP_OUT_OF_TOLERANCE",
|
|
2887
|
+
`Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2526
2890
|
}
|
|
2527
2891
|
const body = toBuffer(opts.payload);
|
|
2528
|
-
const
|
|
2529
|
-
const matched = v1.some((sig) =>
|
|
2892
|
+
const { modern, legacy } = computeSignatures(opts.secret, body, t);
|
|
2893
|
+
const matched = v1.some((sig) => {
|
|
2894
|
+
const lower = sig.toLowerCase();
|
|
2895
|
+
if (timingSafeEqualHex(lower, modern)) return true;
|
|
2896
|
+
if (legacy && timingSafeEqualHex(lower, legacy)) return true;
|
|
2897
|
+
return false;
|
|
2898
|
+
});
|
|
2530
2899
|
if (!matched) {
|
|
2531
2900
|
throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
|
|
2532
2901
|
}
|
|
@@ -2546,6 +2915,125 @@ function isValidWebhookSignature(opts) {
|
|
|
2546
2915
|
return false;
|
|
2547
2916
|
}
|
|
2548
2917
|
}
|
|
2918
|
+
function readHeader(headers, name) {
|
|
2919
|
+
if (typeof headers.get === "function") {
|
|
2920
|
+
return headers.get(name);
|
|
2921
|
+
}
|
|
2922
|
+
const lower = name.toLowerCase();
|
|
2923
|
+
const obj = headers;
|
|
2924
|
+
if (lower in obj) return obj[lower];
|
|
2925
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
2926
|
+
if (k.toLowerCase() === lower) return v;
|
|
2927
|
+
}
|
|
2928
|
+
return void 0;
|
|
2929
|
+
}
|
|
2930
|
+
function pickHeaderValue(value) {
|
|
2931
|
+
if (value == null) return null;
|
|
2932
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
2933
|
+
return value;
|
|
2934
|
+
}
|
|
2935
|
+
function envelopeError(message) {
|
|
2936
|
+
throw new WebhookSignatureError("MALFORMED_ENVELOPE", message);
|
|
2937
|
+
}
|
|
2938
|
+
function parseWebhookEvent(rawBody, headers, secrets, opts = {}) {
|
|
2939
|
+
if (!Array.isArray(secrets) || secrets.length === 0 || secrets.every((s) => !s)) {
|
|
2940
|
+
throw new WebhookSignatureError("MISSING_SECRET", "At least one signing secret is required");
|
|
2941
|
+
}
|
|
2942
|
+
let headerValue = pickHeaderValue(readHeader(headers, IQAUTH_SIGNATURE_HEADER));
|
|
2943
|
+
let usedHeader = IQAUTH_SIGNATURE_HEADER;
|
|
2944
|
+
if (!headerValue) {
|
|
2945
|
+
for (const legacy of LEGACY_SIGNATURE_HEADERS) {
|
|
2946
|
+
const v = pickHeaderValue(readHeader(headers, legacy));
|
|
2947
|
+
if (v) {
|
|
2948
|
+
headerValue = v;
|
|
2949
|
+
usedHeader = legacy;
|
|
2950
|
+
const log = opts.onDeprecation ?? ((m) => console.warn(m));
|
|
2951
|
+
log(
|
|
2952
|
+
`[iqauth] deprecation: webhook delivery used legacy header "${legacy}"; migrate sender to "X-IQAuth-Signature" (back-compat removed in next minor).`
|
|
2953
|
+
);
|
|
2954
|
+
break;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
if (!headerValue) {
|
|
2959
|
+
throw new WebhookSignatureError(
|
|
2960
|
+
"MISSING_HEADER",
|
|
2961
|
+
`Missing webhook signature header. Expected "X-IQAuth-Signature" (or one of: ${LEGACY_SIGNATURE_HEADERS.join(", ")}).`
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
const { t, v1 } = parseHeader(headerValue);
|
|
2965
|
+
if (v1.length === 0) {
|
|
2966
|
+
throw new WebhookSignatureError(
|
|
2967
|
+
"MALFORMED_HEADER",
|
|
2968
|
+
`Could not parse "${usedHeader}" header value: ${headerValue}`
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2971
|
+
const body = toBuffer(rawBody);
|
|
2972
|
+
let verifiedIdx = -1;
|
|
2973
|
+
for (let i = 0; i < secrets.length; i++) {
|
|
2974
|
+
const secret = secrets[i];
|
|
2975
|
+
if (!secret) continue;
|
|
2976
|
+
const { modern, legacy } = computeSignatures(secret, body, t);
|
|
2977
|
+
const ok = v1.some((sig) => {
|
|
2978
|
+
const lower = sig.toLowerCase();
|
|
2979
|
+
if (timingSafeEqualHex(lower, modern)) return true;
|
|
2980
|
+
if (legacy && timingSafeEqualHex(lower, legacy)) return true;
|
|
2981
|
+
return false;
|
|
2982
|
+
});
|
|
2983
|
+
if (ok) {
|
|
2984
|
+
verifiedIdx = i;
|
|
2985
|
+
break;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (verifiedIdx === -1) {
|
|
2989
|
+
throw new WebhookSignatureError(
|
|
2990
|
+
"SIGNATURE_MISMATCH",
|
|
2991
|
+
"Webhook signature does not match any provided secret"
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
let parsed;
|
|
2995
|
+
try {
|
|
2996
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
2997
|
+
} catch {
|
|
2998
|
+
throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
|
|
2999
|
+
}
|
|
3000
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3001
|
+
envelopeError("Webhook body must be a JSON object");
|
|
3002
|
+
}
|
|
3003
|
+
const { id, type, subject, time, data, tenantId, specversion } = parsed;
|
|
3004
|
+
if (specversion !== "1.0") {
|
|
3005
|
+
envelopeError(`Envelope \`specversion\` must be "1.0" (got: ${JSON.stringify(specversion)})`);
|
|
3006
|
+
}
|
|
3007
|
+
if (typeof id !== "string" || !id) envelopeError("Envelope missing required string `id`");
|
|
3008
|
+
if (typeof type !== "string" || !type) envelopeError("Envelope missing required string `type`");
|
|
3009
|
+
if (typeof subject !== "string" || !subject) envelopeError("Envelope missing required string `subject`");
|
|
3010
|
+
if (typeof time !== "string" || !time) envelopeError("Envelope missing required string `time`");
|
|
3011
|
+
if (typeof tenantId !== "string" || !tenantId) envelopeError("Envelope missing required string `tenantId`");
|
|
3012
|
+
if (data === void 0 || data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
3013
|
+
envelopeError("Envelope `data` must be an object");
|
|
3014
|
+
}
|
|
3015
|
+
const tolerance = opts.toleranceSeconds ?? 300;
|
|
3016
|
+
const eventMs = Date.parse(time);
|
|
3017
|
+
if (!Number.isFinite(eventMs)) envelopeError(`Envelope \`time\` is not a valid ISO timestamp: ${time}`);
|
|
3018
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
3019
|
+
if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
|
|
3020
|
+
throw new WebhookSignatureError(
|
|
3021
|
+
"TIMESTAMP_OUT_OF_TOLERANCE",
|
|
3022
|
+
`Envelope time ${time} is outside the ${tolerance}s tolerance window (now=${new Date(nowMs).toISOString()})`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
return {
|
|
3026
|
+
specversion: "1.0",
|
|
3027
|
+
id,
|
|
3028
|
+
type,
|
|
3029
|
+
subject,
|
|
3030
|
+
time,
|
|
3031
|
+
tenantId,
|
|
3032
|
+
data,
|
|
3033
|
+
idempotencyKey: id,
|
|
3034
|
+
verifiedWithSecretIndex: verifiedIdx
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
2549
3037
|
|
|
2550
3038
|
// src/server/provisioningBridge.ts
|
|
2551
3039
|
function defaultIsUniqueViolation(err) {
|
|
@@ -2611,10 +3099,13 @@ function createProvisioningBridge(options) {
|
|
|
2611
3099
|
ErrorCodes,
|
|
2612
3100
|
GdprModule,
|
|
2613
3101
|
HierarchyModule,
|
|
3102
|
+
IQAUTH_SIGNATURE_HEADER,
|
|
2614
3103
|
IQAuthClient,
|
|
2615
3104
|
IQAuthError,
|
|
3105
|
+
IQ_AUTH_ERROR_CODES,
|
|
2616
3106
|
InMemoryOidcStateStore,
|
|
2617
3107
|
InvitesModule,
|
|
3108
|
+
LEGACY_SIGNATURE_HEADERS,
|
|
2618
3109
|
MembershipsModule,
|
|
2619
3110
|
MfaModule,
|
|
2620
3111
|
OidcModule,
|
|
@@ -2632,14 +3123,19 @@ function createProvisioningBridge(options) {
|
|
|
2632
3123
|
WebhookSignatureError,
|
|
2633
3124
|
WebhooksModule,
|
|
2634
3125
|
assertPublishableKey,
|
|
3126
|
+
buildUserinfoResponse,
|
|
2635
3127
|
createProvisioningBridge,
|
|
2636
3128
|
createTestIssuer,
|
|
2637
3129
|
encodePublishableKey,
|
|
3130
|
+
expandPermissions,
|
|
3131
|
+
handleUserinfo,
|
|
3132
|
+
hasPermission,
|
|
2638
3133
|
iqAuthMiddleware,
|
|
2639
3134
|
isPublishableKey,
|
|
2640
3135
|
isSecretKey,
|
|
2641
3136
|
isValidWebhookSignature,
|
|
2642
3137
|
parsePublishableKey,
|
|
3138
|
+
parseWebhookEvent,
|
|
2643
3139
|
verifyWebhookSignature,
|
|
2644
3140
|
verifyWsUpgrade
|
|
2645
3141
|
});
|