@tokenoftrust/storefront-runner 3.3.4 → 3.4.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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",
|
|
@@ -1,13 +1,92 @@
|
|
|
1
|
+
// Renders packages/public-runtime/schemas/managed-analytics/v1/*.schema.json from
|
|
2
|
+
// MANAGED_ANALYTICS_API_V1_JSON_SCHEMAS. The output is a declared derived mirror
|
|
3
|
+
// (.tot/derived-mirror-steps.json), re-derived on the umbrella tip.
|
|
4
|
+
//
|
|
5
|
+
// (no flag) write every schema file
|
|
6
|
+
// --check refuse a stale, missing or orphaned file. Inside a fold gate (TOT_FOLD_GATE=1) a
|
|
7
|
+
// stale or missing file passes with a STALE line, since the tip re-derives it; an
|
|
8
|
+
// orphan still refuses, because no derivation deletes a file.
|
|
9
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
1
10
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { MANAGED_ANALYTICS_API_V1_JSON_SCHEMAS } from "../src/integration/managed-analytics-api-v1.schemas.ts";
|
|
11
|
+
import { dirname, relative, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
13
|
|
|
6
14
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const
|
|
8
|
-
|
|
15
|
+
const repoRoot = resolve(here, "../../..");
|
|
16
|
+
export const OUTPUT_DIR = relative(repoRoot, resolve(here, "../schemas/managed-analytics/v1"));
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Every schema file, as repo-relative path -> text.
|
|
20
|
+
* @returns {Promise<Map<string, string>>}
|
|
21
|
+
*/
|
|
22
|
+
export async function renderManagedAnalyticsSchemas() {
|
|
23
|
+
const { MANAGED_ANALYTICS_API_V1_JSON_SCHEMAS } = await import("../src/integration/managed-analytics-api-v1.schemas.ts");
|
|
24
|
+
const files = new Map();
|
|
25
|
+
for (const schema of Object.values(MANAGED_ANALYTICS_API_V1_JSON_SCHEMAS)) {
|
|
26
|
+
const name = new URL(schema.$id).pathname.split("/").at(-1);
|
|
27
|
+
files.set(`${OUTPUT_DIR}/${name}`, `${JSON.stringify(schema, null, 2)}\n`);
|
|
28
|
+
}
|
|
29
|
+
return files;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The committed files under `root` against a fresh render: `stale` differ or are missing, `orphans`
|
|
34
|
+
* sit in the output directory with no schema rendering them.
|
|
35
|
+
* @param {string} [root]
|
|
36
|
+
*/
|
|
37
|
+
export async function findSchemaDrift(root = repoRoot) {
|
|
38
|
+
const files = await renderManagedAnalyticsSchemas();
|
|
39
|
+
/** @param {string} path */
|
|
40
|
+
const committed = (path) => {
|
|
41
|
+
try {
|
|
42
|
+
return readFileSync(resolve(root, path), "utf8");
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
let present = [];
|
|
48
|
+
try {
|
|
49
|
+
present = readdirSync(resolve(root, OUTPUT_DIR));
|
|
50
|
+
} catch {
|
|
51
|
+
present = [];
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
files,
|
|
55
|
+
stale: [...files].filter(([path, text]) => committed(path) !== text).map(([path]) => path),
|
|
56
|
+
orphans: present.map((name) => `${OUTPUT_DIR}/${name}`).filter((path) => !files.has(path)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The `--check` verdict. A fold gate tolerates stale files, which the tip re-derives; an orphan is
|
|
62
|
+
* always red, since no derivation deletes a file.
|
|
63
|
+
* @param {{ count: number, stale: string[], orphans: string[], foldGate: boolean }} drift
|
|
64
|
+
* @returns {{ ok: boolean, lines: string[] }}
|
|
65
|
+
*/
|
|
66
|
+
export function schemaCheckVerdict({ count, stale, orphans, foldGate }) {
|
|
67
|
+
const lines = [
|
|
68
|
+
...orphans.map((path) => `managed-analytics-schemas-orphan: ${path} is not rendered by any schema; delete it`),
|
|
69
|
+
...stale.map((path) => `${foldGate ? "STALE" : "managed-analytics-schemas-stale"}: ${path}`),
|
|
70
|
+
];
|
|
71
|
+
const ok = orphans.length === 0 && (stale.length === 0 || foldGate);
|
|
72
|
+
if (!ok && stale.length > 0) lines.push(`re-derive: node ${relative(repoRoot, fileURLToPath(import.meta.url))}`);
|
|
73
|
+
if (ok) lines.push(`${count} managed analytics v1 schemas are ${stale.length > 0 ? "stale; the tip re-derives them" : "current"}`);
|
|
74
|
+
return { ok, lines };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function check() {
|
|
78
|
+
const { files, stale, orphans } = await findSchemaDrift();
|
|
79
|
+
const verdict = schemaCheckVerdict({ count: files.size, stale, orphans, foldGate: process.env.TOT_FOLD_GATE === "1" });
|
|
80
|
+
for (const line of verdict.lines) (verdict.ok ? console.log : console.error)(line);
|
|
81
|
+
if (!verdict.ok) process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function write() {
|
|
85
|
+
const files = await renderManagedAnalyticsSchemas();
|
|
86
|
+
await mkdir(resolve(repoRoot, OUTPUT_DIR), { recursive: true });
|
|
87
|
+
for (const [path, text] of files) await writeFile(resolve(repoRoot, path), text, "utf8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
91
|
+
await (process.argv.includes("--check") ? check() : write());
|
|
13
92
|
}
|
|
@@ -34,10 +34,17 @@ export interface AssetProvenance {
|
|
|
34
34
|
at: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* How a derived pool object was made from its `parent` entry. A rendition's identity is its parent's
|
|
39
|
+
* address plus this recipe; the bytes it produces get their own address, so a changed recipe or
|
|
40
|
+
* encoder mints new renditions and never rewrites a URL already served.
|
|
41
|
+
*/
|
|
42
|
+
export interface VariantRecipe { kind: "variant"; parent: string; width: number; format: string; quality: number; encoder: string }
|
|
43
|
+
|
|
37
44
|
export type AssetRecipe =
|
|
38
45
|
| { kind: "original" }
|
|
39
46
|
| { kind: "sanitized"; sanitizer: string; source: string }
|
|
40
|
-
|
|
|
47
|
+
| VariantRecipe;
|
|
41
48
|
|
|
42
49
|
export type RetentionClass = "release" | "durable" | "external-use" | "removal-requested";
|
|
43
50
|
export interface RetentionTransition {
|
|
@@ -55,6 +62,9 @@ export interface AssetRecord {
|
|
|
55
62
|
address: string;
|
|
56
63
|
mediaType?: string;
|
|
57
64
|
size?: number;
|
|
65
|
+
/** Intrinsic pixel dimensions of an image, as decoded (orientation applied). */
|
|
66
|
+
width?: number;
|
|
67
|
+
height?: number;
|
|
58
68
|
provenance?: AssetProvenance;
|
|
59
69
|
/** Tenants authorized to reference this address: each one proved it holds the bytes. */
|
|
60
70
|
tenants?: string[];
|
|
@@ -79,6 +89,8 @@ export interface AssetWrite {
|
|
|
79
89
|
entry: string;
|
|
80
90
|
mediaType?: string;
|
|
81
91
|
size?: number;
|
|
92
|
+
width?: number;
|
|
93
|
+
height?: number;
|
|
82
94
|
provenance?: AssetProvenance;
|
|
83
95
|
tenant?: string;
|
|
84
96
|
owner?: string;
|
|
@@ -103,6 +115,11 @@ const MAX_TENANTS = 256;
|
|
|
103
115
|
const MAX_RENDITIONS = 64;
|
|
104
116
|
const MAX_TRANSITIONS = 32;
|
|
105
117
|
const MAX_LOCATOR = 2048;
|
|
118
|
+
const MAX_DIMENSION = 65535;
|
|
119
|
+
|
|
120
|
+
function validDimension(value: unknown): value is number {
|
|
121
|
+
return Number.isSafeInteger(value) && (value as number) > 0 && (value as number) <= MAX_DIMENSION;
|
|
122
|
+
}
|
|
106
123
|
|
|
107
124
|
/**
|
|
108
125
|
* The retention transitions an owner or a designation may make. A record with no retention facet is
|
|
@@ -176,8 +193,10 @@ function normalizeRecipe(raw: unknown): AssetRecipe | null {
|
|
|
176
193
|
if (raw.kind === "variant" && typeof raw.parent === "string" && entryAddress(raw.parent) !== null &&
|
|
177
194
|
Number.isSafeInteger(raw.width) && (raw.width as number) > 0 && (raw.width as number) <= 16384 &&
|
|
178
195
|
typeof raw.format === "string" && TOKEN_RE.test(raw.format) &&
|
|
196
|
+
Number.isSafeInteger(raw.quality) && (raw.quality as number) >= 1 && (raw.quality as number) <= 100 &&
|
|
179
197
|
typeof raw.encoder === "string" && TOKEN_RE.test(raw.encoder)) {
|
|
180
|
-
return { kind: "variant", parent: raw.parent, width: raw.width as number, format: raw.format,
|
|
198
|
+
return { kind: "variant", parent: raw.parent, width: raw.width as number, format: raw.format,
|
|
199
|
+
quality: raw.quality as number, encoder: raw.encoder };
|
|
181
200
|
}
|
|
182
201
|
return null;
|
|
183
202
|
}
|
|
@@ -227,6 +246,11 @@ export function normalizeAssetRecord(entry: string, raw: unknown): AssetRecord |
|
|
|
227
246
|
const record: AssetRecord = { address };
|
|
228
247
|
if (typeof raw.mediaType === "string" && MEDIA_TYPE_RE.test(raw.mediaType)) record.mediaType = raw.mediaType;
|
|
229
248
|
if (Number.isSafeInteger(raw.size) && (raw.size as number) > 0) record.size = raw.size as number;
|
|
249
|
+
// Dimensions are one fact: both or neither.
|
|
250
|
+
if (validDimension(raw.width) && validDimension(raw.height)) {
|
|
251
|
+
record.width = raw.width;
|
|
252
|
+
record.height = raw.height;
|
|
253
|
+
}
|
|
230
254
|
const provenance = normalizeProvenance(raw.provenance);
|
|
231
255
|
if (provenance) record.provenance = provenance;
|
|
232
256
|
if (Array.isArray(raw.tenants)) {
|
|
@@ -294,6 +318,10 @@ export function applyAssetWrite(existing: AssetRecord | undefined, write: AssetW
|
|
|
294
318
|
const record: AssetRecord = existing ? structuredCloneRecord(existing) : { address };
|
|
295
319
|
if (record.mediaType === undefined && write.mediaType !== undefined && MEDIA_TYPE_RE.test(write.mediaType)) record.mediaType = write.mediaType;
|
|
296
320
|
if (record.size === undefined && Number.isSafeInteger(write.size) && (write.size as number) > 0) record.size = write.size as number;
|
|
321
|
+
if (record.width === undefined && validDimension(write.width) && validDimension(write.height)) {
|
|
322
|
+
record.width = write.width;
|
|
323
|
+
record.height = write.height;
|
|
324
|
+
}
|
|
297
325
|
if (record.provenance === undefined && write.provenance) {
|
|
298
326
|
const provenance = normalizeProvenance(write.provenance);
|
|
299
327
|
if (provenance) record.provenance = provenance;
|
|
@@ -386,6 +414,34 @@ export function authorizeAssetReference(
|
|
|
386
414
|
};
|
|
387
415
|
}
|
|
388
416
|
|
|
417
|
+
export interface RegisteredRendition { entry: string; size: number }
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The registered rendition of `recipe.parent` made by exactly `recipe`, or null. It counts only when
|
|
421
|
+
* the parent lists it among its renditions, its record's recipe has the same width, format, quality
|
|
422
|
+
* and encoder, it is registered to `tenantId`, and the store holds a byte verification of it (so it
|
|
423
|
+
* is in the pool). A recipe or encoder that differs in any field finds nothing: it mints its own
|
|
424
|
+
* renditions. The record's own `parent` is not compared: two sources with the same pixels share
|
|
425
|
+
* rendition bytes, the record keeps its first writer's parent, and each parent's link is the proof
|
|
426
|
+
* that a build made it from that parent.
|
|
427
|
+
*/
|
|
428
|
+
export function registeredRendition(
|
|
429
|
+
registry: AssetRegistry | null | undefined, tenantId: string, recipe: VariantRecipe,
|
|
430
|
+
): RegisteredRendition | null {
|
|
431
|
+
const wanted = normalizeRecipe(recipe);
|
|
432
|
+
if (!registry || !wanted || wanted.kind !== "variant") return null;
|
|
433
|
+
for (const entry of registry.assets[wanted.parent]?.renditions ?? []) {
|
|
434
|
+
const record = registry.assets[entry];
|
|
435
|
+
const made = record?.recipe;
|
|
436
|
+
if (!record?.verified || !record.tenants?.includes(tenantId) || made?.kind !== "variant") continue;
|
|
437
|
+
if (made.width === wanted.width && made.format === wanted.format &&
|
|
438
|
+
made.quality === wanted.quality && made.encoder === wanted.encoder) {
|
|
439
|
+
return { entry, size: record.verified.size };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
389
445
|
/**
|
|
390
446
|
* The tenants a record lists other than `tenantId`, without naming them: true when the address is
|
|
391
447
|
* registered and this tenant is not among its tenants. Capture uses it to refuse a reference to
|