@voyant-travel/auth 0.133.1 → 0.133.2

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.
@@ -0,0 +1,50 @@
1
+ /** Provider-neutral identities crossing the auth/runtime boundary. */
2
+ export type AuthRealm = "admin" | "customer";
3
+ interface PrincipalBase {
4
+ realm: AuthRealm;
5
+ userId: string;
6
+ sessionId: string;
7
+ email: string | null;
8
+ }
9
+ export interface AdminPrincipal extends PrincipalBase {
10
+ realm: "admin";
11
+ actor: "staff";
12
+ scopes: readonly string[];
13
+ }
14
+ export interface CustomerPrincipal extends PrincipalBase {
15
+ realm: "customer";
16
+ actor: "customer";
17
+ }
18
+ export type AuthPrincipal = AdminPrincipal | CustomerPrincipal;
19
+ /** Complete session replacement seam for advanced self-host integrations. */
20
+ export interface AuthRealmSessionPort<Context = unknown> {
21
+ readonly realm: AuthRealm;
22
+ resolve(request: Request, context: Context): Promise<AuthPrincipal | null>;
23
+ signOut(request: Request, context: Context): Promise<Response>;
24
+ revokeUserSessions(userId: string, context: Context): Promise<void>;
25
+ }
26
+ export interface ExternalIdentity {
27
+ providerId: string;
28
+ providerAccountId: string;
29
+ email: string | null;
30
+ emailVerified: boolean;
31
+ name: string | null;
32
+ image: string | null;
33
+ }
34
+ /**
35
+ * Preferred external-IdP seam. Auth0, Clerk, WorkOS, or generic OIDC adapters
36
+ * verify their assertion here; Better Auth remains the local session issuer.
37
+ */
38
+ export interface ExternalIdentityAdapter<Input, Context = unknown> {
39
+ readonly id: string;
40
+ resolve(input: Input, context: Context): Promise<ExternalIdentity>;
41
+ revalidate?(identity: ExternalIdentity, context: Context): Promise<boolean>;
42
+ }
43
+ export interface BetterAuthIdentityBridge<Context = unknown> {
44
+ createSession(identity: ExternalIdentity, context: Context): Promise<Response>;
45
+ }
46
+ export declare function createExternalIdentitySessionAdapter<Input, Context = unknown>(identityAdapter: ExternalIdentityAdapter<Input, Context>, betterAuthBridge: BetterAuthIdentityBridge<Context>): {
47
+ id: string;
48
+ authenticate(input: Input, context: Context): Promise<Response>;
49
+ };
50
+ export {};
@@ -0,0 +1,13 @@
1
+ export function createExternalIdentitySessionAdapter(identityAdapter, betterAuthBridge) {
2
+ return {
3
+ id: identityAdapter.id,
4
+ async authenticate(input, context) {
5
+ const identity = await identityAdapter.resolve(input, context);
6
+ if (identityAdapter.revalidate && !(await identityAdapter.revalidate(identity, context))) {
7
+ return Response.json({ error: "External identity access was revoked" }, { status: 403 });
8
+ }
9
+ return betterAuthBridge.createSession(identity, context);
10
+ },
11
+ };
12
+ }
13
+ //# sourceMappingURL=auth-realms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-realms.js","sourceRoot":"","sources":["../src/auth-realms.ts"],"names":[],"mappings":"AAsDA,MAAM,UAAU,oCAAoC,CAClD,eAAwD,EACxD,gBAAmD;IAEnD,OAAO;QACL,EAAE,EAAE,eAAe,CAAC,EAAE;QACtB,KAAK,CAAC,YAAY,CAAC,KAAY,EAAE,OAAgB;YAC/C,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;YAC9D,IAAI,eAAe,CAAC,UAAU,IAAI,CAAC,CAAC,MAAM,eAAe,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;gBACzF,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sCAAsC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;YAC1F,CAAC;YACD,OAAO,gBAAgB,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC1D,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/** Provider-neutral identities crossing the auth/runtime boundary. */\nexport type AuthRealm = \"admin\" | \"customer\"\n\ninterface PrincipalBase {\n realm: AuthRealm\n userId: string\n sessionId: string\n email: string | null\n}\n\nexport interface AdminPrincipal extends PrincipalBase {\n realm: \"admin\"\n actor: \"staff\"\n scopes: readonly string[]\n}\n\nexport interface CustomerPrincipal extends PrincipalBase {\n realm: \"customer\"\n actor: \"customer\"\n}\n\nexport type AuthPrincipal = AdminPrincipal | CustomerPrincipal\n\n/** Complete session replacement seam for advanced self-host integrations. */\nexport interface AuthRealmSessionPort<Context = unknown> {\n readonly realm: AuthRealm\n resolve(request: Request, context: Context): Promise<AuthPrincipal | null>\n signOut(request: Request, context: Context): Promise<Response>\n revokeUserSessions(userId: string, context: Context): Promise<void>\n}\n\nexport interface ExternalIdentity {\n providerId: string\n providerAccountId: string\n email: string | null\n emailVerified: boolean\n name: string | null\n image: string | null\n}\n\n/**\n * Preferred external-IdP seam. Auth0, Clerk, WorkOS, or generic OIDC adapters\n * verify their assertion here; Better Auth remains the local session issuer.\n */\nexport interface ExternalIdentityAdapter<Input, Context = unknown> {\n readonly id: string\n resolve(input: Input, context: Context): Promise<ExternalIdentity>\n revalidate?(identity: ExternalIdentity, context: Context): Promise<boolean>\n}\n\nexport interface BetterAuthIdentityBridge<Context = unknown> {\n createSession(identity: ExternalIdentity, context: Context): Promise<Response>\n}\n\nexport function createExternalIdentitySessionAdapter<Input, Context = unknown>(\n identityAdapter: ExternalIdentityAdapter<Input, Context>,\n betterAuthBridge: BetterAuthIdentityBridge<Context>,\n) {\n return {\n id: identityAdapter.id,\n async authenticate(input: Input, context: Context): Promise<Response> {\n const identity = await identityAdapter.resolve(input, context)\n if (identityAdapter.revalidate && !(await identityAdapter.revalidate(identity, context))) {\n return Response.json({ error: \"External identity access was revoked\" }, { status: 403 })\n }\n return betterAuthBridge.createSession(identity, context)\n },\n }\n}\n"]}
package/dist/edge.d.ts CHANGED
@@ -11,6 +11,11 @@
11
11
  * With the identity table collapse, authUser.id IS the userId —
