@tileflow/capture 0.1.0-alpha.17 → 0.1.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,625 @@
1
+ // src/receipt.ts
2
+ import { createHash } from "crypto";
3
+ import { types as nodeUtilTypes } from "util";
4
+ import {
5
+ isTileflowWorldReleaseId,
6
+ serializeCanonicalJson,
7
+ tileflowCaptureIdSchema,
8
+ tileflowCaptureSceneLimits,
9
+ tileflowCaptureSceneNameSchema,
10
+ tileflowPortableIdSchema,
11
+ tileflowThemeNameSchema
12
+ } from "@tileflow/core";
13
+
14
+ // src/errors.ts
15
+ var TileflowCaptureError = class extends Error {
16
+ code;
17
+ details;
18
+ constructor(code, message, options) {
19
+ super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
20
+ this.name = "TileflowCaptureError";
21
+ this.code = code;
22
+ this.details = normalizeDetails(options?.details);
23
+ }
24
+ };
25
+ function captureErrorMessage(error, fallback) {
26
+ if (error instanceof TileflowCaptureError) {
27
+ return error.message;
28
+ }
29
+ return fallback;
30
+ }
31
+ function normalizeDetails(details) {
32
+ if (!details) return void 0;
33
+ const diagnostics = (details.diagnostics ?? []).map((diagnostic) => ({
34
+ message: boundText(diagnostic.message.replace(/[\r\n]+/g, " ").trim(), 300),
35
+ path: boundText(diagnostic.path.replaceAll("\\", "/"), 300)
36
+ })).sort((left, right) => {
37
+ const leftKey = `${left.path}\0${left.message}`;
38
+ const rightKey = `${right.path}\0${right.message}`;
39
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
40
+ }).slice(0, 32);
41
+ const resources = (details.resources ?? []).map(normalizeResource).filter((resource) => resource !== void 0).sort((left, right) => {
42
+ const leftKey = `${left.kind}\0${left.origin}\0${left.status ?? ""}\0${left.context ?? ""}`;
43
+ const rightKey = `${right.kind}\0${right.origin}\0${right.status ?? ""}\0${right.context ?? ""}`;
44
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
45
+ }).filter(
46
+ (resource, index, values) => index === 0 || JSON.stringify(resource) !== JSON.stringify(values[index - 1])
47
+ ).slice(0, 8);
48
+ const normalized = {
49
+ ...details.phase ? { phase: details.phase } : {},
50
+ ...diagnostics.length > 0 ? { diagnostics } : {},
51
+ ...resources.length > 0 ? { resources } : {}
52
+ };
53
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
54
+ }
55
+ function normalizeResource(resource) {
56
+ let origin;
57
+ try {
58
+ origin = new URL(resource.origin).origin;
59
+ } catch {
60
+ return void 0;
61
+ }
62
+ const status = resource.status !== void 0 && Number.isInteger(resource.status) && resource.status >= 100 && resource.status <= 599 ? resource.status : void 0;
63
+ const context = resource.context ? boundText(resource.context.replace(/[^A-Za-z0-9 _.,:@+-]/g, "").trim(), 128) : void 0;
64
+ return {
65
+ ...context ? { context } : {},
66
+ kind: resource.kind,
67
+ origin,
68
+ ...status === void 0 ? {} : { status }
69
+ };
70
+ }
71
+ function boundText(value, limit) {
72
+ return value.length <= limit ? value : `${value.slice(0, limit - 1)}\u2026`;
73
+ }
74
+
75
+ // src/receipt.ts
76
+ var tileflowCaptureReceiptSchemaVersion = 4;
77
+ var tileflowCaptureReceiptLimits = Object.freeze({ maximumBytes: 64 * 1024 });
78
+ var maximumReceiptBindingEntries = 128;
79
+ function createTileflowCaptureReceipt(input) {
80
+ return validateTileflowCaptureReceipt({
81
+ schemaVersion: tileflowCaptureReceiptSchemaVersion,
82
+ scene: {
83
+ name: input.scene,
84
+ map: input.map,
85
+ theme: input.theme,
86
+ target: input.target,
87
+ sha256: input.sceneSha256
88
+ },
89
+ style: { sha256: input.styleSha256 },
90
+ image: {
91
+ sha256: input.pngSha256,
92
+ cssWidth: input.width,
93
+ cssHeight: input.height,
94
+ physicalWidth: input.width * input.dpr,
95
+ physicalHeight: input.height * input.dpr,
96
+ dpr: input.dpr
97
+ },
98
+ renderer: input.renderer,
99
+ platform: { os: process.platform, architecture: process.arch },
100
+ data: normalizeCaptureDataInput(input.data),
101
+ verification: verificationForTarget(input.target),
102
+ networkDependent: input.networkDependent
103
+ });
104
+ }
105
+ function serializeTileflowCaptureReceipt(receipt) {
106
+ const serialized = `${serializeCanonicalJson(validateTileflowCaptureReceipt(receipt))}
107
+ `;
108
+ if (new TextEncoder().encode(serialized).byteLength > tileflowCaptureReceiptLimits.maximumBytes) {
109
+ throw invalidReceipt("The capture receipt exceeds the supported byte limit.");
110
+ }
111
+ return serialized;
112
+ }
113
+ function parseTileflowCaptureReceipt(source) {
114
+ const byteLength = typeof source === "string" ? new TextEncoder().encode(source).byteLength : source.byteLength;
115
+ if (byteLength > tileflowCaptureReceiptLimits.maximumBytes) {
116
+ throw invalidReceipt("The baseline receipt exceeds the supported byte limit.");
117
+ }
118
+ let value;
119
+ let text;
120
+ try {
121
+ text = typeof source === "string" ? source : new TextDecoder("utf-8", { fatal: true }).decode(source);
122
+ value = JSON.parse(text);
123
+ } catch (error) {
124
+ throw invalidReceipt("The baseline receipt is not valid UTF-8 JSON.", error);
125
+ }
126
+ const receipt = validateTileflowCaptureReceipt(value);
127
+ if (text.trim() !== serializeCanonicalJson(value)) {
128
+ throw invalidReceipt("The baseline receipt is not canonical JSON.");
129
+ }
130
+ return receipt;
131
+ }
132
+ function validateTileflowCaptureReceipt(value) {
133
+ assertPlainReceiptData(value);
134
+ const receipt = requireRecord(value, "receipt");
135
+ const schemaVersion = receipt.schemaVersion;
136
+ if (schemaVersion !== 2 && schemaVersion !== 3 && schemaVersion !== tileflowCaptureReceiptSchemaVersion) {
137
+ throw invalidReceipt("The baseline receipt schema version is unsupported.");
138
+ }
139
+ const commonKeys = [
140
+ "schemaVersion",
141
+ "scene",
142
+ "style",
143
+ "image",
144
+ "renderer",
145
+ "platform",
146
+ "data",
147
+ ...schemaVersion === 2 && receipt.verification === void 0 ? [] : ["verification"],
148
+ "networkDependent"
149
+ ];
150
+ requireExactKeys(receipt, commonKeys);
151
+ const scene = requireRecord(receipt.scene, "scene");
152
+ requireExactKeys(
153
+ scene,
154
+ schemaVersion === tileflowCaptureReceiptSchemaVersion ? ["name", "map", "theme", "target", "sha256"] : ["name", "map", "target", "sha256"]
155
+ );
156
+ const name = requireSceneName(scene.name);
157
+ const map = schemaVersion === tileflowCaptureReceiptSchemaVersion ? requirePortableIdentifier(scene.map, "scene.map") : requireIdentifier(scene.map, "scene.map");
158
+ const theme = schemaVersion === tileflowCaptureReceiptSchemaVersion ? requireConcreteTheme(scene.theme) : void 0;
159
+ if (scene.target !== "map" && scene.target !== "application") {
160
+ throw invalidReceipt("The baseline receipt has an invalid scene target.");
161
+ }
162
+ const verification = validateVerification(receipt.verification, scene.target);
163
+ const style = requireRecord(receipt.style, "style");
164
+ requireExactKeys(style, ["sha256"]);
165
+ const image = requireRecord(receipt.image, "image");
166
+ requireExactKeys(image, [
167
+ "sha256",
168
+ "cssWidth",
169
+ "cssHeight",
170
+ "physicalWidth",
171
+ "physicalHeight",
172
+ "dpr"
173
+ ]);
174
+ const cssWidth = requirePositiveFinite(image.cssWidth, "image.cssWidth");
175
+ const cssHeight = requirePositiveFinite(image.cssHeight, "image.cssHeight");
176
+ const physicalWidth = requirePositiveInteger(image.physicalWidth, "image.physicalWidth");
177
+ const physicalHeight = requirePositiveInteger(image.physicalHeight, "image.physicalHeight");
178
+ if (image.dpr !== 1 && image.dpr !== 2) {
179
+ throw invalidReceipt("The baseline receipt has an invalid image DPR.");
180
+ }
181
+ if (cssWidth * image.dpr !== physicalWidth || cssHeight * image.dpr !== physicalHeight) {
182
+ throw invalidReceipt("The baseline receipt image dimensions are inconsistent.");
183
+ }
184
+ if (physicalWidth > tileflowCaptureSceneLimits.viewport.maximum * 2 || physicalHeight > tileflowCaptureSceneLimits.viewport.maximum * 2 || physicalWidth * physicalHeight > tileflowCaptureSceneLimits.maximumPhysicalPixels) {
185
+ throw invalidReceipt("The baseline receipt image exceeds the supported pixel limit.");
186
+ }
187
+ const renderer = requireRecord(receipt.renderer, "renderer");
188
+ requireExactKeys(renderer, [
189
+ "tileflow",
190
+ "maplibre",
191
+ "playwright",
192
+ "chromiumRevision",
193
+ "chromiumVersion"
194
+ ]);
195
+ const platform = requireRecord(receipt.platform, "platform");
196
+ requireExactKeys(platform, ["os", "architecture"]);
197
+ if (typeof receipt.networkDependent !== "boolean") {
198
+ throw invalidReceipt("The baseline receipt has an invalid network dependency flag.");
199
+ }
200
+ const data = schemaVersion === 2 ? validateDataIdentityV2(receipt.data) : validateDataIdentityV3(receipt.data);
201
+ const normalized = {
202
+ schemaVersion,
203
+ scene: {
204
+ name,
205
+ map,
206
+ ...theme === void 0 ? {} : { theme },
207
+ target: scene.target,
208
+ sha256: requireHash(scene.sha256, "scene.sha256")
209
+ },
210
+ style: { sha256: requireHash(style.sha256, "style.sha256") },
211
+ image: {
212
+ sha256: requireHash(image.sha256, "image.sha256"),
213
+ cssWidth,
214
+ cssHeight,
215
+ physicalWidth,
216
+ physicalHeight,
217
+ dpr: image.dpr
218
+ },
219
+ renderer: {
220
+ tileflow: requireBoundedString(renderer.tileflow, "renderer.tileflow"),
221
+ maplibre: requireBoundedString(renderer.maplibre, "renderer.maplibre"),
222
+ playwright: requireBoundedString(renderer.playwright, "renderer.playwright"),
223
+ chromiumRevision: requireBoundedString(
224
+ renderer.chromiumRevision,
225
+ "renderer.chromiumRevision"
226
+ ),
227
+ chromiumVersion: requireBoundedString(renderer.chromiumVersion, "renderer.chromiumVersion")
228
+ },
229
+ platform: {
230
+ os: requireBoundedString(platform.os, "platform.os"),
231
+ architecture: requireBoundedString(platform.architecture, "platform.architecture")
232
+ },
233
+ data,
234
+ verification,
235
+ networkDependent: receipt.networkDependent
236
+ };
237
+ assertCanonicalReceiptByteLimit(normalized);
238
+ return normalized;
239
+ }
240
+ function validateDataIdentityV2(value) {
241
+ const data = requireRecord(value, "data");
242
+ if (data.url !== void 0 && data.source !== void 0) {
243
+ throw invalidReceipt(
244
+ "The baseline receipt data identity must not mix legacy and safe sources."
245
+ );
246
+ }
247
+ requireExactKeys(data, [
248
+ ...data.bindings === void 0 ? [] : ["bindings"],
249
+ ...data.capabilities === void 0 ? [] : ["capabilities"],
250
+ "kind",
251
+ ...data.generation === void 0 ? [] : ["generation"],
252
+ ...data.revision === void 0 ? [] : ["revision"],
253
+ "schema",
254
+ "schemaVersion",
255
+ ...data.semantics === void 0 ? [] : ["semantics"],
256
+ "sourceId",
257
+ ...data.source === void 0 ? [] : ["source"],
258
+ ...data.url === void 0 ? [] : ["url"]
259
+ ]);
260
+ if (data.kind !== "tileflow-world" && data.kind !== "vector-tiles") {
261
+ throw invalidReceipt("The baseline receipt has an invalid data.kind.");
262
+ }
263
+ if (data.schema !== "openmaptiles" || data.sourceId !== "tileflow") {
264
+ throw invalidReceipt("The baseline receipt has an unsupported data contract.");
265
+ }
266
+ if (data.kind === "tileflow-world") {
267
+ const hasGeneration = data.generation !== void 0;
268
+ const hasLegacyRevision = data.revision !== void 0;
269
+ if (hasGeneration === hasLegacyRevision) {
270
+ throw invalidReceipt(
271
+ "The baseline receipt must identify World by one generation or one legacy revision."
272
+ );
273
+ }
274
+ } else if (data.generation !== void 0) {
275
+ throw invalidReceipt("The baseline receipt has an invalid data.generation.");
276
+ }
277
+ const bindings = data.bindings === void 0 ? void 0 : validateDataBindings(data.bindings, "data.bindings");
278
+ const capabilities = data.capabilities === void 0 ? void 0 : validateDataCapabilities(data.capabilities, "data.capabilities");
279
+ const source = data.source === void 0 ? data.url === void 0 ? void 0 : fingerprintLegacyDataUrl(data.url) : validateDataSource(data.source, "data.source");
280
+ const semantics = data.semantics === void 0 ? void 0 : validateDataSemantics(data.semantics, "data.semantics");
281
+ return {
282
+ ...bindings ? { bindings } : {},
283
+ ...capabilities ? { capabilities } : {},
284
+ ...data.generation === void 0 ? {} : { generation: requireWorldGeneration(data.generation) },
285
+ kind: data.kind,
286
+ ...data.revision === void 0 ? {} : { revision: requireSourceRevision(data.revision) },
287
+ schema: data.schema,
288
+ schemaVersion: requirePositiveInteger(data.schemaVersion, "data.schemaVersion"),
289
+ ...semantics ? { semantics } : {},
290
+ sourceId: data.sourceId,
291
+ ...source ? { source } : {}
292
+ };
293
+ }
294
+ function validateDataIdentityV3(value) {
295
+ const data = requireRecord(value, "data");
296
+ if (data.kind === "tileflow-world") {
297
+ requireExactKeys(data, [
298
+ "archiveSha256",
299
+ "contractSha256",
300
+ "dataContractSha256",
301
+ "descriptorSha256",
302
+ "kind",
303
+ "product",
304
+ "releaseId",
305
+ "schema",
306
+ "schemaVersion",
307
+ ...data.semantics === void 0 ? [] : ["semantics"],
308
+ "sourceId"
309
+ ]);
310
+ if (data.product !== "world-v1" || data.schema !== "openmaptiles" || data.sourceId !== "tileflow") {
311
+ throw invalidReceipt("The baseline receipt has an unsupported World data contract.");
312
+ }
313
+ const semantics2 = data.semantics === void 0 ? void 0 : validateDataSemantics(data.semantics, "data.semantics");
314
+ return {
315
+ archiveSha256: requireHash(data.archiveSha256, "data.archiveSha256"),
316
+ contractSha256: requireHash(data.contractSha256, "data.contractSha256"),
317
+ dataContractSha256: requireHash(data.dataContractSha256, "data.dataContractSha256"),
318
+ descriptorSha256: requireHash(data.descriptorSha256, "data.descriptorSha256"),
319
+ kind: "tileflow-world",
320
+ product: "world-v1",
321
+ releaseId: requireWorldReleaseId(data.releaseId),
322
+ schema: "openmaptiles",
323
+ schemaVersion: requirePositiveInteger(data.schemaVersion, "data.schemaVersion"),
324
+ ...semantics2 ? { semantics: semantics2 } : {},
325
+ sourceId: "tileflow"
326
+ };
327
+ }
328
+ if (data.kind !== "vector-tiles") {
329
+ throw invalidReceipt("The baseline receipt has an invalid data.kind.");
330
+ }
331
+ requireExactKeys(data, [
332
+ ...data.bindings === void 0 ? [] : ["bindings"],
333
+ ...data.capabilities === void 0 ? [] : ["capabilities"],
334
+ "kind",
335
+ ...data.revision === void 0 ? [] : ["revision"],
336
+ "schema",
337
+ "schemaVersion",
338
+ ...data.semantics === void 0 ? [] : ["semantics"],
339
+ "sourceId",
340
+ ...data.source === void 0 ? [] : ["source"]
341
+ ]);
342
+ if (data.schema !== "openmaptiles" || data.sourceId !== "tileflow") {
343
+ throw invalidReceipt("The baseline receipt has an unsupported data contract.");
344
+ }
345
+ const bindings = data.bindings === void 0 ? void 0 : validateDataBindings(data.bindings, "data.bindings");
346
+ const capabilities = data.capabilities === void 0 ? void 0 : validateDataCapabilities(data.capabilities, "data.capabilities");
347
+ const semantics = data.semantics === void 0 ? void 0 : validateDataSemantics(data.semantics, "data.semantics");
348
+ return {
349
+ ...bindings ? { bindings } : {},
350
+ ...capabilities ? { capabilities } : {},
351
+ kind: "vector-tiles",
352
+ ...data.revision === void 0 ? {} : { revision: requireSourceRevision(data.revision) },
353
+ schema: "openmaptiles",
354
+ schemaVersion: requirePositiveInteger(data.schemaVersion, "data.schemaVersion"),
355
+ ...semantics ? { semantics } : {},
356
+ sourceId: "tileflow",
357
+ ...data.source === void 0 ? {} : { source: validateDataSource(data.source, "data.source") }
358
+ };
359
+ }
360
+ function normalizeCaptureDataInput(input) {
361
+ if (input.kind === "tileflow-world") return input;
362
+ const { url, ...identity } = input;
363
+ return {
364
+ ...identity,
365
+ ...url === void 0 ? {} : { source: fingerprintLegacyDataUrl(url) }
366
+ };
367
+ }
368
+ function validateDataSemantics(value, field) {
369
+ const semantics = requireRecord(value, field);
370
+ requireExactKeys(semantics, ["parkLayer"]);
371
+ if (semantics.parkLayer !== "mixed" && semantics.parkLayer !== "protected-only") {
372
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.parkLayer.`);
373
+ }
374
+ return { parkLayer: semantics.parkLayer };
375
+ }
376
+ function validateDataBindings(value, field) {
377
+ const bindings = requireRecord(value, field);
378
+ requireExactKeys(bindings, ["fields", "layers"]);
379
+ return {
380
+ fields: validateStringBindings(bindings.fields, `${field}.fields`),
381
+ layers: validateStringBindings(bindings.layers, `${field}.layers`)
382
+ };
383
+ }
384
+ function validateStringBindings(value, field) {
385
+ const bindings = requireRecord(value, field);
386
+ const entries = Object.entries(bindings);
387
+ if (entries.length === 0 || entries.length > maximumReceiptBindingEntries) {
388
+ throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
389
+ }
390
+ return Object.fromEntries(
391
+ entries.map(([name, binding]) => [
392
+ requireBindingKey(name, `${field} key`),
393
+ requireBindingValue(binding, `${field}.${name}`)
394
+ ]).sort(([left], [right]) => compareCodeUnits(left, right))
395
+ );
396
+ }
397
+ function requireBindingKey(value, field) {
398
+ if (!/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(value)) {
399
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
400
+ }
401
+ return value;
402
+ }
403
+ function requireBindingValue(value, field) {
404
+ if (typeof value !== "string" || value.length > 256 || value.trim().length === 0) {
405
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
406
+ }
407
+ return value;
408
+ }
409
+ function validateDataCapabilities(value, field) {
410
+ const capabilities = requireRecord(value, field);
411
+ requireExactKeys(capabilities, [
412
+ "businessCorridor",
413
+ ...capabilities.bathymetry === void 0 ? [] : ["bathymetry"],
414
+ "globalLandcover",
415
+ "tree"
416
+ ]);
417
+ if (typeof capabilities.businessCorridor !== "boolean" || capabilities.bathymetry !== void 0 && typeof capabilities.bathymetry !== "boolean" || typeof capabilities.globalLandcover !== "boolean" || typeof capabilities.tree !== "boolean") {
418
+ throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
419
+ }
420
+ return {
421
+ businessCorridor: capabilities.businessCorridor,
422
+ ...capabilities.bathymetry === void 0 ? {} : { bathymetry: capabilities.bathymetry },
423
+ globalLandcover: capabilities.globalLandcover,
424
+ tree: capabilities.tree
425
+ };
426
+ }
427
+ function validateDataSource(value, field) {
428
+ const source = requireRecord(value, field);
429
+ requireExactKeys(source, ["kind", "sha256"]);
430
+ if (source.kind !== "loopback" && source.kind !== "opaque" && source.kind !== "remote" && source.kind !== "root-relative") {
431
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.kind.`);
432
+ }
433
+ return { kind: source.kind, sha256: requireHash(source.sha256, `${field}.sha256`) };
434
+ }
435
+ function validateVerification(value, target) {
436
+ const expected = verificationForTarget(target);
437
+ if (value === void 0) return expected;
438
+ const verification = requireRecord(value, "verification");
439
+ requireExactKeys(verification, ["data", "style"]);
440
+ if (verification.data !== expected.data || verification.style !== expected.style) {
441
+ throw invalidReceipt(
442
+ `The baseline receipt verification does not match its ${target} capture target.`
443
+ );
444
+ }
445
+ return expected;
446
+ }
447
+ function verificationForTarget(target) {
448
+ const state = target === "map" ? "rendered" : "expected-unverified";
449
+ return { data: state, style: state };
450
+ }
451
+ function assertCanonicalReceiptByteLimit(receipt) {
452
+ const serialized = `${serializeCanonicalJson(receipt)}
453
+ `;
454
+ if (new TextEncoder().encode(serialized).byteLength > tileflowCaptureReceiptLimits.maximumBytes) {
455
+ throw invalidReceipt("The capture receipt exceeds the supported byte limit.");
456
+ }
457
+ }
458
+ function assertPlainReceiptData(value, path = "receipt", state = {
459
+ ancestors: /* @__PURE__ */ new WeakSet(),
460
+ nodes: 0
461
+ }, depth = 0) {
462
+ if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
463
+ return;
464
+ }
465
+ if (typeof value !== "object" || Array.isArray(value)) {
466
+ throw invalidReceipt(`The baseline receipt has an executable or non-plain ${path} value.`);
467
+ }
468
+ if (nodeUtilTypes.isProxy(value)) {
469
+ throw invalidReceipt(`The baseline receipt has an executable proxy at ${path}.`);
470
+ }
471
+ if (depth > 32 || ++state.nodes > 4096) {
472
+ throw invalidReceipt("The baseline receipt exceeds the supported structural limit.");
473
+ }
474
+ const prototype = Object.getPrototypeOf(value);
475
+ if (prototype !== Object.prototype && prototype !== null) {
476
+ throw invalidReceipt(`The baseline receipt has a non-plain ${path} object.`);
477
+ }
478
+ if (state.ancestors.has(value)) {
479
+ throw invalidReceipt("The baseline receipt must not contain cyclic data.");
480
+ }
481
+ state.ancestors.add(value);
482
+ try {
483
+ const descriptors = Object.getOwnPropertyDescriptors(value);
484
+ const keys = Reflect.ownKeys(descriptors);
485
+ state.nodes += keys.length;
486
+ if (state.nodes > 4096) {
487
+ throw invalidReceipt("The baseline receipt exceeds the supported structural limit.");
488
+ }
489
+ for (const key of keys) {
490
+ if (typeof key !== "string") {
491
+ throw invalidReceipt("The baseline receipt contains an unsupported symbol field.");
492
+ }
493
+ const descriptor = descriptors[key];
494
+ if (!("value" in descriptor) || !descriptor.enumerable) {
495
+ throw invalidReceipt("The baseline receipt contains an accessor or non-enumerable field.");
496
+ }
497
+ assertPlainReceiptData(descriptor.value, `${path}.${key}`, state, depth + 1);
498
+ }
499
+ } finally {
500
+ state.ancestors.delete(value);
501
+ }
502
+ }
503
+ function requireRecord(value, field) {
504
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
505
+ throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
506
+ }
507
+ return value;
508
+ }
509
+ function requireExactKeys(value, expected) {
510
+ const actual = Object.keys(value).sort();
511
+ const wanted = [...expected].sort();
512
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
513
+ throw invalidReceipt("The baseline receipt contains missing or unsupported fields.");
514
+ }
515
+ }
516
+ function requireIdentifier(value, field) {
517
+ const result = tileflowCaptureIdSchema.safeParse(value);
518
+ if (!result.success) throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
519
+ return result.data;
520
+ }
521
+ function requirePortableIdentifier(value, field) {
522
+ const result = tileflowPortableIdSchema.safeParse(value);
523
+ if (!result.success) throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
524
+ return result.data;
525
+ }
526
+ function requireConcreteTheme(value) {
527
+ const result = tileflowThemeNameSchema.safeParse(value);
528
+ if (!result.success) throw invalidReceipt("The baseline receipt has an invalid scene.theme.");
529
+ return result.data;
530
+ }
531
+ function compareCodeUnits(left, right) {
532
+ return left < right ? -1 : left > right ? 1 : 0;
533
+ }
534
+ function requireSceneName(value) {
535
+ const result = tileflowCaptureSceneNameSchema.safeParse(value);
536
+ if (!result.success) throw invalidReceipt("The baseline receipt has an invalid scene.name.");
537
+ return result.data;
538
+ }
539
+ function requireHash(value, field) {
540
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
541
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
542
+ }
543
+ return value;
544
+ }
545
+ function requirePositiveFinite(value, field) {
546
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
547
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
548
+ }
549
+ return value;
550
+ }
551
+ function requirePositiveInteger(value, field) {
552
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
553
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
554
+ }
555
+ return value;
556
+ }
557
+ function requireBoundedString(value, field) {
558
+ if (typeof value !== "string" || value.length < 1 || value.length > 64 || !isSafeRuntimeIdentity(value)) {
559
+ throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
560
+ }
561
+ return value;
562
+ }
563
+ function requireSourceRevision(value) {
564
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) {
565
+ throw invalidReceipt("The baseline receipt has an invalid data.revision.");
566
+ }
567
+ return value;
568
+ }
569
+ function fingerprintLegacyDataUrl(value) {
570
+ if (typeof value !== "string" || value.length < 1 || value.length > 2048) {
571
+ throw invalidReceipt("The baseline receipt has an invalid data.url.");
572
+ }
573
+ if (value.startsWith("/") && !value.startsWith("//")) {
574
+ const url = new URL(value, "https://tileflow.invalid");
575
+ return fingerprintDataSource("root-relative", `root-relative:${url.pathname}`);
576
+ }
577
+ try {
578
+ const url = new URL(value);
579
+ if (!url.username && !url.password && url.protocol !== "file:") {
580
+ if (isLoopbackHostname(url.hostname)) {
581
+ return fingerprintDataSource("loopback", `loopback:${url.pathname}`);
582
+ }
583
+ const kind = url.protocol === "http:" || url.protocol === "https:" ? "remote" : "opaque";
584
+ return fingerprintDataSource(kind, `${kind}:${url.protocol}//${url.host}${url.pathname}`);
585
+ }
586
+ } catch {
587
+ }
588
+ throw invalidReceipt("The baseline receipt has an invalid data.url.");
589
+ }
590
+ function fingerprintDataSource(kind, identity) {
591
+ return { kind, sha256: createHash("sha256").update(identity).digest("hex") };
592
+ }
593
+ function isLoopbackHostname(value) {
594
+ const hostname = value.toLowerCase().replace(/^\[|\]$/g, "");
595
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
596
+ }
597
+ function requireWorldGeneration(value) {
598
+ if (value !== "v1") {
599
+ throw invalidReceipt("The baseline receipt has an invalid data.generation.");
600
+ }
601
+ return value;
602
+ }
603
+ function requireWorldReleaseId(value) {
604
+ if (!isTileflowWorldReleaseId(value)) {
605
+ throw invalidReceipt("The baseline receipt has an invalid data.releaseId.");
606
+ }
607
+ return value;
608
+ }
609
+ function isSafeRuntimeIdentity(value) {
610
+ return /^[A-Za-z0-9][A-Za-z0-9._:+-]*$/.test(value);
611
+ }
612
+ function invalidReceipt(message, cause) {
613
+ return new TileflowCaptureError("BASELINE_INVALID", message, { cause });
614
+ }
615
+
616
+ export {
617
+ TileflowCaptureError,
618
+ captureErrorMessage,
619
+ tileflowCaptureReceiptSchemaVersion,
620
+ tileflowCaptureReceiptLimits,
621
+ createTileflowCaptureReceipt,
622
+ serializeTileflowCaptureReceipt,
623
+ parseTileflowCaptureReceipt,
624
+ validateTileflowCaptureReceipt
625
+ };