@voyant-travel/auth 0.135.0 → 0.136.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.
@@ -10,13 +10,16 @@
10
10
  */
11
11
  import { openNodeDatabase } from "@voyant-travel/db/runtime";
12
12
  import { authUser, cloudAuthUserLinks, userProfilesTable, } from "@voyant-travel/db/schema/iam";
13
+ import { parseJsonBody, } from "@voyant-travel/hono";
13
14
  import { handleApiError, reportException, requestId, } from "@voyant-travel/hono/middleware/error-boundary";
14
15
  import { getRequestId } from "@voyant-travel/hono/observability";
15
16
  import { accessCatalogScopesForRole, isFullAccessRole } from "@voyant-travel/types/member-roles";
16
17
  import { eq, sql } from "drizzle-orm";
17
18
  import { Hono } from "hono";
19
+ import { z } from "zod";
18
20
  import { createVoyantCloudAdminAuthPlugin, revalidateVoyantCloudAdminAuthSession, revalidateVoyantCloudAdminAuthUser, } from "./cloud-admin-session.js";
19
21
  import { buildClearCloudAdminAuthStateCookie, createCloudAdminAuthStart } from "./cloud-broker.js";
22
+ import { createDrizzleCustomerBuyerAccountStore, listCustomerBuyerAccounts, normalizeCustomerBuyerAccountPolicy, repairCustomerPersonalBuyerAccountEntitlement, resolveActiveCustomerBuyerContext, selectCustomerBuyerAccount, } from "./customer-buyer-accounts.js";
20
23
  import { createAdminBetterAuth, createCustomerBetterAuth, handleApiTokenManagementRequest, handleOrganizationMembersRequest, } from "./server.js";
21
24
  import { ensureCurrentUserProfile } from "./workspace.js";
22
25
  function classifyOperatorApiAuthSurface(requestUrl) {
@@ -292,6 +295,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
292
295
  facebook: { enabled: false, credentialRef: null },
293
296
  apple: { enabled: false, credentialRef: null },
294
297
  },
298
+ accountPolicy: normalizeCustomerBuyerAccountPolicy(),
295
299
  };
296
300
  const raw = env.VOYANT_CUSTOMER_AUTH_CONFIG_JSON?.trim();
297
301
  if (!raw)
@@ -321,11 +325,11 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
321
325
  facebook: social(methods.facebook),
322
326
  apple: social(methods.apple),
323
327
  },
328
+ accountPolicy: normalizeCustomerBuyerAccountPolicy(parsed.accountPolicy),
324
329
  };
325
330
  }
326
- catch {
327
- console.error("[auth/customer] Invalid VOYANT_CUSTOMER_AUTH_CONFIG_JSON; failing social auth closed");
328
- return fallback;
331
+ catch (error) {
332
+ throw new Error("Invalid VOYANT_CUSTOMER_AUTH_CONFIG_JSON", { cause: error });
329
333
  }
330
334
  }
331
335
  function defaultCustomerAuthContext(env, policy) {
@@ -373,6 +377,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
373
377
  : {}),
374
378
  },
375
379
  },
380
+ accountPolicy: policy.accountPolicy,
376
381
  };
377
382
  }
378
383
  async function resolveCustomerAuthContext(env, request) {
@@ -423,7 +428,8 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
423
428
  return parsed.toString().replace(/\/$/, "");
424
429
  }
425
430
  async function publicCustomerAuthConfiguration(env, request) {
426
- const methods = (await resolveCustomerAuthContext(env, request)).methods;
431
+ const context = await resolveCustomerAuthContext(env, request);
432
+ const methods = context.methods;
427
433
  const providers = methods.socialProviders ?? {};
428
434
  return {
429
435
  methods: {
@@ -433,6 +439,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
433
439
  facebook: Boolean(providers.facebook),
434
440
  apple: Boolean(providers.apple),
435
441
  },
442
+ accountPolicy: normalizeCustomerBuyerAccountPolicy(context.accountPolicy),
436
443
  };
437
444
  }
438
445
  function buildAdminBetterAuth(env, db) {
@@ -492,9 +499,9 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
492
499
  },
493
500
  });
494
501
  }
