@tokenoftrust/storefront-runner 6.2.0 → 6.3.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/package.json +1 -1
- package/packages/cli/src/declared-config.mjs +41 -1
- package/packages/public-runtime/src/capture-rewrites.ts +1 -1
- package/packages/public-runtime/src/declared-tenant-config.mjs +33 -1
- package/packages/public-runtime/src/hosted-image-media-types.ts +2 -2
- package/packages/public-runtime/src/tenant.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
export const TOT_CONFIG_KEYS = [
|
|
21
21
|
"$schema", "tenant", "scope", "mappings", "hostPlatform", "sample", "seed",
|
|
22
|
-
"displayName", "siteType", "compliance", "capabilities", "features", "embeds", "seo", "currency", "timezone", "shipping",
|
|
22
|
+
"displayName", "siteType", "compliance", "capabilities", "features", "embeds", "seo", "currency", "timezone", "shipping", "catalog",
|
|
23
23
|
];
|
|
24
24
|
// Keys other tooling has written that the platform does NOT read, with the key it does.
|
|
25
25
|
const RENAMED_CONFIG_KEYS = { appDomain: "scope", tenantId: "tenant" };
|
|
@@ -232,6 +232,37 @@ export function seoDeclarationProblems(seo) {
|
|
|
232
232
|
return problems;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/** Every `catalog` key a tenant may declare. `serving` stays a platform field. */
|
|
236
|
+
export const CATALOG_KEYS = Object.freeze(["cdnSegment"]);
|
|
237
|
+
|
|
238
|
+
/** One DNS label. Anything else is not a catalog object-key segment. */
|
|
239
|
+
const CATALOG_SEGMENT = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* What is wrong with a declared `catalog` value, one sentence per problem; `[]` when it is valid.
|
|
243
|
+
* A tenant cannot set `catalog.serving` here: that field belongs to the platform record.
|
|
244
|
+
* @param {unknown} catalog
|
|
245
|
+
* @returns {string[]}
|
|
246
|
+
*/
|
|
247
|
+
export function catalogDeclarationProblems(catalog) {
|
|
248
|
+
if (!isObject(catalog)) return ["`catalog` must be an object"];
|
|
249
|
+
const source = /** @type {Record<string, unknown>} */ (catalog);
|
|
250
|
+
const problems = [];
|
|
251
|
+
for (const key of Object.keys(source)) {
|
|
252
|
+
if (!CATALOG_KEYS.includes(key)) problems.push(`\`catalog.${key}\` is not a catalog key (${CATALOG_KEYS.join(", ")})`);
|
|
253
|
+
}
|
|
254
|
+
if (source.cdnSegment !== undefined && (typeof source.cdnSegment !== "string" || !CATALOG_SEGMENT.test(source.cdnSegment))) {
|
|
255
|
+
problems.push("`catalog.cdnSegment` must be one lowercase DNS label");
|
|
256
|
+
}
|
|
257
|
+
return problems;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The owner-facing sentence for an invalid `catalog` declaration. */
|
|
261
|
+
function catalogProblemMessage(problems) {
|
|
262
|
+
return `\`catalog\` is not a valid catalog declaration (${problems.join("; ")}). ` +
|
|
263
|
+
"Owned catalog images stay refused until it is fixed.";
|
|
264
|
+
}
|
|
265
|
+
|
|
235
266
|
/** Every `shipping` key (mirrors packages/public-runtime/src/declared-tenant-config.mjs). */
|
|
236
267
|
export const SHIPPING_KEYS = Object.freeze(["tiers"]);
|
|
237
268
|
|
|
@@ -690,6 +721,8 @@ export function declarationFindings(current, previous) {
|
|
|
690
721
|
}
|
|
691
722
|
const shipping = current.shipping === undefined ? [] : shippingDeclarationProblems(current.shipping);
|
|
692
723
|
if (shipping.length > 0) out.push(`CONFIG: ${shippingProblemMessage(shipping)}`);
|
|
724
|
+
const catalog = current.catalog === undefined ? [] : catalogDeclarationProblems(current.catalog);
|
|
725
|
+
if (catalog.length > 0) out.push(`CONFIG: ${catalogProblemMessage(catalog)}`);
|
|
693
726
|
}
|
|
694
727
|
for (const { key, notice } of droppedMandatedNotices(previous, current)) {
|
|
695
728
|
out.push(
|
|
@@ -790,6 +823,13 @@ export function validateDeclaredConfig(config, file = ".tot/config.json") {
|
|
|
790
823
|
'declare e.g. `"shipping": { "tiers": [ { "rate": 5.99 }, { "atOrAbove": 100, "rate": 0, "label": "Free shipping" } ] }`, or remove `shipping` to charge none'));
|
|
791
824
|
}
|
|
792
825
|
}
|
|
826
|
+
if (config.catalog !== undefined) {
|
|
827
|
+
const problems = catalogDeclarationProblems(config.catalog);
|
|
828
|
+
if (problems.length > 0) {
|
|
829
|
+
out.push(mk(ERROR, "config-catalog", `${file} catalog`, catalogProblemMessage(problems),
|
|
830
|
+
'declare `"catalog": { "cdnSegment": "shop" }` as one lowercase DNS label, or remove `catalog`'));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
793
833
|
return out;
|
|
794
834
|
}
|
|
795
835
|
|
|
@@ -39,7 +39,7 @@ export type CaptureRewrite = {
|
|
|
39
39
|
/** A sealed capture-manifest entry, as `original-image-capture.ts` writes it. */
|
|
40
40
|
export interface CapturedManifestEntry {
|
|
41
41
|
/** Remote `locator` is query-stripped for display; remote entries are matched by `locatorDigest`. */
|
|
42
|
-
provenance: { kind: "local" | "remote" | "upload"; locator: string };
|
|
42
|
+
provenance: { kind: "local" | "remote" | "upload" | "owned-cdn"; locator: string };
|
|
43
43
|
address: string;
|
|
44
44
|
mediaType: string;
|
|
45
45
|
/** Remote entries: digest of the exact fetched locator, query included (`captureLocatorDigest`). */
|
|
@@ -29,6 +29,7 @@ export const DECLARED_TENANT_CONFIG_FIELDS = Object.freeze([
|
|
|
29
29
|
"currency",
|
|
30
30
|
"timezone",
|
|
31
31
|
"shipping",
|
|
32
|
+
"catalog",
|
|
32
33
|
]);
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -279,6 +280,32 @@ export function seoDeclarationProblems(seo) {
|
|
|
279
280
|
return problems;
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
/** Every `catalog` key a tenant may declare. `serving` stays a platform field. */
|
|
284
|
+
export const CATALOG_KEYS = Object.freeze(["cdnSegment"]);
|
|
285
|
+
|
|
286
|
+
/** One DNS label. Anything else is not a catalog object-key segment. */
|
|
287
|
+
const CATALOG_SEGMENT = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* What is wrong with a declared `catalog` value, one sentence per problem; `[]` when it is valid.
|
|
291
|
+
* A tenant cannot set `catalog.serving` here: that field belongs to the platform record.
|
|
292
|
+
*
|
|
293
|
+
* @param {unknown} catalog
|
|
294
|
+
* @returns {string[]}
|
|
295
|
+
*/
|
|
296
|
+
export function catalogDeclarationProblems(catalog) {
|
|
297
|
+
if (!isPlainObject(catalog)) return ["`catalog` must be an object"];
|
|
298
|
+
const source = /** @type {Record<string, unknown>} */ (catalog);
|
|
299
|
+
const problems = [];
|
|
300
|
+
for (const key of Object.keys(source)) {
|
|
301
|
+
if (!CATALOG_KEYS.includes(key)) problems.push(`\`catalog.${key}\` is not a catalog key (${CATALOG_KEYS.join(", ")})`);
|
|
302
|
+
}
|
|
303
|
+
if (source.cdnSegment !== undefined && (typeof source.cdnSegment !== "string" || !CATALOG_SEGMENT.test(source.cdnSegment))) {
|
|
304
|
+
problems.push("`catalog.cdnSegment` must be one lowercase DNS label");
|
|
305
|
+
}
|
|
306
|
+
return problems;
|
|
307
|
+
}
|
|
308
|
+
|
|
282
309
|
/** Every `shipping` key (ShippingPolicy, tenant.ts). */
|
|
283
310
|
export const SHIPPING_KEYS = Object.freeze(["tiers"]);
|
|
284
311
|
|
|
@@ -380,7 +407,7 @@ export function declaredTenantConfig(config) {
|
|
|
380
407
|
/** @type {Record<string, unknown>} */
|
|
381
408
|
const out = {};
|
|
382
409
|
if (!isPlainObject(config)) return out;
|
|
383
|
-
const { displayName, siteType, compliance, capabilities, features, embeds, seo, currency, timezone, shipping } = /** @type {Record<string, unknown>} */ (config);
|
|
410
|
+
const { displayName, siteType, compliance, capabilities, features, embeds, seo, currency, timezone, shipping, catalog } = /** @type {Record<string, unknown>} */ (config);
|
|
384
411
|
// The store's name as visitors read it: the `<title>` suffix, og:site_name,
|
|
385
412
|
// Organization JSON-LD, breadcrumbs, the default blog heading, the copyright line.
|
|
386
413
|
if (typeof displayName === "string" && displayName.trim()) out.displayName = displayName.trim();
|
|
@@ -400,6 +427,11 @@ export function declaredTenantConfig(config) {
|
|
|
400
427
|
if (shipping !== undefined && shippingDeclarationProblems(shipping).length === 0) {
|
|
401
428
|
out.shipping = shippingPolicyOf(/** @type {{ tiers: Array<Record<string, unknown>> }} */ (shipping));
|
|
402
429
|
}
|
|
430
|
+
// Only the declared segment. `catalog.serving` is the platform's and is refused by the validator.
|
|
431
|
+
if (catalog !== undefined && catalogDeclarationProblems(catalog).length === 0
|
|
432
|
+
&& isPlainObject(catalog) && typeof catalog.cdnSegment === "string") {
|
|
433
|
+
out.catalog = { cdnSegment: catalog.cdnSegment };
|
|
434
|
+
}
|
|
403
435
|
return out;
|
|
404
436
|
}
|
|
405
437
|
|
|
@@ -54,7 +54,7 @@ export const CAPTURE_ACCEPT_HEADER = UPLOADABLE_MEDIA_TYPES.join(",");
|
|
|
54
54
|
|
|
55
55
|
/** Media-type lookup only; manifest readers must use sealedOriginalEntryAllowed for receipt policy. */
|
|
56
56
|
export function sealedMediaTypeAllowed(kind: unknown, mediaType: unknown): boolean {
|
|
57
|
-
const types: readonly string[] = kind === "upload" || kind === "remote" || kind === "local" ? UPLOADABLE_MEDIA_TYPES : CAPTURABLE_RASTER_MEDIA_TYPES;
|
|
57
|
+
const types: readonly string[] = kind === "upload" || kind === "remote" || kind === "local" || kind === "owned-cdn" ? UPLOADABLE_MEDIA_TYPES : CAPTURABLE_RASTER_MEDIA_TYPES;
|
|
58
58
|
return typeof mediaType === "string" && types.includes(mediaType);
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -81,7 +81,7 @@ export function validSvgSanitizeReceipt(raw: unknown): SvgSanitizeReceipt | null
|
|
|
81
81
|
|
|
82
82
|
/** A sealed capture SVG requires its receipt; upload provenance resides in the upload registry. */
|
|
83
83
|
export function sealedOriginalEntryAllowed(kind: unknown, mediaType: unknown, sanitized: unknown): boolean {
|
|
84
|
-
if (kind !== "local" && kind !== "remote" && kind !== "upload") return false;
|
|
84
|
+
if (kind !== "local" && kind !== "remote" && kind !== "upload" && kind !== "owned-cdn") return false;
|
|
85
85
|
if (!sealedMediaTypeAllowed(kind, mediaType)) return false;
|
|
86
86
|
const needsReceipt = kind !== "upload" && mediaType === "image/svg+xml";
|
|
87
87
|
return needsReceipt ? validSvgSanitizeReceipt(sanitized) !== null : sanitized === undefined;
|
|
@@ -713,6 +713,11 @@ export interface TenantConfig {
|
|
|
713
713
|
*/
|
|
714
714
|
catalog?: {
|
|
715
715
|
serving?: "files" | "d1";
|
|
716
|
+
/**
|
|
717
|
+
* Object-key segment for this tenant's catalog images on the owned CDN
|
|
718
|
+
* (`catalog/<cdnSegment>/<file>`). Set only by the tenant's declaration.
|
|
719
|
+
*/
|
|
720
|
+
cdnSegment?: string;
|
|
716
721
|
};
|
|
717
722
|
/**
|
|
718
723
|
* Availability (hot) provider (ADR 0033). Availability is never read from catalog
|