@honua/react 0.1.7-beta.0 → 0.1.9-beta.0
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/control-plane/admin-client.d.ts +87 -0
- package/control-plane/admin-client.d.ts.map +1 -0
- package/control-plane/admin-client.js +169 -0
- package/control-plane/client.d.ts.map +1 -1
- package/control-plane/client.js +21 -3
- package/control-plane/commands/catalog.d.ts +133 -0
- package/control-plane/commands/catalog.d.ts.map +1 -0
- package/control-plane/commands/catalog.js +353 -0
- package/control-plane/commands/errors.d.ts +96 -0
- package/control-plane/commands/errors.d.ts.map +1 -0
- package/control-plane/commands/errors.js +130 -0
- package/control-plane/commands/identity.d.ts +71 -0
- package/control-plane/commands/identity.d.ts.map +1 -0
- package/control-plane/commands/identity.js +0 -0
- package/control-plane/commands/index.d.ts +39 -0
- package/control-plane/commands/index.d.ts.map +1 -0
- package/control-plane/commands/index.js +34 -0
- package/control-plane/commands/runtime.d.ts +146 -0
- package/control-plane/commands/runtime.d.ts.map +1 -0
- package/control-plane/commands/runtime.js +318 -0
- package/control-plane/commands/types.d.ts +226 -0
- package/control-plane/commands/types.d.ts.map +1 -0
- package/control-plane/commands/types.js +30 -0
- package/control-plane/commands/validate.d.ts +27 -0
- package/control-plane/commands/validate.d.ts.map +1 -0
- package/control-plane/commands/validate.js +132 -0
- package/control-plane/generated/admin-api.d.ts +30263 -0
- package/control-plane/generated/admin-api.d.ts.map +1 -0
- package/control-plane/generated/admin-api.js +5 -0
- package/control-plane/generated/admin-operations.d.ts +4053 -0
- package/control-plane/generated/admin-operations.d.ts.map +1 -0
- package/control-plane/generated/admin-operations.js +820 -0
- package/control-plane/index.d.ts +11 -0
- package/control-plane/index.d.ts.map +1 -1
- package/control-plane/index.js +8 -0
- package/control-plane/types.d.ts +14 -0
- package/control-plane/types.d.ts.map +1 -1
- package/core/client.js +1 -1
- package/core/credential-redaction.d.ts +163 -0
- package/core/credential-redaction.d.ts.map +1 -0
- package/core/credential-redaction.js +389 -0
- package/core/error-classifications.d.ts +15 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +15 -0
- package/core/error-code-registry.d.ts +15 -0
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +15 -0
- package/core/grpc-adapter.d.ts +2 -2
- package/core/grpc-adapter.d.ts.map +1 -1
- package/core/grpc-adapter.js +30 -6
- package/core/job-run-errors.d.ts +10 -0
- package/core/job-run-errors.d.ts.map +1 -0
- package/core/job-run-errors.js +14 -0
- package/core/job-run-lifecycle.d.ts +34 -0
- package/core/job-run-lifecycle.d.ts.map +1 -0
- package/core/job-run-lifecycle.js +193 -0
- package/core/ogc-processes.d.ts +38 -31
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +123 -173
- package/core/process-runner.js +15 -100
- package/core/surfaces.d.ts +2 -8
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +21 -112
- package/core/types.d.ts +7 -6
- package/core/types.d.ts.map +1 -1
- package/gen/geospatial/v1/common_pb.d.ts +514 -0
- package/gen/geospatial/v1/common_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/common_pb.js +335 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts +1186 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/execution_types_pb.js +396 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts +403 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/feature_service_pb.js +47 -0
- package/gen/geospatial/v1/index.d.ts +5 -0
- package/gen/geospatial/v1/index.d.ts.map +1 -0
- package/gen/geospatial/v1/index.js +9 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts +285 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/spatial_types_pb.js +58 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts +379 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.js +151 -0
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/projection.js +10 -1
- package/package.json +2 -2
- package/query-planner/grpc.d.ts +4 -4
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +11 -11
- package/query-planner/types.d.ts +4 -4
- package/query-planner/types.d.ts.map +1 -1
- package/runtime/errors.d.ts +1 -1
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +2 -0
- package/runtime/generated/map-package-schema-meta.d.ts +11 -0
- package/runtime/generated/map-package-schema-meta.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-meta.js +12 -0
- package/runtime/generated/map-package-schema-validator.d.ts +16 -0
- package/runtime/generated/map-package-schema-validator.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-validator.js +1422 -0
- package/runtime/index.d.ts +5 -1
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +6 -0
- package/runtime/map-package-export.d.ts +151 -0
- package/runtime/map-package-export.d.ts.map +1 -0
- package/runtime/map-package-export.js +636 -0
- package/runtime/map-package-validation.d.ts +21 -1
- package/runtime/map-package-validation.d.ts.map +1 -1
- package/runtime/map-package-validation.js +132 -95
- package/runtime/map-package.d.ts +80 -0
- package/runtime/map-package.d.ts.map +1 -1
- package/studio/chart-spec.d.ts +6 -6
- package/studio/chart-spec.d.ts.map +1 -1
- package/studio/chart-spec.js +6 -6
- package/studio/index.d.ts +17 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +15 -5
- package/studio/lifecycle-client.d.ts +262 -22
- package/studio/lifecycle-client.d.ts.map +1 -1
- package/studio/lifecycle-client.js +524 -24
- package/studio/lifecycle-errors.d.ts +8 -2
- package/studio/lifecycle-errors.d.ts.map +1 -1
- package/studio/lifecycle-errors.js +6 -1
- package/studio/lifecycle-types.d.ts +228 -8
- package/studio/lifecycle-types.d.ts.map +1 -1
- package/studio/publication-status.d.ts +55 -0
- package/studio/publication-status.d.ts.map +1 -0
- package/studio/publication-status.js +103 -0
- package/gen/honua/v1/feature_service_pb.d.ts +0 -918
- package/gen/honua/v1/feature_service_pb.d.ts.map +0 -1
- package/gen/honua/v1/feature_service_pb.js +0 -375
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable export / import for the canonical map artifact (honua-sdk-js#1426).
|
|
3
|
+
*
|
|
4
|
+
* `exportMapPackage` turns a {@link HonuaMapPackage} into a self-describing
|
|
5
|
+
* envelope that can cross a client boundary — written to disk by the CLI,
|
|
6
|
+
* returned by an MCP tool, handed to Studio — and `importMapPackage` reads one
|
|
7
|
+
* back. The round trip is the mechanism the issue's "same fixture, every
|
|
8
|
+
* client" acceptance criterion is built on, and it enforces the two properties
|
|
9
|
+
* that make an artifact safe to move at all:
|
|
10
|
+
*
|
|
11
|
+
* 1. **No embedded credentials.** Every string is passed through the SDK's one
|
|
12
|
+
* credential recognizer (`src/core/credential-redaction.ts`, shared with the
|
|
13
|
+
* component kit's export pipeline). Property names that read as secrets are
|
|
14
|
+
* dropped, URLs lose their userinfo and their signature/token query
|
|
15
|
+
* parameters, and the finished envelope is re-scanned before it is
|
|
16
|
+
* returned. A scan hit at that point is a hard failure, never a silent
|
|
17
|
+
* emit. `credentials: "reject"` turns the first layer into a hard failure
|
|
18
|
+
* too, for callers that want to be told rather than quietly cleaned up.
|
|
19
|
+
* 2. **No unbounded embedded data.** A map artifact is a *description* of
|
|
20
|
+
* sources, not a container for them. Inline `data:` payloads and inline
|
|
21
|
+
* GeoJSON bodies are measured and refused above an explicit budget, and the
|
|
22
|
+
* whole envelope is refused above a second one, so a package cannot become
|
|
23
|
+
* a de facto dataset that no client can afford to open.
|
|
24
|
+
* 3. **No claim that the map is hosted, saved, or published.** A location
|
|
25
|
+
* pointer is withheld on export and refused on import, on the envelope as
|
|
26
|
+
* well as on the package. This exporter deliberately holds *no* lifecycle
|
|
27
|
+
* information (see below), so it cannot tell an ephemeral preview from an
|
|
28
|
+
* immutable saved version; an artifact that cannot know it was published
|
|
29
|
+
* must not carry a URL that says it was. #1426's "preview is never reported
|
|
30
|
+
* as persisted or published" is therefore enforced by absence, at the
|
|
31
|
+
* bytes, rather than by a client-minted lifecycle flag that a producer
|
|
32
|
+
* could set wrongly.
|
|
33
|
+
*
|
|
34
|
+
* The rule is *name*-directed, not URL-shaped, and has two tiers.
|
|
35
|
+
* `publicationUrl`, `embedUrl`, `permalink` and their kin assert a
|
|
36
|
+
* publication whatever owns them, so they are refused at **any depth** —
|
|
37
|
+
* the wire format is `additionalProperties: true` everywhere, and a claim
|
|
38
|
+
* hidden under `mapPackage.metadata` is still a claim. The generic relation
|
|
39
|
+
* names — `links`, `self`, `href`, `url`, `uri` — assert one only at the
|
|
40
|
+
* **root** of the envelope or of the package, because below that they
|
|
41
|
+
* belong to whatever owns them: `mapSpec.sources.<id>.url` is a TileJSON
|
|
42
|
+
* endpoint, `mapSpec.sprite[i].url` is required by the schema,
|
|
43
|
+
* `locator.url` is where the *data* lives, `attribution[i].url` is a
|
|
44
|
+
* credit, and `legend[i].iconUrl` is a symbol. None of those says anything
|
|
45
|
+
* about this map having been saved or published, and all of them survive.
|
|
46
|
+
*
|
|
47
|
+
* The envelope stamps {@link mapPackageFingerprint} so an import can prove the
|
|
48
|
+
* bytes it read are the bytes that were written. That fingerprint is a
|
|
49
|
+
* transport integrity check and nothing more: it is deliberately NOT an
|
|
50
|
+
* identity, a version, or a content-addressed name. Stable identity, content
|
|
51
|
+
* hash, optimistic concurrency, and the map lifecycle (ephemeral preview →
|
|
52
|
+
* draft → immutable saved version → proposal → publication) belong to the
|
|
53
|
+
* canonical honua-server composition contract and reach the SDK through
|
|
54
|
+
* honua-sdk-js#1397 / #1398. Minting them here would make this the eighth
|
|
55
|
+
* competing map representation in a repository that already has seven, which
|
|
56
|
+
* is the problem #1426 exists to end.
|
|
57
|
+
*
|
|
58
|
+
* @module
|
|
59
|
+
*/
|
|
60
|
+
import { HonuaExportSafetyError, assertCredentialFreeExportBytes, assertCredentialFreeExportText, containsCredentialMaterial, isSensitiveExportKey, stripCredentialsFromUrl, } from "../core/credential-redaction.js";
|
|
61
|
+
import { HonuaMapPackageError } from "./errors.js";
|
|
62
|
+
import { mapPackageFingerprint } from "./map-package-fetch.js";
|
|
63
|
+
import { hasMapPackageDiagnosticErrors, validateMapPackage } from "./map-package-validation.js";
|
|
64
|
+
import { HONUA_MAP_PACKAGE_FORMAT_V1 } from "./map-package.js";
|
|
65
|
+
/** Envelope kind stamped on every exported map artifact. */
|
|
66
|
+
export const HONUA_MAP_PACKAGE_EXPORT_KIND_V1 = "honua.map-package.export.v1";
|
|
67
|
+
/** Default per-value budget for inline data. 256 KiB. */
|
|
68
|
+
export const DEFAULT_MAX_EMBEDDED_BYTES = 256 * 1024;
|
|
69
|
+
/** Default budget for the whole serialized envelope. 4 MiB. */
|
|
70
|
+
export const DEFAULT_MAX_PACKAGE_BYTES = 4 * 1024 * 1024;
|
|
71
|
+
/**
|
|
72
|
+
* Produces a portable, credential-free, size-bounded envelope for `pkg`.
|
|
73
|
+
*
|
|
74
|
+
* Throws {@link HonuaMapPackageError} when the package is invalid or exceeds a
|
|
75
|
+
* data budget, and {@link HonuaExportSafetyError} when credential material
|
|
76
|
+
* survives sanitization (or is present at all under `credentials: "reject"`).
|
|
77
|
+
*/
|
|
78
|
+
export function exportMapPackage(pkg, options = {}) {
|
|
79
|
+
const packageId = typeof pkg?.mapPackageId === "string" ? pkg.mapPackageId : undefined;
|
|
80
|
+
const maxEmbeddedBytes = normalizeBudget(options.maxEmbeddedBytes, DEFAULT_MAX_EMBEDDED_BYTES);
|
|
81
|
+
const maxPackageBytes = normalizeBudget(options.maxPackageBytes, DEFAULT_MAX_PACKAGE_BYTES);
|
|
82
|
+
const rejectCredentials = options.credentials === "reject";
|
|
83
|
+
if (options.allowInvalid !== true) {
|
|
84
|
+
const validation = validateMapPackage(pkg);
|
|
85
|
+
if (hasMapPackageDiagnosticErrors(validation.diagnostics)) {
|
|
86
|
+
throw new HonuaMapPackageError("MapPackage does not validate; refusing to export a non-portable artifact.", {
|
|
87
|
+
packageId,
|
|
88
|
+
stage: "export",
|
|
89
|
+
detail: { diagnostics: validation.diagnostics.filter((d) => d.severity === "error") },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const redactions = [];
|
|
94
|
+
const sanitizedOrMarker = sanitizeValue(pkg, "", {
|
|
95
|
+
redactions,
|
|
96
|
+
maxEmbeddedBytes,
|
|
97
|
+
packageId,
|
|
98
|
+
rejectCredentials,
|
|
99
|
+
});
|
|
100
|
+
if (sanitizedOrMarker === undefined || typeof sanitizedOrMarker !== "object") {
|
|
101
|
+
throw new HonuaMapPackageError("MapPackage did not survive export sanitization.", {
|
|
102
|
+
packageId,
|
|
103
|
+
stage: "export",
|
|
104
|
+
detail: { redactions },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Preview honesty (#1426). Runs on the sanitized body so its redactions are
|
|
108
|
+
// addressed by paths that exist in the emitted document.
|
|
109
|
+
const sanitized = withholdPublicationPointers(sanitizedOrMarker, redactions);
|
|
110
|
+
// Sanitization *removes* values, so a package that validated on the way in
|
|
111
|
+
// can be schema-invalid on the way out: a credential-shaped
|
|
112
|
+
// `attribution[0].text` is withheld while its containing object remains, and
|
|
113
|
+
// `text` is required. Validating the pre-sanitization package alone would let
|
|
114
|
+
// the exporter emit an artifact its own `importMapPackage` rejects, which is
|
|
115
|
+
// precisely the "not portable" case the pre-check exists to prevent.
|
|
116
|
+
if (options.allowInvalid !== true) {
|
|
117
|
+
const postSanitization = validateMapPackage(sanitized);
|
|
118
|
+
if (hasMapPackageDiagnosticErrors(postSanitization.diagnostics)) {
|
|
119
|
+
throw new HonuaMapPackageError("Withholding credential material left a MapPackage that no longer satisfies its schema; refusing to export an artifact its own importer would reject.", {
|
|
120
|
+
packageId,
|
|
121
|
+
stage: "export",
|
|
122
|
+
detail: {
|
|
123
|
+
diagnostics: postSanitization.diagnostics.filter((d) => d.severity === "error"),
|
|
124
|
+
redactions,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const exportedAt = normalizeTimestamp(options.exportedAt);
|
|
130
|
+
const envelope = {
|
|
131
|
+
kind: HONUA_MAP_PACKAGE_EXPORT_KIND_V1,
|
|
132
|
+
format: HONUA_MAP_PACKAGE_FORMAT_V1,
|
|
133
|
+
exportedAt,
|
|
134
|
+
fingerprint: mapPackageFingerprint(sanitized),
|
|
135
|
+
mapPackage: sanitized,
|
|
136
|
+
redactions,
|
|
137
|
+
};
|
|
138
|
+
const serialized = JSON.stringify(envelope);
|
|
139
|
+
assertWithinPackageBudget(serialized, maxPackageBytes, packageId, "export");
|
|
140
|
+
// Layer two: the finished bytes are re-scanned. Reaching here with credential
|
|
141
|
+
// material means the projection above has a bug, and failing closed is the
|
|
142
|
+
// only honest response.
|
|
143
|
+
assertCredentialFreeExportText(scrubDataUriPayloads(serialized), "Exported MapPackage");
|
|
144
|
+
return envelope;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Reads an envelope produced by {@link exportMapPackage} back into a package.
|
|
148
|
+
*
|
|
149
|
+
* Re-applies all three guarantees rather than trusting the envelope: the file
|
|
150
|
+
* may have been hand-edited, produced by an older exporter, or supplied by
|
|
151
|
+
* someone else entirely. A credential-bearing, over-budget, or
|
|
152
|
+
* publication-asserting envelope is refused.
|
|
153
|
+
*/
|
|
154
|
+
export function importMapPackage(value, options = {}) {
|
|
155
|
+
if (!isRecord(value)) {
|
|
156
|
+
throw new HonuaMapPackageError("MapPackage export must be a JSON object.", { stage: "import" });
|
|
157
|
+
}
|
|
158
|
+
if (value.kind !== HONUA_MAP_PACKAGE_EXPORT_KIND_V1) {
|
|
159
|
+
throw new HonuaMapPackageError(`MapPackage export kind must be "${HONUA_MAP_PACKAGE_EXPORT_KIND_V1}".`, {
|
|
160
|
+
stage: "import",
|
|
161
|
+
detail: { expected: HONUA_MAP_PACKAGE_EXPORT_KIND_V1, received: value.kind },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (value.format !== HONUA_MAP_PACKAGE_FORMAT_V1) {
|
|
165
|
+
throw new HonuaMapPackageError(`MapPackage export format must be "${HONUA_MAP_PACKAGE_FORMAT_V1}".`, {
|
|
166
|
+
stage: "import",
|
|
167
|
+
detail: { expected: HONUA_MAP_PACKAGE_FORMAT_V1, received: value.format },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!isRecord(value.mapPackage)) {
|
|
171
|
+
throw new HonuaMapPackageError("MapPackage export is missing its mapPackage body.", { stage: "import" });
|
|
172
|
+
}
|
|
173
|
+
const mapPackage = value.mapPackage;
|
|
174
|
+
const packageId = typeof mapPackage.mapPackageId === "string" ? mapPackage.mapPackageId : undefined;
|
|
175
|
+
const maxEmbeddedBytes = normalizeBudget(options.maxEmbeddedBytes, DEFAULT_MAX_EMBEDDED_BYTES);
|
|
176
|
+
const maxPackageBytes = normalizeBudget(options.maxPackageBytes, DEFAULT_MAX_PACKAGE_BYTES);
|
|
177
|
+
const serialized = JSON.stringify(value);
|
|
178
|
+
assertWithinPackageBudget(serialized, maxPackageBytes, packageId, "import");
|
|
179
|
+
assertNoPublicationPointers(value, packageId);
|
|
180
|
+
// The deep walk, not the shallow one: the top-level `mapPackage` object has
|
|
181
|
+
// no `data`/`features` key of its own, so the shallow check passed an inline
|
|
182
|
+
// body the exporter refuses. Both guards are load-bearing and independent --
|
|
183
|
+
// one refuses a forged publication claim, the other an oversized payload.
|
|
184
|
+
assertEmbeddedDataWithinBudgetDeep(mapPackage, "mapPackage", maxEmbeddedBytes, packageId);
|
|
185
|
+
assertCredentialFreeExportText(scrubDataUriPayloads(serialized), "Imported MapPackage");
|
|
186
|
+
const fingerprint = mapPackageFingerprint(mapPackage);
|
|
187
|
+
if (options.skipFingerprintCheck !== true) {
|
|
188
|
+
// A missing stamp is a failed integrity check, not an absent one.
|
|
189
|
+
// `fingerprint` is required by `HonuaMapPackageExport`, so an envelope
|
|
190
|
+
// without one was truncated, hand-edited, or written by something that is
|
|
191
|
+
// not this exporter — and treating that as "nothing to compare" let a
|
|
192
|
+
// mutated body through simply by deleting the field that would have caught
|
|
193
|
+
// it. Callers that knowingly rewrote the package pass `skipFingerprintCheck`.
|
|
194
|
+
if (typeof value.fingerprint !== "string" || value.fingerprint.length === 0) {
|
|
195
|
+
throw new HonuaMapPackageError("MapPackage export is missing its fingerprint stamp; pass skipFingerprintCheck to accept an unstamped envelope.", {
|
|
196
|
+
packageId,
|
|
197
|
+
stage: "import",
|
|
198
|
+
detail: { stamped: value.fingerprint, recomputed: fingerprint },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (value.fingerprint !== fingerprint) {
|
|
202
|
+
throw new HonuaMapPackageError("MapPackage export fingerprint does not match its body.", {
|
|
203
|
+
packageId,
|
|
204
|
+
stage: "import",
|
|
205
|
+
detail: { stamped: value.fingerprint, recomputed: fingerprint },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const validation = validateMapPackage(mapPackage);
|
|
210
|
+
if (options.allowInvalid !== true && hasMapPackageDiagnosticErrors(validation.diagnostics)) {
|
|
211
|
+
throw new HonuaMapPackageError("Imported MapPackage does not validate.", {
|
|
212
|
+
packageId,
|
|
213
|
+
stage: "import",
|
|
214
|
+
detail: { diagnostics: validation.diagnostics.filter((d) => d.severity === "error") },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
mapPackage,
|
|
219
|
+
fingerprint,
|
|
220
|
+
redactions: Array.isArray(value.redactions) ? value.redactions : [],
|
|
221
|
+
diagnostics: validation.diagnostics,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// ── preview honesty ──────────────────────────────────────────────────────
|
|
225
|
+
/**
|
|
226
|
+
* Property names that assert a *publication of the artifact* wherever they
|
|
227
|
+
* appear.
|
|
228
|
+
*
|
|
229
|
+
* These words have exactly one meaning: here is where this thing is hosted,
|
|
230
|
+
* published, shared, or embedded. No part of a map *description* needs one —
|
|
231
|
+
* not a source, not a sprite, not a legend swatch — so they are refused at any
|
|
232
|
+
* depth, inside any container the open wire format permits. That is what
|
|
233
|
+
* closes `mapPackage.metadata.publicationUrl` and every variation on it.
|
|
234
|
+
*/
|
|
235
|
+
const PUBLICATION_POINTER_KEYS_ANYWHERE = new Set([
|
|
236
|
+
"publicationurl",
|
|
237
|
+
"publishedurl",
|
|
238
|
+
"publishurl",
|
|
239
|
+
"publicurl",
|
|
240
|
+
"portalurl",
|
|
241
|
+
"itemurl",
|
|
242
|
+
"viewerurl",
|
|
243
|
+
"shareurl",
|
|
244
|
+
"embedurl",
|
|
245
|
+
"permalink",
|
|
246
|
+
"weburl",
|
|
247
|
+
"selflink",
|
|
248
|
+
]);
|
|
249
|
+
/**
|
|
250
|
+
* Generic relation names that assert a publication *only at the root of the
|
|
251
|
+
* thing they describe*.
|
|
252
|
+
*
|
|
253
|
+
* `url` is the clearest case: at the root of a package it means "this map is
|
|
254
|
+
* at ...", but `mapSpec.sources.<id>.url` is a TileJSON endpoint,
|
|
255
|
+
* `mapSpec.sprite[i].url` is required by the schema, `locator.url` is where the
|
|
256
|
+
* *data* lives, and `attribution[i].url` is a credit link. Position, not
|
|
257
|
+
* spelling, is what makes these a claim about the artifact, so they are refused
|
|
258
|
+
* at the package root and at the envelope root and left alone below.
|
|
259
|
+
*
|
|
260
|
+
* The consequence is deliberate: a generic pointer buried in an open container
|
|
261
|
+
* (`widgets[i].config.links`) is not refused, because at that depth it belongs
|
|
262
|
+
* to whatever owns it. Refusing it there would reject legitimate artifacts on
|
|
263
|
+
* the strength of a word rather than a claim, and the names that *are* a claim
|
|
264
|
+
* on their own are already handled above.
|
|
265
|
+
*/
|
|
266
|
+
const PUBLICATION_POINTER_KEYS_AT_ROOT = new Set(["links", "link", "self", "href", "url", "uri"]);
|
|
267
|
+
/**
|
|
268
|
+
* Comparison form: case-insensitive and separator-insensitive, so
|
|
269
|
+
* `publicationUrl`, `publication_url`, and `PUBLICATION-URL` are one name.
|
|
270
|
+
*/
|
|
271
|
+
function publicationPointerName(key) {
|
|
272
|
+
return key.toLowerCase().replace(/[-_\s]/g, "");
|
|
273
|
+
}
|
|
274
|
+
function isPublicationPointerKey(key, atRoot) {
|
|
275
|
+
const name = publicationPointerName(key);
|
|
276
|
+
return PUBLICATION_POINTER_KEYS_ANYWHERE.has(name) || (atRoot && PUBLICATION_POINTER_KEYS_AT_ROOT.has(name));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Collect the paths of every location pointer in `value`.
|
|
280
|
+
*
|
|
281
|
+
* `atRoot` applies to the properties of `value` itself; everything reached
|
|
282
|
+
* through them is below a root and is judged by the "anywhere" names alone. A
|
|
283
|
+
* withheld subtree is not descended into: the whole claim is going, and naming
|
|
284
|
+
* its interior would only add noise.
|
|
285
|
+
*
|
|
286
|
+
* Safe against cycles by construction at both call sites — the exporter walks a
|
|
287
|
+
* tree `sanitizeValue` has already rebuilt, and the importer walks a value
|
|
288
|
+
* `JSON.stringify` has already survived.
|
|
289
|
+
*/
|
|
290
|
+
function collectPublicationPointerPaths(value, path, atRoot, out) {
|
|
291
|
+
if (Array.isArray(value)) {
|
|
292
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
293
|
+
collectPublicationPointerPaths(value[i], `${path}[${i}]`, false, out);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!isRecord(value))
|
|
298
|
+
return;
|
|
299
|
+
for (const [key, child] of Object.entries(value)) {
|
|
300
|
+
const childPath = path.length === 0 ? key : `${path}.${key}`;
|
|
301
|
+
if (isPublicationPointerKey(key, atRoot)) {
|
|
302
|
+
out.push(childPath);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
collectPublicationPointerPaths(child, childPath, false, out);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** Rebuild `value` with every location pointer removed. Mirrors the collector. */
|
|
309
|
+
function stripPublicationPointers(value, atRoot) {
|
|
310
|
+
if (Array.isArray(value))
|
|
311
|
+
return value.map((item) => stripPublicationPointers(item, false));
|
|
312
|
+
if (!isRecord(value))
|
|
313
|
+
return value;
|
|
314
|
+
const out = {};
|
|
315
|
+
for (const [key, child] of Object.entries(value)) {
|
|
316
|
+
if (isPublicationPointerKey(key, atRoot))
|
|
317
|
+
continue;
|
|
318
|
+
out[key] = stripPublicationPointers(child, false);
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Rebuild `pkg` without its location pointers, recording each withheld path on
|
|
324
|
+
* `redactions`.
|
|
325
|
+
*
|
|
326
|
+
* Deliberately not routed through {@link ExportMapPackageOptions.credentials}
|
|
327
|
+
* `"reject"`: a publication pointer is not a secret that leaked, it is a claim
|
|
328
|
+
* the artifact is not entitled to make. Dropping it always — rather than
|
|
329
|
+
* refusing the export — keeps a package that a server happened to stamp with a
|
|
330
|
+
* self-link exportable, while making it impossible for those bytes to say the
|
|
331
|
+
* map is published.
|
|
332
|
+
*
|
|
333
|
+
* The exporter withholds exactly what {@link importMapPackage} refuses. That
|
|
334
|
+
* equality is load-bearing: an exporter that emitted an artifact its own
|
|
335
|
+
* importer rejects would not be producing a portable artifact at all, which is
|
|
336
|
+
* the same invariant the post-sanitization validation below exists to keep.
|
|
337
|
+
*/
|
|
338
|
+
function withholdPublicationPointers(pkg, redactions) {
|
|
339
|
+
const withheld = [];
|
|
340
|
+
collectPublicationPointerPaths(pkg, "", true, withheld);
|
|
341
|
+
if (withheld.length === 0)
|
|
342
|
+
return pkg;
|
|
343
|
+
for (const path of withheld)
|
|
344
|
+
redactions.push({ path, reason: "publication-pointer" });
|
|
345
|
+
return stripPublicationPointers(pkg, true);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Refuse an envelope that asserts a publication anywhere.
|
|
349
|
+
*
|
|
350
|
+
* The envelope and the package it carries are each an artifact with its own
|
|
351
|
+
* root, so both get the root treatment; everything below either one is judged
|
|
352
|
+
* by the unambiguous names alone. `exportMapPackage` never emits any of them,
|
|
353
|
+
* so reaching this means the file was hand-edited or written by something else
|
|
354
|
+
* — exactly the case where a reader would otherwise believe a URL that nothing
|
|
355
|
+
* in the artifact can substantiate.
|
|
356
|
+
*/
|
|
357
|
+
function assertNoPublicationPointers(envelope, packageId) {
|
|
358
|
+
const offending = [];
|
|
359
|
+
// Split so that `mapPackage` is walked as a root in its own right rather
|
|
360
|
+
// than as an ordinary property of the envelope.
|
|
361
|
+
const { mapPackage, ...envelopeRest } = envelope;
|
|
362
|
+
collectPublicationPointerPaths(envelopeRest, "", true, offending);
|
|
363
|
+
collectPublicationPointerPaths(mapPackage, "mapPackage", true, offending);
|
|
364
|
+
if (offending.length === 0)
|
|
365
|
+
return;
|
|
366
|
+
offending.sort();
|
|
367
|
+
throw new HonuaMapPackageError(`MapPackage export carries location pointer(s): ${offending.join(", ")}. A portable map artifact describes a map; it cannot assert where that map is hosted or published, because nothing in it records whether the map was ever persisted.`, { packageId, stage: "import", detail: { properties: offending } });
|
|
368
|
+
}
|
|
369
|
+
const REDACTED_MARKER = Symbol("honua.export.redacted");
|
|
370
|
+
/**
|
|
371
|
+
* Recursively rebuilds `value` without credential material.
|
|
372
|
+
*
|
|
373
|
+
* A rebuild rather than a mutation: the caller's package is never modified,
|
|
374
|
+
* and prototype-polluting keys have no path into the output because only
|
|
375
|
+
* own enumerable string keys are copied onto a null-prototype-free literal.
|
|
376
|
+
*/
|
|
377
|
+
function sanitizeValue(value, path, ctx) {
|
|
378
|
+
// `undefined` is not representable in JSON and is not a withheld value, so
|
|
379
|
+
// it is dropped without a redaction entry — `JSON.stringify` would have
|
|
380
|
+
// dropped it anyway, and recording it would make the audit trail noise.
|
|
381
|
+
if (value === undefined)
|
|
382
|
+
return REDACTED_MARKER;
|
|
383
|
+
if (value === null || typeof value === "boolean" || typeof value === "number")
|
|
384
|
+
return value;
|
|
385
|
+
if (typeof value === "string")
|
|
386
|
+
return sanitizeString(value, path, ctx);
|
|
387
|
+
if (Array.isArray(value)) {
|
|
388
|
+
const out = [];
|
|
389
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
390
|
+
const item = sanitizeValue(value[i], `${path}[${i}]`, ctx);
|
|
391
|
+
// A redacted array element becomes `null` rather than shifting every
|
|
392
|
+
// later index: `sourceBindings[3]` must keep meaning the same binding.
|
|
393
|
+
out.push(item === REDACTED_MARKER ? null : item);
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
if (typeof value !== "object") {
|
|
398
|
+
// Functions, symbols, bigints: not representable in a portable artifact.
|
|
399
|
+
record(ctx, path, "unsupported-value");
|
|
400
|
+
return REDACTED_MARKER;
|
|
401
|
+
}
|
|
402
|
+
assertEmbeddedDataWithinBudget(value, path, ctx.maxEmbeddedBytes, ctx.packageId, "export");
|
|
403
|
+
const out = {};
|
|
404
|
+
for (const [key, child] of Object.entries(value)) {
|
|
405
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
406
|
+
record(ctx, `${path}.${key}`, "unsupported-value");
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const childPath = path.length === 0 ? key : `${path}.${key}`;
|
|
410
|
+
if (isSensitiveExportKey(key)) {
|
|
411
|
+
failOrRecord(ctx, childPath, "sensitive-key", `Property "${key}" names a credential.`);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const sanitizedChild = sanitizeValue(child, childPath, ctx);
|
|
415
|
+
if (sanitizedChild === REDACTED_MARKER)
|
|
416
|
+
continue;
|
|
417
|
+
out[key] = sanitizedChild;
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
function sanitizeString(value, path, ctx) {
|
|
422
|
+
if (value.length === 0)
|
|
423
|
+
return value;
|
|
424
|
+
if (isDataUri(value)) {
|
|
425
|
+
// Bounded and scanned rather than dropped: an inline sprite or legend icon
|
|
426
|
+
// is legitimate content. The payload is decoded and scanned as bytes,
|
|
427
|
+
// because a token hidden in a data: URI is exactly as disclosed as one in
|
|
428
|
+
// plain text, and scanning the encoded form as *text* either produces a
|
|
429
|
+
// false positive on every base64 icon or misses a percent-escaped secret.
|
|
430
|
+
if (byteLength(value) > ctx.maxEmbeddedBytes) {
|
|
431
|
+
throw new HonuaMapPackageError(`Inline data at ${path} exceeds the ${ctx.maxEmbeddedBytes}-byte export budget; a portable map package references data, it does not carry it.`, {
|
|
432
|
+
packageId: ctx.packageId,
|
|
433
|
+
stage: "export",
|
|
434
|
+
detail: { path, bytes: byteLength(value), maxEmbeddedBytes: ctx.maxEmbeddedBytes },
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const decoded = decodeDataUri(value);
|
|
438
|
+
if (decoded === undefined) {
|
|
439
|
+
// Unscannable, therefore not provably credential-free. Withholding it is
|
|
440
|
+
// the fail-closed answer; emitting it would rely on a whole-envelope scan
|
|
441
|
+
// that `scrubDataUriPayloads` has already blinded to this value.
|
|
442
|
+
failOrRecord(ctx, path, "unsupported-value", `Inline data at ${path} has an undecodable payload.`);
|
|
443
|
+
return REDACTED_MARKER;
|
|
444
|
+
}
|
|
445
|
+
assertCredentialFreeExportBytes(decoded, `Inline data at ${path}`);
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
if (looksLikeUrl(value)) {
|
|
449
|
+
const stripped = stripCredentialsFromUrl(value);
|
|
450
|
+
if (stripped.url === undefined) {
|
|
451
|
+
failOrRecord(ctx, path, stripped.reason ?? "credential-pattern", `URL at ${path} cannot be exported safely.`);
|
|
452
|
+
return REDACTED_MARKER;
|
|
453
|
+
}
|
|
454
|
+
if (stripped.reason !== undefined) {
|
|
455
|
+
failOrRecord(ctx, path, stripped.reason, `URL at ${path} carried credential material.`);
|
|
456
|
+
}
|
|
457
|
+
return stripped.url;
|
|
458
|
+
}
|
|
459
|
+
if (containsCredentialMaterial(value)) {
|
|
460
|
+
failOrRecord(ctx, path, "credential-pattern", `Value at ${path} matches a credential shape.`);
|
|
461
|
+
return REDACTED_MARKER;
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
function failOrRecord(ctx, path, reason, message) {
|
|
466
|
+
if (ctx.rejectCredentials) {
|
|
467
|
+
throw new HonuaExportSafetyError(`${message} Refusing to export (credentials: "reject").`, reason);
|
|
468
|
+
}
|
|
469
|
+
record(ctx, path, reason);
|
|
470
|
+
}
|
|
471
|
+
function record(ctx, path, reason) {
|
|
472
|
+
ctx.redactions.push({ path, reason });
|
|
473
|
+
}
|
|
474
|
+
// ── budgets ──────────────────────────────────────────────────────────────
|
|
475
|
+
/**
|
|
476
|
+
* Refuses an inline body that has outgrown "a description of a source".
|
|
477
|
+
*
|
|
478
|
+
* The shapes checked are the ones that actually carry bulk in a MapLibre
|
|
479
|
+
* style: a GeoJSON source's inline `data`, and any inline `features` array.
|
|
480
|
+
* Everything else is bounded by the whole-envelope budget.
|
|
481
|
+
*/
|
|
482
|
+
function assertEmbeddedDataWithinBudget(value, path, maxEmbeddedBytes, packageId, stage) {
|
|
483
|
+
const holder = value;
|
|
484
|
+
for (const key of ["data", "features"]) {
|
|
485
|
+
const inline = holder[key];
|
|
486
|
+
if (inline === undefined || inline === null)
|
|
487
|
+
continue;
|
|
488
|
+
if (typeof inline === "string" && !isDataUri(inline))
|
|
489
|
+
continue;
|
|
490
|
+
if (typeof inline !== "object" && typeof inline !== "string")
|
|
491
|
+
continue;
|
|
492
|
+
const bytes = jsonByteLength(inline);
|
|
493
|
+
if (bytes <= maxEmbeddedBytes)
|
|
494
|
+
continue;
|
|
495
|
+
throw new HonuaMapPackageError(`Inline data at ${path}.${key} is ${bytes} bytes, over the ${maxEmbeddedBytes}-byte budget; a portable map package references data, it does not carry it.`, { packageId, stage, detail: { path: `${path}.${key}`, bytes, maxEmbeddedBytes } });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The import-side per-value budget: {@link assertEmbeddedDataWithinBudget} at
|
|
500
|
+
* every node, plus the inline `data:` URI ceiling.
|
|
501
|
+
*
|
|
502
|
+
* The export side gets this for free because `sanitizeValue` recurses and
|
|
503
|
+
* checks each object as it descends. Import had no such walk and checked only
|
|
504
|
+
* the top-level `mapPackage` holder — which has no `data` or `features` key of
|
|
505
|
+
* its own, so the per-value budget was a no-op on the *reading* side, the one
|
|
506
|
+
* that actually pays for an oversized payload. An artifact carrying a 3 MiB
|
|
507
|
+
* inline GeoJSON body at `mapSpec.sources.*.data` was refused by the exporter
|
|
508
|
+
* at 256 KiB and accepted verbatim by the importer.
|
|
509
|
+
*
|
|
510
|
+
* Iterative rather than recursive: the input is whatever bytes a caller was
|
|
511
|
+
* handed, and a deeply nested document must fail on the budget rather than on
|
|
512
|
+
* the call stack. The whole-envelope budget is asserted before this runs, so
|
|
513
|
+
* the walk is bounded by that.
|
|
514
|
+
*/
|
|
515
|
+
function assertEmbeddedDataWithinBudgetDeep(root, rootPath, maxEmbeddedBytes, packageId) {
|
|
516
|
+
const stack = [[rootPath, root]];
|
|
517
|
+
while (stack.length > 0) {
|
|
518
|
+
const [path, node] = stack.pop();
|
|
519
|
+
if (typeof node === "string") {
|
|
520
|
+
if (isDataUri(node) && byteLength(node) > maxEmbeddedBytes) {
|
|
521
|
+
throw new HonuaMapPackageError(`Inline data at ${path} is ${byteLength(node)} bytes, over the ${maxEmbeddedBytes}-byte budget; a portable map package references data, it does not carry it.`, {
|
|
522
|
+
packageId,
|
|
523
|
+
stage: "import",
|
|
524
|
+
detail: { path, bytes: byteLength(node), maxEmbeddedBytes },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (typeof node !== "object" || node === null)
|
|
530
|
+
continue;
|
|
531
|
+
if (Array.isArray(node)) {
|
|
532
|
+
node.forEach((child, index) => stack.push([`${path}[${index}]`, child]));
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
assertEmbeddedDataWithinBudget(node, path, maxEmbeddedBytes, packageId, "import");
|
|
536
|
+
for (const [key, child] of Object.entries(node)) {
|
|
537
|
+
stack.push([`${path}.${key}`, child]);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function assertWithinPackageBudget(serialized, maxPackageBytes, packageId, stage) {
|
|
542
|
+
const bytes = byteLength(serialized);
|
|
543
|
+
if (bytes <= maxPackageBytes)
|
|
544
|
+
return;
|
|
545
|
+
throw new HonuaMapPackageError(`MapPackage export is ${bytes} bytes, over the ${maxPackageBytes}-byte budget.`, {
|
|
546
|
+
packageId,
|
|
547
|
+
stage,
|
|
548
|
+
detail: { bytes, maxPackageBytes },
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
552
|
+
const DATA_URI = /^data:[a-z0-9.+-]{0,128}\/[a-z0-9.+-]{0,128}(?:;[a-z0-9-]{0,64}=[^;,]{0,64})*(?:;base64)?,/i;
|
|
553
|
+
const URL_LIKE = /^[a-z][a-z0-9+.-]{1,31}:\/\//i;
|
|
554
|
+
function isDataUri(value) {
|
|
555
|
+
return DATA_URI.test(value);
|
|
556
|
+
}
|
|
557
|
+
function looksLikeUrl(value) {
|
|
558
|
+
return URL_LIKE.test(value) || value.startsWith("//");
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Decodes the payload of a `data:` URI to the bytes it actually represents.
|
|
562
|
+
*
|
|
563
|
+
* Both encodings are decoded, not just base64. `scrubDataUriPayloads` replaces
|
|
564
|
+
* *every* data-URI payload before the whole-envelope text scan, so a payload
|
|
565
|
+
* this function declines to decode is a payload that nothing scans at all:
|
|
566
|
+
* `data:text/plain,token%3Ds3cr3t` was emitted verbatim, with the percent
|
|
567
|
+
* escape hiding it from the scan that a plain `token=…` would have tripped.
|
|
568
|
+
*
|
|
569
|
+
* `undefined` means "cannot be decoded", never "safe" — a truncated data URI,
|
|
570
|
+
* a malformed percent escape, or invalid base64. The caller withholds those
|
|
571
|
+
* rather than letting an unscannable payload through.
|
|
572
|
+
*/
|
|
573
|
+
function decodeDataUri(value) {
|
|
574
|
+
const comma = value.indexOf(",");
|
|
575
|
+
if (comma === -1)
|
|
576
|
+
return undefined;
|
|
577
|
+
const payload = value.slice(comma + 1);
|
|
578
|
+
if (/;base64$/i.test(value.slice(0, comma))) {
|
|
579
|
+
try {
|
|
580
|
+
const binary = atob(payload);
|
|
581
|
+
const bytes = new Uint8Array(binary.length);
|
|
582
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
583
|
+
bytes[i] = binary.charCodeAt(i);
|
|
584
|
+
return bytes;
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
return new TextEncoder().encode(decodeURIComponent(payload));
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// A malformed percent escape. `decodeURIComponent` is the only decoder the
|
|
595
|
+
// payload claims, so what it refuses cannot be scanned.
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Replaces `data:` URI payloads with a placeholder before the whole-envelope
|
|
601
|
+
* text scan. The payloads are already bounded and scanned as decoded bytes;
|
|
602
|
+
* scanning base64 as text would report every icon as a high-entropy secret and
|
|
603
|
+
* make the fail-closed assertion useless.
|
|
604
|
+
*/
|
|
605
|
+
function scrubDataUriPayloads(serialized) {
|
|
606
|
+
return serialized.replace(/data:[a-z0-9.+/;=-]{0,256},[A-Za-z0-9+/=%_-]*/gi, "data:[payload]");
|
|
607
|
+
}
|
|
608
|
+
function byteLength(value) {
|
|
609
|
+
return new TextEncoder().encode(value).length;
|
|
610
|
+
}
|
|
611
|
+
function jsonByteLength(value) {
|
|
612
|
+
if (typeof value === "string")
|
|
613
|
+
return byteLength(value);
|
|
614
|
+
try {
|
|
615
|
+
return byteLength(JSON.stringify(value) ?? "");
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
// Cyclic or otherwise unserializable: treat as unbounded and refuse.
|
|
619
|
+
return Number.POSITIVE_INFINITY;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function normalizeBudget(value, fallback) {
|
|
623
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
624
|
+
return fallback;
|
|
625
|
+
return Math.trunc(value);
|
|
626
|
+
}
|
|
627
|
+
function normalizeTimestamp(value) {
|
|
628
|
+
if (value instanceof Date)
|
|
629
|
+
return value.toISOString();
|
|
630
|
+
if (typeof value === "string" && value.length > 0)
|
|
631
|
+
return value;
|
|
632
|
+
return new Date().toISOString();
|
|
633
|
+
}
|
|
634
|
+
function isRecord(value) {
|
|
635
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
636
|
+
}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `MapPackage` validation.
|
|
3
|
+
*
|
|
4
|
+
* The *structural* rules — which properties exist, which are required, which
|
|
5
|
+
* scalar types and enum members are legal, how long a string may be — are not
|
|
6
|
+
* written here. They live in `schemas/honua-map-package.v1.json`, the canonical
|
|
7
|
+
* artifact schema (honua-sdk-js#1426), which `scripts/generate-map-package-validator.mjs`
|
|
8
|
+
* compiles into `./generated/map-package-schema-validator.js`. This module runs
|
|
9
|
+
* that generated validator and translates its errors into the SDK's stable
|
|
10
|
+
* diagnostic codes. Restating the shape in TypeScript would recreate exactly
|
|
11
|
+
* the parallel description the schema exists to eliminate.
|
|
12
|
+
*
|
|
13
|
+
* What stays here is everything a JSON Schema genuinely cannot express:
|
|
14
|
+
* cross-field resolution (does every layer's `source` resolve to a binding or
|
|
15
|
+
* an inline style source?), package-level uniqueness (`sourceId` collisions),
|
|
16
|
+
* runtime capability (which of the schema's protocols the source bridge can
|
|
17
|
+
* actually bind), and wall-clock lifecycle (expiry and staleness).
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import type { HonuaMapPackage } from "./map-package.js";
|
|
2
22
|
export type HonuaMapPackageDiagnosticSeverity = "error" | "warning";
|
|
3
23
|
export type HonuaMapPackageDiagnosticCode = "invalid-package" | "unsupported-format" | "missing-map-package-id" | "missing-source-bindings" | "missing-map-spec" | "invalid-map-spec" | "duplicate-source" | "missing-source-id" | "missing-source-locator" | "unsupported-protocol" | "missing-source" | "stale-package" | "expired-package" | "style-ref-missing-layer" | "style-ref-unresolved" | "style-ref-resolution-failed";
|
|
4
24
|
export interface HonuaMapPackageDiagnostic {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-package-validation.d.ts","sourceRoot":"","sources":["../../../src/runtime/map-package-validation.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"map-package-validation.d.ts","sourceRoot":"","sources":["../../../src/runtime/map-package-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,iCAAiC,GAAG,OAAO,GAAG,SAAS,CAAC;AAEpE,MAAM,MAAM,6BAA6B,GACrC,iBAAiB,GACjB,oBAAoB,GACpB,wBAAwB,GACxB,yBAAyB,GACzB,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,GAClB,mBAAmB,GACnB,wBAAwB,GACxB,sBAAsB,GACtB,gBAAgB,GAChB,eAAe,GACf,iBAAiB,GACjB,yBAAyB,GACzB,sBAAsB,GACtB,6BAA6B,CAAC;AAElC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,iCAAiC,CAAC;IACrD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,iFAAiF;IACjF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AA8ED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,wBAAwB,CAiGpH;AAED,wBAAgB,6BAA6B,CAAC,WAAW,EAAE,SAAS,yBAAyB,EAAE,GAAG,OAAO,CAExG"}
|