@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/browser.js
CHANGED
|
@@ -444,13 +444,30 @@ __export(browser_exports, {
|
|
|
444
444
|
module.exports = __toCommonJS(browser_exports);
|
|
445
445
|
|
|
446
446
|
// src/errors.ts
|
|
447
|
-
var IQAuthError = class extends Error {
|
|
448
|
-
constructor(code, message, status,
|
|
447
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
448
|
+
constructor(code, message, status, cause) {
|
|
449
449
|
super(message);
|
|
450
450
|
this.name = "IQAuthError";
|
|
451
451
|
this.code = code;
|
|
452
452
|
this.status = status;
|
|
453
|
-
this.
|
|
453
|
+
this.cause = cause;
|
|
454
|
+
this.raw = cause;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
458
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
459
|
+
*/
|
|
460
|
+
static isIQAuthError(value) {
|
|
461
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Type-narrowed code check. Lets callers write
|
|
465
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
466
|
+
* taxonomy without losing the ability to handle server codes via
|
|
467
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
468
|
+
*/
|
|
469
|
+
is(code) {
|
|
470
|
+
return this.code === code;
|
|
454
471
|
}
|
|
455
472
|
};
|
|
456
473
|
var ErrorCodes = {
|
|
@@ -549,14 +566,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
549
566
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
550
567
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
551
568
|
throw new IQAuthError(
|
|
552
|
-
"
|
|
569
|
+
"config_invalid",
|
|
553
570
|
`${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.`
|
|
554
571
|
);
|
|
555
572
|
}
|
|
556
573
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
557
574
|
if (!shapeMatch) {
|
|
558
575
|
throw new IQAuthError(
|
|
559
|
-
"
|
|
576
|
+
"config_invalid",
|
|
560
577
|
`${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.`
|
|
561
578
|
);
|
|
562
579
|
}
|
|
@@ -565,19 +582,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
565
582
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
566
583
|
} catch {
|
|
567
584
|
throw new IQAuthError(
|
|
568
|
-
"
|
|
585
|
+
"config_invalid",
|
|
569
586
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
570
587
|
);
|
|
571
588
|
}
|
|
572
589
|
if (!isPublishableKeyPayload(decoded)) {
|
|
573
590
|
throw new IQAuthError(
|
|
574
|
-
"
|
|
591
|
+
"config_invalid",
|
|
575
592
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
576
593
|
);
|
|
577
594
|
}
|
|
578
595
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
579
596
|
throw new IQAuthError(
|
|
580
|
-
"
|
|
597
|
+
"config_invalid",
|
|
581
598
|
`${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.`
|
|
582
599
|
);
|
|
583
600
|
}
|
|
@@ -656,6 +673,7 @@ function clearPkce(state) {
|
|
|
656
673
|
}
|
|
657
674
|
|
|
658
675
|
// src/browser/sessionManager.ts
|
|
676
|
+
var PROBE_WAIT_MS = 80;
|
|
659
677
|
var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
|
|
660
678
|
var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
|
|
661
679
|
async function readAuthErrorCode(res) {
|
|
@@ -693,7 +711,16 @@ function claimsToSessionUser(claims) {
|
|
|
693
711
|
tenantId: claims.tenantId,
|
|
694
712
|
vendorId: claims.vendorId,
|
|
695
713
|
roles: claims.roles ?? [],
|
|
696
|
-
entitlements: claims.entitlements ?? []
|
|
714
|
+
entitlements: claims.entitlements ?? [],
|
|
715
|
+
// SDK 2.7.0 (Task #124) — pass through identity claims when issued.
|
|
716
|
+
...claims.picture !== void 0 ? { picture: claims.picture } : {},
|
|
717
|
+
...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
|
|
718
|
+
...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
|
|
719
|
+
...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
|
|
720
|
+
...claims.locale !== void 0 ? { locale: claims.locale } : {},
|
|
721
|
+
// Task #171 — surface the active source/client scope when the token was
|
|
722
|
+
// minted scoped, so consumers reading useUser().user can branch on it.
|
|
723
|
+
...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
|
|
697
724
|
};
|
|
698
725
|
}
|
|
699
726
|
var EMPTY = {
|
|
@@ -717,11 +744,50 @@ var NO_OP_STORE = {
|
|
|
717
744
|
write: () => void 0,
|
|
718
745
|
clear: () => void 0
|
|
719
746
|
};
|
|
747
|
+
var IDEMPOTENCY_HEADER = "X-IQAuth-Idempotency";
|
|
748
|
+
function randomIdempotencyToken() {
|
|
749
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
750
|
+
const bytes = new Uint8Array(16);
|
|
751
|
+
crypto.getRandomValues(bytes);
|
|
752
|
+
let out = "";
|
|
753
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
757
|
+
}
|
|
720
758
|
var SessionManager = class {
|
|
721
759
|
constructor(options) {
|
|
722
760
|
this.snapshot = { ...EMPTY };
|
|
723
761
|
this.listeners = /* @__PURE__ */ new Set();
|
|
724
762
|
this.refreshPromise = null;
|
|
763
|
+
/**
|
|
764
|
+
* Cancellation handle for the in-flight refresh, if any. `signOut()` (or a
|
|
765
|
+
* `session:signout` broadcast from another tab) calls `abort()` so the
|
|
766
|
+
* refresh response is dropped before it can write a fresh access cookie
|
|
767
|
+
* on top of the just-cleared session — the second root cause of "ghost
|
|
768
|
+
* signed-in" sessions after Sign Out.
|
|
769
|
+
*/
|
|
770
|
+
this.refreshAbort = null;
|
|
771
|
+
/**
|
|
772
|
+
* Set to `true` by `signOut()` / `signOutLocal()` for the lifetime of the
|
|
773
|
+
* call. Used as a safety belt: even if a refresh response arrives while
|
|
774
|
+
* `refreshAbort` was unable to interrupt the network call (e.g. the body
|
|
775
|
+
* was already streaming back), `runRefresh` checks this flag before
|
|
776
|
+
* mutating session state and bails out.
|
|
777
|
+
*/
|
|
778
|
+
this.signoutInProgress = false;
|
|
779
|
+
/**
|
|
780
|
+
* Per-session opaque idempotency token. Sent as `X-IQAuth-Idempotency` on
|
|
781
|
+
* every /refresh and /signout request the SDK makes through a framework
|
|
782
|
+
* adapter (Express/Fastify/Hono/Next), so the adapter's `SignoutRegistry`
|
|
783
|
+
* can collapse a refresh that lands moments after a signout — even when
|
|
784
|
+
* the two requests are routed to different server instances (multi-replica
|
|
785
|
+
* deployments).
|
|
786
|
+
*
|
|
787
|
+
* Generated lazily on first use, rotated on signout so the next session
|
|
788
|
+
* starts with a fresh token. Opaque random — never the raw refresh token.
|
|
789
|
+
*/
|
|
790
|
+
this.idempotencyToken = null;
|
|
725
791
|
this.channel = null;
|
|
726
792
|
this.proactiveTimer = null;
|
|
727
793
|
this.bootstrapped = false;
|
|
@@ -729,6 +795,8 @@ var SessionManager = class {
|
|
|
729
795
|
this.remoteRefreshWaiters = [];
|
|
730
796
|
/** Active claims by other tabs (keyed by source tabId). */
|
|
731
797
|
this.foreignClaim = null;
|
|
798
|
+
/** Resolver for an in-flight cross-tab `session:probe`, set during bootstrap. */
|
|
799
|
+
this.probeResolver = null;
|
|
732
800
|
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
|
|
733
801
|
this.key = parsed;
|
|
734
802
|
const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
|
|
@@ -741,6 +809,8 @@ var SessionManager = class {
|
|
|
741
809
|
this.refreshCookieName = options.cookieNames?.refresh ?? REFRESH_COOKIE;
|
|
742
810
|
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore(this.refreshCookieName) : NO_OP_STORE);
|
|
743
811
|
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
812
|
+
this.debug = options.debug ?? false;
|
|
813
|
+
this.onTimingEvent = options.onTimingEvent ?? null;
|
|
744
814
|
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
745
815
|
throw new Error("global fetch is not available; pass fetchImpl");
|
|
746
816
|
}));
|
|
@@ -767,10 +837,35 @@ var SessionManager = class {
|
|
|
767
837
|
get issuerUrl() {
|
|
768
838
|
return this.issuer;
|
|
769
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* SDK 2.7.0 (Task #124) — The hosted IQAuth host derived from the
|
|
842
|
+
* publishable key's `iss` claim, normalized to URL form. This is what
|
|
843
|
+
* `<SignIn/>` and `buildSignInUrl` use to talk to the hosted UI; it
|
|
844
|
+
* deliberately ignores the `issuer` constructor override so a misrouted
|
|
845
|
+
* `issuer` (e.g. pointed at the consumer app's own domain) cannot break
|
|
846
|
+
* the hosted flow. Use {@link issuerUrl} for token / discovery endpoints.
|
|
847
|
+
*/
|
|
848
|
+
get hostedIssuerUrl() {
|
|
849
|
+
const iss = this.key.iss;
|
|
850
|
+
return (iss.startsWith("http") ? iss : `https://${iss}`).replace(/\/+$/, "");
|
|
851
|
+
}
|
|
770
852
|
/** Cookie name the SDK uses for the refresh token (overridable via `cookieNames.refresh`). */
|
|
771
853
|
get refreshCookie() {
|
|
772
854
|
return this.refreshCookieName;
|
|
773
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Returns the current per-session idempotency token, generating one
|
|
858
|
+
* lazily on first use. Sent as the `X-IQAuth-Idempotency` header on
|
|
859
|
+
* /refresh and /signout requests so the framework adapter's
|
|
860
|
+
* `SignoutRegistry` can collapse a refresh-vs-signout race even across
|
|
861
|
+
* server instances.
|
|
862
|
+
*/
|
|
863
|
+
getIdempotencyToken() {
|
|
864
|
+
if (!this.idempotencyToken) {
|
|
865
|
+
this.idempotencyToken = randomIdempotencyToken();
|
|
866
|
+
}
|
|
867
|
+
return this.idempotencyToken;
|
|
868
|
+
}
|
|
774
869
|
getSnapshot() {
|
|
775
870
|
return this.snapshot;
|
|
776
871
|
}
|
|
@@ -784,9 +879,44 @@ var SessionManager = class {
|
|
|
784
879
|
* One-time bootstrap: warm the session from the refresh cookie if present.
|
|
785
880
|
* Safe to call multiple times.
|
|
786
881
|
*/
|
|
882
|
+
/**
|
|
883
|
+
* Task #126: Public timing-event emitter. Used by the browser sign-in
|
|
884
|
+
* helpers (redirectToSignIn / handleAuthCallback) to surface signIn-phase
|
|
885
|
+
* timings through the same `debug` + `onTimingEvent` channel as
|
|
886
|
+
* bootstrap/refresh. Safe to call from anywhere — internal callers
|
|
887
|
+
* pre-compute durationMs.
|
|
888
|
+
*/
|
|
889
|
+
recordTiming(phase, durationMs, ok, code) {
|
|
890
|
+
this.emitTiming(phase, durationMs, ok, code);
|
|
891
|
+
}
|
|
892
|
+
/** Task #126: emit a session timing event to debug log + onTimingEvent hook. */
|
|
893
|
+
emitTiming(phase, durationMs, ok, code) {
|
|
894
|
+
if (this.debug) {
|
|
895
|
+
try {
|
|
896
|
+
console.debug("[iqauth_session]", { phase, durationMs, ok, code });
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (this.onTimingEvent) {
|
|
901
|
+
try {
|
|
902
|
+
this.onTimingEvent({ phase, durationMs, ok, code });
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
787
907
|
async bootstrap() {
|
|
788
908
|
if (this.bootstrapped) return;
|
|
789
909
|
this.bootstrapped = true;
|
|
910
|
+
const t0 = Date.now();
|
|
911
|
+
try {
|
|
912
|
+
await this.bootstrapInner();
|
|
913
|
+
this.emitTiming("bootstrap", Date.now() - t0, this.snapshot.status === "authenticated");
|
|
914
|
+
} catch (err) {
|
|
915
|
+
this.emitTiming("bootstrap", Date.now() - t0, false, err instanceof Error ? err.message : "ERROR");
|
|
916
|
+
throw err;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
async bootstrapInner() {
|
|
790
920
|
if (this.serverManagedSession) {
|
|
791
921
|
try {
|
|
792
922
|
const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
|
|
@@ -821,6 +951,15 @@ var SessionManager = class {
|
|
|
821
951
|
return;
|
|
822
952
|
}
|
|
823
953
|
}
|
|
954
|
+
const peerSnapshot = await this.probePeers();
|
|
955
|
+
if (peerSnapshot && peerSnapshot.status === "authenticated") {
|
|
956
|
+
this.update({
|
|
957
|
+
...peerSnapshot,
|
|
958
|
+
version: Math.max(this.snapshot.version, peerSnapshot.version) + 1
|
|
959
|
+
});
|
|
960
|
+
this.scheduleProactiveRefresh();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
824
963
|
const stored = await Promise.resolve(this.tokenStore.read());
|
|
825
964
|
if (!stored) {
|
|
826
965
|
this.setStatus("unauthenticated");
|
|
@@ -829,6 +968,22 @@ var SessionManager = class {
|
|
|
829
968
|
const ok = await this.refresh();
|
|
830
969
|
if (!ok) this.setStatus("unauthenticated");
|
|
831
970
|
}
|
|
971
|
+
probePeers() {
|
|
972
|
+
if (!this.channel) return Promise.resolve(null);
|
|
973
|
+
return new Promise((resolve) => {
|
|
974
|
+
let settled = false;
|
|
975
|
+
const finish = (snap) => {
|
|
976
|
+
if (settled) return;
|
|
977
|
+
settled = true;
|
|
978
|
+
this.probeResolver = null;
|
|
979
|
+
clearTimeout(timer);
|
|
980
|
+
resolve(snap);
|
|
981
|
+
};
|
|
982
|
+
this.probeResolver = (snap) => finish(snap);
|
|
983
|
+
const timer = setTimeout(() => finish(null), PROBE_WAIT_MS);
|
|
984
|
+
this.broadcastEnvelope({ type: "session:probe", source: this.tabId, ts: Date.now() });
|
|
985
|
+
});
|
|
986
|
+
}
|
|
832
987
|
/**
|
|
833
988
|
* Single-flight token refresh, coordinated across tabs via BroadcastChannel.
|
|
834
989
|
*
|
|
@@ -842,30 +997,48 @@ var SessionManager = class {
|
|
|
842
997
|
*/
|
|
843
998
|
refresh() {
|
|
844
999
|
if (this.refreshPromise) return this.refreshPromise;
|
|
845
|
-
|
|
1000
|
+
const t0 = Date.now();
|
|
1001
|
+
this.refreshPromise = this.runRefresh().then((ok) => {
|
|
1002
|
+
this.emitTiming("refresh", Date.now() - t0, ok, ok ? void 0 : this.snapshot.error?.code ?? "REFRESH_FAILED");
|
|
1003
|
+
return ok;
|
|
1004
|
+
}).finally(() => {
|
|
846
1005
|
this.refreshPromise = null;
|
|
847
1006
|
});
|
|
848
1007
|
return this.refreshPromise;
|
|
849
1008
|
}
|
|
850
1009
|
async runRefresh() {
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
854
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
855
|
-
const foreign = this.foreignClaim;
|
|
856
|
-
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
857
|
-
return this.waitForForeignRefresh();
|
|
858
|
-
}
|
|
859
|
-
}
|
|
1010
|
+
const abort = new AbortController();
|
|
1011
|
+
this.refreshAbort = abort;
|
|
860
1012
|
try {
|
|
1013
|
+
const myClaim = { source: this.tabId, ts: Date.now() };
|
|
1014
|
+
if (this.channel) {
|
|
1015
|
+
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
1016
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
1017
|
+
if (abort.signal.aborted || this.signoutInProgress) {
|
|
1018
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
const foreign = this.foreignClaim;
|
|
1022
|
+
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
1023
|
+
return this.waitForForeignRefresh();
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
861
1026
|
const refreshToken = await Promise.resolve(this.tokenStore.read());
|
|
862
1027
|
const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
|
|
863
1028
|
method: "POST",
|
|
864
1029
|
credentials: "include",
|
|
865
|
-
headers: {
|
|
866
|
-
|
|
1030
|
+
headers: {
|
|
1031
|
+
"Content-Type": "application/json",
|
|
1032
|
+
[IDEMPOTENCY_HEADER]: this.getIdempotencyToken()
|
|
1033
|
+
},
|
|
1034
|
+
body: JSON.stringify(refreshToken ? { refreshToken } : {}),
|
|
1035
|
+
signal: abort.signal
|
|
867
1036
|
});
|
|
868
1037
|
const body = await res.json().catch(() => ({}));
|
|
1038
|
+
if (this.signoutInProgress || abort.signal.aborted) {
|
|
1039
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
869
1042
|
const data = body.data;
|
|
870
1043
|
if (!res.ok || !body.success || !data?.accessToken) {
|
|
871
1044
|
const err = body.error;
|
|
@@ -884,14 +1057,18 @@ var SessionManager = class {
|
|
|
884
1057
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
|
|
885
1058
|
return true;
|
|
886
1059
|
} catch (err) {
|
|
887
|
-
this.
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1060
|
+
const aborted = err?.name === "AbortError" || abort.signal.aborted || this.signoutInProgress;
|
|
1061
|
+
if (!aborted) {
|
|
1062
|
+
this.setError({
|
|
1063
|
+
code: "NETWORK_ERROR",
|
|
1064
|
+
message: err instanceof Error ? err.message : "Refresh request failed"
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
891
1067
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
892
1068
|
return false;
|
|
893
1069
|
} finally {
|
|
894
1070
|
this.foreignClaim = null;
|
|
1071
|
+
if (this.refreshAbort === abort) this.refreshAbort = null;
|
|
895
1072
|
}
|
|
896
1073
|
}
|
|
897
1074
|
claimWins(foreign, mine) {
|
|
@@ -918,10 +1095,27 @@ var SessionManager = class {
|
|
|
918
1095
|
* session and notify subscribers and other tabs.
|
|
919
1096
|
*/
|
|
920
1097
|
applyAccessToken(accessToken, refreshToken) {
|
|
1098
|
+
this.adoptAccessToken(accessToken, { refreshToken });
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Task #197 — Adopt an access token that the server has already minted
|
|
1102
|
+
* for us (e.g. from `POST /api/v1/auth/switch-scope`) without contacting
|
|
1103
|
+
* the issuer. Swaps the in-memory token, re-decodes claims, bumps
|
|
1104
|
+
* `version`, schedules proactive refresh, and broadcasts a
|
|
1105
|
+
* `session:update` to peer tabs.
|
|
1106
|
+
*
|
|
1107
|
+
* This is the safe path for any server endpoint that returns a fresh
|
|
1108
|
+
* access token in its JSON body: we want the new claims (scope, roles,
|
|
1109
|
+
* etc.) to take effect immediately, even if the refresh-cookie round-trip
|
|
1110
|
+
* would have failed (network blip, rate limit, signout race). When the
|
|
1111
|
+
* server also rotated the refresh token, pass it via
|
|
1112
|
+
* `opts.refreshToken` so the cookie stays aligned.
|
|
1113
|
+
*/
|
|
1114
|
+
adoptAccessToken(accessToken, opts) {
|
|
921
1115
|
const claims = decodeClaims(accessToken);
|
|
922
1116
|
const user = claimsToSessionUser(claims);
|
|
923
|
-
if (refreshToken) {
|
|
924
|
-
void Promise.resolve(this.tokenStore.write(refreshToken, { claims }));
|
|
1117
|
+
if (opts?.refreshToken) {
|
|
1118
|
+
void Promise.resolve(this.tokenStore.write(opts.refreshToken, { claims }));
|
|
925
1119
|
}
|
|
926
1120
|
this.update({
|
|
927
1121
|
status: user ? "authenticated" : "unauthenticated",
|
|
@@ -999,6 +1193,14 @@ var SessionManager = class {
|
|
|
999
1193
|
* the server-side logout request.
|
|
1000
1194
|
*/
|
|
1001
1195
|
signOutLocal(status = "unauthenticated") {
|
|
1196
|
+
this.signoutInProgress = true;
|
|
1197
|
+
if (this.refreshAbort) {
|
|
1198
|
+
try {
|
|
1199
|
+
this.refreshAbort.abort();
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
this.refreshAbort = null;
|
|
1203
|
+
}
|
|
1002
1204
|
void Promise.resolve(this.tokenStore.clear());
|
|
1003
1205
|
if (this.proactiveTimer) {
|
|
1004
1206
|
clearTimeout(this.proactiveTimer);
|
|
@@ -1013,7 +1215,12 @@ var SessionManager = class {
|
|
|
1013
1215
|
error: null,
|
|
1014
1216
|
version: this.snapshot.version + 1
|
|
1015
1217
|
});
|
|
1218
|
+
this.broadcastEnvelope({ type: "refresh:abort", source: this.tabId, ts: Date.now() });
|
|
1016
1219
|
this.broadcast("session:signout");
|
|
1220
|
+
this.idempotencyToken = null;
|
|
1221
|
+
setTimeout(() => {
|
|
1222
|
+
this.signoutInProgress = false;
|
|
1223
|
+
}, 0);
|
|
1017
1224
|
}
|
|
1018
1225
|
/**
|
|
1019
1226
|
* Replace the refresh-token store at runtime. Used by the F22
|
|
@@ -1077,6 +1284,12 @@ var SessionManager = class {
|
|
|
1077
1284
|
}
|
|
1078
1285
|
onBroadcast(env) {
|
|
1079
1286
|
if (!env || env.source === this.tabId) return;
|
|
1287
|
+
if (env.type === "session:probe") {
|
|
1288
|
+
if (this.snapshot.status === "authenticated") {
|
|
1289
|
+
this.broadcast("session:update");
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1080
1293
|
if (env.type === "refresh:claim") {
|
|
1081
1294
|
this.foreignClaim = { source: env.source, ts: env.ts };
|
|
1082
1295
|
return;
|
|
@@ -1089,6 +1302,24 @@ var SessionManager = class {
|
|
|
1089
1302
|
this.foreignClaim = null;
|
|
1090
1303
|
return;
|
|
1091
1304
|
}
|
|
1305
|
+
if (env.type === "refresh:abort") {
|
|
1306
|
+
this.signoutInProgress = true;
|
|
1307
|
+
if (this.refreshAbort) {
|
|
1308
|
+
try {
|
|
1309
|
+
this.refreshAbort.abort();
|
|
1310
|
+
} catch {
|
|
1311
|
+
}
|
|
1312
|
+
this.refreshAbort = null;
|
|
1313
|
+
}
|
|
1314
|
+
const waiters = this.remoteRefreshWaiters;
|
|
1315
|
+
this.remoteRefreshWaiters = [];
|
|
1316
|
+
for (const w of waiters) w(false);
|
|
1317
|
+
this.foreignClaim = null;
|
|
1318
|
+
setTimeout(() => {
|
|
1319
|
+
this.signoutInProgress = false;
|
|
1320
|
+
}, 0);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1092
1323
|
if (env.type === "session:signout") {
|
|
1093
1324
|
this.update({
|
|
1094
1325
|
status: "unauthenticated",
|
|
@@ -1102,6 +1333,12 @@ var SessionManager = class {
|
|
|
1102
1333
|
return;
|
|
1103
1334
|
}
|
|
1104
1335
|
if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
|
|
1336
|
+
if (this.probeResolver && env.payload.status === "authenticated") {
|
|
1337
|
+
const r = this.probeResolver;
|
|
1338
|
+
this.probeResolver = null;
|
|
1339
|
+
r(env.payload);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1105
1342
|
this.update({
|
|
1106
1343
|
...env.payload,
|
|
1107
1344
|
version: Math.max(this.snapshot.version, env.payload.version) + 1
|
|
@@ -1386,7 +1623,7 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
1386
1623
|
returnTo,
|
|
1387
1624
|
createdAt: Date.now()
|
|
1388
1625
|
});
|
|
1389
|
-
const url2 = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.
|
|
1626
|
+
const url2 = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.hostedIssuerUrl);
|
|
1390
1627
|
url2.searchParams.set("response_type", "code");
|
|
1391
1628
|
url2.searchParams.set("app", manager.appKey);
|
|
1392
1629
|
url2.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
@@ -1401,33 +1638,50 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
1401
1638
|
return url2.toString();
|
|
1402
1639
|
}
|
|
1403
1640
|
async function redirectToSignIn(manager, opts = {}) {
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1641
|
+
const t0 = Date.now();
|
|
1642
|
+
let ok = false;
|
|
1643
|
+
let code;
|
|
1644
|
+
try {
|
|
1645
|
+
const url2 = await buildSignInUrl(manager, opts);
|
|
1646
|
+
if (typeof window === "undefined") {
|
|
1647
|
+
code = "NO_WINDOW";
|
|
1648
|
+
throw new Error("redirectToSignIn requires a browser environment");
|
|
1649
|
+
}
|
|
1650
|
+
ok = true;
|
|
1651
|
+
manager.recordTiming("signIn", Date.now() - t0, true);
|
|
1652
|
+
window.location.assign(url2);
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
if (!ok) manager.recordTiming("signIn", Date.now() - t0, false, code ?? (err instanceof Error ? err.message : "ERROR"));
|
|
1655
|
+
throw err;
|
|
1407
1656
|
}
|
|
1408
|
-
window.location.assign(url2);
|
|
1409
1657
|
}
|
|
1410
1658
|
async function signIn(manager, opts = {}) {
|
|
1411
1659
|
return redirectToSignIn(manager, opts);
|
|
1412
1660
|
}
|
|
1413
1661
|
async function handleAuthCallback(manager, options = {}) {
|
|
1662
|
+
const t0 = Date.now();
|
|
1663
|
+
const emit = (ok, code2) => manager.recordTiming("signIn", Date.now() - t0, ok, code2);
|
|
1414
1664
|
const url2 = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
|
|
1415
1665
|
const code = url2.searchParams.get("code");
|
|
1416
1666
|
const state = url2.searchParams.get("state");
|
|
1417
1667
|
const errorParam = url2.searchParams.get("error");
|
|
1418
1668
|
if (errorParam) {
|
|
1669
|
+
emit(false, errorParam);
|
|
1419
1670
|
return { ok: false, returnTo: "/", error: errorParam };
|
|
1420
1671
|
}
|
|
1421
1672
|
if (!code || !state) {
|
|
1673
|
+
emit(false, "missing_code_or_state");
|
|
1422
1674
|
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
1423
1675
|
}
|
|
1424
1676
|
const record = loadPkce(state);
|
|
1425
1677
|
if (!record) {
|
|
1678
|
+
emit(false, "unknown_state");
|
|
1426
1679
|
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
1427
1680
|
}
|
|
1428
1681
|
clearPkce(state);
|
|
1429
1682
|
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
1430
1683
|
if (!fetchImpl) {
|
|
1684
|
+
emit(false, "no_fetch");
|
|
1431
1685
|
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
1432
1686
|
}
|
|
1433
1687
|
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
@@ -1446,10 +1700,12 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
1446
1700
|
const body = await res.json().catch(() => ({}));
|
|
1447
1701
|
if (!res.ok) {
|
|
1448
1702
|
const desc = body.error_description ?? body.error ?? "token_exchange_failed";
|
|
1703
|
+
emit(false, desc);
|
|
1449
1704
|
return { ok: false, returnTo: record.returnTo, error: desc };
|
|
1450
1705
|
}
|
|
1451
1706
|
const tokens = body;
|
|
1452
1707
|
if (!tokens.access_token) {
|
|
1708
|
+
emit(false, "missing_access_token");
|
|
1453
1709
|
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
1454
1710
|
}
|
|
1455
1711
|
if (tokens.refresh_token) {
|
|
@@ -1457,21 +1713,24 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
1457
1713
|
setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
1458
1714
|
}
|
|
1459
1715
|
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
1716
|
+
emit(true);
|
|
1460
1717
|
return { ok: true, returnTo: record.returnTo };
|
|
1461
1718
|
}
|
|
1462
1719
|
async function signOut(manager, opts = {}) {
|
|
1463
1720
|
if (!opts.localOnly) {
|
|
1464
1721
|
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
1722
|
+
const idempotency = manager.getIdempotencyToken();
|
|
1465
1723
|
try {
|
|
1466
1724
|
const url2 = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
1467
|
-
await manager.fetch(url2, { method: "POST" }).catch(() => void 0);
|
|
1725
|
+
await manager.fetch(url2, { method: "POST", headers: { "X-IQAuth-Idempotency": idempotency } }).catch(() => void 0);
|
|
1468
1726
|
} catch {
|
|
1469
1727
|
}
|
|
1470
1728
|
if (opts.endSsoSession !== false) {
|
|
1471
1729
|
try {
|
|
1472
1730
|
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
1473
1731
|
method: "POST",
|
|
1474
|
-
credentials: "include"
|
|
1732
|
+
credentials: "include",
|
|
1733
|
+
headers: { "X-IQAuth-Idempotency": idempotency }
|
|
1475
1734
|
}).catch(() => void 0);
|
|
1476
1735
|
} catch {
|
|
1477
1736
|
}
|
package/dist/browser.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
exitImpersonation,
|
|
5
5
|
reverify,
|
|
6
6
|
withReverification
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DFWHSDYQ.mjs";
|
|
8
8
|
import {
|
|
9
9
|
AccountRegistry,
|
|
10
10
|
MultiAccountTokenStore,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
signInWithPasskey,
|
|
21
21
|
unlinkProvider,
|
|
22
22
|
verifyMagicLink
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-4V7FKOTG.mjs";
|
|
24
24
|
import {
|
|
25
25
|
REFRESH_COOKIE,
|
|
26
26
|
buildSignInUrl,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
setCookie,
|
|
32
32
|
signIn,
|
|
33
33
|
signOut
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-GN37E64I.mjs";
|
|
35
35
|
import {
|
|
36
36
|
createPkcePair,
|
|
37
37
|
randomUrlSafe,
|
|
@@ -42,11 +42,11 @@ import {
|
|
|
42
42
|
isPublishableKey,
|
|
43
43
|
isSecretKey,
|
|
44
44
|
parsePublishableKey
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-HVHNYPDC.mjs";
|
|
46
46
|
import {
|
|
47
47
|
ErrorCodes,
|
|
48
48
|
IQAuthError
|
|
49
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
50
50
|
import "./chunk-Y6FXYEAI.mjs";
|
|
51
51
|
export {
|
|
52
52
|
AccountRegistry,
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertPublishableKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HVHNYPDC.mjs";
|
|
4
4
|
import {
|
|
5
5
|
IQAuthClient
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZLJPABB7.mjs";
|
|
7
7
|
import {
|
|
8
8
|
IQAuthError
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
10
10
|
|
|
11
11
|
// src/middleware/express.ts
|
|
12
12
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
13
|
+
// Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
|
|
13
14
|
"TOKEN_INVALID",
|
|
14
15
|
"TOKEN_EXPIRED",
|
|
15
16
|
"TOKEN_REVOKED",
|
|
16
17
|
"SESSION_EXPIRED",
|
|
17
18
|
"SESSION_INVALID",
|
|
18
|
-
"AUTH_REQUIRED"
|
|
19
|
+
"AUTH_REQUIRED",
|
|
20
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
|
|
21
|
+
// Mapped to 401 here so framework consumers don't have to learn the new
|
|
22
|
+
// codes to keep their auth-failure handling working.
|
|
23
|
+
"token_invalid",
|
|
24
|
+
"token_expired"
|
|
19
25
|
]);
|
|
20
26
|
var DEFAULT_ACCESS_COOKIE = "iqauth_at";
|
|
21
27
|
var DEFAULT_REFRESH_COOKIE = "iqauth_rt";
|