@honua/react 0.1.4-beta.0 → 0.1.9-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contract/types.d.ts +1 -1
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +4 -0
- package/control-plane/admin-client.d.ts +87 -0
- package/control-plane/admin-client.d.ts.map +1 -0
- package/control-plane/admin-client.js +169 -0
- package/control-plane/client.d.ts.map +1 -1
- package/control-plane/client.js +21 -3
- package/control-plane/commands/catalog.d.ts +133 -0
- package/control-plane/commands/catalog.d.ts.map +1 -0
- package/control-plane/commands/catalog.js +353 -0
- package/control-plane/commands/errors.d.ts +96 -0
- package/control-plane/commands/errors.d.ts.map +1 -0
- package/control-plane/commands/errors.js +130 -0
- package/control-plane/commands/identity.d.ts +71 -0
- package/control-plane/commands/identity.d.ts.map +1 -0
- package/control-plane/commands/identity.js +0 -0
- package/control-plane/commands/index.d.ts +39 -0
- package/control-plane/commands/index.d.ts.map +1 -0
- package/control-plane/commands/index.js +34 -0
- package/control-plane/commands/runtime.d.ts +146 -0
- package/control-plane/commands/runtime.d.ts.map +1 -0
- package/control-plane/commands/runtime.js +318 -0
- package/control-plane/commands/types.d.ts +226 -0
- package/control-plane/commands/types.d.ts.map +1 -0
- package/control-plane/commands/types.js +30 -0
- package/control-plane/commands/validate.d.ts +27 -0
- package/control-plane/commands/validate.d.ts.map +1 -0
- package/control-plane/commands/validate.js +132 -0
- package/control-plane/generated/admin-api.d.ts +30263 -0
- package/control-plane/generated/admin-api.d.ts.map +1 -0
- package/control-plane/generated/admin-api.js +5 -0
- package/control-plane/generated/admin-operations.d.ts +4053 -0
- package/control-plane/generated/admin-operations.d.ts.map +1 -0
- package/control-plane/generated/admin-operations.js +820 -0
- package/control-plane/index.d.ts +11 -0
- package/control-plane/index.d.ts.map +1 -1
- package/control-plane/index.js +8 -0
- package/control-plane/types.d.ts +14 -0
- package/control-plane/types.d.ts.map +1 -1
- package/core/client.d.ts +6 -0
- package/core/client.d.ts.map +1 -1
- package/core/client.js +12 -10
- package/core/credential-redaction.d.ts +163 -0
- package/core/credential-redaction.d.ts.map +1 -0
- package/core/credential-redaction.js +389 -0
- package/core/error-classifications.d.ts +32 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +32 -0
- package/core/error-code-registry.d.ts +33 -1
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +32 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.d.ts.map +1 -1
- package/core/geoparquet-sql.d.ts +1 -1
- package/core/geoparquet-sql.js +2 -2
- package/core/grpc-adapter.d.ts +2 -2
- package/core/grpc-adapter.d.ts.map +1 -1
- package/core/grpc-adapter.js +30 -6
- package/core/job-run-errors.d.ts +10 -0
- package/core/job-run-errors.d.ts.map +1 -0
- package/core/job-run-errors.js +14 -0
- package/core/job-run-lifecycle.d.ts +34 -0
- package/core/job-run-lifecycle.d.ts.map +1 -0
- package/core/job-run-lifecycle.js +193 -0
- package/core/ogc-processes.d.ts +38 -31
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +123 -173
- package/core/process-runner.js +15 -100
- package/core/stac.d.ts +2 -1
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +29 -11
- package/core/surfaces.d.ts +2 -8
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +21 -112
- package/core/types.d.ts +22 -8
- package/core/types.d.ts.map +1 -1
- package/gen/geospatial/v1/common_pb.d.ts +514 -0
- package/gen/geospatial/v1/common_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/common_pb.js +335 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts +1186 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/execution_types_pb.js +396 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts +403 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/feature_service_pb.js +47 -0
- package/gen/geospatial/v1/index.d.ts +5 -0
- package/gen/geospatial/v1/index.d.ts.map +1 -0
- package/gen/geospatial/v1/index.js +9 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts +285 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/spatial_types_pb.js +58 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts +379 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.js +151 -0
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/projection.js +10 -1
- package/interactions/declarative-contract.d.ts +149 -0
- package/interactions/declarative-contract.d.ts.map +1 -0
- package/interactions/declarative-contract.js +293 -0
- package/interactions/declarative.d.ts +198 -0
- package/interactions/declarative.d.ts.map +1 -0
- package/interactions/declarative.js +470 -0
- package/interactions/feature-state.d.ts +7 -0
- package/interactions/feature-state.d.ts.map +1 -1
- package/interactions/feature-state.js +10 -2
- package/map/maplibre-target.js +1 -1
- package/package.json +2 -2
- package/query-planner/grpc.d.ts +4 -4
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +11 -11
- package/query-planner/types.d.ts +4 -4
- package/query-planner/types.d.ts.map +1 -1
- package/realtime/sse.d.ts.map +1 -1
- package/realtime/sse.js +7 -1
- package/runtime/errors.d.ts +1 -1
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +2 -0
- package/runtime/generated/map-package-schema-meta.d.ts +11 -0
- package/runtime/generated/map-package-schema-meta.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-meta.js +12 -0
- package/runtime/generated/map-package-schema-validator.d.ts +16 -0
- package/runtime/generated/map-package-schema-validator.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-validator.js +1422 -0
- package/runtime/index.d.ts +5 -1
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +6 -0
- package/runtime/map-package-export.d.ts +151 -0
- package/runtime/map-package-export.d.ts.map +1 -0
- package/runtime/map-package-export.js +636 -0
- package/runtime/map-package-validation.d.ts +21 -1
- package/runtime/map-package-validation.d.ts.map +1 -1
- package/runtime/map-package-validation.js +132 -95
- package/runtime/map-package.d.ts +80 -0
- package/runtime/map-package.d.ts.map +1 -1
- package/runtime/style-compose.d.ts.map +1 -1
- package/runtime/style-compose.js +78 -9
- package/studio/chart-spec.d.ts +6 -6
- package/studio/chart-spec.d.ts.map +1 -1
- package/studio/chart-spec.js +6 -6
- package/studio/index.d.ts +17 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +15 -5
- package/studio/lifecycle-client.d.ts +262 -22
- package/studio/lifecycle-client.d.ts.map +1 -1
- package/studio/lifecycle-client.js +524 -24
- package/studio/lifecycle-errors.d.ts +8 -2
- package/studio/lifecycle-errors.d.ts.map +1 -1
- package/studio/lifecycle-errors.js +6 -1
- package/studio/lifecycle-types.d.ts +228 -8
- package/studio/lifecycle-types.d.ts.map +1 -1
- package/studio/publication-status.d.ts +55 -0
- package/studio/publication-status.d.ts.map +1 -0
- package/studio/publication-status.js +103 -0
- package/gen/honua/v1/feature_service_pb.d.ts +0 -918
- package/gen/honua/v1/feature_service_pb.d.ts.map +0 -1
- package/gen/honua/v1/feature_service_pb.js +0 -375
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 2026.1 command catalog: real commands over the existing control-plane and
|
|
3
|
+
* Studio resource clients.
|
|
4
|
+
*
|
|
5
|
+
* These four cover one end-to-end slice of the terminal release journey —
|
|
6
|
+
* probe a connection, start an import, publish a map package, and save a Studio
|
|
7
|
+
* draft as an immutable version — and exercise every capability the shared
|
|
8
|
+
* layer promises: idempotency, dry run, cancellation, validation, typed errors,
|
|
9
|
+
* and deterministic receipts. Each command owns its own sequencing, which is
|
|
10
|
+
* why `studio.draft.saveVersion` performs the optimistic-generation check here
|
|
11
|
+
* rather than in four transports.
|
|
12
|
+
*
|
|
13
|
+
* ## Where the concurrency check is bracketed, not atomic
|
|
14
|
+
*
|
|
15
|
+
* `studio.draft.saveVersion` cannot make its generation check atomic with the
|
|
16
|
+
* write, because the Studio lifecycle API offers nothing to make it atomic
|
|
17
|
+
* with: `POST /package-drafts/{draftId}/content-versions` takes no request body
|
|
18
|
+
* (`HonuaStudioDraftsClient.createContentVersion`, and the pinned contract
|
|
19
|
+
* fixture `test/fixtures/studio-lifecycle/content-version-create.v1.json`) and
|
|
20
|
+
* `HonuaStudioRequestOptions` has no `ifMatch` — unlike its control-plane
|
|
21
|
+
* sibling `HonuaControlPlaneRequestOptions`. The one optimistic-concurrency
|
|
22
|
+
* primitive in the surface is the `generation` field on the
|
|
23
|
+
* `PUT /package-drafts/{draftId}` *body* (`StudioPackageDraftReplaceRequest`),
|
|
24
|
+
* which this route has no analogue for.
|
|
25
|
+
*
|
|
26
|
+
* So the command brackets the write instead of pretending the check was
|
|
27
|
+
* atomic: it reads the generation before, re-reads it after, and raises a
|
|
28
|
+
* `conflict` naming the version it created if the draft moved at any point.
|
|
29
|
+
* `generation` is documented to increment on every successful `PUT`
|
|
30
|
+
* (`StudioPackageDraft`), so no concurrent edit can slip through the bracket
|
|
31
|
+
* unseen. When honua-server grows a real precondition on this route, the
|
|
32
|
+
* bracket collapses into it and the second read goes away.
|
|
33
|
+
*
|
|
34
|
+
* @experimental
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import { HonuaCommandError } from "./errors.js";
|
|
38
|
+
/**
|
|
39
|
+
* `POST /connections/{connectionId}/test` — probe a stored connection.
|
|
40
|
+
*
|
|
41
|
+
* Modelled as an `action` rather than a `read` so repeated probes carry an
|
|
42
|
+
* `Idempotency-Key` and collapse server-side; the probe itself does not mutate
|
|
43
|
+
* the connection.
|
|
44
|
+
*/
|
|
45
|
+
export const connectionTestCommand = {
|
|
46
|
+
id: "connection.test",
|
|
47
|
+
title: "Test a connection",
|
|
48
|
+
description: "Probe a stored control-plane connection and report whether the server can reach it.",
|
|
49
|
+
mode: "action",
|
|
50
|
+
resourceKind: "connection",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
description: "Identifies the stored connection to probe.",
|
|
54
|
+
properties: {
|
|
55
|
+
connectionId: { type: "string", minLength: 1, description: "Identifier of the stored connection." },
|
|
56
|
+
workspaceId: { type: "string", minLength: 1, description: "Owning workspace, when the deployment scopes them." },
|
|
57
|
+
},
|
|
58
|
+
required: ["connectionId"],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
plan(context) {
|
|
62
|
+
return {
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: connectionTestPath(context.input.connectionId),
|
|
65
|
+
summary: `Probe connection ${context.input.connectionId}`,
|
|
66
|
+
resourceRef: connectionRef(context.input),
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
async execute(context) {
|
|
70
|
+
const result = await context.controlPlane.raw({
|
|
71
|
+
method: "POST",
|
|
72
|
+
path: connectionTestPath(context.input.connectionId),
|
|
73
|
+
...(context.input.workspaceId ? { body: { workspaceId: context.input.workspaceId } } : {}),
|
|
74
|
+
...context.requestOptions(),
|
|
75
|
+
});
|
|
76
|
+
const value = unwrap(result, this.id, {
|
|
77
|
+
correlationId: context.correlationId,
|
|
78
|
+
idempotencyKey: context.idempotencyKey,
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
output: value ?? {},
|
|
82
|
+
...(value?.ok === false ? { status: "denied" } : {}),
|
|
83
|
+
resourceRef: connectionRef(context.input),
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
function connectionTestPath(connectionId) {
|
|
88
|
+
return `/connections/${encodeURIComponent(connectionId)}/test`;
|
|
89
|
+
}
|
|
90
|
+
function connectionRef(input) {
|
|
91
|
+
return {
|
|
92
|
+
type: "connection",
|
|
93
|
+
id: input.connectionId,
|
|
94
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** `POST /imports` — enqueue an import job. */
|
|
98
|
+
export const importCreateCommand = {
|
|
99
|
+
id: "import.create",
|
|
100
|
+
title: "Create an import job",
|
|
101
|
+
description: "Enqueue a control-plane import job for a source URL or a stored connection.",
|
|
102
|
+
mode: "action",
|
|
103
|
+
resourceKind: "import-job",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
description: "Describes the source to import.",
|
|
107
|
+
properties: {
|
|
108
|
+
sourceKind: { type: "string", minLength: 1, description: "Import source kind, e.g. `geojson` or `postgis`." },
|
|
109
|
+
sourceUrl: { type: "string", minLength: 1, description: "Source URL, when the import reads a location." },
|
|
110
|
+
connectionId: { type: "string", minLength: 1, description: "Stored connection, when the import reads a system." },
|
|
111
|
+
workspaceId: { type: "string", minLength: 1, description: "Workspace that will own the imported content." },
|
|
112
|
+
title: { type: "string", minLength: 1, description: "Human title for the resulting content." },
|
|
113
|
+
options: { type: "object", description: "Import-kind-specific options, passed through verbatim." },
|
|
114
|
+
},
|
|
115
|
+
required: ["sourceKind"],
|
|
116
|
+
additionalProperties: false,
|
|
117
|
+
},
|
|
118
|
+
validate(input) {
|
|
119
|
+
// JSON Schema cannot say "one of these two", and this has to run before the
|
|
120
|
+
// dry-run short circuit: a preview that accepts an import with no source
|
|
121
|
+
// would be a preview of a request the server never sees.
|
|
122
|
+
if (!input.sourceUrl && !input.connectionId) {
|
|
123
|
+
return [{ path: "", message: "one of `sourceUrl` or `connectionId` is required" }];
|
|
124
|
+
}
|
|
125
|
+
return [];
|
|
126
|
+
},
|
|
127
|
+
plan(context) {
|
|
128
|
+
const { input } = context;
|
|
129
|
+
return {
|
|
130
|
+
method: "POST",
|
|
131
|
+
path: "/imports",
|
|
132
|
+
summary: `Import ${input.sourceKind} from ${input.sourceUrl ?? input.connectionId ?? "(unspecified source)"}`,
|
|
133
|
+
resourceRef: {
|
|
134
|
+
type: "import-job",
|
|
135
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
async execute(context) {
|
|
140
|
+
const { input } = context;
|
|
141
|
+
const result = await context.controlPlane.imports.create({
|
|
142
|
+
sourceKind: input.sourceKind,
|
|
143
|
+
...(input.sourceUrl ? { sourceUrl: input.sourceUrl } : {}),
|
|
144
|
+
...(input.connectionId ? { connectionId: input.connectionId } : {}),
|
|
145
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
146
|
+
...(input.title ? { title: input.title } : {}),
|
|
147
|
+
...(input.options ? { options: input.options } : {}),
|
|
148
|
+
}, context.requestOptions());
|
|
149
|
+
const job = unwrap(result, this.id, {
|
|
150
|
+
correlationId: context.correlationId,
|
|
151
|
+
idempotencyKey: context.idempotencyKey,
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
output: job,
|
|
155
|
+
resourceRef: {
|
|
156
|
+
type: "import-job",
|
|
157
|
+
...(job?.id ? { id: job.id } : {}),
|
|
158
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
159
|
+
...(job?.links?.self ? { href: job.links.self } : {}),
|
|
160
|
+
},
|
|
161
|
+
...(validatorsOf(job) ? { validators: validatorsOf(job) } : {}),
|
|
162
|
+
...(job?.problem ? { problem: job.problem } : {}),
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
/** `POST /packages` — publish a map package version. */
|
|
167
|
+
export const mapPackagePublishCommand = {
|
|
168
|
+
id: "map-package.publish",
|
|
169
|
+
title: "Publish a map package",
|
|
170
|
+
description: "Publish a map package version. Approval and visibility remain server-side decisions.",
|
|
171
|
+
mode: "action",
|
|
172
|
+
resourceKind: "map-package",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
description: "The map package to publish and where it belongs.",
|
|
176
|
+
properties: {
|
|
177
|
+
mapId: { type: "string", minLength: 1, description: "Hosted map the package version belongs to." },
|
|
178
|
+
workspaceId: { type: "string", minLength: 1, description: "Owning workspace." },
|
|
179
|
+
package: { type: "object", description: "The `honua_map_package.v1` document." },
|
|
180
|
+
message: { type: "string", description: "Publication message recorded with the version." },
|
|
181
|
+
},
|
|
182
|
+
required: ["package"],
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
},
|
|
185
|
+
plan(context) {
|
|
186
|
+
const { input } = context;
|
|
187
|
+
return {
|
|
188
|
+
method: "POST",
|
|
189
|
+
path: "/packages",
|
|
190
|
+
summary: input.mapId ? `Publish a package version for map ${input.mapId}` : "Publish a map package version",
|
|
191
|
+
resourceRef: {
|
|
192
|
+
type: "map-package",
|
|
193
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
async execute(context) {
|
|
198
|
+
const { input } = context;
|
|
199
|
+
const result = await context.controlPlane.packages.publish({
|
|
200
|
+
package: input.package,
|
|
201
|
+
...(input.mapId ? { mapId: input.mapId } : {}),
|
|
202
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
203
|
+
...(input.message ? { message: input.message } : {}),
|
|
204
|
+
}, context.requestOptions());
|
|
205
|
+
const response = unwrap(result, this.id, {
|
|
206
|
+
correlationId: context.correlationId,
|
|
207
|
+
idempotencyKey: context.idempotencyKey,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
output: response,
|
|
211
|
+
resourceRef: {
|
|
212
|
+
type: "map-package",
|
|
213
|
+
...(response?.packageId ? { id: response.packageId } : {}),
|
|
214
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
215
|
+
...(response?.links?.self ? { href: response.links.self } : {}),
|
|
216
|
+
},
|
|
217
|
+
...(validatorsOf(response) ? { validators: validatorsOf(response) } : {}),
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
/** `POST /package-drafts/{draftId}/content-versions` — save a draft as an immutable version. */
|
|
222
|
+
export const studioDraftSaveVersionCommand = {
|
|
223
|
+
id: "studio.draft.saveVersion",
|
|
224
|
+
title: "Save a Studio draft as a version",
|
|
225
|
+
description: "Freeze a mutable Studio package draft into an immutable content version, checking the generation.",
|
|
226
|
+
mode: "action",
|
|
227
|
+
resourceKind: "studio-content-version",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: "object",
|
|
230
|
+
description: "Identifies the draft to freeze.",
|
|
231
|
+
properties: {
|
|
232
|
+
draftId: { type: "string", minLength: 1, description: "Mutable draft identifier." },
|
|
233
|
+
generation: { type: "integer", minimum: 0, description: "Last-seen draft generation for concurrency checking." },
|
|
234
|
+
},
|
|
235
|
+
required: ["draftId"],
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
},
|
|
238
|
+
plan(context) {
|
|
239
|
+
return {
|
|
240
|
+
method: "POST",
|
|
241
|
+
path: `/package-drafts/${encodeURIComponent(context.input.draftId)}/content-versions`,
|
|
242
|
+
summary: `Save draft ${context.input.draftId} as an immutable content version`,
|
|
243
|
+
resourceRef: { type: "studio-package-draft", id: context.input.draftId },
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
async execute(context) {
|
|
247
|
+
const studio = context.studio;
|
|
248
|
+
if (!studio) {
|
|
249
|
+
throw new HonuaCommandError("transport", this.id, "studio.draft.saveVersion requires a Studio lifecycle client; construct the runtime with `studio`.", { correlationId: context.correlationId, idempotencyKey: context.idempotencyKey });
|
|
250
|
+
}
|
|
251
|
+
// The Studio lifecycle client takes `{ signal, headers }` rather than the
|
|
252
|
+
// control-plane option bag, so the idempotency key is threaded as the
|
|
253
|
+
// header it becomes — and only onto the mutating call.
|
|
254
|
+
const options = context.requestOptions();
|
|
255
|
+
const readOptions = {
|
|
256
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
257
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
258
|
+
};
|
|
259
|
+
// The command-owned key goes on last: the runtime already refuses a caller
|
|
260
|
+
// header that would replace it, and this keeps the value on the wire equal
|
|
261
|
+
// to the value the receipt records even if that guard is ever relaxed.
|
|
262
|
+
const writeOptions = {
|
|
263
|
+
...readOptions,
|
|
264
|
+
headers: { ...headersToRecord(options.headers), "Idempotency-Key": context.idempotencyKey },
|
|
265
|
+
};
|
|
266
|
+
if (context.input.generation !== undefined) {
|
|
267
|
+
const draft = await studio.drafts.get(context.input.draftId, readOptions);
|
|
268
|
+
if (draft.generation !== context.input.generation) {
|
|
269
|
+
throw new HonuaCommandError("conflict", this.id, `Studio draft ${context.input.draftId} moved from generation ${context.input.generation} to ${draft.generation}; reload and retry.`, { correlationId: context.correlationId, idempotencyKey: context.idempotencyKey, statusCode: 409 });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const version = await studio.drafts.createContentVersion(context.input.draftId, writeOptions);
|
|
273
|
+
// The lifecycle API exposes no precondition on the content-version POST, so
|
|
274
|
+
// the pre-read alone leaves a window in which another editor's `PUT` lands
|
|
275
|
+
// between the check and the write. When the caller asked for concurrency
|
|
276
|
+
// protection, close the bracket: re-read and refuse to report a success we
|
|
277
|
+
// cannot vouch for. The created version is named so the caller can
|
|
278
|
+
// reconcile it rather than lose track of it.
|
|
279
|
+
if (context.input.generation !== undefined) {
|
|
280
|
+
const after = await studio.drafts.get(context.input.draftId, readOptions);
|
|
281
|
+
if (after.generation !== context.input.generation) {
|
|
282
|
+
throw new HonuaCommandError("conflict", this.id, `Studio draft ${context.input.draftId} moved from generation ${context.input.generation} to ${after.generation} while version ${version?.versionId ?? "(unknown)"} was being cut; reconcile that version, then reload and retry.`, { correlationId: context.correlationId, idempotencyKey: context.idempotencyKey, statusCode: 409 });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
output: version,
|
|
287
|
+
resourceRef: {
|
|
288
|
+
type: "studio-content-version",
|
|
289
|
+
...(version?.versionId ? { id: version.versionId } : {}),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Registry
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
/** Every command id in the 2026.1 catalog, in registry order. */
|
|
298
|
+
export const HONUA_COMMAND_IDS = [
|
|
299
|
+
"connection.test",
|
|
300
|
+
"import.create",
|
|
301
|
+
"map-package.publish",
|
|
302
|
+
"studio.draft.saveVersion",
|
|
303
|
+
];
|
|
304
|
+
/**
|
|
305
|
+
* The shared registry every transport dispatches through. MCP tool schemas,
|
|
306
|
+
* CLI help, Studio clients, and JS methods all project from these entries.
|
|
307
|
+
*/
|
|
308
|
+
export const HONUA_COMMANDS = {
|
|
309
|
+
"connection.test": connectionTestCommand,
|
|
310
|
+
"import.create": importCreateCommand,
|
|
311
|
+
"map-package.publish": mapPackagePublishCommand,
|
|
312
|
+
"studio.draft.saveVersion": studioDraftSaveVersionCommand,
|
|
313
|
+
};
|
|
314
|
+
/** Type guard narrowing an arbitrary string to a registered command id. */
|
|
315
|
+
export function isHonuaCommandId(value) {
|
|
316
|
+
return Object.hasOwn(HONUA_COMMANDS, value);
|
|
317
|
+
}
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Shared helpers
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
/**
|
|
322
|
+
* Unwrap a {@link HonuaControlPlaneResult}, converting the "this deployment
|
|
323
|
+
* does not expose the capability" arm into a typed `transport` failure so every
|
|
324
|
+
* transport reports the gap the same way.
|
|
325
|
+
*/
|
|
326
|
+
function unwrap(result, commandId, trace) {
|
|
327
|
+
if (result.supported)
|
|
328
|
+
return result.value;
|
|
329
|
+
throw new HonuaCommandError("transport", commandId, result.reason, {
|
|
330
|
+
correlationId: trace.correlationId,
|
|
331
|
+
idempotencyKey: trace.idempotencyKey,
|
|
332
|
+
...(result.statusCode !== undefined ? { statusCode: result.statusCode } : {}),
|
|
333
|
+
...(result.problem ? { problem: result.problem } : {}),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function headersToRecord(headers) {
|
|
337
|
+
if (!headers)
|
|
338
|
+
return {};
|
|
339
|
+
if (headers instanceof Headers)
|
|
340
|
+
return Object.fromEntries(headers.entries());
|
|
341
|
+
if (Array.isArray(headers))
|
|
342
|
+
return Object.fromEntries(headers);
|
|
343
|
+
return { ...headers };
|
|
344
|
+
}
|
|
345
|
+
function validatorsOf(value) {
|
|
346
|
+
if (!value)
|
|
347
|
+
return undefined;
|
|
348
|
+
const etag = value.etag;
|
|
349
|
+
const lastModified = value.lastModified;
|
|
350
|
+
if (!etag && !lastModified)
|
|
351
|
+
return undefined;
|
|
352
|
+
return { ...(etag ? { etag } : {}), ...(lastModified ? { lastModified } : {}) };
|
|
353
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure taxonomy for the shared control-plane command layer.
|
|
3
|
+
*
|
|
4
|
+
* Every transport surfaces the *same* five discriminants for the same
|
|
5
|
+
* underlying failure, so a negative RBAC or cross-tenant test written once
|
|
6
|
+
* behaves identically from the CLI, MCP, Studio, and direct JS.
|
|
7
|
+
*
|
|
8
|
+
* Like {@link ../../studio/lifecycle-errors.js | HonuaStudioError} and
|
|
9
|
+
* `HonuaCollaborationError`, this module intentionally does not join the
|
|
10
|
+
* central `HonuaSdkError` registry (`src/core/error-code-registry.ts`):
|
|
11
|
+
* `control-plane` is an `@experimental`, not-yet-semver-covered domain and a
|
|
12
|
+
* registry migration is scoped separately.
|
|
13
|
+
*
|
|
14
|
+
* @experimental
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import type { HonuaProblemDetails } from "../types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Failure classes a command can raise.
|
|
20
|
+
*
|
|
21
|
+
* - `validation` — input failed the command's declared JSON schema, or the
|
|
22
|
+
* server rejected the request shape (`400`/`422`).
|
|
23
|
+
* - `conflict` — optimistic concurrency lost: a stale `If-Match` validator or
|
|
24
|
+
* Studio `generation` (`409`/`412`). Reload and retry.
|
|
25
|
+
* - `authorization` — the caller is not permitted (`401`/`403`), or the caller
|
|
26
|
+
* tried to override authority locally (a reserved credential/authority
|
|
27
|
+
* header). Never retry with different local state.
|
|
28
|
+
* - `cancelled` — the caller's `AbortSignal` fired.
|
|
29
|
+
* - `transport` — everything else: network failure, unsupported capability,
|
|
30
|
+
* `5xx`, or a missing client the command needs.
|
|
31
|
+
*/
|
|
32
|
+
export type HonuaCommandErrorKind = "validation" | "conflict" | "authorization" | "cancelled" | "transport";
|
|
33
|
+
/** One schema or domain validation finding. */
|
|
34
|
+
export interface HonuaCommandValidationIssue {
|
|
35
|
+
/** Dotted path into the input, e.g. `package.sources[0].id`; `""` for the root. */
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}
|
|
39
|
+
/** Constructor options for {@link HonuaCommandError}. */
|
|
40
|
+
export interface HonuaCommandErrorOptions {
|
|
41
|
+
readonly cause?: unknown;
|
|
42
|
+
readonly correlationId?: string;
|
|
43
|
+
readonly idempotencyKey?: string;
|
|
44
|
+
readonly statusCode?: number;
|
|
45
|
+
readonly problem?: HonuaProblemDetails;
|
|
46
|
+
readonly issues?: readonly HonuaCommandValidationIssue[];
|
|
47
|
+
}
|
|
48
|
+
/** Serialization-safe projection of a {@link HonuaCommandError}. */
|
|
49
|
+
export interface SerializedHonuaCommandError {
|
|
50
|
+
readonly kind: "honua.command.error.v1";
|
|
51
|
+
readonly errorKind: HonuaCommandErrorKind;
|
|
52
|
+
readonly commandId: string;
|
|
53
|
+
readonly retryable: boolean;
|
|
54
|
+
readonly correlationId?: string;
|
|
55
|
+
readonly idempotencyKey?: string;
|
|
56
|
+
readonly statusCode?: number;
|
|
57
|
+
readonly issues?: readonly HonuaCommandValidationIssue[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The single error type every command raises. Callers branch on `.kind`
|
|
61
|
+
* rather than on HTTP status numbers or transport-specific error classes.
|
|
62
|
+
*/
|
|
63
|
+
export declare class HonuaCommandError extends Error {
|
|
64
|
+
readonly kind: HonuaCommandErrorKind;
|
|
65
|
+
readonly commandId: string;
|
|
66
|
+
readonly correlationId: string | undefined;
|
|
67
|
+
readonly idempotencyKey: string | undefined;
|
|
68
|
+
readonly statusCode: number | undefined;
|
|
69
|
+
readonly problem: HonuaProblemDetails | undefined;
|
|
70
|
+
readonly issues: readonly HonuaCommandValidationIssue[] | undefined;
|
|
71
|
+
/** `conflict` and `transport` are worth retrying; the rest are terminal. */
|
|
72
|
+
readonly retryable: boolean;
|
|
73
|
+
constructor(kind: HonuaCommandErrorKind, commandId: string, message: string, options?: HonuaCommandErrorOptions);
|
|
74
|
+
/** Safe JSON projection: no message, stack, cause, or response body. */
|
|
75
|
+
toJSON(): SerializedHonuaCommandError;
|
|
76
|
+
}
|
|
77
|
+
/** Type guard for {@link HonuaCommandError}. */
|
|
78
|
+
export declare function isHonuaCommandError(error: unknown): error is HonuaCommandError;
|
|
79
|
+
/** True when the failure is an optimistic-concurrency loss worth reloading and retrying. */
|
|
80
|
+
export declare function isHonuaCommandConflict(error: unknown): error is HonuaCommandError;
|
|
81
|
+
/** Map an HTTP status code onto the command taxonomy. */
|
|
82
|
+
export declare function classifyCommandStatus(statusCode: number): HonuaCommandErrorKind;
|
|
83
|
+
/**
|
|
84
|
+
* Normalize anything thrown beneath a command into a {@link HonuaCommandError}.
|
|
85
|
+
*
|
|
86
|
+
* Recognizes `AbortError`/`AbortSignal.reason` (→ `cancelled`), `HonuaHttpError`
|
|
87
|
+
* and any structurally equivalent error carrying `statusCode` (→ the status
|
|
88
|
+
* classification, which also covers `HonuaStudioError`'s `409`
|
|
89
|
+
* `generation-conflict`), and falls back to `transport`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function toHonuaCommandError(error: unknown, commandId: string, context: {
|
|
92
|
+
readonly correlationId: string;
|
|
93
|
+
readonly idempotencyKey: string;
|
|
94
|
+
readonly signal?: AbortSignal;
|
|
95
|
+
}): HonuaCommandError;
|
|
96
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../../src/control-plane/commands/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,UAAU,GAAG,eAAe,GAAG,WAAW,GAAG,WAAW,CAAC;AAE5G,+CAA+C;AAC/C,MAAM,WAAW,2BAA2B;IAC1C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,yDAAyD;AACzD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,2BAA2B,EAAE,CAAC;CAC1D;AAED,oEAAoE;AACpE,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,2BAA2B,EAAE,CAAC;CAC1D;AAID;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAgB,IAAI,EAAE,qBAAqB,CAAC;IAC5C,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,SAAgB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClD,SAAgB,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnD,SAAgB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,SAAgB,OAAO,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACzD,SAAgB,MAAM,EAAE,SAAS,2BAA2B,EAAE,GAAG,SAAS,CAAC;IAC3E,4EAA4E;IAC5E,SAAgB,SAAS,EAAE,OAAO,CAAC;gBAGjC,IAAI,EAAE,qBAAqB,EAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,wBAA6B;IAcxC,wEAAwE;IACjE,MAAM,IAAI,2BAA2B;CAY7C;AAED,gDAAgD;AAChD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAE9E;AAED,4FAA4F;AAC5F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAEjF;AAED,yDAAyD;AACzD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,qBAAqB,CAK/E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC1G,iBAAiB,CAwBnB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure taxonomy for the shared control-plane command layer.
|
|
3
|
+
*
|
|
4
|
+
* Every transport surfaces the *same* five discriminants for the same
|
|
5
|
+
* underlying failure, so a negative RBAC or cross-tenant test written once
|
|
6
|
+
* behaves identically from the CLI, MCP, Studio, and direct JS.
|
|
7
|
+
*
|
|
8
|
+
* Like {@link ../../studio/lifecycle-errors.js | HonuaStudioError} and
|
|
9
|
+
* `HonuaCollaborationError`, this module intentionally does not join the
|
|
10
|
+
* central `HonuaSdkError` registry (`src/core/error-code-registry.ts`):
|
|
11
|
+
* `control-plane` is an `@experimental`, not-yet-semver-covered domain and a
|
|
12
|
+
* registry migration is scoped separately.
|
|
13
|
+
*
|
|
14
|
+
* @experimental
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { HonuaHttpError } from "../../core/errors.js";
|
|
18
|
+
const RETRYABLE_KINDS = new Set(["conflict", "transport"]);
|
|
19
|
+
/**
|
|
20
|
+
* The single error type every command raises. Callers branch on `.kind`
|
|
21
|
+
* rather than on HTTP status numbers or transport-specific error classes.
|
|
22
|
+
*/
|
|
23
|
+
export class HonuaCommandError extends Error {
|
|
24
|
+
kind;
|
|
25
|
+
commandId;
|
|
26
|
+
correlationId;
|
|
27
|
+
idempotencyKey;
|
|
28
|
+
statusCode;
|
|
29
|
+
problem;
|
|
30
|
+
issues;
|
|
31
|
+
/** `conflict` and `transport` are worth retrying; the rest are terminal. */
|
|
32
|
+
retryable;
|
|
33
|
+
constructor(kind, commandId, message, options = {}) {
|
|
34
|
+
super(message, "cause" in options ? { cause: options.cause } : undefined);
|
|
35
|
+
this.name = "HonuaCommandError";
|
|
36
|
+
this.kind = kind;
|
|
37
|
+
this.commandId = commandId;
|
|
38
|
+
this.correlationId = options.correlationId;
|
|
39
|
+
this.idempotencyKey = options.idempotencyKey;
|
|
40
|
+
this.statusCode = options.statusCode;
|
|
41
|
+
this.problem = options.problem;
|
|
42
|
+
this.issues = options.issues;
|
|
43
|
+
this.retryable = RETRYABLE_KINDS.has(kind);
|
|
44
|
+
}
|
|
45
|
+
/** Safe JSON projection: no message, stack, cause, or response body. */
|
|
46
|
+
toJSON() {
|
|
47
|
+
return {
|
|
48
|
+
kind: "honua.command.error.v1",
|
|
49
|
+
errorKind: this.kind,
|
|
50
|
+
commandId: this.commandId,
|
|
51
|
+
retryable: this.retryable,
|
|
52
|
+
...(this.correlationId ? { correlationId: this.correlationId } : {}),
|
|
53
|
+
...(this.idempotencyKey ? { idempotencyKey: this.idempotencyKey } : {}),
|
|
54
|
+
...(this.statusCode !== undefined ? { statusCode: this.statusCode } : {}),
|
|
55
|
+
...(this.issues ? { issues: this.issues } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Type guard for {@link HonuaCommandError}. */
|
|
60
|
+
export function isHonuaCommandError(error) {
|
|
61
|
+
return error instanceof HonuaCommandError;
|
|
62
|
+
}
|
|
63
|
+
/** True when the failure is an optimistic-concurrency loss worth reloading and retrying. */
|
|
64
|
+
export function isHonuaCommandConflict(error) {
|
|
65
|
+
return isHonuaCommandError(error) && error.kind === "conflict";
|
|
66
|
+
}
|
|
67
|
+
/** Map an HTTP status code onto the command taxonomy. */
|
|
68
|
+
export function classifyCommandStatus(statusCode) {
|
|
69
|
+
if (statusCode === 400 || statusCode === 422)
|
|
70
|
+
return "validation";
|
|
71
|
+
if (statusCode === 401 || statusCode === 403)
|
|
72
|
+
return "authorization";
|
|
73
|
+
if (statusCode === 409 || statusCode === 412 || statusCode === 428)
|
|
74
|
+
return "conflict";
|
|
75
|
+
return "transport";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Normalize anything thrown beneath a command into a {@link HonuaCommandError}.
|
|
79
|
+
*
|
|
80
|
+
* Recognizes `AbortError`/`AbortSignal.reason` (→ `cancelled`), `HonuaHttpError`
|
|
81
|
+
* and any structurally equivalent error carrying `statusCode` (→ the status
|
|
82
|
+
* classification, which also covers `HonuaStudioError`'s `409`
|
|
83
|
+
* `generation-conflict`), and falls back to `transport`.
|
|
84
|
+
*/
|
|
85
|
+
export function toHonuaCommandError(error, commandId, context) {
|
|
86
|
+
if (isHonuaCommandError(error))
|
|
87
|
+
return error;
|
|
88
|
+
const base = {
|
|
89
|
+
cause: error,
|
|
90
|
+
correlationId: context.correlationId,
|
|
91
|
+
idempotencyKey: context.idempotencyKey,
|
|
92
|
+
};
|
|
93
|
+
if (isAbortLike(error) || context.signal?.aborted === true) {
|
|
94
|
+
return new HonuaCommandError("cancelled", commandId, `Command ${commandId} was cancelled by the caller.`, base);
|
|
95
|
+
}
|
|
96
|
+
const statusCode = httpStatusOf(error);
|
|
97
|
+
if (statusCode !== undefined) {
|
|
98
|
+
const kind = classifyCommandStatus(statusCode);
|
|
99
|
+
return new HonuaCommandError(kind, commandId, `Command ${commandId} failed with HTTP ${statusCode}.`, {
|
|
100
|
+
...base,
|
|
101
|
+
statusCode,
|
|
102
|
+
...(problemOf(error) ? { problem: problemOf(error) } : {}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return new HonuaCommandError("transport", commandId, `Command ${commandId} failed before a response was read.`, base);
|
|
106
|
+
}
|
|
107
|
+
function isAbortLike(error) {
|
|
108
|
+
if (typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError")
|
|
109
|
+
return true;
|
|
110
|
+
return error instanceof Error && error.name === "AbortError";
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Read an HTTP status off `HonuaHttpError` or any structurally equivalent
|
|
114
|
+
* error. Studio's `HonuaStudioError` is matched structurally on purpose — it
|
|
115
|
+
* ships in a different split package, so `instanceof` is not reliable.
|
|
116
|
+
*/
|
|
117
|
+
function httpStatusOf(error) {
|
|
118
|
+
if (error instanceof HonuaHttpError)
|
|
119
|
+
return error.statusCode;
|
|
120
|
+
if (typeof error !== "object" || error === null)
|
|
121
|
+
return undefined;
|
|
122
|
+
const statusCode = error.statusCode;
|
|
123
|
+
return typeof statusCode === "number" && Number.isInteger(statusCode) ? statusCode : undefined;
|
|
124
|
+
}
|
|
125
|
+
function problemOf(error) {
|
|
126
|
+
if (typeof error !== "object" || error === null)
|
|
127
|
+
return undefined;
|
|
128
|
+
const problem = error.problem;
|
|
129
|
+
return typeof problem === "object" && problem !== null ? problem : undefined;
|
|
130
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic identity primitives for command receipts.
|
|
3
|
+
*
|
|
4
|
+
* A receipt has to be reproducible: the same command with the same input and
|
|
5
|
+
* the same tenant must yield the same `idempotencyKey`, `correlationId`, and
|
|
6
|
+
* `auditKey` regardless of which transport issued it, which process it ran in,
|
|
7
|
+
* or when. Nothing here reads the clock, the network, or a random source.
|
|
8
|
+
*
|
|
9
|
+
* The canonical serializer mirrors `canonicalJson` in `src/offline/digest.ts`
|
|
10
|
+
* (sorted own keys, no `undefined` members); it is redeclared rather than
|
|
11
|
+
* imported so the control-plane split package does not depend on the offline
|
|
12
|
+
* module. The hash is a 128-bit FNV-1a variant: an identity key, deliberately
|
|
13
|
+
* **not** a security primitive — nothing authorizes on it.
|
|
14
|
+
*
|
|
15
|
+
* @experimental
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import type { HonuaCommandIdentity, HonuaCommandReceipt } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Serialize `value` with sorted own keys, dropping `undefined` members.
|
|
21
|
+
*
|
|
22
|
+
* Dropping (rather than throwing on) `undefined` matches how the command layer
|
|
23
|
+
* builds inputs: optional fields are routinely absent, and an absent field and
|
|
24
|
+
* an explicit `undefined` must hash the same so a CLI adapter that always sets
|
|
25
|
+
* every flag key agrees with a JS caller that omits them.
|
|
26
|
+
*/
|
|
27
|
+
export declare function canonicalCommandJson(value: unknown): string;
|
|
28
|
+
/**
|
|
29
|
+
* 128-bit FNV-1a over UTF-16 code units, rendered as 32 lowercase hex digits.
|
|
30
|
+
*
|
|
31
|
+
* Four independently seeded 32-bit lanes. Stable across engines and platforms
|
|
32
|
+
* because it never touches `TextEncoder`, `crypto`, or locale-sensitive APIs.
|
|
33
|
+
*/
|
|
34
|
+
export declare function commandDigest(text: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Derive the `Idempotency-Key` for a command invocation.
|
|
37
|
+
*
|
|
38
|
+
* Bound to the command id, the canonicalized input, and the tenant echo — so
|
|
39
|
+
* the *same* logical request retried from any transport collapses to one
|
|
40
|
+
* server-side effect, while the same input under a different tenant does not.
|
|
41
|
+
*/
|
|
42
|
+
export declare function deriveIdempotencyKey(commandId: string, input: unknown, identity: HonuaCommandIdentity): string;
|
|
43
|
+
/**
|
|
44
|
+
* Derive a correlation id from the idempotency key.
|
|
45
|
+
*
|
|
46
|
+
* Deterministic on purpose: a random trace id would make receipts differ
|
|
47
|
+
* between transports and break the audit join the issue asks for. Callers that
|
|
48
|
+
* already own a trace id pass it in and this is not used.
|
|
49
|
+
*/
|
|
50
|
+
export declare function deriveCorrelationId(commandId: string, idempotencyKey: string): string;
|
|
51
|
+
/** Normalize a claimed identity: trimmed values, sorted deduplicated scopes, no empty keys. */
|
|
52
|
+
export declare function normalizeCommandIdentity(identity: HonuaCommandIdentity | undefined): HonuaCommandIdentity;
|
|
53
|
+
/**
|
|
54
|
+
* The transport-independent projection of a receipt.
|
|
55
|
+
*
|
|
56
|
+
* Everything a server-side audit join needs, minus the one field that is
|
|
57
|
+
* allowed to differ between two equivalent calls (`transport`) and minus the
|
|
58
|
+
* `auditKey` itself.
|
|
59
|
+
*/
|
|
60
|
+
export declare function honuaCommandReceiptProjection(receipt: HonuaCommandReceipt): Record<string, unknown>;
|
|
61
|
+
/**
|
|
62
|
+
* Stable join key for a receipt. Equal for equivalent CLI, MCP, Studio, and JS
|
|
63
|
+
* calls; changes whenever any recorded field of the invocation changes.
|
|
64
|
+
*/
|
|
65
|
+
export declare function honuaCommandAuditKey(receipt: HonuaCommandReceipt): string;
|
|
66
|
+
/**
|
|
67
|
+
* Canonical, byte-stable serialization of a receipt — sorted keys, no
|
|
68
|
+
* `undefined`. Use this when persisting a receipt or comparing two of them.
|
|
69
|
+
*/
|
|
70
|
+
export declare function serializeHonuaCommandReceipt(receipt: HonuaCommandReceipt): string;
|
|
71
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../../../src/control-plane/commands/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAU5E;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAY3D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASlD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAO9G;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,+FAA+F;AAC/F,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,SAAS,GAAG,oBAAoB,CAUzG;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGnG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAEzE;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAEjF"}
|
|
Binary file
|