@voyant-travel/auth 0.124.2 → 0.125.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 (41) hide show
  1. package/dist/api-token-create.d.ts +2 -1
  2. package/dist/api-token-create.d.ts.map +1 -1
  3. package/dist/api-token-create.js +15 -9
  4. package/dist/api-token-create.js.map +1 -1
  5. package/dist/auth-facades.d.ts +2 -0
  6. package/dist/auth-facades.d.ts.map +1 -1
  7. package/dist/auth-facades.js +1 -1
  8. package/dist/auth-facades.js.map +1 -1
  9. package/dist/identity-access-graph-runtime.d.ts +32 -0
  10. package/dist/identity-access-graph-runtime.d.ts.map +1 -0
  11. package/dist/identity-access-graph-runtime.js +17 -0
  12. package/dist/identity-access-graph-runtime.js.map +1 -0
  13. package/dist/identity-access-runtime-port.d.ts +18 -0
  14. package/dist/identity-access-runtime-port.d.ts.map +1 -0
  15. package/dist/identity-access-runtime-port.js +15 -0
  16. package/dist/identity-access-runtime-port.js.map +1 -0
  17. package/dist/invitations-routes.d.ts +14 -0
  18. package/dist/invitations-routes.d.ts.map +1 -0
  19. package/dist/invitations-routes.js +242 -0
  20. package/dist/invitations-routes.js.map +1 -0
  21. package/dist/operator-node-runtime.d.ts +99 -0
  22. package/dist/operator-node-runtime.d.ts.map +1 -0
  23. package/dist/operator-node-runtime.js +688 -0
  24. package/dist/operator-node-runtime.js.map +1 -0
  25. package/dist/runtime-contributor.d.ts +7 -0
  26. package/dist/runtime-contributor.d.ts.map +1 -0
  27. package/dist/runtime-contributor.js +48 -0
  28. package/dist/runtime-contributor.js.map +1 -0
  29. package/dist/team-routes.d.ts +13 -0
  30. package/dist/team-routes.d.ts.map +1 -0
  31. package/dist/team-routes.js +211 -0
  32. package/dist/team-routes.js.map +1 -0
  33. package/dist/voyant.d.ts +3 -0
  34. package/dist/voyant.d.ts.map +1 -0
  35. package/dist/voyant.js +63 -0
  36. package/dist/voyant.js.map +1 -0
  37. package/openapi/admin/invitations.json +89 -0
  38. package/openapi/admin/team.json +171 -0
  39. package/openapi/storefront/invitations.json +87 -0
  40. package/package.json +62 -5
  41. package/dist/tsconfig.typecheck.tsbuildinfo +0 -1
