@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/server.js
CHANGED
|
@@ -36,24 +36,45 @@ __export(server_exports, {
|
|
|
36
36
|
IQAuthClient: () => IQAuthClient,
|
|
37
37
|
IQAuthError: () => IQAuthError,
|
|
38
38
|
ServerIQAuthClient: () => ServerIQAuthClient,
|
|
39
|
+
buildUserinfoResponse: () => buildUserinfoResponse,
|
|
40
|
+
createDrizzleLinkAdapter: () => createDrizzleLinkAdapter,
|
|
39
41
|
createProvisioningBridge: () => createProvisioningBridge,
|
|
40
42
|
createServerClient: () => createServerClient,
|
|
41
43
|
handleCallback: () => handleCallback,
|
|
42
44
|
handleRefresh: () => handleRefresh,
|
|
43
45
|
handleSignout: () => handleSignout,
|
|
46
|
+
handleUserinfo: () => handleUserinfo,
|
|
44
47
|
iqAuthMiddleware: () => iqAuthMiddleware,
|
|
48
|
+
linkLocalUserToIqAuthSub: () => linkLocalUserToIqAuthSub,
|
|
45
49
|
serializeCookie: () => serializeCookie
|
|
46
50
|
});
|
|
47
51
|
module.exports = __toCommonJS(server_exports);
|
|
48
52
|
|
|
49
53
|
// src/errors.ts
|
|
50
|
-
var IQAuthError = class extends Error {
|
|
51
|
-
constructor(code, message, status,
|
|
54
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
55
|
+
constructor(code, message, status, cause) {
|
|
52
56
|
super(message);
|
|
53
57
|
this.name = "IQAuthError";
|
|
54
58
|
this.code = code;
|
|
55
59
|
this.status = status;
|
|
56
|
-
this.
|
|
60
|
+
this.cause = cause;
|
|
61
|
+
this.raw = cause;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
65
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
66
|
+
*/
|
|
67
|
+
static isIQAuthError(value) {
|
|
68
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Type-narrowed code check. Lets callers write
|
|
72
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
73
|
+
* taxonomy without losing the ability to handle server codes via
|
|
74
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
75
|
+
*/
|
|
76
|
+
is(code) {
|
|
77
|
+
return this.code === code;
|
|
57
78
|
}
|
|
58
79
|
};
|
|
59
80
|
var ErrorCodes = {
|
|
@@ -204,7 +225,7 @@ var HttpClient = class {
|
|
|
204
225
|
headers: this.buildHeaders(),
|
|
205
226
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
206
227
|
const refreshToken = this.config.getRefreshToken();
|
|
207
|
-
if (!refreshToken) throw new IQAuthError("
|
|
228
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
208
229
|
return { body: JSON.stringify({ refreshToken }) };
|
|
209
230
|
})()
|
|
210
231
|
});
|
|
@@ -221,7 +242,7 @@ var HttpClient = class {
|
|
|
221
242
|
return;
|
|
222
243
|
}
|
|
223
244
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
224
|
-
throw new IQAuthError("
|
|
245
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
225
246
|
}
|
|
226
247
|
const tokens = {
|
|
227
248
|
accessToken: body.data.accessToken,
|
|
@@ -239,7 +260,7 @@ var HttpClient = class {
|
|
|
239
260
|
return this.requestWithRetry(method, path, body, options, false);
|
|
240
261
|
}
|
|
241
262
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
242
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
263
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
243
264
|
await this.attemptRefresh();
|
|
244
265
|
}
|
|
245
266
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -467,6 +488,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
467
488
|
"iqvalidate"
|
|
468
489
|
];
|
|
469
490
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
491
|
+
function classifyJoseError(err) {
|
|
492
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
493
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
494
|
+
}
|
|
495
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
496
|
+
return { code: "token_invalid", message: err.message };
|
|
497
|
+
}
|
|
498
|
+
if (err instanceof Error) {
|
|
499
|
+
return { code: "token_invalid", message: err.message };
|
|
500
|
+
}
|
|
501
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
502
|
+
}
|
|
470
503
|
function decodeProtectedHeader(token) {
|
|
471
504
|
const parts = token.split(".");
|
|
472
505
|
if (parts.length < 2) return null;
|
|
@@ -503,11 +536,11 @@ var TokensModule = class {
|
|
|
503
536
|
async verify(token, options = {}) {
|
|
504
537
|
const header = decodeProtectedHeader(token);
|
|
505
538
|
if (!header) {
|
|
506
|
-
throw new IQAuthError("
|
|
539
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
507
540
|
}
|
|
508
541
|
const kid = header.kid;
|
|
509
542
|
if (!kid) {
|
|
510
|
-
throw new IQAuthError("
|
|
543
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
511
544
|
}
|
|
512
545
|
let cache = await this.ensureCache();
|
|
513
546
|
if (!cache.byKid.has(kid)) {
|
|
@@ -515,7 +548,7 @@ var TokensModule = class {
|
|
|
515
548
|
cache = await this.ensureCache();
|
|
516
549
|
}
|
|
517
550
|
if (!cache.byKid.has(kid)) {
|
|
518
|
-
throw new IQAuthError("
|
|
551
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
519
552
|
}
|
|
520
553
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
521
554
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -531,16 +564,8 @@ var TokensModule = class {
|
|
|
531
564
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
532
565
|
return payload;
|
|
533
566
|
} catch (err) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
538
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
539
|
-
}
|
|
540
|
-
if (err instanceof Error) {
|
|
541
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
542
|
-
}
|
|
543
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
567
|
+
const classified = classifyJoseError(err);
|
|
568
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
544
569
|
}
|
|
545
570
|
}
|
|
546
571
|
/**
|
|
@@ -582,7 +607,7 @@ var TokensModule = class {
|
|
|
582
607
|
getClaims(token) {
|
|
583
608
|
const claims = this.decode(token);
|
|
584
609
|
if (!claims) {
|
|
585
|
-
throw new IQAuthError("
|
|
610
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
586
611
|
}
|
|
587
612
|
return claims;
|
|
588
613
|
}
|
|
@@ -592,7 +617,7 @@ var TokensModule = class {
|
|
|
592
617
|
}
|
|
593
618
|
await this.refreshJwks();
|
|
594
619
|
if (!this.jwksCache) {
|
|
595
|
-
throw new IQAuthError("
|
|
620
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
596
621
|
}
|
|
597
622
|
return this.jwksCache;
|
|
598
623
|
}
|
|
@@ -602,22 +627,38 @@ var TokensModule = class {
|
|
|
602
627
|
}
|
|
603
628
|
this.inFlightRefresh = (async () => {
|
|
604
629
|
try {
|
|
605
|
-
|
|
630
|
+
let res;
|
|
631
|
+
try {
|
|
632
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
633
|
+
} catch (err) {
|
|
634
|
+
throw new IQAuthError(
|
|
635
|
+
"network",
|
|
636
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
637
|
+
void 0,
|
|
638
|
+
err
|
|
639
|
+
);
|
|
640
|
+
}
|
|
606
641
|
if (!res.ok) {
|
|
607
642
|
throw new IQAuthError(
|
|
608
|
-
"
|
|
609
|
-
`Failed to fetch JWKS: ${res.status}
|
|
643
|
+
"jwks_fetch_failed",
|
|
644
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
645
|
+
res.status
|
|
610
646
|
);
|
|
611
647
|
}
|
|
612
648
|
let jwks;
|
|
613
649
|
try {
|
|
614
650
|
jwks = await res.json();
|
|
615
|
-
} catch {
|
|
616
|
-
throw new IQAuthError(
|
|
651
|
+
} catch (err) {
|
|
652
|
+
throw new IQAuthError(
|
|
653
|
+
"jwks_fetch_failed",
|
|
654
|
+
"Malformed JWKS response: invalid JSON",
|
|
655
|
+
res.status,
|
|
656
|
+
err
|
|
657
|
+
);
|
|
617
658
|
}
|
|
618
659
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
619
660
|
throw new IQAuthError(
|
|
620
|
-
"
|
|
661
|
+
"jwks_fetch_failed",
|
|
621
662
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
622
663
|
);
|
|
623
664
|
}
|
|
@@ -625,7 +666,7 @@ var TokensModule = class {
|
|
|
625
666
|
for (const key of jwks.keys) {
|
|
626
667
|
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")) {
|
|
627
668
|
throw new IQAuthError(
|
|
628
|
-
"
|
|
669
|
+
"jwks_fetch_failed",
|
|
629
670
|
"Malformed JWKS response: key missing required fields"
|
|
630
671
|
);
|
|
631
672
|
}
|
|
@@ -643,6 +684,19 @@ var TokensModule = class {
|
|
|
643
684
|
clearCache() {
|
|
644
685
|
this.jwksCache = null;
|
|
645
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
689
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
690
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
691
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
692
|
+
*/
|
|
693
|
+
async prewarm() {
|
|
694
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
695
|
+
try {
|
|
696
|
+
await this.refreshJwks();
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
699
|
+
}
|
|
646
700
|
};
|
|
647
701
|
|
|
648
702
|
// src/modules/sessions.ts
|
|
@@ -966,14 +1020,14 @@ var OidcModule = class {
|
|
|
966
1020
|
*/
|
|
967
1021
|
async handleCallback(params) {
|
|
968
1022
|
if (!params.state) {
|
|
969
|
-
throw new IQAuthError("
|
|
1023
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
970
1024
|
}
|
|
971
1025
|
if (!params.code) {
|
|
972
|
-
throw new IQAuthError("
|
|
1026
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
973
1027
|
}
|
|
974
1028
|
const stored = await this.stateStore.get(params.state);
|
|
975
1029
|
if (!stored) {
|
|
976
|
-
throw new IQAuthError("
|
|
1030
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
977
1031
|
}
|
|
978
1032
|
let tokens;
|
|
979
1033
|
try {
|
|
@@ -991,7 +1045,7 @@ var OidcModule = class {
|
|
|
991
1045
|
if (tokens.id_token) {
|
|
992
1046
|
if (!this.tokensModule) {
|
|
993
1047
|
throw new IQAuthError(
|
|
994
|
-
"
|
|
1048
|
+
"config_invalid",
|
|
995
1049
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
996
1050
|
);
|
|
997
1051
|
}
|
|
@@ -1002,7 +1056,7 @@ var OidcModule = class {
|
|
|
1002
1056
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
1003
1057
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
1004
1058
|
throw new IQAuthError(
|
|
1005
|
-
"
|
|
1059
|
+
"token_invalid",
|
|
1006
1060
|
"OIDC id_token nonce did not match the stored value"
|
|
1007
1061
|
);
|
|
1008
1062
|
}
|
|
@@ -1203,6 +1257,9 @@ var AppsModule = class {
|
|
|
1203
1257
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1204
1258
|
*/
|
|
1205
1259
|
async get(appKey) {
|
|
1260
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1261
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1262
|
+
}
|
|
1206
1263
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1207
1264
|
}
|
|
1208
1265
|
/**
|
|
@@ -1222,6 +1279,16 @@ var AppsModule = class {
|
|
|
1222
1279
|
401
|
|
1223
1280
|
);
|
|
1224
1281
|
}
|
|
1282
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1283
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1284
|
+
}
|
|
1285
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1286
|
+
throw new IQAuthError(
|
|
1287
|
+
"ENVIRONMENT_REQUIRED",
|
|
1288
|
+
"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.",
|
|
1289
|
+
400
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1225
1292
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1226
1293
|
}
|
|
1227
1294
|
/**
|
|
@@ -1231,11 +1298,14 @@ var AppsModule = class {
|
|
|
1231
1298
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1232
1299
|
*/
|
|
1233
1300
|
async isRegistered(appKey) {
|
|
1301
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1302
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1303
|
+
}
|
|
1234
1304
|
try {
|
|
1235
1305
|
await this.get(appKey);
|
|
1236
1306
|
return true;
|
|
1237
1307
|
} catch (err) {
|
|
1238
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1308
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1239
1309
|
return false;
|
|
1240
1310
|
}
|
|
1241
1311
|
throw err;
|
|
@@ -1272,6 +1342,20 @@ var RolesModule = class {
|
|
|
1272
1342
|
};
|
|
1273
1343
|
|
|
1274
1344
|
// src/modules/permissionGroups.ts
|
|
1345
|
+
function assertAppKey(appKey, callsite) {
|
|
1346
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1347
|
+
throw new IQAuthError(
|
|
1348
|
+
"VALIDATION_ERROR",
|
|
1349
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1350
|
+
400
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1355
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1356
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1275
1359
|
var PermissionGroupsModule = class {
|
|
1276
1360
|
constructor(http) {
|
|
1277
1361
|
this.http = http;
|
|
@@ -1292,7 +1376,14 @@ var PermissionGroupsModule = class {
|
|
|
1292
1376
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1293
1377
|
}
|
|
1294
1378
|
async addPermission(tenantId, groupId, data) {
|
|
1295
|
-
|
|
1379
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1380
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1381
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1382
|
+
appKey: data.appKey,
|
|
1383
|
+
nodeKey: data.nodeKey,
|
|
1384
|
+
effect: data.effect,
|
|
1385
|
+
weight: data.weight
|
|
1386
|
+
});
|
|
1296
1387
|
}
|
|
1297
1388
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1298
1389
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1316,21 +1407,51 @@ var PermissionGroupsModule = class {
|
|
|
1316
1407
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1317
1408
|
}
|
|
1318
1409
|
async addUserOverride(tenantId, userId, data) {
|
|
1319
|
-
|
|
1410
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1411
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1412
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1413
|
+
appKey: data.appKey,
|
|
1414
|
+
nodeKey: data.nodeKey,
|
|
1415
|
+
effect: data.effect,
|
|
1416
|
+
weight: data.weight,
|
|
1417
|
+
expiresAt: data.expiresAt
|
|
1418
|
+
});
|
|
1320
1419
|
}
|
|
1321
1420
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1322
1421
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1323
1422
|
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1425
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1426
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1427
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1428
|
+
*/
|
|
1324
1429
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const qs = query.toString();
|
|
1329
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1430
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1431
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1432
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1330
1433
|
}
|
|
1331
1434
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1435
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1436
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1332
1437
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1333
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1441
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1442
|
+
* single misconfigured entry can't slip through and silently report
|
|
1443
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1444
|
+
*/
|
|
1445
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1446
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1447
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1448
|
+
}
|
|
1449
|
+
checks.forEach((c, i) => {
|
|
1450
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1451
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1452
|
+
});
|
|
1453
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1454
|
+
}
|
|
1334
1455
|
};
|
|
1335
1456
|
|
|
1336
1457
|
// src/modules/apiKeys.ts
|
|
@@ -1755,6 +1876,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1755
1876
|
this._refreshToken = tokens.refreshToken;
|
|
1756
1877
|
},
|
|
1757
1878
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1879
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1880
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1881
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1882
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1758
1883
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1759
1884
|
sessionHeaderName: config.sessionHeaderName,
|
|
1760
1885
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1795,6 +1920,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1795
1920
|
static forServer(config) {
|
|
1796
1921
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1797
1922
|
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1925
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1926
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1927
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1928
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1929
|
+
*/
|
|
1798
1930
|
static forMobile(config) {
|
|
1799
1931
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1800
1932
|
}
|
|
@@ -1811,6 +1943,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1811
1943
|
getRefreshToken() {
|
|
1812
1944
|
return this._refreshToken;
|
|
1813
1945
|
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1948
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1949
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1950
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1951
|
+
*/
|
|
1952
|
+
async prewarm() {
|
|
1953
|
+
await Promise.all([
|
|
1954
|
+
this.tokens.prewarm(),
|
|
1955
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1956
|
+
]);
|
|
1957
|
+
}
|
|
1814
1958
|
getCurrentClaims() {
|
|
1815
1959
|
if (!this._accessToken) return null;
|
|
1816
1960
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1851,14 +1995,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1851
1995
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1852
1996
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1853
1997
|
throw new IQAuthError(
|
|
1854
|
-
"
|
|
1998
|
+
"config_invalid",
|
|
1855
1999
|
`${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.`
|
|
1856
2000
|
);
|
|
1857
2001
|
}
|
|
1858
2002
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1859
2003
|
if (!shapeMatch) {
|
|
1860
2004
|
throw new IQAuthError(
|
|
1861
|
-
"
|
|
2005
|
+
"config_invalid",
|
|
1862
2006
|
`${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.`
|
|
1863
2007
|
);
|
|
1864
2008
|
}
|
|
@@ -1867,19 +2011,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1867
2011
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1868
2012
|
} catch {
|
|
1869
2013
|
throw new IQAuthError(
|
|
1870
|
-
"
|
|
2014
|
+
"config_invalid",
|
|
1871
2015
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1872
2016
|
);
|
|
1873
2017
|
}
|
|
1874
2018
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1875
2019
|
throw new IQAuthError(
|
|
1876
|
-
"
|
|
2020
|
+
"config_invalid",
|
|
1877
2021
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1878
2022
|
);
|
|
1879
2023
|
}
|
|
1880
2024
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1881
2025
|
throw new IQAuthError(
|
|
1882
|
-
"
|
|
2026
|
+
"config_invalid",
|
|
1883
2027
|
`${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.`
|
|
1884
2028
|
);
|
|
1885
2029
|
}
|
|
@@ -1893,12 +2037,18 @@ function isPublishableKeyPayload(value) {
|
|
|
1893
2037
|
|
|
1894
2038
|
// src/middleware/express.ts
|
|
1895
2039
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2040
|
+
// Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
|
|
1896
2041
|
"TOKEN_INVALID",
|
|
1897
2042
|
"TOKEN_EXPIRED",
|
|
1898
2043
|
"TOKEN_REVOKED",
|
|
1899
2044
|
"SESSION_EXPIRED",
|
|
1900
2045
|
"SESSION_INVALID",
|
|
1901
|
-
"AUTH_REQUIRED"
|
|
2046
|
+
"AUTH_REQUIRED",
|
|
2047
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
|
|
2048
|
+
// Mapped to 401 here so framework consumers don't have to learn the new
|
|
2049
|
+
// codes to keep their auth-failure handling working.
|
|
2050
|
+
"token_invalid",
|
|
2051
|
+
"token_expired"
|
|
1902
2052
|
]);
|
|
1903
2053
|
var DEFAULT_ACCESS_COOKIE = "iqauth_at";
|
|
1904
2054
|
var DEFAULT_REFRESH_COOKIE = "iqauth_rt";
|
|
@@ -2082,6 +2232,41 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2082
2232
|
}
|
|
2083
2233
|
|
|
2084
2234
|
// src/server/handlers.ts
|
|
2235
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2236
|
+
const baseUser = {
|
|
2237
|
+
sub: claims.sub,
|
|
2238
|
+
email: claims.email,
|
|
2239
|
+
name: claims.name,
|
|
2240
|
+
tenantId: claims.tenantId,
|
|
2241
|
+
vendorId: claims.vendorId,
|
|
2242
|
+
roles: claims.roles ?? [],
|
|
2243
|
+
entitlements: claims.entitlements ?? []
|
|
2244
|
+
};
|
|
2245
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2246
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2247
|
+
return {
|
|
2248
|
+
success: true,
|
|
2249
|
+
data: {
|
|
2250
|
+
user,
|
|
2251
|
+
claims,
|
|
2252
|
+
tenantId: claims.tenantId ?? null
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
function emitTiming(cfg, event) {
|
|
2257
|
+
if (cfg.debug) {
|
|
2258
|
+
try {
|
|
2259
|
+
console.debug("[iqauth_helper]", event);
|
|
2260
|
+
} catch {
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
if (cfg.onTimingEvent) {
|
|
2264
|
+
try {
|
|
2265
|
+
cfg.onTimingEvent(event);
|
|
2266
|
+
} catch {
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2085
2270
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2086
2271
|
"TOKEN_REVOKED",
|
|
2087
2272
|
"SESSION_REVOKED",
|
|
@@ -2121,7 +2306,11 @@ function resolve(config) {
|
|
|
2121
2306
|
})),
|
|
2122
2307
|
appId: parsed.appId,
|
|
2123
2308
|
tenantId: parsed.tenantId,
|
|
2124
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2309
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2310
|
+
debug: config.debug,
|
|
2311
|
+
onTimingEvent: config.onTimingEvent,
|
|
2312
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2313
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
2125
2314
|
};
|
|
2126
2315
|
}
|
|
2127
2316
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -2138,15 +2327,41 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
2138
2327
|
}
|
|
2139
2328
|
function clearCookies(cfg) {
|
|
2140
2329
|
return [
|
|
2141
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
2142
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2330
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2331
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
2143
2332
|
];
|
|
2144
2333
|
}
|
|
2334
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2335
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2336
|
+
function pruneInMemoryMarkers(now) {
|
|
2337
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2338
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2339
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
var defaultSignoutRegistry = {
|
|
2343
|
+
mark(token, ttlMs) {
|
|
2344
|
+
const now = Date.now();
|
|
2345
|
+
pruneInMemoryMarkers(now);
|
|
2346
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2347
|
+
},
|
|
2348
|
+
has(token) {
|
|
2349
|
+
const now = Date.now();
|
|
2350
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2351
|
+
if (!exp) return false;
|
|
2352
|
+
if (exp <= now) {
|
|
2353
|
+
inMemorySignoutMarkers.delete(token);
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
return true;
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2145
2359
|
function serializeCookie(d) {
|
|
2146
2360
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
2147
2361
|
parts.push(`Path=${d.path}`);
|
|
2148
2362
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
2149
2363
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
2364
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
2150
2365
|
if (d.secure) parts.push("Secure");
|
|
2151
2366
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
2152
2367
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -2154,7 +2369,9 @@ function serializeCookie(d) {
|
|
|
2154
2369
|
}
|
|
2155
2370
|
async function handleCallback(config, input) {
|
|
2156
2371
|
const cfg = resolve(config);
|
|
2372
|
+
const t0 = Date.now();
|
|
2157
2373
|
if (!input.code || !input.redirectUri) {
|
|
2374
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
2158
2375
|
return {
|
|
2159
2376
|
status: 400,
|
|
2160
2377
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
@@ -2162,6 +2379,7 @@ async function handleCallback(config, input) {
|
|
|
2162
2379
|
};
|
|
2163
2380
|
}
|
|
2164
2381
|
if (!cfg.secretKey) {
|
|
2382
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
2165
2383
|
return {
|
|
2166
2384
|
status: 500,
|
|
2167
2385
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -2185,6 +2403,7 @@ async function handleCallback(config, input) {
|
|
|
2185
2403
|
});
|
|
2186
2404
|
const json = await res.json().catch(() => ({}));
|
|
2187
2405
|
if (!res.ok || !json.access_token) {
|
|
2406
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
2188
2407
|
return {
|
|
2189
2408
|
status: res.status || 502,
|
|
2190
2409
|
body: {
|
|
@@ -2204,6 +2423,7 @@ async function handleCallback(config, input) {
|
|
|
2204
2423
|
if (json.refresh_token) {
|
|
2205
2424
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
2206
2425
|
}
|
|
2426
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
2207
2427
|
return {
|
|
2208
2428
|
status: 200,
|
|
2209
2429
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -2212,8 +2432,18 @@ async function handleCallback(config, input) {
|
|
|
2212
2432
|
}
|
|
2213
2433
|
async function handleRefresh(config, input) {
|
|
2214
2434
|
const cfg = resolve(config);
|
|
2435
|
+
const t0 = Date.now();
|
|
2215
2436
|
const refreshToken = input.refreshToken;
|
|
2437
|
+
const idemKey = input.idempotencyToken;
|
|
2438
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2439
|
+
return {
|
|
2440
|
+
status: 401,
|
|
2441
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2442
|
+
cookies: clearCookies(cfg)
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2216
2445
|
if (!refreshToken) {
|
|
2446
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
2217
2447
|
return {
|
|
2218
2448
|
status: 401,
|
|
2219
2449
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -2229,6 +2459,7 @@ async function handleRefresh(config, input) {
|
|
|
2229
2459
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
2230
2460
|
const status = res.status || 401;
|
|
2231
2461
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2462
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
2232
2463
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
2233
2464
|
cfg.clearCookiesOnRefreshFailure,
|
|
2234
2465
|
status,
|
|
@@ -2252,6 +2483,7 @@ async function handleRefresh(config, input) {
|
|
|
2252
2483
|
if (json.data.refreshToken) {
|
|
2253
2484
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2254
2485
|
}
|
|
2486
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2255
2487
|
return {
|
|
2256
2488
|
status: 200,
|
|
2257
2489
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2260,6 +2492,10 @@ async function handleRefresh(config, input) {
|
|
|
2260
2492
|
}
|
|
2261
2493
|
async function handleSignout(config, input) {
|
|
2262
2494
|
const cfg = resolve(config);
|
|
2495
|
+
const t0 = Date.now();
|
|
2496
|
+
if (input.idempotencyToken) {
|
|
2497
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2498
|
+
}
|
|
2263
2499
|
if (input.accessToken) {
|
|
2264
2500
|
try {
|
|
2265
2501
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2281,12 +2517,52 @@ async function handleSignout(config, input) {
|
|
|
2281
2517
|
} catch {
|
|
2282
2518
|
}
|
|
2283
2519
|
}
|
|
2520
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2284
2521
|
return {
|
|
2285
2522
|
status: 200,
|
|
2286
2523
|
body: { success: true, data: { signedOut: true } },
|
|
2287
2524
|
cookies: clearCookies(cfg)
|
|
2288
2525
|
};
|
|
2289
2526
|
}
|
|
2527
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2528
|
+
function getTokensFor(issuer) {
|
|
2529
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2530
|
+
if (!m) {
|
|
2531
|
+
m = new TokensModule(issuer);
|
|
2532
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2533
|
+
}
|
|
2534
|
+
return m;
|
|
2535
|
+
}
|
|
2536
|
+
async function handleUserinfo(config, input) {
|
|
2537
|
+
const cfg = resolve(config);
|
|
2538
|
+
if (!input.accessToken) {
|
|
2539
|
+
return {
|
|
2540
|
+
status: 401,
|
|
2541
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2542
|
+
cookies: []
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
let claims;
|
|
2546
|
+
try {
|
|
2547
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
|
|
2548
|
+
} catch (err) {
|
|
2549
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2550
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2551
|
+
return {
|
|
2552
|
+
status: 401,
|
|
2553
|
+
body: { success: false, error: { code, message } },
|
|
2554
|
+
cookies: []
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2558
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2559
|
+
});
|
|
2560
|
+
return {
|
|
2561
|
+
status: 200,
|
|
2562
|
+
body: envelope,
|
|
2563
|
+
cookies: []
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2290
2566
|
|
|
2291
2567
|
// src/server/provisioningBridge.ts
|
|
2292
2568
|
function defaultIsUniqueViolation(err) {
|
|
@@ -2339,6 +2615,82 @@ function createProvisioningBridge(options) {
|
|
|
2339
2615
|
return { ensureUser };
|
|
2340
2616
|
}
|
|
2341
2617
|
|
|
2618
|
+
// src/server/linkLocalUser.ts
|
|
2619
|
+
async function linkLocalUserToIqAuthSub(options) {
|
|
2620
|
+
const { adapter, claims } = options;
|
|
2621
|
+
const lookupBy = options.lookupBy ?? ["email"];
|
|
2622
|
+
const caseInsensitive = options.caseInsensitiveEmail !== false;
|
|
2623
|
+
if (!claims?.sub) throw new Error("linkLocalUserToIqAuthSub: claims.sub is required");
|
|
2624
|
+
return adapter.withTransaction(async (tx) => {
|
|
2625
|
+
const bySub = await tx.findByIqAuthSub(claims.sub);
|
|
2626
|
+
if (bySub) return { status: "already_linked", userId: bySub.id };
|
|
2627
|
+
for (const key of lookupBy) {
|
|
2628
|
+
if (key !== "email") continue;
|
|
2629
|
+
if (!claims.email) continue;
|
|
2630
|
+
const matches = await tx.findByEmail(claims.email, { caseInsensitive });
|
|
2631
|
+
if (matches.length === 0) continue;
|
|
2632
|
+
if (matches.length > 1) {
|
|
2633
|
+
return { status: "conflict", reason: "duplicate_email" };
|
|
2634
|
+
}
|
|
2635
|
+
const row = matches[0];
|
|
2636
|
+
if (row.iqauthSub && row.iqauthSub !== claims.sub) {
|
|
2637
|
+
return { status: "conflict", userId: row.id, reason: "different_sub" };
|
|
2638
|
+
}
|
|
2639
|
+
if (row.iqauthSub === claims.sub) {
|
|
2640
|
+
return { status: "already_linked", userId: row.id };
|
|
2641
|
+
}
|
|
2642
|
+
const wrote = await tx.setIqAuthSub(row.id, claims.sub);
|
|
2643
|
+
if (wrote === false) {
|
|
2644
|
+
return { status: "conflict", userId: row.id, reason: "different_sub" };
|
|
2645
|
+
}
|
|
2646
|
+
return { status: "linked", userId: row.id };
|
|
2647
|
+
}
|
|
2648
|
+
return { status: "not_found" };
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
function createDrizzleLinkAdapter(deps) {
|
|
2652
|
+
const { db, table, columns, eq, sql } = deps;
|
|
2653
|
+
const iqauthSubKey = deps.columnNames?.iqauthSub ?? "iqauthSub";
|
|
2654
|
+
return {
|
|
2655
|
+
async withTransaction(fn) {
|
|
2656
|
+
return db.transaction(async (txDb) => {
|
|
2657
|
+
const lockedRead = async (cond, limit) => {
|
|
2658
|
+
const built = txDb.select().from(table).where(cond).limit(limit);
|
|
2659
|
+
if (typeof built.for === "function") {
|
|
2660
|
+
return built.for("update");
|
|
2661
|
+
}
|
|
2662
|
+
return built;
|
|
2663
|
+
};
|
|
2664
|
+
const tx = {
|
|
2665
|
+
async findByIqAuthSub(sub) {
|
|
2666
|
+
const rows = await lockedRead(eq(columns.iqauthSub, sub), 1);
|
|
2667
|
+
return rows[0] ?? null;
|
|
2668
|
+
},
|
|
2669
|
+
async findByEmail(email, { caseInsensitive }) {
|
|
2670
|
+
const cond = caseInsensitive ? sql`lower(${columns.email}) = lower(${email})` : eq(columns.email, email);
|
|
2671
|
+
return lockedRead(cond, 2);
|
|
2672
|
+
},
|
|
2673
|
+
async setIqAuthSub(userId, sub) {
|
|
2674
|
+
const result = await txDb.update(table).set({ [iqauthSubKey]: sub }).where(
|
|
2675
|
+
sql`${columns.id} = ${userId} AND (${columns.iqauthSub} IS NULL OR ${columns.iqauthSub} = ${sub})`
|
|
2676
|
+
);
|
|
2677
|
+
const r = result;
|
|
2678
|
+
if (Array.isArray(r)) {
|
|
2679
|
+
return (r[0]?.affectedRows ?? 1) > 0;
|
|
2680
|
+
}
|
|
2681
|
+
if (r && typeof r === "object") {
|
|
2682
|
+
const n = r.rowCount ?? r.rowsAffected ?? r.changes;
|
|
2683
|
+
if (typeof n === "number") return n > 0;
|
|
2684
|
+
}
|
|
2685
|
+
return true;
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
return fn(tx);
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2342
2694
|
// src/server.ts
|
|
2343
2695
|
var ServerIQAuthClient = class extends IQAuthClient {
|
|
2344
2696
|
constructor(config) {
|
|
@@ -2359,11 +2711,15 @@ function createServerClient(config) {
|
|
|
2359
2711
|
IQAuthClient,
|
|
2360
2712
|
IQAuthError,
|
|
2361
2713
|
ServerIQAuthClient,
|
|
2714
|
+
buildUserinfoResponse,
|
|
2715
|
+
createDrizzleLinkAdapter,
|
|
2362
2716
|
createProvisioningBridge,
|
|
2363
2717
|
createServerClient,
|
|
2364
2718
|
handleCallback,
|
|
2365
2719
|
handleRefresh,
|
|
2366
2720
|
handleSignout,
|
|
2721
|
+
handleUserinfo,
|
|
2367
2722
|
iqAuthMiddleware,
|
|
2723
|
+
linkLocalUserToIqAuthSub,
|
|
2368
2724
|
serializeCookie
|
|
2369
2725
|
});
|