@iqauth/sdk 2.6.4 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -1
- package/dist/browser-session.d.mts +4 -4
- package/dist/browser-session.d.ts +4 -4
- package/dist/browser-session.js +212 -46
- package/dist/browser-session.mjs +3 -3
- package/dist/browser.d.mts +5 -5
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +293 -34
- package/dist/browser.mjs +5 -5
- package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
- package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
- package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/chunk-VYQ3ETCK.mjs +244 -0
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/chunk-WHT6WKTY.mjs +3180 -0
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/chunk-WSH4SW7F.mjs +490 -0
- package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
- package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
- package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
- package/dist/errors-Jl1Jtm-6.d.mts +107 -0
- package/dist/errors-Jl1Jtm-6.d.ts +107 -0
- package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +563 -85
- package/dist/express.mjs +73 -34
- package/dist/fastify.d.mts +10 -0
- package/dist/fastify.d.ts +10 -0
- package/dist/fastify.js +589 -65
- package/dist/fastify.mjs +101 -11
- package/dist/hono.d.mts +10 -0
- package/dist/hono.d.ts +10 -0
- package/dist/hono.js +566 -65
- package/dist/hono.mjs +78 -11
- package/dist/index-Cko-d5po.d.mts +1848 -0
- package/dist/index-RNqwEcmY.d.ts +1848 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +694 -75
- package/dist/index.mjs +30 -10
- package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/locales.js +36 -0
- package/dist/locales.mjs +1 -1
- package/dist/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +307 -46
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +10 -1
- package/dist/next.d.ts +10 -1
- package/dist/next.js +596 -205
- package/dist/next.mjs +83 -10
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
- package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
- package/dist/react-permissions.d.mts +52 -0
- package/dist/react-permissions.d.ts +52 -0
- package/dist/react-permissions.js +239 -0
- package/dist/react-permissions.mjs +98 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +882 -73
- package/dist/react.mjs +71 -2631
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +200 -4
- package/dist/server/handlers.d.ts +200 -4
- package/dist/server/handlers.js +530 -16
- package/dist/server/handlers.mjs +14 -3
- package/dist/server.d.mts +171 -8
- package/dist/server.d.ts +171 -8
- package/dist/server.js +579 -61
- package/dist/server.mjs +99 -12
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +212 -46
- package/dist/service.mjs +3 -3
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
- package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
- package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
- package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
- package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
- package/dist/webhooks.d.mts +113 -17
- package/dist/webhooks.d.ts +113 -17
- package/dist/webhooks.js +179 -15
- package/dist/webhooks.mjs +7 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/dist/ws.js +80 -30
- package/dist/ws.mjs +4 -4
- package/docs/error-handling.md +101 -0
- package/docs/guides/effective-permissions.md +171 -0
- package/docs/guides/invitations.md +65 -0
- package/package.json +19 -4
- package/dist/chunk-6TDJJER7.mjs +0 -217
- package/dist/chunk-UKZLOHZG.mjs +0 -83
- package/dist/errors-CDdl24MP.d.mts +0 -52
- package/dist/errors-CDdl24MP.d.ts +0 -52
package/dist/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}`;
|
|
@@ -318,17 +335,27 @@ function parseLoginResponse(data, browserSessionMode) {
|
|
|
318
335
|
tenants: data.tenants
|
|
319
336
|
};
|
|
320
337
|
}
|
|
338
|
+
if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
|
|
339
|
+
return {
|
|
340
|
+
status: "scope_selection",
|
|
341
|
+
scopeSelectionToken: data.scopeSelectionToken,
|
|
342
|
+
tenantId: data.tenantId,
|
|
343
|
+
scopes: data.scopes
|
|
344
|
+
};
|
|
345
|
+
}
|
|
321
346
|
throw new Error("Unexpected login response shape");
|
|
322
347
|
}
|
|
323
348
|
var AuthModule = class {
|
|
324
349
|
constructor(http) {
|
|
325
350
|
this.http = http;
|
|
326
351
|
}
|
|
327
|
-
async login(email, password) {
|
|
352
|
+
async login(email, password, opts) {
|
|
353
|
+
const body = { email, password };
|
|
354
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
328
355
|
const data = await this.http.request(
|
|
329
356
|
"POST",
|
|
330
357
|
"/api/v1/auth/login",
|
|
331
|
-
|
|
358
|
+
body,
|
|
332
359
|
{ skipAutoRefresh: true }
|
|
333
360
|
);
|
|
334
361
|
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
@@ -366,13 +393,29 @@ var AuthModule = class {
|
|
|
366
393
|
method
|
|
367
394
|
}, { skipAutoRefresh: true });
|
|
368
395
|
}
|
|
369
|
-
async selectTenant(tenantSelectionToken, tenantId) {
|
|
396
|
+
async selectTenant(tenantSelectionToken, tenantId, opts) {
|
|
397
|
+
const body = { tenantSelectionToken, tenantId };
|
|
398
|
+
if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
|
|
370
399
|
const data = await this.http.request(
|
|
371
400
|
"POST",
|
|
372
401
|
"/api/v1/auth/select-tenant",
|
|
402
|
+
body,
|
|
403
|
+
{ skipAutoRefresh: true }
|
|
404
|
+
);
|
|
405
|
+
return parseLoginResponse(data, this.http.isBrowserSession());
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Task #171 — redeem a scope-selection token + chosen membership for a
|
|
409
|
+
* real authenticated session. `membershipId` must be one of the scopes
|
|
410
|
+
* returned in the prior `scope_selection` envelope.
|
|
411
|
+
*/
|
|
412
|
+
async selectScope(scopeSelectionToken, membershipId) {
|
|
413
|
+
const data = await this.http.request(
|
|
414
|
+
"POST",
|
|
415
|
+
"/api/v1/auth/select-scope",
|
|
373
416
|
{
|
|
374
|
-
|
|
375
|
-
|
|
417
|
+
scopeSelectionToken,
|
|
418
|
+
membershipId
|
|
376
419
|
},
|
|
377
420
|
{ skipAutoRefresh: true }
|
|
378
421
|
);
|
|
@@ -459,6 +502,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
459
502
|
"iqvalidate"
|
|
460
503
|
];
|
|
461
504
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
505
|
+
function classifyJoseError(err) {
|
|
506
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
507
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
508
|
+
}
|
|
509
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
510
|
+
return { code: "token_invalid", message: err.message };
|
|
511
|
+
}
|
|
512
|
+
if (err instanceof Error) {
|
|
513
|
+
return { code: "token_invalid", message: err.message };
|
|
514
|
+
}
|
|
515
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
516
|
+
}
|
|
462
517
|
function decodeProtectedHeader(token) {
|
|
463
518
|
const parts = token.split(".");
|
|
464
519
|
if (parts.length < 2) return null;
|
|
@@ -495,11 +550,11 @@ var TokensModule = class {
|
|
|
495
550
|
async verify(token, options = {}) {
|
|
496
551
|
const header = decodeProtectedHeader(token);
|
|
497
552
|
if (!header) {
|
|
498
|
-
throw new IQAuthError("
|
|
553
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
499
554
|
}
|
|
500
555
|
const kid = header.kid;
|
|
501
556
|
if (!kid) {
|
|
502
|
-
throw new IQAuthError("
|
|
557
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
503
558
|
}
|
|
504
559
|
let cache = await this.ensureCache();
|
|
505
560
|
if (!cache.byKid.has(kid)) {
|
|
@@ -507,7 +562,7 @@ var TokensModule = class {
|
|
|
507
562
|
cache = await this.ensureCache();
|
|
508
563
|
}
|
|
509
564
|
if (!cache.byKid.has(kid)) {
|
|
510
|
-
throw new IQAuthError("
|
|
565
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
511
566
|
}
|
|
512
567
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
513
568
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -523,16 +578,8 @@ var TokensModule = class {
|
|
|
523
578
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
524
579
|
return payload;
|
|
525
580
|
} 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");
|
|
581
|
+
const classified = classifyJoseError(err);
|
|
582
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
536
583
|
}
|
|
537
584
|
}
|
|
538
585
|
/**
|
|
@@ -574,7 +621,7 @@ var TokensModule = class {
|
|
|
574
621
|
getClaims(token) {
|
|
575
622
|
const claims = this.decode(token);
|
|
576
623
|
if (!claims) {
|
|
577
|
-
throw new IQAuthError("
|
|
624
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
578
625
|
}
|
|
579
626
|
return claims;
|
|
580
627
|
}
|
|
@@ -584,7 +631,7 @@ var TokensModule = class {
|
|
|
584
631
|
}
|
|
585
632
|
await this.refreshJwks();
|
|
586
633
|
if (!this.jwksCache) {
|
|
587
|
-
throw new IQAuthError("
|
|
634
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
588
635
|
}
|
|
589
636
|
return this.jwksCache;
|
|
590
637
|
}
|
|
@@ -594,22 +641,38 @@ var TokensModule = class {
|
|
|
594
641
|
}
|
|
595
642
|
this.inFlightRefresh = (async () => {
|
|
596
643
|
try {
|
|
597
|
-
|
|
644
|
+
let res;
|
|
645
|
+
try {
|
|
646
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
throw new IQAuthError(
|
|
649
|
+
"network",
|
|
650
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
651
|
+
void 0,
|
|
652
|
+
err
|
|
653
|
+
);
|
|
654
|
+
}
|
|
598
655
|
if (!res.ok) {
|
|
599
656
|
throw new IQAuthError(
|
|
600
|
-
"
|
|
601
|
-
`Failed to fetch JWKS: ${res.status}
|
|
657
|
+
"jwks_fetch_failed",
|
|
658
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
659
|
+
res.status
|
|
602
660
|
);
|
|
603
661
|
}
|
|
604
662
|
let jwks;
|
|
605
663
|
try {
|
|
606
664
|
jwks = await res.json();
|
|
607
|
-
} catch {
|
|
608
|
-
throw new IQAuthError(
|
|
665
|
+
} catch (err) {
|
|
666
|
+
throw new IQAuthError(
|
|
667
|
+
"jwks_fetch_failed",
|
|
668
|
+
"Malformed JWKS response: invalid JSON",
|
|
669
|
+
res.status,
|
|
670
|
+
err
|
|
671
|
+
);
|
|
609
672
|
}
|
|
610
673
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
611
674
|
throw new IQAuthError(
|
|
612
|
-
"
|
|
675
|
+
"jwks_fetch_failed",
|
|
613
676
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
614
677
|
);
|
|
615
678
|
}
|
|
@@ -617,7 +680,7 @@ var TokensModule = class {
|
|
|
617
680
|
for (const key of jwks.keys) {
|
|
618
681
|
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
682
|
throw new IQAuthError(
|
|
620
|
-
"
|
|
683
|
+
"jwks_fetch_failed",
|
|
621
684
|
"Malformed JWKS response: key missing required fields"
|
|
622
685
|
);
|
|
623
686
|
}
|
|
@@ -635,6 +698,19 @@ var TokensModule = class {
|
|
|
635
698
|
clearCache() {
|
|
636
699
|
this.jwksCache = null;
|
|
637
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
703
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
704
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
705
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
706
|
+
*/
|
|
707
|
+
async prewarm() {
|
|
708
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
709
|
+
try {
|
|
710
|
+
await this.refreshJwks();
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
}
|
|
638
714
|
};
|
|
639
715
|
|
|
640
716
|
// src/modules/sessions.ts
|
|
@@ -958,14 +1034,14 @@ var OidcModule = class {
|
|
|
958
1034
|
*/
|
|
959
1035
|
async handleCallback(params) {
|
|
960
1036
|
if (!params.state) {
|
|
961
|
-
throw new IQAuthError("
|
|
1037
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
962
1038
|
}
|
|
963
1039
|
if (!params.code) {
|
|
964
|
-
throw new IQAuthError("
|
|
1040
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
965
1041
|
}
|
|
966
1042
|
const stored = await this.stateStore.get(params.state);
|
|
967
1043
|
if (!stored) {
|
|
968
|
-
throw new IQAuthError("
|
|
1044
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
969
1045
|
}
|
|
970
1046
|
let tokens;
|
|
971
1047
|
try {
|
|
@@ -983,7 +1059,7 @@ var OidcModule = class {
|
|
|
983
1059
|
if (tokens.id_token) {
|
|
984
1060
|
if (!this.tokensModule) {
|
|
985
1061
|
throw new IQAuthError(
|
|
986
|
-
"
|
|
1062
|
+
"config_invalid",
|
|
987
1063
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
988
1064
|
);
|
|
989
1065
|
}
|
|
@@ -994,7 +1070,7 @@ var OidcModule = class {
|
|
|
994
1070
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
995
1071
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
996
1072
|
throw new IQAuthError(
|
|
997
|
-
"
|
|
1073
|
+
"token_invalid",
|
|
998
1074
|
"OIDC id_token nonce did not match the stored value"
|
|
999
1075
|
);
|
|
1000
1076
|
}
|
|
@@ -1195,6 +1271,9 @@ var AppsModule = class {
|
|
|
1195
1271
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1196
1272
|
*/
|
|
1197
1273
|
async get(appKey) {
|
|
1274
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1275
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1276
|
+
}
|
|
1198
1277
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1199
1278
|
}
|
|
1200
1279
|
/**
|
|
@@ -1214,6 +1293,16 @@ var AppsModule = class {
|
|
|
1214
1293
|
401
|
|
1215
1294
|
);
|
|
1216
1295
|
}
|
|
1296
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1297
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1298
|
+
}
|
|
1299
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1300
|
+
throw new IQAuthError(
|
|
1301
|
+
"ENVIRONMENT_REQUIRED",
|
|
1302
|
+
"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.",
|
|
1303
|
+
400
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1217
1306
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1218
1307
|
}
|
|
1219
1308
|
/**
|
|
@@ -1223,11 +1312,14 @@ var AppsModule = class {
|
|
|
1223
1312
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1224
1313
|
*/
|
|
1225
1314
|
async isRegistered(appKey) {
|
|
1315
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1316
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1317
|
+
}
|
|
1226
1318
|
try {
|
|
1227
1319
|
await this.get(appKey);
|
|
1228
1320
|
return true;
|
|
1229
1321
|
} catch (err) {
|
|
1230
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1322
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1231
1323
|
return false;
|
|
1232
1324
|
}
|
|
1233
1325
|
throw err;
|
|
@@ -1264,6 +1356,20 @@ var RolesModule = class {
|
|
|
1264
1356
|
};
|
|
1265
1357
|
|
|
1266
1358
|
// src/modules/permissionGroups.ts
|
|
1359
|
+
function assertAppKey(appKey, callsite) {
|
|
1360
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1361
|
+
throw new IQAuthError(
|
|
1362
|
+
"VALIDATION_ERROR",
|
|
1363
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1364
|
+
400
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1369
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1370
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1267
1373
|
var PermissionGroupsModule = class {
|
|
1268
1374
|
constructor(http) {
|
|
1269
1375
|
this.http = http;
|
|
@@ -1284,7 +1390,14 @@ var PermissionGroupsModule = class {
|
|
|
1284
1390
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1285
1391
|
}
|
|
1286
1392
|
async addPermission(tenantId, groupId, data) {
|
|
1287
|
-
|
|
1393
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1394
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1395
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1396
|
+
appKey: data.appKey,
|
|
1397
|
+
nodeKey: data.nodeKey,
|
|
1398
|
+
effect: data.effect,
|
|
1399
|
+
weight: data.weight
|
|
1400
|
+
});
|
|
1288
1401
|
}
|
|
1289
1402
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1290
1403
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1308,21 +1421,51 @@ var PermissionGroupsModule = class {
|
|
|
1308
1421
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1309
1422
|
}
|
|
1310
1423
|
async addUserOverride(tenantId, userId, data) {
|
|
1311
|
-
|
|
1424
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1425
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1426
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1427
|
+
appKey: data.appKey,
|
|
1428
|
+
nodeKey: data.nodeKey,
|
|
1429
|
+
effect: data.effect,
|
|
1430
|
+
weight: data.weight,
|
|
1431
|
+
expiresAt: data.expiresAt
|
|
1432
|
+
});
|
|
1312
1433
|
}
|
|
1313
1434
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1314
1435
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1315
1436
|
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1439
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1440
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1441
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1442
|
+
*/
|
|
1316
1443
|
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}` : ""}`);
|
|
1444
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1445
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1446
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1322
1447
|
}
|
|
1323
1448
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1449
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1450
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1324
1451
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1325
1452
|
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1455
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1456
|
+
* single misconfigured entry can't slip through and silently report
|
|
1457
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1458
|
+
*/
|
|
1459
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1460
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1461
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1462
|
+
}
|
|
1463
|
+
checks.forEach((c, i) => {
|
|
1464
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1465
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1466
|
+
});
|
|
1467
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1468
|
+
}
|
|
1326
1469
|
};
|
|
1327
1470
|
|
|
1328
1471
|
// src/modules/apiKeys.ts
|
|
@@ -1747,6 +1890,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1747
1890
|
this._refreshToken = tokens.refreshToken;
|
|
1748
1891
|
},
|
|
1749
1892
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1893
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1894
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1895
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1896
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1750
1897
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1751
1898
|
sessionHeaderName: config.sessionHeaderName,
|
|
1752
1899
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1787,6 +1934,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1787
1934
|
static forServer(config) {
|
|
1788
1935
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1789
1936
|
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1939
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1940
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1941
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1942
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1943
|
+
*/
|
|
1790
1944
|
static forMobile(config) {
|
|
1791
1945
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1792
1946
|
}
|
|
@@ -1803,6 +1957,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1803
1957
|
getRefreshToken() {
|
|
1804
1958
|
return this._refreshToken;
|
|
1805
1959
|
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1962
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1963
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1964
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1965
|
+
*/
|
|
1966
|
+
async prewarm() {
|
|
1967
|
+
await Promise.all([
|
|
1968
|
+
this.tokens.prewarm(),
|
|
1969
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1970
|
+
]);
|
|
1971
|
+
}
|
|
1806
1972
|
getCurrentClaims() {
|
|
1807
1973
|
if (!this._accessToken) return null;
|
|
1808
1974
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1815,9 +1981,104 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1815
1981
|
};
|
|
1816
1982
|
|
|
1817
1983
|
// src/mobile.ts
|
|
1984
|
+
function resolveAppState() {
|
|
1985
|
+
try {
|
|
1986
|
+
const rn = Function("return require")()("react-native");
|
|
1987
|
+
return rn?.AppState ?? null;
|
|
1988
|
+
} catch {
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1818
1992
|
var MobileIQAuthClient = class extends IQAuthClient {
|
|
1819
1993
|
constructor(config) {
|
|
1820
1994
|
super({ ...config, environment: "mobile" });
|
|
1995
|
+
this.appStateSub = null;
|
|
1996
|
+
this.lastAppState = "active";
|
|
1997
|
+
this.refreshing = false;
|
|
1998
|
+
this.appStateMode = config.autoRefresh === "app-state";
|
|
1999
|
+
this.leewaySeconds = config.appStateRefreshLeewaySeconds ?? 300;
|
|
2000
|
+
this.onTokenRefreshCb = config.onTokenRefresh;
|
|
2001
|
+
this.onAppStateRefreshError = config.onAppStateRefreshError;
|
|
2002
|
+
if (this.appStateMode) {
|
|
2003
|
+
const appState = config.appState !== void 0 ? config.appState : resolveAppState();
|
|
2004
|
+
if (appState) this.startAppStateListener(appState);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
/** True iff the client is configured for AppState-driven refresh. */
|
|
2008
|
+
get isAppStateMode() {
|
|
2009
|
+
return this.appStateMode;
|
|
2010
|
+
}
|
|
2011
|
+
startAppStateListener(appState) {
|
|
2012
|
+
this.lastAppState = appState.currentState ?? "active";
|
|
2013
|
+
const handler = (next) => {
|
|
2014
|
+
const prev = this.lastAppState;
|
|
2015
|
+
this.lastAppState = next;
|
|
2016
|
+
if (next === "active" && prev !== "active") {
|
|
2017
|
+
void this.maybeRefreshOnForeground();
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
const sub = appState.addEventListener("change", handler);
|
|
2021
|
+
if (sub && typeof sub.remove === "function") {
|
|
2022
|
+
this.appStateSub = sub;
|
|
2023
|
+
} else if (typeof sub === "function") {
|
|
2024
|
+
this.appStateSub = { remove: sub };
|
|
2025
|
+
} else if (typeof appState.removeEventListener === "function") {
|
|
2026
|
+
this.appStateSub = {
|
|
2027
|
+
remove: () => appState.removeEventListener("change", handler)
|
|
2028
|
+
};
|
|
2029
|
+
} else {
|
|
2030
|
+
this.appStateSub = { remove: () => {
|
|
2031
|
+
} };
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Public hook: call this from your own AppState handler if you've passed
|
|
2036
|
+
* `appState: null` to opt out of the auto-subscription. Returns true if a
|
|
2037
|
+
* refresh was attempted.
|
|
2038
|
+
*/
|
|
2039
|
+
async refreshIfStale() {
|
|
2040
|
+
return this.maybeRefreshOnForeground();
|
|
2041
|
+
}
|
|
2042
|
+
async maybeRefreshOnForeground() {
|
|
2043
|
+
if (this.refreshing) return false;
|
|
2044
|
+
if (!this.getRefreshToken()) return false;
|
|
2045
|
+
if (!this.isAccessTokenStale()) return false;
|
|
2046
|
+
const refreshToken = this.getRefreshToken();
|
|
2047
|
+
if (!refreshToken) return false;
|
|
2048
|
+
this.refreshing = true;
|
|
2049
|
+
try {
|
|
2050
|
+
const next = await this.auth.refreshTokens(refreshToken);
|
|
2051
|
+
this.setTokens(next);
|
|
2052
|
+
this.onTokenRefreshCb?.(next);
|
|
2053
|
+
return true;
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
this.onAppStateRefreshError?.(err);
|
|
2056
|
+
return false;
|
|
2057
|
+
} finally {
|
|
2058
|
+
this.refreshing = false;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
isAccessTokenStale() {
|
|
2062
|
+
const token = this.getAccessToken();
|
|
2063
|
+
if (!token) return true;
|
|
2064
|
+
try {
|
|
2065
|
+
const parts = token.split(".");
|
|
2066
|
+
if (parts.length !== 3) return true;
|
|
2067
|
+
const payload = JSON.parse(
|
|
2068
|
+
typeof atob === "function" ? atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")) : Buffer.from(parts[1], "base64url").toString("utf8")
|
|
2069
|
+
);
|
|
2070
|
+
if (!payload.exp) return false;
|
|
2071
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2072
|
+
return payload.exp - now < this.leewaySeconds;
|
|
2073
|
+
} catch {
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
/** Remove the AppState subscription. Idempotent. */
|
|
2078
|
+
stop() {
|
|
2079
|
+
const sub = this.appStateSub;
|
|
2080
|
+
this.appStateSub = null;
|
|
2081
|
+
sub?.remove();
|
|
1821
2082
|
}
|
|
1822
2083
|
};
|
|
1823
2084
|
function createMobileClient(config) {
|