@honua/sdk-esri-compat 0.1.7-beta.0 → 0.1.9-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/core/client.js +1 -1
  2. package/core/credential-redaction.d.ts +163 -0
  3. package/core/credential-redaction.d.ts.map +1 -0
  4. package/core/credential-redaction.js +389 -0
  5. package/core/error-classifications.d.ts +15 -0
  6. package/core/error-classifications.d.ts.map +1 -1
  7. package/core/error-classifications.js +15 -0
  8. package/core/error-code-registry.d.ts +15 -0
  9. package/core/error-code-registry.d.ts.map +1 -1
  10. package/core/error-code-registry.js +15 -0
  11. package/core/grpc-adapter.d.ts +2 -2
  12. package/core/grpc-adapter.d.ts.map +1 -1
  13. package/core/grpc-adapter.js +30 -6
  14. package/core/job-run-errors.d.ts +10 -0
  15. package/core/job-run-errors.d.ts.map +1 -0
  16. package/core/job-run-errors.js +14 -0
  17. package/core/job-run-lifecycle.d.ts +34 -0
  18. package/core/job-run-lifecycle.d.ts.map +1 -0
  19. package/core/job-run-lifecycle.js +193 -0
  20. package/core/ogc-processes.d.ts +38 -31
  21. package/core/ogc-processes.d.ts.map +1 -1
  22. package/core/ogc-processes.js +123 -173
  23. package/core/process-runner.js +15 -100
  24. package/core/surfaces.d.ts +2 -8
  25. package/core/surfaces.d.ts.map +1 -1
  26. package/core/surfaces.js +21 -112
  27. package/core/types.d.ts +7 -6
  28. package/core/types.d.ts.map +1 -1
  29. package/gen/geospatial/v1/common_pb.d.ts +514 -0
  30. package/gen/geospatial/v1/common_pb.d.ts.map +1 -0
  31. package/gen/geospatial/v1/common_pb.js +335 -0
  32. package/gen/geospatial/v1/execution_types_pb.d.ts +1186 -0
  33. package/gen/geospatial/v1/execution_types_pb.d.ts.map +1 -0
  34. package/gen/geospatial/v1/execution_types_pb.js +396 -0
  35. package/gen/geospatial/v1/feature_service_pb.d.ts +403 -0
  36. package/gen/geospatial/v1/feature_service_pb.d.ts.map +1 -0
  37. package/gen/geospatial/v1/feature_service_pb.js +47 -0
  38. package/gen/geospatial/v1/index.d.ts +5 -0
  39. package/gen/geospatial/v1/index.d.ts.map +1 -0
  40. package/gen/geospatial/v1/index.js +9 -0
  41. package/gen/geospatial/v1/spatial_types_pb.d.ts +285 -0
  42. package/gen/geospatial/v1/spatial_types_pb.d.ts.map +1 -0
  43. package/gen/geospatial/v1/spatial_types_pb.js +58 -0
  44. package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts +379 -0
  45. package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts.map +1 -0
  46. package/gen/geospatial/v1/workspace_artifact_types_pb.js +151 -0
  47. package/package.json +2 -2
  48. package/gen/honua/v1/feature_service_pb.d.ts +0 -918
  49. package/gen/honua/v1/feature_service_pb.d.ts.map +0 -1
  50. package/gen/honua/v1/feature_service_pb.js +0 -375