@@ -0,0 +1,688 @@
1
+ /**
2
+ * Better Auth Node runtime for Hono.
3
+ *
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
+ *
6
+ * Mounts Better Auth at /auth/* for authentication operations.
7
+ * Same-origin — no CORS needed. Session cookies work naturally.
8
+ *
9
+ * Also provides /auth/status (user provisioning) and /auth/me (user info).
10
+ */
11
+ import { openNodeDatabase } from "@voyant-travel/db/runtime";
12
+ import { authUser, cloudAuthUserLinks, userProfilesTable, } from "@voyant-travel/db/schema/iam";
13
+ import { handleApiError, reportException, requestId, } from "@voyant-travel/hono/middleware/error-boundary";
14
+ import { getRequestId } from "@voyant-travel/hono/observability";
15
+ import { scopesForRole } from "@voyant-travel/types/member-roles";
16
+ import { eq, sql } from "drizzle-orm";
17
+ import { Hono } from "hono";
18
+ import { createVoyantCloudAdminAuthPlugin, revalidateVoyantCloudAdminAuthSession, revalidateVoyantCloudAdminAuthUser, } from "./cloud-admin-session.js";
19
+ import { buildClearCloudAdminAuthStateCookie, createCloudAdminAuthStart } from "./cloud-broker.js";
20
+ import { createBetterAuth, handleApiTokenManagementRequest, handleOrganizationMembersRequest, } from "./server.js";
21
+ import { ensureCurrentUserProfile } from "./workspace.js";
22
+ /** Resolve Better Auth's standard cross-subdomain cookie policy for Node hosts. */
23
+ export function buildBetterAuthCookieAdvancedOptions(env) {
24
+ const domain = env.AUTH_COOKIE_DOMAIN?.trim();
25
+ if (!domain)
26
+ return undefined;
27
+ return {
28
+ crossSubDomainCookies: { enabled: true, domain },
29
+ defaultCookieAttributes: { domain },
30
+ };
31
+ }
32
+ export function createOperatorAuthNodeRuntime(runtimeOptions) {
33
+ const openDatabase = runtimeOptions.openDatabase ??
34
+ ((env) => {
35
+ const resource = openNodeDatabase(env);
36
+ return { db: resource.db, dispose: resource.dispose };
37
+ });
38
+ const auth = new Hono();
39
+ // This lean auth app is dispatched around `createVoyantApp` (see
40
+ // hono-api-dispatch.ts), so it must mint/propagate the correlation id and wire
41
+ // the reporter itself — otherwise auth 5xx are an observability blind spot and
42
+ // the user-facing requestId wouldn't be findable (RFC voyant#1553).
43
+ auth.use("*", requestId);
44
+ // `onError` only fires on THROWN exceptions. Several handlers (e.g.
45
+ // /auth/status, the cloud token/revalidate/callback paths) instead RETURN a
46
+ // 5xx via `c.json(..., 50x)`; those bypass `onError`, and this lean app is
47
+ // dispatched around `createVoyantApp` so the framework's auth status-bridge
48
+ // (`mountAuthForwarding`) never runs either. Bridge returned 5xx to the
49
+ // reporter here so they aren't an observability blind spot (RFC voyant#1553).
50
+ // Thrown errors skip this middleware's post-`next()` code (the rejection
51
+ // propagates straight to `onError`), so there's no double report.
52
+ auth.use("*", async (c, next) => {
53
+ await next();
54
+ if (c.res.status >= 500) {
55
+ reportException(runtimeOptions.reporter, c, {
56
+ requestId: getRequestId() ?? "",
57
+ app: runtimeOptions.appName,
58
+ error: new Error(`auth handler returned HTTP ${c.res.status}`),
59
+ context: { path: c.req.path, method: c.req.method, status: c.res.status, surface: "auth" },
60
+ });
61
+ }
62
+ });
63
+ auth.onError((err, c) => handleApiError(err, c, { reporter: runtimeOptions.reporter, appName: runtimeOptions.appName }));
64
+ const DEFAULT_APP_URL = "http://localhost:3300";
65
+ const CLOUD_BETTER_AUTH_ALLOWLIST = new Set([
66
+ "/auth/get-session",
67
+ "/auth/jwks",
68
+ "/auth/session",
69
+ "/auth/sign-out",
70
+ "/auth/token",
71
+ ]);
72
+ function resolveOperatorAuthMode(env) {
73
+ const mode = env.VOYANT_ADMIN_AUTH_MODE?.trim() || "local";
74
+ if (mode === "local" || mode === "voyant-cloud") {
75
+ return mode;
76
+ }
77
+ console.error(`[auth] Invalid VOYANT_ADMIN_AUTH_MODE="${mode}". Failing closed as voyant-cloud.`);
78
+ return "voyant-cloud";
79
+ }
80
+ function isVoyantCloudAuthMode(env) {
81
+ return resolveOperatorAuthMode(env) === "voyant-cloud";
82
+ }
83
+ function isCloudAllowedBetterAuthRoute(request) {
84
+ const url = new URL(request.url);
85
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
86
+ return CLOUD_BETTER_AUTH_ALLOWLIST.has(pathname);
87
+ }
88
+ function localAuthDisabledResponse(c) {
89
+ return c.json({ error: "Local auth routes are disabled in Voyant Cloud auth mode" }, 404);
90
+ }
91
+ function cloudAuthNotConfiguredResponse(c) {
92
+ return c.json({ error: "Voyant Cloud auth broker is not configured yet" }, 501);
93
+ }
94
+ function normalizeUrl(url) {
95
+ return url.trim().replace(/\/$/, "");
96
+ }
97
+ function getAppUrl(env) {
98
+ const candidates = [
99
+ env.APP_URL,
100
+ env.DASH_BASE_URL,
101
+ env.CORS_ALLOWLIST?.split(",")[0],
102
+ DEFAULT_APP_URL,
103
+ ];
104
+ for (const candidate of candidates) {
105
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
106
+ return normalizeUrl(candidate);
107
+ }
108
+ }
109
+ return DEFAULT_APP_URL;
110
+ }
111
+ function getTrustedOrigins(env) {
112
+ return Array.from(new Set([env.APP_URL, env.DASH_BASE_URL, ...(env.CORS_ALLOWLIST ?? "").split(",")]
113
+ .map((value) => value?.trim())
114
+ .filter((value) => Boolean(value)))).map(normalizeUrl);
115
+ }
116
+ function getAuthBaseUrl(env) {
117
+ // entry.ts strips /api before delegating to the Hono app, so Better Auth
118
+ // sees paths like /auth/*. Its baseURL must be the origin only (no /api).
119
+ const appUrl = getAppUrl(env);
120
+ try {
121
+ const parsed = new URL(appUrl);
122
+ return `${parsed.protocol}//${parsed.host}`;
123
+ }
124
+ catch {
125
+ return appUrl;
126
+ }
127
+ }
128
+ function getPublicApiBaseUrl(env) {
129
+ const candidate = env.API_BASE_URL?.trim() || env.APP_URL?.trim() || `${getAppUrl(env)}/api`;
130
+ const normalized = normalizeUrl(candidate);
131
+ try {
132
+ const parsed = new URL(normalized);
133
+ if (parsed.pathname === "/" || parsed.pathname === "") {
134
+ parsed.pathname = "/api";
135
+ return normalizeUrl(parsed.toString());
136
+ }
137
+ }
138
+ catch {
139
+ return normalized;
140
+ }
141
+ return normalized;
142
+ }
143
+ function getCloudAuthStartConfig(env) {
144
+ const deploymentId = env.VOYANT_CLOUD_DEPLOYMENT_ID?.trim();
145
+ const cloudAuthStartUrl = env.VOYANT_CLOUD_ADMIN_AUTH_START_URL?.trim();
146
+ if (!deploymentId || !cloudAuthStartUrl)
147
+ return null;
148
+ return {
149
+ cloudAuthStartUrl,
150
+ deploymentId,
151
+ adminCallbackUrl: `${getPublicApiBaseUrl(env)}/auth/cloud/callback`,
152
+ cookieSecret: env.SESSION_CLAIMS_SECRET,
153
+ appId: env.VOYANT_CLOUD_APP_ID?.trim() || undefined,
154
+ environment: env.VOYANT_CLOUD_ENVIRONMENT?.trim() || undefined,
155
+ };
156
+ }
157
+ function getCloudAuthExchangeConfig(env) {
158
+ const deploymentId = env.VOYANT_CLOUD_DEPLOYMENT_ID?.trim();
159
+ const exchangeUrl = env.VOYANT_CLOUD_ADMIN_AUTH_EXCHANGE_URL?.trim();
160
+ const assertionJwksUrl = env.VOYANT_CLOUD_ADMIN_AUTH_JWKS_URL?.trim();
161
+ const clientToken = env.VOYANT_CLOUD_ADMIN_AUTH_CLIENT_TOKEN?.trim();
162
+ if (!deploymentId || !exchangeUrl || !assertionJwksUrl || !clientToken)
163
+ return null;
164
+ return {
165
+ exchangeUrl,
166
+ deploymentId,
167
+ clientToken,
168
+ assertionJwksUrl,
169
+ assertionAudience: env.VOYANT_CLOUD_ADMIN_AUTH_AUDIENCE?.trim() || deploymentId,
170
+ };
171
+ }
172
+ function getCloudAuthRevalidateConfig(env) {
173
+ const deploymentId = env.VOYANT_CLOUD_DEPLOYMENT_ID?.trim();
174
+ const revalidateUrl = env.VOYANT_CLOUD_ADMIN_AUTH_REVALIDATE_URL?.trim();
175
+ const clientToken = env.VOYANT_CLOUD_ADMIN_AUTH_CLIENT_TOKEN?.trim();
176
+ if (!deploymentId || !revalidateUrl || !clientToken)
177
+ return null;
178
+ return {
179
+ revalidateUrl,
180
+ deploymentId,
181
+ clientToken,
182
+ };
183
+ }
184
+ function isLocalRequest(request) {
185
+ const hostname = new URL(request.url).hostname;
186
+ return hostname === "127.0.0.1" || hostname === "localhost";
187
+ }
188
+ function shouldUseBrowserEvidenceAuthFallback(env, request) {
189
+ return env.VOYANT_OPERATOR_BROWSER_EVIDENCE === "1" && isLocalRequest(request);
190
+ }
191
+ function allowAuthSecretLogging(env) {
192
+ return env.VOYANT_AUTH_LOG_SECRET_FALLBACKS === "1";
193
+ }
194
+ function buildBetterAuth(env, db, options = {}) {
195
+ const emailSender = runtimeOptions.resolveEmailSender?.(env);
196
+ const cloudAuthExchange = isVoyantCloudAuthMode(env) ? getCloudAuthExchangeConfig(env) : null;
197
+ const authDb = db;
198
+ const cloudAuthDb = db;
199
+ return createBetterAuth({
200
+ // `db` is a `NeonDatabase` (neon-serverless WebSocket); the
201
+ // `CreateBetterAuthOptions.db` type still references the older
202
+ // `getDb` return union (postgres-js + neon-http). Drizzle's
203
+ // PgDatabase surface is identical across flavors at runtime, so
204
+ // the cast is structurally safe — better-auth's drizzleAdapter
205
+ // works on any PgDatabase. See #500 for context.
206
+ db: authDb,
207
+ secret: env.BETTER_AUTH_SECRET,
208
+ baseURL: getAuthBaseUrl(env),
209
+ basePath: "/auth",
210
+ trustedOrigins: getTrustedOrigins(env),
211
+ advanced: (runtimeOptions.cookieAdvanced ?? buildBetterAuthCookieAdvancedOptions)(env),
212
+ customerSignupSurfaces: options.customerSignup ? ["customer"] : undefined,
213
+ plugins: cloudAuthExchange
214
+ ? [
215
+ createVoyantCloudAdminAuthPlugin({
216
+ db: cloudAuthDb,
217
+ cookieSecret: env.SESSION_CLAIMS_SECRET,
218
+ exchange: cloudAuthExchange,
219
+ }),
220
+ ]
221
+ : undefined,
222
+ sendResetPassword: async ({ user, url }) => {
223
+ if (!emailSender) {
224
+ // No email provider (e.g. local dev without a sending domain): with the
225
+ // debug flag on, log the link to the console instead of sending;
226
+ // otherwise fail loudly. Never bypasses a configured cloud sender.
227
+ if (allowAuthSecretLogging(env)) {
228
+ console.info(`[auth] reset-password (debug fallback) -> ${user.email}: ${url}`);
229
+ return;
230
+ }
231
+ throw new Error("Password reset email provider is not configured");
232
+ }
233
+ await emailSender.sendResetPassword({ user, url });
234
+ },
235
+ sendVerificationOTP: async ({ email, otp, type }) => {
236
+ if (!emailSender) {
237
+ // No email provider (e.g. local dev without a sending domain): with the
238
+ // debug flag on, log the OTP to the console instead of sending;
239
+ // otherwise fail loudly. Never bypasses a configured cloud sender.
240
+ if (allowAuthSecretLogging(env)) {
241
+ console.info(`[auth] verification-otp (debug fallback) [${type}] -> ${email}: ${otp}`);
242
+ return;
243
+ }
244
+ throw new Error("Verification OTP email provider is not configured");
245
+ }
246
+ await emailSender.sendVerificationOtp({ email, otp, type });
247
+ },
248
+ });
249
+ }
250
+ function rewriteCustomerAuthRequest(request) {
251
+ const url = new URL(request.url);
252
+ url.pathname = url.pathname.replace("/auth/customer", "/auth");
253
+ return new Request(url, request);
254
+ }
255
+ const FULL_ACCESS_SCOPES = ["*"];
256
+ function scopesForOperatorRole(role) {
257
+ const base = scopesForRole(role);
258
+ if (!base)
259
+ return null;
260
+ const normalizedRole = (role ?? "").trim().toLowerCase();
261
+ const presetId = normalizedRole === "member"
262
+ ? "editor"
263
+ : normalizedRole === "guest"
264
+ ? "viewer"
265
+ : normalizedRole;
266
+ const selected = runtimeOptions.accessCatalog.presets.find((preset) => preset.kind === "staff" && preset.id === presetId);
267
+ return [...new Set([...base, ...(selected?.grants ?? [])])].sort();
268
+ }
269
+ /**
270
+ * Resolve a member's RBAC scope set for the request (`resource:action` strings,
271
+ * shared with API keys — see @voyant-travel/types/member-roles, RFC voyant#2085).
272
+ *
273
+ * Phase 1: storage + seam are wired but default to full access, so behavior is
274
+ * unchanged until an admin assigns permissions (Phase 2) and routes gate on them
275
+ * (Phase 3).
276
+ * - voyant-cloud: assertion-mirrored `cloud_auth_user_links.scopes` if the
277
+ * platform sent them, else the role bundle for `roleSlug`, else full access.
278
+ * - local: the member's `user_profiles.permissions` if assigned, else full
279
+ * access (no local role concept yet).
280
+ */
281
+ async function resolveMemberScopes(db, env, userId) {
282
+ if (isVoyantCloudAuthMode(env)) {
283
+ const [link] = await db
284
+ .select({ scopes: cloudAuthUserLinks.scopes, roleSlug: cloudAuthUserLinks.roleSlug })
285
+ .from(cloudAuthUserLinks)
286
+ .where(eq(cloudAuthUserLinks.userId, userId))
287
+ .limit(1);
288
+ return link?.scopes ?? scopesForOperatorRole(link?.roleSlug) ?? FULL_ACCESS_SCOPES;
289
+ }
290
+ const [profile] = await db
291
+ .select({ permissions: userProfilesTable.permissions })
292
+ .from(userProfilesTable)
293
+ .where(eq(userProfilesTable.id, userId))
294
+ .limit(1);
295
+ return profile?.permissions ?? FULL_ACCESS_SCOPES;
296
+ }
297
+ async function resolveAuthRequest(request, env) {
298
+ const { db, dispose } = openDatabase(env);
299
+ try {
300
+ const auth = buildBetterAuth(env, db);
301
+ const session = await auth.api.getSession({ headers: request.headers });
302
+ if (!session) {
303
+ return null;
304
+ }
305
+ if (isVoyantCloudAuthMode(env)) {
306
+ const revalidateConfig = getCloudAuthRevalidateConfig(env);
307
+ if (!revalidateConfig) {
308
+ return null;
309
+ }
310
+ try {
311
+ const revalidation = await revalidateVoyantCloudAdminAuthSession({
312
+ db: db,
313
+ sessionId: session.session.id,
314
+ config: revalidateConfig,
315
+ });
316
+ if (!revalidation.ok) {
317
+ return null;
318
+ }
319
+ }
320
+ catch (error) {
321
+ console.error("[auth/session] Cloud revalidation failed:", error);
322
+ return null;
323
+ }
324
+ }
325
+ return {
326
+ userId: session.user.id,
327
+ sessionId: session.session.id,
328
+ organizationId: null,
329
+ callerType: "session",
330
+ actor: "staff",
331
+ // Member RBAC scope set (RFC voyant#2085). Defaults to full access until
332
+ // an admin assigns permissions, so existing deployments are unchanged.
333
+ // `actor: "staff"` is retained, so actor-gated paths (incl. the
334
+ // bookings-pii reveal, which short-circuits on staff) are unaffected.
335
+ scopes: await resolveMemberScopes(db, env, session.user.id),
336
+ email: session.user.email ?? null,
337
+ };
338
+ }
339
+ finally {
340
+ // No `executionCtx` reachable here (called from middleware that
341
+ // doesn't pass one through). Await inline so the WebSocket closes
342
+ // before this fn returns.
343
+ await dispose();
344
+ }
345
+ }
346
+ /**
347
+ * Single-tenant: every authenticated session is a staff user with full access.
348
+ * When `resolveAuthRequest` has already granted a session, permissions are
349
+ * implicitly allowed. If you need granular RBAC later, switch on
350
+ * `user_profiles.isSuperAdmin` / `isSupportUser` here.
351
+ */
352
+ async function hasAuthPermission(request, env) {
353
+ const auth = await resolveAuthRequest(request, env);
354
+ return auth !== null;
355
+ }
356
+ class CurrentUserNotFoundError extends Error {
357
+ constructor() {
358
+ super("User not found");
359
+ this.name = "CurrentUserNotFoundError";
360
+ }
361
+ }
362
+ async function getCurrentUserForRequest(request, env) {
363
+ if (shouldUseBrowserEvidenceAuthFallback(env, request)) {
364
+ return null;
365
+ }
366
+ const { db, dispose } = openDatabase(env);
367
+ try {
368
+ const betterAuth = buildBetterAuth(env, db);
369
+ const session = await betterAuth.api.getSession({ headers: request.headers });
370
+ if (!session) {
371
+ return null;
372
+ }
373
+ const [row] = await db
374
+ .select({
375
+ id: authUser.id,
376
+ email: authUser.email,
377
+ createdAt: authUser.createdAt,
378
+ firstName: userProfilesTable.firstName,
379
+ lastName: userProfilesTable.lastName,
380
+ locale: userProfilesTable.locale,
381
+ timezone: userProfilesTable.timezone,
382
+ uiPrefs: userProfilesTable.uiPrefs,
383
+ avatarUrl: userProfilesTable.avatarUrl,
384
+ isSuperAdmin: userProfilesTable.isSuperAdmin,
385
+ isSupportUser: userProfilesTable.isSupportUser,
386
+ })
387
+ .from(authUser)
388
+ .leftJoin(userProfilesTable, eq(userProfilesTable.id, authUser.id))
389
+ .where(eq(authUser.id, session.user.id))
390
+ .limit(1);
391
+ if (!row) {
392
+ throw new CurrentUserNotFoundError();
393
+ }
394
+ return {
395
+ id: row.id,
396
+ email: row.email ?? session.user.email ?? "",
397
+ firstName: row.firstName ?? null,
398
+ lastName: row.lastName ?? null,
399
+ locale: row.locale ?? "en",
400
+ timezone: row.timezone ?? null,
401
+ uiPrefs: row.uiPrefs ?? null,
402
+ isSuperAdmin: row.isSuperAdmin ?? false,
403
+ isSupportUser: row.isSupportUser ?? false,
404
+ createdAt: row.createdAt?.toISOString() ?? new Date().toISOString(),
405
+ profilePictureUrl: row.avatarUrl ?? null,
406
+ };
407
+ }
408
+ finally {
409
+ await dispose();
410
+ }
411
+ }
412
+ async function getBootstrapStatusForRequest(request, env) {
413
+ if (shouldUseBrowserEvidenceAuthFallback(env, request)) {
414
+ return { hasUsers: true };
415
+ }
416
+ if (isVoyantCloudAuthMode(env)) {
417
+ return { hasUsers: true, authMode: "voyant-cloud" };
418
+ }
419
+ const { db, dispose } = openDatabase(env);
420
+ try {
421
+ const [row] = await db.select({ count: sql `count(*)::int` }).from(authUser);
422
+ return { hasUsers: (row?.count ?? 0) > 0, authMode: "local" };
423
+ }
424
+ finally {
425
+ await dispose();
426
+ }
427
+ }
428
+ async function validateApiTokenAccess(env, db, apiKey) {
429
+ if (!isVoyantCloudAuthMode(env))
430
+ return true;
431
+ const revalidateConfig = getCloudAuthRevalidateConfig(env);
432
+ if (!revalidateConfig)
433
+ return false;
434
+ try {
435
+ const revalidation = await revalidateVoyantCloudAdminAuthUser({
436
+ db: db,
437
+ userId: apiKey.referenceId,
438
+ config: revalidateConfig,
439
+ });
440
+ return revalidation.ok;
441
+ }
442
+ catch (error) {
443
+ console.error("[auth/api-token] Cloud revalidation failed:", error);
444
+ return false;
445
+ }
446
+ }
447
+ /**
448
+ * GET /auth/me
449
+ * Returns the current authenticated user's profile.
450
+ * Validates the session cookie directly (no Bearer token needed).
451
+ */
452
+ auth.get("/auth/me", async (c) => {
453
+ try {
454
+ const currentUser = await getCurrentUserForRequest(c.req.raw, c.env);
455
+ if (!currentUser) {
456
+ return c.json({ error: "Unauthorized" }, 401);
457
+ }
458
+ return c.json(currentUser);
459
+ }
460
+ catch (error) {
461
+ if (error instanceof CurrentUserNotFoundError) {
462
+ return c.json({ error: "User not found" }, 404);
463
+ }
464
+ throw error;
465
+ }
466
+ });
467
+ /**
468
+ * GET /auth/status
469
+ * Ensures the authenticated user has a user_profiles row.
470
+ * Profile is normally created by the BA databaseHook on sign-up,
471
+ * but this route serves as an idempotent fallback.
472
+ */
473
+ auth.get("/auth/status", async (c) => {
474
+ // See `/auth/me` above — auth sub-app runs before the request `db`
475
+ // middleware, so own the Pool here.
476
+ const { db, dispose } = openDatabase(c.env);
477
+ try {
478
+ const betterAuth = buildBetterAuth(c.env, db);
479
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
480
+ if (!session) {
481
+ return c.json({ userExists: false, authenticated: false });
482
+ }
483
+ const userId = session.user.id;
484
+ const status = await ensureCurrentUserProfile(db, userId);
485
+ if (!status.userExists && status.authenticated) {
486
+ console.error("[auth/status] Error:", status.reason);
487
+ return c.json(status, 500);
488
+ }
489
+ return c.json(status);
490
+ }
491
+ finally {
492
+ c.executionCtx.waitUntil(dispose());
493
+ }
494
+ });
495
+ /**
496
+ * GET /auth/bootstrap-status
497
+ * Public endpoint — reveals whether any user exists. Used by the sign-in /
498
+ * sign-up route loaders to pick the right flow.
499
+ */
500
+ auth.get("/auth/bootstrap-status", async (c) => {
501
+ return c.json(await getBootstrapStatusForRequest(c.req.raw, c.env));
502
+ });
503
+ async function handleApiTokensFacade(c) {
504
+ const { db, dispose } = openDatabase(c.env);
505
+ try {
506
+ const betterAuth = buildBetterAuth(c.env, db);
507
+ const cloudAuthDb = db;
508
+ if (isVoyantCloudAuthMode(c.env)) {
509
+ const revalidateConfig = getCloudAuthRevalidateConfig(c.env);
510
+ if (!revalidateConfig) {
511
+ return c.json({ error: "Cloud-mode API token management requires membership revalidation" }, 501);
512
+ }
513
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
514
+ if (!session) {
515
+ return c.json({ error: "Unauthorized" }, 401);
516
+ }
517
+ try {
518
+ const revalidation = await revalidateVoyantCloudAdminAuthSession({
519
+ db: cloudAuthDb,
520
+ sessionId: session.session.id,
521
+ config: revalidateConfig,
522
+ });
523
+ if (!revalidation.ok) {
524
+ return c.json({ error: "Voyant Cloud access revoked" }, 403);
525
+ }
526
+ }
527
+ catch (error) {
528
+ console.error("[auth/api-tokens] Cloud revalidation failed:", error);
529
+ return c.json({ error: "Voyant Cloud access could not be revalidated" }, 503);
530
+ }
531
+ }
532
+ return ((await handleApiTokenManagementRequest(c.req.raw, betterAuth, {
533
+ db,
534
+ accessCatalog: runtimeOptions.accessCatalog,
535
+ })) ?? c.json({ error: "Not found" }, 404));
536
+ }
537
+ finally {
538
+ c.executionCtx.waitUntil(dispose());
539
+ }
540
+ }
541
+ async function handleOrganizationMembersFacade(c) {
542
+ const { db, dispose } = openDatabase(c.env);
543
+ try {
544
+ const betterAuth = buildBetterAuth(c.env, db);
545
+ const cloudAuthDb = db;
546
+ if (isVoyantCloudAuthMode(c.env)) {
547
+ const revalidateConfig = getCloudAuthRevalidateConfig(c.env);
548
+ if (!revalidateConfig) {
549
+ return c.json({ error: "Cloud-mode organization member lookup requires membership revalidation" }, 501);
550
+ }
551
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
552
+ if (!session) {
553
+ return c.json({ error: "Unauthorized" }, 401);
554
+ }
555
+ try {
556
+ const revalidation = await revalidateVoyantCloudAdminAuthSession({
557
+ db: cloudAuthDb,
558
+ sessionId: session.session.id,
559
+ config: revalidateConfig,
560
+ });
561
+ if (!revalidation.ok) {
562
+ return c.json({ error: "Voyant Cloud access revoked" }, 403);
563
+ }
564
+ }
565
+ catch (error) {
566
+ console.error("[auth/organization-members] Cloud revalidation failed:", error);
567
+ return c.json({ error: "Voyant Cloud access could not be revalidated" }, 503);
568
+ }
569
+ }
570
+ const organizationMembersDb = db;
571
+ return ((await handleOrganizationMembersRequest(c.req.raw, betterAuth, {
572
+ db: organizationMembersDb,
573
+ })) ?? c.json({ error: "Not found" }, 404));
574
+ }
575
+ finally {
576
+ c.executionCtx.waitUntil(dispose());
577
+ }
578
+ }
579
+ auth.get("/auth/cloud/start", async (c) => {
580
+ if (!isVoyantCloudAuthMode(c.env)) {
581
+ return c.json({ error: "Not found" }, 404);
582
+ }
583
+ const config = getCloudAuthStartConfig(c.env);
584
+ if (!config) {
585
+ return cloudAuthNotConfiguredResponse(c);
586
+ }
587
+ try {
588
+ const start = await createCloudAdminAuthStart({
589
+ requestUrl: c.req.url,
590
+ next: c.req.query("next"),
591
+ config,
592
+ });
593
+ return new Response(null, {
594
+ status: 302,
595
+ headers: {
596
+ Location: start.redirectUrl,
597
+ "Set-Cookie": start.setCookie,
598
+ },
599
+ });
600
+ }
601
+ catch (error) {
602
+ console.error("[auth/cloud/start] Error:", error);
603
+ return c.json({ error: "Voyant Cloud auth broker is misconfigured" }, 500);
604
+ }
605
+ });
606
+ auth.get("/auth/cloud/callback", async (c) => {
607
+ if (!isVoyantCloudAuthMode(c.env)) {
608
+ return c.json({ error: "Not found" }, 404);
609
+ }
610
+ const exchangeConfig = getCloudAuthExchangeConfig(c.env);
611
+ if (!exchangeConfig) {
612
+ const url = new URL(c.req.url);
613
+ return c.json({ error: "Voyant Cloud auth exchange is not configured yet" }, 501, {
614
+ "Set-Cookie": buildClearCloudAdminAuthStateCookie(url.protocol === "https:", url.pathname.replace(/\/callback$/, "") || "/auth/cloud"),
615
+ });
616
+ }
617
+ const { db, dispose } = openDatabase(c.env);
618
+ try {
619
+ const betterAuth = buildBetterAuth(c.env, db);
620
+ return await betterAuth.handler(c.req.raw);
621
+ }
622
+ catch (error) {
623
+ console.error("[auth/cloud/callback] Error:", error);
624
+ return c.json({ error: "Voyant Cloud auth callback failed" }, 500);
625
+ }
626
+ finally {
627
+ c.executionCtx.waitUntil(dispose());
628
+ }
629
+ });
630
+ auth.all("/auth/api-tokens", handleApiTokensFacade);
631
+ auth.all("/auth/api-tokens/:keyId", handleApiTokensFacade);
632
+ auth.all("/auth/api-tokens/:keyId/rotate", handleApiTokensFacade);
633
+ auth.get("/auth/organization/list-members", handleOrganizationMembersFacade);
634
+ auth.get("/auth/customer/status", async (c) => {
635
+ const { db, dispose } = openDatabase(c.env);
636
+ try {
637
+ const betterAuth = buildBetterAuth(c.env, db, { customerSignup: true });
638
+ const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
639
+ return c.json({ authenticated: Boolean(session) });
640
+ }
641
+ finally {
642
+ c.executionCtx.waitUntil(dispose());
643
+ }
644
+ });
645
+ auth.all("/auth/customer/*", async (c) => {
646
+ if (isVoyantCloudAuthMode(c.env) &&
647
+ !isCloudAllowedBetterAuthRoute(rewriteCustomerAuthRequest(c.req.raw))) {
648
+ return localAuthDisabledResponse(c);
649
+ }
650
+ const { db, dispose } = openDatabase(c.env);
651
+ try {
652
+ const betterAuth = buildBetterAuth(c.env, db, { customerSignup: true });
653
+ return await betterAuth.handler(rewriteCustomerAuthRequest(c.req.raw));
654
+ }
655
+ finally {
656
+ c.executionCtx.waitUntil(dispose());
657
+ }
658
+ });
659
+ /**
660
+ * Catch-all: delegate to Better Auth handler.
661
+ * Handles sign-in, sign-up, sign-out, password reset, OAuth callbacks, etc.
662
+ *
663
+ * Sign-up is gated by a `user.create.before` hook in @voyant-travel/auth — once a
664
+ * user exists, the hook throws and BA returns an error to the client.
665
+ */
666
+ auth.all("/auth/*", async (c) => {
667
+ if (isVoyantCloudAuthMode(c.env) && !isCloudAllowedBetterAuthRoute(c.req.raw)) {
668
+ return localAuthDisabledResponse(c);
669
+ }
670
+ const { db, dispose } = openDatabase(c.env);
671
+ try {
672
+ const betterAuth = buildBetterAuth(c.env, db);
673
+ return await betterAuth.handler(c.req.raw);
674
+ }
675
+ finally {
676
+ c.executionCtx.waitUntil(dispose());
677
+ }
678
+ });
679
+ return {
680
+ handler: auth,
681
+ getBootstrapStatusForRequest,
682
+ getCurrentUserForRequest,
683
+ hasAuthPermission,
684
+ resolveAuthRequest,
685
+ validateApiTokenAccess,
686
+ };
687
+ }
688
+ //# sourceMappingURL=operator-node-runtime.js.map