@iqauth/sdk 2.7.0 → 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.
Files changed (88) hide show
  1. package/dist/browser-session.d.mts +3 -3
  2. package/dist/browser-session.d.ts +3 -3
  3. package/dist/browser-session.js +31 -5
  4. package/dist/browser-session.mjs +1 -1
  5. package/dist/browser.d.mts +3 -3
  6. package/dist/browser.d.ts +3 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/browser.mjs +1 -1
  9. package/dist/{chunk-YVALAG3B.mjs → chunk-25SSYDIP.mjs} +1 -1
  10. package/dist/{chunk-RTJAIBXY.mjs → chunk-4V7FKOTG.mjs} +23 -3
  11. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  12. package/dist/chunk-JRDVUWAL.mjs +46 -0
  13. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  14. package/dist/{chunk-PMAFENVI.mjs → chunk-VYQ3ETCK.mjs} +27 -12
  15. package/dist/{chunk-RR2MGPTK.mjs → chunk-WHT6WKTY.mjs} +539 -83
  16. package/dist/{chunk-RUJXRTEW.mjs → chunk-WSH4SW7F.mjs} +122 -8
  17. package/dist/{chunk-JXQI62A7.mjs → chunk-ZLJPABB7.mjs} +31 -5
  18. package/dist/{client-BGFnBpfc.d.mts → client-D8L-PaWr.d.mts} +14 -4
  19. package/dist/{client-CDQ21LvW.d.ts → client-DkPL0EPZ.d.ts} +14 -4
  20. package/dist/{express-Piv2WhWM.d.ts → express-Budysq4h.d.ts} +2 -2
  21. package/dist/{express-CVNQEkOr.d.mts → express-DDTA3qV1.d.mts} +2 -2
  22. package/dist/express.d.mts +5 -5
  23. package/dist/express.d.ts +5 -5
  24. package/dist/express.js +217 -36
  25. package/dist/express.mjs +38 -26
  26. package/dist/fastify.d.mts +10 -2
  27. package/dist/fastify.d.ts +10 -2
  28. package/dist/fastify.js +260 -16
  29. package/dist/fastify.mjs +80 -5
  30. package/dist/hono.d.mts +10 -2
  31. package/dist/hono.d.ts +10 -2
  32. package/dist/hono.js +240 -16
  33. package/dist/hono.mjs +60 -5
  34. package/dist/{index-5KSZEnDe.d.ts → index-Cko-d5po.d.mts} +227 -5
  35. package/dist/{index-CKoZHAoc.d.mts → index-RNqwEcmY.d.ts} +227 -5
  36. package/dist/index.d.mts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +149 -26
  39. package/dist/index.mjs +5 -5
  40. package/dist/locales.d.mts +1 -1
  41. package/dist/locales.d.ts +1 -1
  42. package/dist/locales.js +36 -0
  43. package/dist/locales.mjs +1 -1
  44. package/dist/mobile.d.mts +3 -3
  45. package/dist/mobile.d.ts +3 -3
  46. package/dist/mobile.js +31 -5
  47. package/dist/mobile.mjs +1 -1
  48. package/dist/next.d.mts +10 -2
  49. package/dist/next.d.ts +10 -2
  50. package/dist/next.js +212 -11
  51. package/dist/next.mjs +62 -4
  52. package/dist/{provisioningBridge-M5G47LWO.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  53. package/dist/{provisioningBridge-CGpMRie4.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  54. package/dist/react-permissions.d.mts +4 -4
  55. package/dist/react-permissions.d.ts +4 -4
  56. package/dist/react-permissions.mjs +4 -3
  57. package/dist/react.d.mts +4 -4
  58. package/dist/react.d.ts +4 -4
  59. package/dist/react.js +570 -41
  60. package/dist/react.mjs +19 -5
  61. package/dist/server/handlers.d.mts +56 -5
  62. package/dist/server/handlers.d.ts +56 -5
  63. package/dist/server/handlers.js +123 -8
  64. package/dist/server/handlers.mjs +3 -1
  65. package/dist/server.d.mts +28 -8
  66. package/dist/server.d.ts +28 -8
  67. package/dist/server.js +176 -14
  68. package/dist/server.mjs +9 -4
  69. package/dist/service.d.mts +3 -3
  70. package/dist/service.d.ts +3 -3
  71. package/dist/service.js +31 -5
  72. package/dist/service.mjs +1 -1
  73. package/dist/{signIn-T-CZ6t6r.d.mts → signIn-CReqfXsh.d.mts} +18 -1
  74. package/dist/{signIn-BLFnz8SV.d.ts → signIn-Cfa1GTpO.d.ts} +18 -1
  75. package/dist/{tokens-Bqhmqq_R.d.ts → tokens-9F6ETrzk.d.ts} +1 -1
  76. package/dist/{tokens-CITeoG6P.d.mts → tokens-B06VtvUi.d.mts} +1 -1
  77. package/dist/{types-XOV9XPVi.d.mts → types-Bn8O-OEd.d.mts} +66 -2
  78. package/dist/{types-XOV9XPVi.d.ts → types-Bn8O-OEd.d.ts} +66 -2
  79. package/dist/{types-BdQ2lqfT.d.mts → types-DnU2LhXR.d.mts} +6 -0
  80. package/dist/{types-BdQ2lqfT.d.ts → types-DnU2LhXR.d.ts} +6 -0
  81. package/dist/webhooks.d.mts +22 -9
  82. package/dist/webhooks.d.ts +22 -9
  83. package/dist/webhooks.js +27 -12
  84. package/dist/webhooks.mjs +1 -1
  85. package/dist/ws.d.mts +2 -2
  86. package/dist/ws.d.ts +2 -2
  87. package/docs/guides/invitations.md +65 -0
  88. package/package.json +7 -2
package/dist/server.js CHANGED
@@ -35,6 +35,7 @@ __export(server_exports, {
35
35
  ErrorCodes: () => ErrorCodes,
36
36
  IQAuthClient: () => IQAuthClient,
37
37
  IQAuthError: () => IQAuthError,
38
+ ProvisioningError: () => ProvisioningError,
38
39
  ServerIQAuthClient: () => ServerIQAuthClient,
39
40
  buildUserinfoResponse: () => buildUserinfoResponse,
40
41
  createDrizzleLinkAdapter: () => createDrizzleLinkAdapter,
@@ -347,17 +348,27 @@ function parseLoginResponse(data, browserSessionMode) {
347
348
  tenants: data.tenants
348
349
  };
349
350
  }
351
+ if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
352
+ return {
353
+ status: "scope_selection",
354
+ scopeSelectionToken: data.scopeSelectionToken,
355
+ tenantId: data.tenantId,
356
+ scopes: data.scopes
357
+ };
358
+ }
350
359
  throw new Error("Unexpected login response shape");
351
360
  }
352
361
  var AuthModule = class {
353
362
  constructor(http) {
354
363
  this.http = http;
355
364
  }
356
- async login(email, password) {
365
+ async login(email, password, opts) {
366
+ const body = { email, password };
367
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
357
368
  const data = await this.http.request(
358
369
  "POST",
359
370
  "/api/v1/auth/login",
360
- { email, password },
371
+ body,
361
372
  { skipAutoRefresh: true }
362
373
  );
363
374
  return parseLoginResponse(data, this.http.isBrowserSession());
@@ -395,13 +406,29 @@ var AuthModule = class {
395
406
  method
396
407
  }, { skipAutoRefresh: true });
397
408
  }
398
- async selectTenant(tenantSelectionToken, tenantId) {
409
+ async selectTenant(tenantSelectionToken, tenantId, opts) {
410
+ const body = { tenantSelectionToken, tenantId };
411
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
399
412
  const data = await this.http.request(
400
413
  "POST",
401
414
  "/api/v1/auth/select-tenant",
415
+ body,
416
+ { skipAutoRefresh: true }
417
+ );
418
+ return parseLoginResponse(data, this.http.isBrowserSession());
419
+ }
420
+ /**
421
+ * Task #171 — redeem a scope-selection token + chosen membership for a
422
+ * real authenticated session. `membershipId` must be one of the scopes
423
+ * returned in the prior `scope_selection` envelope.
424
+ */
425
+ async selectScope(scopeSelectionToken, membershipId) {
426
+ const data = await this.http.request(
427
+ "POST",
428
+ "/api/v1/auth/select-scope",
402
429
  {
403
- tenantSelectionToken,
404
- tenantId
430
+ scopeSelectionToken,
431
+ membershipId
405
432
  },
406
433
  { skipAutoRefresh: true }
407
434
  );
@@ -2240,7 +2267,11 @@ async function buildUserinfoResponse(claims, opts = {}) {
2240
2267
  tenantId: claims.tenantId,
2241
2268
  vendorId: claims.vendorId,
2242
2269
  roles: claims.roles ?? [],
2243
- entitlements: claims.entitlements ?? []
2270
+ entitlements: claims.entitlements ?? [],
2271
+ // Task #171 — project the active source/client scope onto the userinfo
2272
+ // payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
2273
+ // expose it without consumers having to re-decode the JWT.
2274
+ ...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
2244
2275
  };
2245
2276
  const enriched = opts.enrich ? await opts.enrich(claims) : null;
2246
2277
  const user = enriched ? { ...baseUser, ...enriched } : baseUser;
@@ -2285,19 +2316,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
2285
2316
  }
2286
2317
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
2287
2318
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
2319
+ function assertCookiePrefixInvariants(name, secure, path, domain) {
2320
+ if (name.startsWith("__Host-")) {
2321
+ if (!secure) {
2322
+ throw new IQAuthError(
2323
+ "config_invalid",
2324
+ `Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2325
+ );
2326
+ }
2327
+ if (path !== "/") {
2328
+ throw new IQAuthError(
2329
+ "config_invalid",
2330
+ `Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
2331
+ );
2332
+ }
2333
+ if (domain) {
2334
+ throw new IQAuthError(
2335
+ "config_invalid",
2336
+ `Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
2337
+ );
2338
+ }
2339
+ } else if (name.startsWith("__Secure-") && !secure) {
2340
+ throw new IQAuthError(
2341
+ "config_invalid",
2342
+ `Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2343
+ );
2344
+ }
2345
+ }
2288
2346
  function resolve(config) {
2289
2347
  const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
2290
2348
  const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
2349
+ maybeWarnDefaultSignoutRegistry(config);
2350
+ const secure = config.secure ?? true;
2351
+ if (config.secure === false && config.allowInsecureCookies !== true) {
2352
+ throw new IQAuthError(
2353
+ "config_invalid",
2354
+ "Refusing to issue auth cookies with secure:false \u2014 this exposes session cookies over plaintext HTTP. For local HTTP development, set allowInsecureCookies:true to acknowledge the risk. Production MUST use HTTPS with secure cookies."
2355
+ );
2356
+ }
2357
+ const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
2358
+ const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
2359
+ const stateCookieName = config.stateCookieName ?? "iqauth_state";
2360
+ const cookiePath = config.cookiePath ?? "/";
2361
+ const cookieDomain = config.cookieDomain;
2362
+ for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
2363
+ assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
2364
+ }
2291
2365
  return {
2292
2366
  publishableKey: config.publishableKey,
2293
2367
  secretKey: config.secretKey,
2294
2368
  issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
2295
- accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
2296
- refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
2297
- cookieDomain: config.cookieDomain,
2369
+ accessCookieName,
2370
+ refreshCookieName,
2371
+ cookieDomain,
2298
2372
  sameSite: config.sameSite ?? "lax",
2299
- secure: config.secure ?? true,
2300
- cookiePath: config.cookiePath ?? "/",
2373
+ secure,
2374
+ cookiePath,
2301
2375
  tokenPath: config.tokenPath ?? "/oidc/token",
2302
2376
  refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
2303
2377
  logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
@@ -2310,9 +2384,19 @@ function resolve(config) {
2310
2384
  debug: config.debug,
2311
2385
  onTimingEvent: config.onTimingEvent,
2312
2386
  signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
2313
- signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
2387
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
2388
+ requireOAuthState: config.requireOAuthState ?? true,
2389
+ stateCookieName: config.stateCookieName ?? "iqauth_state"
2314
2390
  };
2315
2391
  }
2392
+ function timingSafeEqualStr(a, b) {
2393
+ const len = Math.max(a.length, b.length);
2394
+ let diff = a.length ^ b.length;
2395
+ for (let i = 0; i < len; i++) {
2396
+ diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
2397
+ }
2398
+ return diff === 0;
2399
+ }
2316
2400
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
2317
2401
  return {
2318
2402
  name,
@@ -2331,6 +2415,9 @@ function clearCookies(cfg) {
2331
2415
  { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
2332
2416
  ];
2333
2417
  }
2418
+ function clearStateCookie(cfg) {
2419
+ return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
2420
+ }
2334
2421
  var DEFAULT_SIGNOUT_TTL_MS = 6e4;
2335
2422
  var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
2336
2423
  function pruneInMemoryMarkers(now) {
@@ -2356,6 +2443,15 @@ var defaultSignoutRegistry = {
2356
2443
  return true;
2357
2444
  }
2358
2445
  };
2446
+ var warnedDefaultSignoutRegistry = false;
2447
+ function maybeWarnDefaultSignoutRegistry(config) {
2448
+ if (warnedDefaultSignoutRegistry) return;
2449
+ if (config.signoutRegistry) return;
2450
+ warnedDefaultSignoutRegistry = true;
2451
+ console.warn(
2452
+ "[IQAuth] Using the in-memory signout registry (process-local). Signout idempotency is NOT shared across instances \u2014 in a multi-replica deployment a /refresh racing a /signout on another replica can reissue cookies after sign-out. Plug a shared backend (e.g. Redis) into IQAuthHelperConfig.signoutRegistry to fix this and silence this warning."
2453
+ );
2454
+ }
2359
2455
  function serializeCookie(d) {
2360
2456
  const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
2361
2457
  parts.push(`Path=${d.path}`);
@@ -2378,6 +2474,23 @@ async function handleCallback(config, input) {
2378
2474
  cookies: []
2379
2475
  };
2380
2476
  }
2477
+ const provided = input.state;
2478
+ const expected = input.expectedState;
2479
+ const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
2480
+ if (!stateOk) {
2481
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
2482
+ return {
2483
+ status: 400,
2484
+ body: {
2485
+ success: false,
2486
+ error: {
2487
+ code: "STATE_MISMATCH",
2488
+ message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
2489
+ }
2490
+ },
2491
+ cookies: [clearStateCookie(cfg)]
2492
+ };
2493
+ }
2381
2494
  if (!cfg.secretKey) {
2382
2495
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
2383
2496
  return {
@@ -2416,6 +2529,26 @@ async function handleCallback(config, input) {
2416
2529
  cookies: []
2417
2530
  };
2418
2531
  }
2532
+ try {
2533
+ await getTokensFor(cfg.issuer).verify(json.access_token, {
2534
+ issuer: cfg.issuer,
2535
+ ...config.verify
2536
+ });
2537
+ } catch (err) {
2538
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2539
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
2540
+ return {
2541
+ status: 502,
2542
+ body: {
2543
+ success: false,
2544
+ error: {
2545
+ code: "ACCESS_TOKEN_VERIFICATION_FAILED",
2546
+ message: "The issuer returned an access token that failed verification; no session was established."
2547
+ }
2548
+ },
2549
+ cookies: []
2550
+ };
2551
+ }
2419
2552
  const cookies = [];
2420
2553
  cookies.push(
2421
2554
  makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
@@ -2423,6 +2556,7 @@ async function handleCallback(config, input) {
2423
2556
  if (json.refresh_token) {
2424
2557
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
2425
2558
  }
2559
+ cookies.push(clearStateCookie(cfg));
2426
2560
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
2427
2561
  return {
2428
2562
  status: 200,
@@ -2544,7 +2678,10 @@ async function handleUserinfo(config, input) {
2544
2678
  }
2545
2679
  let claims;
2546
2680
  try {
2547
- claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
2681
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
2682
+ issuer: cfg.issuer,
2683
+ ...config.verify
2684
+ });
2548
2685
  } catch (err) {
2549
2686
  const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2550
2687
  const message = err instanceof Error ? err.message : "Access token verification failed";
@@ -2565,6 +2702,13 @@ async function handleUserinfo(config, input) {
2565
2702
  }
2566
2703
 
2567
2704
  // src/server/provisioningBridge.ts
2705
+ var ProvisioningError = class extends Error {
2706
+ constructor(code, message) {
2707
+ super(message);
2708
+ this.name = "ProvisioningError";
2709
+ this.code = code;
2710
+ }
2711
+ };
2568
2712
  function defaultIsUniqueViolation(err) {
2569
2713
  if (!err || typeof err !== "object") return false;
2570
2714
  const e = err;
@@ -2576,6 +2720,16 @@ function defaultIsUniqueViolation(err) {
2576
2720
  function createProvisioningBridge(options) {
2577
2721
  const { storage } = options;
2578
2722
  const isUniqueViolation = options.isUniqueViolation ?? defaultIsUniqueViolation;
2723
+ const allowUnverifiedEmailAdopt = options.allowUnverifiedEmailAdopt === true;
2724
+ const emailVerified = (claims) => claims.email_verified === true;
2725
+ const assertAdoptAllowed = (claims) => {
2726
+ if (!allowUnverifiedEmailAdopt && !emailVerified(claims)) {
2727
+ throw new ProvisioningError(
2728
+ "UNVERIFIED_EMAIL_ADOPT_REFUSED",
2729
+ "Refusing to adopt a pre-existing local account from an unverified email (claims.email_verified !== true). Set allowUnverifiedEmailAdopt:true only if your issuer is trusted to never emit unverified emails for adoption."
2730
+ );
2731
+ }
2732
+ };
2579
2733
  const roleOf = (claims) => {
2580
2734
  try {
2581
2735
  return options.roleMapper?.(claims) ?? null;
@@ -2592,6 +2746,7 @@ function createProvisioningBridge(options) {
2592
2746
  if (claims.email) {
2593
2747
  const byEmail = await storage.findByEmail(claims.email);
2594
2748
  if (byEmail) {
2749
+ assertAdoptAllowed(claims);
2595
2750
  if (storage.adoptByEmail) {
2596
2751
  const adopted = await storage.adoptByEmail(byEmail, claims, roleOf(claims));
2597
2752
  return { user: adopted, claims, created: false, adopted: true };
@@ -2607,7 +2762,10 @@ function createProvisioningBridge(options) {
2607
2762
  if (after) return { user: after, claims, created: false, adopted: false };
2608
2763
  if (claims.email) {
2609
2764
  const byEmail = await storage.findByEmail(claims.email);
2610
- if (byEmail) return { user: byEmail, claims, created: false, adopted: true };
2765
+ if (byEmail) {
2766
+ assertAdoptAllowed(claims);
2767
+ return { user: byEmail, claims, created: false, adopted: true };
2768
+ }
2611
2769
  }
2612
2770
  throw err;
2613
2771
  }
@@ -2639,6 +2797,9 @@ async function linkLocalUserToIqAuthSub(options) {
2639
2797
  if (row.iqauthSub === claims.sub) {
2640
2798
  return { status: "already_linked", userId: row.id };
2641
2799
  }
2800
+ if (claims.email_verified !== true && options.allowUnverifiedEmail !== true) {
2801
+ return { status: "conflict", userId: row.id, reason: "unverified_email" };
2802
+ }
2642
2803
  const wrote = await tx.setIqAuthSub(row.id, claims.sub);
2643
2804
  if (wrote === false) {
2644
2805
  return { status: "conflict", userId: row.id, reason: "different_sub" };
@@ -2710,6 +2871,7 @@ function createServerClient(config) {
2710
2871
  ErrorCodes,
2711
2872
  IQAuthClient,
2712
2873
  IQAuthError,
2874
+ ProvisioningError,
2713
2875
  ServerIQAuthClient,
2714
2876
  buildUserinfoResponse,
2715
2877
  createDrizzleLinkAdapter,
package/dist/server.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import {
2
+ ProvisioningError,
2
3
  createProvisioningBridge
3
- } from "./chunk-SL3KRS4W.mjs";
4
+ } from "./chunk-CIJORODR.mjs";
4
5
  import {
5
6
  DEFAULT_ACCESS_COOKIE,
6
7
  DEFAULT_REFRESH_COOKIE,
7
8
  iqAuthMiddleware
8
- } from "./chunk-YVALAG3B.mjs";
9
+ } from "./chunk-25SSYDIP.mjs";
9
10
  import {
10
11
  buildUserinfoResponse,
11
12
  handleCallback,
@@ -13,11 +14,11 @@ import {
13
14
  handleSignout,
14
15
  handleUserinfo,
15
16
  serializeCookie
16
- } from "./chunk-RUJXRTEW.mjs";
17
+ } from "./chunk-WSH4SW7F.mjs";
17
18
  import "./chunk-HVHNYPDC.mjs";
18
19
  import {
19
20
  IQAuthClient
20
- } from "./chunk-JXQI62A7.mjs";
21
+ } from "./chunk-ZLJPABB7.mjs";
21
22
  import "./chunk-NUO2I65G.mjs";
22
23
  import {
23
24
  ErrorCodes,
@@ -49,6 +50,9 @@ async function linkLocalUserToIqAuthSub(options) {
49
50
  if (row.iqauthSub === claims.sub) {
50
51
  return { status: "already_linked", userId: row.id };
51
52
  }
53
+ if (claims.email_verified !== true && options.allowUnverifiedEmail !== true) {
54
+ return { status: "conflict", userId: row.id, reason: "unverified_email" };
55
+ }
52
56
  const wrote = await tx.setIqAuthSub(row.id, claims.sub);
53
57
  if (wrote === false) {
54
58
  return { status: "conflict", userId: row.id, reason: "different_sub" };
@@ -119,6 +123,7 @@ export {
119
123
  ErrorCodes,
120
124
  IQAuthClient,
121
125
  IQAuthError,
126
+ ProvisioningError,
122
127
  ServerIQAuthClient,
123
128
  buildUserinfoResponse,
124
129
  createDrizzleLinkAdapter,
@@ -1,7 +1,7 @@
1
- import { I as IQAuthClient } from './client-BGFnBpfc.mjs';
2
- import { f as IQAuthTokenClientConfig } from './types-XOV9XPVi.mjs';
1
+ import { I as IQAuthClient } from './client-D8L-PaWr.mjs';
2
+ import { f as IQAuthTokenClientConfig } from './types-Bn8O-OEd.mjs';
3
3
  export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.mjs';
4
- import './tokens-CITeoG6P.mjs';
4
+ import './tokens-B06VtvUi.mjs';
5
5
 
6
6
  declare class ServiceIQAuthClient extends IQAuthClient {
7
7
  constructor(config: IQAuthTokenClientConfig);
package/dist/service.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { I as IQAuthClient } from './client-CDQ21LvW.js';
2
- import { f as IQAuthTokenClientConfig } from './types-XOV9XPVi.js';
1
+ import { I as IQAuthClient } from './client-DkPL0EPZ.js';
2
+ import { f as IQAuthTokenClientConfig } from './types-Bn8O-OEd.js';
3
3
  export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.js';
4
- import './tokens-Bqhmqq_R.js';
4
+ import './tokens-9F6ETrzk.js';
5
5
 
6
6
  declare class ServiceIQAuthClient extends IQAuthClient {
7
7
  constructor(config: IQAuthTokenClientConfig);
package/dist/service.js CHANGED
@@ -335,17 +335,27 @@ function parseLoginResponse(data, browserSessionMode) {
335
335
  tenants: data.tenants
336
336
  };
337
337
  }
338
+ if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
339
+ return {
340
+ status: "scope_selection",
341
+ scopeSelectionToken: data.scopeSelectionToken,
342
+ tenantId: data.tenantId,
343
+ scopes: data.scopes
344
+ };
345
+ }
338
346
  throw new Error("Unexpected login response shape");
339
347
  }
340
348
  var AuthModule = class {
341
349
  constructor(http) {
342
350
  this.http = http;
343
351
  }
344
- async login(email, password) {
352
+ async login(email, password, opts) {
353
+ const body = { email, password };
354
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
345
355
  const data = await this.http.request(
346
356
  "POST",
347
357
  "/api/v1/auth/login",
348
- { email, password },
358
+ body,
349
359
  { skipAutoRefresh: true }
350
360
  );
351
361
  return parseLoginResponse(data, this.http.isBrowserSession());
@@ -383,13 +393,29 @@ var AuthModule = class {
383
393
  method
384
394
  }, { skipAutoRefresh: true });
385
395
  }
386
- async selectTenant(tenantSelectionToken, tenantId) {
396
+ async selectTenant(tenantSelectionToken, tenantId, opts) {
397
+ const body = { tenantSelectionToken, tenantId };
398
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
387
399
  const data = await this.http.request(
388
400
  "POST",
389
401
  "/api/v1/auth/select-tenant",
402
+ body,
403
+ { skipAutoRefresh: true }
404
+ );
405
+ return parseLoginResponse(data, this.http.isBrowserSession());
406
+ }
407
+ /**
408
+ * Task #171 — redeem a scope-selection token + chosen membership for a
409
+ * real authenticated session. `membershipId` must be one of the scopes
410
+ * returned in the prior `scope_selection` envelope.
411
+ */
412
+ async selectScope(scopeSelectionToken, membershipId) {
413
+ const data = await this.http.request(
414
+ "POST",
415
+ "/api/v1/auth/select-scope",
390
416
  {
391
- tenantSelectionToken,
392
- tenantId
417
+ scopeSelectionToken,
418
+ membershipId
393
419
  },
394
420
  { skipAutoRefresh: true }
395
421
  );
package/dist/service.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IQAuthClient
3
- } from "./chunk-JXQI62A7.mjs";
3
+ } from "./chunk-ZLJPABB7.mjs";
4
4
  import "./chunk-NUO2I65G.mjs";
5
5
  import {
6
6
  ErrorCodes,
@@ -1,5 +1,5 @@
1
1
  import { c as ParsedPublishableKey } from './publishableKey-f2kq-rKw.mjs';
2
- import { J as JwtClaims, S as SessionUser } from './types-XOV9XPVi.mjs';
2
+ import { J as JwtClaims, S as SessionUser } from './types-Bn8O-OEd.mjs';
3
3
 
4
4
  /**
5
5
  * SessionManager — core browser-side session state.
@@ -253,6 +253,23 @@ declare class SessionManager {
253
253
  * session and notify subscribers and other tabs.
254
254
  */
255
255
  applyAccessToken(accessToken: string, refreshToken?: string): void;
256
+ /**
257
+ * Task #197 — Adopt an access token that the server has already minted
258
+ * for us (e.g. from `POST /api/v1/auth/switch-scope`) without contacting
259
+ * the issuer. Swaps the in-memory token, re-decodes claims, bumps
260
+ * `version`, schedules proactive refresh, and broadcasts a
261
+ * `session:update` to peer tabs.
262
+ *
263
+ * This is the safe path for any server endpoint that returns a fresh
264
+ * access token in its JSON body: we want the new claims (scope, roles,
265
+ * etc.) to take effect immediately, even if the refresh-cookie round-trip
266
+ * would have failed (network blip, rate limit, signout race). When the
267
+ * server also rotated the refresh token, pass it via
268
+ * `opts.refreshToken` so the cookie stays aligned.
269
+ */
270
+ adoptAccessToken(accessToken: string, opts?: {
271
+ refreshToken?: string;
272
+ }): void;
256
273
  /**
257
274
  * Returns a valid access token, refreshing once if it is expired or about
258
275
  * to expire. Resolves to `null` if the session can no longer be revived.
@@ -1,5 +1,5 @@
1
1
  import { c as ParsedPublishableKey } from './publishableKey-f2kq-rKw.js';
2
- import { J as JwtClaims, S as SessionUser } from './types-XOV9XPVi.js';
2
+ import { J as JwtClaims, S as SessionUser } from './types-Bn8O-OEd.js';
3
3
 
4
4
  /**
5
5
  * SessionManager — core browser-side session state.
@@ -253,6 +253,23 @@ declare class SessionManager {
253
253
  * session and notify subscribers and other tabs.
254
254
  */
255
255
  applyAccessToken(accessToken: string, refreshToken?: string): void;
256
+ /**
257
+ * Task #197 — Adopt an access token that the server has already minted
258
+ * for us (e.g. from `POST /api/v1/auth/switch-scope`) without contacting
259
+ * the issuer. Swaps the in-memory token, re-decodes claims, bumps
260
+ * `version`, schedules proactive refresh, and broadcasts a
261
+ * `session:update` to peer tabs.
262
+ *
263
+ * This is the safe path for any server endpoint that returns a fresh
264
+ * access token in its JSON body: we want the new claims (scope, roles,
265
+ * etc.) to take effect immediately, even if the refresh-cookie round-trip
266
+ * would have failed (network blip, rate limit, signout race). When the
267
+ * server also rotated the refresh token, pass it via
268
+ * `opts.refreshToken` so the cookie stays aligned.
269
+ */
270
+ adoptAccessToken(accessToken: string, opts?: {
271
+ refreshToken?: string;
272
+ }): void;
256
273
  /**
257
274
  * Returns a valid access token, refreshing once if it is expired or about
258
275
  * to expire. Resolves to `null` if the session can no longer be revived.
@@ -1,4 +1,4 @@
1
- import { h as IQAuthClaims, J as JwtClaims } from './types-XOV9XPVi.js';
1
+ import { h as IQAuthClaims, J as JwtClaims } from './types-Bn8O-OEd.js';
2
2
 
3
3
  /**
4
4
  * SOURCE REFS:
@@ -1,4 +1,4 @@
1
- import { h as IQAuthClaims, J as JwtClaims } from './types-XOV9XPVi.mjs';
1
+ import { h as IQAuthClaims, J as JwtClaims } from './types-Bn8O-OEd.mjs';
2
2
 
3
3
  /**
4
4
  * SOURCE REFS:
@@ -176,6 +176,14 @@ interface SessionUser {
176
176
  givenName?: string;
177
177
  familyName?: string;
178
178
  locale?: string;
179
+ /**
180
+ * Task #171 — When the active session was minted under a source/client
181
+ * scope (either via a scope_hint, single-resolved scope, or post-pick),
182
+ * the access token carries a `scopeContext` claim and we project it here
183
+ * so SDK consumers (`useUser()`, framework adapters) can read the active
184
+ * scope without re-parsing the JWT. Absent for tenant-wide sessions.
185
+ */
186
+ scopeContext?: ScopeContext;
179
187
  }
180
188
  interface Tenant {
181
189
  tenantId: string;
@@ -201,6 +209,24 @@ interface SessionAuthenticatedLoginResult {
201
209
  authMode: "session";
202
210
  user: SessionUser;
203
211
  }
212
+ /**
213
+ * Task #171 — A user can have multiple source/client scoped memberships in
214
+ * the same tenant with no tenant-wide role. When login resolves to that
215
+ * state the backend returns a short-lived `scopeSelectionToken` plus the
216
+ * list of choices; the caller redeems it via `AuthModule.selectScope`.
217
+ */
218
+ interface ScopeChoice {
219
+ membershipId: string;
220
+ scopeType: "vendor" | "source" | "client";
221
+ scopeId: string;
222
+ scopeName: string;
223
+ roleName: string;
224
+ }
225
+ /** Task #171 — Optional hint forwarded with login / select-tenant / OIDC. */
226
+ interface ScopeHint {
227
+ type: "vendor" | "source" | "client";
228
+ id: string;
229
+ }
204
230
  type LoginResult = TokenAuthenticatedLoginResult | SessionAuthenticatedLoginResult | {
205
231
  status: "mfa_required";
206
232
  mfaChallengeToken: string;
@@ -209,6 +235,11 @@ type LoginResult = TokenAuthenticatedLoginResult | SessionAuthenticatedLoginResu
209
235
  status: "tenant_selection";
210
236
  tenantSelectionToken: string;
211
237
  tenants: Tenant[];
238
+ } | {
239
+ status: "scope_selection";
240
+ scopeSelectionToken: string;
241
+ tenantId: string;
242
+ scopes: ScopeChoice[];
212
243
  };
213
244
  interface Session {
214
245
  id: string;
@@ -718,13 +749,46 @@ interface Invitation {
718
749
  invitedBy: string;
719
750
  expiresAt?: string;
720
751
  createdAt?: string;
752
+ /** Scope the invite grants into ("tenant" | "vendor" | "source" | "client"). */
753
+ scopeType?: string | null;
754
+ /** Scope target id (paired with `scopeType`). */
755
+ scopeId?: string | null;
756
+ /** OIDC client bound for post-accept auto-redirect (paired with `redirectUri`). */
757
+ clientId?: string | null;
758
+ /** Registered redirect URI the new invitee is sent to after account creation. */
759
+ redirectUri?: string | null;
760
+ /** Display name pre-filled on the hosted accept page. */
761
+ inviteeName?: string | null;
721
762
  }
722
763
  interface CreateInviteRequest {
723
764
  email: string;
724
- tenantId: string;
765
+ /**
766
+ * Target tenant. Optional for service (API-key) callers — the backend
767
+ * derives the tenant from the key and rejects a mismatching value. Platform
768
+ * admins may target any tenant.
769
+ */
770
+ tenantId?: string;
725
771
  vendorId?: string;
726
772
  role: string;
727
773
  products?: string[];
774
+ /** Scope to grant into. Must match the backend's accepted values. */
775
+ scopeType?: "tenant" | "vendor" | "source" | "client";
776
+ /** Scope target id (paired with `scopeType`). */
777
+ scopeId?: string;
778
+ /**
779
+ * Opt-in auto-redirect after the invitee creates their account. `clientId`
780
+ * and `redirectUri` are all-or-nothing — pass both or neither. The backend
781
+ * validates that `clientId` is an active OIDC client in the invite's tenant
782
+ * and that `redirectUri` is in that client's registered allowlist, then mints
783
+ * an OIDC authorization code and 302s the brand-new invitee to
784
+ * `${redirectUri}?code=…&state=…`. Point this at your app's
785
+ * `/api/iqauth/callback` so the framework adapter signs the user in on first
786
+ * paint. Existing-user accepts do NOT auto-redirect (see the integration guide).
787
+ */
788
+ clientId?: string;
789
+ redirectUri?: string;
790
+ /** Optional display name to pre-fill on the hosted accept page. Never used for auth. */
791
+ inviteeName?: string;
728
792
  }
729
793
  interface InviteValidation {
730
794
  valid: boolean;
@@ -992,4 +1056,4 @@ interface BackupCodeCountResult {
992
1056
  remainingBackupCodes: number;
993
1057
  }
994
1058
 
995
- export type { AppManifest as $, ApiSuccessResponse as A, BrandingConfig as B, CreateTenantRequest as C, ApiErrorResponse as D, ApiResponse as E, MfaMethod as F, MfaEnrollment as G, TotpEnrollmentResult as H, IQAuthBrowserSessionClientConfig as I, JwtClaims as J, MfaVerifyResult as K, LoginResult as L, MigrateUserRequest as M, PasswordPolicy as N, OidcDiscovery as O, PromoteToVendorRequest as P, MfaPolicy as Q, UserPermissions as R, SessionUser as S, TokenPair as T, UserProfile as U, ProvisionUserRequest as V, ProvisionUserResponse as W, ExpressMiddlewareOptions as X, IQAuthRetryConfig as Y, IQAuthVerifyConfig as Z, PermissionNodeManifest as _, IQAuthRequestLike as a, BackupCodesResult as a$, AppInfo as a0, PermissionNodeInfo as a1, AppSyncResult as a2, Role as a3, CreateRoleRequest as a4, UpdateRoleRequest as a5, AssignRoleRequest as a6, UserRoleAssignment as a7, UserGroupAssignment as a8, TenantUser as a9, Source as aA, CreateSourceRequest as aB, UpdateSourceRequest as aC, Client as aD, CreateClientRequest as aE, UpdateClientRequest as aF, HierarchyVendor as aG, HierarchySource as aH, HierarchyClient as aI, HierarchyLink as aJ, Membership as aK, CreateMembershipRequest as aL, UpdateMembershipRequest as aM, MembershipWithDetails as aN, AvailableScopesTree as aO, ScopeTreeClient as aP, ScopeTreeSource as aQ, ScopeTreeVendor as aR, ScopeSwitchResult as aS, GdprExportData as aT, PinStatus as aU, PinLoginResult as aV, MfaAvailableMethods as aW, TotpEnrollResult as aX, TotpVerifyResult as aY, SmsEnrollResult as aZ, EmailEnrollResult as a_, PermissionGroup as aa, GroupPermission as ab, AddGroupPermissionRequest as ac, InheritanceRelation as ad, UserPermissionOverride as ae, AddUserOverrideRequest as af, EffectivePermission as ag, PermissionCheckResult as ah, ApiKeyInfo as ai, CreateApiKeyRequest as aj, CreateApiKeyResult as ak, ApiKeyIntrospection as al, Invitation as am, CreateInviteRequest as an, InviteValidation as ao, AcceptInviteRequest as ap, WebhookEndpoint as aq, CreateWebhookRequest as ar, CreateWebhookResult as as, WebhookDelivery as at, WebhookTestResult as au, Entitlement as av, GrantEntitlementRequest as aw, Vendor as ax, CreateVendorRequest as ay, UpdateVendorRequest as az, IQAuthResponseLike as b, BackupCodeCountResult as b0, SignupRequest as b1, HostedClientContext as b2, IQAuthNextFunction as c, IQAuthEnvironment as d, IQAuthClientConfig as e, IQAuthTokenClientConfig as f, ScopeContext as g, IQAuthClaims as h, IQAuthBaseClaims as i, Tenant as j, TokenAuthenticatedLoginResult as k, SessionAuthenticatedLoginResult as l, Session as m, TenantInfo as n, UpdateTenantRequest as o, PromoteToVendorResult as p, InviteTenantUserRequest as q, InviteTenantUserResult as r, TenantUserRoleUpdate as s, UpdateBrandingRequest as t, BrandingAsset as u, UploadAssetRequest as v, BrandingDomainMapping as w, JwksKey as x, JwksResponse as y, OidcTokenResponse as z };
1059
+ export type { AppManifest as $, ApiSuccessResponse as A, BrandingConfig as B, CreateTenantRequest as C, ApiErrorResponse as D, ApiResponse as E, MfaMethod as F, MfaEnrollment as G, TotpEnrollmentResult as H, IQAuthBrowserSessionClientConfig as I, JwtClaims as J, MfaVerifyResult as K, LoginResult as L, MigrateUserRequest as M, PasswordPolicy as N, OidcDiscovery as O, PromoteToVendorRequest as P, MfaPolicy as Q, UserPermissions as R, SessionUser as S, TokenPair as T, UserProfile as U, ProvisionUserRequest as V, ProvisionUserResponse as W, ExpressMiddlewareOptions as X, IQAuthRetryConfig as Y, IQAuthVerifyConfig as Z, PermissionNodeManifest as _, IQAuthRequestLike as a, BackupCodesResult as a$, AppInfo as a0, PermissionNodeInfo as a1, AppSyncResult as a2, Role as a3, CreateRoleRequest as a4, UpdateRoleRequest as a5, AssignRoleRequest as a6, UserRoleAssignment as a7, UserGroupAssignment as a8, TenantUser as a9, Source as aA, CreateSourceRequest as aB, UpdateSourceRequest as aC, Client as aD, CreateClientRequest as aE, UpdateClientRequest as aF, HierarchyVendor as aG, HierarchySource as aH, HierarchyClient as aI, HierarchyLink as aJ, Membership as aK, CreateMembershipRequest as aL, UpdateMembershipRequest as aM, MembershipWithDetails as aN, AvailableScopesTree as aO, ScopeTreeClient as aP, ScopeTreeSource as aQ, ScopeTreeVendor as aR, ScopeSwitchResult as aS, GdprExportData as aT, PinStatus as aU, PinLoginResult as aV, MfaAvailableMethods as aW, TotpEnrollResult as aX, TotpVerifyResult as aY, SmsEnrollResult as aZ, EmailEnrollResult as a_, PermissionGroup as aa, GroupPermission as ab, AddGroupPermissionRequest as ac, InheritanceRelation as ad, UserPermissionOverride as ae, AddUserOverrideRequest as af, EffectivePermission as ag, PermissionCheckResult as ah, ApiKeyInfo as ai, CreateApiKeyRequest as aj, CreateApiKeyResult as ak, ApiKeyIntrospection as al, Invitation as am, CreateInviteRequest as an, InviteValidation as ao, AcceptInviteRequest as ap, WebhookEndpoint as aq, CreateWebhookRequest as ar, CreateWebhookResult as as, WebhookDelivery as at, WebhookTestResult as au, Entitlement as av, GrantEntitlementRequest as aw, Vendor as ax, CreateVendorRequest as ay, UpdateVendorRequest as az, IQAuthResponseLike as b, BackupCodeCountResult as b0, ScopeHint as b1, SignupRequest as b2, HostedClientContext as b3, IQAuthNextFunction as c, IQAuthEnvironment as d, IQAuthClientConfig as e, IQAuthTokenClientConfig as f, ScopeContext as g, IQAuthClaims as h, IQAuthBaseClaims as i, Tenant as j, TokenAuthenticatedLoginResult as k, SessionAuthenticatedLoginResult as l, Session as m, TenantInfo as n, UpdateTenantRequest as o, PromoteToVendorResult as p, InviteTenantUserRequest as q, InviteTenantUserResult as r, TenantUserRoleUpdate as s, UpdateBrandingRequest as t, BrandingAsset as u, UploadAssetRequest as v, BrandingDomainMapping as w, JwksKey as x, JwksResponse as y, OidcTokenResponse as z };