@tileflow/capture 0.1.0-alpha.28 → 0.1.0-alpha.30

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/README.md CHANGED
@@ -106,6 +106,66 @@ The equivalent CLI command is `tileflow setup capture`. These settings control b
106
106
  not the network access required by a map's remote resources. Tileflow never deletes the shared
107
107
  per-user browser cache.
108
108
 
109
+ ## Render a timed shot
110
+
111
+ A [capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
112
+ is a committed camera move with keyframes and an optional theme blend. This module assumes a
113
+ `dusk-flyover` shot in `tileflow.config.ts` and writes its WebM video and receipt:
114
+
115
+ <!-- docs:check -->
116
+
117
+ ```ts
118
+ import {mkdir, writeFile} from 'node:fs/promises';
119
+ import {captureTileflowShot, serializeTileflowShotReceipt} from '@tileflow/capture';
120
+
121
+ const shot = await captureTileflowShot({shot: 'dusk-flyover', allowBrowserInstall: true});
122
+ if (!shot.video) throw new Error('The shot produced no video.');
123
+
124
+ await mkdir('.tileflow/captures/shots', {recursive: true});
125
+ await writeFile('.tileflow/captures/shots/dusk-flyover.webm', shot.video);
126
+ await writeFile(
127
+ '.tileflow/captures/shots/dusk-flyover.receipt.json',
128
+ serializeTileflowShotReceipt(shot.receipt),
129
+ );
130
+ console.log(shot.frameCount, shot.codec, shot.framesSha256);
131
+ ```
132
+
133
+ Every frame moves the camera with MapLibre `jumpTo`, moves the blend position, and waits for
134
+ MapLibre `idle` and two animation frames before its screenshot, exactly as a scene does. A slow
135
+ machine or network makes the render take longer; the video keeps its frame rate and shows every
136
+ frame. `timeoutMs` bounds page setup and each frame separately.
137
+
138
+ The pinned browser encodes the captured PNGs with WebCodecs: VP9, or VP8 where VP9 is unavailable.
139
+ A browser that supports neither fails before the first frame with `VIDEO_ENCODER_UNAVAILABLE`.
140
+ Use `format: 'frames'` to receive the PNG frames through `onFrame` instead of a video; frames are
141
+ delivered in order and are not kept in memory:
142
+
143
+ <!-- docs:check -->
144
+
145
+ ```ts
146
+ import {mkdir, writeFile} from 'node:fs/promises';
147
+ import {createTileflowCaptureSession} from '@tileflow/capture';
148
+
149
+ await mkdir('frames', {recursive: true});
150
+ const session = createTileflowCaptureSession({allowBrowserInstall: true});
151
+ try {
152
+ const shot = await session.captureShot('dusk-flyover', {
153
+ format: 'frames',
154
+ onFrame: (frame) =>
155
+ writeFile(`frames/frame-${String(frame.index).padStart(4, '0')}.png`, frame.png),
156
+ });
157
+ console.log(shot.frameCount, shot.framesSha256);
158
+ } finally {
159
+ await session.close();
160
+ }
161
+ ```
162
+
163
+ `captureShots` and `captureAllShots` render several shots from one compiled snapshot. A shot
164
+ receipt records the shot, theme and blend styles, timing, dimensions, renderer, data identity, and
165
+ `frames.sha256`, a digest of every frame's PNG SHA-256. Frames are reproducible under the same
166
+ conditions as scene captures; encoder bytes may differ between runs, so `output.sha256` identifies
167
+ the written video while `frames.sha256` identifies its pixels.
168
+
109
169
  ## Capture the running application
110
170
 
111
171
  Application capture uses the app's normal loopback server; it does not start a server or discover a
@@ -195,9 +255,9 @@ validation, browser startup, resource loading, MapLibre readiness, and screensho
195
255
  Resource diagnostics contain safe classification and origin/status information, not credentials,
196
256
  query strings, response bodies, raw browser stacks, or DOM contents.
197
257
 
198
- New receipts use schema version 4. Version-2 and version-3 receipts remain readable as historical
199
- evidence, not as newly verified version-4 captures. Import `@tileflow/capture/receipt` for receipt-only
200
- tooling without loading the capture runtime. World selectors resolve once per capture session to
258
+ Receipts use schema version 4; parsing rejects any other version. Shot receipts use their own
259
+ schema version 1. Import
260
+ `@tileflow/capture/receipt` for receipt-only tooling without loading the capture runtime. World selectors resolve once per capture session to
201
261
  an immutable release. Capture retains the complete resolved TileJSON, including its zoom range,
202
262
  bounds, attribution and vector-layer metadata. Above the data's maximum zoom, MapLibre enlarges
203
263
  available tiles while preserving the scene camera. Retries reuse the same frozen metadata.
@@ -18,7 +18,7 @@ the platform-specific headless shell into its versioned per-user browser cache;
18
18
  distribution carries Chromium's license and credits. Chromium source and license information are
19
19
  available at https://www.chromium.org/chromium-projects/.
20
20
 
21
- ## MapLibre GL JS 6.9.0
21
+ ## MapLibre GL JS 6.13.0
22
22
 
23
23
  Copyright (c) 2023 MapLibre contributors. Licensed under BSD-3-Clause. The installed `maplibre-gl`
24
24
  package includes `LICENSE.txt`, including notices for code derived from Mapbox GL JS and other
@@ -4,9 +4,9 @@ import { types as nodeUtilTypes } from "util";
4
4
  import {
5
5
  isTileflowWorldReleaseId,
6
6
  serializeCanonicalJson,
7
- tileflowCaptureIdSchema,
8
7
  tileflowCaptureSceneLimits,
9
8
  tileflowCaptureSceneNameSchema,
9
+ tileflowCaptureShotLimits,
10
10
  tileflowPortableIdSchema,
11
11
  tileflowThemeNameSchema
12
12
  } from "@tileflow/core";
@@ -132,11 +132,10 @@ function parseTileflowCaptureReceipt(source) {
132
132
  function validateTileflowCaptureReceipt(value) {
133
133
  assertPlainReceiptData(value);
134
134
  const receipt = requireRecord(value, "receipt");
135
- const schemaVersion = receipt.schemaVersion;
136
- if (schemaVersion !== 2 && schemaVersion !== 3 && schemaVersion !== tileflowCaptureReceiptSchemaVersion) {
135
+ if (receipt.schemaVersion !== tileflowCaptureReceiptSchemaVersion) {
137
136
  throw invalidReceipt("The baseline receipt schema version is unsupported.");
138
137
  }
139
- const commonKeys = [
138
+ requireExactKeys(receipt, [
140
139
  "schemaVersion",
141
140
  "scene",
142
141
  "style",
@@ -144,18 +143,14 @@ function validateTileflowCaptureReceipt(value) {
144
143
  "renderer",
145
144
  "platform",
146
145
  "data",
147
- ...schemaVersion === 2 && receipt.verification === void 0 ? [] : ["verification"],
146
+ "verification",
148
147
  "networkDependent"
149
- ];
150
- requireExactKeys(receipt, commonKeys);
148
+ ]);
151
149
  const scene = requireRecord(receipt.scene, "scene");
152
- requireExactKeys(
153
- scene,
154
- schemaVersion === tileflowCaptureReceiptSchemaVersion ? ["name", "map", "theme", "target", "sha256"] : ["name", "map", "target", "sha256"]
155
- );
150
+ requireExactKeys(scene, ["name", "map", "theme", "target", "sha256"]);
156
151
  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;
152
+ const map = requirePortableIdentifier(scene.map, "scene.map");
153
+ const theme = requireConcreteTheme(scene.theme);
159
154
  if (scene.target !== "map" && scene.target !== "application") {
160
155
  throw invalidReceipt("The baseline receipt has an invalid scene target.");
161
156
  }
@@ -197,13 +192,13 @@ function validateTileflowCaptureReceipt(value) {
197
192
  if (typeof receipt.networkDependent !== "boolean") {
198
193
  throw invalidReceipt("The baseline receipt has an invalid network dependency flag.");
199
194
  }
200
- const data = schemaVersion === 2 ? validateDataIdentityV2(receipt.data) : validateDataIdentityV3(receipt.data);
195
+ const data = validateDataIdentity(receipt.data);
201
196
  const normalized = {
202
- schemaVersion,
197
+ schemaVersion: tileflowCaptureReceiptSchemaVersion,
203
198
  scene: {
204
199
  name,
205
200
  map,
206
- ...theme === void 0 ? {} : { theme },
201
+ theme,
207
202
  target: scene.target,
208
203
  sha256: requireHash(scene.sha256, "scene.sha256")
209
204
  },
@@ -237,61 +232,220 @@ function validateTileflowCaptureReceipt(value) {
237
232
  assertCanonicalReceiptByteLimit(normalized);
238
233
  return normalized;
239
234
  }
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
- );
235
+ var tileflowShotReceiptSchemaVersion = 1;
236
+ var tileflowShotReceiptLimits = Object.freeze({ maximumBytes: 64 * 1024 });
237
+ function createTileflowShotReceipt(input) {
238
+ return validateTileflowShotReceipt({
239
+ schemaVersion: tileflowShotReceiptSchemaVersion,
240
+ kind: "shot",
241
+ shot: { name: input.shot, map: input.map, theme: input.theme, sha256: input.shotSha256 },
242
+ style: { sha256: input.styleSha256 },
243
+ ...input.blend ? { blend: { themes: input.blend } } : {},
244
+ timing: { fps: input.fps, frameCount: input.frameCount, durationMs: input.durationMs },
245
+ image: {
246
+ cssWidth: input.width,
247
+ cssHeight: input.height,
248
+ physicalWidth: input.width * input.dpr,
249
+ physicalHeight: input.height * input.dpr,
250
+ dpr: input.dpr
251
+ },
252
+ frames: { sha256: input.framesSha256 },
253
+ output: input.output,
254
+ renderer: input.renderer,
255
+ platform: { os: process.platform, architecture: process.arch },
256
+ data: normalizeCaptureDataInput(input.data),
257
+ verification: { data: "rendered", style: "rendered" },
258
+ networkDependent: input.networkDependent
259
+ });
260
+ }
261
+ function serializeTileflowShotReceipt(receipt) {
262
+ const serialized = `${serializeCanonicalJson(validateTileflowShotReceipt(receipt))}
263
+ `;
264
+ if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
265
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
246
266
  }
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",
267
+ return serialized;
268
+ }
269
+ function parseTileflowShotReceipt(source) {
270
+ const byteLength = typeof source === "string" ? new TextEncoder().encode(source).byteLength : source.byteLength;
271
+ if (byteLength > tileflowShotReceiptLimits.maximumBytes) {
272
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
273
+ }
274
+ let value;
275
+ let text;
276
+ try {
277
+ text = typeof source === "string" ? source : new TextDecoder("utf-8", { fatal: true }).decode(source);
278
+ value = JSON.parse(text);
279
+ } catch (error) {
280
+ throw invalidReceipt("The shot receipt is not valid UTF-8 JSON.", error);
281
+ }
282
+ const receipt = validateTileflowShotReceipt(value);
283
+ if (text.trim() !== serializeCanonicalJson(value)) {
284
+ throw invalidReceipt("The shot receipt is not canonical JSON.");
285
+ }
286
+ return receipt;
287
+ }
288
+ function validateTileflowShotReceipt(value) {
289
+ assertPlainReceiptData(value, "receipt", {
290
+ allowArrays: true,
291
+ ancestors: /* @__PURE__ */ new WeakSet(),
292
+ nodes: 0
293
+ });
294
+ const receipt = requireRecord(value, "receipt");
295
+ if (receipt.schemaVersion !== tileflowShotReceiptSchemaVersion || receipt.kind !== "shot") {
296
+ throw invalidReceipt("The shot receipt schema version is unsupported.");
297
+ }
298
+ requireExactKeys(receipt, [
254
299
  "schemaVersion",
255
- ...data.semantics === void 0 ? [] : ["semantics"],
256
- "sourceId",
257
- ...data.source === void 0 ? [] : ["source"],
258
- ...data.url === void 0 ? [] : ["url"]
300
+ "kind",
301
+ "shot",
302
+ "style",
303
+ ...receipt.blend === void 0 ? [] : ["blend"],
304
+ "timing",
305
+ "image",
306
+ "frames",
307
+ "output",
308
+ "renderer",
309
+ "platform",
310
+ "data",
311
+ "verification",
312
+ "networkDependent"
259
313
  ]);
260
- if (data.kind !== "tileflow-world" && data.kind !== "vector-tiles") {
261
- throw invalidReceipt("The baseline receipt has an invalid data.kind.");
314
+ const shot = requireRecord(receipt.shot, "shot");
315
+ requireExactKeys(shot, ["name", "map", "theme", "sha256"]);
316
+ const style = requireRecord(receipt.style, "style");
317
+ requireExactKeys(style, ["sha256"]);
318
+ const blend = receipt.blend === void 0 ? void 0 : validateShotBlend(receipt.blend);
319
+ const timing = requireRecord(receipt.timing, "timing");
320
+ requireExactKeys(timing, ["fps", "frameCount", "durationMs"]);
321
+ const fps = timing.fps;
322
+ if (fps !== 24 && fps !== 25 && fps !== 30 && fps !== 50 && fps !== 60) {
323
+ throw invalidReceipt("The shot receipt has an invalid timing.fps.");
324
+ }
325
+ const frameCount = requirePositiveInteger(timing.frameCount, "timing.frameCount");
326
+ const durationMs = requirePositiveInteger(timing.durationMs, "timing.durationMs");
327
+ if (durationMs > tileflowCaptureShotLimits.durationMs.maximum || frameCount > tileflowCaptureShotLimits.maximumFrames || frameCount !== Math.floor(durationMs * fps / 1e3 + 1e-9) + 1) {
328
+ throw invalidReceipt("The shot receipt timing is inconsistent.");
262
329
  }
263
- if (data.schema !== "openmaptiles" || data.sourceId !== "tileflow") {
264
- throw invalidReceipt("The baseline receipt has an unsupported data contract.");
330
+ const image = requireRecord(receipt.image, "image");
331
+ requireExactKeys(image, ["cssWidth", "cssHeight", "physicalWidth", "physicalHeight", "dpr"]);
332
+ const cssWidth = requirePositiveInteger(image.cssWidth, "image.cssWidth");
333
+ const cssHeight = requirePositiveInteger(image.cssHeight, "image.cssHeight");
334
+ const physicalWidth = requirePositiveInteger(image.physicalWidth, "image.physicalWidth");
335
+ const physicalHeight = requirePositiveInteger(image.physicalHeight, "image.physicalHeight");
336
+ if (image.dpr !== 1 && image.dpr !== 2) {
337
+ throw invalidReceipt("The shot receipt has an invalid image DPR.");
265
338
  }
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
- );
339
+ if (cssWidth * image.dpr !== physicalWidth || cssHeight * image.dpr !== physicalHeight) {
340
+ throw invalidReceipt("The shot receipt image dimensions are inconsistent.");
341
+ }
342
+ if (physicalWidth > tileflowCaptureSceneLimits.viewport.maximum || physicalHeight > tileflowCaptureSceneLimits.viewport.maximum || physicalWidth * physicalHeight > tileflowCaptureSceneLimits.maximumPhysicalPixels) {
343
+ throw invalidReceipt("The shot receipt image exceeds the supported pixel limit.");
344
+ }
345
+ const frames = requireRecord(receipt.frames, "frames");
346
+ requireExactKeys(frames, ["sha256"]);
347
+ const framesSha256 = requireHash(frames.sha256, "frames.sha256");
348
+ const output = requireRecord(receipt.output, "output");
349
+ if (output.kind === "webm") {
350
+ requireExactKeys(output, ["kind", "codec", "sha256"]);
351
+ if (output.codec !== "vp8" && output.codec !== "vp9") {
352
+ throw invalidReceipt("The shot receipt has an invalid output.codec.");
273
353
  }
274
- } else if (data.generation !== void 0) {
275
- throw invalidReceipt("The baseline receipt has an invalid data.generation.");
354
+ } else if (output.kind === "frames") {
355
+ requireExactKeys(output, ["kind", "sha256"]);
356
+ if (output.sha256 !== framesSha256) {
357
+ throw invalidReceipt("The shot receipt frame output does not match its frames.");
358
+ }
359
+ } else {
360
+ throw invalidReceipt("The shot receipt has an invalid output.kind.");
276
361
  }
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 } : {}
362
+ const renderer = requireRecord(receipt.renderer, "renderer");
363
+ requireExactKeys(renderer, [
364
+ "tileflow",
365
+ "maplibre",
366
+ "playwright",
367
+ "chromiumRevision",
368
+ "chromiumVersion"
369
+ ]);
370
+ const platform = requireRecord(receipt.platform, "platform");
371
+ requireExactKeys(platform, ["os", "architecture"]);
372
+ const verification = requireRecord(receipt.verification, "verification");
373
+ requireExactKeys(verification, ["data", "style"]);
374
+ if (verification.data !== "rendered" || verification.style !== "rendered") {
375
+ throw invalidReceipt("The shot receipt verification must record rendered style and data.");
376
+ }
377
+ if (typeof receipt.networkDependent !== "boolean") {
378
+ throw invalidReceipt("The shot receipt has an invalid network dependency flag.");
379
+ }
380
+ const normalized = {
381
+ schemaVersion: tileflowShotReceiptSchemaVersion,
382
+ kind: "shot",
383
+ shot: {
384
+ name: requireSceneName(shot.name),
385
+ map: requirePortableIdentifier(shot.map, "shot.map"),
386
+ theme: requireShotTheme(shot.theme, "shot.theme"),
387
+ sha256: requireHash(shot.sha256, "shot.sha256")
388
+ },
389
+ style: { sha256: requireHash(style.sha256, "style.sha256") },
390
+ ...blend ? { blend } : {},
391
+ timing: { fps, frameCount, durationMs },
392
+ image: { cssWidth, cssHeight, physicalWidth, physicalHeight, dpr: image.dpr },
393
+ frames: { sha256: framesSha256 },
394
+ output: output.kind === "webm" ? {
395
+ kind: "webm",
396
+ codec: output.codec,
397
+ sha256: requireHash(output.sha256, "output.sha256")
398
+ } : { kind: "frames", sha256: framesSha256 },
399
+ renderer: {
400
+ tileflow: requireBoundedString(renderer.tileflow, "renderer.tileflow"),
401
+ maplibre: requireBoundedString(renderer.maplibre, "renderer.maplibre"),
402
+ playwright: requireBoundedString(renderer.playwright, "renderer.playwright"),
403
+ chromiumRevision: requireBoundedString(
404
+ renderer.chromiumRevision,
405
+ "renderer.chromiumRevision"
406
+ ),
407
+ chromiumVersion: requireBoundedString(renderer.chromiumVersion, "renderer.chromiumVersion")
408
+ },
409
+ platform: {
410
+ os: requireBoundedString(platform.os, "platform.os"),
411
+ architecture: requireBoundedString(platform.architecture, "platform.architecture")
412
+ },
413
+ data: validateDataIdentity(receipt.data),
414
+ verification: { data: "rendered", style: "rendered" },
415
+ networkDependent: receipt.networkDependent
292
416
  };
417
+ const serialized = `${serializeCanonicalJson(normalized)}
418
+ `;
419
+ if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
420
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
421
+ }
422
+ return normalized;
293
423
  }
294
- function validateDataIdentityV3(value) {
424
+ function requireShotTheme(value, field) {
425
+ const result = tileflowThemeNameSchema.safeParse(value);
426
+ if (!result.success) throw invalidReceipt(`The shot receipt has an invalid ${field}.`);
427
+ return result.data;
428
+ }
429
+ function validateShotBlend(value) {
430
+ const blend = requireRecord(value, "blend");
431
+ requireExactKeys(blend, ["themes"]);
432
+ if (!Array.isArray(blend.themes) || blend.themes.length < tileflowCaptureShotLimits.blendThemes.minimum || blend.themes.length > tileflowCaptureShotLimits.blendThemes.maximum) {
433
+ throw invalidReceipt("The shot receipt has an invalid blend.themes list.");
434
+ }
435
+ const themes = blend.themes.map((entry, index) => {
436
+ const theme = requireRecord(entry, `blend.themes.${index}`);
437
+ requireExactKeys(theme, ["name", "styleSha256"]);
438
+ return {
439
+ name: requireShotTheme(theme.name, `blend.themes.${index}.name`),
440
+ styleSha256: requireHash(theme.styleSha256, `blend.themes.${index}.styleSha256`)
441
+ };
442
+ });
443
+ if (new Set(themes.map((theme) => theme.name)).size !== themes.length) {
444
+ throw invalidReceipt("The shot receipt blends a theme more than once.");
445
+ }
446
+ return { themes };
447
+ }
448
+ function validateDataIdentity(value) {
295
449
  const data = requireRecord(value, "data");
296
450
  if (data.kind === "tileflow-world") {
297
451
  requireExactKeys(data, [
@@ -304,13 +458,12 @@ function validateDataIdentityV3(value) {
304
458
  "releaseId",
305
459
  "schema",
306
460
  "schemaVersion",
307
- ...data.semantics === void 0 ? [] : ["semantics"],
461
+ "semantics",
308
462
  "sourceId"
309
463
  ]);
310
464
  if (data.product !== "world-v1" || data.schema !== "openmaptiles" || data.sourceId !== "tileflow") {
311
465
  throw invalidReceipt("The baseline receipt has an unsupported World data contract.");
312
466
  }
313
- const semantics2 = data.semantics === void 0 ? void 0 : validateDataSemantics(data.semantics, "data.semantics");
314
467
  return {
315
468
  archiveSha256: requireHash(data.archiveSha256, "data.archiveSha256"),
316
469
  contractSha256: requireHash(data.contractSha256, "data.contractSha256"),
@@ -321,7 +474,7 @@ function validateDataIdentityV3(value) {
321
474
  releaseId: requireWorldReleaseId(data.releaseId),
322
475
  schema: "openmaptiles",
323
476
  schemaVersion: requirePositiveInteger(data.schemaVersion, "data.schemaVersion"),
324
- ...semantics2 ? { semantics: semantics2 } : {},
477
+ semantics: validateDataSemantics(data.semantics, "data.semantics"),
325
478
  sourceId: "tileflow"
326
479
  };
327
480
  }
@@ -329,41 +482,35 @@ function validateDataIdentityV3(value) {
329
482
  throw invalidReceipt("The baseline receipt has an invalid data.kind.");
330
483
  }
331
484
  requireExactKeys(data, [
332
- ...data.bindings === void 0 ? [] : ["bindings"],
333
- ...data.capabilities === void 0 ? [] : ["capabilities"],
485
+ "bindings",
486
+ "capabilities",
334
487
  "kind",
335
488
  ...data.revision === void 0 ? [] : ["revision"],
336
489
  "schema",
337
490
  "schemaVersion",
338
- ...data.semantics === void 0 ? [] : ["semantics"],
339
- "sourceId",
340
- ...data.source === void 0 ? [] : ["source"]
491
+ "semantics",
492
+ "source",
493
+ "sourceId"
341
494
  ]);
342
495
  if (data.schema !== "openmaptiles" || data.sourceId !== "tileflow") {
343
496
  throw invalidReceipt("The baseline receipt has an unsupported data contract.");
344
497
  }
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
498
  return {
349
- ...bindings ? { bindings } : {},
350
- ...capabilities ? { capabilities } : {},
499
+ bindings: validateDataBindings(data.bindings, "data.bindings"),
500
+ capabilities: validateDataCapabilities(data.capabilities, "data.capabilities"),
351
501
  kind: "vector-tiles",
352
502
  ...data.revision === void 0 ? {} : { revision: requireSourceRevision(data.revision) },
353
503
  schema: "openmaptiles",
354
504
  schemaVersion: requirePositiveInteger(data.schemaVersion, "data.schemaVersion"),
355
- ...semantics ? { semantics } : {},
356
- sourceId: "tileflow",
357
- ...data.source === void 0 ? {} : { source: validateDataSource(data.source, "data.source") }
505
+ semantics: validateDataSemantics(data.semantics, "data.semantics"),
506
+ source: validateDataSource(data.source, "data.source"),
507
+ sourceId: "tileflow"
358
508
  };
359
509
  }
360
510
  function normalizeCaptureDataInput(input) {
361
511
  if (input.kind === "tileflow-world") return input;
362
512
  const { url, ...identity } = input;
363
- return {
364
- ...identity,
365
- ...url === void 0 ? {} : { source: fingerprintLegacyDataUrl(url) }
366
- };
513
+ return { ...identity, source: fingerprintDataUrl(url) };
367
514
  }
368
515
  function validateDataSemantics(value, field) {
369
516
  const semantics = requireRecord(value, field);
@@ -410,16 +557,18 @@ function validateDataCapabilities(value, field) {
410
557
  const capabilities = requireRecord(value, field);
411
558
  requireExactKeys(capabilities, [
412
559
  "businessCorridor",
413
- ...capabilities.bathymetry === void 0 ? [] : ["bathymetry"],
560
+ "bathymetry",
561
+ "canopy",
414
562
  "globalLandcover",
415
563
  "tree"
416
564
  ]);
417
- if (typeof capabilities.businessCorridor !== "boolean" || capabilities.bathymetry !== void 0 && typeof capabilities.bathymetry !== "boolean" || typeof capabilities.globalLandcover !== "boolean" || typeof capabilities.tree !== "boolean") {
565
+ if (typeof capabilities.businessCorridor !== "boolean" || typeof capabilities.bathymetry !== "boolean" || typeof capabilities.canopy !== "boolean" || typeof capabilities.globalLandcover !== "boolean" || typeof capabilities.tree !== "boolean") {
418
566
  throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
419
567
  }
420
568
  return {
421
569
  businessCorridor: capabilities.businessCorridor,
422
- ...capabilities.bathymetry === void 0 ? {} : { bathymetry: capabilities.bathymetry },
570
+ bathymetry: capabilities.bathymetry,
571
+ canopy: capabilities.canopy,
423
572
  globalLandcover: capabilities.globalLandcover,
424
573
  tree: capabilities.tree
425
574
  };
@@ -434,7 +583,6 @@ function validateDataSource(value, field) {
434
583
  }
435
584
  function validateVerification(value, target) {
436
585
  const expected = verificationForTarget(target);
437
- if (value === void 0) return expected;
438
586
  const verification = requireRecord(value, "verification");
439
587
  requireExactKeys(verification, ["data", "style"]);
440
588
  if (verification.data !== expected.data || verification.style !== expected.style) {
@@ -456,12 +604,17 @@ function assertCanonicalReceiptByteLimit(receipt) {
456
604
  }
457
605
  }
458
606
  function assertPlainReceiptData(value, path = "receipt", state = {
607
+ allowArrays: false,
459
608
  ancestors: /* @__PURE__ */ new WeakSet(),
460
609
  nodes: 0
461
610
  }, depth = 0) {
462
611
  if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
463
612
  return;
464
613
  }
614
+ if (typeof value === "object" && Array.isArray(value) && state.allowArrays) {
615
+ assertPlainReceiptArray(value, path, state, depth);
616
+ return;
617
+ }
465
618
  if (typeof value !== "object" || Array.isArray(value)) {
466
619
  throw invalidReceipt(`The baseline receipt has an executable or non-plain ${path} value.`);
467
620
  }
@@ -500,6 +653,35 @@ function assertPlainReceiptData(value, path = "receipt", state = {
500
653
  state.ancestors.delete(value);
501
654
  }
502
655
  }
656
+ function assertPlainReceiptArray(value, path, state, depth) {
657
+ if (nodeUtilTypes.isProxy(value)) {
658
+ throw invalidReceipt(`The baseline receipt has an executable proxy at ${path}.`);
659
+ }
660
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
661
+ throw invalidReceipt(`The baseline receipt has a non-plain ${path} array.`);
662
+ }
663
+ if (depth > 32 || ++state.nodes > 4096 || value.length > 4096) {
664
+ throw invalidReceipt("The baseline receipt exceeds the supported structural limit.");
665
+ }
666
+ if (state.ancestors.has(value)) {
667
+ throw invalidReceipt("The baseline receipt must not contain cyclic data.");
668
+ }
669
+ if (Reflect.ownKeys(value).length !== value.length + 1) {
670
+ throw invalidReceipt(`The baseline receipt has a sparse or extended ${path} array.`);
671
+ }
672
+ state.ancestors.add(value);
673
+ try {
674
+ for (let index = 0; index < value.length; index += 1) {
675
+ const descriptor = Object.getOwnPropertyDescriptor(value, index);
676
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
677
+ throw invalidReceipt("The baseline receipt contains an accessor or non-enumerable field.");
678
+ }
679
+ assertPlainReceiptData(descriptor.value, `${path}.${index}`, state, depth + 1);
680
+ }
681
+ } finally {
682
+ state.ancestors.delete(value);
683
+ }
684
+ }
503
685
  function requireRecord(value, field) {
504
686
  if (!value || typeof value !== "object" || Array.isArray(value)) {
505
687
  throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
@@ -513,11 +695,6 @@ function requireExactKeys(value, expected) {
513
695
  throw invalidReceipt("The baseline receipt contains missing or unsupported fields.");
514
696
  }
515
697
  }
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
698
  function requirePortableIdentifier(value, field) {
522
699
  const result = tileflowPortableIdSchema.safeParse(value);
523
700
  if (!result.success) throw invalidReceipt(`The baseline receipt has an invalid ${field}.`);
@@ -566,7 +743,7 @@ function requireSourceRevision(value) {
566
743
  }
567
744
  return value;
568
745
  }
569
- function fingerprintLegacyDataUrl(value) {
746
+ function fingerprintDataUrl(value) {
570
747
  if (typeof value !== "string" || value.length < 1 || value.length > 2048) {
571
748
  throw invalidReceipt("The baseline receipt has an invalid data.url.");
572
749
  }
@@ -594,12 +771,6 @@ function isLoopbackHostname(value) {
594
771
  const hostname = value.toLowerCase().replace(/^\[|\]$/g, "");
595
772
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
596
773
  }
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
774
  function requireWorldReleaseId(value) {
604
775
  if (!isTileflowWorldReleaseId(value)) {
605
776
  throw invalidReceipt("The baseline receipt has an invalid data.releaseId.");
@@ -621,5 +792,11 @@ export {
621
792
  createTileflowCaptureReceipt,
622
793
  serializeTileflowCaptureReceipt,
623
794
  parseTileflowCaptureReceipt,
624
- validateTileflowCaptureReceipt
795
+ validateTileflowCaptureReceipt,
796
+ tileflowShotReceiptSchemaVersion,
797
+ tileflowShotReceiptLimits,
798
+ createTileflowShotReceipt,
799
+ serializeTileflowShotReceipt,
800
+ parseTileflowShotReceipt,
801
+ validateTileflowShotReceipt
625
802
  };