@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/mobile.js
CHANGED
|
@@ -39,13 +39,30 @@ __export(mobile_exports, {
|
|
|
39
39
|
module.exports = __toCommonJS(mobile_exports);
|
|
40
40
|
|
|
41
41
|
// src/errors.ts
|
|
42
|
-
var IQAuthError = class extends Error {
|
|
43
|
-
constructor(code, message, status,
|
|
42
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
43
|
+
constructor(code, message, status, cause) {
|
|
44
44
|
super(message);
|
|
45
45
|
this.name = "IQAuthError";
|
|
46
46
|
this.code = code;
|
|
47
47
|
this.status = status;
|
|
48
|
-
this.
|
|
48
|
+
this.cause = cause;
|
|
49
|
+
this.raw = cause;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
53
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
54
|
+
*/
|
|
55
|
+
static isIQAuthError(value) {
|
|
56
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Type-narrowed code check. Lets callers write
|
|
60
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
61
|
+
* taxonomy without losing the ability to handle server codes via
|
|
62
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
63
|
+
*/
|
|
64
|
+
is(code) {
|
|
65
|
+
return this.code === code;
|
|
49
66
|
}
|
|
50
67
|
};
|
|
51
68
|
var ErrorCodes = {
|
|
@@ -196,7 +213,7 @@ var HttpClient = class {
|
|
|
196
213
|
headers: this.buildHeaders(),
|
|
197
214
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
198
215
|
const refreshToken = this.config.getRefreshToken();
|
|
199
|
-
if (!refreshToken) throw new IQAuthError("
|
|
216
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
200
217
|
return { body: JSON.stringify({ refreshToken }) };
|
|
201
218
|
})()
|
|
202
219
|
});
|
|
@@ -213,7 +230,7 @@ var HttpClient = class {
|
|
|
213
230
|
return;
|
|
214
231
|
}
|
|
215
232
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
216
|
-
throw new IQAuthError("
|
|
233
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
217
234
|
}
|
|
218
235
|
const tokens = {
|
|
219
236
|
accessToken: body.data.accessToken,
|
|
@@ -231,7 +248,7 @@ var HttpClient = class {
|
|
|
231
248
|
return this.requestWithRetry(method, path, body, options, false);
|
|
232
249
|
}
|
|
233
250
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
234
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
251
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
235
252
|
await this.attemptRefresh();
|
|
236
253
|
}
|
|
237
254
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -459,6 +476,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
459
476
|
"iqvalidate"
|
|
460
477
|
];
|
|
461
478
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
479
|
+
function classifyJoseError(err) {
|
|
480
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
481
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
482
|
+
}
|
|
483
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
484
|
+
return { code: "token_invalid", message: err.message };
|
|
485
|
+
}
|
|
486
|
+
if (err instanceof Error) {
|
|
487
|
+
return { code: "token_invalid", message: err.message };
|
|
488
|
+
}
|
|
489
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
490
|
+
}
|
|
462
491
|
function decodeProtectedHeader(token) {
|
|
463
492
|
const parts = token.split(".");
|
|
464
493
|
if (parts.length < 2) return null;
|
|
@@ -495,11 +524,11 @@ var TokensModule = class {
|
|
|
495
524
|
async verify(token, options = {}) {
|
|
496
525
|
const header = decodeProtectedHeader(token);
|
|
497
526
|
if (!header) {
|
|
498
|
-
throw new IQAuthError("
|
|
527
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
499
528
|
}
|
|
500
529
|
const kid = header.kid;
|
|
501
530
|
if (!kid) {
|
|
502
|
-
throw new IQAuthError("
|
|
531
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
503
532
|
}
|
|
504
533
|
let cache = await this.ensureCache();
|
|
505
534
|
if (!cache.byKid.has(kid)) {
|
|
@@ -507,7 +536,7 @@ var TokensModule = class {
|
|
|
507
536
|
cache = await this.ensureCache();
|
|
508
537
|
}
|
|
509
538
|
if (!cache.byKid.has(kid)) {
|
|
510
|
-
throw new IQAuthError("
|
|
539
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
511
540
|
}
|
|
512
541
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
513
542
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -523,16 +552,8 @@ var TokensModule = class {
|
|
|
523
552
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
524
553
|
return payload;
|
|
525
554
|
} catch (err) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
530
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
531
|
-
}
|
|
532
|
-
if (err instanceof Error) {
|
|
533
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
534
|
-
}
|
|
535
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
555
|
+
const classified = classifyJoseError(err);
|
|
556
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
536
557
|
}
|
|
537
558
|
}
|
|
538
559
|
/**
|
|
@@ -574,7 +595,7 @@ var TokensModule = class {
|
|
|
574
595
|
getClaims(token) {
|
|
575
596
|
const claims = this.decode(token);
|
|
576
597
|
if (!claims) {
|
|
577
|
-
throw new IQAuthError("
|
|
598
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
578
599
|
}
|
|
579
600
|
return claims;
|
|
580
601
|
}
|
|
@@ -584,7 +605,7 @@ var TokensModule = class {
|
|
|
584
605
|
}
|
|
585
606
|
await this.refreshJwks();
|
|
586
607
|
if (!this.jwksCache) {
|
|
587
|
-
throw new IQAuthError("
|
|
608
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
588
609
|
}
|
|
589
610
|
return this.jwksCache;
|
|
590
611
|
}
|
|
@@ -594,22 +615,38 @@ var TokensModule = class {
|
|
|
594
615
|
}
|
|
595
616
|
this.inFlightRefresh = (async () => {
|
|
596
617
|
try {
|
|
597
|
-
|
|
618
|
+
let res;
|
|
619
|
+
try {
|
|
620
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
621
|
+
} catch (err) {
|
|
622
|
+
throw new IQAuthError(
|
|
623
|
+
"network",
|
|
624
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
625
|
+
void 0,
|
|
626
|
+
err
|
|
627
|
+
);
|
|
628
|
+
}
|
|
598
629
|
if (!res.ok) {
|
|
599
630
|
throw new IQAuthError(
|
|
600
|
-
"
|
|
601
|
-
`Failed to fetch JWKS: ${res.status}
|
|
631
|
+
"jwks_fetch_failed",
|
|
632
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
633
|
+
res.status
|
|
602
634
|
);
|
|
603
635
|
}
|
|
604
636
|
let jwks;
|
|
605
637
|
try {
|
|
606
638
|
jwks = await res.json();
|
|
607
|
-
} catch {
|
|
608
|
-
throw new IQAuthError(
|
|
639
|
+
} catch (err) {
|
|
640
|
+
throw new IQAuthError(
|
|
641
|
+
"jwks_fetch_failed",
|
|
642
|
+
"Malformed JWKS response: invalid JSON",
|
|
643
|
+
res.status,
|
|
644
|
+
err
|
|
645
|
+
);
|
|
609
646
|
}
|
|
610
647
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
611
648
|
throw new IQAuthError(
|
|
612
|
-
"
|
|
649
|
+
"jwks_fetch_failed",
|
|
613
650
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
614
651
|
);
|
|
615
652
|
}
|
|
@@ -617,7 +654,7 @@ var TokensModule = class {
|
|
|
617
654
|
for (const key of jwks.keys) {
|
|
618
655
|
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")) {
|
|
619
656
|
throw new IQAuthError(
|
|
620
|
-
"
|
|
657
|
+
"jwks_fetch_failed",
|
|
621
658
|
"Malformed JWKS response: key missing required fields"
|
|
622
659
|
);
|
|
623
660
|
}
|
|
@@ -635,6 +672,19 @@ var TokensModule = class {
|
|
|
635
672
|
clearCache() {
|
|
636
673
|
this.jwksCache = null;
|
|
637
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
677
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
678
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
679
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
680
|
+
*/
|
|
681
|
+
async prewarm() {
|
|
682
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
683
|
+
try {
|
|
684
|
+
await this.refreshJwks();
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
}
|
|
638
688
|
};
|
|
639
689
|
|
|
640
690
|
// src/modules/sessions.ts
|
|
@@ -958,14 +1008,14 @@ var OidcModule = class {
|
|
|
958
1008
|
*/
|
|
959
1009
|
async handleCallback(params) {
|
|
960
1010
|
if (!params.state) {
|
|
961
|
-
throw new IQAuthError("
|
|
1011
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
962
1012
|
}
|
|
963
1013
|
if (!params.code) {
|
|
964
|
-
throw new IQAuthError("
|
|
1014
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
965
1015
|
}
|
|
966
1016
|
const stored = await this.stateStore.get(params.state);
|
|
967
1017
|
if (!stored) {
|
|
968
|
-
throw new IQAuthError("
|
|
1018
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
969
1019
|
}
|
|
970
1020
|
let tokens;
|
|
971
1021
|
try {
|
|
@@ -983,7 +1033,7 @@ var OidcModule = class {
|
|
|
983
1033
|
if (tokens.id_token) {
|
|
984
1034
|
if (!this.tokensModule) {
|
|
985
1035
|
throw new IQAuthError(
|
|
986
|
-
"
|
|
1036
|
+
"config_invalid",
|
|
987
1037
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
988
1038
|
);
|
|
989
1039
|
}
|
|
@@ -994,7 +1044,7 @@ var OidcModule = class {
|
|
|
994
1044
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
995
1045
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
996
1046
|
throw new IQAuthError(
|
|
997
|
-
"
|
|
1047
|
+
"token_invalid",
|
|
998
1048
|
"OIDC id_token nonce did not match the stored value"
|
|
999
1049
|
);
|
|
1000
1050
|
}
|
|
@@ -1195,6 +1245,9 @@ var AppsModule = class {
|
|
|
1195
1245
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1196
1246
|
*/
|
|
1197
1247
|
async get(appKey) {
|
|
1248
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1249
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1250
|
+
}
|
|
1198
1251
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1199
1252
|
}
|
|
1200
1253
|
/**
|
|
@@ -1214,6 +1267,16 @@ var AppsModule = class {
|
|
|
1214
1267
|
401
|
|
1215
1268
|
);
|
|
1216
1269
|
}
|
|
1270
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1271
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1272
|
+
}
|
|
1273
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1274
|
+
throw new IQAuthError(
|
|
1275
|
+
"ENVIRONMENT_REQUIRED",
|
|
1276
|
+
"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.",
|
|
1277
|
+
400
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1217
1280
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1218
1281
|
}
|
|
1219
1282
|
/**
|
|
@@ -1223,11 +1286,14 @@ var AppsModule = class {
|
|
|
1223
1286
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1224
1287
|
*/
|
|
1225
1288
|
async isRegistered(appKey) {
|
|
1289
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1290
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1291
|
+
}
|
|
1226
1292
|
try {
|
|
1227
1293
|
await this.get(appKey);
|
|
1228
1294
|
return true;
|
|
1229
1295
|
} catch (err) {
|
|
1230
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1296
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1231
1297
|
return false;
|
|
1232
1298
|
}
|
|
1233
1299
|
throw err;
|
|
@@ -1264,6 +1330,20 @@ var RolesModule = class {
|
|
|
1264
1330
|
};
|
|
1265
1331
|
|
|
1266
1332
|
// src/modules/permissionGroups.ts
|
|
1333
|
+
function assertAppKey(appKey, callsite) {
|
|
1334
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1335
|
+
throw new IQAuthError(
|
|
1336
|
+
"VALIDATION_ERROR",
|
|
1337
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1338
|
+
400
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1343
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1344
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1267
1347
|
var PermissionGroupsModule = class {
|
|
1268
1348
|
constructor(http) {
|
|
1269
1349
|
this.http = http;
|
|
@@ -1284,7 +1364,14 @@ var PermissionGroupsModule = class {
|
|
|
1284
1364
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1285
1365
|
}
|
|
1286
1366
|
async addPermission(tenantId, groupId, data) {
|
|
1287
|
-
|
|
1367
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1368
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1369
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1370
|
+
appKey: data.appKey,
|
|
1371
|
+
nodeKey: data.nodeKey,
|
|
1372
|
+
effect: data.effect,
|
|
1373
|
+
weight: data.weight
|
|
1374
|
+
});
|
|
1288
1375
|
}
|
|
1289
1376
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1290
1377
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1308,21 +1395,51 @@ var PermissionGroupsModule = class {
|
|
|
1308
1395
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1309
1396
|
}
|
|
1310
1397
|
async addUserOverride(tenantId, userId, data) {
|
|
1311
|
-
|
|
1398
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1399
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1400
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1401
|
+
appKey: data.appKey,
|
|
1402
|
+
nodeKey: data.nodeKey,
|
|
1403
|
+
effect: data.effect,
|
|
1404
|
+
weight: data.weight,
|
|
1405
|
+
expiresAt: data.expiresAt
|
|
1406
|
+
});
|
|
1312
1407
|
}
|
|
1313
1408
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1314
1409
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1315
1410
|
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1413
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1414
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1415
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1416
|
+
*/
|
|
1316
1417
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const qs = query.toString();
|
|
1321
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1418
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1419
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1420
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1322
1421
|
}
|
|
1323
1422
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1423
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1424
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1324
1425
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1325
1426
|
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1429
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1430
|
+
* single misconfigured entry can't slip through and silently report
|
|
1431
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1432
|
+
*/
|
|
1433
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1434
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1435
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1436
|
+
}
|
|
1437
|
+
checks.forEach((c, i) => {
|
|
1438
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1439
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1440
|
+
});
|
|
1441
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1442
|
+
}
|
|
1326
1443
|
};
|
|
1327
1444
|
|
|
1328
1445
|
// src/modules/apiKeys.ts
|
|
@@ -1747,6 +1864,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1747
1864
|
this._refreshToken = tokens.refreshToken;
|
|
1748
1865
|
},
|
|
1749
1866
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1867
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1868
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1869
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1870
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1750
1871
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1751
1872
|
sessionHeaderName: config.sessionHeaderName,
|
|
1752
1873
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1787,6 +1908,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1787
1908
|
static forServer(config) {
|
|
1788
1909
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1789
1910
|
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1913
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1914
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1915
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1916
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1917
|
+
*/
|
|
1790
1918
|
static forMobile(config) {
|
|
1791
1919
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1792
1920
|
}
|
|
@@ -1803,6 +1931,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1803
1931
|
getRefreshToken() {
|
|
1804
1932
|
return this._refreshToken;
|
|
1805
1933
|
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1936
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1937
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1938
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1939
|
+
*/
|
|
1940
|
+
async prewarm() {
|
|
1941
|
+
await Promise.all([
|
|
1942
|
+
this.tokens.prewarm(),
|
|
1943
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1944
|
+
]);
|
|
1945
|
+
}
|
|
1806
1946
|
getCurrentClaims() {
|
|
1807
1947
|
if (!this._accessToken) return null;
|
|
1808
1948
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1815,9 +1955,104 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1815
1955
|
};
|
|
1816
1956
|
|
|
1817
1957
|
// src/mobile.ts
|
|
1958
|
+
function resolveAppState() {
|
|
1959
|
+
try {
|
|
1960
|
+
const rn = Function("return require")()("react-native");
|
|
1961
|
+
return rn?.AppState ?? null;
|
|
1962
|
+
} catch {
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1818
1966
|
var MobileIQAuthClient = class extends IQAuthClient {
|
|
1819
1967
|
constructor(config) {
|
|
1820
1968
|
super({ ...config, environment: "mobile" });
|
|
1969
|
+
this.appStateSub = null;
|
|
1970
|
+
this.lastAppState = "active";
|
|
1971
|
+
this.refreshing = false;
|
|
1972
|
+
this.appStateMode = config.autoRefresh === "app-state";
|
|
1973
|
+
this.leewaySeconds = config.appStateRefreshLeewaySeconds ?? 300;
|
|
1974
|
+
this.onTokenRefreshCb = config.onTokenRefresh;
|
|
1975
|
+
this.onAppStateRefreshError = config.onAppStateRefreshError;
|
|
1976
|
+
if (this.appStateMode) {
|
|
1977
|
+
const appState = config.appState !== void 0 ? config.appState : resolveAppState();
|
|
1978
|
+
if (appState) this.startAppStateListener(appState);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
/** True iff the client is configured for AppState-driven refresh. */
|
|
1982
|
+
get isAppStateMode() {
|
|
1983
|
+
return this.appStateMode;
|
|
1984
|
+
}
|
|
1985
|
+
startAppStateListener(appState) {
|
|
1986
|
+
this.lastAppState = appState.currentState ?? "active";
|
|
1987
|
+
const handler = (next) => {
|
|
1988
|
+
const prev = this.lastAppState;
|
|
1989
|
+
this.lastAppState = next;
|
|
1990
|
+
if (next === "active" && prev !== "active") {
|
|
1991
|
+
void this.maybeRefreshOnForeground();
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
const sub = appState.addEventListener("change", handler);
|
|
1995
|
+
if (sub && typeof sub.remove === "function") {
|
|
1996
|
+
this.appStateSub = sub;
|
|
1997
|
+
} else if (typeof sub === "function") {
|
|
1998
|
+
this.appStateSub = { remove: sub };
|
|
1999
|
+
} else if (typeof appState.removeEventListener === "function") {
|
|
2000
|
+
this.appStateSub = {
|
|
2001
|
+
remove: () => appState.removeEventListener("change", handler)
|
|
2002
|
+
};
|
|
2003
|
+
} else {
|
|
2004
|
+
this.appStateSub = { remove: () => {
|
|
2005
|
+
} };
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Public hook: call this from your own AppState handler if you've passed
|
|
2010
|
+
* `appState: null` to opt out of the auto-subscription. Returns true if a
|
|
2011
|
+
* refresh was attempted.
|
|
2012
|
+
*/
|
|
2013
|
+
async refreshIfStale() {
|
|
2014
|
+
return this.maybeRefreshOnForeground();
|
|
2015
|
+
}
|
|
2016
|
+
async maybeRefreshOnForeground() {
|
|
2017
|
+
if (this.refreshing) return false;
|
|
2018
|
+
if (!this.getRefreshToken()) return false;
|
|
2019
|
+
if (!this.isAccessTokenStale()) return false;
|
|
2020
|
+
const refreshToken = this.getRefreshToken();
|
|
2021
|
+
if (!refreshToken) return false;
|
|
2022
|
+
this.refreshing = true;
|
|
2023
|
+
try {
|
|
2024
|
+
const next = await this.auth.refreshTokens(refreshToken);
|
|
2025
|
+
this.setTokens(next);
|
|
2026
|
+
this.onTokenRefreshCb?.(next);
|
|
2027
|
+
return true;
|
|
2028
|
+
} catch (err) {
|
|
2029
|
+
this.onAppStateRefreshError?.(err);
|
|
2030
|
+
return false;
|
|
2031
|
+
} finally {
|
|
2032
|
+
this.refreshing = false;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
isAccessTokenStale() {
|
|
2036
|
+
const token = this.getAccessToken();
|
|
2037
|
+
if (!token) return true;
|
|
2038
|
+
try {
|
|
2039
|
+
const parts = token.split(".");
|
|
2040
|
+
if (parts.length !== 3) return true;
|
|
2041
|
+
const payload = JSON.parse(
|
|
2042
|
+
typeof atob === "function" ? atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")) : Buffer.from(parts[1], "base64url").toString("utf8")
|
|
2043
|
+
);
|
|
2044
|
+
if (!payload.exp) return false;
|
|
2045
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2046
|
+
return payload.exp - now < this.leewaySeconds;
|
|
2047
|
+
} catch {
|
|
2048
|
+
return true;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
/** Remove the AppState subscription. Idempotent. */
|
|
2052
|
+
stop() {
|
|
2053
|
+
const sub = this.appStateSub;
|
|
2054
|
+
this.appStateSub = null;
|
|
2055
|
+
sub?.remove();
|
|
1821
2056
|
}
|
|
1822
2057
|
};
|
|
1823
2058
|
function createMobileClient(config) {
|
package/dist/mobile.mjs
CHANGED
|
@@ -1,17 +1,112 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IQAuthClient
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-JXQI62A7.mjs";
|
|
4
|
+
import "./chunk-NUO2I65G.mjs";
|
|
5
5
|
import {
|
|
6
6
|
ErrorCodes,
|
|
7
7
|
IQAuthError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
9
9
|
import "./chunk-Y6FXYEAI.mjs";
|
|
10
10
|
|
|
11
11
|
// src/mobile.ts
|
|
12
|
+
function resolveAppState() {
|
|
13
|
+
try {
|
|
14
|
+
const rn = Function("return require")()("react-native");
|
|
15
|
+
return rn?.AppState ?? null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
12
20
|
var MobileIQAuthClient = class extends IQAuthClient {
|
|
13
21
|
constructor(config) {
|
|
14
22
|
super({ ...config, environment: "mobile" });
|
|
23
|
+
this.appStateSub = null;
|
|
24
|
+
this.lastAppState = "active";
|
|
25
|
+
this.refreshing = false;
|
|
26
|
+
this.appStateMode = config.autoRefresh === "app-state";
|
|
27
|
+
this.leewaySeconds = config.appStateRefreshLeewaySeconds ?? 300;
|
|
28
|
+
this.onTokenRefreshCb = config.onTokenRefresh;
|
|
29
|
+
this.onAppStateRefreshError = config.onAppStateRefreshError;
|
|
30
|
+
if (this.appStateMode) {
|
|
31
|
+
const appState = config.appState !== void 0 ? config.appState : resolveAppState();
|
|
32
|
+
if (appState) this.startAppStateListener(appState);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** True iff the client is configured for AppState-driven refresh. */
|
|
36
|
+
get isAppStateMode() {
|
|
37
|
+
return this.appStateMode;
|
|
38
|
+
}
|
|
39
|
+
startAppStateListener(appState) {
|
|
40
|
+
this.lastAppState = appState.currentState ?? "active";
|
|
41
|
+
const handler = (next) => {
|
|
42
|
+
const prev = this.lastAppState;
|
|
43
|
+
this.lastAppState = next;
|
|
44
|
+
if (next === "active" && prev !== "active") {
|
|
45
|
+
void this.maybeRefreshOnForeground();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const sub = appState.addEventListener("change", handler);
|
|
49
|
+
if (sub && typeof sub.remove === "function") {
|
|
50
|
+
this.appStateSub = sub;
|
|
51
|
+
} else if (typeof sub === "function") {
|
|
52
|
+
this.appStateSub = { remove: sub };
|
|
53
|
+
} else if (typeof appState.removeEventListener === "function") {
|
|
54
|
+
this.appStateSub = {
|
|
55
|
+
remove: () => appState.removeEventListener("change", handler)
|
|
56
|
+
};
|
|
57
|
+
} else {
|
|
58
|
+
this.appStateSub = { remove: () => {
|
|
59
|
+
} };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Public hook: call this from your own AppState handler if you've passed
|
|
64
|
+
* `appState: null` to opt out of the auto-subscription. Returns true if a
|
|
65
|
+
* refresh was attempted.
|
|
66
|
+
*/
|
|
67
|
+
async refreshIfStale() {
|
|
68
|
+
return this.maybeRefreshOnForeground();
|
|
69
|
+
}
|
|
70
|
+
async maybeRefreshOnForeground() {
|
|
71
|
+
if (this.refreshing) return false;
|
|
72
|
+
if (!this.getRefreshToken()) return false;
|
|
73
|
+
if (!this.isAccessTokenStale()) return false;
|
|
74
|
+
const refreshToken = this.getRefreshToken();
|
|
75
|
+
if (!refreshToken) return false;
|
|
76
|
+
this.refreshing = true;
|
|
77
|
+
try {
|
|
78
|
+
const next = await this.auth.refreshTokens(refreshToken);
|
|
79
|
+
this.setTokens(next);
|
|
80
|
+
this.onTokenRefreshCb?.(next);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this.onAppStateRefreshError?.(err);
|
|
84
|
+
return false;
|
|
85
|
+
} finally {
|
|
86
|
+
this.refreshing = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
isAccessTokenStale() {
|
|
90
|
+
const token = this.getAccessToken();
|
|
91
|
+
if (!token) return true;
|
|
92
|
+
try {
|
|
93
|
+
const parts = token.split(".");
|
|
94
|
+
if (parts.length !== 3) return true;
|
|
95
|
+
const payload = JSON.parse(
|
|
96
|
+
typeof atob === "function" ? atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")) : Buffer.from(parts[1], "base64url").toString("utf8")
|
|
97
|
+
);
|
|
98
|
+
if (!payload.exp) return false;
|
|
99
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
100
|
+
return payload.exp - now < this.leewaySeconds;
|
|
101
|
+
} catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Remove the AppState subscription. Idempotent. */
|
|
106
|
+
stop() {
|
|
107
|
+
const sub = this.appStateSub;
|
|
108
|
+
this.appStateSub = null;
|
|
109
|
+
sub?.remove();
|
|
15
110
|
}
|
|
16
111
|
};
|
|
17
112
|
function createMobileClient(config) {
|
package/dist/next.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IQAuthHelperConfig } from './server/handlers.mjs';
|
|
2
|
-
import { J as JwtClaims } from './types-
|
|
2
|
+
import { J as JwtClaims } from './types-XOV9XPVi.mjs';
|
|
3
|
+
import './tokens-CITeoG6P.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @iqauth/sdk/next — Next.js (App Router) adapter.
|
package/dist/next.d.ts
CHANGED