495
- async function buildCustomerBetterAuth(env, db, request) {
502
+ async function buildCustomerBetterAuth(env, db, request, resolvedContext) {
496
503
  const emailSender = runtimeOptions.resolveEmailSender?.(env);
497
- const context = await resolveCustomerAuthContext(env, request);
504
+ const context = resolvedContext ?? (await resolveCustomerAuthContext(env, request));
498
505
  const publicApiBaseURL = context.publicApiBaseURL ?? getPublicApiBaseUrl(env);
499
506
  const authDb = db;
500
507
  return createCustomerBetterAuth({
@@ -504,6 +511,7 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
504
511
  basePath: "/auth/customer",
505
512
  trustedOrigins: context.trustedOrigins,
506
513
  methods: withCustomerSocialRedirectUris(context.methods, publicApiBaseURL),
514
+ accountPolicy: context.accountPolicy,
507
515
  sendResetPassword: async ({ user, url }) => {
508
516
  const publicUrl = withCustomerPublicResetPasswordUrl(url, publicApiBaseURL);
509
517
  if (!emailSender) {
@@ -581,8 +589,11 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
581
589
  }
582
590
  const { db, dispose } = openDatabase(env);
583
591
  try {
592
+ const customerContext = customerSurface
593
+ ? await resolveCustomerAuthContext(env, request)
594
+ : null;
584
595
  const auth = customerSurface
585
- ? await buildCustomerBetterAuth(env, db, request)
596
+ ? await buildCustomerBetterAuth(env, db, request, customerContext ?? undefined)
586
597
  : buildAdminBetterAuth(env, db);
587
598
  const session = await auth.api.getSession({ headers: request.headers });
588
599
  if (!session) {
@@ -609,9 +620,25 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
609
620
  }
610
621
  }
611
622
  if (customerSurface) {
612
- return {
623
+ await repairCustomerPersonalBuyerAccountEntitlement(db, session.user.id);
624
+ const buyerStore = createDrizzleCustomerBuyerAccountStore(db);
625
+ const relationshipPersonId = await buyerStore.getRelationshipPersonId(session.user.id);
626
+ const activeAuthOrganizationId = session.session.activeOrganizationId ?? null;
627
+ const buyer = await resolveActiveCustomerBuyerContext({
628
+ identity: {
629
+ userId: session.user.id,
630
+ name: session.user.name ?? null,
631
+ email: session.user.email ?? null,
632
+ },
633
+ activeAuthOrganizationId,
634
+ policy: customerContext?.accountPolicy,
635
+ store: buyerStore,
636
+ });
637
+ const customerIdentity = {
613
638
  userId: session.user.id,
614
639
  sessionId: session.session.id,
640
+ // Existing organizationId is staff/workspace context. Customer buyer
641
+ // scoping uses the explicit discriminated buyer fields below.
615
642
  organizationId: null,
616
643
  callerType: "session",
617
644
  actor: "customer",
@@ -619,6 +646,22 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
619
646
  scopes: [],
620
647
  email: session.user.email ?? null,
621
648
  };
649
+ if (!buyer) {
650
+ return {
651
+ ...customerIdentity,
652
+ relationshipPersonId,
653
+ };
654
+ }
655
+ return {
656
+ ...customerIdentity,
657
+ buyerAccountId: buyer.id,
658
+ buyerAccountKind: buyer.kind,
659
+ authOrganizationId: buyer.authOrganizationId,
660
+ relationshipOrganizationId: buyer.relationshipOrganizationId,
661
+ relationshipPersonId,
662
+ buyerMembershipId: buyer.membershipId,
663
+ buyerMembershipRole: buyer.membershipRole,
664
+ };
622
665
  }
623
666
  return {
624
667
  userId: session.user.id,
@@ -951,8 +994,10 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
951
994
  }
952
995
  });
953
996
  auth.get("/auth/customer/config", async (c) => {
997
+ c.header("Cache-Control", "no-store");
954
998
  if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
955
999
  return c.json({
1000
+ disabled: true,
956
1001
  methods: {
957
1002
  emailCode: false,
958
1003
  emailPassword: false,
@@ -964,6 +1009,88 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
964
1009
  }
965
1010
  return c.json(await publicCustomerAuthConfiguration(c.env, c.req.raw));
966
1011
  });
1012
+ const selectBuyerAccountSchema = z.object({ accountId: z.string().min(1) });
1013
+ auth.get("/auth/customer/buyer-accounts", async (c) => {
1014
+ c.header("Cache-Control", "no-store");
1015
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1016
+ return c.json({ error: "Customer auth is disabled" }, 404);
1017
+ }
1018
+ const { db, dispose } = openDatabase(c.env);
1019
+ try {
1020
+ const context = await resolveCustomerAuthContext(c.env, c.req.raw);
1021
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw, context);
1022
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
1023
+ if (!session)
1024
+ return c.json({ error: "Unauthorized" }, 401);
1025
+ await repairCustomerPersonalBuyerAccountEntitlement(db, session.user.id);
1026
+ return c.json(await listCustomerBuyerAccounts({
1027
+ identity: {
1028
+ userId: session.user.id,
1029
+ name: session.user.name ?? null,
1030
+ email: session.user.email ?? null,
1031
+ },
1032
+ activeAuthOrganizationId: session.session.activeOrganizationId ??
1033
+ null,
1034
+ policy: context.accountPolicy,
1035
+ store: createDrizzleCustomerBuyerAccountStore(db),
1036
+ }));
1037
+ }
1038
+ finally {
1039
+ c.executionCtx.waitUntil(dispose());
1040
+ }
1041
+ });
1042
+ auth.post("/auth/customer/buyer-accounts/active", async (c) => {
1043
+ c.header("Cache-Control", "no-store");
1044
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1045
+ return c.json({ error: "Customer auth is disabled" }, 404);
1046
+ }
1047
+ const body = await parseJsonBody(c, selectBuyerAccountSchema);
1048
+ const { db, dispose } = openDatabase(c.env);
1049
+ try {
1050
+ const context = await resolveCustomerAuthContext(c.env, c.req.raw);
1051
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw, context);
1052
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
1053
+ if (!session)
1054
+ return c.json({ error: "Unauthorized" }, 401);
1055
+ await repairCustomerPersonalBuyerAccountEntitlement(db, session.user.id);
1056
+ const selected = await selectCustomerBuyerAccount({
1057
+ accountId: body.accountId,
1058
+ identity: {
1059
+ userId: session.user.id,
1060
+ name: session.user.name ?? null,
1061
+ email: session.user.email ?? null,
1062
+ },
1063
+ activeAuthOrganizationId: session.session.activeOrganizationId ??
1064
+ null,
1065
+ policy: context.accountPolicy,
1066
+ store: createDrizzleCustomerBuyerAccountStore(db),
1067
+ });
1068
+ if (!selected) {
1069
+ return c.json({ error: "Buyer account is unavailable" }, 403);
1070
+ }
1071
+ const activationUrl = new URL(c.req.url);
1072
+ activationUrl.pathname = "/auth/customer/organization/set-active";
1073
+ activationUrl.search = "";
1074
+ const activationHeaders = new Headers(c.req.raw.headers);
1075
+ activationHeaders.delete("content-length");
1076
+ activationHeaders.set("content-type", "application/json");
1077
+ const activationResponse = await betterAuth.handler(new Request(activationUrl, {
1078
+ method: "POST",
1079
+ headers: activationHeaders,
1080
+ body: JSON.stringify({ organizationId: selected.authOrganizationId }),
1081
+ }));
1082
+ if (!activationResponse.ok)
1083
+ return activationResponse;
1084
+ const headers = new Headers(activationResponse.headers);
1085
+ headers.delete("content-length");
1086
+ headers.set("cache-control", "no-store");
1087
+ headers.set("content-type", "application/json; charset=UTF-8");
1088
+ return new Response(JSON.stringify({ activeAccount: selected }), { status: 200, headers });
1089
+ }
1090
+ finally {
1091
+ c.executionCtx.waitUntil(dispose());
1092
+ }
1093
+ });
967
1094
  auth.all("/auth/customer/*", async (c) => {
968
1095
  if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
969
1096
  return c.json({ error: "Customer auth is disabled" }, 404);