@voyant-travel/auth 0.133.5 → 0.135.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.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * agent-quality: file-size exception -- Auth handler keeps local and Voyant Cloud auth flows co-located until the route surface is split by auth mode.
5
5
  *
6
- * Mounts Better Auth at /auth/* for authentication operations.
6
+ * Mounts Better Auth at /auth/admin/* and /auth/customer/*.
7
7
  * Same-origin — no CORS needed. Session cookies work naturally.
8
8
  *
9
9
  * Also provides /auth/status (user provisioning) and /auth/me (user info).
@@ -19,6 +19,15 @@ import { createVoyantCloudAdminAuthPlugin, revalidateVoyantCloudAdminAuthSession
19
19
  import { buildClearCloudAdminAuthStateCookie, createCloudAdminAuthStart } from "./cloud-broker.js";
20
20
  import { createAdminBetterAuth, createCustomerBetterAuth, handleApiTokenManagementRequest, handleOrganizationMembersRequest, } from "./server.js";
21
21
  import { ensureCurrentUserProfile } from "./workspace.js";
22
+ function classifyOperatorApiAuthSurface(requestUrl) {
23
+ const pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
24
+ const normalized = pathname.startsWith("/api/") ? pathname.slice(4) : pathname;
25
+ if (normalized === "/v1/admin" || normalized.startsWith("/v1/admin/"))
26
+ return "admin";
27
+ if (normalized === "/v1/public" || normalized.startsWith("/v1/public/"))
28
+ return "customer";
29
+ return null;
30
+ }
22
31
  /** Resolve Better Auth's standard cross-subdomain cookie policy for Node hosts. */
23
32
  export function buildBetterAuthCookieAdvancedOptions(env) {
24
33
  const domain = env.AUTH_COOKIE_DOMAIN?.trim();
@@ -71,6 +80,24 @@ export function withCustomerSocialRedirectUris(methods, publicApiBaseUrl) {
71
80
  },
72
81
  };
73
82
  }
83
+ /**
84
+ * Rewrite Better Auth's internally generated customer password-reset URL to
85
+ * the browser-visible storefront/BFF API base.
86
+ */
87
+ export function withCustomerPublicResetPasswordUrl(generatedUrl, publicApiBaseURL) {
88
+ try {
89
+ const generated = new URL(generatedUrl);
90
+ const publicApi = new URL(publicApiBaseURL);
91
+ const apiPath = publicApi.pathname.replace(/\/+$/, "");
92
+ publicApi.pathname = `${apiPath}${generated.pathname}`;
93
+ publicApi.search = generated.search;
94
+ publicApi.hash = generated.hash;
95
+ return publicApi.toString();
96
+ }
97
+ catch {
98
+ return generatedUrl;
99
+ }
100
+ }
74
101
  export function createOperatorAuthNodeRuntime(runtimeOptions) {
75
102
  const openDatabase = runtimeOptions.openDatabase ??
76
103
  ((env) => {
@@ -186,7 +213,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
186
213
  cloudAuthStartUrl,
187
214
  deploymentId,
188
215
  adminCallbackUrl: `${getPublicApiBaseUrl(env)}/auth/admin/cloud/callback`,
189
- cookieSecret: env.SESSION_CLAIMS_SECRET,
216
+ cookieSecret: env.SESSION_CLAIMS_ADMIN_SECRET,
190
217
  appId: env.VOYANT_CLOUD_APP_ID?.trim() || undefined,
191
218
  environment: env.VOYANT_CLOUD_ENVIRONMENT?.trim() || undefined,
192
219
  };
@@ -313,6 +340,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
313
340
  env.CUSTOMER_AUTH_APPLE_CLIENT_SECRET?.trim() &&
314
341
  (!hasCanonicalPolicy || policy.methods.apple.enabled));
315
342
  return {
343
+ publicApiBaseURL: getPublicApiBaseUrl(env),
316
344
  baseURL: getAuthBaseUrl(env),
317
345
  trustedOrigins: getTrustedOrigins(env),
318
346
  methods: {
@@ -347,13 +375,55 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
347
375
  },
348
376
  };
349
377
  }