package/core/client.js CHANGED
@@ -311,7 +311,7 @@ export class HonuaClient {
311
311
  initPromise = Promise.all([
312
312
  import("@connectrpc/connect"),
313
313
  import("@connectrpc/connect-web"),
314
- import("../gen/honua/v1/feature_service_pb.js"),
314
+ import("../gen/geospatial/v1/feature_service_pb.js"),
315
315
  ]).then(([{ createClient }, { createGrpcWebTransport }, { FeatureService }]) => {
316
316
  const transport = createGrpcWebTransport(this.connectTransportOptions());
317
317
  this.connectClient = createClient(FeatureService, transport);
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Credential-recognition and redaction primitives shared by every Honua export
3
+ * pipeline.
4
+ *
5
+ * These were extracted verbatim from `src/web-components/export-redaction.ts`
6
+ * (issue #683) when the runtime's portable MapPackage export (#1426) needed the
7
+ * same scanner. There must be exactly one credential recognizer in this SDK: a
8
+ * second copy would drift, and the copy that drifts is the one that leaks. The
9
+ * web-components module re-exports every symbol below so its published surface
10
+ * is unchanged, and keeps the app-platform-specific state projection that
11
+ * layers on top of these primitives.
12
+ *
13
+ * This module lives in `src/core` rather than `src/web-components` because
14
+ * `src/runtime` ships in the stable-tier `@honua/sdk` split package while
15
+ * `src/web-components` ships in `@honua/app-platform`; the runtime cannot
16
+ * reach across that boundary (`scripts/prepare-split-packages.mjs`).
17
+ *
18
+ * Deliberately dependency-free apart from the SDK's shared error base: no
19
+ * `node:crypto`, no DOM, no renderer. Safe in a browser, a worker, or an SSR
20
+ * render pass.
21
+ *
22
+ * @module
23
+ */
24
+ import { HonuaSdkError } from "./error-envelope.js";
25
+ /** Placeholder substituted for any value the export pipeline refuses to emit. */
26
+ export declare const HONUA_EXPORT_REDACTED = "[REDACTED]";
27
+ /** Why a value was withheld from an export. */
28
+ export type HonuaExportRedactionReason =
29
+ /** The property name itself named a credential (`token`, `apiKey`, `cookie`, ...). */
30
+ "sensitive-key"
31
+ /** A URL carried a signature, SAS, or token query parameter, or embedded userinfo. */
32
+ | "signed-url"
33
+ /** A free-text value matched a credential shape (bearer token, JWT, provider key, ...). */
34
+ | "credential-pattern"
35
+ /** A request-header map was dropped because it is not on the export allowlist. */
36
+ | "private-header"
37
+ /** OAuth/authorization scope, audience, tenant, or grant detail. */
38
+ | "authorization-scope"
39
+ /** A source explicitly marked as not exportable by the plan or the source itself. */
40
+ | "non-exportable-source"
41
+ /**
42
+ * A pointer to where the artifact is hosted or published (`links`, `href`,
43
+ * `publicationUrl`, `embedUrl`, ...). Not a secret — a claim. An exporter
44
+ * that cannot tell whether the thing it is exporting was ever persisted or
45
+ * published must not carry a URL that says it was.
46
+ */
47
+ | "publication-pointer"
48
+ /** A value could not be represented safely (unserializable, cyclic, over budget). */
49
+ | "unsupported-value";
50
+ /** One withheld value, addressed by its path in the sanitized document. */
51
+ export interface HonuaExportRedaction {
52
+ /** Dotted/bracketed path in the *sanitized* document, e.g. `sources[0].endpoint`. */
53
+ readonly path: string;
54
+ readonly reason: HonuaExportRedactionReason;
55
+ }
56
+ /** Thrown when the export pipeline cannot produce a provably credential-free artifact. */
57
+ export declare class HonuaExportSafetyError extends HonuaSdkError {
58
+ readonly reason: HonuaExportRedactionReason | "assertion";
59
+ constructor(message: string, reason?: HonuaExportRedactionReason | "assertion");
60
+ }
61
+ /**
62
+ * Redacts credential material from arbitrary human-readable text: adapter
63
+ * error messages, titles, status strings, log lines, event detail messages.
64
+ *
65
+ * Percent-encoded payloads are decoded (twice, bounded) before matching so a
66
+ * `%42earer%20abc` style evasion cannot slip a token past the scan; the
67
+ * returned string is the redacted *decoded* form when decoding changed
68
+ * anything, because emitting the still-encoded original would defeat the
69
+ * point.
70
+ */
71
+ export declare function redactHonuaExportText(value: string): string;
72
+ /** Whether `value` still looks like it carries credential material. */
73
+ export declare function containsCredentialMaterial(value: string): boolean;
74
+ /**
75
+ * Fail-closed assertion for finished export payloads (REQ-002). Throws
76
+ * {@link HonuaExportSafetyError} rather than scrubbing, because reaching this
77
+ * point with credential material means an earlier layer is broken.
78
+ */
79
+ export declare function assertCredentialFreeExportText(value: string, label: string): void;
80
+ /**
81
+ * Extracts runs of printable ASCII from a binary buffer, joined by newlines.
82
+ *
83
+ * This is the `strings(1)` idea, and it exists because "binary" is not a
84
+ * security boundary. A PNG `tEXt`/`iTXt` chunk, a JPEG `COM` marker, PDF
85
+ * metadata or an embedded XMP packet, an EXIF `UserComment`, a ZIP comment —
86
+ * all carry plainly readable text inside a buffer that will never decode as
87
+ * UTF-8 as a whole. Scanning only whole-buffer-decodable artifacts would let a
88
+ * token ride out inside any of them.
89
+ *
90
+ * Runs shorter than {@link MIN_PRINTABLE_RUN} are dropped: they are the
91
+ * incidental byte values that fall in the ASCII range inside compressed data,
92
+ * and keeping them would produce noise without ever forming a credential.
93
+ */
94
+ export declare function extractPrintableRuns(bytes: Uint8Array, start?: number, end?: number): string;
95
+ /**
96
+ * Fail-closed credential scan over a binary artifact (REQ-002).
97
+ *
98
+ * Scans in bounded chunks with an overlap, so an arbitrarily large artifact
99
+ * costs bounded memory while a token that straddles a chunk boundary is still
100
+ * caught. Throws {@link HonuaExportSafetyError} on a hit, like the text scan —
101
+ * a credential surviving to this point means an earlier layer is broken, and
102
+ * emitting the bytes anyway is the one outcome that cannot be walked back.
103
+ */
104
+ export declare function assertCredentialFreeExportBytes(bytes: Uint8Array, label: string): void;
105
+ /** Result of reducing an endpoint URL to its exportable form. */
106
+ export interface HonuaExportEndpointProjection {
107
+ /** `scheme://host[:port]/path` with query, fragment, and userinfo removed, or `undefined` when nothing is safe to emit. */
108
+ readonly endpoint: string | undefined;
109
+ readonly reason?: HonuaExportRedactionReason;
110
+ }
111
+ /**
112
+ * Reduces a source URL to `origin + pathname`, always dropping the query
113
+ * string and fragment.
114
+ *
115
+ * Query strings are where every practical map-service credential lives (Esri
116
+ * `?token=`, Azure SAS `?sv=...&sig=`, S3/GCS presigned `?X-Amz-Signature=`,
117
+ * tile-provider `?api_key=`), and a partial per-parameter allowlist has to be
118
+ * right about every provider forever. Dropping the whole query is the only
119
+ * rule that is right by construction; the origin+path that remains is what an
120
+ * attribution/provenance block legitimately needs.
121
+ *
122
+ * Non-HTTP(S) schemes (`data:`, `blob:`, `file:`), URLs with embedded
123
+ * `user:password@` userinfo, and unparseable strings yield no endpoint at all.
124
+ */
125
+ export declare function projectExportEndpoint(url: string): HonuaExportEndpointProjection;
126
+ /** Result of removing credential material from a URL while keeping it usable. */
127
+ export interface HonuaUrlCredentialStrip {
128
+ /** The URL with credentials removed, or `undefined` when nothing is safe to emit. */
129
+ readonly url: string | undefined;
130
+ readonly reason?: HonuaExportRedactionReason;
131
+ }
132
+ /**
133
+ * Removes credential material from a URL while leaving the URL *functional*.
134
+ *
135
+ * This is the counterpart to {@link projectExportEndpoint}, not a replacement
136
+ * for it. `projectExportEndpoint` reduces a URL to origin+path for a
137
+ * provenance/attribution block, where the query is never needed and dropping
138
+ * all of it is right by construction. That rule cannot be used on a URL the
139
+ * artifact still has to *fetch*: an OGC WMS `GetMap` request, an Esri
140
+ * `?f=json` endpoint, and a tile template with `?format=pbf` all carry their
141
+ * operation in the query string, and a portable map that lost it would import
142
+ * as a map that no longer renders.
143
+ *
144
+ * So this function removes exactly what is credential-bearing — `user:pass@`
145
+ * userinfo, and query parameters whose *name* reads as a secret or a
146
+ * signature — and then re-scans the remainder, refusing to emit anything that
147
+ * still looks like it carries a token. Non-HTTP(S) schemes are refused
148
+ * outright; `data:` payloads are handled by the caller, which can bound and
149
+ * scan the decoded bytes with {@link assertCredentialFreeExportBytes}.
150
+ */
151
+ export declare function stripCredentialsFromUrl(url: string): HonuaUrlCredentialStrip;
152
+ /**
153
+ * Filters a header map down to the export allowlist, recording every drop.
154
+ * Exposed so adapters can prove — in their own tests — that no `Authorization`,
155
+ * `Cookie`, or provider-specific key header rides along with export metadata.
156
+ */
157
+ export declare function sanitizeHonuaExportHeaders(headers: Readonly<Record<string, string>> | undefined): {
158
+ headers: Readonly<Record<string, string>>;
159
+ redactions: readonly HonuaExportRedaction[];
160
+ };
161
+ /** Whether a property name is refused outright by the export pipeline. */
162
+ export declare function isSensitiveExportKey(name: string): boolean;
163
+ //# sourceMappingURL=credential-redaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-redaction.d.ts","sourceRoot":"","sources":["../../../src/core/credential-redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,eAAe,CAAC;AAElD,+CAA+C;AAC/C,MAAM,MAAM,0BAA0B;AACpC,sFAAsF;AACpF,eAAe;AACjB,sFAAsF;GACpF,YAAY;AACd,2FAA2F;GACzF,oBAAoB;AACtB,kFAAkF;GAChF,gBAAgB;AAClB,oEAAoE;GAClE,qBAAqB;AACvB,qFAAqF;GACnF,uBAAuB;AACzB;;;;;GAKG;GACD,qBAAqB;AACvB,qFAAqF;GACnF,mBAAmB,CAAC;AAExB,2EAA2E;AAC3E,MAAM,WAAW,oBAAoB;IACnC,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;CAC7C;AAED,0FAA0F;AAC1F,qBAAa,sBAAuB,SAAQ,aAAa;IACvD,SAAgB,MAAM,EAAE,0BAA0B,GAAG,WAAW,CAAC;gBAE9C,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,0BAA0B,GAAG,WAAyB;CAKnG;AA4FD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED,uEAAuE;AACvE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAOjF;AAgBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,SAAI,EAAE,GAAG,SAAmB,GAAG,MAAM,CAiBjG;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAStF;AAID,iEAAiE;AACjE,MAAM,WAAW,6BAA6B;IAC5C,2HAA2H;IAC3H,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,0BAA0B,CAAC;CAC9C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,6BAA6B,CAiChF;AAED,iFAAiF;AACjF,MAAM,WAAW,uBAAuB;IACtC,qFAAqF;IACrF,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,0BAA0B,CAAC;CAC9C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,uBAAuB,CA4D5E;AAuBD;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,GAAG;IACjG,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7C,CAoBA;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D"}
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Credential-recognition and redaction primitives shared by every Honua export
3
+ * pipeline.
4
+ *
5
+ * These were extracted verbatim from `src/web-components/export-redaction.ts`
6
+ * (issue #683) when the runtime's portable MapPackage export (#1426) needed the
7
+ * same scanner. There must be exactly one credential recognizer in this SDK: a
8
+ * second copy would drift, and the copy that drifts is the one that leaks. The
9
+ * web-components module re-exports every symbol below so its published surface
10
+ * is unchanged, and keeps the app-platform-specific state projection that
11
+ * layers on top of these primitives.
12
+ *
13
+ * This module lives in `src/core` rather than `src/web-components` because
14
+ * `src/runtime` ships in the stable-tier `@honua/sdk` split package while
15
+ * `src/web-components` ships in `@honua/app-platform`; the runtime cannot
16
+ * reach across that boundary (`scripts/prepare-split-packages.mjs`).
17
+ *
18
+ * Deliberately dependency-free apart from the SDK's shared error base: no
19
+ * `node:crypto`, no DOM, no renderer. Safe in a browser, a worker, or an SSR
20
+ * render pass.
21
+ *
22
+ * @module
23
+ */
24
+ import { HonuaSdkError } from "./error-envelope.js";
25
+ /** Placeholder substituted for any value the export pipeline refuses to emit. */
26
+ export const HONUA_EXPORT_REDACTED = "[REDACTED]";
27
+ /** Thrown when the export pipeline cannot produce a provably credential-free artifact. */
28
+ export class HonuaExportSafetyError extends HonuaSdkError {
29
+ reason;
30
+ constructor(message, reason = "assertion") {
31
+ super("app.export-unsafe", message);
32
+ this.name = "HonuaExportSafetyError";
33
+ this.reason = reason;
34
+ }
35
+ }
36
+ // ── credential recognition ───────────────────────────────────────────────
37
+ /**
38
+ * Property names that are never exported, whatever their value. Matched
39
+ * case-insensitively against the whole key (substring match), so `apiKey`,
40
+ * `X-API-KEY`, `refresh_token`, and `sessionCookie` are all caught.
41
+ *
42
+ * The `<qualifier>Key` family is enumerated rather than reduced to a bare
43
+ * `key`: `key` alone would refuse `primaryKey`, `layerKey`, `sortKey`, and the
44
+ * MapLibre style spec's own `*-key` properties, and an exporter that withholds
45
+ * a table's primary key is not safer, only broken. The qualifiers listed here
46
+ * have no innocent reading — `adminKey` in particular is the property name this
47
+ * repository's own CLI configuration uses for the root administrator
48
+ * credential (`profiles.<name>.adminKey`), so it must never ride out of an
49
+ * export or a diagnostic bundle.
50
+ */
51
+ const SENSITIVE_KEY = /(?:api[-_]?key|(?:access|admin|encryption|master|root|signing)[-_]?key|auth|bearer|cookie|credential|cred(?:s)?\b|jwt|nonce|pass(?:word|wd)?|private[-_]?key|pwd|secret|session|sig(?:nature)?|subscription[-_]?key|token)/i;
52
+ /** Property names that leak *authorization scope* rather than the secret itself. */
53
+ const AUTHORIZATION_SCOPE_KEY = /(?:^|[-_.])(?:scope|scopes|audience|aud|tenant|tenant[-_]?id|client[-_]?id|grant|grant[-_]?type|roles|permissions|principal|subject|assume[-_]?role)(?:$|[-_.])/i;
54
+ /** Query-parameter names that make a URL a *signed* URL (presigned S3/GCS/SAS/Esri/CDN). */
55
+ 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;
56
+ /**
57
+ * Free-text credential shapes. Every pattern is a linear scan over bounded
58
+ * character classes — no nested quantifiers, no backtracking traps (the repo's
59
+ * CodeQL ReDoS rule).
60
+ */
61
+ const CREDENTIAL_PATTERNS = [
62
+ [/\b(?:Bearer|Basic|Digest|Negotiate)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "[REDACTED_AUTH]"],
63
+ // `scheme://user:password@host` — basic-auth credentials embedded in a URL.
64
+ // `projectExportEndpoint` refuses these structurally, but a URL can also reach
65
+ // an export through an adapter's free-text error message, so the text scan has
66
+ // to recognize the same shape.
67
+ [/\b[a-z][a-z0-9+.-]{1,31}:\/\/[^\s/@:]{1,128}:[^\s/@]{1,128}@/gi, "[REDACTED_URL_CREDENTIALS]@"],
68
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g, "[REDACTED_JWT]"],
69
+ [/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
70
+ [/\bASIA[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"],
71
+ [/\bAIza[0-9A-Za-z_-]{20,}/g, "[REDACTED_PROVIDER_TOKEN]"],
72
+ [/\bgh[pousr]_[A-Za-z0-9]{16,}/g, "[REDACTED_PROVIDER_TOKEN]"],
73
+ [/\bglpat-[A-Za-z0-9_-]{10,}/g, "[REDACTED_PROVIDER_TOKEN]"],
74
+ [/\bxox[baprs]-[A-Za-z0-9-]{8,}/gi, "[REDACTED_PROVIDER_TOKEN]"],
75
+ [/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{8,}/gi, "[REDACTED_PROVIDER_TOKEN]"],
76
+ [/\bsk-[A-Za-z0-9]{16,}/g, "[REDACTED_PROVIDER_TOKEN]"],
77
+ // Labelled `name=value` / `name: value` pairs. The name alternation covers
78
+ // the short, provider-specific parameter names real signed URLs use — Azure
79
+ // SAS `sig`/`sv`/`se`/`st`, AWS/GCS `x-amz-signature`/`x-goog-signature`, and
80
+ // the bare `key` that tile providers favour — because those values are
81
+ // frequently *short* (a 24-character SAS signature is common) and so slip
82
+ // under any entropy heuristic. `\b` before each name keeps `turnkey=` and
83
+ // `monkey=` from matching.
84
+ [
85
+ /\b(access[-_]?key|admin[-_]?key|api[-_]?key|apikey|authorization|client[-_]?secret|cookie|credential|encryption[-_]?key|jwt|key|master[-_]?key|pass(?:password)?|password|pwd|root[-_]?key|se|secret|session|sig|signature|signing[-_]?key|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,
86
+ `$1=${HONUA_EXPORT_REDACTED}`,
87
+ ],
88
+ ];
89
+ /**
90
+ * High-entropy blob heuristic, applied last so labelled matches win first.
91
+ *
92
+ * Deliberately excludes `/` and `-` from the character class. Including them
93
+ * made ordinary URL paths match — `arcgis/rest/services/Roads/FeatureServer/0`
94
+ * is 41 characters of `[A-Za-z0-9/]` and was being reported as a secret, which
95
+ * would have thrown away the endpoint an attribution block legitimately needs.
96
+ * Base64 blobs containing `/` still get caught: in practice they appear as a
97
+ * labelled parameter value or an `Authorization` header, both of which the
98
+ * patterns above match first.
99
+ */
100
+ const OPAQUE_SECRET = /\b[A-Za-z0-9_+=]{40,}\b/g;
101
+ function decodePercentEncoding(value) {
102
+ let decoded = value;
103
+ for (let pass = 0; pass < 2; pass += 1) {
104
+ if (!decoded.includes("%"))
105
+ break;
106
+ let next;
107
+ try {
108
+ next = decodeURIComponent(decoded);
109
+ }
110
+ catch {
111
+ break;
112
+ }
113
+ if (next === decoded)
114
+ break;
115
+ decoded = next;
116
+ }
117
+ return decoded;
118
+ }
119
+ /**
120
+ * Redacts credential material from arbitrary human-readable text: adapter
121
+ * error messages, titles, status strings, log lines, event detail messages.
122
+ *
123
+ * Percent-encoded payloads are decoded (twice, bounded) before matching so a
124
+ * `%42earer%20abc` style evasion cannot slip a token past the scan; the
125
+ * returned string is the redacted *decoded* form when decoding changed
126
+ * anything, because emitting the still-encoded original would defeat the
127
+ * point.
128
+ */
129
+ export function redactHonuaExportText(value) {
130
+ if (value.length === 0)
131
+ return value;
132
+ let redacted = decodePercentEncoding(value);
133
+ for (const [pattern, replacement] of CREDENTIAL_PATTERNS) {
134
+ redacted = redacted.replace(pattern, replacement);
135
+ }
136
+ return redacted.replace(OPAQUE_SECRET, "[REDACTED_TOKEN]");
137
+ }
138
+ /** Whether `value` still looks like it carries credential material. */
139
+ export function containsCredentialMaterial(value) {
140
+ return redactHonuaExportText(value) !== decodePercentEncoding(value);
141
+ }
142
+ /**
143
+ * Fail-closed assertion for finished export payloads (REQ-002). Throws
144
+ * {@link HonuaExportSafetyError} rather than scrubbing, because reaching this
145
+ * point with credential material means an earlier layer is broken.
146
+ */
147
+ export function assertCredentialFreeExportText(value, label) {
148
+ if (containsCredentialMaterial(value)) {
149
+ throw new HonuaExportSafetyError(`${label} still contains credential material after redaction; refusing to emit the export.`, "assertion");
150
+ }
151
+ }
152
+ // ── binary artifact scanning ─────────────────────────────────────────────
153
+ /** Shortest printable run worth scanning. Below this it is structural noise, not text. */
154
+ const MIN_PRINTABLE_RUN = 4;
155
+ /** Printable text accumulated before a chunk is scanned and flushed. */
156
+ const PRINTABLE_CHUNK_BYTES = 1 << 20;
157
+ /**
158
+ * Characters carried between chunks so a token straddling a flush boundary is
159
+ * still matched. Comfortably longer than the longest pattern this module has.
160
+ */
161
+ const PRINTABLE_CHUNK_OVERLAP = 512;
162
+ /**
163
+ * Extracts runs of printable ASCII from a binary buffer, joined by newlines.
164
+ *
165
+ * This is the `strings(1)` idea, and it exists because "binary" is not a
166
+ * security boundary. A PNG `tEXt`/`iTXt` chunk, a JPEG `COM` marker, PDF
167
+ * metadata or an embedded XMP packet, an EXIF `UserComment`, a ZIP comment —
168
+ * all carry plainly readable text inside a buffer that will never decode as
169
+ * UTF-8 as a whole. Scanning only whole-buffer-decodable artifacts would let a
170
+ * token ride out inside any of them.
171
+ *
172
+ * Runs shorter than {@link MIN_PRINTABLE_RUN} are dropped: they are the
173
+ * incidental byte values that fall in the ASCII range inside compressed data,
174
+ * and keeping them would produce noise without ever forming a credential.
175
+ */
176
+ export function extractPrintableRuns(bytes, start = 0, end = bytes.byteLength) {
177
+ const runs = [];
178
+ let current = "";
179
+ for (let index = start; index < end; index += 1) {
180
+ const byte = bytes[index];
181
+ // Printable ASCII plus tab. Deliberately not the full latin-1 upper range:
182
+ // every credential shape this module recognizes is ASCII, and admitting
183
+ // 0x80-0xFF would turn arbitrary compressed data into "text".
184
+ if ((byte >= 0x20 && byte <= 0x7e) || byte === 0x09) {
185
+ current += String.fromCharCode(byte);
186
+ continue;
187
+ }
188
+ if (current.length >= MIN_PRINTABLE_RUN)
189
+ runs.push(current);
190
+ current = "";
191
+ }
192
+ if (current.length >= MIN_PRINTABLE_RUN)
193
+ runs.push(current);
194
+ return runs.join("\n");
195
+ }
196
+ /**
197
+ * Fail-closed credential scan over a binary artifact (REQ-002).
198
+ *
199
+ * Scans in bounded chunks with an overlap, so an arbitrarily large artifact
200
+ * costs bounded memory while a token that straddles a chunk boundary is still
201
+ * caught. Throws {@link HonuaExportSafetyError} on a hit, like the text scan —
202
+ * a credential surviving to this point means an earlier layer is broken, and
203
+ * emitting the bytes anyway is the one outcome that cannot be walked back.
204
+ */
205
+ export function assertCredentialFreeExportBytes(bytes, label) {
206
+ let carry = "";
207
+ for (let offset = 0; offset < bytes.byteLength; offset += PRINTABLE_CHUNK_BYTES) {
208
+ const end = Math.min(bytes.byteLength, offset + PRINTABLE_CHUNK_BYTES);
209
+ const printable = extractPrintableRuns(bytes, offset, end);
210
+ if (printable.length === 0 && carry.length === 0)
211
+ continue;
212
+ assertCredentialFreeExportText(`${carry}${printable}`, label);
213
+ carry = printable.slice(-PRINTABLE_CHUNK_OVERLAP);
214
+ }
215
+ }
216
+ /**
217
+ * Reduces a source URL to `origin + pathname`, always dropping the query
218
+ * string and fragment.
219
+ *
220
+ * Query strings are where every practical map-service credential lives (Esri
221
+ * `?token=`, Azure SAS `?sv=...&sig=`, S3/GCS presigned `?X-Amz-Signature=`,
222
+ * tile-provider `?api_key=`), and a partial per-parameter allowlist has to be
223
+ * right about every provider forever. Dropping the whole query is the only
224
+ * rule that is right by construction; the origin+path that remains is what an
225
+ * attribution/provenance block legitimately needs.
226
+ *
227
+ * Non-HTTP(S) schemes (`data:`, `blob:`, `file:`), URLs with embedded
228
+ * `user:password@` userinfo, and unparseable strings yield no endpoint at all.
229
+ */
230
+ export function projectExportEndpoint(url) {
231
+ const trimmed = url.trim();
232
+ if (trimmed.length === 0 || trimmed.length > 4096) {
233
+ return { endpoint: undefined, reason: "unsupported-value" };
234
+ }
235
+ let parsed;
236
+ try {
237
+ parsed = new URL(trimmed);
238
+ }
239
+ catch {
240
+ // Relative locators carry no host and therefore no credential context, but
241
+ // they can still carry a query string; keep only the path portion.
242
+ const [path] = trimmed.split(/[?#]/, 1);
243
+ if (path.length === 0 || containsCredentialMaterial(path)) {
244
+ return { endpoint: undefined, reason: "credential-pattern" };
245
+ }
246
+ return path === trimmed ? { endpoint: path } : { endpoint: path, reason: "signed-url" };
247
+ }
248
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
249
+ return { endpoint: undefined, reason: "unsupported-value" };
250
+ }
251
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
252
+ return { endpoint: undefined, reason: "signed-url" };
253
+ }
254
+ const hadSecretQuery = [...parsed.searchParams.keys()].some((name) => SIGNED_QUERY_NAME.test(name) || SENSITIVE_KEY.test(name));
255
+ const endpoint = `${parsed.origin}${parsed.pathname}`;
256
+ if (containsCredentialMaterial(endpoint)) {
257
+ return { endpoint: undefined, reason: "credential-pattern" };
258
+ }
259
+ const droppedSomething = parsed.search.length > 0 || parsed.hash.length > 0;
260
+ if (hadSecretQuery)
261
+ return { endpoint, reason: "signed-url" };
262
+ return droppedSomething ? { endpoint, reason: "unsupported-value" } : { endpoint };
263
+ }
264
+ /**
265
+ * Removes credential material from a URL while leaving the URL *functional*.
266
+ *
267
+ * This is the counterpart to {@link projectExportEndpoint}, not a replacement
268
+ * for it. `projectExportEndpoint` reduces a URL to origin+path for a
269
+ * provenance/attribution block, where the query is never needed and dropping
270
+ * all of it is right by construction. That rule cannot be used on a URL the
271
+ * artifact still has to *fetch*: an OGC WMS `GetMap` request, an Esri
272
+ * `?f=json` endpoint, and a tile template with `?format=pbf` all carry their
273
+ * operation in the query string, and a portable map that lost it would import
274
+ * as a map that no longer renders.
275
+ *
276
+ * So this function removes exactly what is credential-bearing — `user:pass@`
277
+ * userinfo, and query parameters whose *name* reads as a secret or a
278
+ * signature — and then re-scans the remainder, refusing to emit anything that
279
+ * still looks like it carries a token. Non-HTTP(S) schemes are refused
280
+ * outright; `data:` payloads are handled by the caller, which can bound and
281
+ * scan the decoded bytes with {@link assertCredentialFreeExportBytes}.
282
+ */
283
+ export function stripCredentialsFromUrl(url) {
284
+ const trimmed = url.trim();
285
+ if (trimmed.length === 0 || trimmed.length > 8192) {
286
+ return { url: undefined, reason: "unsupported-value" };
287
+ }
288
+ let parsed;
289
+ try {
290
+ parsed = new URL(trimmed);
291
+ }
292
+ catch {
293
+ parsed = undefined;
294
+ }
295
+ if (parsed === undefined) {
296
+ // Relative or template locator: no host, so no userinfo, but it can still
297
+ // carry a query string. Filter the query the same way and keep the rest.
298
+ const hashIndex = trimmed.indexOf("#");
299
+ const withoutHash = hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex);
300
+ const queryIndex = withoutHash.indexOf("?");
301
+ const pathPart = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
302
+ const queryPart = queryIndex === -1 ? "" : withoutHash.slice(queryIndex + 1);
303
+ const { query, dropped } = filterCredentialQuery(queryPart);
304
+ const rebuilt = query.length > 0 ? `${pathPart}?${query}` : pathPart;
305
+ if (rebuilt.length === 0 || containsCredentialMaterial(rebuilt)) {
306
+ return { url: undefined, reason: "credential-pattern" };
307
+ }
308
+ // Nothing was dropped: return the caller's exact string rather than a
309
+ // reassembled one, so a safe URL round-trips byte-for-byte.
310
+ return dropped ? { url: rebuilt, reason: "signed-url" } : { url: trimmed };
311
+ }
312
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
313
+ return { url: undefined, reason: "unsupported-value" };
314
+ }
315
+ let dropped = false;
316
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
317
+ parsed.username = "";
318
+ parsed.password = "";
319
+ dropped = true;
320
+ }
321
+ for (const name of [...parsed.searchParams.keys()]) {
322
+ if (SIGNED_QUERY_NAME.test(name) || SENSITIVE_KEY.test(name)) {
323
+ parsed.searchParams.delete(name);
324
+ dropped = true;
325
+ }
326
+ }
327
+ // `URL` percent-encodes `{`/`}`, which would break a MapLibre tile template
328
+ // (`{z}/{x}/{y}`, `{bbox-epsg-3857}`). Restore them; they are structural, not
329
+ // credential material.
330
+ const rebuilt = parsed.toString().replace(/%7B/g, "{").replace(/%7D/g, "}");
331
+ if (containsCredentialMaterial(rebuilt)) {
332
+ return { url: undefined, reason: "credential-pattern" };
333
+ }
334
+ // Nothing was dropped: return the caller's exact string. `URL.toString()`
335
+ // normalizes (a bare origin gains a trailing `/`, reserved characters are
336
+ // re-encoded), and a portable artifact should not have its endpoints
337
+ // rewritten when there was nothing to remove.
338
+ return dropped ? { url: rebuilt, reason: "signed-url" } : { url: trimmed };
339
+ }
340
+ function filterCredentialQuery(query) {
341
+ if (query.length === 0)
342
+ return { query: "", dropped: false };
343
+ const kept = [];
344
+ let dropped = false;
345
+ for (const pair of query.split("&")) {
346
+ if (pair.length === 0)
347
+ continue;
348
+ const eq = pair.indexOf("=");
349
+ const name = eq === -1 ? pair : pair.slice(0, eq);
350
+ if (SIGNED_QUERY_NAME.test(name) || SENSITIVE_KEY.test(name)) {
351
+ dropped = true;
352
+ continue;
353
+ }
354
+ kept.push(pair);
355
+ }
356
+ return { query: kept.join("&"), dropped };
357
+ }
358
+ // ── header + key allowlists ──────────────────────────────────────────────
359
+ const EXPORTABLE_HEADERS = new Set(["accept", "accept-language", "content-type", "user-agent"]);
360
+ /**
361
+ * Filters a header map down to the export allowlist, recording every drop.
362
+ * Exposed so adapters can prove — in their own tests — that no `Authorization`,
363
+ * `Cookie`, or provider-specific key header rides along with export metadata.
364
+ */
365
+ export function sanitizeHonuaExportHeaders(headers) {
366
+ const redactions = [];
367
+ const output = {};
368
+ for (const [rawName, rawValue] of Object.entries(headers ?? {})) {
369
+ const name = rawName.toLowerCase();
370
+ if (!EXPORTABLE_HEADERS.has(name)) {
371
+ redactions.push({
372
+ path: `headers[${name}]`,
373
+ reason: SENSITIVE_KEY.test(name) || AUTHORIZATION_SCOPE_KEY.test(name) ? "sensitive-key" : "private-header",
374
+ });
375
+ continue;
376
+ }
377
+ const value = redactHonuaExportText(rawValue);
378
+ if (value !== rawValue) {
379
+ redactions.push({ path: `headers[${name}]`, reason: "credential-pattern" });
380
+ continue;
381
+ }
382
+ output[name] = value.slice(0, 512);
383
+ }
384
+ return { headers: output, redactions };
385
+ }
386
+ /** Whether a property name is refused outright by the export pipeline. */
387
+ export function isSensitiveExportKey(name) {
388
+ return SENSITIVE_KEY.test(name) || AUTHORIZATION_SCOPE_KEY.test(name);
389
+ }
@@ -47,6 +47,19 @@ export declare const HONUA_ERROR_RUNTIME_CLASSIFICATIONS: Readonly<{
47
47
  readonly "core.coverage.unsupported-format": HonuaErrorRuntimeClassification;
48
48
  readonly "core.coverage.service-error": HonuaErrorRuntimeClassification;
49
49
  readonly "core.coverage.wcs-exception": HonuaErrorRuntimeClassification;
50
+ readonly "core.zarr.invalid-request": HonuaErrorRuntimeClassification;
51
+ readonly "core.zarr.invalid-response": HonuaErrorRuntimeClassification;
52
+ readonly "core.zarr.response-too-large": HonuaErrorRuntimeClassification;
53
+ readonly "core.zarr.metadata-pending": HonuaErrorRuntimeClassification;
54
+ readonly "core.zarr.missing-spatial-extent": HonuaErrorRuntimeClassification;
55
+ readonly "core.zarr.no-tileable-variable": HonuaErrorRuntimeClassification;
56
+ readonly "core.zarr.missing-spatial-reference": HonuaErrorRuntimeClassification;
57
+ readonly "core.zarr.spatial-reference-mismatch": HonuaErrorRuntimeClassification;
58
+ readonly "core.zarr.unsupported-version": HonuaErrorRuntimeClassification;
59
+ readonly "core.zarr.unsupported-codec": HonuaErrorRuntimeClassification;
60
+ readonly "core.zarr.unsupported-dtype": HonuaErrorRuntimeClassification;
61
+ readonly "core.zarr.ambiguous-dimensions": HonuaErrorRuntimeClassification;
62
+ readonly "core.zarr.service-error": HonuaErrorRuntimeClassification;
50
63
  readonly "discovery.ambiguous-protocol": HonuaErrorRuntimeClassification;
51
64
  readonly "discovery.ambiguous-source": HonuaErrorRuntimeClassification;
52
65
  readonly "discovery.invalid-cloud-native-input": HonuaErrorRuntimeClassification;
@@ -106,6 +119,8 @@ export declare const HONUA_ERROR_RUNTIME_CLASSIFICATIONS: Readonly<{
106
119
  readonly "runtime.map-package.fetch": HonuaErrorRuntimeClassification;
107
120
  readonly "runtime.map-package.load": HonuaErrorRuntimeClassification;
108
121
  readonly "runtime.map-package.validate": HonuaErrorRuntimeClassification;
122
+ readonly "runtime.map-package.export": HonuaErrorRuntimeClassification;
123
+ readonly "runtime.map-package.import": HonuaErrorRuntimeClassification;
109
124
  readonly "runtime.map-package.update": HonuaErrorRuntimeClassification;
110
125
  readonly "runtime.map-package.style-compose": HonuaErrorRuntimeClassification;
111
126
  readonly "runtime.map-package.source-bind": HonuaErrorRuntimeClassification;
@@ -1 +1 @@
1
- {"version":3,"file":"error-classifications.d.ts","sourceRoot":"","sources":["../../../src/core/error-classifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAkB,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAErG;;;;;;;;GAQG;AACH,MAAM,MAAM,+BAA+B,GAAG,SAAS;IACrD,MAAM,EAAE,gBAAgB;IACxB,QAAQ,EAAE,kBAAkB;IAC5B,SAAS,EAAE,OAAO;CACnB,CAAC;AAEF,gBAAgB;AAChB,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8J6B,CAAC"}
1
+ {"version":3,"file":"error-classifications.d.ts","sourceRoot":"","sources":["../../../src/core/error-classifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAkB,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAErG;;;;;;;;GAQG;AACH,MAAM,MAAM,+BAA+B,GAAG,SAAS;IACrD,MAAM,EAAE,gBAAgB;IACxB,QAAQ,EAAE,kBAAkB;IAC5B,SAAS,EAAE,OAAO;CACnB,CAAC;AAEF,gBAAgB;AAChB,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6K6B,CAAC"}
@@ -32,6 +32,19 @@ export const HONUA_ERROR_RUNTIME_CLASSIFICATIONS = Object.freeze({
32
32
  "core.coverage.unsupported-format": runtimeClassification("core", "capability", false),
33
33
  "core.coverage.service-error": runtimeClassification("core", "protocol", false),
34
34
  "core.coverage.wcs-exception": runtimeClassification("core", "protocol", false),
35
+ "core.zarr.invalid-request": runtimeClassification("core", "validation", false),
36
+ "core.zarr.invalid-response": runtimeClassification("core", "protocol", false),
37
+ "core.zarr.response-too-large": runtimeClassification("core", "validation", false),
38
+ "core.zarr.metadata-pending": runtimeClassification("core", "capability", false),
39
+ "core.zarr.missing-spatial-extent": runtimeClassification("core", "capability", false),
40
+ "core.zarr.no-tileable-variable": runtimeClassification("core", "capability", false),
41
+ "core.zarr.missing-spatial-reference": runtimeClassification("core", "capability", false),
42
+ "core.zarr.spatial-reference-mismatch": runtimeClassification("core", "capability", false),
43
+ "core.zarr.unsupported-version": runtimeClassification("core", "capability", false),
44
+ "core.zarr.unsupported-codec": runtimeClassification("core", "capability", false),
45
+ "core.zarr.unsupported-dtype": runtimeClassification("core", "capability", false),
46
+ "core.zarr.ambiguous-dimensions": runtimeClassification("core", "validation", false),
47
+ "core.zarr.service-error": runtimeClassification("core", "protocol", false),
35
48
  "discovery.ambiguous-protocol": runtimeClassification("discovery", "validation", false),
36
49
  "discovery.ambiguous-source": runtimeClassification("discovery", "validation", false),
37
50
  "discovery.invalid-cloud-native-input": runtimeClassification("discovery", "validation", false),
@@ -91,6 +104,8 @@ export const HONUA_ERROR_RUNTIME_CLASSIFICATIONS = Object.freeze({
91
104
  "runtime.map-package.fetch": runtimeClassification("runtime", "network", true),
92
105
  "runtime.map-package.load": runtimeClassification("runtime", "internal", false),
93
106
  "runtime.map-package.validate": runtimeClassification("runtime", "validation", false),
107
+ "runtime.map-package.export": runtimeClassification("runtime", "validation", false),
108
+ "runtime.map-package.import": runtimeClassification("runtime", "validation", false),
94
109
  "runtime.map-package.update": runtimeClassification("runtime", "internal", false),
95
110
  "runtime.map-package.style-compose": runtimeClassification("runtime", "validation", false),
96
111
  "runtime.map-package.source-bind": runtimeClassification("runtime", "internal", false),