@honua/app-platform 0.1.2-beta.0 → 0.1.4-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/collaboration/client.d.ts.map +1 -1
- package/connect-url-safety.d.ts +53 -0
- package/connect-url-safety.js +189 -0
- package/contract/discovery.d.ts +34 -0
- package/contract/discovery.d.ts.map +1 -1
- package/contract/index.d.ts +7 -5
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +3 -2
- package/contract/pmtiles.d.ts +10 -1
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +10 -2
- package/contract/protocol-module.d.ts +84 -9
- package/contract/protocol-module.d.ts.map +1 -1
- package/contract/query-filter-fes.d.ts +18 -0
- package/contract/query-filter-fes.d.ts.map +1 -0
- package/contract/query-filter-fes.js +122 -0
- package/contract/query-filter.d.ts +300 -0
- package/contract/query-filter.d.ts.map +1 -0
- package/contract/query-filter.js +883 -0
- package/contract/schema.d.ts +12 -0
- package/contract/schema.d.ts.map +1 -1
- package/contract/schema.js +34 -0
- package/contract/source-capability-support.d.ts.map +1 -1
- package/contract/source-capability-support.js +15 -3
- package/contract/source.d.ts +45 -25
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +719 -479
- package/contract/types.d.ts +87 -18
- package/contract/types.d.ts.map +1 -1
- package/contract/widget-source.d.ts.map +1 -1
- package/controls/basemap-switcher.d.ts +19 -11
- package/controls/basemap-switcher.d.ts.map +1 -1
- package/controls/basemap-switcher.js +58 -25
- package/controls/catalog.d.ts +105 -0
- package/controls/catalog.d.ts.map +1 -0
- package/controls/catalog.js +341 -0
- package/controls/element-utils.d.ts +9 -0
- package/controls/element-utils.d.ts.map +1 -1
- package/controls/element-utils.js +17 -0
- package/controls/index.d.ts +29 -8
- package/controls/index.d.ts.map +1 -1
- package/controls/index.js +26 -8
- package/controls/layer-list.d.ts +13 -5
- package/controls/layer-list.d.ts.map +1 -1
- package/controls/layer-list.js +72 -15
- package/controls/legend.d.ts +32 -5
- package/controls/legend.d.ts.map +1 -1
- package/controls/legend.js +95 -25
- package/controls/qualification.d.ts +152 -0
- package/controls/qualification.d.ts.map +1 -0
- package/controls/qualification.js +1265 -0
- package/controls/registry.d.ts +104 -0
- package/controls/registry.d.ts.map +1 -0
- package/controls/registry.js +150 -0
- package/controls/swipe-control.d.ts +6 -1
- package/controls/swipe-control.d.ts.map +1 -1
- package/controls/swipe-control.js +82 -20
- package/core/client.d.ts +45 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +224 -46
- package/core/error-classifications.d.ts +6 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +6 -0
- package/core/error-code-registry.d.ts +6 -0
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +6 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +68 -4
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +325 -8
- package/core/odata-write-codec.d.ts.map +1 -1
- package/core/odata-write-codec.js +0 -1
- package/core/odata.d.ts +6 -0
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +6 -1
- package/core/ogc-processes.d.ts +182 -25
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +602 -30
- package/core/process-runner.d.ts +8 -1
- package/core/process-runner.d.ts.map +1 -1
- package/core/process-runner.js +1 -0
- package/core/protocol-transport.d.ts +2 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/stac.d.ts +31 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +136 -19
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +123 -9
- package/core/types.d.ts.map +1 -1
- package/core/wfs-axis-order.d.ts +18 -0
- package/core/wfs-axis-order.d.ts.map +1 -0
- package/core/wfs-axis-order.js +108 -0
- package/core/wfs-capabilities.d.ts +65 -24
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +237 -27
- package/core/wfs-filter.d.ts +26 -3
- package/core/wfs-filter.d.ts.map +1 -1
- package/core/wfs-filter.js +48 -53
- package/core/wfs-protocol-error.d.ts +12 -0
- package/core/wfs-protocol-error.d.ts.map +1 -0
- package/core/wfs-protocol-error.js +13 -0
- package/core/wfs.d.ts +53 -1
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +251 -29
- package/core/wms-feature-info.d.ts +92 -0
- package/core/wms-feature-info.d.ts.map +1 -0
- package/core/wms-feature-info.js +285 -0
- package/core/wms.d.ts +9 -0
- package/core/wms.d.ts.map +1 -1
- package/core/wms.js +17 -5
- package/esri-compat/event-bus.d.ts +20 -0
- package/esri-compat/event-bus.d.ts.map +1 -1
- package/esri-compat/feature-filter.d.ts +46 -1
- package/esri-compat/feature-filter.d.ts.map +1 -1
- package/esri-compat/feature-filter.js +49 -0
- package/esri-compat/geojson-layer.d.ts +4 -2
- package/esri-compat/geojson-layer.d.ts.map +1 -1
- package/esri-compat/geojson-layer.js +4 -2
- package/esri-compat/identity-manager.js +2 -2
- package/esri-compat/imagery-layer.d.ts +2 -1
- package/esri-compat/imagery-layer.d.ts.map +1 -1
- package/esri-compat/imagery-layer.js +2 -1
- package/esri-compat/layer-list.d.ts +8 -1
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +11 -4
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +1 -1
- package/esri-compat/locator.d.ts +297 -0
- package/esri-compat/locator.d.ts.map +1 -0
- package/esri-compat/locator.js +579 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -1
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +1 -1
- package/esri-compat/scene-view.d.ts.map +1 -1
- package/esri-compat/time-slider.d.ts +29 -0
- package/esri-compat/time-slider.d.ts.map +1 -1
- package/esri-compat/time-slider.js +117 -0
- package/esri-compat/wfs-layer.d.ts +2 -1
- package/esri-compat/wfs-layer.d.ts.map +1 -1
- package/esri-compat/wfs-layer.js +2 -1
- package/esri-compat/widget-host.d.ts +42 -6
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +99 -10
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/runtime.d.ts.map +1 -1
- package/map/data-to-map-bridge.d.ts +1 -1
- package/map/data-to-map-bridge.d.ts.map +1 -1
- package/map/data-to-map-bridge.js +1 -2
- package/map/honua-map.d.ts.map +1 -1
- package/map/honua-map.js +1 -1
- package/map/index.d.ts +2 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +2 -0
- package/map/realtime-reconciliation-adapter.d.ts +98 -0
- package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
- package/map/realtime-reconciliation-adapter.js +84 -0
- package/map/temporal-playback.d.ts +25 -0
- package/map/temporal-playback.d.ts.map +1 -1
- package/map/temporal-playback.js +29 -1
- package/operator/workspace/workspace.d.ts.map +1 -1
- package/operator/workspace/workspace.js +0 -2
- package/package.json +4 -3
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/diagnostics.d.ts +3 -3
- package/query-planner/diagnostics.d.ts.map +1 -1
- package/query-planner/diagnostics.js +17 -4
- package/query-planner/duckdb.d.ts.map +1 -1
- package/query-planner/duckdb.js +16 -2
- package/query-planner/execution-context.d.ts.map +1 -1
- package/query-planner/execution-context.js +3 -1
- package/query-planner/executor.d.ts.map +1 -1
- package/query-planner/executor.js +7 -1
- package/query-planner/geoservices.d.ts.map +1 -1
- package/query-planner/geoservices.js +24 -5
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +29 -5
- package/query-planner/index.d.ts +20 -5
- package/query-planner/index.d.ts.map +1 -1
- package/query-planner/index.js +16 -3
- package/query-planner/ir.d.ts +6 -1
- package/query-planner/ir.d.ts.map +1 -1
- package/query-planner/ir.js +63 -1
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +0 -3
- package/query-planner/odata.d.ts.map +1 -1
- package/query-planner/odata.js +0 -143
- package/query-planner/ogc-features.d.ts.map +1 -1
- package/query-planner/ogc-features.js +23 -1
- package/query-planner/planner.d.ts +2 -2
- package/query-planner/planner.d.ts.map +1 -1
- package/query-planner/planner.js +146 -27
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/types.d.ts +153 -3
- package/query-planner/types.d.ts.map +1 -1
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +1 -3
- package/query-planner/wfs.d.ts.map +1 -1
- package/query-planner/wfs.js +1 -70
- package/realtime/checkpoint-store.d.ts +206 -0
- package/realtime/checkpoint-store.d.ts.map +1 -0
- package/realtime/checkpoint-store.js +504 -0
- package/realtime/honua-server.d.ts +8 -0
- package/realtime/honua-server.d.ts.map +1 -1
- package/realtime/honua-server.js +123 -14
- package/realtime/index.d.ts +4 -0
- package/realtime/index.d.ts.map +1 -1
- package/realtime/index.js +2 -0
- package/realtime/reconciliation.d.ts +245 -0
- package/realtime/reconciliation.d.ts.map +1 -0
- package/realtime/reconciliation.js +377 -0
- package/realtime/resumable-transport.d.ts.map +1 -1
- package/realtime/sse.d.ts.map +1 -1
- package/realtime/websocket.d.ts.map +1 -1
- package/realtime/wire.d.ts +2 -1
- package/realtime/wire.d.ts.map +1 -1
- package/realtime/wire.js +24 -8
- package/replica-sync/errors.d.ts +19 -1
- package/replica-sync/errors.d.ts.map +1 -1
- package/replica-sync/errors.js +16 -0
- package/replica-sync/geoservices.d.ts +260 -0
- package/replica-sync/geoservices.d.ts.map +1 -0
- package/replica-sync/geoservices.js +1093 -0
- package/replica-sync/index.d.ts +5 -1
- package/replica-sync/index.d.ts.map +1 -1
- package/replica-sync/index.js +3 -1
- package/replica-sync/transport-conformance.d.ts +58 -0
- package/replica-sync/transport-conformance.d.ts.map +1 -0
- package/replica-sync/transport-conformance.js +265 -0
- package/replica-sync/types.d.ts +22 -0
- package/replica-sync/types.d.ts.map +1 -1
- package/runtime/index.d.ts +6 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +3 -1
- package/runtime/load-package.d.ts +24 -0
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +45 -1
- package/runtime/maplibre-renderer.d.ts +11 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -1
- package/runtime/maplibre-renderer.js +32 -7
- package/runtime/offline-region-protocol.d.ts +73 -0
- package/runtime/offline-region-protocol.d.ts.map +1 -0
- package/runtime/offline-region-protocol.js +63 -0
- package/runtime/offline-region-style.d.ts +78 -0
- package/runtime/offline-region-style.d.ts.map +1 -0
- package/runtime/offline-region-style.js +147 -0
- package/runtime/pmtiles-protocol.d.ts +7 -8
- package/runtime/pmtiles-protocol.d.ts.map +1 -1
- package/runtime/pmtiles-protocol.js +19 -66
- package/runtime/protocol-registry.d.ts +55 -0
- package/runtime/protocol-registry.d.ts.map +1 -0
- package/runtime/protocol-registry.js +103 -0
- package/runtime/runtime.d.ts +2 -0
- package/runtime/runtime.d.ts.map +1 -1
- package/runtime/runtime.js +6 -1
- package/scene-workspace/analysis-widgets.d.ts +4 -2
- package/scene-workspace/analysis-widgets.d.ts.map +1 -1
- package/scene-workspace/analysis-widgets.js +4 -2
- package/scene-workspace/camera-correspondence.d.ts +151 -0
- package/scene-workspace/camera-correspondence.d.ts.map +1 -0
- package/scene-workspace/camera-correspondence.js +220 -0
- package/scene-workspace/cesium-adapter.d.ts +230 -29
- package/scene-workspace/cesium-adapter.d.ts.map +1 -1
- package/scene-workspace/cesium-adapter.js +741 -56
- package/scene-workspace/cesium-mount.d.ts +205 -0
- package/scene-workspace/cesium-mount.d.ts.map +1 -0
- package/scene-workspace/cesium-mount.js +392 -0
- package/scene-workspace/cesium-scene-owner.d.ts +160 -0
- package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
- package/scene-workspace/cesium-scene-owner.js +205 -0
- package/scene-workspace/cesium-state-sync.d.ts +101 -0
- package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
- package/scene-workspace/cesium-state-sync.js +432 -0
- package/scene-workspace/cesium-time.d.ts +346 -0
- package/scene-workspace/cesium-time.d.ts.map +1 -0
- package/scene-workspace/cesium-time.js +403 -0
- package/scene-workspace/index.d.ts +42 -10
- package/scene-workspace/index.d.ts.map +1 -1
- package/scene-workspace/index.js +32 -7
- package/scene-workspace/maplibre-state-sync.d.ts +112 -0
- package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
- package/scene-workspace/maplibre-state-sync.js +400 -0
- package/scene-workspace/primitives.d.ts +237 -3
- package/scene-workspace/primitives.d.ts.map +1 -1
- package/scene-workspace/primitives.js +1087 -17
- package/scene-workspace/scene-discovery.d.ts +5 -2
- package/scene-workspace/scene-discovery.d.ts.map +1 -1
- package/scene-workspace/scene-discovery.js +5 -2
- package/scene-workspace/scene-view.d.ts +5 -2
- package/scene-workspace/scene-view.d.ts.map +1 -1
- package/scene-workspace/scene-view.js +5 -2
- package/scene-workspace/source-to-cesium.d.ts +141 -8
- package/scene-workspace/source-to-cesium.d.ts.map +1 -1
- package/scene-workspace/source-to-cesium.js +497 -72
- package/scene-workspace/state-sync-port-kit.d.ts +156 -0
- package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
- package/scene-workspace/state-sync-port-kit.js +319 -0
- package/scene-workspace/state-sync.d.ts +34 -1
- package/scene-workspace/state-sync.d.ts.map +1 -1
- package/scene-workspace/state-sync.js +48 -0
- package/scene-workspace/types.d.ts +8 -0
- package/scene-workspace/types.d.ts.map +1 -1
- package/scene-workspace/types.js +2 -0
- package/scene-workspace/workspace.d.ts +12 -1
- package/scene-workspace/workspace.d.ts.map +1 -1
- package/scene-workspace/workspace.js +13 -0
- package/studio/index.d.ts +24 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +21 -7
- package/studio/lifecycle-client.d.ts +164 -0
- package/studio/lifecycle-client.d.ts.map +1 -0
- package/studio/lifecycle-client.js +299 -0
- package/studio/lifecycle-errors.d.ts +91 -0
- package/studio/lifecycle-errors.d.ts.map +1 -0
- package/studio/lifecycle-errors.js +77 -0
- package/studio/lifecycle-types.d.ts +321 -0
- package/studio/lifecycle-types.d.ts.map +1 -0
- package/studio/lifecycle-types.js +22 -0
- package/web-components/controller.d.ts.map +1 -1
- package/web-components/csp-styles.d.ts +6 -0
- package/web-components/csp-styles.d.ts.map +1 -0
- package/web-components/csp-styles.js +53 -0
- package/web-components/elements.d.ts +158 -2
- package/web-components/elements.d.ts.map +1 -1
- package/web-components/elements.js +1302 -215
- package/web-components/export-redaction.d.ts +252 -0
- package/web-components/export-redaction.d.ts.map +1 -0
- package/web-components/export-redaction.js +588 -0
- package/web-components/export.d.ts +417 -0
- package/web-components/export.d.ts.map +1 -0
- package/web-components/export.js +719 -0
- package/web-components/feature-editor-model.d.ts +265 -0
- package/web-components/feature-editor-model.d.ts.map +1 -0
- package/web-components/feature-editor-model.js +438 -0
- package/web-components/feature-editor-sketch.d.ts +125 -0
- package/web-components/feature-editor-sketch.d.ts.map +1 -0
- package/web-components/feature-editor-sketch.js +194 -0
- package/web-components/feature-editor-workflow.d.ts +286 -0
- package/web-components/feature-editor-workflow.d.ts.map +1 -0
- package/web-components/feature-editor-workflow.js +889 -0
- package/web-components/feature-editor.d.ts +83 -0
- package/web-components/feature-editor.d.ts.map +1 -0
- package/web-components/feature-editor.js +829 -0
- package/web-components/feature-table-engine.d.ts +465 -0
- package/web-components/feature-table-engine.d.ts.map +1 -0
- package/web-components/feature-table-engine.js +1360 -0
- package/web-components/feature-table-view.d.ts +95 -0
- package/web-components/feature-table-view.d.ts.map +1 -0
- package/web-components/feature-table-view.js +343 -0
- package/web-components/index.d.ts +34 -3
- package/web-components/index.d.ts.map +1 -1
- package/web-components/index.js +35 -2
- package/web-components/maplibre-renderer.d.ts.map +1 -1
- package/web-components/maplibre-renderer.js +13 -4
- package/web-components/measurement.d.ts +3 -1
- package/web-components/measurement.d.ts.map +1 -1
- package/web-components/measurement.js +62 -18
- package/web-components/time-slider.d.ts +122 -0
- package/web-components/time-slider.d.ts.map +1 -0
- package/web-components/time-slider.js +740 -0
- package/web-components/types.d.ts +310 -0
- package/web-components/types.d.ts.map +1 -1
- package/webmap/convert-label.js +0 -1
- package/webmap/convert-symbol.d.ts.map +1 -1
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Honua component kit's **export-adapter contract** (issue #683, parent
|
|
3
|
+
* epic #678; REQ-001/REQ-002/REQ-003).
|
|
4
|
+
*
|
|
5
|
+
* ## Why an adapter at all
|
|
6
|
+
*
|
|
7
|
+
* `<honua-print-export>` has always offered three actions — print layout,
|
|
8
|
+
* renderer snapshot, sanitized state JSON — but only browser print was ever
|
|
9
|
+
* implemented. The other two were *correctly* reported as unsupported rather
|
|
10
|
+
* than being faked, because producing them safely needs something the
|
|
11
|
+
* component kit does not have and must not guess at:
|
|
12
|
+
*
|
|
13
|
+
* - A **renderer** it is allowed to read pixels from. `canvas.toDataURL()` on a
|
|
14
|
+
* WebGL map silently returns a blank image unless the map was created with
|
|
15
|
+
* `canvasContextAttributes: { preserveDrawingBuffer: true }` (MapLibre 5 and
|
|
16
|
+
* 6; earlier majors took a top-level `preserveDrawingBuffer`), and it throws on a canvas tainted by
|
|
17
|
+
* cross-origin tiles. Only the application that constructed the map knows
|
|
18
|
+
* which of those is true, and only it can decide whether reading those
|
|
19
|
+
* pixels is authorized.
|
|
20
|
+
* - An **authorization boundary**. A map's live state transitively references
|
|
21
|
+
* the credentials that made it work: signed tile URLs, `Authorization`
|
|
22
|
+
* headers, SAS query strings, OAuth scope detail. Serializing "the current
|
|
23
|
+
* state" naively hands all of that to whoever receives the file.
|
|
24
|
+
*
|
|
25
|
+
* So export is modelled as an explicit, injected capability. There is no
|
|
26
|
+
* ambient fallback that "does its best": with no adapter, snapshot and state
|
|
27
|
+
* export **fail closed** with {@link HonuaCapabilityNotSupportedError} and
|
|
28
|
+
* produce no bytes at all. That is the whole point — a missing adapter must
|
|
29
|
+
* never degrade into a silent, partially-credentialed artifact.
|
|
30
|
+
*
|
|
31
|
+
* ## The four things the contract makes explicit (REQ-001)
|
|
32
|
+
*
|
|
33
|
+
* 1. **Capability** — {@link HonuaExportAdapter.describeCapabilities} declares
|
|
34
|
+
* exactly which kinds the adapter implements. A kind it does not declare is
|
|
35
|
+
* refused before the adapter is called; a kind it declares but does not
|
|
36
|
+
* implement is a *reported* contract violation, never a silent no-op.
|
|
37
|
+
* 2. **Ownership** — every result carries {@link HonuaExportResult.ownership}
|
|
38
|
+
* and an always-present {@link HonuaExportResult.release}. `bytes` handed to
|
|
39
|
+
* the caller are the caller's to keep and remain valid after `release()`;
|
|
40
|
+
* `release()` frees only adapter-held resources (object URLs, offscreen
|
|
41
|
+
* canvases, server-side render jobs). Callers can therefore always call it
|
|
42
|
+
* exactly once, unconditionally, in a `finally`.
|
|
43
|
+
* 3. **Cancellation** — an {@link AbortSignal} is checked before the adapter
|
|
44
|
+
* runs, forwarded to it, and re-checked after it returns; a cancelled export
|
|
45
|
+
* releases whatever the adapter produced and yields `status: "cancelled"`
|
|
46
|
+
* with no bytes.
|
|
47
|
+
* 4. **Error reporting** — the runner never throws. It returns a discriminated
|
|
48
|
+
* {@link HonuaExportResult} whose `status` is one of
|
|
49
|
+
* `ready`/`unsupported`/`cancelled`/`error`, with a redacted `message` and
|
|
50
|
+
* the structured `error` for programmatic handling. Elements can render the
|
|
51
|
+
* envelope directly; callers who prefer exceptions use
|
|
52
|
+
* {@link assertHonuaExportReady}.
|
|
53
|
+
*
|
|
54
|
+
* ## Redaction is not the adapter's job (REQ-002)
|
|
55
|
+
*
|
|
56
|
+
* The runner sanitizes state **before** the adapter is invoked, so an adapter
|
|
57
|
+
* — including a third-party or server-side one — never receives credential
|
|
58
|
+
* material in the first place. It then re-scans everything the adapter returned
|
|
59
|
+
* (bytes, text, filename hint, fidelity warnings) and refuses to emit an
|
|
60
|
+
* artifact that fails the scan. Both layers live in
|
|
61
|
+
* {@link "./export-redaction.js"}; see that module for the two-layer model.
|
|
62
|
+
*
|
|
63
|
+
* ## Provenance travels with the artifact (REQ-003)
|
|
64
|
+
*
|
|
65
|
+
* Attribution, license notices, scale, export timestamp, data-freshness, and
|
|
66
|
+
* fidelity warnings are assembled by the runner from the map package and
|
|
67
|
+
* controller state, handed to the adapter, and echoed on the result. Where a
|
|
68
|
+
* source declares attribution, the export **cannot** drop it: an artifact
|
|
69
|
+
* missing required attribution fails closed exactly like a credential leak,
|
|
70
|
+
* because shipping an unattributed map is a licence violation, not a cosmetic
|
|
71
|
+
* defect.
|
|
72
|
+
*
|
|
73
|
+
* ## Bundle posture (NFR-001)
|
|
74
|
+
*
|
|
75
|
+
* This module and its redaction sibling import nothing but the SDK's own error
|
|
76
|
+
* base and types. No renderer, PDF writer, image encoder, localization
|
|
77
|
+
* framework, or test-only peer is referenced, statically or dynamically —
|
|
78
|
+
* every one of those lives on the application side of the adapter seam. The
|
|
79
|
+
* built-in adapters here are thin structural shims over objects the caller
|
|
80
|
+
* already owns.
|
|
81
|
+
*
|
|
82
|
+
* @module
|
|
83
|
+
*/
|
|
84
|
+
import { HonuaSdkError } from "../core/error-envelope.js";
|
|
85
|
+
import { HonuaAbortError, HonuaCapabilityNotSupportedError } from "../core/errors.js";
|
|
86
|
+
import { HonuaExportSafetyError, assertCredentialFreeExportBytes, assertCredentialFreeExportText, redactHonuaExportText, sanitizeHonuaExportFilename, sanitizeHonuaExportState, } from "./export-redaction.js";
|
|
87
|
+
/** Protocol label used in capability errors raised by the export pipeline. */
|
|
88
|
+
const EXPORT_PROTOCOL = "honua-app-platform-export";
|
|
89
|
+
/** Every export kind, in a stable order. */
|
|
90
|
+
export const HONUA_EXPORT_KINDS = Object.freeze(["print", "snapshot", "state"]);
|
|
91
|
+
/** Thrown by {@link assertHonuaExportReady} and used to wrap adapter faults. */
|
|
92
|
+
export class HonuaExportError extends HonuaSdkError {
|
|
93
|
+
status;
|
|
94
|
+
constructor(status, message, options = {}) {
|
|
95
|
+
super("app.export-failed", message, options);
|
|
96
|
+
this.name = "HonuaExportError";
|
|
97
|
+
this.status = status;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ── provenance ───────────────────────────────────────────────────────────
|
|
101
|
+
/** `1:24,000`-style approximate scale for a web-mercator viewport at 96 DPI. */
|
|
102
|
+
export function approximateHonuaScaleLabel(viewport) {
|
|
103
|
+
const zoom = viewport?.zoom;
|
|
104
|
+
if (!Number.isFinite(zoom))
|
|
105
|
+
return undefined;
|
|
106
|
+
const latitude = viewport?.center?.[1] ?? 0;
|
|
107
|
+
const metersPerPixel = (156543.03392 * Math.cos((latitude * Math.PI) / 180)) / 2 ** Number(zoom);
|
|
108
|
+
const scale = Math.max(1, Math.round(metersPerPixel * 96 * 39.37));
|
|
109
|
+
return `1:${scale.toLocaleString("en-US")}`;
|
|
110
|
+
}
|
|
111
|
+
/** Attribution strings a map package's own bindings and style sources declare. */
|
|
112
|
+
function collectDeclaredAttribution(mapPackage) {
|
|
113
|
+
const attribution = new Set();
|
|
114
|
+
for (const binding of mapPackage?.sourceBindings ?? []) {
|
|
115
|
+
if (typeof binding.attribution === "string" && binding.attribution.trim().length > 0) {
|
|
116
|
+
attribution.add(redactHonuaExportText(binding.attribution.trim()).slice(0, 512));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const styleSources = mapPackage?.mapSpec?.sources;
|
|
120
|
+
if (styleSources && typeof styleSources === "object") {
|
|
121
|
+
for (const source of Object.values(styleSources)) {
|
|
122
|
+
const value = source?.attribution;
|
|
123
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
124
|
+
attribution.add(redactHonuaExportText(value.trim()).slice(0, 512));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [...attribution];
|
|
129
|
+
}
|
|
130
|
+
/** Licence notices declared on the map package (`license`, `licence`, or `metadata.license`). */
|
|
131
|
+
function collectDeclaredLicenseNotices(mapPackage) {
|
|
132
|
+
const notices = new Set();
|
|
133
|
+
const candidates = [
|
|
134
|
+
mapPackage?.license,
|
|
135
|
+
mapPackage?.licence,
|
|
136
|
+
mapPackage?.metadata?.license,
|
|
137
|
+
];
|
|
138
|
+
for (const candidate of candidates) {
|
|
139
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
140
|
+
notices.add(redactHonuaExportText(candidate.trim()).slice(0, 512));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [...notices];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Assembles the provenance block for an export (REQ-003) from the map package
|
|
147
|
+
* and controller state, unioned with anything the accepted plan additionally
|
|
148
|
+
* requires. Every string is redacted, because attribution text is
|
|
149
|
+
* caller-supplied and has appeared with a signed URL in it in the wild.
|
|
150
|
+
*/
|
|
151
|
+
export function buildHonuaExportProvenance(state, sanitized, options = {}) {
|
|
152
|
+
const attribution = new Set(collectDeclaredAttribution(state?.mapPackage));
|
|
153
|
+
for (const entry of options.requiredAttribution ?? []) {
|
|
154
|
+
const trimmed = redactHonuaExportText(entry.trim()).slice(0, 512);
|
|
155
|
+
if (trimmed.length > 0)
|
|
156
|
+
attribution.add(trimmed);
|
|
157
|
+
}
|
|
158
|
+
const licenseNotices = new Set(collectDeclaredLicenseNotices(state?.mapPackage));
|
|
159
|
+
for (const entry of options.requiredLicenseNotices ?? []) {
|
|
160
|
+
const trimmed = redactHonuaExportText(entry.trim()).slice(0, 512);
|
|
161
|
+
if (trimmed.length > 0)
|
|
162
|
+
licenseNotices.add(trimmed);
|
|
163
|
+
}
|
|
164
|
+
const fidelityWarnings = new Set((options.fidelityWarnings ?? []).map((warning) => redactHonuaExportText(warning).slice(0, 512)));
|
|
165
|
+
// A withheld non-exportable source is a fidelity loss the artifact must
|
|
166
|
+
// admit to; silently shipping a map that is missing a layer is worse than
|
|
167
|
+
// shipping one that says so.
|
|
168
|
+
const omitted = sanitized.sources.filter((source) => source.omitted).map((source) => source.sourceId);
|
|
169
|
+
if (omitted.length > 0) {
|
|
170
|
+
fidelityWarnings.add(`${omitted.length} source(s) marked non-exportable were omitted from this export: ${omitted.join(", ")}.`);
|
|
171
|
+
}
|
|
172
|
+
const provenance = {
|
|
173
|
+
exportedAt: options.exportedAt ?? new Date().toISOString(),
|
|
174
|
+
attribution: [...attribution],
|
|
175
|
+
licenseNotices: [...licenseNotices],
|
|
176
|
+
fidelityWarnings: [...fidelityWarnings],
|
|
177
|
+
generator: "@honua/sdk-js app-platform component kit",
|
|
178
|
+
};
|
|
179
|
+
const scaleLabel = approximateHonuaScaleLabel(sanitized.viewport ?? state?.viewport);
|
|
180
|
+
if (scaleLabel)
|
|
181
|
+
provenance.scaleLabel = scaleLabel;
|
|
182
|
+
if (sanitized.refreshedAt)
|
|
183
|
+
provenance.dataFreshnessAt = sanitized.refreshedAt;
|
|
184
|
+
if (typeof sanitized.stale === "boolean")
|
|
185
|
+
provenance.stale = sanitized.stale;
|
|
186
|
+
if (sanitized.packageId)
|
|
187
|
+
provenance.packageId = sanitized.packageId;
|
|
188
|
+
return provenance;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Fail-closed provenance check (REQ-003): every attribution string and licence
|
|
192
|
+
* notice a source or the accepted plan declares must be present on the
|
|
193
|
+
* provenance that ships with the artifact.
|
|
194
|
+
*
|
|
195
|
+
* This exists because provenance is the one part of an export a well-meaning
|
|
196
|
+
* adapter or caller is most likely to "simplify away". Dropping attribution is
|
|
197
|
+
* a licence breach, so it is enforced with the same severity as a credential
|
|
198
|
+
* leak rather than logged as a warning.
|
|
199
|
+
*/
|
|
200
|
+
export function assertHonuaExportProvenanceComplete(provenance, required) {
|
|
201
|
+
const missingAttribution = required.attribution.filter((entry) => !provenance.attribution.includes(entry));
|
|
202
|
+
const missingLicenses = required.licenseNotices.filter((entry) => !provenance.licenseNotices.includes(entry));
|
|
203
|
+
if (missingAttribution.length === 0 && missingLicenses.length === 0)
|
|
204
|
+
return;
|
|
205
|
+
throw new HonuaExportSafetyError(`Export is missing required provenance: ${[...missingAttribution, ...missingLicenses].join("; ")}.`, "unsupported-value");
|
|
206
|
+
}
|
|
207
|
+
// ── the runner ───────────────────────────────────────────────────────────
|
|
208
|
+
const ADAPTER_METHODS = {
|
|
209
|
+
print: "print",
|
|
210
|
+
snapshot: "snapshot",
|
|
211
|
+
state: "exportState",
|
|
212
|
+
};
|
|
213
|
+
function noopRelease() {
|
|
214
|
+
return Promise.resolve();
|
|
215
|
+
}
|
|
216
|
+
/** Wraps a payload's `release` so it runs at most once and never rejects. */
|
|
217
|
+
function onceRelease(release) {
|
|
218
|
+
if (!release)
|
|
219
|
+
return noopRelease;
|
|
220
|
+
let released = false;
|
|
221
|
+
return async () => {
|
|
222
|
+
if (released)
|
|
223
|
+
return;
|
|
224
|
+
released = true;
|
|
225
|
+
try {
|
|
226
|
+
await release();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// A failing release is not worth surfacing over the export's own
|
|
230
|
+
// outcome; the resource is the adapter's to leak.
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function unsupported(kind, adapterId, provenance, redactions, capability, message) {
|
|
235
|
+
const redactedAdapterId = adapterId === undefined ? undefined : redactHonuaExportText(adapterId);
|
|
236
|
+
const redactedMessage = redactHonuaExportText(redactedAdapterId && adapterId ? message.replaceAll(adapterId, redactedAdapterId) : message);
|
|
237
|
+
return {
|
|
238
|
+
kind,
|
|
239
|
+
status: "unsupported",
|
|
240
|
+
adapterId,
|
|
241
|
+
provenance,
|
|
242
|
+
redactions,
|
|
243
|
+
ownership: "none",
|
|
244
|
+
release: noopRelease,
|
|
245
|
+
message: redactedMessage,
|
|
246
|
+
error: new HonuaCapabilityNotSupportedError(capability, EXPORT_PROTOCOL, adapterId),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function failed(kind, adapterId, provenance, redactions, error, ownership) {
|
|
250
|
+
return {
|
|
251
|
+
kind,
|
|
252
|
+
status: "error",
|
|
253
|
+
adapterId,
|
|
254
|
+
provenance,
|
|
255
|
+
redactions,
|
|
256
|
+
ownership,
|
|
257
|
+
release: noopRelease,
|
|
258
|
+
message: redactHonuaExportText(error.message),
|
|
259
|
+
error,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Copies adapter bytes into a buffer the caller owns outright.
|
|
264
|
+
*
|
|
265
|
+
* Adapters routinely hand back a view onto a canvas-backed or pooled buffer
|
|
266
|
+
* they will reuse or free. Copying is what makes the ownership contract
|
|
267
|
+
* ("`bytes` stay valid after `release()`") true rather than aspirational.
|
|
268
|
+
*/
|
|
269
|
+
function adoptBytes(bytes) {
|
|
270
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
271
|
+
copy.set(bytes);
|
|
272
|
+
return copy;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Runs one export end to end. Never throws: every failure mode — no adapter,
|
|
276
|
+
* undeclared kind, missing method, adapter exception, cancellation, unsafe
|
|
277
|
+
* artifact, missing provenance — comes back as a {@link HonuaExportResult}.
|
|
278
|
+
*
|
|
279
|
+
* @example Fail closed with no adapter
|
|
280
|
+
* ```ts doc-test=skip reason="illustrative excerpt over an ambient controller state"
|
|
281
|
+
* const result = await runHonuaExport({ kind: "snapshot", state });
|
|
282
|
+
* result.status; // "unsupported"
|
|
283
|
+
* result.bytes; // undefined — no partial artifact is ever produced
|
|
284
|
+
* result.error?.sdkCode // "core.capability-not-supported"
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export async function runHonuaExport(request) {
|
|
288
|
+
const { kind, adapter } = request;
|
|
289
|
+
const sanitization = sanitizeHonuaExportState(request.state, request.nonExportableSourceIds ? { nonExportableSourceIds: request.nonExportableSourceIds } : {});
|
|
290
|
+
const requiredAttribution = [
|
|
291
|
+
...collectDeclaredAttribution(request.state?.mapPackage),
|
|
292
|
+
...(request.requiredAttribution ?? []).map((entry) => redactHonuaExportText(entry.trim()).slice(0, 512)),
|
|
293
|
+
].filter((entry) => entry.length > 0);
|
|
294
|
+
const requiredLicenseNotices = [
|
|
295
|
+
...collectDeclaredLicenseNotices(request.state?.mapPackage),
|
|
296
|
+
...(request.requiredLicenseNotices ?? []).map((entry) => redactHonuaExportText(entry.trim()).slice(0, 512)),
|
|
297
|
+
].filter((entry) => entry.length > 0);
|
|
298
|
+
const baseProvenance = buildHonuaExportProvenance(request.state, sanitization.state, {
|
|
299
|
+
...(request.requiredAttribution ? { requiredAttribution: request.requiredAttribution } : {}),
|
|
300
|
+
...(request.requiredLicenseNotices ? { requiredLicenseNotices: request.requiredLicenseNotices } : {}),
|
|
301
|
+
...(request.exportedAt ? { exportedAt: request.exportedAt } : {}),
|
|
302
|
+
});
|
|
303
|
+
const redactions = sanitization.redactions;
|
|
304
|
+
const title = request.title === undefined ? undefined : redactHonuaExportText(request.title).slice(0, 256);
|
|
305
|
+
if (!adapter) {
|
|
306
|
+
return unsupported(kind, undefined, baseProvenance, redactions, `export:${kind}`, `${describeKind(kind)} requires an explicit export adapter. Without one the component kit cannot read renderer pixels or authorize a state export, so it refuses rather than emitting a partial or credentialed artifact.`);
|
|
307
|
+
}
|
|
308
|
+
let capabilities;
|
|
309
|
+
try {
|
|
310
|
+
capabilities = adapter.describeCapabilities();
|
|
311
|
+
}
|
|
312
|
+
catch (cause) {
|
|
313
|
+
return failed(kind, typeof adapter.id === "string" ? adapter.id : undefined, baseProvenance, redactions, new HonuaExportError("error", "Export adapter failed to describe its capabilities.", { cause }), "none");
|
|
314
|
+
}
|
|
315
|
+
// Validated rather than trusted, and validated *inside* the guarded region:
|
|
316
|
+
// an adapter that returns undefined, null, or an object without a `kinds`
|
|
317
|
+
// array used to be dereferenced here and reject the whole call with a
|
|
318
|
+
// TypeError, breaking this function's one guarantee — that it always resolves
|
|
319
|
+
// to a structured result. A malformed capability declaration is a contract
|
|
320
|
+
// violation by the adapter and is reported as one.
|
|
321
|
+
const declaredId = capabilities?.adapterId;
|
|
322
|
+
const adapterId = (typeof declaredId === "string" && declaredId) || adapter.id || "unknown-adapter";
|
|
323
|
+
const declaredKinds = capabilities?.kinds;
|
|
324
|
+
if (!capabilities || typeof capabilities !== "object" || !Array.isArray(declaredKinds)) {
|
|
325
|
+
return failed(kind, typeof adapterId === "string" ? adapterId : undefined, baseProvenance, redactions, new HonuaExportError("error", `Export adapter "${String(adapterId)}" returned a malformed capability declaration; describeCapabilities() must return an object with a kinds array.`), "none");
|
|
326
|
+
}
|
|
327
|
+
if (!declaredKinds.includes(kind)) {
|
|
328
|
+
return unsupported(kind, adapterId, baseProvenance, redactions, `export:${kind}`, `Export adapter "${adapterId}" does not declare the "${kind}" export kind.`);
|
|
329
|
+
}
|
|
330
|
+
const method = adapter[ADAPTER_METHODS[kind]];
|
|
331
|
+
if (typeof method !== "function") {
|
|
332
|
+
return unsupported(kind, adapterId, baseProvenance, redactions, `export:${kind}`, `Export adapter "${adapterId}" declares the "${kind}" kind but does not implement ${String(ADAPTER_METHODS[kind])}().`);
|
|
333
|
+
}
|
|
334
|
+
if (request.signal?.aborted) {
|
|
335
|
+
return {
|
|
336
|
+
kind,
|
|
337
|
+
status: "cancelled",
|
|
338
|
+
adapterId,
|
|
339
|
+
provenance: baseProvenance,
|
|
340
|
+
redactions,
|
|
341
|
+
ownership: "released",
|
|
342
|
+
release: noopRelease,
|
|
343
|
+
message: "Export was cancelled before the adapter ran.",
|
|
344
|
+
error: new HonuaAbortError("Export was cancelled before the adapter ran."),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const context = {
|
|
348
|
+
kind,
|
|
349
|
+
title,
|
|
350
|
+
provenance: baseProvenance,
|
|
351
|
+
state: sanitization.state,
|
|
352
|
+
signal: request.signal,
|
|
353
|
+
mediaType: request.mediaType,
|
|
354
|
+
};
|
|
355
|
+
let payload;
|
|
356
|
+
try {
|
|
357
|
+
payload = await method.call(adapter, context);
|
|
358
|
+
}
|
|
359
|
+
catch (cause) {
|
|
360
|
+
if (request.signal?.aborted || isAbortLike(cause)) {
|
|
361
|
+
return {
|
|
362
|
+
kind,
|
|
363
|
+
status: "cancelled",
|
|
364
|
+
adapterId,
|
|
365
|
+
provenance: baseProvenance,
|
|
366
|
+
redactions,
|
|
367
|
+
ownership: "released",
|
|
368
|
+
release: noopRelease,
|
|
369
|
+
message: "Export was cancelled.",
|
|
370
|
+
error: new HonuaAbortError("Export was cancelled."),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// An adapter that already speaks the SDK's error envelope has said
|
|
374
|
+
// something specific and actionable ("the canvas is not readable because
|
|
375
|
+
// the map lacks preserveDrawingBuffer"); wrapping that in a generic message
|
|
376
|
+
// would throw away the only useful part. Its message is redacted by
|
|
377
|
+
// `failed()` like any other.
|
|
378
|
+
return failed(kind, adapterId, baseProvenance, redactions, cause instanceof HonuaSdkError
|
|
379
|
+
? cause
|
|
380
|
+
: new HonuaExportError("error", `Export adapter "${adapterId}" failed to produce a ${kind} artifact.`, {
|
|
381
|
+
cause,
|
|
382
|
+
}), "none");
|
|
383
|
+
}
|
|
384
|
+
const release = onceRelease(payload?.release);
|
|
385
|
+
if (!payload || typeof payload !== "object") {
|
|
386
|
+
await release();
|
|
387
|
+
return failed(kind, adapterId, baseProvenance, redactions, new HonuaExportError("error", `Export adapter "${adapterId}" returned no payload for the ${kind} export.`), "released");
|
|
388
|
+
}
|
|
389
|
+
if (request.signal?.aborted) {
|
|
390
|
+
await release();
|
|
391
|
+
return {
|
|
392
|
+
kind,
|
|
393
|
+
status: "cancelled",
|
|
394
|
+
adapterId,
|
|
395
|
+
provenance: baseProvenance,
|
|
396
|
+
redactions,
|
|
397
|
+
ownership: "released",
|
|
398
|
+
release: noopRelease,
|
|
399
|
+
message: "Export was cancelled; the adapter's resources were released.",
|
|
400
|
+
error: new HonuaAbortError("Export was cancelled after the adapter produced an artifact."),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const result = finalize(kind, adapterId, payload, baseProvenance, redactions, release, { attribution: requiredAttribution, licenseNotices: requiredLicenseNotices }, request.requireEmbeddedProvenance === true);
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
catch (cause) {
|
|
408
|
+
await release();
|
|
409
|
+
const error = cause instanceof HonuaExportSafetyError
|
|
410
|
+
? cause
|
|
411
|
+
: new HonuaExportError("error", `Export adapter "${adapterId}" produced an unusable ${kind} artifact.`, {
|
|
412
|
+
cause,
|
|
413
|
+
});
|
|
414
|
+
return failed(kind, adapterId, baseProvenance, redactions, error, "released");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function describeKind(kind) {
|
|
418
|
+
if (kind === "snapshot")
|
|
419
|
+
return "Snapshot export";
|
|
420
|
+
if (kind === "state")
|
|
421
|
+
return "State export";
|
|
422
|
+
return "Print export";
|
|
423
|
+
}
|
|
424
|
+
function isAbortLike(cause) {
|
|
425
|
+
const name = cause?.name;
|
|
426
|
+
return name === "AbortError" || cause instanceof HonuaAbortError;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Validates and adopts an adapter payload. Throws on anything unsafe — the
|
|
430
|
+
* caller turns that into a `status: "error"` result after releasing the
|
|
431
|
+
* adapter's resources.
|
|
432
|
+
*/
|
|
433
|
+
function finalize(kind, adapterId, payload, baseProvenance, redactions, release, required, requireEmbeddedProvenance) {
|
|
434
|
+
const projectedMediaType = projectMediaType(payload.mediaType);
|
|
435
|
+
const adapterWarnings = (payload.fidelityWarnings ?? []).map((warning) => redactHonuaExportText(String(warning)).slice(0, 512));
|
|
436
|
+
const warnings = new Set([...baseProvenance.fidelityWarnings, ...adapterWarnings]);
|
|
437
|
+
if (projectedMediaType.violation && projectedMediaType.mediaType.length > 0) {
|
|
438
|
+
warnings.add(`Export adapter "${adapterId}": ${projectedMediaType.violation}.`);
|
|
439
|
+
}
|
|
440
|
+
// REQ-003: whether the artifact's *bytes* carry the required attribution is a
|
|
441
|
+
// separate fact from whether the export knows the attribution, and conflating
|
|
442
|
+
// them is how an unattributed image ships. An adapter must say which it did;
|
|
443
|
+
// the safe default for an artifact that carries a body is "did not".
|
|
444
|
+
const requiresProvenance = required.attribution.length > 0 || required.licenseNotices.length > 0;
|
|
445
|
+
const provenanceEmbedded = payload.sideEffectOnly === true || payload.provenanceEmbedded === true;
|
|
446
|
+
if (requiresProvenance && !provenanceEmbedded) {
|
|
447
|
+
warnings.add(`This artifact's bytes do not carry the attribution and licence notices its sources require (${[
|
|
448
|
+
...required.attribution,
|
|
449
|
+
...required.licenseNotices,
|
|
450
|
+
].join("; ")}). The caller must present them alongside the artifact wherever it is published.`);
|
|
451
|
+
}
|
|
452
|
+
const provenance = { ...baseProvenance, fidelityWarnings: [...warnings] };
|
|
453
|
+
assertHonuaExportProvenanceComplete(provenance, required);
|
|
454
|
+
// The strict mode, for callers who cannot present provenance out of band.
|
|
455
|
+
if (requireEmbeddedProvenance && requiresProvenance && !provenanceEmbedded) {
|
|
456
|
+
throw new HonuaExportSafetyError(`Export adapter "${adapterId}" cannot embed the required attribution into the ${kind} artifact, and requireEmbeddedProvenance was set. Refusing to emit an artifact whose bytes would travel without the attribution its sources require.`, "unsupported-value");
|
|
457
|
+
}
|
|
458
|
+
if (payload.sideEffectOnly === true) {
|
|
459
|
+
return {
|
|
460
|
+
kind,
|
|
461
|
+
status: "ready",
|
|
462
|
+
adapterId,
|
|
463
|
+
sideEffectOnly: true,
|
|
464
|
+
provenance,
|
|
465
|
+
provenanceEmbedded,
|
|
466
|
+
redactions,
|
|
467
|
+
// A side-effect export can still hold resources (a print stylesheet
|
|
468
|
+
// injected into the document, an off-screen render frame). Reporting
|
|
469
|
+
// "none" here told documented callers to skip cleanup and leaked it.
|
|
470
|
+
ownership: payload.release ? "caller-releases" : "none",
|
|
471
|
+
release,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (projectedMediaType.mediaType.length === 0) {
|
|
475
|
+
throw new HonuaExportError("error", `Export adapter "${adapterId}" returned an artifact with no media type.`);
|
|
476
|
+
}
|
|
477
|
+
const mediaType = projectedMediaType.mediaType;
|
|
478
|
+
const result = {
|
|
479
|
+
kind,
|
|
480
|
+
status: "ready",
|
|
481
|
+
adapterId,
|
|
482
|
+
filename: sanitizeHonuaExportFilename({
|
|
483
|
+
...(payload.filenameHint === undefined ? {} : { title: payload.filenameHint }),
|
|
484
|
+
mediaType,
|
|
485
|
+
}),
|
|
486
|
+
mediaType,
|
|
487
|
+
provenance,
|
|
488
|
+
provenanceEmbedded,
|
|
489
|
+
redactions,
|
|
490
|
+
ownership: payload.release ? "caller-releases" : "none",
|
|
491
|
+
release,
|
|
492
|
+
};
|
|
493
|
+
if (payload.bytes instanceof Uint8Array) {
|
|
494
|
+
// Scanned two ways, because "binary" is not a security boundary. A buffer
|
|
495
|
+
// that decodes cleanly as UTF-8 (SVG, JSON, HTML) is scanned as text; every
|
|
496
|
+
// buffer, including a genuine PNG or PDF that will never decode, also has
|
|
497
|
+
// its printable runs extracted and scanned, so a token inside a PNG tEXt
|
|
498
|
+
// chunk, a JPEG comment, or PDF/XMP metadata cannot ride out under cover of
|
|
499
|
+
// the surrounding binary.
|
|
500
|
+
const decoded = decodeUtf8Strict(payload.bytes);
|
|
501
|
+
if (decoded !== undefined)
|
|
502
|
+
assertCredentialFreeExportText(decoded, `${kind} export bytes`);
|
|
503
|
+
assertCredentialFreeExportBytes(payload.bytes, `${kind} export bytes`);
|
|
504
|
+
result.bytes = adoptBytes(payload.bytes);
|
|
505
|
+
}
|
|
506
|
+
else if (typeof payload.text === "string") {
|
|
507
|
+
assertCredentialFreeExportText(payload.text, `${kind} export text`);
|
|
508
|
+
result.text = payload.text;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
throw new HonuaExportError("error", `Export adapter "${adapterId}" returned neither bytes nor text for the ${kind} export.`);
|
|
512
|
+
}
|
|
513
|
+
for (const warning of provenance.fidelityWarnings) {
|
|
514
|
+
assertCredentialFreeExportText(warning, "export fidelity warning");
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
/** Fallback for a media type the adapter got wrong; deliberately inert. */
|
|
519
|
+
const SAFE_FALLBACK_MEDIA_TYPE = "application/octet-stream";
|
|
520
|
+
/** RFC 9110 token characters, the only thing allowed in a type, subtype, or parameter name. */
|
|
521
|
+
const MEDIA_TYPE_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
|
|
522
|
+
/**
|
|
523
|
+
* Parameters an export media type may carry. Everything else is dropped: a
|
|
524
|
+
* media type is echoed onto the result, onto the `honua-export` event, and into
|
|
525
|
+
* a `Content-Type` header by most callers, so an adapter-controlled parameter
|
|
526
|
+
* bag is a credential channel (`text/plain; token=...`) and a header-injection
|
|
527
|
+
* channel. `charset` is the only parameter an export artifact legitimately
|
|
528
|
+
* needs, and its value is checked against the same token grammar.
|
|
529
|
+
*/
|
|
530
|
+
const MEDIA_TYPE_PARAMETER_ALLOWLIST = new Set(["charset"]);
|
|
531
|
+
/**
|
|
532
|
+
* Validates an adapter-supplied media type against a strict `type/subtype`
|
|
533
|
+
* grammar with an allowlisted parameter set, replacing anything else with
|
|
534
|
+
* {@link SAFE_FALLBACK_MEDIA_TYPE} and reporting the substitution.
|
|
535
|
+
*
|
|
536
|
+
* Truncation alone (the previous behavior) is not validation: it bounds length
|
|
537
|
+
* and nothing else, so a value carrying a token, a newline, or arbitrary
|
|
538
|
+
* parameters passed straight through to the result and the event detail.
|
|
539
|
+
*/
|
|
540
|
+
function projectMediaType(raw) {
|
|
541
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
542
|
+
return { mediaType: "", violation: "no media type" };
|
|
543
|
+
}
|
|
544
|
+
const value = raw.slice(0, 255);
|
|
545
|
+
const [essence, ...parameters] = value.split(";");
|
|
546
|
+
const [type, subtype, ...extra] = essence.trim().toLowerCase().split("/");
|
|
547
|
+
if (extra.length > 0 || !MEDIA_TYPE_TOKEN.test(type ?? "") || !MEDIA_TYPE_TOKEN.test(subtype ?? "")) {
|
|
548
|
+
return {
|
|
549
|
+
mediaType: SAFE_FALLBACK_MEDIA_TYPE,
|
|
550
|
+
violation: `media type is not a valid type/subtype; substituted ${SAFE_FALLBACK_MEDIA_TYPE}`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const kept = [];
|
|
554
|
+
let dropped = 0;
|
|
555
|
+
for (const parameter of parameters) {
|
|
556
|
+
const separator = parameter.indexOf("=");
|
|
557
|
+
if (separator < 0) {
|
|
558
|
+
dropped += 1;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const name = parameter.slice(0, separator).trim().toLowerCase();
|
|
562
|
+
const parameterValue = parameter
|
|
563
|
+
.slice(separator + 1)
|
|
564
|
+
.trim()
|
|
565
|
+
.replace(/^"|"$/g, "");
|
|
566
|
+
if (!MEDIA_TYPE_PARAMETER_ALLOWLIST.has(name) || !MEDIA_TYPE_TOKEN.test(parameterValue)) {
|
|
567
|
+
dropped += 1;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
kept.push(`${name}=${parameterValue}`);
|
|
571
|
+
}
|
|
572
|
+
const mediaType = [`${type}/${subtype}`, ...kept].join("; ");
|
|
573
|
+
return dropped === 0
|
|
574
|
+
? { mediaType }
|
|
575
|
+
: { mediaType, violation: `${dropped} disallowed media-type parameter(s) were dropped` };
|
|
576
|
+
}
|
|
577
|
+
function decodeUtf8Strict(bytes) {
|
|
578
|
+
if (typeof TextDecoder === "undefined")
|
|
579
|
+
return undefined;
|
|
580
|
+
try {
|
|
581
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/** Throws `result.error` unless the export succeeded; returns the result otherwise. */
|
|
588
|
+
export function assertHonuaExportReady(result) {
|
|
589
|
+
if (result.status === "ready")
|
|
590
|
+
return result;
|
|
591
|
+
throw result.error ?? new HonuaExportError(result.status, result.message ?? "Export did not complete.");
|
|
592
|
+
}
|
|
593
|
+
const DATA_URL_PREFIX = /^data:([^;,]{1,128})?(;base64)?,/i;
|
|
594
|
+
function decodeDataUrl(dataUrl) {
|
|
595
|
+
const match = DATA_URL_PREFIX.exec(dataUrl);
|
|
596
|
+
if (!match)
|
|
597
|
+
throw new HonuaExportError("error", "Renderer returned a value that is not a data URL.");
|
|
598
|
+
const mediaType = match[1] || "image/png";
|
|
599
|
+
const body = dataUrl.slice(match[0].length);
|
|
600
|
+
if (match[2]) {
|
|
601
|
+
const binary = typeof atob === "function" ? atob(body) : Buffer.from(body, "base64").toString("binary");
|
|
602
|
+
const bytes = new Uint8Array(binary.length);
|
|
603
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
604
|
+
bytes[index] = binary.charCodeAt(index);
|
|
605
|
+
return { mediaType, bytes };
|
|
606
|
+
}
|
|
607
|
+
return { mediaType, bytes: new TextEncoder().encode(decodeURIComponent(body)) };
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Builds an export adapter from capabilities the application explicitly grants.
|
|
611
|
+
*
|
|
612
|
+
* Everything here is structural: no `maplibre-gl`, `cesium`, PDF writer, or
|
|
613
|
+
* image encoder is imported, so wiring an adapter costs a consumer bundle
|
|
614
|
+
* nothing beyond the object it passes in (NFR-001).
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* ```ts doc-test=skip reason="requires a live MapLibre map instance"
|
|
618
|
+
* const adapter = createHonuaExportAdapter({
|
|
619
|
+
* id: "my-app",
|
|
620
|
+
* // map created with canvasContextAttributes: { preserveDrawingBuffer: true }
|
|
621
|
+
* snapshot: { getCanvas: () => map.getCanvas() },
|
|
622
|
+
* state: true,
|
|
623
|
+
* });
|
|
624
|
+
* printExportElement.exportAdapter = adapter;
|
|
625
|
+
* ```
|
|
626
|
+
*/
|
|
627
|
+
export function createHonuaExportAdapter(options) {
|
|
628
|
+
const kinds = [];
|
|
629
|
+
if (options.print)
|
|
630
|
+
kinds.push("print");
|
|
631
|
+
if (options.snapshot)
|
|
632
|
+
kinds.push("snapshot");
|
|
633
|
+
if (options.state)
|
|
634
|
+
kinds.push("state");
|
|
635
|
+
const snapshotMediaTypes = options.snapshot ? [options.snapshot.mediaType ?? "image/png"] : undefined;
|
|
636
|
+
const adapter = {
|
|
637
|
+
id: options.id,
|
|
638
|
+
describeCapabilities: () => ({
|
|
639
|
+
adapterId: options.id,
|
|
640
|
+
kinds: [...kinds],
|
|
641
|
+
cancellable: true,
|
|
642
|
+
...(snapshotMediaTypes ? { snapshotMediaTypes } : {}),
|
|
643
|
+
}),
|
|
644
|
+
};
|
|
645
|
+
if (options.print)
|
|
646
|
+
adapter.print = options.print;
|
|
647
|
+
if (options.snapshot) {
|
|
648
|
+
const source = options.snapshot;
|
|
649
|
+
adapter.snapshot = (context) => {
|
|
650
|
+
const canvas = source.getCanvas();
|
|
651
|
+
if (!canvas) {
|
|
652
|
+
throw new HonuaExportError("error", "The renderer canvas is not currently readable, so no snapshot was produced. A map must be created with canvasContextAttributes: { preserveDrawingBuffer: true } for its pixels to be readable.");
|
|
653
|
+
}
|
|
654
|
+
const mediaType = context.mediaType ?? source.mediaType ?? "image/png";
|
|
655
|
+
if (typeof canvas.toDataURL !== "function") {
|
|
656
|
+
throw new HonuaExportError("error", "The supplied renderer canvas does not implement toDataURL().");
|
|
657
|
+
}
|
|
658
|
+
const decoded = decodeDataUrl(canvas.toDataURL(mediaType));
|
|
659
|
+
return {
|
|
660
|
+
mediaType: decoded.mediaType,
|
|
661
|
+
bytes: decoded.bytes,
|
|
662
|
+
filenameHint: context.title ?? context.provenance.packageId ?? "snapshot",
|
|
663
|
+
fidelityWarnings: source.fidelityWarnings ?? [],
|
|
664
|
+
// Reading a canvas captures the map's pixels and nothing else: MapLibre
|
|
665
|
+
// renders attribution as DOM siblings of the canvas, so it is provably
|
|
666
|
+
// absent from these bytes. Declaring `true` here would make the result
|
|
667
|
+
// claim an attribution guarantee the image does not carry, which is the
|
|
668
|
+
// licence-breaking failure REQ-003 exists to prevent. The runner turns
|
|
669
|
+
// this into an explicit fidelity warning naming what the caller must
|
|
670
|
+
// present, or a hard failure under `requireEmbeddedProvenance`.
|
|
671
|
+
// An adapter that composites a watermark or footer should declare true.
|
|
672
|
+
provenanceEmbedded: source.provenanceEmbedded === true,
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
if (options.state) {
|
|
677
|
+
const custom = typeof options.state === "function" ? options.state : undefined;
|
|
678
|
+
adapter.exportState =
|
|
679
|
+
custom ??
|
|
680
|
+
((context) => ({
|
|
681
|
+
mediaType: "application/json",
|
|
682
|
+
text: JSON.stringify({ ...context.state, provenance: context.provenance }, null, 2),
|
|
683
|
+
filenameHint: context.title ?? context.provenance.packageId ?? "state",
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
return adapter;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* The browser-print adapter: the one export the component kit can serve without
|
|
690
|
+
* application help, because `window.print()` reads no pixels and serializes no
|
|
691
|
+
* state — the browser prints what it already renders, under the user's own
|
|
692
|
+
* session.
|
|
693
|
+
*
|
|
694
|
+
* It is a *default* for `print` only. Snapshot and state export deliberately
|
|
695
|
+
* have no equivalent built-in, because both require reading privileged data.
|
|
696
|
+
*/
|
|
697
|
+
export function createBrowserPrintExportAdapter(windowLike = typeof window === "undefined" ? undefined : window) {
|
|
698
|
+
return {
|
|
699
|
+
id: "honua-browser-print",
|
|
700
|
+
describeCapabilities: () => ({
|
|
701
|
+
adapterId: "honua-browser-print",
|
|
702
|
+
kinds: windowLike && typeof windowLike.print === "function" ? ["print"] : [],
|
|
703
|
+
cancellable: false,
|
|
704
|
+
}),
|
|
705
|
+
print: () => {
|
|
706
|
+
if (!windowLike || typeof windowLike.print !== "function") {
|
|
707
|
+
throw new HonuaExportError("error", "Window printing is unavailable in this environment.");
|
|
708
|
+
}
|
|
709
|
+
windowLike.print();
|
|
710
|
+
return {
|
|
711
|
+
mediaType: "application/vnd.honua.print-dialog",
|
|
712
|
+
sideEffectOnly: true,
|
|
713
|
+
fidelityWarnings: [
|
|
714
|
+
"Browser print reproduces the on-screen layout only; page breaks, scale, and layer legibility are controlled by the print stylesheet, not by the map.",
|
|
715
|
+
],
|
|
716
|
+
};
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|