@iqauth/sdk 2.6.3 → 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 +10 -8
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/chunk-C2ZTBOAC.mjs +36 -0
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
- 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-76W5TLQQ.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/pkce-7WKV4OIN.mjs +11 -0
- 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 +343 -36
- package/dist/react.mjs +59 -2611
- 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-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
- package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
- package/dist/{signIn-OCr88Zf8.d.ts → 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/hono.js
CHANGED
|
@@ -35,13 +35,30 @@ __export(hono_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(hono_exports);
|
|
36
36
|
|
|
37
37
|
// src/errors.ts
|
|
38
|
-
var IQAuthError = class extends Error {
|
|
39
|
-
constructor(code, message, status,
|
|
38
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
39
|
+
constructor(code, message, status, cause) {
|
|
40
40
|
super(message);
|
|
41
41
|
this.name = "IQAuthError";
|
|
42
42
|
this.code = code;
|
|
43
43
|
this.status = status;
|
|
44
|
-
this.
|
|
44
|
+
this.cause = cause;
|
|
45
|
+
this.raw = cause;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
49
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
50
|
+
*/
|
|
51
|
+
static isIQAuthError(value) {
|
|
52
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Type-narrowed code check. Lets callers write
|
|
56
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
57
|
+
* taxonomy without losing the ability to handle server codes via
|
|
58
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
59
|
+
*/
|
|
60
|
+
is(code) {
|
|
61
|
+
return this.code === code;
|
|
45
62
|
}
|
|
46
63
|
};
|
|
47
64
|
|
|
@@ -156,7 +173,7 @@ var HttpClient = class {
|
|
|
156
173
|
headers: this.buildHeaders(),
|
|
157
174
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
158
175
|
const refreshToken = this.config.getRefreshToken();
|
|
159
|
-
if (!refreshToken) throw new IQAuthError("
|
|
176
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
160
177
|
return { body: JSON.stringify({ refreshToken }) };
|
|
161
178
|
})()
|
|
162
179
|
});
|
|
@@ -173,7 +190,7 @@ var HttpClient = class {
|
|
|
173
190
|
return;
|
|
174
191
|
}
|
|
175
192
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
176
|
-
throw new IQAuthError("
|
|
193
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
177
194
|
}
|
|
178
195
|
const tokens = {
|
|
179
196
|
accessToken: body.data.accessToken,
|
|
@@ -191,7 +208,7 @@ var HttpClient = class {
|
|
|
191
208
|
return this.requestWithRetry(method, path, body, options, false);
|
|
192
209
|
}
|
|
193
210
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
194
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
211
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
195
212
|
await this.attemptRefresh();
|
|
196
213
|
}
|
|
197
214
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -419,6 +436,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
419
436
|
"iqvalidate"
|
|
420
437
|
];
|
|
421
438
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
439
|
+
function classifyJoseError(err) {
|
|
440
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
441
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
442
|
+
}
|
|
443
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
444
|
+
return { code: "token_invalid", message: err.message };
|
|
445
|
+
}
|
|
446
|
+
if (err instanceof Error) {
|
|
447
|
+
return { code: "token_invalid", message: err.message };
|
|
448
|
+
}
|
|
449
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
450
|
+
}
|
|
422
451
|
function decodeProtectedHeader(token) {
|
|
423
452
|
const parts = token.split(".");
|
|
424
453
|
if (parts.length < 2) return null;
|
|
@@ -455,11 +484,11 @@ var TokensModule = class {
|
|
|
455
484
|
async verify(token, options = {}) {
|
|
456
485
|
const header = decodeProtectedHeader(token);
|
|
457
486
|
if (!header) {
|
|
458
|
-
throw new IQAuthError("
|
|
487
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
459
488
|
}
|
|
460
489
|
const kid = header.kid;
|
|
461
490
|
if (!kid) {
|
|
462
|
-
throw new IQAuthError("
|
|
491
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
463
492
|
}
|
|
464
493
|
let cache = await this.ensureCache();
|
|
465
494
|
if (!cache.byKid.has(kid)) {
|
|
@@ -467,7 +496,7 @@ var TokensModule = class {
|
|
|
467
496
|
cache = await this.ensureCache();
|
|
468
497
|
}
|
|
469
498
|
if (!cache.byKid.has(kid)) {
|
|
470
|
-
throw new IQAuthError("
|
|
499
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
471
500
|
}
|
|
472
501
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
473
502
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -483,16 +512,8 @@ var TokensModule = class {
|
|
|
483
512
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
484
513
|
return payload;
|
|
485
514
|
} catch (err) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
490
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
491
|
-
}
|
|
492
|
-
if (err instanceof Error) {
|
|
493
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
494
|
-
}
|
|
495
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
515
|
+
const classified = classifyJoseError(err);
|
|
516
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
496
517
|
}
|
|
497
518
|
}
|
|
498
519
|
/**
|
|
@@ -534,7 +555,7 @@ var TokensModule = class {
|
|
|
534
555
|
getClaims(token) {
|
|
535
556
|
const claims = this.decode(token);
|
|
536
557
|
if (!claims) {
|
|
537
|
-
throw new IQAuthError("
|
|
558
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
538
559
|
}
|
|
539
560
|
return claims;
|
|
540
561
|
}
|
|
@@ -544,7 +565,7 @@ var TokensModule = class {
|
|
|
544
565
|
}
|
|
545
566
|
await this.refreshJwks();
|
|
546
567
|
if (!this.jwksCache) {
|
|
547
|
-
throw new IQAuthError("
|
|
568
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
548
569
|
}
|
|
549
570
|
return this.jwksCache;
|
|
550
571
|
}
|
|
@@ -554,22 +575,38 @@ var TokensModule = class {
|
|
|
554
575
|
}
|
|
555
576
|
this.inFlightRefresh = (async () => {
|
|
556
577
|
try {
|
|
557
|
-
|
|
578
|
+
let res;
|
|
579
|
+
try {
|
|
580
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
581
|
+
} catch (err) {
|
|
582
|
+
throw new IQAuthError(
|
|
583
|
+
"network",
|
|
584
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
585
|
+
void 0,
|
|
586
|
+
err
|
|
587
|
+
);
|
|
588
|
+
}
|
|
558
589
|
if (!res.ok) {
|
|
559
590
|
throw new IQAuthError(
|
|
560
|
-
"
|
|
561
|
-
`Failed to fetch JWKS: ${res.status}
|
|
591
|
+
"jwks_fetch_failed",
|
|
592
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
593
|
+
res.status
|
|
562
594
|
);
|
|
563
595
|
}
|
|
564
596
|
let jwks;
|
|
565
597
|
try {
|
|
566
598
|
jwks = await res.json();
|
|
567
|
-
} catch {
|
|
568
|
-
throw new IQAuthError(
|
|
599
|
+
} catch (err) {
|
|
600
|
+
throw new IQAuthError(
|
|
601
|
+
"jwks_fetch_failed",
|
|
602
|
+
"Malformed JWKS response: invalid JSON",
|
|
603
|
+
res.status,
|
|
604
|
+
err
|
|
605
|
+
);
|
|
569
606
|
}
|
|
570
607
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
571
608
|
throw new IQAuthError(
|
|
572
|
-
"
|
|
609
|
+
"jwks_fetch_failed",
|
|
573
610
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
574
611
|
);
|
|
575
612
|
}
|
|
@@ -577,7 +614,7 @@ var TokensModule = class {
|
|
|
577
614
|
for (const key of jwks.keys) {
|
|
578
615
|
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")) {
|
|
579
616
|
throw new IQAuthError(
|
|
580
|
-
"
|
|
617
|
+
"jwks_fetch_failed",
|
|
581
618
|
"Malformed JWKS response: key missing required fields"
|
|
582
619
|
);
|
|
583
620
|
}
|
|
@@ -595,6 +632,19 @@ var TokensModule = class {
|
|
|
595
632
|
clearCache() {
|
|
596
633
|
this.jwksCache = null;
|
|
597
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
637
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
638
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
639
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
640
|
+
*/
|
|
641
|
+
async prewarm() {
|
|
642
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
643
|
+
try {
|
|
644
|
+
await this.refreshJwks();
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
}
|
|
598
648
|
};
|
|
599
649
|
|
|
600
650
|
// src/modules/sessions.ts
|
|
@@ -918,14 +968,14 @@ var OidcModule = class {
|
|
|
918
968
|
*/
|
|
919
969
|
async handleCallback(params) {
|
|
920
970
|
if (!params.state) {
|
|
921
|
-
throw new IQAuthError("
|
|
971
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
922
972
|
}
|
|
923
973
|
if (!params.code) {
|
|
924
|
-
throw new IQAuthError("
|
|
974
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
925
975
|
}
|
|
926
976
|
const stored = await this.stateStore.get(params.state);
|
|
927
977
|
if (!stored) {
|
|
928
|
-
throw new IQAuthError("
|
|
978
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
929
979
|
}
|
|
930
980
|
let tokens;
|
|
931
981
|
try {
|
|
@@ -943,7 +993,7 @@ var OidcModule = class {
|
|
|
943
993
|
if (tokens.id_token) {
|
|
944
994
|
if (!this.tokensModule) {
|
|
945
995
|
throw new IQAuthError(
|
|
946
|
-
"
|
|
996
|
+
"config_invalid",
|
|
947
997
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
948
998
|
);
|
|
949
999
|
}
|
|
@@ -954,7 +1004,7 @@ var OidcModule = class {
|
|
|
954
1004
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
955
1005
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
956
1006
|
throw new IQAuthError(
|
|
957
|
-
"
|
|
1007
|
+
"token_invalid",
|
|
958
1008
|
"OIDC id_token nonce did not match the stored value"
|
|
959
1009
|
);
|
|
960
1010
|
}
|
|
@@ -1155,6 +1205,9 @@ var AppsModule = class {
|
|
|
1155
1205
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1156
1206
|
*/
|
|
1157
1207
|
async get(appKey) {
|
|
1208
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1209
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1210
|
+
}
|
|
1158
1211
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1159
1212
|
}
|
|
1160
1213
|
/**
|
|
@@ -1174,6 +1227,16 @@ var AppsModule = class {
|
|
|
1174
1227
|
401
|
|
1175
1228
|
);
|
|
1176
1229
|
}
|
|
1230
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1231
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1232
|
+
}
|
|
1233
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1234
|
+
throw new IQAuthError(
|
|
1235
|
+
"ENVIRONMENT_REQUIRED",
|
|
1236
|
+
"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.",
|
|
1237
|
+
400
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1177
1240
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1178
1241
|
}
|
|
1179
1242
|
/**
|
|
@@ -1183,11 +1246,14 @@ var AppsModule = class {
|
|
|
1183
1246
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1184
1247
|
*/
|
|
1185
1248
|
async isRegistered(appKey) {
|
|
1249
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1250
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1251
|
+
}
|
|
1186
1252
|
try {
|
|
1187
1253
|
await this.get(appKey);
|
|
1188
1254
|
return true;
|
|
1189
1255
|
} catch (err) {
|
|
1190
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1256
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1191
1257
|
return false;
|
|
1192
1258
|
}
|
|
1193
1259
|
throw err;
|
|
@@ -1224,6 +1290,20 @@ var RolesModule = class {
|
|
|
1224
1290
|
};
|
|
1225
1291
|
|
|
1226
1292
|
// src/modules/permissionGroups.ts
|
|
1293
|
+
function assertAppKey(appKey, callsite) {
|
|
1294
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1295
|
+
throw new IQAuthError(
|
|
1296
|
+
"VALIDATION_ERROR",
|
|
1297
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1298
|
+
400
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1303
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1304
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1227
1307
|
var PermissionGroupsModule = class {
|
|
1228
1308
|
constructor(http) {
|
|
1229
1309
|
this.http = http;
|
|
@@ -1244,7 +1324,14 @@ var PermissionGroupsModule = class {
|
|
|
1244
1324
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1245
1325
|
}
|
|
1246
1326
|
async addPermission(tenantId, groupId, data) {
|
|
1247
|
-
|
|
1327
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1328
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1329
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1330
|
+
appKey: data.appKey,
|
|
1331
|
+
nodeKey: data.nodeKey,
|
|
1332
|
+
effect: data.effect,
|
|
1333
|
+
weight: data.weight
|
|
1334
|
+
});
|
|
1248
1335
|
}
|
|
1249
1336
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1250
1337
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1268,21 +1355,51 @@ var PermissionGroupsModule = class {
|
|
|
1268
1355
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1269
1356
|
}
|
|
1270
1357
|
async addUserOverride(tenantId, userId, data) {
|
|
1271
|
-
|
|
1358
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1359
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1360
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1361
|
+
appKey: data.appKey,
|
|
1362
|
+
nodeKey: data.nodeKey,
|
|
1363
|
+
effect: data.effect,
|
|
1364
|
+
weight: data.weight,
|
|
1365
|
+
expiresAt: data.expiresAt
|
|
1366
|
+
});
|
|
1272
1367
|
}
|
|
1273
1368
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1274
1369
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1275
1370
|
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1373
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1374
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1375
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1376
|
+
*/
|
|
1276
1377
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const qs = query.toString();
|
|
1281
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1378
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1379
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1380
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1282
1381
|
}
|
|
1283
1382
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1383
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1384
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1284
1385
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1285
1386
|
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1389
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1390
|
+
* single misconfigured entry can't slip through and silently report
|
|
1391
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1392
|
+
*/
|
|
1393
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1394
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1395
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1396
|
+
}
|
|
1397
|
+
checks.forEach((c, i) => {
|
|
1398
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1399
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1400
|
+
});
|
|
1401
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1402
|
+
}
|
|
1286
1403
|
};
|
|
1287
1404
|
|
|
1288
1405
|
// src/modules/apiKeys.ts
|
|
@@ -1707,6 +1824,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1707
1824
|
this._refreshToken = tokens.refreshToken;
|
|
1708
1825
|
},
|
|
1709
1826
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1827
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1828
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1829
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1830
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1710
1831
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1711
1832
|
sessionHeaderName: config.sessionHeaderName,
|
|
1712
1833
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1747,6 +1868,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1747
1868
|
static forServer(config) {
|
|
1748
1869
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1749
1870
|
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1873
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1874
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1875
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1876
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1877
|
+
*/
|
|
1750
1878
|
static forMobile(config) {
|
|
1751
1879
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1752
1880
|
}
|
|
@@ -1763,6 +1891,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1763
1891
|
getRefreshToken() {
|
|
1764
1892
|
return this._refreshToken;
|
|
1765
1893
|
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1896
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1897
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1898
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1899
|
+
*/
|
|
1900
|
+
async prewarm() {
|
|
1901
|
+
await Promise.all([
|
|
1902
|
+
this.tokens.prewarm(),
|
|
1903
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1904
|
+
]);
|
|
1905
|
+
}
|
|
1766
1906
|
getCurrentClaims() {
|
|
1767
1907
|
if (!this._accessToken) return null;
|
|
1768
1908
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1803,14 +1943,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1803
1943
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1804
1944
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1805
1945
|
throw new IQAuthError(
|
|
1806
|
-
"
|
|
1946
|
+
"config_invalid",
|
|
1807
1947
|
`${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.`
|
|
1808
1948
|
);
|
|
1809
1949
|
}
|
|
1810
1950
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1811
1951
|
if (!shapeMatch) {
|
|
1812
1952
|
throw new IQAuthError(
|
|
1813
|
-
"
|
|
1953
|
+
"config_invalid",
|
|
1814
1954
|
`${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.`
|
|
1815
1955
|
);
|
|
1816
1956
|
}
|
|
@@ -1819,19 +1959,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1819
1959
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1820
1960
|
} catch {
|
|
1821
1961
|
throw new IQAuthError(
|
|
1822
|
-
"
|
|
1962
|
+
"config_invalid",
|
|
1823
1963
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1824
1964
|
);
|
|
1825
1965
|
}
|
|
1826
1966
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1827
1967
|
throw new IQAuthError(
|
|
1828
|
-
"
|
|
1968
|
+
"config_invalid",
|
|
1829
1969
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1830
1970
|
);
|
|
1831
1971
|
}
|
|
1832
1972
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1833
1973
|
throw new IQAuthError(
|
|
1834
|
-
"
|
|
1974
|
+
"config_invalid",
|
|
1835
1975
|
`${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.`
|
|
1836
1976
|
);
|
|
1837
1977
|
}
|
|
@@ -1844,6 +1984,41 @@ function isPublishableKeyPayload(value) {
|
|
|
1844
1984
|
}
|
|
1845
1985
|
|
|
1846
1986
|
// src/server/handlers.ts
|
|
1987
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
1988
|
+
const baseUser = {
|
|
1989
|
+
sub: claims.sub,
|
|
1990
|
+
email: claims.email,
|
|
1991
|
+
name: claims.name,
|
|
1992
|
+
tenantId: claims.tenantId,
|
|
1993
|
+
vendorId: claims.vendorId,
|
|
1994
|
+
roles: claims.roles ?? [],
|
|
1995
|
+
entitlements: claims.entitlements ?? []
|
|
1996
|
+
};
|
|
1997
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
1998
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
1999
|
+
return {
|
|
2000
|
+
success: true,
|
|
2001
|
+
data: {
|
|
2002
|
+
user,
|
|
2003
|
+
claims,
|
|
2004
|
+
tenantId: claims.tenantId ?? null
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function emitTiming(cfg, event) {
|
|
2009
|
+
if (cfg.debug) {
|
|
2010
|
+
try {
|
|
2011
|
+
console.debug("[iqauth_helper]", event);
|
|
2012
|
+
} catch {
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (cfg.onTimingEvent) {
|
|
2016
|
+
try {
|
|
2017
|
+
cfg.onTimingEvent(event);
|
|
2018
|
+
} catch {
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
1847
2022
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1848
2023
|
"TOKEN_REVOKED",
|
|
1849
2024
|
"SESSION_REVOKED",
|
|
@@ -1883,7 +2058,11 @@ function resolve(config) {
|
|
|
1883
2058
|
})),
|
|
1884
2059
|
appId: parsed.appId,
|
|
1885
2060
|
tenantId: parsed.tenantId,
|
|
1886
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2061
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2062
|
+
debug: config.debug,
|
|
2063
|
+
onTimingEvent: config.onTimingEvent,
|
|
2064
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2065
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
1887
2066
|
};
|
|
1888
2067
|
}
|
|
1889
2068
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -1900,15 +2079,41 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
1900
2079
|
}
|
|
1901
2080
|
function clearCookies(cfg) {
|
|
1902
2081
|
return [
|
|
1903
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
1904
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2082
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2083
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
1905
2084
|
];
|
|
1906
2085
|
}
|
|
2086
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2087
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2088
|
+
function pruneInMemoryMarkers(now) {
|
|
2089
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2090
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2091
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
var defaultSignoutRegistry = {
|
|
2095
|
+
mark(token, ttlMs) {
|
|
2096
|
+
const now = Date.now();
|
|
2097
|
+
pruneInMemoryMarkers(now);
|
|
2098
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2099
|
+
},
|
|
2100
|
+
has(token) {
|
|
2101
|
+
const now = Date.now();
|
|
2102
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2103
|
+
if (!exp) return false;
|
|
2104
|
+
if (exp <= now) {
|
|
2105
|
+
inMemorySignoutMarkers.delete(token);
|
|
2106
|
+
return false;
|
|
2107
|
+
}
|
|
2108
|
+
return true;
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
1907
2111
|
function serializeCookie(d) {
|
|
1908
2112
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
1909
2113
|
parts.push(`Path=${d.path}`);
|
|
1910
2114
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
1911
2115
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
2116
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
1912
2117
|
if (d.secure) parts.push("Secure");
|
|
1913
2118
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
1914
2119
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -1916,7 +2121,9 @@ function serializeCookie(d) {
|
|
|
1916
2121
|
}
|
|
1917
2122
|
async function handleCallback(config, input) {
|
|
1918
2123
|
const cfg = resolve(config);
|
|
2124
|
+
const t0 = Date.now();
|
|
1919
2125
|
if (!input.code || !input.redirectUri) {
|
|
2126
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
1920
2127
|
return {
|
|
1921
2128
|
status: 400,
|
|
1922
2129
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
@@ -1924,6 +2131,7 @@ async function handleCallback(config, input) {
|
|
|
1924
2131
|
};
|
|
1925
2132
|
}
|
|
1926
2133
|
if (!cfg.secretKey) {
|
|
2134
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
1927
2135
|
return {
|
|
1928
2136
|
status: 500,
|
|
1929
2137
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -1947,6 +2155,7 @@ async function handleCallback(config, input) {
|
|
|
1947
2155
|
});
|
|
1948
2156
|
const json = await res.json().catch(() => ({}));
|
|
1949
2157
|
if (!res.ok || !json.access_token) {
|
|
2158
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
1950
2159
|
return {
|
|
1951
2160
|
status: res.status || 502,
|
|
1952
2161
|
body: {
|
|
@@ -1966,6 +2175,7 @@ async function handleCallback(config, input) {
|
|
|
1966
2175
|
if (json.refresh_token) {
|
|
1967
2176
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
1968
2177
|
}
|
|
2178
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
1969
2179
|
return {
|
|
1970
2180
|
status: 200,
|
|
1971
2181
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -1974,8 +2184,18 @@ async function handleCallback(config, input) {
|
|
|
1974
2184
|
}
|
|
1975
2185
|
async function handleRefresh(config, input) {
|
|
1976
2186
|
const cfg = resolve(config);
|
|
2187
|
+
const t0 = Date.now();
|
|
1977
2188
|
const refreshToken = input.refreshToken;
|
|
2189
|
+
const idemKey = input.idempotencyToken;
|
|
2190
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2191
|
+
return {
|
|
2192
|
+
status: 401,
|
|
2193
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2194
|
+
cookies: clearCookies(cfg)
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
1978
2197
|
if (!refreshToken) {
|
|
2198
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
1979
2199
|
return {
|
|
1980
2200
|
status: 401,
|
|
1981
2201
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -1991,6 +2211,7 @@ async function handleRefresh(config, input) {
|
|
|
1991
2211
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
1992
2212
|
const status = res.status || 401;
|
|
1993
2213
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2214
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
1994
2215
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
1995
2216
|
cfg.clearCookiesOnRefreshFailure,
|
|
1996
2217
|
status,
|
|
@@ -2014,6 +2235,7 @@ async function handleRefresh(config, input) {
|
|
|
2014
2235
|
if (json.data.refreshToken) {
|
|
2015
2236
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2016
2237
|
}
|
|
2238
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2017
2239
|
return {
|
|
2018
2240
|
status: 200,
|
|
2019
2241
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2022,6 +2244,10 @@ async function handleRefresh(config, input) {
|
|
|
2022
2244
|
}
|
|
2023
2245
|
async function handleSignout(config, input) {
|
|
2024
2246
|
const cfg = resolve(config);
|
|
2247
|
+
const t0 = Date.now();
|
|
2248
|
+
if (input.idempotencyToken) {
|
|
2249
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2250
|
+
}
|
|
2025
2251
|
if (input.accessToken) {
|
|
2026
2252
|
try {
|
|
2027
2253
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2043,12 +2269,52 @@ async function handleSignout(config, input) {
|
|
|
2043
2269
|
} catch {
|
|
2044
2270
|
}
|
|
2045
2271
|
}
|
|
2272
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2046
2273
|
return {
|
|
2047
2274
|
status: 200,
|
|
2048
2275
|
body: { success: true, data: { signedOut: true } },
|
|
2049
2276
|
cookies: clearCookies(cfg)
|
|
2050
2277
|
};
|
|
2051
2278
|
}
|
|
2279
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2280
|
+
function getTokensFor(issuer) {
|
|
2281
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2282
|
+
if (!m) {
|
|
2283
|
+
m = new TokensModule(issuer);
|
|
2284
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2285
|
+
}
|
|
2286
|
+
return m;
|
|
2287
|
+
}
|
|
2288
|
+
async function handleUserinfo(config, input) {
|
|
2289
|
+
const cfg = resolve(config);
|
|
2290
|
+
if (!input.accessToken) {
|
|
2291
|
+
return {
|
|
2292
|
+
status: 401,
|
|
2293
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2294
|
+
cookies: []
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
let claims;
|
|
2298
|
+
try {
|
|
2299
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2302
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2303
|
+
return {
|
|
2304
|
+
status: 401,
|
|
2305
|
+
body: { success: false, error: { code, message } },
|
|
2306
|
+
cookies: []
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2310
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2311
|
+
});
|
|
2312
|
+
return {
|
|
2313
|
+
status: 200,
|
|
2314
|
+
body: envelope,
|
|
2315
|
+
cookies: []
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2052
2318
|
|
|
2053
2319
|
// src/hono.ts
|
|
2054
2320
|
var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
@@ -2057,7 +2323,10 @@ var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
|
2057
2323
|
"TOKEN_REVOKED",
|
|
2058
2324
|
"SESSION_EXPIRED",
|
|
2059
2325
|
"SESSION_INVALID",
|
|
2060
|
-
"AUTH_REQUIRED"
|
|
2326
|
+
"AUTH_REQUIRED",
|
|
2327
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy.
|
|
2328
|
+
"token_invalid",
|
|
2329
|
+
"token_expired"
|
|
2061
2330
|
]);
|
|
2062
2331
|
function readCookieFromHeader(header, name) {
|
|
2063
2332
|
if (!header) return void 0;
|
|
@@ -2096,6 +2365,11 @@ function iqAuth(options) {
|
|
|
2096
2365
|
return async (c, next) => {
|
|
2097
2366
|
const url = new URL(c.req.url);
|
|
2098
2367
|
const path = url.pathname;
|
|
2368
|
+
if (options.mountUserinfo && path === `${mount}/me` && c.req.method === "GET") {
|
|
2369
|
+
const auth2 = c.req.header("authorization");
|
|
2370
|
+
const accessToken = auth2 && auth2.replace(/^Bearer /i, "") || readCookieFromHeader(c.req.header("cookie"), accessCookie);
|
|
2371
|
+
return honoResponse(await handleUserinfo(helperConfig, { accessToken, req: c.req }));
|
|
2372
|
+
}
|
|
2099
2373
|
if (mountHelpers && path.startsWith(mount + "/") && c.req.method === "POST") {
|
|
2100
2374
|
const body = await c.req.json().catch(() => ({}));
|
|
2101
2375
|
const cookieHeader = c.req.header("cookie");
|
|
@@ -2108,12 +2382,15 @@ function iqAuth(options) {
|
|
|
2108
2382
|
}
|
|
2109
2383
|
if (path === `${mount}/refresh`) {
|
|
2110
2384
|
const refreshToken = body.refreshToken || readCookieFromHeader(cookieHeader, refreshCookie);
|
|
2111
|
-
|
|
2385
|
+
const idempotencyToken = c.req.header("x-iqauth-idempotency") || body.idempotencyToken;
|
|
2386
|
+
return honoResponse(await handleRefresh(helperConfig, { refreshToken, idempotencyToken }));
|
|
2112
2387
|
}
|
|
2113
2388
|
if (path === `${mount}/signout`) {
|
|
2114
2389
|
const auth2 = c.req.header("authorization");
|
|
2115
2390
|
const accessToken = auth2 && auth2.replace(/^Bearer /i, "") || readCookieFromHeader(cookieHeader, accessCookie);
|
|
2116
|
-
|
|
2391
|
+
const refreshToken = readCookieFromHeader(cookieHeader, refreshCookie);
|
|
2392
|
+
const idempotencyToken = c.req.header("x-iqauth-idempotency");
|
|
2393
|
+
return honoResponse(await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader: cookieHeader }));
|
|
2117
2394
|
}
|
|
2118
2395
|
}
|
|
2119
2396
|
if (isPublic(path)) return next();
|