@voyant-travel/auth 0.136.0 → 0.138.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/customer-business-accounts-contracts.d.ts +159 -0
- package/dist/customer-business-accounts-contracts.js +112 -0
- package/dist/customer-business-accounts-contracts.js.map +1 -0
- package/dist/customer-business-onboarding-graph-runtime.d.ts +13 -0
- package/dist/customer-business-onboarding-graph-runtime.js +8 -0
- package/dist/customer-business-onboarding-graph-runtime.js.map +1 -0
- package/dist/customer-business-onboarding-routes.d.ts +13 -0
- package/dist/customer-business-onboarding-routes.js +151 -0
- package/dist/customer-business-onboarding-routes.js.map +1 -0
- package/dist/customer-business-onboarding-runtime-port.d.ts +40 -0
- package/dist/customer-business-onboarding-runtime-port.js +25 -0
- package/dist/customer-business-onboarding-runtime-port.js.map +1 -0
- package/dist/customer-business-onboarding-service.d.ts +32 -0
- package/dist/customer-business-onboarding-service.js +237 -0
- package/dist/customer-business-onboarding-service.js.map +1 -0
- package/dist/customer-buyer-accounts.d.ts +0 -19
- package/dist/customer-buyer-accounts.js.map +1 -1
- package/dist/node-runtime.d.ts +13 -0
- package/dist/node-runtime.js +285 -1
- package/dist/node-runtime.js.map +1 -1
- package/dist/ports.d.ts +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/runtime-contributor.js +11 -0
- package/dist/runtime-contributor.js.map +1 -1
- package/dist/server.d.ts +39 -3
- package/dist/server.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 +2 -0
- package/dist/voyant.js +99 -0
- package/dist/voyant.js.map +1 -1
- package/openapi/admin/customer-business-accounts.json +44 -0
- package/package.json +50 -4
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local storefront runtime provider — backs {@link storefrontRuntimePort} with
|
|
3
|
+
* the deployment's own runtime DB (self-host). Mirrors the team-management
|
|
4
|
+
* local adapter: every operator write is bounded to `context.organizationId`,
|
|
5
|
+
* and request-time resolution (`resolveStorefrontByApiKey`) is org-agnostic.
|
|
6
|
+
*/
|
|
7
|
+
import { storefrontApiKeys, storefrontCustomerAuthCredentials, storefronts, } from "@voyant-travel/db/schema/iam";
|
|
8
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
9
|
+
import { validateStorefrontCredentialBundle, } from "./storefront-credentials.js";
|
|
10
|
+
import { classifyStorefrontApiKey, generateStorefrontApiKey, hashStorefrontApiKey, } from "./storefront-keys.js";
|
|
11
|
+
import { enabledStorefrontSocialProviders, normalizeStorefrontAllowedOrigins, normalizeStorefrontCustomerAccountPolicy, normalizeStorefrontCustomerAuthMethods, StorefrontInputError, } from "./storefront-origins.js";
|
|
12
|
+
export { isStorefrontOriginAllowed } from "./storefront-origins.js";
|
|
13
|
+
function toStorefrontDto(row) {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
organizationId: row.organizationId,
|
|
17
|
+
name: row.name,
|
|
18
|
+
slug: row.slug,
|
|
19
|
+
hostingKind: row.hostingKind,
|
|
20
|
+
siteId: row.siteId ?? null,
|
|
21
|
+
allowedOrigins: [...row.allowedOrigins],
|
|
22
|
+
methods: row.methods,
|
|
23
|
+
accountPolicy: row.accountPolicy,
|
|
24
|
+
hostOnlyCookies: row.hostOnlyCookies,
|
|
25
|
+
createdAt: row.createdAt.toISOString(),
|
|
26
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function toApiKeyDto(row) {
|
|
30
|
+
return {
|
|
31
|
+
id: row.id,
|
|
32
|
+
storefrontId: row.storefrontId,
|
|
33
|
+
kind: row.kind,
|
|
34
|
+
tokenPreview: row.tokenPreview,
|
|
35
|
+
name: row.name ?? null,
|
|
36
|
+
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
|
|
37
|
+
revokedAt: row.revokedAt?.toISOString() ?? null,
|
|
38
|
+
createdAt: row.createdAt.toISOString(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function createLocalStorefrontAdapter(options) {
|
|
42
|
+
const { resolveCipher } = options;
|
|
43
|
+
/** Load a storefront and prove it belongs to the acting organization. */
|
|
44
|
+
async function requireOwnedStorefront(context, storefrontId) {
|
|
45
|
+
const [row] = await context.db
|
|
46
|
+
.select()
|
|
47
|
+
.from(storefronts)
|
|
48
|
+
.where(and(eq(storefronts.id, storefrontId), eq(storefronts.organizationId, context.organizationId)))
|
|
49
|
+
.limit(1);
|
|
50
|
+
if (!row)
|
|
51
|
+
throw new StorefrontInputError("Storefront was not found.");
|
|
52
|
+
return row;
|
|
53
|
+
}
|
|
54
|
+
async function requireCredentialsForEnabledSocialMethods(context, storefrontId, methods) {
|
|
55
|
+
const providers = enabledStorefrontSocialProviders(methods);
|
|
56
|
+
if (providers.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
const rows = await context.db
|
|
59
|
+
.select({ provider: storefrontCustomerAuthCredentials.provider })
|
|
60
|
+
.from(storefrontCustomerAuthCredentials)
|
|
61
|
+
.where(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId));
|
|
62
|
+
const configured = new Set(rows.map((row) => row.provider));
|
|
63
|
+
for (const provider of providers) {
|
|
64
|
+
if (!configured.has(provider)) {
|
|
65
|
+
throw new StorefrontInputError(`Customer auth provider ${provider} cannot be enabled without a stored credential.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function persistStorefrontPatch(context, storefrontId, patch) {
|
|
70
|
+
const [row] = await context.db
|
|
71
|
+
.update(storefronts)
|
|
72
|
+
.set({ ...patch, updatedAt: new Date() })
|
|
73
|
+
.where(and(eq(storefronts.id, storefrontId), eq(storefronts.organizationId, context.organizationId)))
|
|
74
|
+
.returning();
|
|
75
|
+
if (!row)
|
|
76
|
+
throw new StorefrontInputError("Storefront was not found.");
|
|
77
|
+
return toStorefrontDto(row);
|
|
78
|
+
}
|
|
79
|
+
async function issueKeyRow(context, storefront, kind, name) {
|
|
80
|
+
const generated = await generateStorefrontApiKey(kind);
|
|
81
|
+
const [row] = await context.db
|
|
82
|
+
.insert(storefrontApiKeys)
|
|
83
|
+
.values({
|
|
84
|
+
storefrontId: storefront.id,
|
|
85
|
+
organizationId: storefront.organizationId,
|
|
86
|
+
kind,
|
|
87
|
+
tokenHash: generated.tokenHash,
|
|
88
|
+
tokenPreview: generated.tokenPreview,
|
|
89
|
+
name: name?.trim() || null,
|
|
90
|
+
})
|
|
91
|
+
.returning();
|
|
92
|
+
if (!row)
|
|
93
|
+
throw new StorefrontInputError("Failed to issue storefront access key.");
|
|
94
|
+
return { ...toApiKeyDto(row), token: generated.token };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
async listStorefronts(context) {
|
|
98
|
+
const rows = await context.db
|
|
99
|
+
.select()
|
|
100
|
+
.from(storefronts)
|
|
101
|
+
.where(eq(storefronts.organizationId, context.organizationId))
|
|
102
|
+
.orderBy(desc(storefronts.createdAt));
|
|
103
|
+
return rows.map(toStorefrontDto);
|
|
104
|
+
},
|
|
105
|
+
async getStorefront(context, storefrontId) {
|
|
106
|
+
return toStorefrontDto(await requireOwnedStorefront(context, storefrontId));
|
|
107
|
+
},
|
|
108
|
+
async createStorefront(context, input) {
|
|
109
|
+
const methods = normalizeStorefrontCustomerAuthMethods(input.methods);
|
|
110
|
+
if (enabledStorefrontSocialProviders(methods).length > 0) {
|
|
111
|
+
throw new StorefrontInputError("Add the provider credential before creating a storefront with a social method enabled.");
|
|
112
|
+
}
|
|
113
|
+
const accountPolicy = normalizeStorefrontCustomerAccountPolicy(input.accountPolicy ?? {
|
|
114
|
+
allowedKinds: ["personal"],
|
|
115
|
+
personalSignup: "open",
|
|
116
|
+
businessOnboarding: "disabled",
|
|
117
|
+
});
|
|
118
|
+
const allowedOrigins = normalizeStorefrontAllowedOrigins(input.allowedOrigins);
|
|
119
|
+
if (input.hostingKind === "external" && input.siteId) {
|
|
120
|
+
throw new StorefrontInputError("External storefronts cannot reference a hosting site.");
|
|
121
|
+
}
|
|
122
|
+
const slug = input.slug.trim();
|
|
123
|
+
if (!slug)
|
|
124
|
+
throw new StorefrontInputError("Storefront slug is required.");
|
|
125
|
+
const [row] = await context.db
|
|
126
|
+
.insert(storefronts)
|
|
127
|
+
.values({
|
|
128
|
+
organizationId: context.organizationId,
|
|
129
|
+
name: input.name.trim(),
|
|
130
|
+
slug,
|
|
131
|
+
hostingKind: input.hostingKind,
|
|
132
|
+
siteId: input.hostingKind === "cloud_site" ? (input.siteId ?? null) : null,
|
|
133
|
+
methods,
|
|
134
|
+
accountPolicy,
|
|
135
|
+
allowedOrigins,
|
|
136
|
+
hostOnlyCookies: true,
|
|
137
|
+
})
|
|
138
|
+
.returning();
|
|
139
|
+
if (!row)
|
|
140
|
+
throw new StorefrontInputError("Failed to create storefront.");
|
|
141
|
+
return toStorefrontDto(row);
|
|
142
|
+
},
|
|
143
|
+
async updateStorefront(context, storefrontId, patch) {
|
|
144
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
145
|
+
const update = {};
|
|
146
|
+
if (patch.name !== undefined)
|
|
147
|
+
update.name = patch.name.trim();
|
|
148
|
+
if (patch.allowedOrigins !== undefined) {
|
|
149
|
+
update.allowedOrigins = normalizeStorefrontAllowedOrigins(patch.allowedOrigins);
|
|
150
|
+
}
|
|
151
|
+
if (patch.accountPolicy !== undefined) {
|
|
152
|
+
update.accountPolicy = normalizeStorefrontCustomerAccountPolicy(patch.accountPolicy);
|
|
153
|
+
}
|
|
154
|
+
if (patch.methods !== undefined) {
|
|
155
|
+
const methods = normalizeStorefrontCustomerAuthMethods(patch.methods);
|
|
156
|
+
await requireCredentialsForEnabledSocialMethods(context, storefrontId, methods);
|
|
157
|
+
update.methods = methods;
|
|
158
|
+
}
|
|
159
|
+
return persistStorefrontPatch(context, storefrontId, update);
|
|
160
|
+
},
|
|
161
|
+
async deleteStorefront(context, storefrontId) {
|
|
162
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
163
|
+
await context.db
|
|
164
|
+
.delete(storefronts)
|
|
165
|
+
.where(and(eq(storefronts.id, storefrontId), eq(storefronts.organizationId, context.organizationId)));
|
|
166
|
+
},
|
|
167
|
+
async setAllowedOrigins(context, storefrontId, origins) {
|
|
168
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
169
|
+
return persistStorefrontPatch(context, storefrontId, {
|
|
170
|
+
allowedOrigins: normalizeStorefrontAllowedOrigins(origins),
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
async listApiKeys(context, storefrontId) {
|
|
174
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
175
|
+
const rows = await context.db
|
|
176
|
+
.select()
|
|
177
|
+
.from(storefrontApiKeys)
|
|
178
|
+
.where(eq(storefrontApiKeys.storefrontId, storefrontId))
|
|
179
|
+
.orderBy(desc(storefrontApiKeys.createdAt));
|
|
180
|
+
return rows.map(toApiKeyDto);
|
|
181
|
+
},
|
|
182
|
+
async issueApiKey(context, storefrontId, kind, name) {
|
|
183
|
+
const storefront = await requireOwnedStorefront(context, storefrontId);
|
|
184
|
+
return issueKeyRow(context, storefront, kind, name);
|
|
185
|
+
},
|
|
186
|
+
async rotateApiKey(context, storefrontId, keyId) {
|
|
187
|
+
const storefront = await requireOwnedStorefront(context, storefrontId);
|
|
188
|
+
const [existing] = await context.db
|
|
189
|
+
.select()
|
|
190
|
+
.from(storefrontApiKeys)
|
|
191
|
+
.where(and(eq(storefrontApiKeys.id, keyId), eq(storefrontApiKeys.storefrontId, storefrontId)))
|
|
192
|
+
.limit(1);
|
|
193
|
+
if (!existing)
|
|
194
|
+
throw new StorefrontInputError("Storefront access key was not found.");
|
|
195
|
+
await context.db
|
|
196
|
+
.update(storefrontApiKeys)
|
|
197
|
+
.set({ revokedAt: new Date(), updatedAt: new Date() })
|
|
198
|
+
.where(eq(storefrontApiKeys.id, keyId));
|
|
199
|
+
return issueKeyRow(context, storefront, existing.kind, existing.name);
|
|
200
|
+
},
|
|
201
|
+
async revokeApiKey(context, storefrontId, keyId) {
|
|
202
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
203
|
+
await context.db
|
|
204
|
+
.update(storefrontApiKeys)
|
|
205
|
+
.set({ revokedAt: new Date(), updatedAt: new Date() })
|
|
206
|
+
.where(and(eq(storefrontApiKeys.id, keyId), eq(storefrontApiKeys.storefrontId, storefrontId)));
|
|
207
|
+
},
|
|
208
|
+
async resolveStorefrontByApiKey(context, token) {
|
|
209
|
+
if (classifyStorefrontApiKey(token) === null)
|
|
210
|
+
return null;
|
|
211
|
+
const tokenHash = await hashStorefrontApiKey(token);
|
|
212
|
+
const [key] = await context.db
|
|
213
|
+
.select()
|
|
214
|
+
.from(storefrontApiKeys)
|
|
215
|
+
.where(eq(storefrontApiKeys.tokenHash, tokenHash))
|
|
216
|
+
.limit(1);
|
|
217
|
+
if (!key || key.revokedAt)
|
|
218
|
+
return null;
|
|
219
|
+
const [storefront] = await context.db
|
|
220
|
+
.select()
|
|
221
|
+
.from(storefronts)
|
|
222
|
+
.where(eq(storefronts.id, key.storefrontId))
|
|
223
|
+
.limit(1);
|
|
224
|
+
if (!storefront)
|
|
225
|
+
return null;
|
|
226
|
+
try {
|
|
227
|
+
await context.db
|
|
228
|
+
.update(storefrontApiKeys)
|
|
229
|
+
.set({ lastUsedAt: new Date() })
|
|
230
|
+
.where(eq(storefrontApiKeys.id, key.id));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Best-effort usage stamping; never fail the request on it.
|
|
234
|
+
}
|
|
235
|
+
return { storefront: toStorefrontDto(storefront), key: toApiKeyDto(key) };
|
|
236
|
+
},
|
|
237
|
+
async updateAccountPolicy(context, storefrontId, policy) {
|
|
238
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
239
|
+
return persistStorefrontPatch(context, storefrontId, {
|
|
240
|
+
accountPolicy: normalizeStorefrontCustomerAccountPolicy(policy),
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
async updateMethods(context, storefrontId, methods) {
|
|
244
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
245
|
+
const normalized = normalizeStorefrontCustomerAuthMethods(methods);
|
|
246
|
+
await requireCredentialsForEnabledSocialMethods(context, storefrontId, normalized);
|
|
247
|
+
return persistStorefrontPatch(context, storefrontId, { methods: normalized });
|
|
248
|
+
},
|
|
249
|
+
async listProviderCredentials(context, storefrontId) {
|
|
250
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
251
|
+
const rows = await context.db
|
|
252
|
+
.select()
|
|
253
|
+
.from(storefrontCustomerAuthCredentials)
|
|
254
|
+
.where(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId));
|
|
255
|
+
const byProvider = new Map(rows.map((row) => [row.provider, row]));
|
|
256
|
+
return ["google", "facebook", "apple"].map((provider) => {
|
|
257
|
+
const row = byProvider.get(provider);
|
|
258
|
+
return {
|
|
259
|
+
provider,
|
|
260
|
+
configured: Boolean(row),
|
|
261
|
+
updatedAt: row?.updatedAt.toISOString() ?? null,
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
async putProviderCredential(context, storefrontId, provider, credentials) {
|
|
266
|
+
const storefront = await requireOwnedStorefront(context, storefrontId);
|
|
267
|
+
const bundle = validateStorefrontCredentialBundle(provider, credentials);
|
|
268
|
+
const encrypted = await resolveCipher(context.bindings).encrypt(JSON.stringify(bundle));
|
|
269
|
+
const [existing] = await context.db
|
|
270
|
+
.select({ id: storefrontCustomerAuthCredentials.id })
|
|
271
|
+
.from(storefrontCustomerAuthCredentials)
|
|
272
|
+
.where(and(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId), eq(storefrontCustomerAuthCredentials.provider, provider)))
|
|
273
|
+
.limit(1);
|
|
274
|
+
if (existing) {
|
|
275
|
+
await context.db
|
|
276
|
+
.update(storefrontCustomerAuthCredentials)
|
|
277
|
+
.set({ encryptedCredentials: encrypted, updatedAt: new Date() })
|
|
278
|
+
.where(eq(storefrontCustomerAuthCredentials.id, existing.id));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await context.db.insert(storefrontCustomerAuthCredentials).values({
|
|
282
|
+
storefrontId,
|
|
283
|
+
organizationId: storefront.organizationId,
|
|
284
|
+
provider,
|
|
285
|
+
encryptedCredentials: encrypted,
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
async deleteProviderCredential(context, storefrontId, provider) {
|
|
289
|
+
await requireOwnedStorefront(context, storefrontId);
|
|
290
|
+
await context.db
|
|
291
|
+
.delete(storefrontCustomerAuthCredentials)
|
|
292
|
+
.where(and(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId), eq(storefrontCustomerAuthCredentials.provider, provider)));
|
|
293
|
+
},
|
|
294
|
+
async resolveProviderCredentials(context, storefrontId, providers) {
|
|
295
|
+
if (providers.length === 0)
|
|
296
|
+
return {};
|
|
297
|
+
const rows = await context.db
|
|
298
|
+
.select()
|
|
299
|
+
.from(storefrontCustomerAuthCredentials)
|
|
300
|
+
.where(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId));
|
|
301
|
+
const cipher = resolveCipher(context.bindings);
|
|
302
|
+
const resolved = {};
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
if (!providers.includes(row.provider))
|
|
305
|
+
continue;
|
|
306
|
+
const plaintext = await cipher.decrypt(row.encryptedCredentials);
|
|
307
|
+
resolved[row.provider] = JSON.parse(plaintext);
|
|
308
|
+
}
|
|
309
|
+
return resolved;
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=storefront-local-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storefront-local-adapter.js","sourceRoot":"","sources":["../src/storefront-local-adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAML,iBAAiB,EACjB,iCAAiC,EACjC,WAAW,GACZ,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,EAEL,kCAAkC,GACnC,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,gCAAgC,EAChC,iCAAiC,EACjC,wCAAwC,EACxC,sCAAsC,EACtC,oBAAoB,GACrB,MAAM,yBAAyB,CAAA;AAahC,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAA;AAEnE,SAAS,eAAe,CAAC,GAAqB;IAC5C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI;QAC1B,cAAc,EAAE,CAAC,GAAG,GAAG,CAAC,cAAc,CAAC;QACvC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,aAAa,EAAE,GAAG,CAAC,aAAa;QAChC,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;QACtC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;KACvC,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAA2B;IAC9C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI;QACtB,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,IAAI;QACjD,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI;QAC/C,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;KACvC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,OAO5C;IACC,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAA;IAEjC,yEAAyE;IACzE,KAAK,UAAU,sBAAsB,CACnC,OAAiC,EACjC,YAAoB;QAEpB,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;aAC3B,MAAM,EAAE;aACR,IAAI,CAAC,WAAW,CAAC;aACjB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,YAAY,CAAC,EAChC,EAAE,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,CACvD,CACF;aACA,KAAK,CAAC,CAAC,CAAC,CAAA;QACX,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,oBAAoB,CAAC,2BAA2B,CAAC,CAAA;QACrE,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,KAAK,UAAU,yCAAyC,CACtD,OAAiC,EACjC,YAAoB,EACpB,OAAiC;QAEjC,MAAM,SAAS,GAAG,gCAAgC,CAAC,OAAO,CAAC,CAAA;QAC3D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAClC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,EAAE;aAC1B,MAAM,CAAC,EAAE,QAAQ,EAAE,iCAAiC,CAAC,QAAQ,EAAE,CAAC;aAChE,IAAI,CAAC,iCAAiC,CAAC;aACvC,KAAK,CAAC,EAAE,CAAC,iCAAiC,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAA;QAC1E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;QAC3D,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC9B,MAAM,IAAI,oBAAoB,CAC5B,0BAA0B,QAAQ,iDAAiD,CACpF,CAAA;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,UAAU,sBAAsB,CACnC,OAAiC,EACjC,YAAoB,EACpB,KAAgC;QAEhC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;aAC3B,MAAM,CAAC,WAAW,CAAC;aACnB,GAAG,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;aACxC,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,YAAY,CAAC,EAChC,EAAE,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,CACvD,CACF;aACA,SAAS,EAAE,CAAA;QACd,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,oBAAoB,CAAC,2BAA2B,CAAC,CAAA;QACrE,OAAO,eAAe,CAAC,GAAG,CAAC,CAAA;IAC7B,CAAC;IAED,KAAK,UAAU,WAAW,CACxB,OAAiC,EACjC,UAA4B,EAC5B,IAAiC,EACjC,IAAoB;QAEpB,MAAM,SAAS,GAAG,MAAM,wBAAwB,CAAC,IAAI,CAAC,CAAA;QACtD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;aAC3B,MAAM,CAAC,iBAAiB,CAAC;aACzB,MAAM,CAAC;YACN,YAAY,EAAE,UAAU,CAAC,EAAE;YAC3B,cAAc,EAAE,UAAU,CAAC,cAAc;YACzC,IAAI;YACJ,SAAS,EAAE,SAAS,CAAC,SAAS;YAC9B,YAAY,EAAE,SAAS,CAAC,YAAY;YACpC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI;SAC3B,CAAC;aACD,SAAS,EAAE,CAAA;QACd,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,oBAAoB,CAAC,wCAAwC,CAAC,CAAA;QAClF,OAAO,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAA;IACxD,CAAC;IAED,OAAO;QACL,KAAK,CAAC,eAAe,CAAC,OAAO;YAC3B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,EAAE;iBAC1B,MAAM,EAAE;iBACR,IAAI,CAAC,WAAW,CAAC;iBACjB,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;iBAC7D,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAA;YACvC,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;QAClC,CAAC;QAED,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,YAAY;YACvC,OAAO,eAAe,CAAC,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAA;QAC7E,CAAC;QAED,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK;YACnC,MAAM,OAAO,GAAG,sCAAsC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACrE,IAAI,gCAAgC,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzD,MAAM,IAAI,oBAAoB,CAC5B,wFAAwF,CACzF,CAAA;YACH,CAAC;YACD,MAAM,aAAa,GAAG,wCAAwC,CAC5D,KAAK,CAAC,aAAa,IAAI;gBACrB,YAAY,EAAE,CAAC,UAAU,CAAC;gBAC1B,cAAc,EAAE,MAAM;gBACtB,kBAAkB,EAAE,UAAU;aAC/B,CACF,CAAA;YACD,MAAM,cAAc,GAAG,iCAAiC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;YAC9E,IAAI,KAAK,CAAC,WAAW,KAAK,UAAU,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACrD,MAAM,IAAI,oBAAoB,CAAC,uDAAuD,CAAC,CAAA;YACzF,CAAC;YACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;YAC9B,IAAI,CAAC,IAAI;gBAAE,MAAM,IAAI,oBAAoB,CAAC,8BAA8B,CAAC,CAAA;YACzE,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;iBAC3B,MAAM,CAAC,WAAW,CAAC;iBACnB,MAAM,CAAC;gBACN,cAAc,EAAE,OAAO,CAAC,cAAc;gBACtC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;gBACvB,IAAI;gBACJ,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,MAAM,EAAE,KAAK,CAAC,WAAW,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;gBAC1E,OAAO;gBACP,aAAa;gBACb,cAAc;gBACd,eAAe,EAAE,IAAI;aACtB,CAAC;iBACD,SAAS,EAAE,CAAA;YACd,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,oBAAoB,CAAC,8BAA8B,CAAC,CAAA;YACxE,OAAO,eAAe,CAAC,GAAG,CAAC,CAAA;QAC7B,CAAC;QAED,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK;YACjD,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,MAAM,MAAM,GAA8B,EAAE,CAAA;YAC5C,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;gBAAE,MAAM,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;YAC7D,IAAI,KAAK,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;gBACvC,MAAM,CAAC,cAAc,GAAG,iCAAiC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;YACjF,CAAC;YACD,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;gBACtC,MAAM,CAAC,aAAa,GAAG,wCAAwC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;YACtF,CAAC;YACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBAChC,MAAM,OAAO,GAAG,sCAAsC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;gBACrE,MAAM,yCAAyC,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,CAAA;gBAC/E,MAAM,CAAC,OAAO,GAAG,OAAO,CAAA;YAC1B,CAAC;YACD,OAAO,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,CAAA;QAC9D,CAAC;QAED,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY;YAC1C,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,MAAM,OAAO,CAAC,EAAE;iBACb,MAAM,CAAC,WAAW,CAAC;iBACnB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,YAAY,CAAC,EAChC,EAAE,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,CACvD,CACF,CAAA;QACL,CAAC;QAED,KAAK,CAAC,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO;YACpD,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,OAAO,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE;gBACnD,cAAc,EAAE,iCAAiC,CAAC,OAAO,CAAC;aAC3D,CAAC,CAAA;QACJ,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,YAAY;YACrC,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,EAAE;iBAC1B,MAAM,EAAE;iBACR,IAAI,CAAC,iBAAiB,CAAC;iBACvB,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;iBACvD,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC,CAAA;YAC7C,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAC9B,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI;YACjD,MAAM,UAAU,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACtE,OAAO,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;QACrD,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK;YAC7C,MAAM,UAAU,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACtE,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;iBAChC,MAAM,EAAE;iBACR,IAAI,CAAC,iBAAiB,CAAC;iBACvB,KAAK,CACJ,GAAG,CAAC,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CACvF;iBACA,KAAK,CAAC,CAAC,CAAC,CAAA;YACX,IAAI,CAAC,QAAQ;gBAAE,MAAM,IAAI,oBAAoB,CAAC,sCAAsC,CAAC,CAAA;YACrF,MAAM,OAAO,CAAC,EAAE;iBACb,MAAM,CAAC,iBAAiB,CAAC;iBACzB,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;iBACrD,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAA;YACzC,OAAO,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACvE,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK;YAC7C,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,MAAM,OAAO,CAAC,EAAE;iBACb,MAAM,CAAC,iBAAiB,CAAC;iBACzB,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;iBACrD,KAAK,CACJ,GAAG,CAAC,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CACvF,CAAA;QACL,CAAC;QAED,KAAK,CAAC,yBAAyB,CAC7B,OAAiC,EACjC,KAAa;YAEb,IAAI,wBAAwB,CAAC,KAAK,CAAC,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAA;YACzD,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAAC,KAAK,CAAC,CAAA;YACnD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;iBAC3B,MAAM,EAAE;iBACR,IAAI,CAAC,iBAAiB,CAAC;iBACvB,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;iBACjD,KAAK,CAAC,CAAC,CAAC,CAAA;YACX,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAA;YACtC,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;iBAClC,MAAM,EAAE;iBACR,IAAI,CAAC,WAAW,CAAC;iBACjB,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;iBAC3C,KAAK,CAAC,CAAC,CAAC,CAAA;YACX,IAAI,CAAC,UAAU;gBAAE,OAAO,IAAI,CAAA;YAC5B,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,EAAE;qBACb,MAAM,CAAC,iBAAiB,CAAC;qBACzB,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;qBAC/B,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,4DAA4D;YAC9D,CAAC;YACD,OAAO,EAAE,UAAU,EAAE,eAAe,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,CAAA;QAC3E,CAAC;QAED,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM;YACrD,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,OAAO,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE;gBACnD,aAAa,EAAE,wCAAwC,CAAC,MAAM,CAAC;aAChE,CAAC,CAAA;QACJ,CAAC;QAED,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO;YAChD,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,MAAM,UAAU,GAAG,sCAAsC,CAAC,OAAO,CAAC,CAAA;YAClE,MAAM,yCAAyC,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,CAAC,CAAA;YAClF,OAAO,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAA;QAC/E,CAAC;QAED,KAAK,CAAC,uBAAuB,CAAC,OAAO,EAAE,YAAY;YACjD,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,EAAE;iBAC1B,MAAM,EAAE;iBACR,IAAI,CAAC,iCAAiC,CAAC;iBACvC,KAAK,CAAC,EAAE,CAAC,iCAAiC,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAA;YAC1E,MAAM,UAAU,GAAG,IAAI,GAAG,CAGxB,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;YACzC,OAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAW,CAAC,GAAG,CACnD,CAAC,QAAQ,EAAyC,EAAE;gBAClD,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;gBACpC,OAAO;oBACL,QAAQ;oBACR,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC;oBACxB,SAAS,EAAE,GAAG,EAAE,SAAS,CAAC,WAAW,EAAE,IAAI,IAAI;iBAChD,CAAA;YACH,CAAC,CACF,CAAA;QACH,CAAC;QAED,KAAK,CAAC,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW;YACtE,MAAM,UAAU,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACtE,MAAM,MAAM,GAAG,kCAAkC,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;YACxE,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;YACvF,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;iBAChC,MAAM,CAAC,EAAE,EAAE,EAAE,iCAAiC,CAAC,EAAE,EAAE,CAAC;iBACpD,IAAI,CAAC,iCAAiC,CAAC;iBACvC,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,iCAAiC,CAAC,YAAY,EAAE,YAAY,CAAC,EAChE,EAAE,CAAC,iCAAiC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACzD,CACF;iBACA,KAAK,CAAC,CAAC,CAAC,CAAA;YACX,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,OAAO,CAAC,EAAE;qBACb,MAAM,CAAC,iCAAiC,CAAC;qBACzC,GAAG,CAAC,EAAE,oBAAoB,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;qBAC/D,KAAK,CAAC,EAAE,CAAC,iCAAiC,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;gBAC/D,OAAM;YACR,CAAC;YACD,MAAM,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,iCAAiC,CAAC,CAAC,MAAM,CAAC;gBAChE,YAAY;gBACZ,cAAc,EAAE,UAAU,CAAC,cAAc;gBACzC,QAAQ;gBACR,oBAAoB,EAAE,SAAS;aAChC,CAAC,CAAA;QACJ,CAAC;QAED,KAAK,CAAC,wBAAwB,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ;YAC5D,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACnD,MAAM,OAAO,CAAC,EAAE;iBACb,MAAM,CAAC,iCAAiC,CAAC;iBACzC,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,iCAAiC,CAAC,YAAY,EAAE,YAAY,CAAC,EAChE,EAAE,CAAC,iCAAiC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACzD,CACF,CAAA;QACL,CAAC;QAED,KAAK,CAAC,0BAA0B,CAC9B,OAAiC,EACjC,YAAoB,EACpB,SAA0D;YAE1D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAA;YACrC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,EAAE;iBAC1B,MAAM,EAAE;iBACR,IAAI,CAAC,iCAAiC,CAAC;iBACvC,KAAK,CAAC,EAAE,CAAC,iCAAiC,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAA;YAC1E,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YAC9C,MAAM,QAAQ,GAA0C,EAAE,CAAA;YAC1D,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;oBAAE,SAAQ;gBAC/C,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;gBAChE,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAA2B,CAAA;YAC1E,CAAC;YACD,OAAO,QAAQ,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * Local storefront runtime provider — backs {@link storefrontRuntimePort} with\n * the deployment's own runtime DB (self-host). Mirrors the team-management\n * local adapter: every operator write is bounded to `context.organizationId`,\n * and request-time resolution (`resolveStorefrontByApiKey`) is org-agnostic.\n */\nimport {\n type InsertStorefront,\n type SelectStorefront,\n type SelectStorefrontApiKey,\n type SelectStorefrontCustomerAuthCredential,\n type StorefrontCustomerAuthSocialProvider,\n storefrontApiKeys,\n storefrontCustomerAuthCredentials,\n storefronts,\n} from \"@voyant-travel/db/schema/iam\"\nimport { and, desc, eq } from \"drizzle-orm\"\n\nimport {\n type StorefrontCredentialCipher,\n validateStorefrontCredentialBundle,\n} from \"./storefront-credentials.js\"\nimport {\n classifyStorefrontApiKey,\n generateStorefrontApiKey,\n hashStorefrontApiKey,\n} from \"./storefront-keys.js\"\nimport {\n enabledStorefrontSocialProviders,\n normalizeStorefrontAllowedOrigins,\n normalizeStorefrontCustomerAccountPolicy,\n normalizeStorefrontCustomerAuthMethods,\n StorefrontInputError,\n} from \"./storefront-origins.js\"\nimport type {\n IssuedStorefrontApiKeyDto,\n ResolvedStorefrontApiKey,\n ResolvedStorefrontProviderCredentials,\n StorefrontApiKeyDto,\n StorefrontDto,\n StorefrontProviderCredentialStatusDto,\n StorefrontRequestContext,\n StorefrontResolveContext,\n StorefrontRuntimeProvider,\n} from \"./storefront-runtime-port.js\"\n\nexport { isStorefrontOriginAllowed } from \"./storefront-origins.js\"\n\nfunction toStorefrontDto(row: SelectStorefront): StorefrontDto {\n return {\n id: row.id,\n organizationId: row.organizationId,\n name: row.name,\n slug: row.slug,\n hostingKind: row.hostingKind,\n siteId: row.siteId ?? null,\n allowedOrigins: [...row.allowedOrigins],\n methods: row.methods,\n accountPolicy: row.accountPolicy,\n hostOnlyCookies: row.hostOnlyCookies,\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n }\n}\n\nfunction toApiKeyDto(row: SelectStorefrontApiKey): StorefrontApiKeyDto {\n return {\n id: row.id,\n storefrontId: row.storefrontId,\n kind: row.kind,\n tokenPreview: row.tokenPreview,\n name: row.name ?? null,\n lastUsedAt: row.lastUsedAt?.toISOString() ?? null,\n revokedAt: row.revokedAt?.toISOString() ?? null,\n createdAt: row.createdAt.toISOString(),\n }\n}\n\nexport function createLocalStorefrontAdapter(options: {\n /**\n * Resolve the KMS-backed credential cipher for a deployment. Called with the\n * request bindings so per-deployment KMS config is honoured; the self-host\n * host passes `createKmsStorefrontCredentialCipher(env)`.\n */\n resolveCipher: (bindings: Record<string, unknown>) => StorefrontCredentialCipher\n}): StorefrontRuntimeProvider {\n const { resolveCipher } = options\n\n /** Load a storefront and prove it belongs to the acting organization. */\n async function requireOwnedStorefront(\n context: StorefrontRequestContext,\n storefrontId: string,\n ): Promise<SelectStorefront> {\n const [row] = await context.db\n .select()\n .from(storefronts)\n .where(\n and(\n eq(storefronts.id, storefrontId),\n eq(storefronts.organizationId, context.organizationId),\n ),\n )\n .limit(1)\n if (!row) throw new StorefrontInputError(\"Storefront was not found.\")\n return row\n }\n\n async function requireCredentialsForEnabledSocialMethods(\n context: StorefrontRequestContext,\n storefrontId: string,\n methods: StorefrontDto[\"methods\"],\n ): Promise<void> {\n const providers = enabledStorefrontSocialProviders(methods)\n if (providers.length === 0) return\n const rows = await context.db\n .select({ provider: storefrontCustomerAuthCredentials.provider })\n .from(storefrontCustomerAuthCredentials)\n .where(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId))\n const configured = new Set(rows.map((row) => row.provider))\n for (const provider of providers) {\n if (!configured.has(provider)) {\n throw new StorefrontInputError(\n `Customer auth provider ${provider} cannot be enabled without a stored credential.`,\n )\n }\n }\n }\n\n async function persistStorefrontPatch(\n context: StorefrontRequestContext,\n storefrontId: string,\n patch: Partial<InsertStorefront>,\n ): Promise<StorefrontDto> {\n const [row] = await context.db\n .update(storefronts)\n .set({ ...patch, updatedAt: new Date() })\n .where(\n and(\n eq(storefronts.id, storefrontId),\n eq(storefronts.organizationId, context.organizationId),\n ),\n )\n .returning()\n if (!row) throw new StorefrontInputError(\"Storefront was not found.\")\n return toStorefrontDto(row)\n }\n\n async function issueKeyRow(\n context: StorefrontRequestContext,\n storefront: SelectStorefront,\n kind: StorefrontApiKeyDto[\"kind\"],\n name?: string | null,\n ): Promise<IssuedStorefrontApiKeyDto> {\n const generated = await generateStorefrontApiKey(kind)\n const [row] = await context.db\n .insert(storefrontApiKeys)\n .values({\n storefrontId: storefront.id,\n organizationId: storefront.organizationId,\n kind,\n tokenHash: generated.tokenHash,\n tokenPreview: generated.tokenPreview,\n name: name?.trim() || null,\n })\n .returning()\n if (!row) throw new StorefrontInputError(\"Failed to issue storefront access key.\")\n return { ...toApiKeyDto(row), token: generated.token }\n }\n\n return {\n async listStorefronts(context) {\n const rows = await context.db\n .select()\n .from(storefronts)\n .where(eq(storefronts.organizationId, context.organizationId))\n .orderBy(desc(storefronts.createdAt))\n return rows.map(toStorefrontDto)\n },\n\n async getStorefront(context, storefrontId) {\n return toStorefrontDto(await requireOwnedStorefront(context, storefrontId))\n },\n\n async createStorefront(context, input) {\n const methods = normalizeStorefrontCustomerAuthMethods(input.methods)\n if (enabledStorefrontSocialProviders(methods).length > 0) {\n throw new StorefrontInputError(\n \"Add the provider credential before creating a storefront with a social method enabled.\",\n )\n }\n const accountPolicy = normalizeStorefrontCustomerAccountPolicy(\n input.accountPolicy ?? {\n allowedKinds: [\"personal\"],\n personalSignup: \"open\",\n businessOnboarding: \"disabled\",\n },\n )\n const allowedOrigins = normalizeStorefrontAllowedOrigins(input.allowedOrigins)\n if (input.hostingKind === \"external\" && input.siteId) {\n throw new StorefrontInputError(\"External storefronts cannot reference a hosting site.\")\n }\n const slug = input.slug.trim()\n if (!slug) throw new StorefrontInputError(\"Storefront slug is required.\")\n const [row] = await context.db\n .insert(storefronts)\n .values({\n organizationId: context.organizationId,\n name: input.name.trim(),\n slug,\n hostingKind: input.hostingKind,\n siteId: input.hostingKind === \"cloud_site\" ? (input.siteId ?? null) : null,\n methods,\n accountPolicy,\n allowedOrigins,\n hostOnlyCookies: true,\n })\n .returning()\n if (!row) throw new StorefrontInputError(\"Failed to create storefront.\")\n return toStorefrontDto(row)\n },\n\n async updateStorefront(context, storefrontId, patch) {\n await requireOwnedStorefront(context, storefrontId)\n const update: Partial<InsertStorefront> = {}\n if (patch.name !== undefined) update.name = patch.name.trim()\n if (patch.allowedOrigins !== undefined) {\n update.allowedOrigins = normalizeStorefrontAllowedOrigins(patch.allowedOrigins)\n }\n if (patch.accountPolicy !== undefined) {\n update.accountPolicy = normalizeStorefrontCustomerAccountPolicy(patch.accountPolicy)\n }\n if (patch.methods !== undefined) {\n const methods = normalizeStorefrontCustomerAuthMethods(patch.methods)\n await requireCredentialsForEnabledSocialMethods(context, storefrontId, methods)\n update.methods = methods\n }\n return persistStorefrontPatch(context, storefrontId, update)\n },\n\n async deleteStorefront(context, storefrontId) {\n await requireOwnedStorefront(context, storefrontId)\n await context.db\n .delete(storefronts)\n .where(\n and(\n eq(storefronts.id, storefrontId),\n eq(storefronts.organizationId, context.organizationId),\n ),\n )\n },\n\n async setAllowedOrigins(context, storefrontId, origins) {\n await requireOwnedStorefront(context, storefrontId)\n return persistStorefrontPatch(context, storefrontId, {\n allowedOrigins: normalizeStorefrontAllowedOrigins(origins),\n })\n },\n\n async listApiKeys(context, storefrontId) {\n await requireOwnedStorefront(context, storefrontId)\n const rows = await context.db\n .select()\n .from(storefrontApiKeys)\n .where(eq(storefrontApiKeys.storefrontId, storefrontId))\n .orderBy(desc(storefrontApiKeys.createdAt))\n return rows.map(toApiKeyDto)\n },\n\n async issueApiKey(context, storefrontId, kind, name) {\n const storefront = await requireOwnedStorefront(context, storefrontId)\n return issueKeyRow(context, storefront, kind, name)\n },\n\n async rotateApiKey(context, storefrontId, keyId) {\n const storefront = await requireOwnedStorefront(context, storefrontId)\n const [existing] = await context.db\n .select()\n .from(storefrontApiKeys)\n .where(\n and(eq(storefrontApiKeys.id, keyId), eq(storefrontApiKeys.storefrontId, storefrontId)),\n )\n .limit(1)\n if (!existing) throw new StorefrontInputError(\"Storefront access key was not found.\")\n await context.db\n .update(storefrontApiKeys)\n .set({ revokedAt: new Date(), updatedAt: new Date() })\n .where(eq(storefrontApiKeys.id, keyId))\n return issueKeyRow(context, storefront, existing.kind, existing.name)\n },\n\n async revokeApiKey(context, storefrontId, keyId) {\n await requireOwnedStorefront(context, storefrontId)\n await context.db\n .update(storefrontApiKeys)\n .set({ revokedAt: new Date(), updatedAt: new Date() })\n .where(\n and(eq(storefrontApiKeys.id, keyId), eq(storefrontApiKeys.storefrontId, storefrontId)),\n )\n },\n\n async resolveStorefrontByApiKey(\n context: StorefrontResolveContext,\n token: string,\n ): Promise<ResolvedStorefrontApiKey | null> {\n if (classifyStorefrontApiKey(token) === null) return null\n const tokenHash = await hashStorefrontApiKey(token)\n const [key] = await context.db\n .select()\n .from(storefrontApiKeys)\n .where(eq(storefrontApiKeys.tokenHash, tokenHash))\n .limit(1)\n if (!key || key.revokedAt) return null\n const [storefront] = await context.db\n .select()\n .from(storefronts)\n .where(eq(storefronts.id, key.storefrontId))\n .limit(1)\n if (!storefront) return null\n try {\n await context.db\n .update(storefrontApiKeys)\n .set({ lastUsedAt: new Date() })\n .where(eq(storefrontApiKeys.id, key.id))\n } catch {\n // Best-effort usage stamping; never fail the request on it.\n }\n return { storefront: toStorefrontDto(storefront), key: toApiKeyDto(key) }\n },\n\n async updateAccountPolicy(context, storefrontId, policy) {\n await requireOwnedStorefront(context, storefrontId)\n return persistStorefrontPatch(context, storefrontId, {\n accountPolicy: normalizeStorefrontCustomerAccountPolicy(policy),\n })\n },\n\n async updateMethods(context, storefrontId, methods) {\n await requireOwnedStorefront(context, storefrontId)\n const normalized = normalizeStorefrontCustomerAuthMethods(methods)\n await requireCredentialsForEnabledSocialMethods(context, storefrontId, normalized)\n return persistStorefrontPatch(context, storefrontId, { methods: normalized })\n },\n\n async listProviderCredentials(context, storefrontId) {\n await requireOwnedStorefront(context, storefrontId)\n const rows = await context.db\n .select()\n .from(storefrontCustomerAuthCredentials)\n .where(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId))\n const byProvider = new Map<\n StorefrontCustomerAuthSocialProvider,\n SelectStorefrontCustomerAuthCredential\n >(rows.map((row) => [row.provider, row]))\n return ([\"google\", \"facebook\", \"apple\"] as const).map(\n (provider): StorefrontProviderCredentialStatusDto => {\n const row = byProvider.get(provider)\n return {\n provider,\n configured: Boolean(row),\n updatedAt: row?.updatedAt.toISOString() ?? null,\n }\n },\n )\n },\n\n async putProviderCredential(context, storefrontId, provider, credentials) {\n const storefront = await requireOwnedStorefront(context, storefrontId)\n const bundle = validateStorefrontCredentialBundle(provider, credentials)\n const encrypted = await resolveCipher(context.bindings).encrypt(JSON.stringify(bundle))\n const [existing] = await context.db\n .select({ id: storefrontCustomerAuthCredentials.id })\n .from(storefrontCustomerAuthCredentials)\n .where(\n and(\n eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId),\n eq(storefrontCustomerAuthCredentials.provider, provider),\n ),\n )\n .limit(1)\n if (existing) {\n await context.db\n .update(storefrontCustomerAuthCredentials)\n .set({ encryptedCredentials: encrypted, updatedAt: new Date() })\n .where(eq(storefrontCustomerAuthCredentials.id, existing.id))\n return\n }\n await context.db.insert(storefrontCustomerAuthCredentials).values({\n storefrontId,\n organizationId: storefront.organizationId,\n provider,\n encryptedCredentials: encrypted,\n })\n },\n\n async deleteProviderCredential(context, storefrontId, provider) {\n await requireOwnedStorefront(context, storefrontId)\n await context.db\n .delete(storefrontCustomerAuthCredentials)\n .where(\n and(\n eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId),\n eq(storefrontCustomerAuthCredentials.provider, provider),\n ),\n )\n },\n\n async resolveProviderCredentials(\n context: StorefrontResolveContext,\n storefrontId: string,\n providers: readonly StorefrontCustomerAuthSocialProvider[],\n ): Promise<ResolvedStorefrontProviderCredentials> {\n if (providers.length === 0) return {}\n const rows = await context.db\n .select()\n .from(storefrontCustomerAuthCredentials)\n .where(eq(storefrontCustomerAuthCredentials.storefrontId, storefrontId))\n const cipher = resolveCipher(context.bindings)\n const resolved: ResolvedStorefrontProviderCredentials = {}\n for (const row of rows) {\n if (!providers.includes(row.provider)) continue\n const plaintext = await cipher.decrypt(row.encryptedCredentials)\n resolved[row.provider] = JSON.parse(plaintext) as Record<string, string>\n }\n return resolved\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront origin + policy normalization (provider-neutral, pure).
|
|
3
|
+
*
|
|
4
|
+
* Operators declare which browser origins may use a storefront's keys. There is
|
|
5
|
+
* no ownership proof — declaration by the authenticated operator IS the
|
|
6
|
+
* authorization, so a leaked publishable key is still bound to these origins by
|
|
7
|
+
* CORS and Better Auth trusted origins.
|
|
8
|
+
*/
|
|
9
|
+
import type { StorefrontCustomerAccountPolicy, StorefrontCustomerAuthMethods, StorefrontCustomerAuthSocialProvider } from "@voyant-travel/db/schema/iam";
|
|
10
|
+
export declare const STOREFRONT_SOCIAL_PROVIDERS: readonly ["google", "facebook", "apple"];
|
|
11
|
+
export declare class StorefrontInputError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a set of operator-declared allowed origins. Each entry is either an
|
|
16
|
+
* exact origin or a single-label `https://*.host` wildcard. HTTP is permitted
|
|
17
|
+
* only for localhost (so a storefront can run against a sandbox from a
|
|
18
|
+
* developer's machine); every other host must use HTTPS. Duplicates are removed
|
|
19
|
+
* and the result is sorted for a stable persisted value.
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizeStorefrontAllowedOrigins(origins: readonly string[]): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Whether a request origin is authorized by a storefront's declared origins.
|
|
24
|
+
* Exact origins match verbatim; a `https://*.host` wildcard matches exactly one
|
|
25
|
+
* additional leading DNS label (`https://app.host`, not `https://a.b.host` and
|
|
26
|
+
* not the bare `https://host`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function isStorefrontOriginAllowed(origin: string, allowedOrigins: readonly string[]): boolean;
|
|
29
|
+
/** Public storefront methods projection: coerces every flag to a strict boolean. */
|
|
30
|
+
export declare function normalizeStorefrontCustomerAuthMethods(methods: StorefrontCustomerAuthMethods): StorefrontCustomerAuthMethods;
|
|
31
|
+
/** Social providers whose method flag is enabled on a storefront. */
|
|
32
|
+
export declare function enabledStorefrontSocialProviders(methods: StorefrontCustomerAuthMethods): StorefrontCustomerAuthSocialProvider[];
|
|
33
|
+
/** Normalize and validate the public B2C/B2B buyer-account capability policy. */
|
|
34
|
+
export declare function normalizeStorefrontCustomerAccountPolicy(policy: StorefrontCustomerAccountPolicy): StorefrontCustomerAccountPolicy;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export const STOREFRONT_SOCIAL_PROVIDERS = ["google", "facebook", "apple"];
|
|
2
|
+
export class StorefrontInputError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "StorefrontInputError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
9
|
+
const WILDCARD_PREFIX = "https://*.";
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a set of operator-declared allowed origins. Each entry is either an
|
|
12
|
+
* exact origin or a single-label `https://*.host` wildcard. HTTP is permitted
|
|
13
|
+
* only for localhost (so a storefront can run against a sandbox from a
|
|
14
|
+
* developer's machine); every other host must use HTTPS. Duplicates are removed
|
|
15
|
+
* and the result is sorted for a stable persisted value.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeStorefrontAllowedOrigins(origins) {
|
|
18
|
+
const normalized = origins.map((candidate) => {
|
|
19
|
+
const trimmed = candidate.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
throw new StorefrontInputError("Storefront allowed origins cannot be empty.");
|
|
22
|
+
}
|
|
23
|
+
// Wildcard form: https://*.example.com (exactly one leading label).
|
|
24
|
+
if (trimmed.startsWith(WILDCARD_PREFIX)) {
|
|
25
|
+
const host = trimmed.slice(WILDCARD_PREFIX.length);
|
|
26
|
+
if (!host || host.includes("*") || host.includes("/") || host.includes(":")) {
|
|
27
|
+
throw new StorefrontInputError(`Invalid storefront wildcard origin: ${candidate}`);
|
|
28
|
+
}
|
|
29
|
+
return trimmed.replace(/\/$/, "");
|
|
30
|
+
}
|
|
31
|
+
let url;
|
|
32
|
+
try {
|
|
33
|
+
url = new URL(trimmed);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new StorefrontInputError(`Invalid storefront allowed origin: ${candidate}`);
|
|
37
|
+
}
|
|
38
|
+
const isLocalhost = LOCALHOST_HOSTNAMES.has(url.hostname);
|
|
39
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocalhost)) {
|
|
40
|
+
throw new StorefrontInputError("Storefront allowed origins must use HTTPS (HTTP is permitted only for localhost).");
|
|
41
|
+
}
|
|
42
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
43
|
+
throw new StorefrontInputError("Storefront allowed origins cannot contain credentials, query, or hash.");
|
|
44
|
+
}
|
|
45
|
+
if (url.pathname !== "/" || trimmed.replace(/\/$/, "") !== url.origin) {
|
|
46
|
+
throw new StorefrontInputError("Storefront allowed entries must be origins, not URLs with paths.");
|
|
47
|
+
}
|
|
48
|
+
return url.origin;
|
|
49
|
+
});
|
|
50
|
+
return [...new Set(normalized)].sort();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether a request origin is authorized by a storefront's declared origins.
|
|
54
|
+
* Exact origins match verbatim; a `https://*.host` wildcard matches exactly one
|
|
55
|
+
* additional leading DNS label (`https://app.host`, not `https://a.b.host` and
|
|
56
|
+
* not the bare `https://host`).
|
|
57
|
+
*/
|
|
58
|
+
export function isStorefrontOriginAllowed(origin, allowedOrigins) {
|
|
59
|
+
let candidate;
|
|
60
|
+
try {
|
|
61
|
+
candidate = new URL(origin);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (candidate.origin !== origin)
|
|
67
|
+
return false;
|
|
68
|
+
for (const entry of allowedOrigins) {
|
|
69
|
+
if (entry.startsWith(WILDCARD_PREFIX)) {
|
|
70
|
+
if (candidate.protocol !== "https:")
|
|
71
|
+
continue;
|
|
72
|
+
const suffix = entry.slice(WILDCARD_PREFIX.length);
|
|
73
|
+
const host = candidate.hostname;
|
|
74
|
+
if (!host.endsWith(`.${suffix}`))
|
|
75
|
+
continue;
|
|
76
|
+
const label = host.slice(0, host.length - suffix.length - 1);
|
|
77
|
+
if (label.length > 0 && !label.includes("."))
|
|
78
|
+
return true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (entry === candidate.origin)
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/** Public storefront methods projection: coerces every flag to a strict boolean. */
|
|
87
|
+
export function normalizeStorefrontCustomerAuthMethods(methods) {
|
|
88
|
+
const normalized = {
|
|
89
|
+
emailCode: methods.emailCode === true,
|
|
90
|
+
emailPassword: methods.emailPassword === true,
|
|
91
|
+
google: methods.google === true,
|
|
92
|
+
facebook: methods.facebook === true,
|
|
93
|
+
apple: methods.apple === true,
|
|
94
|
+
};
|
|
95
|
+
if (!Object.values(normalized).some(Boolean)) {
|
|
96
|
+
throw new StorefrontInputError("Enable at least one customer authentication method.");
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
/** Social providers whose method flag is enabled on a storefront. */
|
|
101
|
+
export function enabledStorefrontSocialProviders(methods) {
|
|
102
|
+
return STOREFRONT_SOCIAL_PROVIDERS.filter((provider) => methods[provider] === true);
|
|
103
|
+
}
|
|
104
|
+
/** Normalize and validate the public B2C/B2B buyer-account capability policy. */
|
|
105
|
+
export function normalizeStorefrontCustomerAccountPolicy(policy) {
|
|
106
|
+
const suppliedKinds = [...policy.allowedKinds];
|
|
107
|
+
if (suppliedKinds.length === 0) {
|
|
108
|
+
throw new StorefrontInputError("Allow at least one storefront buyer account kind.");
|
|
109
|
+
}
|
|
110
|
+
if (new Set(suppliedKinds).size !== suppliedKinds.length) {
|
|
111
|
+
throw new StorefrontInputError("Storefront buyer account kinds must be unique.");
|
|
112
|
+
}
|
|
113
|
+
const allowedKinds = ["personal", "business"].filter((kind) => suppliedKinds.includes(kind));
|
|
114
|
+
if (allowedKinds.length !== suppliedKinds.length) {
|
|
115
|
+
throw new StorefrontInputError("Storefront buyer account kind is unsupported.");
|
|
116
|
+
}
|
|
117
|
+
if (!allowedKinds.includes("personal") && policy.personalSignup !== "disabled") {
|
|
118
|
+
throw new StorefrontInputError("Personal signup must be disabled when personal buyer accounts are not allowed.");
|
|
119
|
+
}
|
|
120
|
+
if (!allowedKinds.includes("business") && policy.businessOnboarding !== "disabled") {
|
|
121
|
+
throw new StorefrontInputError("Business onboarding must be disabled when business buyer accounts are not allowed.");
|
|
122
|
+
}
|
|
123
|
+
if (allowedKinds.includes("business") && policy.businessOnboarding === "disabled") {
|
|
124
|
+
throw new StorefrontInputError("Business onboarding must be enabled when business buyer accounts are allowed.");
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
allowedKinds,
|
|
128
|
+
personalSignup: policy.personalSignup,
|
|
129
|
+
businessOnboarding: policy.businessOnboarding,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=storefront-origins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storefront-origins.js","sourceRoot":"","sources":["../src/storefront-origins.ts"],"names":[],"mappings":"AAcA,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAU,CAAA;AAEnF,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAA;IACpC,CAAC;CACF;AAED,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAA;AACxE,MAAM,eAAe,GAAG,YAAY,CAAA;AAEpC;;;;;;GAMG;AACH,MAAM,UAAU,iCAAiC,CAAC,OAA0B;IAC1E,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QAC3C,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,CAAA;QAChC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,oBAAoB,CAAC,6CAA6C,CAAC,CAAA;QAC/E,CAAC;QAED,oEAAoE;QACpE,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;YAClD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC5E,MAAM,IAAI,oBAAoB,CAAC,uCAAuC,SAAS,EAAE,CAAC,CAAA;YACpF,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QACnC,CAAC;QAED,IAAI,GAAQ,CAAA;QACZ,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,oBAAoB,CAAC,sCAAsC,SAAS,EAAE,CAAC,CAAA;QACnF,CAAC;QACD,MAAM,WAAW,GAAG,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACzD,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,WAAW,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,oBAAoB,CAC5B,mFAAmF,CACpF,CAAA;QACH,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;YAC3D,MAAM,IAAI,oBAAoB,CAC5B,wEAAwE,CACzE,CAAA;QACH,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,KAAK,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC;YACtE,MAAM,IAAI,oBAAoB,CAC5B,kEAAkE,CACnE,CAAA;QACH,CAAC;QACD,OAAO,GAAG,CAAC,MAAM,CAAA;IACnB,CAAC,CAAC,CAAA;IACF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAAc,EACd,cAAiC;IAEjC,IAAI,SAAc,CAAA;IAClB,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAA;IAE7C,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YACtC,IAAI,SAAS,CAAC,QAAQ,KAAK,QAAQ;gBAAE,SAAQ;YAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;YAClD,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAA;YAC/B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC;gBAAE,SAAQ;YAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;YAC5D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAA;YACzD,SAAQ;QACV,CAAC;QACD,IAAI,KAAK,KAAK,SAAS,CAAC,MAAM;YAAE,OAAO,IAAI,CAAA;IAC7C,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,sCAAsC,CACpD,OAAsC;IAEtC,MAAM,UAAU,GAAkC;QAChD,SAAS,EAAE,OAAO,CAAC,SAAS,KAAK,IAAI;QACrC,aAAa,EAAE,OAAO,CAAC,aAAa,KAAK,IAAI;QAC7C,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI;QAC/B,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,IAAI;QACnC,KAAK,EAAE,OAAO,CAAC,KAAK,KAAK,IAAI;KAC9B,CAAA;IACD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,oBAAoB,CAAC,qDAAqD,CAAC,CAAA;IACvF,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,gCAAgC,CAC9C,OAAsC;IAEtC,OAAO,2BAA2B,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAA;AACrF,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,wCAAwC,CACtD,MAAuC;IAEvC,MAAM,aAAa,GAAG,CAAC,GAAG,MAAM,CAAC,YAAY,CAAC,CAAA;IAC9C,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,oBAAoB,CAAC,mDAAmD,CAAC,CAAA;IACrF,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,MAAM,EAAE,CAAC;QACzD,MAAM,IAAI,oBAAoB,CAAC,gDAAgD,CAAC,CAAA;IAClF,CAAC;IACD,MAAM,YAAY,GAAI,CAAC,UAAU,EAAE,UAAU,CAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACvE,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAC7B,CAAA;IACD,IAAI,YAAY,CAAC,MAAM,KAAK,aAAa,CAAC,MAAM,EAAE,CAAC;QACjD,MAAM,IAAI,oBAAoB,CAAC,+CAA+C,CAAC,CAAA;IACjF,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,cAAc,KAAK,UAAU,EAAE,CAAC;QAC/E,MAAM,IAAI,oBAAoB,CAC5B,gFAAgF,CACjF,CAAA;IACH,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,kBAAkB,KAAK,UAAU,EAAE,CAAC;QACnF,MAAM,IAAI,oBAAoB,CAC5B,oFAAoF,CACrF,CAAA;IACH,CAAC;IACD,IAAI,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,kBAAkB,KAAK,UAAU,EAAE,CAAC;QAClF,MAAM,IAAI,oBAAoB,CAC5B,+EAA+E,CAChF,CAAA;IACH,CAAC;IACD,OAAO;QACL,YAAY;QACZ,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,kBAAkB,EAAE,MAAM,CAAC,kBAAkB;KAC9C,CAAA;AACH,CAAC","sourcesContent":["/**\n * Storefront origin + policy normalization (provider-neutral, pure).\n *\n * Operators declare which browser origins may use a storefront's keys. There is\n * no ownership proof — declaration by the authenticated operator IS the\n * authorization, so a leaked publishable key is still bound to these origins by\n * CORS and Better Auth trusted origins.\n */\nimport type {\n StorefrontCustomerAccountPolicy,\n StorefrontCustomerAuthMethods,\n StorefrontCustomerAuthSocialProvider,\n} from \"@voyant-travel/db/schema/iam\"\n\nexport const STOREFRONT_SOCIAL_PROVIDERS = [\"google\", \"facebook\", \"apple\"] as const\n\nexport class StorefrontInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"StorefrontInputError\"\n }\n}\n\nconst LOCALHOST_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"])\nconst WILDCARD_PREFIX = \"https://*.\"\n\n/**\n * Normalize a set of operator-declared allowed origins. Each entry is either an\n * exact origin or a single-label `https://*.host` wildcard. HTTP is permitted\n * only for localhost (so a storefront can run against a sandbox from a\n * developer's machine); every other host must use HTTPS. Duplicates are removed\n * and the result is sorted for a stable persisted value.\n */\nexport function normalizeStorefrontAllowedOrigins(origins: readonly string[]): string[] {\n const normalized = origins.map((candidate) => {\n const trimmed = candidate.trim()\n if (!trimmed) {\n throw new StorefrontInputError(\"Storefront allowed origins cannot be empty.\")\n }\n\n // Wildcard form: https://*.example.com (exactly one leading label).\n if (trimmed.startsWith(WILDCARD_PREFIX)) {\n const host = trimmed.slice(WILDCARD_PREFIX.length)\n if (!host || host.includes(\"*\") || host.includes(\"/\") || host.includes(\":\")) {\n throw new StorefrontInputError(`Invalid storefront wildcard origin: ${candidate}`)\n }\n return trimmed.replace(/\\/$/, \"\")\n }\n\n let url: URL\n try {\n url = new URL(trimmed)\n } catch {\n throw new StorefrontInputError(`Invalid storefront allowed origin: ${candidate}`)\n }\n const isLocalhost = LOCALHOST_HOSTNAMES.has(url.hostname)\n if (url.protocol !== \"https:\" && !(url.protocol === \"http:\" && isLocalhost)) {\n throw new StorefrontInputError(\n \"Storefront allowed origins must use HTTPS (HTTP is permitted only for localhost).\",\n )\n }\n if (url.username || url.password || url.search || url.hash) {\n throw new StorefrontInputError(\n \"Storefront allowed origins cannot contain credentials, query, or hash.\",\n )\n }\n if (url.pathname !== \"/\" || trimmed.replace(/\\/$/, \"\") !== url.origin) {\n throw new StorefrontInputError(\n \"Storefront allowed entries must be origins, not URLs with paths.\",\n )\n }\n return url.origin\n })\n return [...new Set(normalized)].sort()\n}\n\n/**\n * Whether a request origin is authorized by a storefront's declared origins.\n * Exact origins match verbatim; a `https://*.host` wildcard matches exactly one\n * additional leading DNS label (`https://app.host`, not `https://a.b.host` and\n * not the bare `https://host`).\n */\nexport function isStorefrontOriginAllowed(\n origin: string,\n allowedOrigins: readonly string[],\n): boolean {\n let candidate: URL\n try {\n candidate = new URL(origin)\n } catch {\n return false\n }\n if (candidate.origin !== origin) return false\n\n for (const entry of allowedOrigins) {\n if (entry.startsWith(WILDCARD_PREFIX)) {\n if (candidate.protocol !== \"https:\") continue\n const suffix = entry.slice(WILDCARD_PREFIX.length)\n const host = candidate.hostname\n if (!host.endsWith(`.${suffix}`)) continue\n const label = host.slice(0, host.length - suffix.length - 1)\n if (label.length > 0 && !label.includes(\".\")) return true\n continue\n }\n if (entry === candidate.origin) return true\n }\n return false\n}\n\n/** Public storefront methods projection: coerces every flag to a strict boolean. */\nexport function normalizeStorefrontCustomerAuthMethods(\n methods: StorefrontCustomerAuthMethods,\n): StorefrontCustomerAuthMethods {\n const normalized: StorefrontCustomerAuthMethods = {\n emailCode: methods.emailCode === true,\n emailPassword: methods.emailPassword === true,\n google: methods.google === true,\n facebook: methods.facebook === true,\n apple: methods.apple === true,\n }\n if (!Object.values(normalized).some(Boolean)) {\n throw new StorefrontInputError(\"Enable at least one customer authentication method.\")\n }\n return normalized\n}\n\n/** Social providers whose method flag is enabled on a storefront. */\nexport function enabledStorefrontSocialProviders(\n methods: StorefrontCustomerAuthMethods,\n): StorefrontCustomerAuthSocialProvider[] {\n return STOREFRONT_SOCIAL_PROVIDERS.filter((provider) => methods[provider] === true)\n}\n\n/** Normalize and validate the public B2C/B2B buyer-account capability policy. */\nexport function normalizeStorefrontCustomerAccountPolicy(\n policy: StorefrontCustomerAccountPolicy,\n): StorefrontCustomerAccountPolicy {\n const suppliedKinds = [...policy.allowedKinds]\n if (suppliedKinds.length === 0) {\n throw new StorefrontInputError(\"Allow at least one storefront buyer account kind.\")\n }\n if (new Set(suppliedKinds).size !== suppliedKinds.length) {\n throw new StorefrontInputError(\"Storefront buyer account kinds must be unique.\")\n }\n const allowedKinds = ([\"personal\", \"business\"] as const).filter((kind) =>\n suppliedKinds.includes(kind),\n )\n if (allowedKinds.length !== suppliedKinds.length) {\n throw new StorefrontInputError(\"Storefront buyer account kind is unsupported.\")\n }\n if (!allowedKinds.includes(\"personal\") && policy.personalSignup !== \"disabled\") {\n throw new StorefrontInputError(\n \"Personal signup must be disabled when personal buyer accounts are not allowed.\",\n )\n }\n if (!allowedKinds.includes(\"business\") && policy.businessOnboarding !== \"disabled\") {\n throw new StorefrontInputError(\n \"Business onboarding must be disabled when business buyer accounts are not allowed.\",\n )\n }\n if (allowedKinds.includes(\"business\") && policy.businessOnboarding === \"disabled\") {\n throw new StorefrontInputError(\n \"Business onboarding must be enabled when business buyer accounts are allowed.\",\n )\n }\n return {\n allowedKinds,\n personalSignup: policy.personalSignup,\n businessOnboarding: policy.businessOnboarding,\n }\n}\n"]}
|