@honua/app-platform 0.1.1-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.
Files changed (383) hide show
  1. package/collaboration/client.d.ts.map +1 -1
  2. package/connect-url-safety.d.ts +53 -0
  3. package/connect-url-safety.js +189 -0
  4. package/contract/discovery.d.ts +34 -0
  5. package/contract/discovery.d.ts.map +1 -1
  6. package/contract/index.d.ts +7 -5
  7. package/contract/index.d.ts.map +1 -1
  8. package/contract/index.js +3 -2
  9. package/contract/pmtiles.d.ts +10 -1
  10. package/contract/pmtiles.d.ts.map +1 -1
  11. package/contract/pmtiles.js +10 -2
  12. package/contract/protocol-module.d.ts +84 -9
  13. package/contract/protocol-module.d.ts.map +1 -1
  14. package/contract/query-filter-fes.d.ts +18 -0
  15. package/contract/query-filter-fes.d.ts.map +1 -0
  16. package/contract/query-filter-fes.js +122 -0
  17. package/contract/query-filter.d.ts +300 -0
  18. package/contract/query-filter.d.ts.map +1 -0
  19. package/contract/query-filter.js +883 -0
  20. package/contract/schema.d.ts +12 -0
  21. package/contract/schema.d.ts.map +1 -1
  22. package/contract/schema.js +34 -0
  23. package/contract/source-capability-support.d.ts.map +1 -1
  24. package/contract/source-capability-support.js +15 -3
  25. package/contract/source.d.ts +45 -25
  26. package/contract/source.d.ts.map +1 -1
  27. package/contract/source.js +719 -479
  28. package/contract/types.d.ts +87 -18
  29. package/contract/types.d.ts.map +1 -1
  30. package/contract/widget-source.d.ts.map +1 -1
  31. package/controls/basemap-switcher.d.ts +19 -11
  32. package/controls/basemap-switcher.d.ts.map +1 -1
  33. package/controls/basemap-switcher.js +58 -25
  34. package/controls/catalog.d.ts +105 -0
  35. package/controls/catalog.d.ts.map +1 -0
  36. package/controls/catalog.js +341 -0
  37. package/controls/element-utils.d.ts +9 -0
  38. package/controls/element-utils.d.ts.map +1 -1
  39. package/controls/element-utils.js +17 -0
  40. package/controls/index.d.ts +29 -8
  41. package/controls/index.d.ts.map +1 -1
  42. package/controls/index.js +26 -8
  43. package/controls/layer-list.d.ts +13 -5
  44. package/controls/layer-list.d.ts.map +1 -1
  45. package/controls/layer-list.js +72 -15
  46. package/controls/legend.d.ts +32 -5
  47. package/controls/legend.d.ts.map +1 -1
  48. package/controls/legend.js +95 -25
  49. package/controls/qualification.d.ts +152 -0
  50. package/controls/qualification.d.ts.map +1 -0
  51. package/controls/qualification.js +1265 -0
  52. package/controls/registry.d.ts +104 -0
  53. package/controls/registry.d.ts.map +1 -0
  54. package/controls/registry.js +150 -0
  55. package/controls/swipe-control.d.ts +6 -1
  56. package/controls/swipe-control.d.ts.map +1 -1
  57. package/controls/swipe-control.js +82 -20
  58. package/core/client.d.ts +45 -21
  59. package/core/client.d.ts.map +1 -1
  60. package/core/client.js +224 -46
  61. package/core/error-classifications.d.ts +6 -0
  62. package/core/error-classifications.d.ts.map +1 -1
  63. package/core/error-classifications.js +6 -0
  64. package/core/error-code-registry.d.ts +6 -0
  65. package/core/error-code-registry.d.ts.map +1 -1
  66. package/core/error-code-registry.js +6 -0
  67. package/core/error-envelope.js +2 -0
  68. package/core/errors.d.ts +1 -1
  69. package/core/errors.js +1 -1
  70. package/core/geoparquet-sql.d.ts +68 -4
  71. package/core/geoparquet-sql.d.ts.map +1 -1
  72. package/core/geoparquet-sql.js +325 -8
  73. package/core/odata-write-codec.d.ts.map +1 -1
  74. package/core/odata-write-codec.js +0 -1
  75. package/core/odata.d.ts +6 -0
  76. package/core/odata.d.ts.map +1 -1
  77. package/core/odata.js +6 -1
  78. package/core/ogc-processes.d.ts +182 -25
  79. package/core/ogc-processes.d.ts.map +1 -1
  80. package/core/ogc-processes.js +602 -30
  81. package/core/process-runner.d.ts +8 -1
  82. package/core/process-runner.d.ts.map +1 -1
  83. package/core/process-runner.js +1 -0
  84. package/core/protocol-transport.d.ts +2 -0
  85. package/core/protocol-transport.d.ts.map +1 -1
  86. package/core/stac.d.ts +31 -0
  87. package/core/stac.d.ts.map +1 -1
  88. package/core/stac.js +136 -19
  89. package/core/surfaces.d.ts.map +1 -1
  90. package/core/surfaces.js +273 -68
  91. package/core/types.d.ts +123 -9
  92. package/core/types.d.ts.map +1 -1
  93. package/core/wfs-axis-order.d.ts +18 -0
  94. package/core/wfs-axis-order.d.ts.map +1 -0
  95. package/core/wfs-axis-order.js +108 -0
  96. package/core/wfs-capabilities.d.ts +65 -24
  97. package/core/wfs-capabilities.d.ts.map +1 -1
  98. package/core/wfs-capabilities.js +237 -27
  99. package/core/wfs-filter.d.ts +26 -3
  100. package/core/wfs-filter.d.ts.map +1 -1
  101. package/core/wfs-filter.js +48 -53
  102. package/core/wfs-protocol-error.d.ts +12 -0
  103. package/core/wfs-protocol-error.d.ts.map +1 -0
  104. package/core/wfs-protocol-error.js +13 -0
  105. package/core/wfs.d.ts +53 -1
  106. package/core/wfs.d.ts.map +1 -1
  107. package/core/wfs.js +251 -29
  108. package/core/wms-feature-info.d.ts +92 -0
  109. package/core/wms-feature-info.d.ts.map +1 -0
  110. package/core/wms-feature-info.js +285 -0
  111. package/core/wms.d.ts +9 -0
  112. package/core/wms.d.ts.map +1 -1
  113. package/core/wms.js +17 -5
  114. package/esri-compat/event-bus.d.ts +20 -0
  115. package/esri-compat/event-bus.d.ts.map +1 -1
  116. package/esri-compat/feature-filter.d.ts +46 -1
  117. package/esri-compat/feature-filter.d.ts.map +1 -1
  118. package/esri-compat/feature-filter.js +49 -0
  119. package/esri-compat/geojson-layer.d.ts +4 -2
  120. package/esri-compat/geojson-layer.d.ts.map +1 -1
  121. package/esri-compat/geojson-layer.js +4 -2
  122. package/esri-compat/identity-manager.js +2 -2
  123. package/esri-compat/imagery-layer.d.ts +2 -1
  124. package/esri-compat/imagery-layer.d.ts.map +1 -1
  125. package/esri-compat/imagery-layer.js +2 -1
  126. package/esri-compat/layer-list.d.ts +8 -1
  127. package/esri-compat/layer-list.d.ts.map +1 -1
  128. package/esri-compat/layer-list.js +11 -4
  129. package/esri-compat/legend.d.ts.map +1 -1
  130. package/esri-compat/legend.js +1 -1
  131. package/esri-compat/locator.d.ts +297 -0
  132. package/esri-compat/locator.d.ts.map +1 -0
  133. package/esri-compat/locator.js +579 -0
  134. package/esri-compat/map-image-layer.d.ts.map +1 -1
  135. package/esri-compat/map-view.d.ts.map +1 -1
  136. package/esri-compat/map-view.js +1 -1
  137. package/esri-compat/scene-view.d.ts.map +1 -1
  138. package/esri-compat/time-slider.d.ts +29 -0
  139. package/esri-compat/time-slider.d.ts.map +1 -1
  140. package/esri-compat/time-slider.js +117 -0
  141. package/esri-compat/wfs-layer.d.ts +2 -1
  142. package/esri-compat/wfs-layer.d.ts.map +1 -1
  143. package/esri-compat/wfs-layer.js +2 -1
  144. package/esri-compat/widget-host.d.ts +42 -6
  145. package/esri-compat/widget-host.d.ts.map +1 -1
  146. package/esri-compat/widget-host.js +99 -10
  147. package/generated-app/projection.d.ts.map +1 -1
  148. package/generated-app/runtime.d.ts.map +1 -1
  149. package/map/data-to-map-bridge.d.ts +1 -1
  150. package/map/data-to-map-bridge.d.ts.map +1 -1
  151. package/map/data-to-map-bridge.js +1 -2
  152. package/map/honua-map.d.ts.map +1 -1
  153. package/map/honua-map.js +1 -1
  154. package/map/index.d.ts +2 -0
  155. package/map/index.d.ts.map +1 -1
  156. package/map/index.js +2 -0
  157. package/map/realtime-reconciliation-adapter.d.ts +98 -0
  158. package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
  159. package/map/realtime-reconciliation-adapter.js +84 -0
  160. package/map/temporal-playback.d.ts +25 -0
  161. package/map/temporal-playback.d.ts.map +1 -1
  162. package/map/temporal-playback.js +29 -1
  163. package/operator/workspace/workspace.d.ts.map +1 -1
  164. package/operator/workspace/workspace.js +0 -2
  165. package/package.json +4 -3
  166. package/query-planner/canonical-filter.d.ts +30 -0
  167. package/query-planner/canonical-filter.d.ts.map +1 -0
  168. package/query-planner/canonical-filter.js +30 -0
  169. package/query-planner/columnar-plan.d.ts +63 -0
  170. package/query-planner/columnar-plan.d.ts.map +1 -0
  171. package/query-planner/columnar-plan.js +94 -0
  172. package/query-planner/diagnostics.d.ts +3 -3
  173. package/query-planner/diagnostics.d.ts.map +1 -1
  174. package/query-planner/diagnostics.js +17 -4
  175. package/query-planner/duckdb.d.ts.map +1 -1
  176. package/query-planner/duckdb.js +16 -2
  177. package/query-planner/execution-context.d.ts.map +1 -1
  178. package/query-planner/execution-context.js +3 -1
  179. package/query-planner/executor.d.ts.map +1 -1
  180. package/query-planner/executor.js +7 -1
  181. package/query-planner/geoservices.d.ts.map +1 -1
  182. package/query-planner/geoservices.js +24 -5
  183. package/query-planner/grpc.d.ts.map +1 -1
  184. package/query-planner/grpc.js +29 -5
  185. package/query-planner/index.d.ts +20 -5
  186. package/query-planner/index.d.ts.map +1 -1
  187. package/query-planner/index.js +16 -3
  188. package/query-planner/ir.d.ts +6 -1
  189. package/query-planner/ir.d.ts.map +1 -1
  190. package/query-planner/ir.js +63 -1
  191. package/query-planner/odata-protocol-module.d.ts +54 -0
  192. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  193. package/query-planner/odata-protocol-module.js +199 -0
  194. package/query-planner/odata-v1.d.ts +23 -0
  195. package/query-planner/odata-v1.d.ts.map +1 -0
  196. package/query-planner/odata-v1.js +202 -0
  197. package/query-planner/odata.d.ts +0 -3
  198. package/query-planner/odata.d.ts.map +1 -1
  199. package/query-planner/odata.js +0 -143
  200. package/query-planner/ogc-features.d.ts.map +1 -1
  201. package/query-planner/ogc-features.js +23 -1
  202. package/query-planner/planner.d.ts +2 -2
  203. package/query-planner/planner.d.ts.map +1 -1
  204. package/query-planner/planner.js +146 -27
  205. package/query-planner/representation.d.ts +59 -0
  206. package/query-planner/representation.d.ts.map +1 -0
  207. package/query-planner/representation.js +140 -0
  208. package/query-planner/types.d.ts +153 -3
  209. package/query-planner/types.d.ts.map +1 -1
  210. package/query-planner/wfs-protocol-module.d.ts +57 -0
  211. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  212. package/query-planner/wfs-protocol-module.js +731 -0
  213. package/query-planner/wfs-v1.d.ts +41 -0
  214. package/query-planner/wfs-v1.d.ts.map +1 -0
  215. package/query-planner/wfs-v1.js +366 -0
  216. package/query-planner/wfs.d.ts +1 -3
  217. package/query-planner/wfs.d.ts.map +1 -1
  218. package/query-planner/wfs.js +1 -70
  219. package/realtime/checkpoint-store.d.ts +206 -0
  220. package/realtime/checkpoint-store.d.ts.map +1 -0
  221. package/realtime/checkpoint-store.js +504 -0
  222. package/realtime/honua-server.d.ts +8 -0
  223. package/realtime/honua-server.d.ts.map +1 -1
  224. package/realtime/honua-server.js +123 -14
  225. package/realtime/index.d.ts +4 -0
  226. package/realtime/index.d.ts.map +1 -1
  227. package/realtime/index.js +2 -0
  228. package/realtime/reconciliation.d.ts +245 -0
  229. package/realtime/reconciliation.d.ts.map +1 -0
  230. package/realtime/reconciliation.js +377 -0
  231. package/realtime/resumable-transport.d.ts.map +1 -1
  232. package/realtime/sse.d.ts.map +1 -1
  233. package/realtime/websocket.d.ts.map +1 -1
  234. package/realtime/wire.d.ts +2 -1
  235. package/realtime/wire.d.ts.map +1 -1
  236. package/realtime/wire.js +24 -8
  237. package/replica-sync/errors.d.ts +19 -1
  238. package/replica-sync/errors.d.ts.map +1 -1
  239. package/replica-sync/errors.js +16 -0
  240. package/replica-sync/geoservices.d.ts +260 -0
  241. package/replica-sync/geoservices.d.ts.map +1 -0
  242. package/replica-sync/geoservices.js +1093 -0
  243. package/replica-sync/index.d.ts +5 -1
  244. package/replica-sync/index.d.ts.map +1 -1
  245. package/replica-sync/index.js +3 -1
  246. package/replica-sync/transport-conformance.d.ts +58 -0
  247. package/replica-sync/transport-conformance.d.ts.map +1 -0
  248. package/replica-sync/transport-conformance.js +265 -0
  249. package/replica-sync/types.d.ts +22 -0
  250. package/replica-sync/types.d.ts.map +1 -1
  251. package/runtime/index.d.ts +6 -2
  252. package/runtime/index.d.ts.map +1 -1
  253. package/runtime/index.js +3 -1
  254. package/runtime/load-package.d.ts +24 -0
  255. package/runtime/load-package.d.ts.map +1 -1
  256. package/runtime/load-package.js +45 -1
  257. package/runtime/maplibre-renderer.d.ts +11 -0
  258. package/runtime/maplibre-renderer.d.ts.map +1 -1
  259. package/runtime/maplibre-renderer.js +32 -7
  260. package/runtime/offline-region-protocol.d.ts +73 -0
  261. package/runtime/offline-region-protocol.d.ts.map +1 -0
  262. package/runtime/offline-region-protocol.js +63 -0
  263. package/runtime/offline-region-style.d.ts +78 -0
  264. package/runtime/offline-region-style.d.ts.map +1 -0
  265. package/runtime/offline-region-style.js +147 -0
  266. package/runtime/pmtiles-protocol.d.ts +7 -8
  267. package/runtime/pmtiles-protocol.d.ts.map +1 -1
  268. package/runtime/pmtiles-protocol.js +19 -66
  269. package/runtime/protocol-registry.d.ts +55 -0
  270. package/runtime/protocol-registry.d.ts.map +1 -0
  271. package/runtime/protocol-registry.js +103 -0
  272. package/runtime/runtime.d.ts +2 -0
  273. package/runtime/runtime.d.ts.map +1 -1
  274. package/runtime/runtime.js +6 -1
  275. package/scene-workspace/analysis-widgets.d.ts +4 -2
  276. package/scene-workspace/analysis-widgets.d.ts.map +1 -1
  277. package/scene-workspace/analysis-widgets.js +4 -2
  278. package/scene-workspace/camera-correspondence.d.ts +151 -0
  279. package/scene-workspace/camera-correspondence.d.ts.map +1 -0
  280. package/scene-workspace/camera-correspondence.js +220 -0
  281. package/scene-workspace/cesium-adapter.d.ts +230 -29
  282. package/scene-workspace/cesium-adapter.d.ts.map +1 -1
  283. package/scene-workspace/cesium-adapter.js +741 -56
  284. package/scene-workspace/cesium-mount.d.ts +205 -0
  285. package/scene-workspace/cesium-mount.d.ts.map +1 -0
  286. package/scene-workspace/cesium-mount.js +392 -0
  287. package/scene-workspace/cesium-scene-owner.d.ts +160 -0
  288. package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
  289. package/scene-workspace/cesium-scene-owner.js +205 -0
  290. package/scene-workspace/cesium-state-sync.d.ts +101 -0
  291. package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
  292. package/scene-workspace/cesium-state-sync.js +432 -0
  293. package/scene-workspace/cesium-time.d.ts +346 -0
  294. package/scene-workspace/cesium-time.d.ts.map +1 -0
  295. package/scene-workspace/cesium-time.js +403 -0
  296. package/scene-workspace/index.d.ts +42 -10
  297. package/scene-workspace/index.d.ts.map +1 -1
  298. package/scene-workspace/index.js +32 -7
  299. package/scene-workspace/maplibre-state-sync.d.ts +112 -0
  300. package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
  301. package/scene-workspace/maplibre-state-sync.js +400 -0
  302. package/scene-workspace/primitives.d.ts +237 -3
  303. package/scene-workspace/primitives.d.ts.map +1 -1
  304. package/scene-workspace/primitives.js +1087 -17
  305. package/scene-workspace/scene-discovery.d.ts +5 -2
  306. package/scene-workspace/scene-discovery.d.ts.map +1 -1
  307. package/scene-workspace/scene-discovery.js +5 -2
  308. package/scene-workspace/scene-view.d.ts +5 -2
  309. package/scene-workspace/scene-view.d.ts.map +1 -1
  310. package/scene-workspace/scene-view.js +5 -2
  311. package/scene-workspace/source-to-cesium.d.ts +141 -8
  312. package/scene-workspace/source-to-cesium.d.ts.map +1 -1
  313. package/scene-workspace/source-to-cesium.js +497 -72
  314. package/scene-workspace/state-sync-port-kit.d.ts +156 -0
  315. package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
  316. package/scene-workspace/state-sync-port-kit.js +319 -0
  317. package/scene-workspace/state-sync.d.ts +34 -1
  318. package/scene-workspace/state-sync.d.ts.map +1 -1
  319. package/scene-workspace/state-sync.js +48 -0
  320. package/scene-workspace/types.d.ts +8 -0
  321. package/scene-workspace/types.d.ts.map +1 -1
  322. package/scene-workspace/types.js +2 -0
  323. package/scene-workspace/workspace.d.ts +12 -1
  324. package/scene-workspace/workspace.d.ts.map +1 -1
  325. package/scene-workspace/workspace.js +13 -0
  326. package/studio/index.d.ts +24 -7
  327. package/studio/index.d.ts.map +1 -1
  328. package/studio/index.js +21 -7
  329. package/studio/lifecycle-client.d.ts +164 -0
  330. package/studio/lifecycle-client.d.ts.map +1 -0
  331. package/studio/lifecycle-client.js +299 -0
  332. package/studio/lifecycle-errors.d.ts +91 -0
  333. package/studio/lifecycle-errors.d.ts.map +1 -0
  334. package/studio/lifecycle-errors.js +77 -0
  335. package/studio/lifecycle-types.d.ts +321 -0
  336. package/studio/lifecycle-types.d.ts.map +1 -0
  337. package/studio/lifecycle-types.js +22 -0
  338. package/web-components/controller.d.ts.map +1 -1
  339. package/web-components/csp-styles.d.ts +6 -0
  340. package/web-components/csp-styles.d.ts.map +1 -0
  341. package/web-components/csp-styles.js +53 -0
  342. package/web-components/elements.d.ts +158 -2
  343. package/web-components/elements.d.ts.map +1 -1
  344. package/web-components/elements.js +1302 -215
  345. package/web-components/export-redaction.d.ts +252 -0
  346. package/web-components/export-redaction.d.ts.map +1 -0
  347. package/web-components/export-redaction.js +588 -0
  348. package/web-components/export.d.ts +417 -0
  349. package/web-components/export.d.ts.map +1 -0
  350. package/web-components/export.js +719 -0
  351. package/web-components/feature-editor-model.d.ts +265 -0
  352. package/web-components/feature-editor-model.d.ts.map +1 -0
  353. package/web-components/feature-editor-model.js +438 -0
  354. package/web-components/feature-editor-sketch.d.ts +125 -0
  355. package/web-components/feature-editor-sketch.d.ts.map +1 -0
  356. package/web-components/feature-editor-sketch.js +194 -0
  357. package/web-components/feature-editor-workflow.d.ts +286 -0
  358. package/web-components/feature-editor-workflow.d.ts.map +1 -0
  359. package/web-components/feature-editor-workflow.js +889 -0
  360. package/web-components/feature-editor.d.ts +83 -0
  361. package/web-components/feature-editor.d.ts.map +1 -0
  362. package/web-components/feature-editor.js +829 -0
  363. package/web-components/feature-table-engine.d.ts +465 -0
  364. package/web-components/feature-table-engine.d.ts.map +1 -0
  365. package/web-components/feature-table-engine.js +1360 -0
  366. package/web-components/feature-table-view.d.ts +95 -0
  367. package/web-components/feature-table-view.d.ts.map +1 -0
  368. package/web-components/feature-table-view.js +343 -0
  369. package/web-components/index.d.ts +34 -3
  370. package/web-components/index.d.ts.map +1 -1
  371. package/web-components/index.js +35 -2
  372. package/web-components/maplibre-renderer.d.ts.map +1 -1
  373. package/web-components/maplibre-renderer.js +13 -4
  374. package/web-components/measurement.d.ts +3 -1
  375. package/web-components/measurement.d.ts.map +1 -1
  376. package/web-components/measurement.js +62 -18
  377. package/web-components/time-slider.d.ts +122 -0
  378. package/web-components/time-slider.d.ts.map +1 -0
  379. package/web-components/time-slider.js +740 -0
  380. package/web-components/types.d.ts +310 -0
  381. package/web-components/types.d.ts.map +1 -1
  382. package/webmap/convert-label.js +0 -1
  383. package/webmap/convert-symbol.d.ts.map +1 -1
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Credential-safe redaction primitives for the Honua component kit's export
3
+ * pipeline (issue #683, parent epic #678 "app-platform component production
4
+ * readiness"; REQ-002).
5
+ *
6
+ * The export pipeline is **redact-by-default in two independent layers**, and
7
+ * both layers run on every export regardless of who supplied the state:
8
+ *
9
+ * 1. **Allowlist projection.** {@link sanitizeHonuaExportState} never walks a
10
+ * caller's state object generically. It projects a fixed, enumerated set of
11
+ * presentation fields (viewport, layer identity/visibility/opacity, legend
12
+ * labels, filter text, selection identity, source identity + attribution)
13
+ * into a new object. Anything the projection does not name — arbitrary
14
+ * `metadata` bags, request headers, credential stores, feature attributes,
15
+ * locator query strings — simply has no path into the output. New fields
16
+ * added to `HonuaWebComponentState` upstream are therefore *excluded* until
17
+ * somebody deliberately allowlists them, which is the safe default
18
+ * direction for a security boundary.
19
+ * 2. **Outbound scrub + assertion.** Every string that survives projection is
20
+ * passed through {@link redactHonuaExportText}, URLs are reduced to
21
+ * origin+path, and the finished payload is re-scanned by
22
+ * {@link assertCredentialFreeExportText}. A scan hit is a *hard failure*
23
+ * (`HonuaExportSafetyError`), not a silent scrub — a leak that reaches the
24
+ * assertion means the projection has a bug, and failing closed is the only
25
+ * honest response.
26
+ *
27
+ * The same primitives are applied to filenames ({@link
28
+ * sanitizeHonuaExportFilename}) and to every human-readable message the export
29
+ * pipeline puts on an event or a log line ({@link redactHonuaExportText}),
30
+ * because REQ-002 covers "exported bytes, serialized state, logs, events, or
31
+ * filenames" — a token leaked through a download filename or an error string
32
+ * is exactly as disclosed as one leaked through the payload.
33
+ *
34
+ * This module is deliberately dependency-free apart from the SDK's shared
35
+ * error base: no `node:crypto` (unlike `src/diagnostics/sanitize.ts`, which is
36
+ * a Node-side bundle helper), no DOM, no renderer, no localization framework.
37
+ * It is safe to import in a browser, a worker, or an SSR render pass, and
38
+ * carries no weight that NFR-001 forbids in the component bundles.
39
+ *
40
+ * @module
41
+ */
42
+ import { HonuaSdkError } from "../core/error-envelope.js";
43
+ /** Schema id stamped on every sanitized state document. */
44
+ export const HONUA_EXPORT_STATE_SCHEMA = "honua.app-platform.export-state.v1";
45
+ /** Placeholder substituted for any value the export pipeline refuses to emit. */
46
+ export const HONUA_EXPORT_REDACTED = "[REDACTED]";
47
+ /** Thrown when the export pipeline cannot produce a provably credential-free artifact. */
48
+ export class HonuaExportSafetyError extends HonuaSdkError {
49
+ reason;
50
+ constructor(message, reason = "assertion") {
51
+ super("app.export-unsafe", message);
52
+ this.name = "HonuaExportSafetyError";
53
+ this.reason = reason;
54
+ }
55
+ }
56
+ // ── credential recognition ───────────────────────────────────────────────
57
+ /**
58
+ * Property names that are never exported, whatever their value. Matched
59
+ * case-insensitively against the whole key (substring match), so `apiKey`,
60
+ * `X-API-KEY`, `refresh_token`, and `sessionCookie` are all caught.
61
+ */
62
+ const SENSITIVE_KEY = /(?:api[-_]?key|auth|bearer|cookie|credential|cred(?:s)?\b|jwt|nonce|pass(?:word|wd)?|private[-_]?key|pwd|secret|session|sig(?:nature)?|subscription[-_]?key|token)/i;
63
+ /** Property names that leak *authorization scope* rather than the secret itself. */
64
+ const AUTHORIZATION_SCOPE_KEY = /(?:^|[-_.])(?:scope|scopes|audience|aud|tenant|tenant[-_]?id|client[-_]?id|grant|grant[-_]?type|roles|permissions|principal|subject|assume[-_]?role)(?:$|[-_.])/i;
65
+ /** Query-parameter names that make a URL a *signed* URL (presigned S3/GCS/SAS/Esri/CDN). */
66
+ const SIGNED_QUERY_NAME = /(?:^|[-_])(?:sig|signature|token|access[-_]?token|id[-_]?token|refresh[-_]?token|api[-_]?key|apikey|key|subscription[-_]?key|code|password|pwd|auth|jwt|hmac|policy|credential|se|sp|sv|st|srt|ss|skoid|sktid|sig)(?:$|[-_])/i;
67
+ /**
68
+ * Free-text credential shapes. Every pattern is a linear scan over bounded
69
+ * character classes — no nested quantifiers, no backtracking traps (the repo's
70
+ * CodeQL ReDoS rule).
71
+ */
72
+ const CREDENTIAL_PATTERNS = [
73
+ [/\b(?:Bearer|Basic|Digest|Negotiate)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "[REDACTED_AUTH]"],
74
+ // `scheme://user:password@host` — basic-auth credentials embedded in a URL.
75
+ // `projectExportEndpoint` refuses these structurally, but a URL can also reach
76
+ // an export through an adapter's free-text error message, so the text scan has
77
+ // to recognize the same shape.
78
+ [/\b[a-z][a-z0-9+.-]{1,31}:\/\/[^\s/@:]{1,128}:[^\s/@]{1,128}@/gi, "[REDACTED_URL_CREDENTIALS]@"],
79
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g, "[REDACTED_JWT]"],
80
+ [/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
81
+ [/\bASIA[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
82
+ [/\bAIza[0-9A-Za-z_-]{20,}/g, "[REDACTED_PROVIDER_TOKEN]"],
83
+ [/\bgh[pousr]_[A-Za-z0-9]{16,}/g, "[REDACTED_PROVIDER_TOKEN]"],
84
+ [/\bglpat-[A-Za-z0-9_-]{10,}/g, "[REDACTED_PROVIDER_TOKEN]"],
85
+ [/\bxox[baprs]-[A-Za-z0-9-]{8,}/gi, "[REDACTED_PROVIDER_TOKEN]"],
86
+ [/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{8,}/gi, "[REDACTED_PROVIDER_TOKEN]"],
87
+ [/\bsk-[A-Za-z0-9]{16,}/g, "[REDACTED_PROVIDER_TOKEN]"],
88
+ // Labelled `name=value` / `name: value` pairs. The name alternation covers
89
+ // the short, provider-specific parameter names real signed URLs use — Azure
90
+ // SAS `sig`/`sv`/`se`/`st`, AWS/GCS `x-amz-signature`/`x-goog-signature`, and
91
+ // the bare `key` that tile providers favour — because those values are
92
+ // frequently *short* (a 24-character SAS signature is common) and so slip
93
+ // under any entropy heuristic. `\b` before each name keeps `turnkey=` and
94
+ // `monkey=` from matching.
95
+ [
96
+ /\b(access[-_]?key|api[-_]?key|apikey|authorization|client[-_]?secret|cookie|credential|jwt|key|pass(?:password)?|password|pwd|se|secret|session|sig|signature|st|subscription[-_]?key|sv|token|x-amz-[a-z-]{1,32}|x-goog-[a-z-]{1,32})\s*[:=]\s*(?:"[^"]{0,512}"|'[^']{0,512}'|[^\s&,;}"']{1,512})/gi,
97
+ `$1=${HONUA_EXPORT_REDACTED}`,
98
+ ],
99
+ ];
100
+ /**
101
+ * High-entropy blob heuristic, applied last so labelled matches win first.
102
+ *
103
+ * Deliberately excludes `/` and `-` from the character class. Including them
104
+ * made ordinary URL paths match — `arcgis/rest/services/Roads/FeatureServer/0`
105
+ * is 41 characters of `[A-Za-z0-9/]` and was being reported as a secret, which
106
+ * would have thrown away the endpoint an attribution block legitimately needs.
107
+ * Base64 blobs containing `/` still get caught: in practice they appear as a
108
+ * labelled parameter value or an `Authorization` header, both of which the
109
+ * patterns above match first.
110
+ */
111
+ const OPAQUE_SECRET = /\b[A-Za-z0-9_+=]{40,}\b/g;
112
+ function decodePercentEncoding(value) {
113
+ let decoded = value;
114
+ for (let pass = 0; pass < 2; pass += 1) {
115
+ if (!decoded.includes("%"))
116
+ break;
117
+ let next;
118
+ try {
119
+ next = decodeURIComponent(decoded);
120
+ }
121
+ catch {
122
+ break;
123
+ }
124
+ if (next === decoded)
125
+ break;
126
+ decoded = next;
127
+ }
128
+ return decoded;
129
+ }
130
+ /**
131
+ * Redacts credential material from arbitrary human-readable text: adapter
132
+ * error messages, titles, status strings, log lines, event detail messages.
133
+ *
134
+ * Percent-encoded payloads are decoded (twice, bounded) before matching so a
135
+ * `%42earer%20abc` style evasion cannot slip a token past the scan; the
136
+ * returned string is the redacted *decoded* form when decoding changed
137
+ * anything, because emitting the still-encoded original would defeat the
138
+ * point.
139
+ */
140
+ export function redactHonuaExportText(value) {
141
+ if (value.length === 0)
142
+ return value;
143
+ let redacted = decodePercentEncoding(value);
144
+ for (const [pattern, replacement] of CREDENTIAL_PATTERNS) {
145
+ redacted = redacted.replace(pattern, replacement);
146
+ }
147
+ return redacted.replace(OPAQUE_SECRET, "[REDACTED_TOKEN]");
148
+ }
149
+ /** Whether `value` still looks like it carries credential material. */
150
+ export function containsCredentialMaterial(value) {
151
+ return redactHonuaExportText(value) !== decodePercentEncoding(value);
152
+ }
153
+ /**
154
+ * Fail-closed assertion for finished export payloads (REQ-002). Throws
155
+ * {@link HonuaExportSafetyError} rather than scrubbing, because reaching this
156
+ * point with credential material means an earlier layer is broken.
157
+ */
158
+ export function assertCredentialFreeExportText(value, label) {
159
+ if (containsCredentialMaterial(value)) {
160
+ throw new HonuaExportSafetyError(`${label} still contains credential material after redaction; refusing to emit the export.`, "assertion");
161
+ }
162
+ }
163
+ // ── binary artifact scanning ─────────────────────────────────────────────
164
+ /** Shortest printable run worth scanning. Below this it is structural noise, not text. */
165
+ const MIN_PRINTABLE_RUN = 4;
166
+ /** Printable text accumulated before a chunk is scanned and flushed. */
167
+ const PRINTABLE_CHUNK_BYTES = 1 << 20;
168
+ /**
169
+ * Characters carried between chunks so a token straddling a flush boundary is
170
+ * still matched. Comfortably longer than the longest pattern this module has.
171
+ */
172
+ const PRINTABLE_CHUNK_OVERLAP = 512;
173
+ /**
174
+ * Extracts runs of printable ASCII from a binary buffer, joined by newlines.
175
+ *
176
+ * This is the `strings(1)` idea, and it exists because "binary" is not a
177
+ * security boundary. A PNG `tEXt`/`iTXt` chunk, a JPEG `COM` marker, PDF
178
+ * metadata or an embedded XMP packet, an EXIF `UserComment`, a ZIP comment —
179
+ * all carry plainly readable text inside a buffer that will never decode as
180
+ * UTF-8 as a whole. Scanning only whole-buffer-decodable artifacts would let a
181
+ * token ride out inside any of them.
182
+ *
183
+ * Runs shorter than {@link MIN_PRINTABLE_RUN} are dropped: they are the
184
+ * incidental byte values that fall in the ASCII range inside compressed data,
185
+ * and keeping them would produce noise without ever forming a credential.
186
+ */
187
+ export function extractPrintableRuns(bytes, start = 0, end = bytes.byteLength) {
188
+ const runs = [];
189
+ let current = "";
190
+ for (let index = start; index < end; index += 1) {
191
+ const byte = bytes[index];
192
+ // Printable ASCII plus tab. Deliberately not the full latin-1 upper range:
193
+ // every credential shape this module recognizes is ASCII, and admitting
194
+ // 0x80-0xFF would turn arbitrary compressed data into "text".
195
+ if ((byte >= 0x20 && byte <= 0x7e) || byte === 0x09) {
196
+ current += String.fromCharCode(byte);
197
+ continue;
198
+ }
199
+ if (current.length >= MIN_PRINTABLE_RUN)
200
+ runs.push(current);
201
+ current = "";
202
+ }
203
+ if (current.length >= MIN_PRINTABLE_RUN)
204
+ runs.push(current);
205
+ return runs.join("\n");
206
+ }
207
+ /**
208
+ * Fail-closed credential scan over a binary artifact (REQ-002).
209
+ *
210
+ * Scans in bounded chunks with an overlap, so an arbitrarily large artifact
211
+ * costs bounded memory while a token that straddles a chunk boundary is still
212
+ * caught. Throws {@link HonuaExportSafetyError} on a hit, like the text scan —
213
+ * a credential surviving to this point means an earlier layer is broken, and
214
+ * emitting the bytes anyway is the one outcome that cannot be walked back.
215
+ */
216
+ export function assertCredentialFreeExportBytes(bytes, label) {
217
+ let carry = "";
218
+ for (let offset = 0; offset < bytes.byteLength; offset += PRINTABLE_CHUNK_BYTES) {
219
+ const end = Math.min(bytes.byteLength, offset + PRINTABLE_CHUNK_BYTES);
220
+ const printable = extractPrintableRuns(bytes, offset, end);
221
+ if (printable.length === 0 && carry.length === 0)
222
+ continue;
223
+ assertCredentialFreeExportText(`${carry}${printable}`, label);
224
+ carry = printable.slice(-PRINTABLE_CHUNK_OVERLAP);
225
+ }
226
+ }
227
+ /**
228
+ * Reduces a source URL to `origin + pathname`, always dropping the query
229
+ * string and fragment.
230
+ *
231
+ * Query strings are where every practical map-service credential lives (Esri
232
+ * `?token=`, Azure SAS `?sv=...&sig=`, S3/GCS presigned `?X-Amz-Signature=`,
233
+ * tile-provider `?api_key=`), and a partial per-parameter allowlist has to be
234
+ * right about every provider forever. Dropping the whole query is the only
235
+ * rule that is right by construction; the origin+path that remains is what an
236
+ * attribution/provenance block legitimately needs.
237
+ *
238
+ * Non-HTTP(S) schemes (`data:`, `blob:`, `file:`), URLs with embedded
239
+ * `user:password@` userinfo, and unparseable strings yield no endpoint at all.
240
+ */
241
+ export function projectExportEndpoint(url) {
242
+ const trimmed = url.trim();
243
+ if (trimmed.length === 0 || trimmed.length > 4096) {
244
+ return { endpoint: undefined, reason: "unsupported-value" };
245
+ }
246
+ let parsed;
247
+ try {
248
+ parsed = new URL(trimmed);
249
+ }
250
+ catch {
251
+ // Relative locators carry no host and therefore no credential context, but
252
+ // they can still carry a query string; keep only the path portion.
253
+ const [path] = trimmed.split(/[?#]/, 1);
254
+ if (path.length === 0 || containsCredentialMaterial(path)) {
255
+ return { endpoint: undefined, reason: "credential-pattern" };
256
+ }
257
+ return path === trimmed ? { endpoint: path } : { endpoint: path, reason: "signed-url" };
258
+ }
259
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
260
+ return { endpoint: undefined, reason: "unsupported-value" };
261
+ }
262
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
263
+ return { endpoint: undefined, reason: "signed-url" };
264
+ }
265
+ const hadSecretQuery = [...parsed.searchParams.keys()].some((name) => SIGNED_QUERY_NAME.test(name) || SENSITIVE_KEY.test(name));
266
+ const endpoint = `${parsed.origin}${parsed.pathname}`;
267
+ if (containsCredentialMaterial(endpoint)) {
268
+ return { endpoint: undefined, reason: "credential-pattern" };
269
+ }
270
+ const droppedSomething = parsed.search.length > 0 || parsed.hash.length > 0;
271
+ if (hadSecretQuery)
272
+ return { endpoint, reason: "signed-url" };
273
+ return droppedSomething ? { endpoint, reason: "unsupported-value" } : { endpoint };
274
+ }
275
+ // ── filenames ────────────────────────────────────────────────────────────
276
+ const FILENAME_FALLBACK = "honua-export";
277
+ const FILENAME_MAX_LENGTH = 96;
278
+ /** Extension chosen for a media type; unknown types get no extension. */
279
+ const MEDIA_TYPE_EXTENSIONS = {
280
+ "application/json": "json",
281
+ "application/geo+json": "geojson",
282
+ "application/pdf": "pdf",
283
+ "image/png": "png",
284
+ "image/jpeg": "jpg",
285
+ "image/webp": "webp",
286
+ "image/svg+xml": "svg",
287
+ "text/html": "html",
288
+ "text/plain": "txt",
289
+ };
290
+ /**
291
+ * Produces a download filename that cannot disclose a credential (REQ-002
292
+ * "…or filenames").
293
+ *
294
+ * A title that matches any credential shape is **discarded wholesale** rather
295
+ * than partially redacted: emitting `map-REDACTED_TOKEN-tiles.png` still tells
296
+ * an observer that a token was present and can preserve unmatched prefixes of
297
+ * it. The result is always confined to `[A-Za-z0-9._-]`, never begins with a
298
+ * dot, contains no path separators, and is length-bounded.
299
+ */
300
+ export function sanitizeHonuaExportFilename(options = {}) {
301
+ const prefix = slugifyFilenamePart(options.prefix ?? "honua") || "honua";
302
+ const rawTitle = options.title ?? "";
303
+ const titleSlug = containsCredentialMaterial(rawTitle) ? "" : slugifyFilenamePart(rawTitle);
304
+ const stem = (titleSlug.length > 0 ? `${prefix}-${titleSlug}` : FILENAME_FALLBACK).slice(0, FILENAME_MAX_LENGTH);
305
+ const extension = MEDIA_TYPE_EXTENSIONS[(options.mediaType ?? "").split(";", 1)[0].trim().toLowerCase()];
306
+ const filename = extension ? `${stem}.${extension}` : stem;
307
+ assertCredentialFreeExportText(filename, "Export filename");
308
+ return filename;
309
+ }
310
+ function slugifyFilenamePart(value) {
311
+ let slug = "";
312
+ let pendingSeparator = false;
313
+ for (const character of value.slice(0, 512)) {
314
+ const code = character.charCodeAt(0);
315
+ const safe = (code >= 48 && code <= 57) ||
316
+ (code >= 65 && code <= 90) ||
317
+ (code >= 97 && code <= 122) ||
318
+ character === "." ||
319
+ character === "_";
320
+ if (safe && !(character === "." && slug.length === 0)) {
321
+ if (pendingSeparator && slug.length > 0)
322
+ slug += "-";
323
+ pendingSeparator = false;
324
+ slug += character;
325
+ continue;
326
+ }
327
+ pendingSeparator = slug.length > 0;
328
+ }
329
+ return slug.slice(0, FILENAME_MAX_LENGTH);
330
+ }
331
+ /** Binding metadata keys that mark a source non-exportable. */
332
+ const NON_EXPORTABLE_METADATA_KEYS = ["honua:exportable", "exportable"];
333
+ function isNonExportableBinding(binding) {
334
+ const metadata = binding.metadata;
335
+ if (!metadata)
336
+ return false;
337
+ for (const key of NON_EXPORTABLE_METADATA_KEYS) {
338
+ const value = metadata[key];
339
+ if (typeof value === "string" && ["false", "no", "0", "off"].includes(value.trim().toLowerCase()))
340
+ return true;
341
+ }
342
+ const explicit = metadata["honua:export"] ?? metadata.export;
343
+ return typeof explicit === "string" && explicit.trim().toLowerCase() === "forbidden";
344
+ }
345
+ /** Redacts a short label-ish string, recording a redaction when it changed. */
346
+ function projectText(value, path, redactions, reason = "credential-pattern") {
347
+ if (value === undefined)
348
+ return undefined;
349
+ const redacted = redactHonuaExportText(value);
350
+ if (redacted !== value)
351
+ redactions.push({ path, reason });
352
+ const bounded = redacted.slice(0, 512);
353
+ return bounded.length > 0 ? bounded : undefined;
354
+ }
355
+ function projectViewport(viewport) {
356
+ if (!viewport)
357
+ return undefined;
358
+ const projected = {};
359
+ const bbox = viewport.bbox;
360
+ const center = viewport.center;
361
+ if (bbox?.every((value) => Number.isFinite(value)))
362
+ projected.bbox = [...bbox];
363
+ if (center?.every((value) => Number.isFinite(value)))
364
+ projected.center = [center[0], center[1]];
365
+ for (const key of ["zoom", "pitch", "bearing"]) {
366
+ const value = viewport[key];
367
+ if (Number.isFinite(value))
368
+ projected[key] = Number(value);
369
+ }
370
+ return Object.keys(projected).length > 0 ? projected : undefined;
371
+ }
372
+ function projectSources(mapPackage, nonExportable, redactions) {
373
+ const bindings = mapPackage?.sourceBindings ?? [];
374
+ const sources = [];
375
+ for (const [index, binding] of bindings.entries()) {
376
+ const path = `sources[${index}]`;
377
+ const sourceId = projectText(binding.sourceId, `${path}.sourceId`, redactions) ?? `source-${index}`;
378
+ if (nonExportable.has(binding.sourceId) || isNonExportableBinding(binding)) {
379
+ redactions.push({ path, reason: "non-exportable-source" });
380
+ sources.push({ sourceId, omitted: true });
381
+ continue;
382
+ }
383
+ const source = { sourceId };
384
+ const protocol = projectText(typeof binding.protocol === "string" ? binding.protocol : undefined, `${path}.protocol`, redactions);
385
+ if (protocol)
386
+ source.protocol = protocol;
387
+ const rawUrl = typeof binding.locator?.url === "string" ? binding.locator.url : undefined;
388
+ if (rawUrl) {
389
+ const projection = projectExportEndpoint(rawUrl);
390
+ if (projection.reason)
391
+ redactions.push({ path: `${path}.endpoint`, reason: projection.reason });
392
+ if (projection.endpoint)
393
+ source.endpoint = projection.endpoint;
394
+ }
395
+ // `binding.metadata` and every other locator field are intentionally not
396
+ // projected: they are free-form round-trip bags that routinely carry
397
+ // service keys, and an export has no need for them.
398
+ if (binding.metadata && Object.keys(binding.metadata).length > 0) {
399
+ redactions.push({ path: `${path}.metadata`, reason: "private-header" });
400
+ }
401
+ const attribution = projectText(binding.attribution, `${path}.attribution`, redactions);
402
+ if (attribution)
403
+ source.attribution = attribution;
404
+ sources.push(source);
405
+ }
406
+ return sources;
407
+ }
408
+ function projectLayers(layers, omittedSourceIds, redactions) {
409
+ const projected = [];
410
+ for (const [index, layer] of layers.entries()) {
411
+ const path = `layers[${index}]`;
412
+ if (layer.sourceId && omittedSourceIds.has(layer.sourceId)) {
413
+ redactions.push({ path, reason: "non-exportable-source" });
414
+ continue;
415
+ }
416
+ const entry = {
417
+ id: projectText(layer.id, `${path}.id`, redactions) ?? `layer-${index}`,
418
+ title: projectText(layer.title, `${path}.title`, redactions) ?? "",
419
+ visible: layer.visible === true,
420
+ };
421
+ if (Number.isFinite(layer.opacity))
422
+ entry.opacity = Number(layer.opacity);
423
+ const sourceId = projectText(layer.sourceId, `${path}.sourceId`, redactions);
424
+ if (sourceId)
425
+ entry.sourceId = sourceId;
426
+ const type = projectText(layer.type, `${path}.type`, redactions);
427
+ if (type)
428
+ entry.type = type;
429
+ // `layer.metadata` is an untyped bag; never projected.
430
+ if (layer.metadata && Object.keys(layer.metadata).length > 0) {
431
+ redactions.push({ path: `${path}.metadata`, reason: "private-header" });
432
+ }
433
+ projected.push(entry);
434
+ }
435
+ return projected;
436
+ }
437
+ function projectLegend(legend, redactions) {
438
+ return legend.map((item, index) => {
439
+ const path = `legend[${index}]`;
440
+ const entry = {
441
+ id: projectText(item.id, `${path}.id`, redactions) ?? `legend-${index}`,
442
+ label: projectText(item.label, `${path}.label`, redactions) ?? "",
443
+ };
444
+ const color = projectText(item.color, `${path}.color`, redactions);
445
+ if (color)
446
+ entry.color = color;
447
+ const layerId = projectText(item.layerId, `${path}.layerId`, redactions);
448
+ if (layerId)
449
+ entry.layerId = layerId;
450
+ // `iconUrl` is deliberately dropped: legend icons are routinely signed
451
+ // service URLs, and an export document has no way to re-sign them.
452
+ if (item.iconUrl)
453
+ redactions.push({ path: `${path}.iconUrl`, reason: "signed-url" });
454
+ return entry;
455
+ });
456
+ }
457
+ function projectFilters(filters, omittedSourceIds, redactions) {
458
+ if (!filters)
459
+ return [];
460
+ const projected = [];
461
+ for (const [key, filter] of Object.entries(filters)) {
462
+ if (!filter)
463
+ continue;
464
+ const path = `filters[${key}]`;
465
+ const sourceId = projectText(filter.sourceId ?? key, `${path}.sourceId`, redactions);
466
+ if (sourceId && omittedSourceIds.has(sourceId)) {
467
+ redactions.push({ path, reason: "non-exportable-source" });
468
+ continue;
469
+ }
470
+ const entry = {};
471
+ if (sourceId)
472
+ entry.sourceId = sourceId;
473
+ const text = projectText(filter.text, `${path}.text`, redactions);
474
+ if (text)
475
+ entry.text = text;
476
+ const expression = projectText(filter.expression, `${path}.expression`, redactions);
477
+ if (expression)
478
+ entry.expression = expression;
479
+ if (Object.keys(entry).length > 0)
480
+ projected.push(entry);
481
+ }
482
+ return projected;
483
+ }
484
+ function projectSelection(selection, redactions) {
485
+ if (!selection)
486
+ return undefined;
487
+ const entry = {};
488
+ const sourceId = projectText(selection.sourceId, "selection.sourceId", redactions);
489
+ if (sourceId)
490
+ entry.sourceId = sourceId;
491
+ const sourceLayer = projectText(selection.sourceLayer, "selection.sourceLayer", redactions);
492
+ if (sourceLayer)
493
+ entry.sourceLayer = sourceLayer;
494
+ const featureId = selection.featureId;
495
+ if (typeof featureId === "number" && Number.isFinite(featureId))
496
+ entry.featureId = featureId;
497
+ else if (typeof featureId === "string") {
498
+ const projectedId = projectText(featureId, "selection.featureId", redactions);
499
+ if (projectedId)
500
+ entry.featureId = projectedId;
501
+ }
502
+ // `selection.feature` (full attributes + geometry) is never projected: an
503
+ // export of the *view state* has no business carrying customer records.
504
+ if (selection.feature)
505
+ redactions.push({ path: "selection.feature", reason: "unsupported-value" });
506
+ return Object.keys(entry).length > 0 ? entry : undefined;
507
+ }
508
+ function projectTimestamp(value, path, redactions) {
509
+ if (typeof value !== "string" || value.length === 0 || value.length > 64)
510
+ return undefined;
511
+ if (Number.isNaN(Date.parse(value))) {
512
+ redactions.push({ path, reason: "unsupported-value" });
513
+ return undefined;
514
+ }
515
+ return value;
516
+ }
517
+ /**
518
+ * Projects live component state into the credential-free export document
519
+ * (REQ-002). Never throws for hostile input — hostile values are withheld and
520
+ * recorded — but the finished document is asserted credential-free by
521
+ * {@link assertCredentialFreeExportText} in the export runner before any byte
522
+ * reaches a caller.
523
+ */
524
+ export function sanitizeHonuaExportState(state, options = {}) {
525
+ const redactions = [];
526
+ const nonExportable = new Set(options.nonExportableSourceIds ?? []);
527
+ const sources = projectSources(state?.mapPackage, nonExportable, redactions);
528
+ const omittedSourceIds = new Set(sources.filter((source) => source.omitted).map((source) => source.sourceId));
529
+ const document = {
530
+ schema: HONUA_EXPORT_STATE_SCHEMA,
531
+ layers: projectLayers(state?.layers ?? [], omittedSourceIds, redactions),
532
+ legend: projectLegend(state?.legend ?? [], redactions),
533
+ sources,
534
+ filters: projectFilters(state?.filters, omittedSourceIds, redactions),
535
+ redactions,
536
+ };
537
+ const packageId = projectText(state?.packageId, "packageId", redactions);
538
+ if (packageId)
539
+ document.packageId = packageId;
540
+ const viewport = projectViewport(state?.viewport);
541
+ if (viewport)
542
+ document.viewport = viewport;
543
+ const selection = projectSelection(state?.selection, redactions);
544
+ if (selection)
545
+ document.selection = selection;
546
+ const refreshedAt = projectTimestamp(state?.refreshedAt, "refreshedAt", redactions);
547
+ if (refreshedAt)
548
+ document.refreshedAt = refreshedAt;
549
+ if (typeof state?.stale === "boolean")
550
+ document.stale = state.stale;
551
+ return { state: document, redactions };
552
+ }
553
+ /**
554
+ * Header allowlist for adapters that make their own requests while producing an
555
+ * export (a server-side renderer, a tile fetch for a print layout). Everything
556
+ * outside this set is a private header for export purposes (REQ-002).
557
+ */
558
+ const EXPORTABLE_HEADERS = new Set(["accept", "accept-language", "content-type", "user-agent"]);
559
+ /**
560
+ * Filters a header map down to the export allowlist, recording every drop.
561
+ * Exposed so adapters can prove — in their own tests — that no `Authorization`,
562
+ * `Cookie`, or provider-specific key header rides along with export metadata.
563
+ */
564
+ export function sanitizeHonuaExportHeaders(headers) {
565
+ const redactions = [];
566
+ const output = {};
567
+ for (const [rawName, rawValue] of Object.entries(headers ?? {})) {
568
+ const name = rawName.toLowerCase();
569
+ if (!EXPORTABLE_HEADERS.has(name)) {
570
+ redactions.push({
571
+ path: `headers[${name}]`,
572
+ reason: SENSITIVE_KEY.test(name) || AUTHORIZATION_SCOPE_KEY.test(name) ? "sensitive-key" : "private-header",
573
+ });
574
+ continue;
575
+ }
576
+ const value = redactHonuaExportText(rawValue);
577
+ if (value !== rawValue) {
578
+ redactions.push({ path: `headers[${name}]`, reason: "credential-pattern" });
579
+ continue;
580
+ }
581
+ output[name] = value.slice(0, 512);
582
+ }
583
+ return { headers: output, redactions };
584
+ }
585
+ /** Whether a property name is refused outright by the export pipeline. */
586
+ export function isSensitiveExportKey(name) {
587
+ return SENSITIVE_KEY.test(name) || AUTHORIZATION_SCOPE_KEY.test(name);
588
+ }