12
12
  * no extra identity lookup required.
13
13
  */
14
+ export interface EdgeAuthRealmOptions {
15
+ realm?: "admin" | "customer";
16
+ cookieName?: string;
17
+ secret?: string;
18
+ }
14
19
  type AuthContext = {
15
20
  userId: string | null;
16
21
  email: string | null;
@@ -23,5 +28,5 @@ type AuthContext = {
23
28
  * Returns `userId` which is Better Auth's `user.id` — the single
24
29
  * canonical user identifier after the identity table collapse.
25
30
  */
26
- export declare function getAuthContextFromHeaders(headers: Headers): Promise<AuthContext>;
31
+ export declare function getAuthContextFromHeaders(headers: Headers, options?: EdgeAuthRealmOptions): Promise<AuthContext>;
27
32
  export {};
package/dist/edge.js CHANGED
@@ -12,11 +12,16 @@
12
12
  * no extra identity lookup required.
13
13
  */
14
14
  import { getDb } from "@voyant-travel/db";
15
- import { authSession, authUser } from "@voyant-travel/db/schema/iam";
15
+ import { authSession, authUser, customerAuthSession, customerAuthUser, } from "@voyant-travel/db/schema/iam";
16
16
  import { and, eq, gt } from "drizzle-orm";
17
- const SESSION_COOKIE_NAME = "better-auth.session_token";
18
- function getAuthSecret() {
19
- return process.env.BETTER_AUTH_SECRET || process.env.SESSION_CLAIMS_SECRET || "";
17
+ function getAuthSecret(realm) {
18
+ if (realm === "customer") {
19
+ return process.env.BETTER_AUTH_CUSTOMER_SECRET || "";
20
+ }
21
+ return (process.env.BETTER_AUTH_ADMIN_SECRET ||
22
+ process.env.BETTER_AUTH_SECRET ||
23
+ process.env.SESSION_CLAIMS_SECRET ||
24
+ "");
20
25
  }
21
26
  /**
22
27
  * Unsign a Better Auth cookie value.
@@ -46,7 +51,7 @@ async function unsignCookie(rawCookieValue, secret) {
46
51
  const valid = await crypto.subtle.verify("HMAC", key, sigBytes, encoder.encode(value));
47
52
  return valid ? value : null;
48
53
  }
49
- function getRawCookieValue(headers) {
54
+ function getRawCookieValue(headers, cookieNames) {
50
55
  const cookieHeader = headers.get("cookie");
51
56
  if (!cookieHeader)
52
57
  return null;
@@ -56,7 +61,7 @@ function getRawCookieValue(headers) {
56
61
  if (eqIdx === -1)
57
62
  continue;
58
63
  const name = trimmed.slice(0, eqIdx).trim();
59
- if (name === SESSION_COOKIE_NAME) {
64
+ if (cookieNames.includes(name)) {
60
65
  return trimmed.slice(eqIdx + 1).trim() || null;
61
66
  }
62
67
  }
@@ -74,27 +79,44 @@ const EMPTY = {
74
79
  * Returns `userId` which is Better Auth's `user.id` — the single
75
80
  * canonical user identifier after the identity table collapse.
76
81
  */
77
- export async function getAuthContextFromHeaders(headers) {
78
- const rawCookie = getRawCookieValue(headers);
82
+ export async function getAuthContextFromHeaders(headers, options = {}) {
83
+ const realm = options.realm ?? "admin";
84
+ const cookieNames = options.cookieName
85
+ ? [options.cookieName]
86
+ : realm === "customer"
87
+ ? ["voyant-customer.session_token"]
88
+ : ["voyant-admin.session_token", "better-auth.session_token"];
89
+ const rawCookie = getRawCookieValue(headers, cookieNames);
79
90
  if (!rawCookie)
80
91
  return EMPTY;
81
- const secret = getAuthSecret();
92
+ const secret = options.secret ?? getAuthSecret(realm);
82
93
  if (!secret)
83
94
  return EMPTY;
84
95
  const token = await unsignCookie(rawCookie, secret);
85
96
  if (!token)
86
97
  return EMPTY;
87
98
  const db = getDb("edge");
88
- const [row] = await db
89
- .select({
90
- sessionId: authSession.id,
91
- userId: authSession.userId,
92
- email: authUser.email,
93
- })
94
- .from(authSession)
95
- .innerJoin(authUser, eq(authUser.id, authSession.userId))
96
- .where(and(eq(authSession.token, token), gt(authSession.expiresAt, new Date())))
97
- .limit(1);
99
+ const [row] = realm === "customer"
100
+ ? await db
101
+ .select({
102
+ sessionId: customerAuthSession.id,
103
+ userId: customerAuthSession.userId,
104
+ email: customerAuthUser.email,
105
+ })
106
+ .from(customerAuthSession)
107
+ .innerJoin(customerAuthUser, eq(customerAuthUser.id, customerAuthSession.userId))
108
+ .where(and(eq(customerAuthSession.token, token), gt(customerAuthSession.expiresAt, new Date())))
109
+ .limit(1)
110
+ : await db
111
+ .select({
112
+ sessionId: authSession.id,
113
+ userId: authSession.userId,
114
+ email: authUser.email,
115
+ })
116
+ .from(authSession)
117
+ .innerJoin(authUser, eq(authUser.id, authSession.userId))
118
+ .where(and(eq(authSession.token, token), gt(authSession.expiresAt, new Date())))
119
+ .limit(1);
98
120
  if (!row)
99
121
  return EMPTY;
100
122
  return {
package/dist/edge.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"edge.js","sourceRoot":"","sources":["../src/edge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Edge-compatible session check for Next.js middleware.\n *\n * Does NOT import better-auth (which uses eval / new Function via\n * createTelemetry, incompatible with Edge Runtime).\n *\n * Instead it reads the session cookie, unsigns it using the same\n * HMAC-SHA256 scheme that better-call uses, and queries better-auth's\n * `session` + `user` tables directly via the Neon HTTP drizzle adapter.\n *\n * With the identity table collapse, authUser.id IS the userId —\n * no extra identity lookup required.\n */\n\nimport { getDb } from \"@voyant-travel/db\"\nimport { authSession, authUser } from \"@voyant-travel/db/schema/iam\"\nimport { and, eq, gt } from \"drizzle-orm\"\n\nconst SESSION_COOKIE_NAME = \"better-auth.session_token\"\n\nfunction getAuthSecret(): string {\n return process.env.BETTER_AUTH_SECRET || process.env.SESSION_CLAIMS_SECRET || \"\"\n}\n\n/**\n * Unsign a Better Auth cookie value.\n *\n * Better Auth (via better-call) signs cookies as:\n * encodeURIComponent(value + \".\" + base64(HMAC-SHA256(value, secret)))\n *\n * This function reverses that: URL-decode, split at last \".\",\n * verify the HMAC-SHA256 signature, and return the raw value.\n */\nasync function unsignCookie(rawCookieValue: string, secret: string): Promise<string | null> {\n const decoded = decodeURIComponent(rawCookieValue)\n\n const lastDot = decoded.lastIndexOf(\".\")\n if (lastDot < 1) return null\n\n const value = decoded.substring(0, lastDot)\n const signature = decoded.substring(lastDot + 1)\n\n // Better Auth signatures are 44-char base64 strings ending with \"=\"\n if (signature.length !== 44 || !signature.endsWith(\"=\")) return null\n\n const encoder = new TextEncoder()\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"verify\"],\n )\n\n const sigBinStr = atob(signature)\n const sigBytes = new Uint8Array(sigBinStr.length)\n for (let i = 0; i < sigBinStr.length; i++) sigBytes[i] = sigBinStr.charCodeAt(i)\n\n const valid = await crypto.subtle.verify(\"HMAC\", key, sigBytes, encoder.encode(value))\n return valid ? value : null\n}\n\nfunction getRawCookieValue(headers: Headers): string | null {\n const cookieHeader = headers.get(\"cookie\")\n if (!cookieHeader) return null\n\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n const eqIdx = trimmed.indexOf(\"=\")\n if (eqIdx === -1) continue\n const name = trimmed.slice(0, eqIdx).trim()\n if (name === SESSION_COOKIE_NAME) {\n return trimmed.slice(eqIdx + 1).trim() || null\n }\n }\n return null\n}\n\ntype AuthContext = {\n userId: string | null\n email: string | null\n sessionId: string | null\n}\n\nconst EMPTY: AuthContext = {\n userId: null,\n email: null,\n sessionId: null,\n}\n\n/**\n * Lightweight edge-compatible session check.\n * Safe to call from Next.js middleware.\n *\n * Returns `userId` which is Better Auth's `user.id` — the single\n * canonical user identifier after the identity table collapse.\n */\nexport async function getAuthContextFromHeaders(headers: Headers): Promise<AuthContext> {\n const rawCookie = getRawCookieValue(headers)\n if (!rawCookie) return EMPTY\n\n const secret = getAuthSecret()\n if (!secret) return EMPTY\n\n const token = await unsignCookie(rawCookie, secret)\n if (!token) return EMPTY\n\n const db = getDb(\"edge\")\n\n const [row] = await db\n .select({\n sessionId: authSession.id,\n userId: authSession.userId,\n email: authUser.email,\n })\n .from(authSession)\n .innerJoin(authUser, eq(authUser.id, authSession.userId))\n .where(and(eq(authSession.token, token), gt(authSession.expiresAt, new Date())))\n .limit(1)\n\n if (!row) return EMPTY\n\n return {\n userId: row.userId,\n email: row.email,\n sessionId: row.sessionId,\n }\n}\n"]}
1
+ {"version":3,"file":"edge.js","sourceRoot":"","sources":["../src/edge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Edge-compatible session check for Next.js middleware.\n *\n * Does NOT import better-auth (which uses eval / new Function via\n * createTelemetry, incompatible with Edge Runtime).\n *\n * Instead it reads the session cookie, unsigns it using the same\n * HMAC-SHA256 scheme that better-call uses, and queries better-auth's\n * `session` + `user` tables directly via the Neon HTTP drizzle adapter.\n *\n * With the identity table collapse, authUser.id IS the userId —\n * no extra identity lookup required.\n */\n\nimport { getDb } from \"@voyant-travel/db\"\nimport {\n authSession,\n authUser,\n customerAuthSession,\n customerAuthUser,\n} from \"@voyant-travel/db/schema/iam\"\nimport { and, eq, gt } from \"drizzle-orm\"\n\nexport interface EdgeAuthRealmOptions {\n realm?: \"admin\" | \"customer\"\n cookieName?: string\n secret?: string\n}\n\nfunction getAuthSecret(realm: \"admin\" | \"customer\"): string {\n if (realm === \"customer\") {\n return process.env.BETTER_AUTH_CUSTOMER_SECRET || \"\"\n }\n return (\n process.env.BETTER_AUTH_ADMIN_SECRET ||\n process.env.BETTER_AUTH_SECRET ||\n process.env.SESSION_CLAIMS_SECRET ||\n \"\"\n )\n}\n\n/**\n * Unsign a Better Auth cookie value.\n *\n * Better Auth (via better-call) signs cookies as:\n * encodeURIComponent(value + \".\" + base64(HMAC-SHA256(value, secret)))\n *\n * This function reverses that: URL-decode, split at last \".\",\n * verify the HMAC-SHA256 signature, and return the raw value.\n */\nasync function unsignCookie(rawCookieValue: string, secret: string): Promise<string | null> {\n const decoded = decodeURIComponent(rawCookieValue)\n\n const lastDot = decoded.lastIndexOf(\".\")\n if (lastDot < 1) return null\n\n const value = decoded.substring(0, lastDot)\n const signature = decoded.substring(lastDot + 1)\n\n // Better Auth signatures are 44-char base64 strings ending with \"=\"\n if (signature.length !== 44 || !signature.endsWith(\"=\")) return null\n\n const encoder = new TextEncoder()\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"verify\"],\n )\n\n const sigBinStr = atob(signature)\n const sigBytes = new Uint8Array(sigBinStr.length)\n for (let i = 0; i < sigBinStr.length; i++) sigBytes[i] = sigBinStr.charCodeAt(i)\n\n const valid = await crypto.subtle.verify(\"HMAC\", key, sigBytes, encoder.encode(value))\n return valid ? value : null\n}\n\nfunction getRawCookieValue(headers: Headers, cookieNames: readonly string[]): string | null {\n const cookieHeader = headers.get(\"cookie\")\n if (!cookieHeader) return null\n\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n const eqIdx = trimmed.indexOf(\"=\")\n if (eqIdx === -1) continue\n const name = trimmed.slice(0, eqIdx).trim()\n if (cookieNames.includes(name)) {\n return trimmed.slice(eqIdx + 1).trim() || null\n }\n }\n return null\n}\n\ntype AuthContext = {\n userId: string | null\n email: string | null\n sessionId: string | null\n}\n\nconst EMPTY: AuthContext = {\n userId: null,\n email: null,\n sessionId: null,\n}\n\n/**\n * Lightweight edge-compatible session check.\n * Safe to call from Next.js middleware.\n *\n * Returns `userId` which is Better Auth's `user.id` — the single\n * canonical user identifier after the identity table collapse.\n */\nexport async function getAuthContextFromHeaders(\n headers: Headers,\n options: EdgeAuthRealmOptions = {},\n): Promise<AuthContext> {\n const realm = options.realm ?? \"admin\"\n const cookieNames = options.cookieName\n ? [options.cookieName]\n : realm === \"customer\"\n ? [\"voyant-customer.session_token\"]\n : [\"voyant-admin.session_token\", \"better-auth.session_token\"]\n const rawCookie = getRawCookieValue(headers, cookieNames)\n if (!rawCookie) return EMPTY\n\n const secret = options.secret ?? getAuthSecret(realm)\n if (!secret) return EMPTY\n\n const token = await unsignCookie(rawCookie, secret)\n if (!token) return EMPTY\n\n const db = getDb(\"edge\")\n\n const [row] =\n realm === \"customer\"\n ? await db\n .select({\n sessionId: customerAuthSession.id,\n userId: customerAuthSession.userId,\n email: customerAuthUser.email,\n })\n .from(customerAuthSession)\n .innerJoin(customerAuthUser, eq(customerAuthUser.id, customerAuthSession.userId))\n .where(\n and(\n eq(customerAuthSession.token, token),\n gt(customerAuthSession.expiresAt, new Date()),\n ),\n )\n .limit(1)\n : await db\n .select({\n sessionId: authSession.id,\n userId: authSession.userId,\n email: authUser.email,\n })\n .from(authSession)\n .innerJoin(authUser, eq(authUser.id, authSession.userId))\n .where(and(eq(authSession.token, token), gt(authSession.expiresAt, new Date())))\n .limit(1)\n\n if (!row) return EMPTY\n\n return {\n userId: row.userId,\n email: row.email,\n sessionId: row.sessionId,\n }\n}\n"]}
@@ -13,18 +13,31 @@ import { type SelectApikey } from "@voyant-travel/db/schema/iam";
13
13
  import type { Reporter, VoyantDb, VoyantRequestAuthContext } from "@voyant-travel/hono";
14
14
  import type { AccessCatalog } from "@voyant-travel/types/api-keys";
15
15
  import { Hono } from "hono";
16
- import { type CreateBetterAuthOptions } from "./server.js";
16
+ import { type CreateBetterAuthOptions, type CustomerAuthMethods } from "./server.js";
17
17
  type OperatorAuthMode = "local" | "voyant-cloud";
18
18
  export interface OperatorAuthNodeEnv extends NodeDatabaseEnv {
19
19
  API_BASE_URL?: string;
20
20
  APP_URL?: string;
21
21
  AUTH_COOKIE_DOMAIN?: string;
22
- BETTER_AUTH_SECRET: string;
22
+ /** @deprecated Use BETTER_AUTH_ADMIN_SECRET. */
23
+ BETTER_AUTH_SECRET?: string;
24
+ BETTER_AUTH_ADMIN_SECRET?: string;
25
+ BETTER_AUTH_CUSTOMER_SECRET?: string;
23
26
  CORS_ALLOWLIST?: string;
24
27
  DASH_BASE_URL?: string;
25
28
  EMAIL_FROM?: string;
26
29
  SESSION_CLAIMS_SECRET: string;
27
30
  VOYANT_ADMIN_AUTH_MODE?: string;
31
+ VOYANT_CUSTOMER_AUTH_MODE?: string;
32
+ VOYANT_CUSTOMER_AUTH_CONFIG_JSON?: string;
33
+ VOYANT_CUSTOMER_AUTH_EMAIL_CODE?: string;
34
+ VOYANT_CUSTOMER_AUTH_EMAIL_PASSWORD?: string;
35
+ CUSTOMER_AUTH_GOOGLE_CLIENT_ID?: string;
36
+ CUSTOMER_AUTH_GOOGLE_CLIENT_SECRET?: string;
37
+ CUSTOMER_AUTH_FACEBOOK_CLIENT_ID?: string;
38
+ CUSTOMER_AUTH_FACEBOOK_CLIENT_SECRET?: string;
39
+ CUSTOMER_AUTH_APPLE_CLIENT_ID?: string;
40
+ CUSTOMER_AUTH_APPLE_CLIENT_SECRET?: string;
28
41
  VOYANT_AUTH_LOG_SECRET_FALLBACKS?: string;
29
42
  VOYANT_CLOUD_ADMIN_AUTH_AUDIENCE?: string;
30
43
  VOYANT_CLOUD_ADMIN_AUTH_CLIENT_TOKEN?: string;
@@ -82,7 +95,36 @@ export interface CreateOperatorAuthNodeRuntimeOptions<Env extends OperatorAuthNo
82
95
  };
83
96
  cookieAdvanced?: (env: Env) => Pick<BetterAuthAdvancedOptions, "crossSubDomainCookies" | "defaultCookieAttributes"> | undefined;
84
97
  resolveEmailSender?: (env: Env) => OperatorAuthEmailSender | null;
98
+ /**
99
+ * Storefront/BFF seam for a canonical public auth origin and resolved
100
+ * merchant credentials. Never derive these values from Host/X-Forwarded-Host.
101
+ */
102
+ resolveCustomerAuthContext?: (env: Env, policy: CustomerAuthPolicy, request: Request) => CustomerAuthRuntimeContext;
85
103
  }
104
+ export interface CustomerAuthRuntimeContext {
105
+ baseURL: string;
106
+ trustedOrigins: string[];
107
+ methods: CustomerAuthMethods;
108
+ }
109
+ export interface CustomerAuthSocialPolicy {
110
+ enabled: boolean;
111
+ credentialRef: string | null;
112
+ }
113
+ export interface CustomerAuthPolicy {
114
+ methods: {
115
+ emailCode: boolean;
116
+ emailPassword: boolean;
117
+ google: CustomerAuthSocialPolicy;
118
+ facebook: CustomerAuthSocialPolicy;
119
+ apple: CustomerAuthSocialPolicy;
120
+ };
121
+ }
122
+ /**
123
+ * Better Auth is dispatched internally without the hosting `/api` prefix, but
124
+ * OAuth providers must return to the browser-visible route. Keep explicit
125
+ * merchant overrides and supply the canonical public callback otherwise.
126
+ */
127
+ export declare function withCustomerSocialRedirectUris(methods: CustomerAuthMethods, publicApiBaseUrl: string): CustomerAuthMethods;
86
128
  export declare function createOperatorAuthNodeRuntime<Env extends OperatorAuthNodeEnv>(runtimeOptions: CreateOperatorAuthNodeRuntimeOptions<Env>): {
87
129
  handler: Hono<{
88
130
  Bindings: Env;