@voyant-travel/auth 0.136.0 → 0.138.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 (50) hide show
  1. package/dist/customer-business-accounts-contracts.d.ts +159 -0
  2. package/dist/customer-business-accounts-contracts.js +112 -0
  3. package/dist/customer-business-accounts-contracts.js.map +1 -0
  4. package/dist/customer-business-onboarding-graph-runtime.d.ts +13 -0
  5. package/dist/customer-business-onboarding-graph-runtime.js +8 -0
  6. package/dist/customer-business-onboarding-graph-runtime.js.map +1 -0
  7. package/dist/customer-business-onboarding-routes.d.ts +13 -0
  8. package/dist/customer-business-onboarding-routes.js +151 -0
  9. package/dist/customer-business-onboarding-routes.js.map +1 -0
  10. package/dist/customer-business-onboarding-runtime-port.d.ts +40 -0
  11. package/dist/customer-business-onboarding-runtime-port.js +25 -0
  12. package/dist/customer-business-onboarding-runtime-port.js.map +1 -0
  13. package/dist/customer-business-onboarding-service.d.ts +32 -0
  14. package/dist/customer-business-onboarding-service.js +237 -0
  15. package/dist/customer-business-onboarding-service.js.map +1 -0
  16. package/dist/customer-buyer-accounts.d.ts +0 -19
  17. package/dist/customer-buyer-accounts.js.map +1 -1
  18. package/dist/node-runtime.d.ts +13 -0
  19. package/dist/node-runtime.js +285 -1
  20. package/dist/node-runtime.js.map +1 -1
  21. package/dist/ports.d.ts +1 -0
  22. package/dist/ports.js +2 -0
  23. package/dist/ports.js.map +1 -0
  24. package/dist/runtime-contributor.js +11 -0
  25. package/dist/runtime-contributor.js.map +1 -1
  26. package/dist/server.d.ts +39 -3
  27. package/dist/server.js.map +1 -1
  28. package/dist/storefront-credentials.d.ts +31 -0
  29. package/dist/storefront-credentials.js +54 -0
  30. package/dist/storefront-credentials.js.map +1 -0
  31. package/dist/storefront-customer-auth-resolver.d.ts +30 -0
  32. package/dist/storefront-customer-auth-resolver.js +72 -0
  33. package/dist/storefront-customer-auth-resolver.js.map +1 -0
  34. package/dist/storefront-keys.d.ts +40 -0
  35. package/dist/storefront-keys.js +50 -0
  36. package/dist/storefront-keys.js.map +1 -0
  37. package/dist/storefront-local-adapter.d.ts +11 -0
  38. package/dist/storefront-local-adapter.js +313 -0
  39. package/dist/storefront-local-adapter.js.map +1 -0
  40. package/dist/storefront-origins.d.ts +34 -0
  41. package/dist/storefront-origins.js +132 -0
  42. package/dist/storefront-origins.js.map +1 -0
  43. package/dist/storefront-runtime-port.d.ts +93 -0
  44. package/dist/storefront-runtime-port.js +35 -0
  45. package/dist/storefront-runtime-port.js.map +1 -0
  46. package/dist/voyant.d.ts +2 -0
  47. package/dist/voyant.js +99 -0
  48. package/dist/voyant.js.map +1 -1
  49. package/openapi/admin/customer-business-accounts.json +44 -0
  50. package/package.json +50 -4
@@ -19,6 +19,8 @@ import { Hono } from "hono";
19
19
  import { z } from "zod";
20
20
  import { createVoyantCloudAdminAuthPlugin, revalidateVoyantCloudAdminAuthSession, revalidateVoyantCloudAdminAuthUser, } from "./cloud-admin-session.js";
21
21
  import { buildClearCloudAdminAuthStateCookie, createCloudAdminAuthStart } from "./cloud-broker.js";
22
+ import { customerBusinessAccountCreateInputSchema, customerBusinessAccountRequestCreateInputSchema, customerBusinessAccountRequestListQuerySchema, customerBusinessInvitationAcceptInputSchema, } from "./customer-business-accounts-contracts.js";
23
+ import { CustomerBusinessOnboardingConflictError, CustomerBusinessOnboardingNotFoundError, } from "./customer-business-onboarding-service.js";
22
24
  import { createDrizzleCustomerBuyerAccountStore, listCustomerBuyerAccounts, normalizeCustomerBuyerAccountPolicy, repairCustomerPersonalBuyerAccountEntitlement, resolveActiveCustomerBuyerContext, selectCustomerBuyerAccount, } from "./customer-buyer-accounts.js";
