@iqauth/sdk 2.0.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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -0
  3. package/dist/browser-session.d.mts +12 -0
  4. package/dist/browser-session.d.ts +12 -0
  5. package/dist/browser-session.js +1812 -0
  6. package/dist/browser-session.mjs +28 -0
  7. package/dist/browser.d.mts +46 -0
  8. package/dist/browser.d.ts +46 -0
  9. package/dist/browser.js +768 -0
  10. package/dist/browser.mjs +47 -0
  11. package/dist/chunk-5HF3OBNO.mjs +189 -0
  12. package/dist/chunk-5WFR6Y33.mjs +59 -0
  13. package/dist/chunk-6I6RM4MN.mjs +51 -0
  14. package/dist/chunk-73R6BEGO.mjs +176 -0
  15. package/dist/chunk-E46DKOVI.mjs +632 -0
  16. package/dist/chunk-JQWYIIIS.mjs +1740 -0
  17. package/dist/chunk-X3K3WOBR.mjs +64 -0
  18. package/dist/chunk-Y6FXYEAI.mjs +10 -0
  19. package/dist/cli/index.d.mts +1 -0
  20. package/dist/cli/index.d.ts +1 -0
  21. package/dist/cli/index.js +581 -0
  22. package/dist/cli/index.mjs +57 -0
  23. package/dist/client-C1DXfB8Z.d.mts +911 -0
  24. package/dist/client-CggvJmmm.d.ts +911 -0
  25. package/dist/dev-FUTJZSWN.mjs +56 -0
  26. package/dist/doctor-OHJRZBBT.mjs +89 -0
  27. package/dist/errors-CDdl24MP.d.mts +52 -0
  28. package/dist/errors-CDdl24MP.d.ts +52 -0
  29. package/dist/express-BKAXB5Nl.d.ts +61 -0
  30. package/dist/express-CpfyYTmw.d.mts +61 -0
  31. package/dist/express.d.mts +45 -0
  32. package/dist/express.d.ts +45 -0
  33. package/dist/express.js +2252 -0
  34. package/dist/express.mjs +122 -0
  35. package/dist/fastify.d.mts +23 -0
  36. package/dist/fastify.d.ts +23 -0
  37. package/dist/fastify.js +2062 -0
  38. package/dist/fastify.mjs +118 -0
  39. package/dist/hono.d.mts +22 -0
  40. package/dist/hono.d.ts +22 -0
  41. package/dist/hono.js +2051 -0
  42. package/dist/hono.mjs +107 -0
  43. package/dist/index.d.mts +6 -0
  44. package/dist/index.d.ts +6 -0
  45. package/dist/index.js +2070 -0
  46. package/dist/index.mjs +83 -0
  47. package/dist/init-LLCSQGNL.mjs +198 -0
  48. package/dist/keys-NLWFAOEM.mjs +63 -0
  49. package/dist/mobile.d.mts +11 -0
  50. package/dist/mobile.d.ts +11 -0
  51. package/dist/mobile.js +1809 -0
  52. package/dist/mobile.mjs +25 -0
  53. package/dist/next.d.mts +37 -0
  54. package/dist/next.d.ts +37 -0
  55. package/dist/next.js +2078 -0
  56. package/dist/next.mjs +130 -0
  57. package/dist/publishableKey-B5DIK81A.d.mts +24 -0
  58. package/dist/publishableKey-B5DIK81A.d.ts +24 -0
  59. package/dist/react.d.mts +196 -0
  60. package/dist/react.d.ts +196 -0
  61. package/dist/react.js +1457 -0
  62. package/dist/react.mjs +787 -0
  63. package/dist/server/handlers.d.mts +96 -0
  64. package/dist/server/handlers.d.ts +96 -0
  65. package/dist/server/handlers.js +243 -0
  66. package/dist/server/handlers.mjs +14 -0
  67. package/dist/server.d.mts +14 -0
  68. package/dist/server.d.ts +14 -0
  69. package/dist/server.js +2195 -0
  70. package/dist/server.mjs +47 -0
  71. package/dist/service.d.mts +11 -0
  72. package/dist/service.d.ts +11 -0
  73. package/dist/service.js +1809 -0
  74. package/dist/service.mjs +25 -0
  75. package/dist/signIn-C8f6qVjD.d.mts +238 -0
  76. package/dist/signIn-Cy2lbEXb.d.ts +238 -0
  77. package/dist/types-Cxl3bQHt.d.mts +900 -0
  78. package/dist/types-Cxl3bQHt.d.ts +900 -0
  79. package/docs/APP_INTEGRATION_MATRIX.md +59 -0
  80. package/docs/BROWSER_SESSION_MIGRATION.md +69 -0
  81. package/docs/FRESH_IMPLEMENTATION_GUIDE.md +188 -0
  82. package/docs/TARBALL_RELEASE_WORKFLOW.md +98 -0
  83. package/docs/V1_TO_V2_UPGRADE_GUIDE.md +318 -0
  84. package/docs/guides/api-keys.md +130 -0
  85. package/docs/guides/app-registration.md +149 -0
  86. package/docs/guides/auth-flows.md +168 -0
  87. package/docs/guides/branding.md +160 -0
  88. package/docs/guides/entitlements.md +115 -0
  89. package/docs/guides/entity-hierarchy.md +200 -0
  90. package/docs/guides/error-handling.md +251 -0
  91. package/docs/guides/gdpr-compliance.md +123 -0
  92. package/docs/guides/invitations.md +143 -0
  93. package/docs/guides/mfa-enrollment.md +170 -0
  94. package/docs/guides/middleware-reference.md +205 -0
  95. package/docs/guides/mobile-native.md +110 -0
  96. package/docs/guides/roles-and-permissions.md +220 -0
  97. package/docs/guides/scoped-authorization.md +247 -0
  98. package/docs/guides/server-platform-integration.md +52 -0
  99. package/docs/guides/service-automation-integration.md +36 -0
  100. package/docs/guides/session-management.md +97 -0
  101. package/docs/guides/tenant-management.md +216 -0
  102. package/docs/guides/token-verification.md +178 -0
  103. package/docs/guides/user-management.md +184 -0
  104. package/docs/guides/webhooks.md +136 -0
  105. package/docs/integration-prompts/README.md +20 -0
  106. package/docs/integration-prompts/first-party-browser-app.md +29 -0
  107. package/docs/integration-prompts/install-from-tarball.md +41 -0
  108. package/docs/integration-prompts/migrate-from-local-packages-source.md +57 -0
  109. package/docs/integration-prompts/native-mobile-app.md +24 -0
  110. package/docs/integration-prompts/server-platform-app.md +20 -0
  111. package/docs/integration-prompts/service-automation-app.md +20 -0
  112. package/package.json +115 -0
