@voyant-travel/auth 0.137.0 → 0.139.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.
- package/dist/runtime-contributor.js +11 -0
- package/dist/runtime-contributor.js.map +1 -1
- package/dist/storefront-credentials.d.ts +31 -0
- package/dist/storefront-credentials.js +54 -0
- package/dist/storefront-credentials.js.map +1 -0
- package/dist/storefront-customer-auth-resolver.d.ts +30 -0
- package/dist/storefront-customer-auth-resolver.js +72 -0
- package/dist/storefront-customer-auth-resolver.js.map +1 -0
- package/dist/storefront-keys.d.ts +40 -0
- package/dist/storefront-keys.js +50 -0
- package/dist/storefront-keys.js.map +1 -0
- package/dist/storefront-local-adapter.d.ts +11 -0
- package/dist/storefront-local-adapter.js +313 -0
- package/dist/storefront-local-adapter.js.map +1 -0
- package/dist/storefront-origins.d.ts +34 -0
- package/dist/storefront-origins.js +132 -0
- package/dist/storefront-origins.js.map +1 -0
- package/dist/storefront-runtime-port.d.ts +93 -0
- package/dist/storefront-runtime-port.js +35 -0
- package/dist/storefront-runtime-port.js.map +1 -0
- package/dist/voyant.d.ts +1 -0
- package/dist/voyant.js +18 -0
- package/dist/voyant.js.map +1 -1
- package/package.json +24 -4
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { cloudAdminMembersConfigFromRevalidate } from "./cloud-broker.js";
|
|
2
2
|
import { identityAccessRuntimePort, } from "./identity-access-runtime-port.js";
|
|
3
|
+
import { createKmsStorefrontCredentialCipher } from "./storefront-credentials.js";
|
|
4
|
+
import { createLocalStorefrontAdapter } from "./storefront-local-adapter.js";
|
|
5
|
+
import { storefrontRuntimePort } from "./storefront-runtime-port.js";
|
|
3
6
|
import { createCloudTeamManagementAdapter } from "./team-management-cloud-adapter.js";
|
|
4
7
|
import { createLocalTeamManagementAdapter } from "./team-management-local-adapter.js";
|
|
5
8
|
import { createGuardedTeamManagementProvider } from "./team-management-policy.js";
|
|
@@ -64,9 +67,17 @@ export function createAuthRuntimePortContribution(host) {
|
|
|
64
67
|
return cloudTeamManagement;
|
|
65
68
|
throw new Error("Team management requires deployment.providers.adminAuth to select better-auth or voyant-cloud.");
|
|
66
69
|
});
|
|
70
|
+
// Self-host storefront access: keys, declared origins, and KMS-encrypted
|
|
71
|
+
// provider credentials served from this deployment's own runtime DB. The
|
|
72
|
+
// managed cloud storefront adapter is a follow-up; the port carries the
|
|
73
|
+
// storefront config the local customer-auth resolver reads.
|
|
74
|
+
const storefront = createLocalStorefrontAdapter({
|
|
75
|
+
resolveCipher: (bindings) => createKmsStorefrontCredentialCipher(host.primitives.env(bindings)),
|
|
76
|
+
});
|
|
67
77
|
return {
|
|
68
78
|
[identityAccessRuntimePort.id]: identityAccess,
|
|
69
79
|
[teamManagementRuntimePort.id]: teamManagement,
|
|
80
|
+
[storefrontRuntimePort.id]: storefront,
|
|
70
81
|
};
|
|
71
82
|
}
|
|
72
83
|
//# sourceMappingURL=runtime-contributor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-contributor.js","sourceRoot":"","sources":["../src/runtime-contributor.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"runtime-contributor.js","sourceRoot":"","sources":["../src/runtime-contributor.ts"],"names":[],"mappings":"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","sourcesContent":["import type { VoyantRuntimeHostPrimitives } from \"@voyant-travel/core\"\nimport { cloudAdminMembersConfigFromRevalidate } from \"./cloud-broker.js\"\nimport {\n type IdentityAccessRuntimeProvider,\n identityAccessRuntimePort,\n} from \"./identity-access-runtime-port.js\"\nimport { createKmsStorefrontCredentialCipher } from \"./storefront-credentials.js\"\nimport { createLocalStorefrontAdapter } from \"./storefront-local-adapter.js\"\nimport { storefrontRuntimePort } from \"./storefront-runtime-port.js\"\nimport { createCloudTeamManagementAdapter } from \"./team-management-cloud-adapter.js\"\nimport { createLocalTeamManagementAdapter } from \"./team-management-local-adapter.js\"\nimport { createGuardedTeamManagementProvider } from \"./team-management-policy.js\"\nimport { teamManagementRuntimePort } from \"./team-management-runtime-port.js\"\n\ninterface InvitationNotificationProvider {\n readonly channels: ReadonlyArray<string>\n send(payload: {\n channel: \"email\"\n to: string\n template: string\n subject: string\n html: string\n }): Promise<unknown>\n}\n\nexport interface AuthRuntimeContributorHost {\n primitives: VoyantRuntimeHostPrimitives\n}\n\nfunction selectedAdminAuthProvider(host: AuthRuntimeContributorHost, bindings: unknown): unknown {\n return host.primitives.config.read(bindings, \"deployment.providers.adminAuth\")\n}\n\n/** Package-owned registration map for Auth deployment adapters. */\nexport function createAuthRuntimePortContribution(\n host: AuthRuntimeContributorHost,\n): Readonly<Record<string, unknown>> {\n const identityAccess: IdentityAccessRuntimeProvider = {\n resolveDeployment(bindings) {\n const env = bindings as Record<string, string | undefined>\n const selectedAuthProvider = selectedAdminAuthProvider(host, bindings)\n if (selectedAuthProvider !== \"better-auth\" && selectedAuthProvider !== \"voyant-cloud\") {\n throw new Error(\n \"Auth runtime requires deployment.providers.adminAuth to select better-auth or voyant-cloud.\",\n )\n }\n const appUrl = (env.APP_URL || env.DASH_BASE_URL || \"http://localhost:3300\")\n .trim()\n .replace(/\\/$/, \"\")\n const deploymentId = env.VOYANT_CLOUD_DEPLOYMENT_ID?.trim()\n const revalidateUrl = env.VOYANT_CLOUD_ADMIN_AUTH_REVALIDATE_URL?.trim()\n const clientToken = env.VOYANT_CLOUD_ADMIN_AUTH_CLIENT_TOKEN?.trim()\n return {\n appUrl,\n authMode: selectedAuthProvider === \"voyant-cloud\" ? \"voyant-cloud\" : \"local\",\n cloudAdminMembers:\n deploymentId && revalidateUrl && clientToken\n ? cloudAdminMembersConfigFromRevalidate({ revalidateUrl, deploymentId, clientToken })\n : null,\n }\n },\n async sendInvitationEmail(bindings, message) {\n const resolver = host.primitives.config.read(bindings, \"notificationProviders\")\n const providers =\n typeof resolver === \"function\"\n ? (resolver(\n host.primitives.env(bindings),\n ) as ReadonlyArray<InvitationNotificationProvider>)\n : []\n const provider = providers.find((candidate) => candidate.channels.includes(\"email\"))\n if (!provider) return false\n try {\n await provider.send({\n channel: \"email\",\n to: message.to,\n template: \"auth.invitation\",\n subject: \"You've been invited to Voyant\",\n html: `<p>You've been invited to join a Voyant workspace.</p><p><a href=\"${message.acceptUrl}\">Accept invitation</a></p><p>The link expires in ${message.expiresInHours} hours.</p>`,\n })\n return true\n } catch (error) {\n console.error(\"[invitations] email send failed:\", error)\n return false\n }\n },\n }\n\n const localTeamManagement = createLocalTeamManagementAdapter(identityAccess)\n const cloudTeamManagement = createCloudTeamManagementAdapter(identityAccess)\n const teamManagement = createGuardedTeamManagementProvider((context) => {\n const selectedAuthProvider = selectedAdminAuthProvider(host, context.bindings)\n if (selectedAuthProvider === \"better-auth\") return localTeamManagement\n if (selectedAuthProvider === \"voyant-cloud\") return cloudTeamManagement\n throw new Error(\n \"Team management requires deployment.providers.adminAuth to select better-auth or voyant-cloud.\",\n )\n })\n\n // Self-host storefront access: keys, declared origins, and KMS-encrypted\n // provider credentials served from this deployment's own runtime DB. The\n // managed cloud storefront adapter is a follow-up; the port carries the\n // storefront config the local customer-auth resolver reads.\n const storefront = createLocalStorefrontAdapter({\n resolveCipher: (bindings) =>\n createKmsStorefrontCredentialCipher(\n host.primitives.env(bindings) as Record<string, string | undefined>,\n ),\n })\n\n return {\n [identityAccessRuntimePort.id]: identityAccess,\n [teamManagementRuntimePort.id]: teamManagement,\n [storefrontRuntimePort.id]: storefront,\n }\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront OAuth provider credential bundles + the encryption seam.
|
|
3
|
+
*
|
|
4
|
+
* Plaintext provider secrets never touch persisted config: the local adapter
|
|
5
|
+
* encrypts a validated bundle through a `StorefrontCredentialCipher` (backed by
|
|
6
|
+
* the deployment's KMS) into the opaque `{ enc }` envelope stored on
|
|
7
|
+
* `storefrontCustomerAuthCredentials`, and decrypts it only when resolving a
|
|
8
|
+
* customer-auth context at request time.
|
|
9
|
+
*/
|
|
10
|
+
import type { KmsEnvelope, StorefrontCustomerAuthSocialProvider } from "@voyant-travel/db/schema/iam";
|
|
11
|
+
export type StorefrontCredentialBundle = {
|
|
12
|
+
provider: StorefrontCustomerAuthSocialProvider;
|
|
13
|
+
clientId: string;
|
|
14
|
+
clientSecret: string;
|
|
15
|
+
};
|
|
16
|
+
/** Validate a provider-specific secret bundle before encryption. */
|
|
17
|
+
export declare function validateStorefrontCredentialBundle(provider: StorefrontCustomerAuthSocialProvider, credentials: Record<string, unknown>): StorefrontCredentialBundle;
|
|
18
|
+
/**
|
|
19
|
+
* Encryption seam for storefront provider secrets. The self-host runtime binds
|
|
20
|
+
* this to its KMS; tests bind a deterministic in-memory cipher.
|
|
21
|
+
*/
|
|
22
|
+
export interface StorefrontCredentialCipher {
|
|
23
|
+
encrypt(plaintext: string): Promise<KmsEnvelope>;
|
|
24
|
+
decrypt(envelope: KmsEnvelope): Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Default cipher backed by the deployment's configured KMS (GCP/AWS/env/local),
|
|
28
|
+
* selected from environment exactly like every other framework secret. The
|
|
29
|
+
* ciphertext is wrapped in the opaque `{ enc }` envelope stored on the row.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createKmsStorefrontCredentialCipher(env: Record<string, string | undefined>): StorefrontCredentialCipher;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createKmsProviderFromEnv } from "@voyant-travel/utils/kms";
|
|
2
|
+
import { StorefrontInputError } from "./storefront-origins.js";
|
|
3
|
+
// Every provider is stored as `{ clientId, clientSecret }`, matching the shape
|
|
4
|
+
// `defaultCustomerAuthContext` feeds Better Auth's `socialProviders`. For Apple
|
|
5
|
+
// the `clientSecret` is the operator's pre-generated Sign-in-with-Apple JWT.
|
|
6
|
+
const REQUIRED_FIELDS = {
|
|
7
|
+
google: ["clientId", "clientSecret"],
|
|
8
|
+
facebook: ["clientId", "clientSecret"],
|
|
9
|
+
apple: ["clientId", "clientSecret"],
|
|
10
|
+
};
|
|
11
|
+
const MAX_CREDENTIAL_FIELD_LENGTH = 16_384;
|
|
12
|
+
function requireCredentialField(credentials, key) {
|
|
13
|
+
const value = credentials[key];
|
|
14
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
15
|
+
throw new StorefrontInputError(`Customer auth credential field ${key} is required.`);
|
|
16
|
+
}
|
|
17
|
+
if (value.length > MAX_CREDENTIAL_FIELD_LENGTH) {
|
|
18
|
+
throw new StorefrontInputError(`Customer auth credential field ${key} is too large.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Validate a provider-specific secret bundle before encryption. */
|
|
22
|
+
export function validateStorefrontCredentialBundle(provider, credentials) {
|
|
23
|
+
const required = REQUIRED_FIELDS[provider];
|
|
24
|
+
for (const key of required)
|
|
25
|
+
requireCredentialField(credentials, key);
|
|
26
|
+
const allowed = new Set(["provider", ...required]);
|
|
27
|
+
const unexpected = Object.keys(credentials).filter((key) => !allowed.has(key));
|
|
28
|
+
if (unexpected.length > 0) {
|
|
29
|
+
throw new StorefrontInputError(`Unexpected ${provider} customer auth credential field(s): ${unexpected.join(", ")}.`);
|
|
30
|
+
}
|
|
31
|
+
return { provider, ...credentials };
|
|
32
|
+
}
|
|
33
|
+
// Storefront OAuth secrets are third-party integration credentials.
|
|
34
|
+
const STOREFRONT_CREDENTIAL_KEY = { keyType: "integrations" };
|
|
35
|
+
/**
|
|
36
|
+
* Default cipher backed by the deployment's configured KMS (GCP/AWS/env/local),
|
|
37
|
+
* selected from environment exactly like every other framework secret. The
|
|
38
|
+
* ciphertext is wrapped in the opaque `{ enc }` envelope stored on the row.
|
|
39
|
+
*/
|
|
40
|
+
export function createKmsStorefrontCredentialCipher(env) {
|
|
41
|
+
const provider = createKmsProviderFromEnv(env);
|
|
42
|
+
return {
|
|
43
|
+
async encrypt(plaintext) {
|
|
44
|
+
return { enc: await provider.encrypt(plaintext, STOREFRONT_CREDENTIAL_KEY) };
|
|
45
|
+
},
|
|
46
|
+
async decrypt(envelope) {
|
|
47
|
+
if (!envelope) {
|
|
48
|
+
throw new StorefrontInputError("Stored storefront credential is empty.");
|
|
49
|
+
}
|
|
50
|
+
return provider.decrypt(envelope.enc, STOREFRONT_CREDENTIAL_KEY);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=storefront-credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storefront-credentials.js","sourceRoot":"","sources":["../src/storefront-credentials.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,wBAAwB,EAAe,MAAM,0BAA0B,CAAA;AAEhF,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAQ9D,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,MAAM,eAAe,GAAoE;IACvF,MAAM,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,QAAQ,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;IACtC,KAAK,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;CACpC,CAAA;AAED,MAAM,2BAA2B,GAAG,MAAM,CAAA;AAE1C,SAAS,sBAAsB,CAAC,WAAoC,EAAE,GAAW;IAC/E,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,oBAAoB,CAAC,kCAAkC,GAAG,eAAe,CAAC,CAAA;IACtF,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,2BAA2B,EAAE,CAAC;QAC/C,MAAM,IAAI,oBAAoB,CAAC,kCAAkC,GAAG,gBAAgB,CAAC,CAAA;IACvF,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,kCAAkC,CAChD,QAA8C,EAC9C,WAAoC;IAEpC,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IAC1C,KAAK,MAAM,GAAG,IAAI,QAAQ;QAAE,sBAAsB,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;IACpE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAA;IAClD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9E,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,oBAAoB,CAC5B,cAAc,QAAQ,uCAAuC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACtF,CAAA;IACH,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAgC,CAAA;AACnE,CAAC;AAWD,oEAAoE;AACpE,MAAM,yBAAyB,GAAW,EAAE,OAAO,EAAE,cAAc,EAAE,CAAA;AAErE;;;;GAIG;AACH,MAAM,UAAU,mCAAmC,CACjD,GAAuC;IAEvC,MAAM,QAAQ,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAA;IAC9C,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,SAAS;YACrB,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,yBAAyB,CAAC,EAAE,CAAA;QAC9E,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,QAAQ;YACpB,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,oBAAoB,CAAC,wCAAwC,CAAC,CAAA;YAC1E,CAAC;YACD,OAAO,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAA;QAClE,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * Storefront OAuth provider credential bundles + the encryption seam.\n *\n * Plaintext provider secrets never touch persisted config: the local adapter\n * encrypts a validated bundle through a `StorefrontCredentialCipher` (backed by\n * the deployment's KMS) into the opaque `{ enc }` envelope stored on\n * `storefrontCustomerAuthCredentials`, and decrypts it only when resolving a\n * customer-auth context at request time.\n */\nimport type {\n KmsEnvelope,\n StorefrontCustomerAuthSocialProvider,\n} from \"@voyant-travel/db/schema/iam\"\nimport { createKmsProviderFromEnv, type KeyRef } from \"@voyant-travel/utils/kms\"\n\nimport { StorefrontInputError } from \"./storefront-origins.js\"\n\nexport type StorefrontCredentialBundle = {\n provider: StorefrontCustomerAuthSocialProvider\n clientId: string\n clientSecret: string\n}\n\n// Every provider is stored as `{ clientId, clientSecret }`, matching the shape\n// `defaultCustomerAuthContext` feeds Better Auth's `socialProviders`. For Apple\n// the `clientSecret` is the operator's pre-generated Sign-in-with-Apple JWT.\nconst REQUIRED_FIELDS: Record<StorefrontCustomerAuthSocialProvider, readonly string[]> = {\n google: [\"clientId\", \"clientSecret\"],\n facebook: [\"clientId\", \"clientSecret\"],\n apple: [\"clientId\", \"clientSecret\"],\n}\n\nconst MAX_CREDENTIAL_FIELD_LENGTH = 16_384\n\nfunction requireCredentialField(credentials: Record<string, unknown>, key: string): void {\n const value = credentials[key]\n if (typeof value !== \"string\" || !value.trim()) {\n throw new StorefrontInputError(`Customer auth credential field ${key} is required.`)\n }\n if (value.length > MAX_CREDENTIAL_FIELD_LENGTH) {\n throw new StorefrontInputError(`Customer auth credential field ${key} is too large.`)\n }\n}\n\n/** Validate a provider-specific secret bundle before encryption. */\nexport function validateStorefrontCredentialBundle(\n provider: StorefrontCustomerAuthSocialProvider,\n credentials: Record<string, unknown>,\n): StorefrontCredentialBundle {\n const required = REQUIRED_FIELDS[provider]\n for (const key of required) requireCredentialField(credentials, key)\n const allowed = new Set([\"provider\", ...required])\n const unexpected = Object.keys(credentials).filter((key) => !allowed.has(key))\n if (unexpected.length > 0) {\n throw new StorefrontInputError(\n `Unexpected ${provider} customer auth credential field(s): ${unexpected.join(\", \")}.`,\n )\n }\n return { provider, ...credentials } as StorefrontCredentialBundle\n}\n\n/**\n * Encryption seam for storefront provider secrets. The self-host runtime binds\n * this to its KMS; tests bind a deterministic in-memory cipher.\n */\nexport interface StorefrontCredentialCipher {\n encrypt(plaintext: string): Promise<KmsEnvelope>\n decrypt(envelope: KmsEnvelope): Promise<string>\n}\n\n// Storefront OAuth secrets are third-party integration credentials.\nconst STOREFRONT_CREDENTIAL_KEY: KeyRef = { keyType: \"integrations\" }\n\n/**\n * Default cipher backed by the deployment's configured KMS (GCP/AWS/env/local),\n * selected from environment exactly like every other framework secret. The\n * ciphertext is wrapped in the opaque `{ enc }` envelope stored on the row.\n */\nexport function createKmsStorefrontCredentialCipher(\n env: Record<string, string | undefined>,\n): StorefrontCredentialCipher {\n const provider = createKmsProviderFromEnv(env)\n return {\n async encrypt(plaintext) {\n return { enc: await provider.encrypt(plaintext, STOREFRONT_CREDENTIAL_KEY) }\n },\n async decrypt(envelope) {\n if (!envelope) {\n throw new StorefrontInputError(\"Stored storefront credential is empty.\")\n }\n return provider.decrypt(envelope.enc, STOREFRONT_CREDENTIAL_KEY)\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CustomerAuthRuntimeContext } from "./node-runtime.js";
|
|
2
|
+
import type { StorefrontResolveContext, StorefrontRuntimeProvider } from "./storefront-runtime-port.js";
|
|
3
|
+
/** Default header the storefront BFF uses to declare its browser origin. */
|
|
4
|
+
export declare const STOREFRONT_ORIGIN_HEADER = "x-voyant-storefront-origin";
|
|
5
|
+
/** Default header carrying the storefront's publishable/secret access key. */
|
|
6
|
+
export declare const STOREFRONT_KEY_HEADER = "x-api-key";
|
|
7
|
+
export interface LocalStorefrontCustomerAuthResolverConfig<Env> {
|
|
8
|
+
provider: StorefrontRuntimeProvider;
|
|
9
|
+
/**
|
|
10
|
+
* Open the request-time resolve context (db + bindings) for this runtime.
|
|
11
|
+
* The self-host host wires this to its database lifecycle; `dispose` is
|
|
12
|
+
* always awaited, success or failure.
|
|
13
|
+
*/
|
|
14
|
+
openResolveContext: (env: Env, request: Request) => Promise<{
|
|
15
|
+
context: StorefrontResolveContext;
|
|
16
|
+
dispose?: () => Promise<void>;
|
|
17
|
+
}>;
|
|
18
|
+
originHeader?: string;
|
|
19
|
+
keyHeader?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class StorefrontCustomerAuthResolutionError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the `resolveCustomerAuthContext` function a self-host runtime passes to
|
|
26
|
+
* {@link createOperatorAuthNodeRuntime}. Keeps the existing seam intact — this
|
|
27
|
+
* is simply the local (port-backed) option alongside the env default and the
|
|
28
|
+
* managed cloud fetch.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createLocalStorefrontCustomerAuthResolver<Env>(config: LocalStorefrontCustomerAuthResolverConfig<Env>): (env: Env, request: Request) => Promise<CustomerAuthRuntimeContext>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { enabledStorefrontSocialProviders, isStorefrontOriginAllowed, } from "./storefront-origins.js";
|
|
2
|
+
/** Default header the storefront BFF uses to declare its browser origin. */
|
|
3
|
+
export const STOREFRONT_ORIGIN_HEADER = "x-voyant-storefront-origin";
|
|
4
|
+
/** Default header carrying the storefront's publishable/secret access key. */
|
|
5
|
+
export const STOREFRONT_KEY_HEADER = "x-api-key";
|
|
6
|
+
export class StorefrontCustomerAuthResolutionError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "StorefrontCustomerAuthResolutionError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function toSocialProviders(secrets) {
|
|
13
|
+
const providers = {};
|
|
14
|
+
for (const [provider, credential] of Object.entries(secrets)) {
|
|
15
|
+
const clientId = credential?.clientId;
|
|
16
|
+
const clientSecret = credential?.clientSecret;
|
|
17
|
+
if (!clientId || !clientSecret)
|
|
18
|
+
continue;
|
|
19
|
+
providers[provider] = { clientId, clientSecret };
|
|
20
|
+
}
|
|
21
|
+
return providers;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the `resolveCustomerAuthContext` function a self-host runtime passes to
|
|
25
|
+
* {@link createOperatorAuthNodeRuntime}. Keeps the existing seam intact — this
|
|
26
|
+
* is simply the local (port-backed) option alongside the env default and the
|
|
27
|
+
* managed cloud fetch.
|
|
28
|
+
*/
|
|
29
|
+
export function createLocalStorefrontCustomerAuthResolver(config) {
|
|
30
|
+
const originHeader = config.originHeader ?? STOREFRONT_ORIGIN_HEADER;
|
|
31
|
+
const keyHeader = config.keyHeader ?? STOREFRONT_KEY_HEADER;
|
|
32
|
+
return async (env, request) => {
|
|
33
|
+
const origin = request.headers.get(originHeader)?.trim();
|
|
34
|
+
if (!origin) {
|
|
35
|
+
throw new StorefrontCustomerAuthResolutionError(`Local storefront customer auth requires ${originHeader} from the storefront BFF.`);
|
|
36
|
+
}
|
|
37
|
+
const token = request.headers.get(keyHeader)?.trim();
|
|
38
|
+
if (!token) {
|
|
39
|
+
throw new StorefrontCustomerAuthResolutionError(`Local storefront customer auth requires a storefront key (${keyHeader}).`);
|
|
40
|
+
}
|
|
41
|
+
const { context, dispose } = await config.openResolveContext(env, request);
|
|
42
|
+
try {
|
|
43
|
+
const resolved = await config.provider.resolveStorefrontByApiKey(context, token);
|
|
44
|
+
if (!resolved) {
|
|
45
|
+
throw new StorefrontCustomerAuthResolutionError("The presented storefront key is unknown or revoked.");
|
|
46
|
+
}
|
|
47
|
+
const { storefront } = resolved;
|
|
48
|
+
if (!isStorefrontOriginAllowed(origin, storefront.allowedOrigins)) {
|
|
49
|
+
throw new StorefrontCustomerAuthResolutionError("The request origin is not a declared allowed origin for this storefront.");
|
|
50
|
+
}
|
|
51
|
+
const enabledProviders = enabledStorefrontSocialProviders(storefront.methods);
|
|
52
|
+
const secrets = await config.provider.resolveProviderCredentials(context, storefront.id, enabledProviders);
|
|
53
|
+
const methods = {
|
|
54
|
+
emailCode: storefront.methods.emailCode,
|
|
55
|
+
emailPassword: storefront.methods.emailPassword,
|
|
56
|
+
socialProviders: toSocialProviders(secrets),
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
baseURL: origin,
|
|
60
|
+
publicApiBaseURL: `${origin}/api`,
|
|
61
|
+
invitationAcceptBaseURL: origin,
|
|
62
|
+
trustedOrigins: [origin],
|
|
63
|
+
methods,
|
|
64
|
+
accountPolicy: storefront.accountPolicy,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await dispose?.();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=storefront-customer-auth-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storefront-customer-auth-resolver.js","sourceRoot":"","sources":["../src/storefront-customer-auth-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local storefront customer-auth resolver.\n *\n * Implements the `resolveCustomerAuthContext` seam of\n * {@link createOperatorAuthNodeRuntime} for a self-host runtime: instead of\n * fetching a merchant policy from Voyant Cloud, it resolves the storefront from\n * the presented access key via {@link storefrontRuntimePort}, enforces the\n * operator-declared origin allowlist, and projects the storefront's methods,\n * trusted origins, buyer-account policy, and decrypted provider secrets into a\n * `CustomerAuthRuntimeContext`.\n *\n * The BFF must forward the storefront origin explicitly (never derived from\n * Host/X-Forwarded-Host) plus the storefront key, exactly like the managed\n * broker.\n */\nimport type { CustomerBuyerAccountPolicy } from \"./customer-buyer-accounts.js\"\nimport type { CustomerAuthRuntimeContext } from \"./node-runtime.js\"\nimport type { CustomerAuthMethods } from \"./server.js\"\nimport {\n enabledStorefrontSocialProviders,\n isStorefrontOriginAllowed,\n} from \"./storefront-origins.js\"\nimport type {\n StorefrontResolveContext,\n StorefrontRuntimeProvider,\n} from \"./storefront-runtime-port.js\"\n\n/** Default header the storefront BFF uses to declare its browser origin. */\nexport const STOREFRONT_ORIGIN_HEADER = \"x-voyant-storefront-origin\"\n/** Default header carrying the storefront's publishable/secret access key. */\nexport const STOREFRONT_KEY_HEADER = \"x-api-key\"\n\nexport interface LocalStorefrontCustomerAuthResolverConfig<Env> {\n provider: StorefrontRuntimeProvider\n /**\n * Open the request-time resolve context (db + bindings) for this runtime.\n * The self-host host wires this to its database lifecycle; `dispose` is\n * always awaited, success or failure.\n */\n openResolveContext: (\n env: Env,\n request: Request,\n ) => Promise<{ context: StorefrontResolveContext; dispose?: () => Promise<void> }>\n originHeader?: string\n keyHeader?: string\n}\n\nexport class StorefrontCustomerAuthResolutionError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"StorefrontCustomerAuthResolutionError\"\n }\n}\n\nfunction toSocialProviders(\n secrets: Partial<Record<\"google\" | \"facebook\" | \"apple\", Record<string, string>>>,\n): CustomerAuthMethods[\"socialProviders\"] {\n const providers: Record<string, { clientId: string; clientSecret: string }> = {}\n for (const [provider, credential] of Object.entries(secrets)) {\n const clientId = credential?.clientId\n const clientSecret = credential?.clientSecret\n if (!clientId || !clientSecret) continue\n providers[provider] = { clientId, clientSecret }\n }\n return providers as CustomerAuthMethods[\"socialProviders\"]\n}\n\n/**\n * Build the `resolveCustomerAuthContext` function a self-host runtime passes to\n * {@link createOperatorAuthNodeRuntime}. Keeps the existing seam intact — this\n * is simply the local (port-backed) option alongside the env default and the\n * managed cloud fetch.\n */\nexport function createLocalStorefrontCustomerAuthResolver<Env>(\n config: LocalStorefrontCustomerAuthResolverConfig<Env>,\n): (env: Env, request: Request) => Promise<CustomerAuthRuntimeContext> {\n const originHeader = config.originHeader ?? STOREFRONT_ORIGIN_HEADER\n const keyHeader = config.keyHeader ?? STOREFRONT_KEY_HEADER\n\n return async (env, request) => {\n const origin = request.headers.get(originHeader)?.trim()\n if (!origin) {\n throw new StorefrontCustomerAuthResolutionError(\n `Local storefront customer auth requires ${originHeader} from the storefront BFF.`,\n )\n }\n const token = request.headers.get(keyHeader)?.trim()\n if (!token) {\n throw new StorefrontCustomerAuthResolutionError(\n `Local storefront customer auth requires a storefront key (${keyHeader}).`,\n )\n }\n\n const { context, dispose } = await config.openResolveContext(env, request)\n try {\n const resolved = await config.provider.resolveStorefrontByApiKey(context, token)\n if (!resolved) {\n throw new StorefrontCustomerAuthResolutionError(\n \"The presented storefront key is unknown or revoked.\",\n )\n }\n const { storefront } = resolved\n if (!isStorefrontOriginAllowed(origin, storefront.allowedOrigins)) {\n throw new StorefrontCustomerAuthResolutionError(\n \"The request origin is not a declared allowed origin for this storefront.\",\n )\n }\n\n const enabledProviders = enabledStorefrontSocialProviders(storefront.methods)\n const secrets = await config.provider.resolveProviderCredentials(\n context,\n storefront.id,\n enabledProviders,\n )\n\n const methods: CustomerAuthMethods = {\n emailCode: storefront.methods.emailCode,\n emailPassword: storefront.methods.emailPassword,\n socialProviders: toSocialProviders(secrets),\n }\n\n return {\n baseURL: origin,\n publicApiBaseURL: `${origin}/api`,\n invitationAcceptBaseURL: origin,\n trustedOrigins: [origin],\n methods,\n accountPolicy: storefront.accountPolicy as CustomerBuyerAccountPolicy,\n }\n } finally {\n await dispose?.()\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront access-key helpers.
|
|
3
|
+
*
|
|
4
|
+
* A storefront access key is an opaque bearer token with a kind-specific
|
|
5
|
+
* prefix. Only the SHA-256 hash of the token is ever persisted; the plaintext
|
|
6
|
+
* is returned exactly once at issuance (reveal-once) and can never be
|
|
7
|
+
* re-derived. `tokenPreview` is a short, non-secret display fragment so an
|
|
8
|
+
* operator can recognise a key in a list without revealing it.
|
|
9
|
+
*
|
|
10
|
+
* - `vpk_` publishable — safe to embed in a browser bundle or native app;
|
|
11
|
+
* authorizes public read + customer-auth initiation from a declared origin.
|
|
12
|
+
* - `vsk_` secret — server-only (SSR/BFF); carries full storefront trust.
|
|
13
|
+
*
|
|
14
|
+
* Runtime-agnostic: uses only Web Crypto (`crypto.getRandomValues`,
|
|
15
|
+
* `crypto.subtle`), which is available in Node, workerd, and the browser.
|
|
16
|
+
*/
|
|
17
|
+
import type { StorefrontApiKeyKind } from "@voyant-travel/db/schema/iam";
|
|
18
|
+
export interface GeneratedStorefrontApiKey {
|
|
19
|
+
kind: StorefrontApiKeyKind;
|
|
20
|
+
/** Full plaintext token — shown once, never stored. */
|
|
21
|
+
token: string;
|
|
22
|
+
/** SHA-256 hex digest of `token`; the only value persisted for lookup. */
|
|
23
|
+
tokenHash: string;
|
|
24
|
+
/** Non-secret display fragment, e.g. "vpk_ab12cd". */
|
|
25
|
+
tokenPreview: string;
|
|
26
|
+
}
|
|
27
|
+
/** SHA-256 hex digest of an access token. */
|
|
28
|
+
export declare function hashStorefrontApiKey(token: string): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Classify a presented token by its prefix. Returns null when the token has no
|
|
31
|
+
* recognised storefront-key prefix, letting callers reject obviously-invalid
|
|
32
|
+
* tokens before hitting the database.
|
|
33
|
+
*/
|
|
34
|
+
export declare function classifyStorefrontApiKey(token: string): StorefrontApiKeyKind | null;
|
|
35
|
+
/**
|
|
36
|
+
* Mint a new storefront access key. The returned `token` is the only time the
|
|
37
|
+
* plaintext exists; persist `tokenHash`/`tokenPreview` and surface `token` to
|
|
38
|
+
* the operator exactly once.
|
|
39
|
+
*/
|
|
40
|
+
export declare function generateStorefrontApiKey(kind: StorefrontApiKeyKind): Promise<GeneratedStorefrontApiKey>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const KEY_PREFIXES = {
|
|
2
|
+
publishable: "vpk_",
|
|
3
|
+
secret: "vsk_",
|
|
4
|
+
};
|
|
5
|
+
/** Bytes of entropy in the random portion of a key (256 bits). */
|
|
6
|
+
const KEY_RANDOM_BYTES = 32;
|
|
7
|
+
function base64UrlFromBytes(bytes) {
|
|
8
|
+
let binary = "";
|
|
9
|
+
for (const byte of bytes)
|
|
10
|
+
binary += String.fromCharCode(byte);
|
|
11
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12
|
+
}
|
|
13
|
+
/** SHA-256 hex digest of an access token. */
|
|
14
|
+
export async function hashStorefrontApiKey(token) {
|
|
15
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
|
|
16
|
+
return Array.from(new Uint8Array(digest))
|
|
17
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
18
|
+
.join("");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Classify a presented token by its prefix. Returns null when the token has no
|
|
22
|
+
* recognised storefront-key prefix, letting callers reject obviously-invalid
|
|
23
|
+
* tokens before hitting the database.
|
|
24
|
+
*/
|
|
25
|
+
export function classifyStorefrontApiKey(token) {
|
|
26
|
+
for (const kind of Object.keys(KEY_PREFIXES)) {
|
|
27
|
+
if (token.startsWith(KEY_PREFIXES[kind]))
|
|
28
|
+
return kind;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mint a new storefront access key. The returned `token` is the only time the
|
|
34
|
+
* plaintext exists; persist `tokenHash`/`tokenPreview` and surface `token` to
|
|
35
|
+
* the operator exactly once.
|
|
36
|
+
*/
|
|
37
|
+
export async function generateStorefrontApiKey(kind) {
|
|
38
|
+
const random = new Uint8Array(KEY_RANDOM_BYTES);
|
|
39
|
+
crypto.getRandomValues(random);
|
|
40
|
+
const token = `${KEY_PREFIXES[kind]}${base64UrlFromBytes(random)}`;
|
|
41
|
+
return {
|
|
42
|
+
kind,
|
|
43
|
+
token,
|
|
44
|
+
tokenHash: await hashStorefrontApiKey(token),
|
|
45
|
+
// Prefix (4 chars) + first 6 chars of the random body: enough to
|
|
46
|
+
// disambiguate in a list, far too little to guess the token.
|
|
47
|
+
tokenPreview: token.slice(0, KEY_PREFIXES[kind].length + 6),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=storefront-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storefront-keys.js","sourceRoot":"","sources":["../src/storefront-keys.ts"],"names":[],"mappings":"AAkBA,MAAM,YAAY,GAAG;IACnB,WAAW,EAAE,MAAM;IACnB,MAAM,EAAE,MAAM;CACyC,CAAA;AAEzD,kEAAkE;AAClE,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAY3B,SAAS,kBAAkB,CAAC,KAAiB;IAC3C,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IAC7D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAChF,CAAC;AAED,6CAA6C;AAC7C,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAAa;IACtD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IACrF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACjD,IAAI,CAAC,EAAE,CAAC,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAa;IACpD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAA2B,EAAE,CAAC;QACvE,IAAI,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;IACvD,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,IAA0B;IAE1B,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,gBAAgB,CAAC,CAAA;IAC/C,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;IAC9B,MAAM,KAAK,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAA;IAClE,OAAO;QACL,IAAI;QACJ,KAAK;QACL,SAAS,EAAE,MAAM,oBAAoB,CAAC,KAAK,CAAC;QAC5C,iEAAiE;QACjE,6DAA6D;QAC7D,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;KAC5D,CAAA;AACH,CAAC","sourcesContent":["/**\n * Storefront access-key helpers.\n *\n * A storefront access key is an opaque bearer token with a kind-specific\n * prefix. Only the SHA-256 hash of the token is ever persisted; the plaintext\n * is returned exactly once at issuance (reveal-once) and can never be\n * re-derived. `tokenPreview` is a short, non-secret display fragment so an\n * operator can recognise a key in a list without revealing it.\n *\n * - `vpk_` publishable — safe to embed in a browser bundle or native app;\n * authorizes public read + customer-auth initiation from a declared origin.\n * - `vsk_` secret — server-only (SSR/BFF); carries full storefront trust.\n *\n * Runtime-agnostic: uses only Web Crypto (`crypto.getRandomValues`,\n * `crypto.subtle`), which is available in Node, workerd, and the browser.\n */\nimport type { StorefrontApiKeyKind } from \"@voyant-travel/db/schema/iam\"\n\nconst KEY_PREFIXES = {\n publishable: \"vpk_\",\n secret: \"vsk_\",\n} as const satisfies Record<StorefrontApiKeyKind, string>\n\n/** Bytes of entropy in the random portion of a key (256 bits). */\nconst KEY_RANDOM_BYTES = 32\n\nexport interface GeneratedStorefrontApiKey {\n kind: StorefrontApiKeyKind\n /** Full plaintext token — shown once, never stored. */\n token: string\n /** SHA-256 hex digest of `token`; the only value persisted for lookup. */\n tokenHash: string\n /** Non-secret display fragment, e.g. \"vpk_ab12cd\". */\n tokenPreview: string\n}\n\nfunction base64UrlFromBytes(bytes: Uint8Array): string {\n let binary = \"\"\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\n\n/** SHA-256 hex digest of an access token. */\nexport async function hashStorefrontApiKey(token: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(token))\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\")\n}\n\n/**\n * Classify a presented token by its prefix. Returns null when the token has no\n * recognised storefront-key prefix, letting callers reject obviously-invalid\n * tokens before hitting the database.\n */\nexport function classifyStorefrontApiKey(token: string): StorefrontApiKeyKind | null {\n for (const kind of Object.keys(KEY_PREFIXES) as StorefrontApiKeyKind[]) {\n if (token.startsWith(KEY_PREFIXES[kind])) return kind\n }\n return null\n}\n\n/**\n * Mint a new storefront access key. The returned `token` is the only time the\n * plaintext exists; persist `tokenHash`/`tokenPreview` and surface `token` to\n * the operator exactly once.\n */\nexport async function generateStorefrontApiKey(\n kind: StorefrontApiKeyKind,\n): Promise<GeneratedStorefrontApiKey> {\n const random = new Uint8Array(KEY_RANDOM_BYTES)\n crypto.getRandomValues(random)\n const token = `${KEY_PREFIXES[kind]}${base64UrlFromBytes(random)}`\n return {\n kind,\n token,\n tokenHash: await hashStorefrontApiKey(token),\n // Prefix (4 chars) + first 6 chars of the random body: enough to\n // disambiguate in a list, far too little to guess the token.\n tokenPreview: token.slice(0, KEY_PREFIXES[kind].length + 6),\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type StorefrontCredentialCipher } from "./storefront-credentials.js";
|
|
2
|
+
import type { StorefrontRuntimeProvider } from "./storefront-runtime-port.js";
|
|
3
|
+
export { isStorefrontOriginAllowed } from "./storefront-origins.js";
|
|
4
|
+
export declare function createLocalStorefrontAdapter(options: {
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the KMS-backed credential cipher for a deployment. Called with the
|
|
7
|
+
* request bindings so per-deployment KMS config is honoured; the self-host
|
|
8
|
+
* host passes `createKmsStorefrontCredentialCipher(env)`.
|
|
9
|
+
*/
|
|
10
|
+
resolveCipher: (bindings: Record<string, unknown>) => StorefrontCredentialCipher;
|
|
11
|
+
}): StorefrontRuntimeProvider;
|