350
- function resolveCustomerAuthContext(env, request) {
351
- const policy = parseCustomerAuthPolicy(env);
352
- return (runtimeOptions.resolveCustomerAuthContext?.(env, policy, request) ??
353
- defaultCustomerAuthContext(env, policy));
378
+ async function resolveCustomerAuthContext(env, request) {
379
+ const context = runtimeOptions.resolveCustomerAuthContext
380
+ ? await runtimeOptions.resolveCustomerAuthContext(env, request)
381
+ : defaultCustomerAuthContext(env, parseCustomerAuthPolicy(env));
382
+ const baseURL = requireCanonicalOrigin(context.baseURL, "customer auth baseURL");
383
+ const publicApiBaseURL = requirePublicApiBaseUrl(context.publicApiBaseURL ?? `${baseURL}/api`, baseURL);
384
+ const trustedOrigins = context.trustedOrigins.map((origin) => requireCanonicalOrigin(origin, "customer auth trusted origin"));
385
+ return { ...context, baseURL, publicApiBaseURL, trustedOrigins };
386
+ }
387
+ function requireCanonicalOrigin(value, label) {
388
+ let parsed;
389
+ try {
390
+ parsed = new URL(value);
391
+ }
392
+ catch {
393
+ throw new Error(`${label} must be an absolute HTTP(S) origin`);
394
+ }
395
+ if (!["http:", "https:"].includes(parsed.protocol) ||
396
+ parsed.username ||
397
+ parsed.password ||
398
+ (parsed.pathname !== "/" && parsed.pathname !== "") ||
399
+ parsed.search ||
400
+ parsed.hash) {
401
+ throw new Error(`${label} must be an absolute HTTP(S) origin`);
402
+ }
403
+ return parsed.origin;
354
404
  }
355
- function publicCustomerAuthConfiguration(env, request) {
356
- const methods = resolveCustomerAuthContext(env, request).methods;
405
+ function requirePublicApiBaseUrl(value, baseURL) {
406
+ let parsed;
407
+ try {
408
+ parsed = new URL(value);
409
+ }
410
+ catch {
411
+ throw new Error("customer auth publicApiBaseURL must be an absolute HTTP(S) URL");
412
+ }
413
+ if (!["http:", "https:"].includes(parsed.protocol) ||
414
+ parsed.username ||
415
+ parsed.password ||
416
+ parsed.search ||
417
+ parsed.hash ||
418
+ parsed.origin !== baseURL ||
419
+ parsed.pathname === "/") {
420
+ throw new Error("customer auth publicApiBaseURL must be an absolute HTTP(S) URL with a path");
421
+ }
422
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
423
+ return parsed.toString().replace(/\/$/, "");
424
+ }
425
+ async function publicCustomerAuthConfiguration(env, request) {
426
+ const methods = (await resolveCustomerAuthContext(env, request)).methods;
357
427
  const providers = methods.socialProviders ?? {};
358
428
  return {
359
429
  methods: {
@@ -387,7 +457,9 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
387
457
  ? [
388
458
  createVoyantCloudAdminAuthPlugin({
389
459
  db: cloudAuthDb,
390
- cookieSecret: env.SESSION_CLAIMS_SECRET,
460
+ cookieSecret: env.SESSION_CLAIMS_ADMIN_SECRET,
461
+ secureStateCookie: new URL(`${getPublicApiBaseUrl(env)}/auth/admin/cloud/callback`).protocol ===
462
+ "https:",
391
463
  exchange: cloudAuthExchange,
392
464
  }),
393
465
  ]
@@ -420,9 +492,10 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
420
492
  },
421
493
  });
422
494
  }
423
- function buildCustomerBetterAuth(env, db, request) {
495
+ async function buildCustomerBetterAuth(env, db, request) {
424
496
  const emailSender = runtimeOptions.resolveEmailSender?.(env);
425
- const context = resolveCustomerAuthContext(env, request);
497
+ const context = await resolveCustomerAuthContext(env, request);
498
+ const publicApiBaseURL = context.publicApiBaseURL ?? getPublicApiBaseUrl(env);
426
499
  const authDb = db;
427
500
  return createCustomerBetterAuth({
428
501
  db: authDb,
@@ -430,16 +503,17 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
430
503
  baseURL: context.baseURL,
431
504
  basePath: "/auth/customer",
432
505
  trustedOrigins: context.trustedOrigins,
433
- methods: withCustomerSocialRedirectUris(context.methods, getPublicApiBaseUrl(env)),
506
+ methods: withCustomerSocialRedirectUris(context.methods, publicApiBaseURL),
434
507
  sendResetPassword: async ({ user, url }) => {
508
+ const publicUrl = withCustomerPublicResetPasswordUrl(url, publicApiBaseURL);
435
509
  if (!emailSender) {
436
510
  if (allowAuthSecretLogging(env)) {
437
- console.info(`[auth/customer] reset-password (debug fallback) -> ${user.email}: ${url}`);
511
+ console.info(`[auth/customer] reset-password (debug fallback) -> ${user.email}: ${publicUrl}`);
438
512
  return;
439
513
  }
440
514
  throw new Error("Password reset email provider is not configured");
441
515
  }
442
- await emailSender.sendResetPassword({ user, url });
516
+ await emailSender.sendResetPassword({ user, url: publicUrl });
443
517
  },
444
518
  sendVerificationOTP: async ({ email, otp, type }) => {
445
519
  if (!emailSender) {
@@ -453,11 +527,6 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
453
527
  },
454
528
  });
455
529
  }
456
- function rewriteLegacyAdminAuthRequest(request) {
457
- const url = new URL(request.url);
458
- url.pathname = url.pathname.replace("/auth", "/auth/admin");
459
- return new Request(url, request);
460
- }
461
530
  const FULL_ACCESS_SCOPES = ["*"];
462
531
  function scopesForOperatorRole(role) {
463
532
  const base = accessCatalogScopesForRole(role, runtimeOptions.accessCatalog);
@@ -503,14 +572,17 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
503
572
  return profile?.permissions ?? FULL_ACCESS_SCOPES;
504
573
  }
505
574
  async function resolveAuthRequest(request, env) {
506
- const customerSurface = new URL(request.url).pathname.includes("/v1/public/");
575
+ const surface = classifyOperatorApiAuthSurface(request.url);
576
+ if (!surface)
577
+ return null;
578
+ const customerSurface = surface === "customer";
507
579
  if (customerSurface && env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
508
580
  return null;
509
581
  }
510
582
  const { db, dispose } = openDatabase(env);
511
583
  try {
512
584
  const auth = customerSurface
513
- ? buildCustomerBetterAuth(env, db, request)
585
+ ? await buildCustomerBetterAuth(env, db, request)
514
586
  : buildAdminBetterAuth(env, db);
515
587
  const session = await auth.api.getSession({ headers: request.headers });
516
588
  if (!session) {
@@ -637,16 +709,19 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
637
709
  }
638
710
  }
639
711
  async function getBootstrapStatusForRequest(request, env) {
712
+ const modules = runtimeOptions.activeModules
713
+ ? { modules: [...runtimeOptions.activeModules] }
714
+ : {};
640
715
  if (shouldUseBrowserEvidenceAuthFallback(env, request)) {
641
- return { hasUsers: true };
716
+ return { hasUsers: true, ...modules };
642
717
  }
643
718
  if (isVoyantCloudAuthMode(env)) {
644
- return { hasUsers: true, authMode: "voyant-cloud" };
719
+ return { hasUsers: true, authMode: "voyant-cloud", ...modules };
645
720
  }
646
721
  const { db, dispose } = openDatabase(env);
647
722
  try {
648
723
  const [row] = await db.select({ count: sql `count(*)::int` }).from(authUser);
649
- return { hasUsers: (row?.count ?? 0) > 0, authMode: "local" };
724
+ return { hasUsers: (row?.count ?? 0) > 0, authMode: "local", ...modules };
650
725
  }
651
726
  finally {
652
727
  await dispose();
@@ -837,17 +912,15 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
837
912
  const exchangeConfig = getCloudAuthExchangeConfig(c.env);
838
913
  if (!exchangeConfig) {
839
914
  const url = new URL(c.req.url);
915
+ const callbackUrl = getCloudAuthStartConfig(c.env)?.adminCallbackUrl;
840
916
  return c.json({ error: "Voyant Cloud auth exchange is not configured yet" }, 501, {
841
- "Set-Cookie": buildClearCloudAdminAuthStateCookie(url.protocol === "https:", url.pathname.replace(/\/callback$/, "") || "/auth/cloud"),
917
+ "Set-Cookie": buildClearCloudAdminAuthStateCookie(callbackUrl ? new URL(callbackUrl).protocol === "https:" : url.protocol === "https:", url.pathname.replace(/\/callback$/, "") || "/auth/admin/cloud"),
842
918
  });
843
919
  }
844
920
  const { db, dispose } = openDatabase(c.env);
845
921
  try {
846
922
  const betterAuth = buildAdminBetterAuth(c.env, db);
847
- const request = c.req.path.startsWith("/auth/admin/")
848
- ? c.req.raw
849
- : rewriteLegacyAdminAuthRequest(c.req.raw);
850
- return await betterAuth.handler(request);
923
+ return await betterAuth.handler(c.req.raw);
851
924
  }
852
925
  catch (error) {
853
926
  console.error("[auth/cloud/callback] Error:", error);
@@ -859,17 +932,17 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
859
932
  }
860
933
  auth.get("/auth/admin/cloud/start", handleCloudAuthStart);
861
934
  auth.get("/auth/admin/cloud/callback", handleCloudAuthCallback);
862
- /** @deprecated Use /auth/admin/cloud/*. */
863
- auth.get("/auth/cloud/start", handleCloudAuthStart);
864
- auth.get("/auth/cloud/callback", handleCloudAuthCallback);
865
935
  auth.all("/auth/api-tokens", handleApiTokensFacade);
866
936
  auth.all("/auth/api-tokens/:keyId", handleApiTokensFacade);
867
937
  auth.all("/auth/api-tokens/:keyId/rotate", handleApiTokensFacade);
868
938
  auth.get("/auth/organization/list-members", handleOrganizationMembersFacade);
869
939
  auth.get("/auth/customer/status", async (c) => {
940
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
941
+ return c.json({ authenticated: false, disabled: true });
942
+ }
870
943
  const { db, dispose } = openDatabase(c.env);
871
944
  try {
872
- const betterAuth = buildCustomerBetterAuth(c.env, db, c.req.raw);
945
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw);
873
946
  const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
874
947
  return c.json({ authenticated: Boolean(session) });
875
948
  }
@@ -877,7 +950,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
877
950
  c.executionCtx.waitUntil(dispose());
878
951
  }
879
952
  });
880
- auth.get("/auth/customer/config", (c) => {
953
+ auth.get("/auth/customer/config", async (c) => {
881
954
  if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
882
955
  return c.json({
883
956
  methods: {
@@ -889,7 +962,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
889
962
  },
890
963
  });
891
964
  }
892
- return c.json(publicCustomerAuthConfiguration(c.env, c.req.raw));
965
+ return c.json(await publicCustomerAuthConfiguration(c.env, c.req.raw));
893
966
  });
894
967
  auth.all("/auth/customer/*", async (c) => {
895
968
  if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
@@ -897,7 +970,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
897
970
  }
898
971
  const { db, dispose } = openDatabase(c.env);
899
972
  try {
900
- const betterAuth = buildCustomerBetterAuth(c.env, db, c.req.raw);
973
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw);
901
974
  return await betterAuth.handler(c.req.raw);
902
975
  }
903
976
  finally {
@@ -924,20 +997,6 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
924
997
  c.executionCtx.waitUntil(dispose());
925
998
  }
926
999
  });
927
- /** @deprecated Admin Better Auth routes moved to /auth/admin/*. */
928
- auth.all("/auth/*", async (c) => {
929
- const rewritten = rewriteLegacyAdminAuthRequest(c.req.raw);
930
- if (isVoyantCloudAuthMode(c.env) && !isCloudAllowedBetterAuthRoute(rewritten)) {
931
- return localAuthDisabledResponse(c);
932
- }
933
- const { db, dispose } = openDatabase(c.env);
934
- try {
935
- return await buildAdminBetterAuth(c.env, db).handler(rewritten);
936
- }
937
- finally {
938
- c.executionCtx.waitUntil(dispose());
939
- }
940
- });
941
1000
  return {
942
1001
  handler: auth,
943
1002
  getBootstrapStatusForRequest,