@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 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: 1200, height: 800},
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: `createRenderManifest`, `validateStaticRenderManifest`,
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 a Static
88
- Maps renderer. Its top-level `mapId` identifies that resolved destination while `scene.map` retains
89
- the portable map name supplied by the application. It is unrelated to the runtime delivery manifest
90
- generated by application build or deploy workflows.
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`. A live
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 the immutable PNG consumes
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-MGQCHCU3.js";
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({
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  normalizeStaticOverlay,
3
3
  roundNumber
4
- } from "./chunk-MGQCHCU3.js";
4
+ } from "./chunk-NM2JSFUF.js";
5
5
 
6
6
  // src/overlays.ts
7
7
  function line(input) {
@@ -2,16 +2,18 @@ import {
2
2
  StaticMapRequestError,
3
3
  staticMapRequestErrorResponseSchema,
4
4
  validateStaticScene
5
- } from "./chunk-NHQK5UZ2.js";
5
+ } from "./chunk-EMCKSFZV.js";
6
6
  import {
7
7
  isSafeHttpUrl,
8
+ jsonByteLength,
8
9
  stableStringify
9
- } from "./chunk-MGQCHCU3.js";
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 = staticMapReadyResultSchema.safeParse(json);
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: 1280,
54
+ maxDimension: 2048,
55
55
  maxGeoJsonBytes: 96e3,
56
56
  maxOverlays: 24,
57
57
  maxPathCoordinates: 2e3,
58
- maxPhysicalPixels: 1280 * 1280,
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) => size.width * size.height * (size.dpr ?? 1) <= staticSceneLimits.maxPhysicalPixels,
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
+ };