@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
|
@@ -3,15 +3,16 @@ import {
|
|
|
3
3
|
clearCookie,
|
|
4
4
|
getCookie,
|
|
5
5
|
setCookie
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-GN37E64I.mjs";
|
|
7
7
|
import {
|
|
8
8
|
assertPublishableKey
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-HVHNYPDC.mjs";
|
|
10
10
|
import {
|
|
11
11
|
IQAuthError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
13
13
|
|
|
14
14
|
// src/browser/sessionManager.ts
|
|
15
|
+
var PROBE_WAIT_MS = 80;
|
|
15
16
|
var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
|
|
16
17
|
var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
|
|
17
18
|
async function readAuthErrorCode(res) {
|
|
@@ -49,7 +50,13 @@ function claimsToSessionUser(claims) {
|
|
|
49
50
|
tenantId: claims.tenantId,
|
|
50
51
|
vendorId: claims.vendorId,
|
|
51
52
|
roles: claims.roles ?? [],
|
|
52
|
-
entitlements: claims.entitlements ?? []
|
|
53
|
+
entitlements: claims.entitlements ?? [],
|
|
54
|
+
// SDK 2.7.0 (Task #124) — pass through identity claims when issued.
|
|
55
|
+
...claims.picture !== void 0 ? { picture: claims.picture } : {},
|
|
56
|
+
...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
|
|
57
|
+
...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
|
|
58
|
+
...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
|
|
59
|
+
...claims.locale !== void 0 ? { locale: claims.locale } : {}
|
|
53
60
|
};
|
|
54
61
|
}
|
|
55
62
|
var EMPTY = {
|
|
@@ -73,11 +80,50 @@ var NO_OP_STORE = {
|
|
|
73
80
|
write: () => void 0,
|
|
74
81
|
clear: () => void 0
|
|
75
82
|
};
|
|
83
|
+
var IDEMPOTENCY_HEADER = "X-IQAuth-Idempotency";
|
|
84
|
+
function randomIdempotencyToken() {
|
|
85
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
86
|
+
const bytes = new Uint8Array(16);
|
|
87
|
+
crypto.getRandomValues(bytes);
|
|
88
|
+
let out = "";
|
|
89
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
93
|
+
}
|
|
76
94
|
var SessionManager = class {
|
|
77
95
|
constructor(options) {
|
|
78
96
|
this.snapshot = { ...EMPTY };
|
|
79
97
|
this.listeners = /* @__PURE__ */ new Set();
|
|
80
98
|
this.refreshPromise = null;
|
|
99
|
+
/**
|
|
100
|
+
* Cancellation handle for the in-flight refresh, if any. `signOut()` (or a
|
|
101
|
+
* `session:signout` broadcast from another tab) calls `abort()` so the
|
|
102
|
+
* refresh response is dropped before it can write a fresh access cookie
|
|
103
|
+
* on top of the just-cleared session — the second root cause of "ghost
|
|
104
|
+
* signed-in" sessions after Sign Out.
|
|
105
|
+
*/
|
|
106
|
+
this.refreshAbort = null;
|
|
107
|
+
/**
|
|
108
|
+
* Set to `true` by `signOut()` / `signOutLocal()` for the lifetime of the
|
|
109
|
+
* call. Used as a safety belt: even if a refresh response arrives while
|
|
110
|
+
* `refreshAbort` was unable to interrupt the network call (e.g. the body
|
|
111
|
+
* was already streaming back), `runRefresh` checks this flag before
|
|
112
|
+
* mutating session state and bails out.
|
|
113
|
+
*/
|
|
114
|
+
this.signoutInProgress = false;
|
|
115
|
+
/**
|
|
116
|
+
* Per-session opaque idempotency token. Sent as `X-IQAuth-Idempotency` on
|
|
117
|
+
* every /refresh and /signout request the SDK makes through a framework
|
|
118
|
+
* adapter (Express/Fastify/Hono/Next), so the adapter's `SignoutRegistry`
|
|
119
|
+
* can collapse a refresh that lands moments after a signout — even when
|
|
120
|
+
* the two requests are routed to different server instances (multi-replica
|
|
121
|
+
* deployments).
|
|
122
|
+
*
|
|
123
|
+
* Generated lazily on first use, rotated on signout so the next session
|
|
124
|
+
* starts with a fresh token. Opaque random — never the raw refresh token.
|
|
125
|
+
*/
|
|
126
|
+
this.idempotencyToken = null;
|
|
81
127
|
this.channel = null;
|
|
82
128
|
this.proactiveTimer = null;
|
|
83
129
|
this.bootstrapped = false;
|
|
@@ -85,6 +131,8 @@ var SessionManager = class {
|
|
|
85
131
|
this.remoteRefreshWaiters = [];
|
|
86
132
|
/** Active claims by other tabs (keyed by source tabId). */
|
|
87
133
|
this.foreignClaim = null;
|
|
134
|
+
/** Resolver for an in-flight cross-tab `session:probe`, set during bootstrap. */
|
|
135
|
+
this.probeResolver = null;
|
|
88
136
|
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
|
|
89
137
|
this.key = parsed;
|
|
90
138
|
const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
|
|
@@ -97,6 +145,8 @@ var SessionManager = class {
|
|
|
97
145
|
this.refreshCookieName = options.cookieNames?.refresh ?? REFRESH_COOKIE;
|
|
98
146
|
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore(this.refreshCookieName) : NO_OP_STORE);
|
|
99
147
|
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
148
|
+
this.debug = options.debug ?? false;
|
|
149
|
+
this.onTimingEvent = options.onTimingEvent ?? null;
|
|
100
150
|
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
101
151
|
throw new Error("global fetch is not available; pass fetchImpl");
|
|
102
152
|
}));
|
|
@@ -123,10 +173,35 @@ var SessionManager = class {
|
|
|
123
173
|
get issuerUrl() {
|
|
124
174
|
return this.issuer;
|
|
125
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* SDK 2.7.0 (Task #124) — The hosted IQAuth host derived from the
|
|
178
|
+
* publishable key's `iss` claim, normalized to URL form. This is what
|
|
179
|
+
* `<SignIn/>` and `buildSignInUrl` use to talk to the hosted UI; it
|
|
180
|
+
* deliberately ignores the `issuer` constructor override so a misrouted
|
|
181
|
+
* `issuer` (e.g. pointed at the consumer app's own domain) cannot break
|
|
182
|
+
* the hosted flow. Use {@link issuerUrl} for token / discovery endpoints.
|
|
183
|
+
*/
|
|
184
|
+
get hostedIssuerUrl() {
|
|
185
|
+
const iss = this.key.iss;
|
|
186
|
+
return (iss.startsWith("http") ? iss : `https://${iss}`).replace(/\/+$/, "");
|
|
187
|
+
}
|
|
126
188
|
/** Cookie name the SDK uses for the refresh token (overridable via `cookieNames.refresh`). */
|
|
127
189
|
get refreshCookie() {
|
|
128
190
|
return this.refreshCookieName;
|
|
129
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Returns the current per-session idempotency token, generating one
|
|
194
|
+
* lazily on first use. Sent as the `X-IQAuth-Idempotency` header on
|
|
195
|
+
* /refresh and /signout requests so the framework adapter's
|
|
196
|
+
* `SignoutRegistry` can collapse a refresh-vs-signout race even across
|
|
197
|
+
* server instances.
|
|
198
|
+
*/
|
|
199
|
+
getIdempotencyToken() {
|
|
200
|
+
if (!this.idempotencyToken) {
|
|
201
|
+
this.idempotencyToken = randomIdempotencyToken();
|
|
202
|
+
}
|
|
203
|
+
return this.idempotencyToken;
|
|
204
|
+
}
|
|
130
205
|
getSnapshot() {
|
|
131
206
|
return this.snapshot;
|
|
132
207
|
}
|
|
@@ -140,9 +215,44 @@ var SessionManager = class {
|
|
|
140
215
|
* One-time bootstrap: warm the session from the refresh cookie if present.
|
|
141
216
|
* Safe to call multiple times.
|
|
142
217
|
*/
|
|
218
|
+
/**
|
|
219
|
+
* Task #126: Public timing-event emitter. Used by the browser sign-in
|
|
220
|
+
* helpers (redirectToSignIn / handleAuthCallback) to surface signIn-phase
|
|
221
|
+
* timings through the same `debug` + `onTimingEvent` channel as
|
|
222
|
+
* bootstrap/refresh. Safe to call from anywhere — internal callers
|
|
223
|
+
* pre-compute durationMs.
|
|
224
|
+
*/
|
|
225
|
+
recordTiming(phase, durationMs, ok, code) {
|
|
226
|
+
this.emitTiming(phase, durationMs, ok, code);
|
|
227
|
+
}
|
|
228
|
+
/** Task #126: emit a session timing event to debug log + onTimingEvent hook. */
|
|
229
|
+
emitTiming(phase, durationMs, ok, code) {
|
|
230
|
+
if (this.debug) {
|
|
231
|
+
try {
|
|
232
|
+
console.debug("[iqauth_session]", { phase, durationMs, ok, code });
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (this.onTimingEvent) {
|
|
237
|
+
try {
|
|
238
|
+
this.onTimingEvent({ phase, durationMs, ok, code });
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
143
243
|
async bootstrap() {
|
|
144
244
|
if (this.bootstrapped) return;
|
|
145
245
|
this.bootstrapped = true;
|
|
246
|
+
const t0 = Date.now();
|
|
247
|
+
try {
|
|
248
|
+
await this.bootstrapInner();
|
|
249
|
+
this.emitTiming("bootstrap", Date.now() - t0, this.snapshot.status === "authenticated");
|
|
250
|
+
} catch (err) {
|
|
251
|
+
this.emitTiming("bootstrap", Date.now() - t0, false, err instanceof Error ? err.message : "ERROR");
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async bootstrapInner() {
|
|
146
256
|
if (this.serverManagedSession) {
|
|
147
257
|
try {
|
|
148
258
|
const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
|
|
@@ -177,6 +287,15 @@ var SessionManager = class {
|
|
|
177
287
|
return;
|
|
178
288
|
}
|
|
179
289
|
}
|
|
290
|
+
const peerSnapshot = await this.probePeers();
|
|
291
|
+
if (peerSnapshot && peerSnapshot.status === "authenticated") {
|
|
292
|
+
this.update({
|
|
293
|
+
...peerSnapshot,
|
|
294
|
+
version: Math.max(this.snapshot.version, peerSnapshot.version) + 1
|
|
295
|
+
});
|
|
296
|
+
this.scheduleProactiveRefresh();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
180
299
|
const stored = await Promise.resolve(this.tokenStore.read());
|
|
181
300
|
if (!stored) {
|
|
182
301
|
this.setStatus("unauthenticated");
|
|
@@ -185,6 +304,22 @@ var SessionManager = class {
|
|
|
185
304
|
const ok = await this.refresh();
|
|
186
305
|
if (!ok) this.setStatus("unauthenticated");
|
|
187
306
|
}
|
|
307
|
+
probePeers() {
|
|
308
|
+
if (!this.channel) return Promise.resolve(null);
|
|
309
|
+
return new Promise((resolve) => {
|
|
310
|
+
let settled = false;
|
|
311
|
+
const finish = (snap) => {
|
|
312
|
+
if (settled) return;
|
|
313
|
+
settled = true;
|
|
314
|
+
this.probeResolver = null;
|
|
315
|
+
clearTimeout(timer);
|
|
316
|
+
resolve(snap);
|
|
317
|
+
};
|
|
318
|
+
this.probeResolver = (snap) => finish(snap);
|
|
319
|
+
const timer = setTimeout(() => finish(null), PROBE_WAIT_MS);
|
|
320
|
+
this.broadcastEnvelope({ type: "session:probe", source: this.tabId, ts: Date.now() });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
188
323
|
/**
|
|
189
324
|
* Single-flight token refresh, coordinated across tabs via BroadcastChannel.
|
|
190
325
|
*
|
|
@@ -198,30 +333,48 @@ var SessionManager = class {
|
|
|
198
333
|
*/
|
|
199
334
|
refresh() {
|
|
200
335
|
if (this.refreshPromise) return this.refreshPromise;
|
|
201
|
-
|
|
336
|
+
const t0 = Date.now();
|
|
337
|
+
this.refreshPromise = this.runRefresh().then((ok) => {
|
|
338
|
+
this.emitTiming("refresh", Date.now() - t0, ok, ok ? void 0 : this.snapshot.error?.code ?? "REFRESH_FAILED");
|
|
339
|
+
return ok;
|
|
340
|
+
}).finally(() => {
|
|
202
341
|
this.refreshPromise = null;
|
|
203
342
|
});
|
|
204
343
|
return this.refreshPromise;
|
|
205
344
|
}
|
|
206
345
|
async runRefresh() {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
210
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
211
|
-
const foreign = this.foreignClaim;
|
|
212
|
-
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
213
|
-
return this.waitForForeignRefresh();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
346
|
+
const abort = new AbortController();
|
|
347
|
+
this.refreshAbort = abort;
|
|
216
348
|
try {
|
|
349
|
+
const myClaim = { source: this.tabId, ts: Date.now() };
|
|
350
|
+
if (this.channel) {
|
|
351
|
+
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
352
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
353
|
+
if (abort.signal.aborted || this.signoutInProgress) {
|
|
354
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const foreign = this.foreignClaim;
|
|
358
|
+
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
359
|
+
return this.waitForForeignRefresh();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
217
362
|
const refreshToken = await Promise.resolve(this.tokenStore.read());
|
|
218
363
|
const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
|
|
219
364
|
method: "POST",
|
|
220
365
|
credentials: "include",
|
|
221
|
-
headers: {
|
|
222
|
-
|
|
366
|
+
headers: {
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
[IDEMPOTENCY_HEADER]: this.getIdempotencyToken()
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify(refreshToken ? { refreshToken } : {}),
|
|
371
|
+
signal: abort.signal
|
|
223
372
|
});
|
|
224
373
|
const body = await res.json().catch(() => ({}));
|
|
374
|
+
if (this.signoutInProgress || abort.signal.aborted) {
|
|
375
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
225
378
|
const data = body.data;
|
|
226
379
|
if (!res.ok || !body.success || !data?.accessToken) {
|
|
227
380
|
const err = body.error;
|
|
@@ -240,14 +393,18 @@ var SessionManager = class {
|
|
|
240
393
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
|
|
241
394
|
return true;
|
|
242
395
|
} catch (err) {
|
|
243
|
-
this.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
396
|
+
const aborted = err?.name === "AbortError" || abort.signal.aborted || this.signoutInProgress;
|
|
397
|
+
if (!aborted) {
|
|
398
|
+
this.setError({
|
|
399
|
+
code: "NETWORK_ERROR",
|
|
400
|
+
message: err instanceof Error ? err.message : "Refresh request failed"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
247
403
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
248
404
|
return false;
|
|
249
405
|
} finally {
|
|
250
406
|
this.foreignClaim = null;
|
|
407
|
+
if (this.refreshAbort === abort) this.refreshAbort = null;
|
|
251
408
|
}
|
|
252
409
|
}
|
|
253
410
|
claimWins(foreign, mine) {
|
|
@@ -355,6 +512,14 @@ var SessionManager = class {
|
|
|
355
512
|
* the server-side logout request.
|
|
356
513
|
*/
|
|
357
514
|
signOutLocal(status = "unauthenticated") {
|
|
515
|
+
this.signoutInProgress = true;
|
|
516
|
+
if (this.refreshAbort) {
|
|
517
|
+
try {
|
|
518
|
+
this.refreshAbort.abort();
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
this.refreshAbort = null;
|
|
522
|
+
}
|
|
358
523
|
void Promise.resolve(this.tokenStore.clear());
|
|
359
524
|
if (this.proactiveTimer) {
|
|
360
525
|
clearTimeout(this.proactiveTimer);
|
|
@@ -369,7 +534,12 @@ var SessionManager = class {
|
|
|
369
534
|
error: null,
|
|
370
535
|
version: this.snapshot.version + 1
|
|
371
536
|
});
|
|
537
|
+
this.broadcastEnvelope({ type: "refresh:abort", source: this.tabId, ts: Date.now() });
|
|
372
538
|
this.broadcast("session:signout");
|
|
539
|
+
this.idempotencyToken = null;
|
|
540
|
+
setTimeout(() => {
|
|
541
|
+
this.signoutInProgress = false;
|
|
542
|
+
}, 0);
|
|
373
543
|
}
|
|
374
544
|
/**
|
|
375
545
|
* Replace the refresh-token store at runtime. Used by the F22
|
|
@@ -433,6 +603,12 @@ var SessionManager = class {
|
|
|
433
603
|
}
|
|
434
604
|
onBroadcast(env) {
|
|
435
605
|
if (!env || env.source === this.tabId) return;
|
|
606
|
+
if (env.type === "session:probe") {
|
|
607
|
+
if (this.snapshot.status === "authenticated") {
|
|
608
|
+
this.broadcast("session:update");
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
436
612
|
if (env.type === "refresh:claim") {
|
|
437
613
|
this.foreignClaim = { source: env.source, ts: env.ts };
|
|
438
614
|
return;
|
|
@@ -445,6 +621,24 @@ var SessionManager = class {
|
|
|
445
621
|
this.foreignClaim = null;
|
|
446
622
|
return;
|
|
447
623
|
}
|
|
624
|
+
if (env.type === "refresh:abort") {
|
|
625
|
+
this.signoutInProgress = true;
|
|
626
|
+
if (this.refreshAbort) {
|
|
627
|
+
try {
|
|
628
|
+
this.refreshAbort.abort();
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
this.refreshAbort = null;
|
|
632
|
+
}
|
|
633
|
+
const waiters = this.remoteRefreshWaiters;
|
|
634
|
+
this.remoteRefreshWaiters = [];
|
|
635
|
+
for (const w of waiters) w(false);
|
|
636
|
+
this.foreignClaim = null;
|
|
637
|
+
setTimeout(() => {
|
|
638
|
+
this.signoutInProgress = false;
|
|
639
|
+
}, 0);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
448
642
|
if (env.type === "session:signout") {
|
|
449
643
|
this.update({
|
|
450
644
|
status: "unauthenticated",
|
|
@@ -458,6 +652,12 @@ var SessionManager = class {
|
|
|
458
652
|
return;
|
|
459
653
|
}
|
|
460
654
|
if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
|
|
655
|
+
if (this.probeResolver && env.payload.status === "authenticated") {
|
|
656
|
+
const r = this.probeResolver;
|
|
657
|
+
this.probeResolver = null;
|
|
658
|
+
r(env.payload);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
461
661
|
this.update({
|
|
462
662
|
...env.payload,
|
|
463
663
|
version: Math.max(this.snapshot.version, env.payload.version) + 1
|
|
@@ -1,8 +1,49 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertPublishableKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HVHNYPDC.mjs";
|
|
4
|
+
import {
|
|
5
|
+
TokensModule
|
|
6
|
+
} from "./chunk-NUO2I65G.mjs";
|
|
7
|
+
import {
|
|
8
|
+
IQAuthError
|
|
9
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
4
10
|
|
|
5
11
|
// src/server/handlers.ts
|
|
12
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
13
|
+
const baseUser = {
|
|
14
|
+
sub: claims.sub,
|
|
15
|
+
email: claims.email,
|
|
16
|
+
name: claims.name,
|
|
17
|
+
tenantId: claims.tenantId,
|
|
18
|
+
vendorId: claims.vendorId,
|
|
19
|
+
roles: claims.roles ?? [],
|
|
20
|
+
entitlements: claims.entitlements ?? []
|
|
21
|
+
};
|
|
22
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
23
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
data: {
|
|
27
|
+
user,
|
|
28
|
+
claims,
|
|
29
|
+
tenantId: claims.tenantId ?? null
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function emitTiming(cfg, event) {
|
|
34
|
+
if (cfg.debug) {
|
|
35
|
+
try {
|
|
36
|
+
console.debug("[iqauth_helper]", event);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (cfg.onTimingEvent) {
|
|
41
|
+
try {
|
|
42
|
+
cfg.onTimingEvent(event);
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
6
47
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
7
48
|
"TOKEN_REVOKED",
|
|
8
49
|
"SESSION_REVOKED",
|
|
@@ -42,7 +83,11 @@ function resolve(config) {
|
|
|
42
83
|
})),
|
|
43
84
|
appId: parsed.appId,
|
|
44
85
|
tenantId: parsed.tenantId,
|
|
45
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
86
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
87
|
+
debug: config.debug,
|
|
88
|
+
onTimingEvent: config.onTimingEvent,
|
|
89
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
90
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
46
91
|
};
|
|
47
92
|
}
|
|
48
93
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -59,15 +104,64 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
59
104
|
}
|
|
60
105
|
function clearCookies(cfg) {
|
|
61
106
|
return [
|
|
62
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
63
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
107
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
108
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
64
109
|
];
|
|
65
110
|
}
|
|
111
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
112
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
113
|
+
function pruneInMemoryMarkers(now) {
|
|
114
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
115
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
116
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
var defaultSignoutRegistry = {
|
|
120
|
+
mark(token, ttlMs) {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
pruneInMemoryMarkers(now);
|
|
123
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
124
|
+
},
|
|
125
|
+
has(token) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
128
|
+
if (!exp) return false;
|
|
129
|
+
if (exp <= now) {
|
|
130
|
+
inMemorySignoutMarkers.delete(token);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function __resetSignoutMarkersForTests() {
|
|
137
|
+
inMemorySignoutMarkers.clear();
|
|
138
|
+
}
|
|
139
|
+
function createInMemorySignoutRegistry() {
|
|
140
|
+
const store = /* @__PURE__ */ new Map();
|
|
141
|
+
return {
|
|
142
|
+
mark(token, ttlMs) {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
for (const [k, exp] of store) if (exp <= now) store.delete(k);
|
|
145
|
+
store.set(token, now + ttlMs);
|
|
146
|
+
},
|
|
147
|
+
has(token) {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const exp = store.get(token);
|
|
150
|
+
if (!exp) return false;
|
|
151
|
+
if (exp <= now) {
|
|
152
|
+
store.delete(token);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
66
159
|
function serializeCookie(d) {
|
|
67
160
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
68
161
|
parts.push(`Path=${d.path}`);
|
|
69
162
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
70
163
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
164
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
71
165
|
if (d.secure) parts.push("Secure");
|
|
72
166
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
73
167
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -75,7 +169,9 @@ function serializeCookie(d) {
|
|
|
75
169
|
}
|
|
76
170
|
async function handleCallback(config, input) {
|
|
77
171
|
const cfg = resolve(config);
|
|
172
|
+
const t0 = Date.now();
|
|
78
173
|
if (!input.code || !input.redirectUri) {
|
|
174
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
79
175
|
return {
|
|
80
176
|
status: 400,
|
|
81
177
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
@@ -83,6 +179,7 @@ async function handleCallback(config, input) {
|
|
|
83
179
|
};
|
|
84
180
|
}
|
|
85
181
|
if (!cfg.secretKey) {
|
|
182
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
86
183
|
return {
|
|
87
184
|
status: 500,
|
|
88
185
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -106,6 +203,7 @@ async function handleCallback(config, input) {
|
|
|
106
203
|
});
|
|
107
204
|
const json = await res.json().catch(() => ({}));
|
|
108
205
|
if (!res.ok || !json.access_token) {
|
|
206
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
109
207
|
return {
|
|
110
208
|
status: res.status || 502,
|
|
111
209
|
body: {
|
|
@@ -125,6 +223,7 @@ async function handleCallback(config, input) {
|
|
|
125
223
|
if (json.refresh_token) {
|
|
126
224
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
127
225
|
}
|
|
226
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
128
227
|
return {
|
|
129
228
|
status: 200,
|
|
130
229
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -133,8 +232,18 @@ async function handleCallback(config, input) {
|
|
|
133
232
|
}
|
|
134
233
|
async function handleRefresh(config, input) {
|
|
135
234
|
const cfg = resolve(config);
|
|
235
|
+
const t0 = Date.now();
|
|
136
236
|
const refreshToken = input.refreshToken;
|
|
237
|
+
const idemKey = input.idempotencyToken;
|
|
238
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
239
|
+
return {
|
|
240
|
+
status: 401,
|
|
241
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
242
|
+
cookies: clearCookies(cfg)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
137
245
|
if (!refreshToken) {
|
|
246
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
138
247
|
return {
|
|
139
248
|
status: 401,
|
|
140
249
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -150,6 +259,7 @@ async function handleRefresh(config, input) {
|
|
|
150
259
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
151
260
|
const status = res.status || 401;
|
|
152
261
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
262
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
153
263
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
154
264
|
cfg.clearCookiesOnRefreshFailure,
|
|
155
265
|
status,
|
|
@@ -173,6 +283,7 @@ async function handleRefresh(config, input) {
|
|
|
173
283
|
if (json.data.refreshToken) {
|
|
174
284
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
175
285
|
}
|
|
286
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
176
287
|
return {
|
|
177
288
|
status: 200,
|
|
178
289
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -181,6 +292,10 @@ async function handleRefresh(config, input) {
|
|
|
181
292
|
}
|
|
182
293
|
async function handleSignout(config, input) {
|
|
183
294
|
const cfg = resolve(config);
|
|
295
|
+
const t0 = Date.now();
|
|
296
|
+
if (input.idempotencyToken) {
|
|
297
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
298
|
+
}
|
|
184
299
|
if (input.accessToken) {
|
|
185
300
|
try {
|
|
186
301
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -202,16 +317,60 @@ async function handleSignout(config, input) {
|
|
|
202
317
|
} catch {
|
|
203
318
|
}
|
|
204
319
|
}
|
|
320
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
205
321
|
return {
|
|
206
322
|
status: 200,
|
|
207
323
|
body: { success: true, data: { signedOut: true } },
|
|
208
324
|
cookies: clearCookies(cfg)
|
|
209
325
|
};
|
|
210
326
|
}
|
|
327
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
328
|
+
function getTokensFor(issuer) {
|
|
329
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
330
|
+
if (!m) {
|
|
331
|
+
m = new TokensModule(issuer);
|
|
332
|
+
TOKENS_CACHE.set(issuer, m);
|
|
333
|
+
}
|
|
334
|
+
return m;
|
|
335
|
+
}
|
|
336
|
+
async function handleUserinfo(config, input) {
|
|
337
|
+
const cfg = resolve(config);
|
|
338
|
+
if (!input.accessToken) {
|
|
339
|
+
return {
|
|
340
|
+
status: 401,
|
|
341
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
342
|
+
cookies: []
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
let claims;
|
|
346
|
+
try {
|
|
347
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
350
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
351
|
+
return {
|
|
352
|
+
status: 401,
|
|
353
|
+
body: { success: false, error: { code, message } },
|
|
354
|
+
cookies: []
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
358
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
status: 200,
|
|
362
|
+
body: envelope,
|
|
363
|
+
cookies: []
|
|
364
|
+
};
|
|
365
|
+
}
|
|
211
366
|
|
|
212
367
|
export {
|
|
368
|
+
buildUserinfoResponse,
|
|
369
|
+
__resetSignoutMarkersForTests,
|
|
370
|
+
createInMemorySignoutRegistry,
|
|
213
371
|
serializeCookie,
|
|
214
372
|
handleCallback,
|
|
215
373
|
handleRefresh,
|
|
216
|
-
handleSignout
|
|
374
|
+
handleSignout,
|
|
375
|
+
handleUserinfo
|
|
217
376
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertPublishableKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HVHNYPDC.mjs";
|
|
4
4
|
import {
|
|
5
5
|
TokensModule
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-NUO2I65G.mjs";
|
|
7
7
|
import {
|
|
8
8
|
IQAuthError
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6PJRLRB4.mjs";
|
|
10
10
|
|
|
11
11
|
// src/ws.ts
|
|
12
12
|
var DEFAULT_COOKIE = "iqauth_at";
|