@tileflow/static 0.1.0-alpha.21 → 0.1.0-alpha.22
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 +65 -12
- package/dist/{chunk-NHQK5UZ2.js → chunk-EMCKSFZV.js} +23 -1
- package/dist/{chunk-KPT7PASB.js → chunk-EPOYQNNT.js} +1 -1
- package/dist/{chunk-A23RC4BE.js → chunk-MSQ4OLKA.js} +49 -3
- package/dist/{chunk-MGQCHCU3.js → chunk-NM2JSFUF.js} +28 -4
- package/dist/chunk-TQMKOTBE.js +287 -0
- package/dist/client.d.ts +94 -5
- package/dist/client.js +11 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +39 -5
- package/dist/manifest.d.ts +553 -8
- package/dist/manifest.js +25 -3
- package/dist/overlays.d.ts +2 -2
- package/dist/overlays.js +2 -2
- package/dist/{scene-contract-DZMhLDnf.d.ts → scene-contract-OMIOisSB.d.ts} +48 -3
- package/dist/scene.d.ts +20 -2
- package/dist/scene.js +6 -2
- package/package.json +1 -1
- package/dist/chunk-S3WXE5P4.js +0 -99
package/README.md
CHANGED
|
@@ -20,19 +20,67 @@ import {
|
|
|
20
20
|
const scene = validateStaticScene({
|
|
21
21
|
map: 'madrid',
|
|
22
22
|
theme: 'dark',
|
|
23
|
+
format: 'webp',
|
|
23
24
|
camera: {type: 'auto', padding: 24},
|
|
24
|
-
size: {width:
|
|
25
|
+
size: {width: 600, height: 400, dpr: 2},
|
|
25
26
|
overlays: [marker({coordinate: [-3.7038, 40.4168]})],
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
if (!scene.ok) throw new Error(scene.error);
|
|
29
30
|
|
|
30
|
-
await precacheStaticMap(scene.scene, {
|
|
31
|
+
const result = await precacheStaticMap(scene.scene, {
|
|
31
32
|
apiKey: process.env.TILEFLOW_API_KEY,
|
|
32
33
|
idempotencyKey: createStaticMapIdempotencyKey(),
|
|
33
34
|
});
|
|
35
|
+
|
|
36
|
+
console.log(result.attribution.entries);
|
|
34
37
|
```
|
|
35
38
|
|
|
39
|
+
`width` and `height` are logical CSS pixels. `dpr` accepts 1 or 2 and defaults to 1, so a 600 by
|
|
40
|
+
400 scene at DPR 2 produces a 1200 by 800 image without changing camera, padding, or overlay paint
|
|
41
|
+
sizes. Logical dimensions may reach 2048 per axis; the physical budget permits 4096 by 4096 output,
|
|
42
|
+
or 16,777,216 total pixels.
|
|
43
|
+
|
|
44
|
+
`format` accepts `png`, `jpeg`, or `webp` and defaults to PNG. PNG's canonical default stays omitted,
|
|
45
|
+
so explicit and omitted PNG requests share identity. JPEG and WebP use a fixed encoder quality;
|
|
46
|
+
there is no public quality option.
|
|
47
|
+
|
|
48
|
+
## Attribution
|
|
49
|
+
|
|
50
|
+
New renders default to `attribution: {mode: 'embedded', position: 'auto'}` without adding that
|
|
51
|
+
omitted default to the normalized scene. Hosted derives notices from the sources referenced by the
|
|
52
|
+
exact selected Style and embeds a safe, compact block. Automatic placement prefers an unobstructed
|
|
53
|
+
corner, then the least interfering corner among declared request overlays. Explicit center and
|
|
54
|
+
bounds cameras never move.
|
|
55
|
+
|
|
56
|
+
Choose a corner when composition requires it:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
attribution: {mode: 'embedded', position: 'bottom-left'}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or request structured external attribution:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const result = await createStaticMap(
|
|
66
|
+
{...scene.scene, attribution: {mode: 'external'}},
|
|
67
|
+
{
|
|
68
|
+
apiKey: process.env.TILEFLOW_API_KEY,
|
|
69
|
+
idempotencyKey: createStaticMapIdempotencyKey(),
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (const entry of result.attribution.entries) {
|
|
74
|
+
renderAttributionText(entry.text, entry.links);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
External mode leaves the image unchanged and returns `position: null`. The caller must retain and
|
|
79
|
+
display every returned entry beside the image. Entries contain bounded plain text, safe HTTP(S)
|
|
80
|
+
links, and `platform-notice` or `team-declared` authority; they never contain source HTML or private
|
|
81
|
+
resource IDs. Embedded results return the resolved corner. Changing mode or requested position
|
|
82
|
+
changes scene and artifact identity.
|
|
83
|
+
|
|
36
84
|
## Automatic framing
|
|
37
85
|
|
|
38
86
|
`camera.type: 'auto'` fits the complete nominal footprint of every declared marker, circle, line,
|
|
@@ -68,7 +116,8 @@ reason, and bounded details. `validateStaticScene` returns that document; reques
|
|
|
68
116
|
`StaticMapRequestError` either locally or from the Hosted response. The reasons distinguish an empty
|
|
69
117
|
scene, an ambiguous antimeridian path, an out-of-range overlay latitude, an insufficient viewport,
|
|
70
118
|
a Globe visibility conflict, an unresolvable camera, and failed post-fit containment. Correct the
|
|
71
|
-
scene; do not retry it unchanged.
|
|
119
|
+
scene; do not retry it unchanged. Unsupported attribution grammar or glyphs, excessive attribution,
|
|
120
|
+
and a block that cannot fit use the same non-retryable `422` boundary.
|
|
72
121
|
|
|
73
122
|
The package has four deliberately separate surfaces, all available from the root for compatibility:
|
|
74
123
|
|
|
@@ -76,24 +125,28 @@ The package has four deliberately separate surfaces, all available from the root
|
|
|
76
125
|
`normalizeStaticScene`;
|
|
77
126
|
- overlay authoring and MapLibre compilation: `line`, `circle`, `marker`, `polygon`, and
|
|
78
127
|
`compileStaticOverlays`;
|
|
79
|
-
- renderer identity: `
|
|
80
|
-
`hashRenderManifest`, and `hashStaticSceneRequest`;
|
|
128
|
+
- renderer identity: `createRenderManifestV1`, `createRenderManifestV2`,
|
|
129
|
+
`validateStaticRenderManifest`, `hashRenderManifest`, and `hashStaticSceneRequest`;
|
|
81
130
|
- bounded Hosted transport: `prepareStaticMapRequest`, `createStaticMap`, `precacheStaticMap`, and
|
|
82
131
|
`requestStaticMapUntilReady`.
|
|
83
132
|
|
|
84
133
|
Applications that need only one surface can use `@tileflow/static/scene`, `/overlays`, `/manifest`,
|
|
85
134
|
or `/client`. The root remains a compatibility entry point.
|
|
86
135
|
|
|
87
|
-
`StaticRenderManifest` is the immutable input contract between a resolved Hosted style and
|
|
88
|
-
Maps renderer.
|
|
89
|
-
|
|
90
|
-
|
|
136
|
+
`StaticRenderManifest` is the versioned immutable input contract between a resolved Hosted style and
|
|
137
|
+
a Static Maps renderer. Manifest v1 remains the exact historical no-attribution contract; new Hosted
|
|
138
|
+
work uses strict v2 with resolved safe entries and immutable provenance. `createRenderManifest`
|
|
139
|
+
remains the v1 compatibility alias. Its top-level `mapId` identifies the resolved destination while
|
|
140
|
+
`scene.map` retains the portable map name supplied by the application. It is unrelated to the
|
|
141
|
+
runtime delivery manifest generated by application build or deploy workflows.
|
|
91
142
|
|
|
92
143
|
Create one idempotency key per intentional create/precache action and retain it across network
|
|
93
144
|
retries. Reusing the key with the same normalized scene returns the stored result; reusing it with a
|
|
94
145
|
different scene returns 409. A successful paid operation reports `unitCost: 15`, shared
|
|
95
146
|
`remainingUnits`, its durable `operationId`, cache reuse, hash, and immutable image URL. An
|
|
96
|
-
unbounded Starter service reports `remainingUnits: null`.
|
|
147
|
+
unbounded Starter service reports `remainingUnits: null`. Creation helpers request strict result v2,
|
|
148
|
+
including `resultVersion: 2` and required attribution. They reject a legacy success rather than
|
|
149
|
+
claiming notices that were never produced. A live
|
|
97
150
|
equal-hash render may return `202`; the helper polls with the same body/key within its bounded wait.
|
|
98
151
|
`maxWaitMs` is the total client completion budget, including network, response parsing, and polling
|
|
99
152
|
delays; `signal` cancels the same complete operation. The helper aborts in-flight work when either
|
|
@@ -106,7 +159,7 @@ the same normalized scene for its `sceneKey` and HTTP body, so equivalent omitte
|
|
|
106
159
|
deduplicate consistently without duplicating the polling implementation.
|
|
107
160
|
|
|
108
161
|
Free cannot initiate Static Maps work. Starter consumes 15 shared API units for each successful
|
|
109
|
-
logical operation, including a distinct-key cache reuse. Downloading
|
|
110
|
-
zero units.
|
|
162
|
+
logical operation, including a distinct-key cache reuse. Downloading an immutable PNG, JPEG, or WebP
|
|
163
|
+
consumes zero units.
|
|
111
164
|
|
|
112
165
|
Docs: https://tileflow.dev/docs
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
staticSceneLimits,
|
|
9
9
|
staticSceneSchema,
|
|
10
10
|
stripUndefined
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-NM2JSFUF.js";
|
|
12
12
|
|
|
13
13
|
// src/auto-fit.ts
|
|
14
14
|
import { z } from "zod";
|
|
@@ -67,6 +67,21 @@ var staticMapRequestErrorResponseSchema = z.discriminatedUnion("code", [
|
|
|
67
67
|
code: z.literal("STATIC_OVERLAY_INVALID"),
|
|
68
68
|
details: staticOverlayInvalidDetailsSchema,
|
|
69
69
|
reason: z.literal("OVERLAY_LATITUDE_OUT_OF_RANGE")
|
|
70
|
+
}).strict(),
|
|
71
|
+
z.object({
|
|
72
|
+
...autoFitErrorBase,
|
|
73
|
+
code: z.literal("STATIC_MAP_ATTRIBUTION_UNSUPPORTED"),
|
|
74
|
+
reason: z.enum(["UNSUPPORTED_DECLARATION", "UNSUPPORTED_GLYPH"])
|
|
75
|
+
}).strict(),
|
|
76
|
+
z.object({
|
|
77
|
+
...autoFitErrorBase,
|
|
78
|
+
code: z.literal("STATIC_MAP_ATTRIBUTION_TOO_LARGE"),
|
|
79
|
+
reason: z.literal("CONTENT_LIMIT_EXCEEDED")
|
|
80
|
+
}).strict(),
|
|
81
|
+
z.object({
|
|
82
|
+
...autoFitErrorBase,
|
|
83
|
+
code: z.literal("STATIC_MAP_ATTRIBUTION_DOES_NOT_FIT"),
|
|
84
|
+
reason: z.literal("BLOCK_DOES_NOT_FIT")
|
|
70
85
|
}).strict()
|
|
71
86
|
]);
|
|
72
87
|
var StaticMapRequestError = class extends Error {
|
|
@@ -398,6 +413,12 @@ function normalizeStaticScene(scene) {
|
|
|
398
413
|
}
|
|
399
414
|
function normalizeParsedStaticScene(parsed) {
|
|
400
415
|
return stripUndefined({
|
|
416
|
+
...parsed.attribution && Object.keys(parsed.attribution).length > 0 ? {
|
|
417
|
+
attribution: parsed.attribution.mode === "external" ? { mode: "external" } : stripUndefined({
|
|
418
|
+
mode: parsed.attribution.mode,
|
|
419
|
+
position: parsed.attribution.position
|
|
420
|
+
})
|
|
421
|
+
} : {},
|
|
401
422
|
camera: parsed.camera.type === "center" ? {
|
|
402
423
|
bearing: roundNumber(parsed.camera.bearing ?? 0),
|
|
403
424
|
center: normalizeStaticCoordinate(parsed.camera.center),
|
|
@@ -414,6 +435,7 @@ function normalizeParsedStaticScene(parsed) {
|
|
|
414
435
|
padding: normalizeAutoPadding(parsed.camera.padding, parsed.size),
|
|
415
436
|
type: "auto"
|
|
416
437
|
},
|
|
438
|
+
...parsed.format && parsed.format !== "png" ? { format: parsed.format } : {},
|
|
417
439
|
map: parsed.map,
|
|
418
440
|
overlays: parsed.overlays.map(
|
|
419
441
|
(overlay, index) => normalizeStaticOverlay({
|
|
@@ -2,16 +2,18 @@ import {
|
|
|
2
2
|
StaticMapRequestError,
|
|
3
3
|
staticMapRequestErrorResponseSchema,
|
|
4
4
|
validateStaticScene
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EMCKSFZV.js";
|
|
6
6
|
import {
|
|
7
7
|
isSafeHttpUrl,
|
|
8
|
+
jsonByteLength,
|
|
8
9
|
stableStringify
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-NM2JSFUF.js";
|
|
10
11
|
|
|
11
12
|
// src/client.ts
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
var maxStaticMapResponseBytes = 64 * 1024;
|
|
14
15
|
var staticMapIdempotencyKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
16
|
+
var STATIC_MAP_RESULT_V2_MEDIA_TYPE = "application/vnd.tileflow.static-map-result+json;version=2";
|
|
15
17
|
var preparedStaticMapRequestBodies = /* @__PURE__ */ new WeakMap();
|
|
16
18
|
var staticMapReadyResultSchema = z.object({
|
|
17
19
|
cached: z.boolean(),
|
|
@@ -22,6 +24,39 @@ var staticMapReadyResultSchema = z.object({
|
|
|
22
24
|
status: z.literal("ready"),
|
|
23
25
|
unitCost: z.literal(15)
|
|
24
26
|
}).strict();
|
|
27
|
+
var safeAttributionTextSchema = z.string().min(1).max(16384).refine(hasNoControlCharacters, { message: "Attribution text contains control characters" });
|
|
28
|
+
var staticMapAttributionEntrySchema = z.object({
|
|
29
|
+
authority: z.enum(["platform-notice", "team-declared"]),
|
|
30
|
+
links: z.array(
|
|
31
|
+
z.object({
|
|
32
|
+
label: safeAttributionTextSchema,
|
|
33
|
+
url: z.string().trim().url().max(2048).refine(isSafeHttpUrl, {
|
|
34
|
+
message: "Expected an http(s) URL without credentials or a fragment"
|
|
35
|
+
})
|
|
36
|
+
}).strict()
|
|
37
|
+
).max(8),
|
|
38
|
+
text: safeAttributionTextSchema
|
|
39
|
+
}).strict();
|
|
40
|
+
var staticMapAttributionResultSchema = z.object({
|
|
41
|
+
entries: z.array(staticMapAttributionEntrySchema).max(18),
|
|
42
|
+
mode: z.enum(["embedded", "external"]),
|
|
43
|
+
position: z.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).nullable()
|
|
44
|
+
}).strict().superRefine((result, context) => {
|
|
45
|
+
if (result.mode === "external" && result.position !== null || result.mode === "embedded" && result.position === null) {
|
|
46
|
+
context.addIssue({
|
|
47
|
+
code: "custom",
|
|
48
|
+
message: "Attribution position must match its mode",
|
|
49
|
+
path: ["position"]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (jsonByteLength(result) > 32 * 1024) {
|
|
53
|
+
context.addIssue({ code: "custom", message: "Attribution result exceeds 32 KiB" });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
var staticMapHostedResultSchema = staticMapReadyResultSchema.extend({
|
|
57
|
+
attribution: staticMapAttributionResultSchema,
|
|
58
|
+
resultVersion: z.literal(2)
|
|
59
|
+
}).strict();
|
|
25
60
|
var staticMapProcessingResultSchema = z.object({
|
|
26
61
|
operationId: z.string().min(20).max(68),
|
|
27
62
|
retryAfterMs: z.number().int().min(0).max(5e3),
|
|
@@ -92,6 +127,7 @@ async function requestStaticMapUntilReady(request, options) {
|
|
|
92
127
|
const fetcher = options.fetch ?? fetch;
|
|
93
128
|
const createUrl = normalizeStaticMapEndpointUrl(options.createUrl);
|
|
94
129
|
const headers = {
|
|
130
|
+
Accept: STATIC_MAP_RESULT_V2_MEDIA_TYPE,
|
|
95
131
|
"Content-Type": "application/json",
|
|
96
132
|
"Idempotency-Key": idempotency.key
|
|
97
133
|
};
|
|
@@ -119,7 +155,7 @@ async function requestStaticMapUntilReady(request, options) {
|
|
|
119
155
|
const json = await readJsonResponse(response);
|
|
120
156
|
throwIfAborted(signal);
|
|
121
157
|
if (response.status !== 202) {
|
|
122
|
-
const parsed =
|
|
158
|
+
const parsed = staticMapHostedResultSchema.safeParse(json);
|
|
123
159
|
if (!parsed.success) {
|
|
124
160
|
throw new Error(
|
|
125
161
|
`Tileflow static map returned an invalid response: ${parsed.error.message}`
|
|
@@ -140,6 +176,12 @@ async function requestStaticMapUntilReady(request, options) {
|
|
|
140
176
|
}
|
|
141
177
|
});
|
|
142
178
|
}
|
|
179
|
+
function hasNoControlCharacters(value) {
|
|
180
|
+
return !Array.from(value).some((character) => {
|
|
181
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
182
|
+
return codePoint <= 31 || codePoint === 127;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
143
185
|
function boundedClientDuration(value, fallback, minimum, maximum, name) {
|
|
144
186
|
const resolved = value ?? fallback;
|
|
145
187
|
if (!Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) {
|
|
@@ -317,7 +359,11 @@ function hasControlCharacter(value) {
|
|
|
317
359
|
}
|
|
318
360
|
|
|
319
361
|
export {
|
|
362
|
+
STATIC_MAP_RESULT_V2_MEDIA_TYPE,
|
|
320
363
|
staticMapReadyResultSchema,
|
|
364
|
+
staticMapAttributionEntrySchema,
|
|
365
|
+
staticMapAttributionResultSchema,
|
|
366
|
+
staticMapHostedResultSchema,
|
|
321
367
|
staticMapProcessingResultSchema,
|
|
322
368
|
validateStaticMapIdempotencyKey,
|
|
323
369
|
createStaticMapIdempotencyKey,
|
|
@@ -51,11 +51,11 @@ import { z } from "zod";
|
|
|
51
51
|
var staticSceneSchemaVersion = 1;
|
|
52
52
|
var MAX_OVERLAY_LATITUDE = 85.051129;
|
|
53
53
|
var staticSceneLimits = {
|
|
54
|
-
maxDimension:
|
|
54
|
+
maxDimension: 2048,
|
|
55
55
|
maxGeoJsonBytes: 96e3,
|
|
56
56
|
maxOverlays: 24,
|
|
57
57
|
maxPathCoordinates: 2e3,
|
|
58
|
-
maxPhysicalPixels:
|
|
58
|
+
maxPhysicalPixels: 2048 * 2048 * 4,
|
|
59
59
|
minDimension: 64
|
|
60
60
|
};
|
|
61
61
|
var coordinateSchema = z.tuple([z.number().finite(), z.number().finite()]).refine(([lng, lat]) => lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90, {
|
|
@@ -79,12 +79,32 @@ var portableIdSchema = z.string().min(1).max(64).regex(/^[a-z][a-z0-9-]{0,63}$/u
|
|
|
79
79
|
var concreteThemeSchema = portableIdSchema.refine((value) => value !== "system", {
|
|
80
80
|
message: 'Static maps require a concrete theme; "system" is browser-only'
|
|
81
81
|
});
|
|
82
|
+
var staticMapFormatSchema = z.enum(["png", "jpeg", "webp"]);
|
|
83
|
+
var staticAttributionPositionSchema = z.enum([
|
|
84
|
+
"auto",
|
|
85
|
+
"top-left",
|
|
86
|
+
"top-right",
|
|
87
|
+
"bottom-left",
|
|
88
|
+
"bottom-right"
|
|
89
|
+
]);
|
|
90
|
+
var staticAttributionRequestSchema = z.union([
|
|
91
|
+
z.object({
|
|
92
|
+
mode: z.literal("embedded").optional(),
|
|
93
|
+
position: staticAttributionPositionSchema.optional()
|
|
94
|
+
}).strict(),
|
|
95
|
+
z.object({
|
|
96
|
+
mode: z.literal("external")
|
|
97
|
+
}).strict()
|
|
98
|
+
]);
|
|
82
99
|
var sizeSchema = z.object({
|
|
83
|
-
dpr: z.literal(1).optional(),
|
|
100
|
+
dpr: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
84
101
|
height: z.number().int().min(staticSceneLimits.minDimension).max(staticSceneLimits.maxDimension),
|
|
85
102
|
width: z.number().int().min(staticSceneLimits.minDimension).max(staticSceneLimits.maxDimension)
|
|
86
103
|
}).refine(
|
|
87
|
-
(size) =>
|
|
104
|
+
(size) => {
|
|
105
|
+
const dpr = size.dpr ?? 1;
|
|
106
|
+
return size.width * size.height * dpr * dpr <= staticSceneLimits.maxPhysicalPixels;
|
|
107
|
+
},
|
|
88
108
|
{
|
|
89
109
|
message: "Static map exceeds the maximum render pixel budget"
|
|
90
110
|
}
|
|
@@ -178,7 +198,9 @@ var staticOverlaySchema = z.discriminatedUnion("type", [
|
|
|
178
198
|
polygonOverlaySchema
|
|
179
199
|
]);
|
|
180
200
|
var staticSceneSchema = z.object({
|
|
201
|
+
attribution: staticAttributionRequestSchema.optional(),
|
|
181
202
|
camera: z.discriminatedUnion("type", [centerCameraSchema, boundsCameraSchema, autoCameraSchema]),
|
|
203
|
+
format: staticMapFormatSchema.optional(),
|
|
182
204
|
map: portableIdSchema,
|
|
183
205
|
overlays: z.array(staticOverlaySchema).max(staticSceneLimits.maxOverlays).default([]),
|
|
184
206
|
size: sizeSchema,
|
|
@@ -220,6 +242,8 @@ export {
|
|
|
220
242
|
staticSceneSchemaVersion,
|
|
221
243
|
MAX_OVERLAY_LATITUDE,
|
|
222
244
|
staticSceneLimits,
|
|
245
|
+
staticAttributionPositionSchema,
|
|
246
|
+
staticAttributionRequestSchema,
|
|
223
247
|
staticPaddingSchema,
|
|
224
248
|
staticOverlaySchema,
|
|
225
249
|
staticSceneSchema,
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeStaticAutoFit,
|
|
3
|
+
normalizeStaticScene,
|
|
4
|
+
staticAutoFitPlanSchema,
|
|
5
|
+
validateStaticScene
|
|
6
|
+
} from "./chunk-EMCKSFZV.js";
|
|
7
|
+
import {
|
|
8
|
+
hashStableValue,
|
|
9
|
+
isSafeHttpUrl,
|
|
10
|
+
jsonByteLength,
|
|
11
|
+
stableStringify,
|
|
12
|
+
staticSceneSchema,
|
|
13
|
+
stripUndefined
|
|
14
|
+
} from "./chunk-NM2JSFUF.js";
|
|
15
|
+
|
|
16
|
+
// src/manifest.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
var staticRendererSchemaVersionV1 = 1;
|
|
19
|
+
var staticRendererSchemaVersion = 2;
|
|
20
|
+
var staticAttributionPlanSchemaVersion = 1;
|
|
21
|
+
var staticAttributionLimits = {
|
|
22
|
+
maxBytes: 32 * 1024,
|
|
23
|
+
maxEntries: 18,
|
|
24
|
+
maxLinksPerEntry: 8,
|
|
25
|
+
maxTextLength: 16384,
|
|
26
|
+
maxUrlLength: 2048
|
|
27
|
+
};
|
|
28
|
+
var sha256HexSchema = z.string().regex(/^[0-9a-f]{64}$/u);
|
|
29
|
+
var sourceIdSchema = z.string().min(1).max(128);
|
|
30
|
+
var boundedIdentitySchema = z.string().min(1).max(512).refine(hasNoControlCharacters, {
|
|
31
|
+
message: "Expected no control characters"
|
|
32
|
+
});
|
|
33
|
+
var staticResolvedAttributionPositionSchema = z.enum([
|
|
34
|
+
"top-left",
|
|
35
|
+
"top-right",
|
|
36
|
+
"bottom-left",
|
|
37
|
+
"bottom-right"
|
|
38
|
+
]);
|
|
39
|
+
var staticAttributionSegmentSchema = z.discriminatedUnion("kind", [
|
|
40
|
+
z.object({
|
|
41
|
+
kind: z.literal("text"),
|
|
42
|
+
text: z.string().min(1).max(staticAttributionLimits.maxTextLength).refine(hasNoControlCharacters, { message: "Attribution text contains control characters" })
|
|
43
|
+
}).strict(),
|
|
44
|
+
z.object({
|
|
45
|
+
kind: z.literal("link"),
|
|
46
|
+
label: z.string().min(1).max(staticAttributionLimits.maxTextLength).refine(hasNoControlCharacters, {
|
|
47
|
+
message: "Attribution link label contains control characters"
|
|
48
|
+
}),
|
|
49
|
+
url: z.string().trim().url().max(staticAttributionLimits.maxUrlLength).refine(isSafeHttpUrl, {
|
|
50
|
+
message: "Expected an http(s) URL without credentials or a fragment"
|
|
51
|
+
})
|
|
52
|
+
}).strict()
|
|
53
|
+
]);
|
|
54
|
+
var attributionSourceSchema = z.object({
|
|
55
|
+
sourceId: sourceIdSchema,
|
|
56
|
+
sourceSelectionIdentity: boundedIdentitySchema
|
|
57
|
+
}).strict();
|
|
58
|
+
var attributionSourcesSchema = z.array(attributionSourceSchema).min(1).max(18).refine(isUniqueSortedSources, {
|
|
59
|
+
message: "Attribution sources must be unique and sorted"
|
|
60
|
+
});
|
|
61
|
+
var staticAttributionProvenanceSchema = z.discriminatedUnion("authority", [
|
|
62
|
+
z.object({
|
|
63
|
+
authority: z.literal("platform-notice"),
|
|
64
|
+
noticeId: boundedIdentitySchema,
|
|
65
|
+
noticeSha256: sha256HexSchema,
|
|
66
|
+
resource: z.enum(["terrain", "world"]),
|
|
67
|
+
sources: attributionSourcesSchema
|
|
68
|
+
}).strict(),
|
|
69
|
+
z.object({
|
|
70
|
+
authority: z.literal("team-declared"),
|
|
71
|
+
sources: z.array(
|
|
72
|
+
attributionSourceSchema.extend({
|
|
73
|
+
mapRevision: boundedIdentitySchema,
|
|
74
|
+
resourceId: boundedIdentitySchema
|
|
75
|
+
}).strict()
|
|
76
|
+
).min(1).max(18).refine(isUniqueSortedSources, {
|
|
77
|
+
message: "Attribution sources must be unique and sorted"
|
|
78
|
+
})
|
|
79
|
+
}).strict()
|
|
80
|
+
]);
|
|
81
|
+
var staticAttributionEntrySchema = z.object({
|
|
82
|
+
authority: z.enum(["platform-notice", "team-declared"]),
|
|
83
|
+
provenance: staticAttributionProvenanceSchema,
|
|
84
|
+
segments: z.array(staticAttributionSegmentSchema).min(1).max(17)
|
|
85
|
+
}).strict().superRefine((entry, context) => {
|
|
86
|
+
if (entry.authority !== entry.provenance.authority) {
|
|
87
|
+
context.addIssue({
|
|
88
|
+
code: "custom",
|
|
89
|
+
message: "Attribution entry authority must match its provenance",
|
|
90
|
+
path: ["authority"]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const links = entry.segments.filter((segment) => segment.kind === "link").length;
|
|
94
|
+
if (links > staticAttributionLimits.maxLinksPerEntry) {
|
|
95
|
+
context.addIssue({
|
|
96
|
+
code: "custom",
|
|
97
|
+
message: `Attribution entries can contain at most ${staticAttributionLimits.maxLinksPerEntry} links`,
|
|
98
|
+
path: ["segments"]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
var staticAttributionPlanSchemaV1 = z.object({
|
|
103
|
+
entries: z.array(staticAttributionEntrySchema).max(staticAttributionLimits.maxEntries),
|
|
104
|
+
mode: z.enum(["embedded", "external"]),
|
|
105
|
+
position: z.union([z.literal("auto"), staticResolvedAttributionPositionSchema, z.null()]),
|
|
106
|
+
schemaVersion: z.literal(staticAttributionPlanSchemaVersion)
|
|
107
|
+
}).strict().superRefine((plan, context) => {
|
|
108
|
+
if (plan.mode === "external" && plan.position !== null || plan.mode === "embedded" && plan.position === null) {
|
|
109
|
+
context.addIssue({
|
|
110
|
+
code: "custom",
|
|
111
|
+
message: "Attribution position must match its mode",
|
|
112
|
+
path: ["position"]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (jsonByteLength(plan) > staticAttributionLimits.maxBytes) {
|
|
116
|
+
context.addIssue({
|
|
117
|
+
code: "custom",
|
|
118
|
+
message: `Attribution plan exceeds ${staticAttributionLimits.maxBytes} bytes`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
var staticRenderManifestBaseShape = {
|
|
123
|
+
autoFit: staticAutoFitPlanSchema.optional(),
|
|
124
|
+
mapId: z.string().trim().min(1).max(128),
|
|
125
|
+
rendererVersion: z.string().trim().min(1).max(64),
|
|
126
|
+
styleId: z.string().trim().min(1).max(128).optional(),
|
|
127
|
+
styleRevision: z.string().trim().min(1).max(128),
|
|
128
|
+
styleUrl: z.string().trim().url().max(512).refine(isSafeHttpUrl, {
|
|
129
|
+
message: "Expected an http(s) style URL without credentials or a fragment"
|
|
130
|
+
})
|
|
131
|
+
};
|
|
132
|
+
var staticSceneSchemaV1 = staticSceneSchema.omit({ attribution: true });
|
|
133
|
+
var staticRenderManifestV1Schema = z.object({
|
|
134
|
+
...staticRenderManifestBaseShape,
|
|
135
|
+
schemaVersion: z.literal(staticRendererSchemaVersionV1),
|
|
136
|
+
scene: staticSceneSchemaV1
|
|
137
|
+
});
|
|
138
|
+
var staticRenderManifestV2Schema = z.object({
|
|
139
|
+
...staticRenderManifestBaseShape,
|
|
140
|
+
attribution: staticAttributionPlanSchemaV1,
|
|
141
|
+
schemaVersion: z.literal(staticRendererSchemaVersion),
|
|
142
|
+
scene: staticSceneSchema
|
|
143
|
+
}).strict();
|
|
144
|
+
var staticRenderManifestSchema = z.discriminatedUnion("schemaVersion", [
|
|
145
|
+
staticRenderManifestV1Schema,
|
|
146
|
+
staticRenderManifestV2Schema
|
|
147
|
+
]);
|
|
148
|
+
function validateStaticRenderManifest(input) {
|
|
149
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
150
|
+
return { error: "manifest: Expected an object", ok: false };
|
|
151
|
+
}
|
|
152
|
+
const schemaVersion = input.schemaVersion;
|
|
153
|
+
if (schemaVersion !== staticRendererSchemaVersionV1 && schemaVersion !== staticRendererSchemaVersion) {
|
|
154
|
+
return { error: "manifest.schemaVersion: Unsupported renderer schema version", ok: false };
|
|
155
|
+
}
|
|
156
|
+
const rawScene = input.scene;
|
|
157
|
+
const sceneValidation = validateVersionedScene(rawScene, schemaVersion);
|
|
158
|
+
if (!sceneValidation.ok) return sceneValidation;
|
|
159
|
+
const schema = schemaVersion === staticRendererSchemaVersionV1 ? staticRenderManifestV1Schema : staticRenderManifestV2Schema;
|
|
160
|
+
const parsed = schema.safeParse({
|
|
161
|
+
...input,
|
|
162
|
+
scene: sceneValidation.scene
|
|
163
|
+
});
|
|
164
|
+
if (!parsed.success) return zodFailure(parsed.error);
|
|
165
|
+
const autoFit = expectedAutoFit(sceneValidation.scene);
|
|
166
|
+
if (autoFit === void 0 !== (parsed.data.autoFit === void 0) || autoFit !== void 0 && stableStringify(autoFit) !== stableStringify(parsed.data.autoFit)) {
|
|
167
|
+
return { error: "manifest.autoFit: Expected the exact plan derived from scene", ok: false };
|
|
168
|
+
}
|
|
169
|
+
if (schemaVersion === staticRendererSchemaVersion && (!("attribution" in parsed.data) || !attributionMatchesScene(parsed.data.attribution, sceneValidation.scene))) {
|
|
170
|
+
return {
|
|
171
|
+
error: "manifest.attribution: Expected the exact request resolved from scene",
|
|
172
|
+
ok: false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
manifest: {
|
|
177
|
+
...parsed.data,
|
|
178
|
+
...autoFit === void 0 ? {} : { autoFit },
|
|
179
|
+
scene: sceneValidation.scene
|
|
180
|
+
},
|
|
181
|
+
ok: true
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function createRenderManifestV1(input) {
|
|
185
|
+
const rawScene = staticSceneSchemaV1.parse(input.scene);
|
|
186
|
+
const scene = normalizeStaticScene(rawScene);
|
|
187
|
+
const autoFit = expectedAutoFit(scene);
|
|
188
|
+
return staticRenderManifestV1Schema.parse(
|
|
189
|
+
stripUndefined({
|
|
190
|
+
autoFit,
|
|
191
|
+
mapId: input.mapId,
|
|
192
|
+
rendererVersion: input.rendererVersion,
|
|
193
|
+
schemaVersion: staticRendererSchemaVersionV1,
|
|
194
|
+
scene,
|
|
195
|
+
styleId: input.styleId,
|
|
196
|
+
styleRevision: input.styleRevision,
|
|
197
|
+
styleUrl: input.styleUrl
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
var createRenderManifest = createRenderManifestV1;
|
|
202
|
+
function createRenderManifestV2(input) {
|
|
203
|
+
const scene = normalizeStaticScene(input.scene);
|
|
204
|
+
const autoFit = expectedAutoFit(scene);
|
|
205
|
+
return staticRenderManifestV2Schema.parse(
|
|
206
|
+
stripUndefined({
|
|
207
|
+
attribution: input.attribution,
|
|
208
|
+
autoFit,
|
|
209
|
+
mapId: input.mapId,
|
|
210
|
+
rendererVersion: input.rendererVersion,
|
|
211
|
+
schemaVersion: staticRendererSchemaVersion,
|
|
212
|
+
scene,
|
|
213
|
+
styleId: input.styleId,
|
|
214
|
+
styleRevision: input.styleRevision,
|
|
215
|
+
styleUrl: input.styleUrl
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
async function hashRenderManifest(manifest) {
|
|
220
|
+
const validation = validateStaticRenderManifest(manifest);
|
|
221
|
+
if (!validation.ok) throw new Error(`Invalid Tileflow render manifest: ${validation.error}`);
|
|
222
|
+
return hashStableValue(validation.manifest);
|
|
223
|
+
}
|
|
224
|
+
async function hashStaticSceneRequest(scene) {
|
|
225
|
+
const validation = validateStaticScene(scene);
|
|
226
|
+
if (!validation.ok) throw new Error(`Invalid Tileflow static scene: ${validation.error}`);
|
|
227
|
+
return hashStableValue(validation.scene);
|
|
228
|
+
}
|
|
229
|
+
function validateVersionedScene(input, version) {
|
|
230
|
+
if (version === 1 && input && typeof input === "object" && !Array.isArray(input)) {
|
|
231
|
+
const { attribution: _attribution, ...legacy } = input;
|
|
232
|
+
return validateStaticScene(legacy);
|
|
233
|
+
}
|
|
234
|
+
return validateStaticScene(input);
|
|
235
|
+
}
|
|
236
|
+
function attributionMatchesScene(plan, scene) {
|
|
237
|
+
const request = scene.attribution;
|
|
238
|
+
const expectedMode = request?.mode === "external" ? "external" : "embedded";
|
|
239
|
+
const expectedPosition = expectedMode === "external" ? null : request && "position" in request ? request.position ?? "auto" : "auto";
|
|
240
|
+
return plan.mode === expectedMode && plan.position === expectedPosition;
|
|
241
|
+
}
|
|
242
|
+
function expectedAutoFit(scene) {
|
|
243
|
+
if (scene.camera.type !== "auto") return void 0;
|
|
244
|
+
const analysis = analyzeStaticAutoFit(scene);
|
|
245
|
+
if (!analysis.ok) throw new Error(`Invalid Tileflow auto-fit scene: ${analysis.error}`);
|
|
246
|
+
return analysis.plan;
|
|
247
|
+
}
|
|
248
|
+
function hasNoControlCharacters(value) {
|
|
249
|
+
return !Array.from(value).some((character) => {
|
|
250
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
251
|
+
return codePoint <= 31 || codePoint === 127;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function isUniqueSortedSources(values) {
|
|
255
|
+
return values.every(
|
|
256
|
+
(value, index) => index === 0 || compareCodeUnits(values[index - 1].sourceId, value.sourceId) < 0
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
function compareCodeUnits(left, right) {
|
|
260
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
261
|
+
}
|
|
262
|
+
function zodFailure(error) {
|
|
263
|
+
return {
|
|
264
|
+
error: error.issues.map((issue) => `${issue.path.join(".") || "manifest"}: ${issue.message}`).join("; "),
|
|
265
|
+
ok: false
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export {
|
|
270
|
+
staticRendererSchemaVersionV1,
|
|
271
|
+
staticRendererSchemaVersion,
|
|
272
|
+
staticAttributionPlanSchemaVersion,
|
|
273
|
+
staticAttributionLimits,
|
|
274
|
+
staticAttributionSegmentSchema,
|
|
275
|
+
staticAttributionProvenanceSchema,
|
|
276
|
+
staticAttributionEntrySchema,
|
|
277
|
+
staticAttributionPlanSchemaV1,
|
|
278
|
+
staticRenderManifestV1Schema,
|
|
279
|
+
staticRenderManifestV2Schema,
|
|
280
|
+
staticRenderManifestSchema,
|
|
281
|
+
validateStaticRenderManifest,
|
|
282
|
+
createRenderManifestV1,
|
|
283
|
+
createRenderManifest,
|
|
284
|
+
createRenderManifestV2,
|
|
285
|
+
hashRenderManifest,
|
|
286
|
+
hashStaticSceneRequest
|
|
287
|
+
};
|