@@ -0,0 +1,632 @@
1
+ import {
2
+ parsePublishableKey
3
+ } from "./chunk-5WFR6Y33.mjs";
4
+ import {
5
+ IQAuthError
6
+ } from "./chunk-6I6RM4MN.mjs";
7
+ import {
8
+ __require
9
+ } from "./chunk-Y6FXYEAI.mjs";
10
+
11
+ // src/browser/storage.ts
12
+ var REFRESH_COOKIE = "iqauth_rt";
13
+ var PKCE_STORAGE_PREFIX = "iqauth.pkce.";
14
+ function isBrowser() {
15
+ return typeof window !== "undefined" && typeof document !== "undefined";
16
+ }
17
+ function setCookie(name, value, opts = {}) {
18
+ if (!isBrowser()) return;
19
+ const parts = [`${name}=${encodeURIComponent(value)}`];
20
+ parts.push(`Path=${opts.path ?? "/"}`);
21
+ if (opts.maxAgeSeconds !== void 0) parts.push(`Max-Age=${opts.maxAgeSeconds}`);
22
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
23
+ if (opts.secure ?? location.protocol === "https:") parts.push("Secure");
24
+ parts.push(`SameSite=${opts.sameSite ?? "lax"}`);
25
+ document.cookie = parts.join("; ");
26
+ }
27
+ function getCookie(name) {
28
+ if (!isBrowser()) return null;
29
+ const target = `${name}=`;
30
+ const segments = document.cookie ? document.cookie.split(";") : [];
31
+ for (const seg of segments) {
32
+ const trimmed = seg.trim();
33
+ if (trimmed.startsWith(target)) {
34
+ try {
35
+ return decodeURIComponent(trimmed.slice(target.length));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ function clearCookie(name, opts = {}) {
44
+ setCookie(name, "", { ...opts, maxAgeSeconds: 0 });
45
+ }
46
+ function savePkce(record) {
47
+ if (!isBrowser()) return;
48
+ try {
49
+ sessionStorage.setItem(PKCE_STORAGE_PREFIX + record.state, JSON.stringify(record));
50
+ } catch {
51
+ }
52
+ }
53
+ function loadPkce(state) {
54
+ if (!isBrowser()) return null;
55
+ try {
56
+ const raw = sessionStorage.getItem(PKCE_STORAGE_PREFIX + state);
57
+ if (!raw) return null;
58
+ return JSON.parse(raw);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ function clearPkce(state) {
64
+ if (!isBrowser()) return;
65
+ try {
66
+ sessionStorage.removeItem(PKCE_STORAGE_PREFIX + state);
67
+ } catch {
68
+ }
69
+ }
70
+
71
+ // src/browser/sessionManager.ts
72
+ var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
73
+ var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
74
+ function decodeClaims(token) {
75
+ try {
76
+ const parts = token.split(".");
77
+ if (parts.length !== 3) return null;
78
+ const json = typeof atob === "function" ? decodeURIComponent(
79
+ atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
80
+ ) : Buffer.from(parts[1], "base64url").toString("utf8");
81
+ return JSON.parse(json);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function claimsToSessionUser(claims) {
87
+ if (!claims) return null;
88
+ return {
89
+ sub: claims.sub,
90
+ email: claims.email,
91
+ name: claims.name,
92
+ tenantId: claims.tenantId,
93
+ vendorId: claims.vendorId,
94
+ roles: claims.roles ?? [],
95
+ entitlements: claims.entitlements ?? []
96
+ };
97
+ }
98
+ var EMPTY = {
99
+ status: "loading",
100
+ accessToken: null,
101
+ user: null,
102
+ claims: null,
103
+ tenantId: null,
104
+ error: null,
105
+ version: 0
106
+ };
107
+ function defaultCookieStore() {
108
+ return {
109
+ read: () => getCookie(REFRESH_COOKIE),
110
+ write: (token) => setCookie(REFRESH_COOKIE, token, { maxAgeSeconds: 60 * 60 * 24 * 30 }),
111
+ clear: () => clearCookie(REFRESH_COOKIE)
112
+ };
113
+ }
114
+ var NO_OP_STORE = {
115
+ read: () => null,
116
+ write: () => void 0,
117
+ clear: () => void 0
118
+ };
119
+ var SessionManager = class {
120
+ constructor(options) {
121
+ this.snapshot = { ...EMPTY };
122
+ this.listeners = /* @__PURE__ */ new Set();
123
+ this.refreshPromise = null;
124
+ this.channel = null;
125
+ this.proactiveTimer = null;
126
+ this.bootstrapped = false;
127
+ /** Pending refresh awaited by other tabs after a `refresh:claim` from us. */
128
+ this.remoteRefreshWaiters = [];
129
+ /** Active claims by other tabs (keyed by source tabId). */
130
+ this.foreignClaim = null;
131
+ const parsed = parsePublishableKey(options.publishableKey);
132
+ if (!parsed) {
133
+ throw new Error(
134
+ `Invalid IQAuth publishable key. Expected pk_test_\u2026 or pk_live_\u2026 (got ${options.publishableKey?.slice(0, 12) ?? "<empty>"}\u2026).`
135
+ );
136
+ }
137
+ this.key = parsed;
138
+ const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
139
+ this.issuer = inferred.replace(/\/+$/, "");
140
+ this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
141
+ this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
142
+ this.useCookies = options.useCookies ?? true;
143
+ this.proactiveRefresh = options.proactiveRefresh ?? true;
144
+ this.tokenStore = options.tokenStore ?? (this.useCookies ? defaultCookieStore() : NO_OP_STORE);
145
+ this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
146
+ this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
147
+ throw new Error("global fetch is not available; pass fetchImpl");
148
+ }));
149
+ this.tabId = Math.random().toString(36).slice(2);
150
+ if (typeof BroadcastChannel !== "undefined") {
151
+ const name = options.channelName ?? `iqauth.${parsed.appId}`;
152
+ try {
153
+ this.channel = new BroadcastChannel(name);
154
+ this.channel.onmessage = (ev) => this.onBroadcast(ev.data);
155
+ } catch {
156
+ this.channel = null;
157
+ }
158
+ }
159
+ }
160
+ get publishableKey() {
161
+ return this.key;
162
+ }
163
+ get appKey() {
164
+ return this.key.appId;
165
+ }
166
+ get tenantIdFromKey() {
167
+ return this.key.tenantId;
168
+ }
169
+ get issuerUrl() {
170
+ return this.issuer;
171
+ }
172
+ getSnapshot() {
173
+ return this.snapshot;
174
+ }
175
+ subscribe(listener) {
176
+ this.listeners.add(listener);
177
+ return () => {
178
+ this.listeners.delete(listener);
179
+ };
180
+ }
181
+ /**
182
+ * One-time bootstrap: warm the session from the refresh cookie if present.
183
+ * Safe to call multiple times.
184
+ */
185
+ async bootstrap() {
186
+ if (this.bootstrapped) return;
187
+ this.bootstrapped = true;
188
+ const stored = await Promise.resolve(this.tokenStore.read());
189
+ if (!stored) {
190
+ this.setStatus("unauthenticated");
191
+ return;
192
+ }
193
+ const ok = await this.refresh();
194
+ if (!ok) this.setStatus("unauthenticated");
195
+ }
196
+ /**
197
+ * Single-flight token refresh, coordinated across tabs via BroadcastChannel.
198
+ *
199
+ * Per-tab: concurrent callers share the same `refreshPromise`.
200
+ * Cross-tab: a tab that wants to refresh first broadcasts a `refresh:claim`
201
+ * with its tabId + timestamp. If it has already received a competing claim
202
+ * with an earlier timestamp (or lower tabId on tie), it waits for that
203
+ * tab's `refresh:done` message instead of issuing its own HTTP request.
204
+ * This prevents two tabs racing on rotating refresh tokens, which would
205
+ * invalidate the session.
206
+ */
207
+ refresh() {
208
+ if (this.refreshPromise) return this.refreshPromise;
209
+ this.refreshPromise = this.runRefresh().finally(() => {
210
+ this.refreshPromise = null;
211
+ });
212
+ return this.refreshPromise;
213
+ }
214
+ async runRefresh() {
215
+ const myClaim = { source: this.tabId, ts: Date.now() };
216
+ if (this.channel) {
217
+ this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
218
+ await new Promise((r) => setTimeout(r, 25));
219
+ const foreign = this.foreignClaim;
220
+ if (foreign && this.claimWins(foreign, myClaim)) {
221
+ return this.waitForForeignRefresh();
222
+ }
223
+ }
224
+ try {
225
+ const refreshToken = await Promise.resolve(this.tokenStore.read());
226
+ const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
227
+ method: "POST",
228
+ credentials: "include",
229
+ headers: { "Content-Type": "application/json" },
230
+ body: JSON.stringify(refreshToken ? { refreshToken } : {})
231
+ });
232
+ const body = await res.json().catch(() => ({}));
233
+ const data = body.data;
234
+ if (!res.ok || !body.success || !data?.accessToken) {
235
+ const err = body.error;
236
+ this.setError({
237
+ code: err?.code ?? "REFRESH_FAILED",
238
+ message: err?.message ?? `Refresh failed with status ${res.status}`
239
+ });
240
+ this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
241
+ return false;
242
+ }
243
+ if (data.refreshToken) {
244
+ await Promise.resolve(this.tokenStore.write(data.refreshToken));
245
+ }
246
+ this.applyAccessToken(data.accessToken);
247
+ this.broadcast("session:refresh");
248
+ this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
249
+ return true;
250
+ } catch (err) {
251
+ this.setError({
252
+ code: "NETWORK_ERROR",
253
+ message: err instanceof Error ? err.message : "Refresh request failed"
254
+ });
255
+ this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
256
+ return false;
257
+ } finally {
258
+ this.foreignClaim = null;
259
+ }
260
+ }
261
+ claimWins(foreign, mine) {
262
+ if (foreign.ts < mine.ts) return true;
263
+ if (foreign.ts > mine.ts) return false;
264
+ return foreign.source < mine.source;
265
+ }
266
+ waitForForeignRefresh() {
267
+ return new Promise((resolve) => {
268
+ let settled = false;
269
+ const finish = (ok) => {
270
+ if (settled) return;
271
+ settled = true;
272
+ const idx = this.remoteRefreshWaiters.indexOf(finish);
273
+ if (idx >= 0) this.remoteRefreshWaiters.splice(idx, 1);
274
+ resolve(ok);
275
+ };
276
+ this.remoteRefreshWaiters.push(finish);
277
+ setTimeout(() => finish(this.snapshot.status === "authenticated"), this.crossTabLockTimeoutMs);
278
+ });
279
+ }
280
+ /**
281
+ * Apply an access token (e.g. fresh from a callback exchange) to the
282
+ * session and notify subscribers and other tabs.
283
+ */
284
+ applyAccessToken(accessToken, refreshToken) {
285
+ const claims = decodeClaims(accessToken);
286
+ const user = claimsToSessionUser(claims);
287
+ if (refreshToken) {
288
+ void Promise.resolve(this.tokenStore.write(refreshToken));
289
+ }
290
+ this.update({
291
+ status: user ? "authenticated" : "unauthenticated",
292
+ accessToken,
293
+ user,
294
+ claims,
295
+ tenantId: claims?.tenantId ?? this.key.tenantId,
296
+ error: null,
297
+ version: this.snapshot.version + 1
298
+ });
299
+ this.scheduleProactiveRefresh();
300
+ this.broadcast("session:update");
301
+ }
302
+ /**
303
+ * Returns a valid access token, refreshing once if it is expired or about
304
+ * to expire. Resolves to `null` if the session can no longer be revived.
305
+ */
306
+ async getToken() {
307
+ if (this.snapshot.accessToken && !this.isTokenExpiringSoon(this.snapshot.accessToken)) {
308
+ return this.snapshot.accessToken;
309
+ }
310
+ const ok = await this.refresh();
311
+ return ok ? this.snapshot.accessToken : null;
312
+ }
313
+ /**
314
+ * Browser-safe HTTP wrapper. Attaches the current access token and retries
315
+ * once on 401 (refreshing in between). Rejects with an `IQAuthError` on
316
+ * the second 401 — the caller is then responsible for redirecting to
317
+ * sign-in (typically by letting the unauthenticated state surface through
318
+ * `<SignedOut/>`).
319
+ */
320
+ async fetch(input, init = {}) {
321
+ const exec = async (token2) => {
322
+ const headers = new Headers(init.headers || {});
323
+ if (token2) headers.set("Authorization", `Bearer ${token2}`);
324
+ return this.fetchImpl(input, {
325
+ ...init,
326
+ headers,
327
+ credentials: init.credentials ?? "include"
328
+ });
329
+ };
330
+ let token = await this.getToken();
331
+ let res = await exec(token);
332
+ if (res.status !== 401) return res;
333
+ const refreshed = await this.refresh();
334
+ if (!refreshed) {
335
+ this.signOutLocal("unauthenticated");
336
+ throw new IQAuthError(
337
+ "TOKEN_EXPIRED",
338
+ "Session refresh failed; user must sign in again",
339
+ 401
340
+ );
341
+ }
342
+ token = this.snapshot.accessToken;
343
+ res = await exec(token);
344
+ if (res.status === 401) {
345
+ this.signOutLocal("unauthenticated");
346
+ throw new IQAuthError(
347
+ "TOKEN_EXPIRED",
348
+ "Authenticated request failed twice with 401; aborting",
349
+ 401
350
+ );
351
+ }
352
+ return res;
353
+ }
354
+ /**
355
+ * Clear the session locally and broadcast a sign-out to other tabs. Does
356
+ * not call the server — callers (e.g. signOut helper) are responsible for
357
+ * the server-side logout request.
358
+ */
359
+ signOutLocal(status = "unauthenticated") {
360
+ void Promise.resolve(this.tokenStore.clear());
361
+ if (this.proactiveTimer) {
362
+ clearTimeout(this.proactiveTimer);
363
+ this.proactiveTimer = null;
364
+ }
365
+ this.update({
366
+ status,
367
+ accessToken: null,
368
+ user: null,
369
+ claims: null,
370
+ tenantId: null,
371
+ error: null,
372
+ version: this.snapshot.version + 1
373
+ });
374
+ this.broadcast("session:signout");
375
+ }
376
+ destroy() {
377
+ if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
378
+ this.proactiveTimer = null;
379
+ if (this.channel) {
380
+ try {
381
+ this.channel.close();
382
+ } catch {
383
+ }
384
+ }
385
+ this.channel = null;
386
+ this.listeners.clear();
387
+ }
388
+ // --- internals -----------------------------------------------------------
389
+ setStatus(status) {
390
+ if (this.snapshot.status === status) return;
391
+ this.update({ ...this.snapshot, status, version: this.snapshot.version + 1 });
392
+ }
393
+ setError(error) {
394
+ this.update({ ...this.snapshot, error, version: this.snapshot.version + 1 });
395
+ }
396
+ update(next) {
397
+ this.snapshot = next;
398
+ for (const l of this.listeners) l(next);
399
+ }
400
+ isTokenExpiringSoon(token) {
401
+ const claims = decodeClaims(token);
402
+ if (!claims?.exp) return false;
403
+ return claims.exp - Math.floor(Date.now() / 1e3) < 60;
404
+ }
405
+ scheduleProactiveRefresh() {
406
+ if (!this.proactiveRefresh) return;
407
+ if (this.proactiveTimer) {
408
+ clearTimeout(this.proactiveTimer);
409
+ this.proactiveTimer = null;
410
+ }
411
+ const claims = this.snapshot.claims;
412
+ if (!claims?.exp) return;
413
+ const msUntilRefresh = Math.max(5e3, claims.exp * 1e3 - Date.now() - 6e4);
414
+ this.proactiveTimer = setTimeout(() => {
415
+ void this.refresh();
416
+ }, msUntilRefresh);
417
+ }
418
+ broadcast(type) {
419
+ this.broadcastEnvelope({ type, source: this.tabId, ts: Date.now(), payload: this.snapshot });
420
+ }
421
+ broadcastEnvelope(env) {
422
+ if (!this.channel) return;
423
+ try {
424
+ this.channel.postMessage(env);
425
+ } catch {
426
+ }
427
+ }
428
+ onBroadcast(env) {
429
+ if (!env || env.source === this.tabId) return;
430
+ if (env.type === "refresh:claim") {
431
+ this.foreignClaim = { source: env.source, ts: env.ts };
432
+ return;
433
+ }
434
+ if (env.type === "refresh:done") {
435
+ const ok = env.ok ?? false;
436
+ const waiters = this.remoteRefreshWaiters;
437
+ this.remoteRefreshWaiters = [];
438
+ for (const w of waiters) w(ok);
439
+ this.foreignClaim = null;
440
+ return;
441
+ }
442
+ if (env.type === "session:signout") {
443
+ this.update({
444
+ status: "unauthenticated",
445
+ accessToken: null,
446
+ user: null,
447
+ claims: null,
448
+ tenantId: null,
449
+ error: null,
450
+ version: this.snapshot.version + 1
451
+ });
452
+ return;
453
+ }
454
+ if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
455
+ this.update({
456
+ ...env.payload,
457
+ version: Math.max(this.snapshot.version, env.payload.version) + 1
458
+ });
459
+ if (this.remoteRefreshWaiters.length) {
460
+ const waiters = this.remoteRefreshWaiters;
461
+ this.remoteRefreshWaiters = [];
462
+ for (const w of waiters) w(true);
463
+ }
464
+ }
465
+ }
466
+ };
467
+
468
+ // src/browser/pkce.ts
469
+ function getCrypto() {
470
+ if (typeof globalThis !== "undefined" && globalThis.crypto) {
471
+ return globalThis.crypto;
472
+ }
473
+ try {
474
+ const nodeCrypto = __require("crypto").webcrypto;
475
+ if (nodeCrypto) return nodeCrypto;
476
+ } catch {
477
+ }
478
+ throw new Error("WebCrypto is not available in this environment");
479
+ }
480
+ function base64UrlEncode(bytes) {
481
+ let bin = "";
482
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
483
+ const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
484
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
485
+ }
486
+ function randomUrlSafe(byteLength = 32) {
487
+ const bytes = new Uint8Array(byteLength);
488
+ getCrypto().getRandomValues(bytes);
489
+ return base64UrlEncode(bytes);
490
+ }
491
+ async function s256Challenge(verifier) {
492
+ const data = new TextEncoder().encode(verifier);
493
+ const digest = await getCrypto().subtle.digest("SHA-256", data);
494
+ return base64UrlEncode(new Uint8Array(digest));
495
+ }
496
+ async function createPkcePair() {
497
+ const codeVerifier = randomUrlSafe(32);
498
+ const codeChallenge = await s256Challenge(codeVerifier);
499
+ const state = randomUrlSafe(16);
500
+ const nonce = randomUrlSafe(16);
501
+ return { codeVerifier, codeChallenge, state, nonce };
502
+ }
503
+
504
+ // src/browser/signIn.ts
505
+ var DEFAULT_SIGN_IN_PATH = "/sign-in";
506
+ var DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
507
+ var DEFAULT_TOKEN_PATH = "/oidc/token";
508
+ var DEFAULT_CALLBACK_PATH = "/auth/callback";
509
+ function defaultRedirectUri() {
510
+ if (typeof window === "undefined") {
511
+ throw new Error("redirectToSignIn requires a browser environment (window)");
512
+ }
513
+ return `${window.location.origin}${DEFAULT_CALLBACK_PATH}`;
514
+ }
515
+ function defaultReturnTo() {
516
+ if (typeof window === "undefined") return "/";
517
+ return window.location.href;
518
+ }
519
+ async function buildSignInUrl(manager, opts = {}) {
520
+ const pkce = await createPkcePair();
521
+ const redirectUri = opts.redirectUri ?? defaultRedirectUri();
522
+ const returnTo = opts.returnTo ?? defaultReturnTo();
523
+ savePkce({
524
+ codeVerifier: pkce.codeVerifier,
525
+ state: pkce.state,
526
+ nonce: pkce.nonce,
527
+ redirectUri,
528
+ appKey: manager.publishableKey.raw,
529
+ returnTo,
530
+ createdAt: Date.now()
531
+ });
532
+ const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.issuerUrl);
533
+ url.searchParams.set("response_type", "code");
534
+ url.searchParams.set("app", manager.appKey);
535
+ url.searchParams.set("publishable_key", manager.publishableKey.raw);
536
+ url.searchParams.set("redirect_uri", redirectUri);
537
+ url.searchParams.set("state", pkce.state);
538
+ url.searchParams.set("nonce", pkce.nonce);
539
+ url.searchParams.set("code_challenge", pkce.codeChallenge);
540
+ url.searchParams.set("code_challenge_method", "S256");
541
+ url.searchParams.set("scope", opts.scope ?? "openid profile email");
542
+ url.searchParams.set("return_to", returnTo);
543
+ return url.toString();
544
+ }
545
+ async function redirectToSignIn(manager, opts = {}) {
546
+ const url = await buildSignInUrl(manager, opts);
547
+ if (typeof window === "undefined") {
548
+ throw new Error("redirectToSignIn requires a browser environment");
549
+ }
550
+ window.location.assign(url);
551
+ }
552
+ async function signIn(manager, opts = {}) {
553
+ return redirectToSignIn(manager, opts);
554
+ }
555
+ async function handleAuthCallback(manager, options = {}) {
556
+ const url = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
557
+ const code = url.searchParams.get("code");
558
+ const state = url.searchParams.get("state");
559
+ const errorParam = url.searchParams.get("error");
560
+ if (errorParam) {
561
+ return { ok: false, returnTo: "/", error: errorParam };
562
+ }
563
+ if (!code || !state) {
564
+ return { ok: false, returnTo: "/", error: "missing_code_or_state" };
565
+ }
566
+ const record = loadPkce(state);
567
+ if (!record) {
568
+ return { ok: false, returnTo: "/", error: "unknown_state" };
569
+ }
570
+ clearPkce(state);
571
+ const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
572
+ if (!fetchImpl) {
573
+ return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
574
+ }
575
+ const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
576
+ const res = await fetchImpl(tokenUrl, {
577
+ method: "POST",
578
+ credentials: "include",
579
+ headers: { "Content-Type": "application/json" },
580
+ body: JSON.stringify({
581
+ grant_type: "authorization_code",
582
+ code,
583
+ redirect_uri: record.redirectUri,
584
+ client_id: manager.appKey,
585
+ code_verifier: record.codeVerifier
586
+ })
587
+ });
588
+ const body = await res.json().catch(() => ({}));
589
+ if (!res.ok) {
590
+ const desc = body.error_description ?? body.error ?? "token_exchange_failed";
591
+ return { ok: false, returnTo: record.returnTo, error: desc };
592
+ }
593
+ const tokens = body;
594
+ if (!tokens.access_token) {
595
+ return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
596
+ }
597
+ if (tokens.refresh_token) {
598
+ setCookie(REFRESH_COOKIE, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
599
+ }
600
+ manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
601
+ return { ok: true, returnTo: record.returnTo };
602
+ }
603
+ async function signOut(manager, opts = {}) {
604
+ if (!opts.localOnly) {
605
+ try {
606
+ const url = `${manager.issuerUrl}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
607
+ await manager.fetch(url, { method: "POST" }).catch(() => void 0);
608
+ } catch {
609
+ }
610
+ }
611
+ clearCookie(REFRESH_COOKIE);
612
+ manager.signOutLocal();
613
+ if (opts.returnTo && typeof window !== "undefined") {
614
+ window.location.assign(opts.returnTo);
615
+ }
616
+ }
617
+
618
+ export {
619
+ REFRESH_COOKIE,
620
+ setCookie,
621
+ getCookie,
622
+ clearCookie,
623
+ SessionManager,
624
+ randomUrlSafe,
625
+ s256Challenge,
626
+ createPkcePair,
627
+ buildSignInUrl,
628
+ redirectToSignIn,
629
+ signIn,
630
+ handleAuthCallback,
631
+ signOut
632
+ };