@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 +63 -3
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/{chunk-BSTB4FN6.js → chunk-BI5PWIH4.js} +276 -99
- package/dist/index.d.ts +78 -19
- package/dist/index.js +992 -139
- package/dist/receipt-CfZkzyXd.d.ts +222 -0
- package/dist/receipt.d.ts +1 -1
- package/dist/receipt.js +15 -3
- package/docs/receipts-and-comparison.md +9 -2
- package/package.json +16 -16
- package/dist/receipt-Q7wQ68tG.d.ts +0 -160
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
|
-
|
|
199
|
-
|
|
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.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
158
|
-
const theme =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
...
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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 (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 (
|
|
275
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
485
|
+
"bindings",
|
|
486
|
+
"capabilities",
|
|
334
487
|
"kind",
|
|
335
488
|
...data.revision === void 0 ? [] : ["revision"],
|
|
336
489
|
"schema",
|
|
337
490
|
"schemaVersion",
|
|
338
|
-
|
|
339
|
-
"
|
|
340
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
560
|
+
"bathymetry",
|
|
561
|
+
"canopy",
|
|
414
562
|
"globalLandcover",
|
|
415
563
|
"tree"
|
|
416
564
|
]);
|
|
417
|
-
if (typeof capabilities.businessCorridor !== "boolean" || capabilities.bathymetry !==
|
|
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
|
-
|
|
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
|
|
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
|
};
|