23
25
  import { createAdminBetterAuth, createCustomerBetterAuth, handleApiTokenManagementRequest, handleOrganizationMembersRequest, } from "./server.js";
24
26
  import { ensureCurrentUserProfile } from "./workspace.js";
@@ -101,6 +103,19 @@ export function withCustomerPublicResetPasswordUrl(generatedUrl, publicApiBaseUR
101
103
  return generatedUrl;
102
104
  }
103
105
  }
106
+ export function customerOrganizationInvitationUrl(invitationAcceptBaseURL, invitationId) {
107
+ const base = new URL(invitationAcceptBaseURL);
108
+ if ((base.protocol !== "http:" && base.protocol !== "https:") ||
109
+ base.username ||
110
+ base.password ||
111
+ base.pathname !== "/" ||
112
+ base.search ||
113
+ base.hash) {
114
+ throw new Error("invitationAcceptBaseURL must be an exact trusted HTTP(S) origin");
115
+ }
116
+ base.pathname = `/account/business-invitations/${encodeURIComponent(invitationId)}`;
117
+ return base.toString();
118
+ }
104
119
  export function createOperatorAuthNodeRuntime(runtimeOptions) {
105
120
  const openDatabase = runtimeOptions.openDatabase ??
106
121
  ((env) => {
@@ -346,6 +361,9 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
346
361
  return {
347
362
  publicApiBaseURL: getPublicApiBaseUrl(env),
348
363
  baseURL: getAuthBaseUrl(env),
364
+ ...(env.CUSTOMER_STOREFRONT_ORIGIN?.trim()
365
+ ? { invitationAcceptBaseURL: env.CUSTOMER_STOREFRONT_ORIGIN.trim() }
366
+ : {}),
349
367
  trustedOrigins: getTrustedOrigins(env),
350
368
  methods: {
351
369
  emailCode: policy.methods.emailCode,
@@ -387,7 +405,16 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
387
405
  const baseURL = requireCanonicalOrigin(context.baseURL, "customer auth baseURL");
388
406
  const publicApiBaseURL = requirePublicApiBaseUrl(context.publicApiBaseURL ?? `${baseURL}/api`, baseURL);
389
407
  const trustedOrigins = context.trustedOrigins.map((origin) => requireCanonicalOrigin(origin, "customer auth trusted origin"));
390
- return { ...context, baseURL, publicApiBaseURL, trustedOrigins };
408
+ const invitationAcceptBaseURL = context.invitationAcceptBaseURL
409
+ ? requireCanonicalOrigin(context.invitationAcceptBaseURL, "customer invitation accept baseURL")
410
+ : undefined;
411
+ return {
412
+ ...context,
413
+ baseURL,
414
+ publicApiBaseURL,
415
+ trustedOrigins,
416
+ ...(invitationAcceptBaseURL ? { invitationAcceptBaseURL } : {}),
417
+ };
391
418
  }
392
419
  function requireCanonicalOrigin(value, label) {
393
420
  let parsed;
@@ -504,6 +531,8 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
504
531
  const context = resolvedContext ?? (await resolveCustomerAuthContext(env, request));
505
532
  const publicApiBaseURL = context.publicApiBaseURL ?? getPublicApiBaseUrl(env);
506
533
  const authDb = db;
534
+ const invitationAcceptBaseURL = context.invitationAcceptBaseURL;
535
+ const invitationEmailSender = invitationAcceptBaseURL ? emailSender : null;
507
536
  return createCustomerBetterAuth({
508
537
  db: authDb,
509
538
  secret: requireCustomerAuthSecret(env),
@@ -512,6 +541,20 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
512
541
  trustedOrigins: context.trustedOrigins,
513
542
  methods: withCustomerSocialRedirectUris(context.methods, publicApiBaseURL),
514
543
  accountPolicy: context.accountPolicy,
544
+ ...(invitationAcceptBaseURL && invitationEmailSender
545
+ ? {
546
+ sendOrganizationInvitation: async ({ id, email, organization, inviter, role }) => {
547
+ const url = customerOrganizationInvitationUrl(invitationAcceptBaseURL, id);
548
+ await invitationEmailSender.sendCustomerOrganizationInvitation({
549
+ email,
550
+ organizationName: organization.name,
551
+ inviterName: inviter.user.name,
552
+ role,
553
+ url,
554
+ });
555
+ },
556
+ }
557
+ : {}),
515
558
  sendResetPassword: async ({ user, url }) => {
516
559
  const publicUrl = withCustomerPublicResetPasswordUrl(url, publicApiBaseURL);
517
560
  if (!emailSender) {
@@ -1010,6 +1053,34 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
1010
1053
  return c.json(await publicCustomerAuthConfiguration(c.env, c.req.raw));
1011
1054
  });
1012
1055
  const selectBuyerAccountSchema = z.object({ accountId: z.string().min(1) });
1056
+ const onboardingRuntime = runtimeOptions.customerBusinessAccountOnboarding;
1057
+ const onboardingErrorResponse = (c, error) => {
1058
+ if (error instanceof CustomerBusinessOnboardingNotFoundError) {
1059
+ return c.json({ error: error.message }, 404);
1060
+ }
1061
+ if (error instanceof CustomerBusinessOnboardingConflictError) {
1062
+ return c.json({ error: error.message }, 409);
1063
+ }
1064
+ throw error;
1065
+ };
1066
+ const requireBusinessOnboarding = (c, context, expected) => {
1067
+ const policy = normalizeCustomerBuyerAccountPolicy(context.accountPolicy);
1068
+ if (!policy.allowedKinds.includes("business")) {
1069
+ return c.json({ error: "Customer business accounts are disabled" }, 404);
1070
+ }
1071
+ if (expected === "invitation") {
1072
+ return policy.businessOnboarding === "disabled"
1073
+ ? c.json({ error: "Customer business accounts are disabled" }, 403)
1074
+ : null;
1075
+ }
1076
+ if (policy.businessOnboarding !== expected) {
1077
+ return c.json({ error: `Customer business onboarding is not ${expected}` }, 403);
1078
+ }
1079
+ if (!onboardingRuntime) {
1080
+ return c.json({ error: "Customer business onboarding provider is unavailable" }, 501);
1081
+ }
1082
+ return null;
1083
+ };
1013
1084
  auth.get("/auth/customer/buyer-accounts", async (c) => {
1014
1085
  c.header("Cache-Control", "no-store");
1015
1086
  if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
@@ -1091,6 +1162,219 @@ export function createOperatorAuthNodeRuntime(runtimeOptions) {
1091
1162
  c.executionCtx.waitUntil(dispose());
1092
1163
  }
1093
1164
  });
1165
+ auth.post("/auth/customer/business-accounts", async (c) => {
1166
+ c.header("Cache-Control", "no-store");
1167
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1168
+ return c.json({ error: "Customer auth is disabled" }, 404);
1169
+ }
1170
+ const input = await parseJsonBody(c, customerBusinessAccountCreateInputSchema);
1171
+ const { db, dispose } = openDatabase(c.env);
1172
+ try {
1173
+ const context = await resolveCustomerAuthContext(c.env, c.req.raw);
1174
+ const policyError = requireBusinessOnboarding(c, context, "open");
1175
+ if (policyError)
1176
+ return policyError;
1177
+ if (!context.invitationAcceptBaseURL) {
1178
+ return c.json({ error: "Trusted storefront origin is not configured" }, 503);
1179
+ }
1180
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw, context);
1181
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
1182
+ if (!session)
1183
+ return c.json({ error: "Unauthorized" }, 401);
1184
+ let account;
1185
+ try {
1186
+ account = await onboardingRuntime.createBusinessAccount({ bindings: c.env, db }, {
1187
+ requesterUserId: session.user.id,
1188
+ storefrontOrigin: new URL(context.invitationAcceptBaseURL).origin,
1189
+ idempotencyKey: input.idempotencyKey,
1190
+ profile: input.profile,
1191
+ });
1192
+ }
1193
+ catch (error) {
1194
+ return onboardingErrorResponse(c, error);
1195
+ }
1196
+ const activationUrl = new URL(c.req.url);
1197
+ activationUrl.pathname = "/auth/customer/organization/set-active";
1198
+ activationUrl.search = "";
1199
+ const activationHeaders = new Headers(c.req.raw.headers);
1200
+ activationHeaders.delete("content-length");
1201
+ activationHeaders.set("content-type", "application/json");
1202
+ const activationResponse = await betterAuth.handler(new Request(activationUrl, {
1203
+ method: "POST",
1204
+ headers: activationHeaders,
1205
+ body: JSON.stringify({ organizationId: account.authOrganizationId }),
1206
+ }));
1207
+ if (!activationResponse.ok)
1208
+ return activationResponse;
1209
+ const headers = new Headers(activationResponse.headers);
1210
+ headers.delete("content-length");
1211
+ headers.set("cache-control", "no-store");
1212
+ headers.set("content-type", "application/json; charset=UTF-8");
1213
+ return new Response(JSON.stringify(account), { status: 201, headers });
1214
+ }
1215
+ finally {
1216
+ c.executionCtx.waitUntil(dispose());
1217
+ }
1218
+ });
1219
+ auth.get("/auth/customer/business-account-requests", async (c) => {
1220
+ c.header("Cache-Control", "no-store");
1221
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1222
+ return c.json({ error: "Customer auth is disabled" }, 404);
1223
+ }
1224
+ const query = customerBusinessAccountRequestListQuerySchema.parse(c.req.query());
1225
+ const { db, dispose } = openDatabase(c.env);
1226
+ try {
1227
+ const context = await resolveCustomerAuthContext(c.env, c.req.raw);
1228
+ const policyError = requireBusinessOnboarding(c, context, "request");
1229
+ if (policyError)
1230
+ return policyError;
1231
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw, context);
1232
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
1233
+ if (!session)
1234
+ return c.json({ error: "Unauthorized" }, 401);
1235
+ try {
1236
+ return c.json(await onboardingRuntime.listRequests({ bindings: c.env, db }, { requesterUserId: session.user.id, status: query.status }));
1237
+ }
1238
+ catch (error) {
1239
+ return onboardingErrorResponse(c, error);
1240
+ }
1241
+ }
1242
+ finally {
1243
+ c.executionCtx.waitUntil(dispose());
1244
+ }
1245
+ });
1246
+ auth.post("/auth/customer/business-account-requests", async (c) => {
1247
+ c.header("Cache-Control", "no-store");
1248
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1249
+ return c.json({ error: "Customer auth is disabled" }, 404);
1250
+ }
1251
+ const input = await parseJsonBody(c, customerBusinessAccountRequestCreateInputSchema);
1252
+ const { db, dispose } = openDatabase(c.env);
1253
+ try {
1254
+ const context = await resolveCustomerAuthContext(c.env, c.req.raw);
1255
+ const policyError = requireBusinessOnboarding(c, context, "request");
1256
+ if (policyError)
1257
+ return policyError;
1258
+ if (!context.invitationAcceptBaseURL) {
1259
+ return c.json({ error: "Trusted storefront origin is not configured" }, 503);
1260
+ }
1261
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw, context);
1262
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
1263
+ if (!session)
1264
+ return c.json({ error: "Unauthorized" }, 401);
1265
+ try {
1266
+ const request = await onboardingRuntime.requestBusinessAccount({ bindings: c.env, db }, {
1267
+ requesterUserId: session.user.id,
1268
+ storefrontOrigin: new URL(context.invitationAcceptBaseURL).origin,
1269
+ idempotencyKey: input.idempotencyKey,
1270
+ profile: input.profile,
1271
+ });
1272
+ return c.json(request, 201);
1273
+ }
1274
+ catch (error) {
1275
+ return onboardingErrorResponse(c, error);
1276
+ }
1277
+ }
1278
+ finally {
1279
+ c.executionCtx.waitUntil(dispose());
1280
+ }
1281
+ });
1282
+ auth.delete("/auth/customer/business-account-requests/:requestId", async (c) => {
1283
+ c.header("Cache-Control", "no-store");
1284
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1285
+ return c.json({ error: "Customer auth is disabled" }, 404);
1286
+ }
1287
+ const { db, dispose } = openDatabase(c.env);
1288
+ try {
1289
+ const context = await resolveCustomerAuthContext(c.env, c.req.raw);
1290
+ const policyError = requireBusinessOnboarding(c, context, "request");
1291
+ if (policyError)
1292
+ return policyError;
1293
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw, context);
1294
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
1295
+ if (!session)
1296
+ return c.json({ error: "Unauthorized" }, 401);
1297
+ try {
1298
+ return c.json(await onboardingRuntime.cancelRequest({ bindings: c.env, db }, { requestId: c.req.param("requestId"), requesterUserId: session.user.id }));
1299
+ }
1300
+ catch (error) {
1301
+ return onboardingErrorResponse(c, error);
1302
+ }
1303
+ }
1304
+ finally {
1305
+ c.executionCtx.waitUntil(dispose());
1306
+ }
1307
+ });
1308
+ auth.post("/auth/customer/business-account-invitations/accept", async (c) => {
1309
+ c.header("Cache-Control", "no-store");
1310
+ if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1311
+ return c.json({ error: "Customer auth is disabled" }, 404);
1312
+ }
1313
+ const input = await parseJsonBody(c, customerBusinessInvitationAcceptInputSchema);
1314
+ const { db, dispose } = openDatabase(c.env);
1315
+ try {
1316
+ const context = await resolveCustomerAuthContext(c.env, c.req.raw);
1317
+ const policyError = requireBusinessOnboarding(c, context, "invitation");
1318
+ if (policyError)
1319
+ return policyError;
1320
+ const betterAuth = await buildCustomerBetterAuth(c.env, db, c.req.raw, context);
1321
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
1322
+ if (!session)
1323
+ return c.json({ error: "Unauthorized" }, 401);
1324
+ const acceptUrl = new URL(c.req.url);
1325
+ acceptUrl.pathname = "/auth/customer/organization/accept-invitation";
1326
+ acceptUrl.search = "";
1327
+ const betterAuthHeaders = new Headers(c.req.raw.headers);
1328
+ betterAuthHeaders.delete("content-length");
1329
+ betterAuthHeaders.set("content-type", "application/json");
1330
+ const accepted = await betterAuth.handler(new Request(acceptUrl, {
1331
+ method: "POST",
1332
+ headers: betterAuthHeaders,
1333
+ body: JSON.stringify({ invitationId: input.invitationId }),
1334
+ }));
1335
+ if (!accepted.ok)
1336
+ return accepted;
1337
+ const acceptedPayload = (await accepted.clone().json());
1338
+ const organizationId = acceptedPayload.member?.organizationId ?? acceptedPayload.invitation?.organizationId;
1339
+ if (!organizationId) {
1340
+ return c.json({ error: "Accepted invitation did not identify an organization" }, 502);
1341
+ }
1342
+ const activationUrl = new URL(c.req.url);
1343
+ activationUrl.pathname = "/auth/customer/organization/set-active";
1344
+ activationUrl.search = "";
1345
+ const activated = await betterAuth.handler(new Request(activationUrl, {
1346
+ method: "POST",
1347
+ headers: betterAuthHeaders,
1348
+ body: JSON.stringify({ organizationId }),
1349
+ }));
1350
+ if (!activated.ok)
1351
+ return activated;
1352
+ const listed = await listCustomerBuyerAccounts({
1353
+ identity: {
1354
+ userId: session.user.id,
1355
+ name: session.user.name ?? null,
1356
+ email: session.user.email ?? null,
1357
+ },
1358
+ activeAuthOrganizationId: organizationId,
1359
+ policy: context.accountPolicy,
1360
+ store: createDrizzleCustomerBuyerAccountStore(db),
1361
+ });
1362
+ const account = listed.accounts.find((candidate) => candidate.kind === "business" && candidate.authOrganizationId === organizationId);
1363
+ if (!account) {
1364
+ return c.json({ error: "Accepted business account is unavailable" }, 409);
1365
+ }
1366
+ const headers = new Headers(accepted.headers);
1367
+ headers.delete("content-length");
1368
+ for (const cookie of activated.headers.getSetCookie())
1369
+ headers.append("set-cookie", cookie);
1370
+ headers.set("cache-control", "no-store");
1371
+ headers.set("content-type", "application/json; charset=UTF-8");
1372
+ return new Response(JSON.stringify({ account }), { status: 200, headers });
1373
+ }
1374
+ finally {
1375
+ c.executionCtx.waitUntil(dispose());
1376
+ }
1377
+ });
1094
1378
  auth.all("/auth/customer/*", async (c) => {
1095
1379
  if (c.env.VOYANT_CUSTOMER_AUTH_MODE?.trim() === "disabled") {
1096
1380
  return c.json({ error: "Customer auth is disabled" }, 404);