@honua/mcp-server 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/README.md +30 -2
- package/dist/src/certification/admin-parity.d.ts +96 -0
- package/dist/src/certification/admin-parity.d.ts.map +1 -0
- package/dist/src/certification/admin-parity.js +235 -0
- package/dist/src/certification/target.d.ts.map +1 -1
- package/dist/src/certification/target.js +18 -26
- package/dist/src/credential-endpoint.d.ts +3 -0
- package/dist/src/credential-endpoint.d.ts.map +1 -0
- package/dist/src/credential-endpoint.js +24 -0
- package/dist/src/entrypoint.d.ts +20 -0
- package/dist/src/entrypoint.d.ts.map +1 -0
- package/dist/src/entrypoint.js +38 -0
- package/dist/src/index.d.ts +22 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +33 -5
- package/dist/src/proxy.d.ts.map +1 -1
- package/dist/src/proxy.js +31 -16
- package/dist/src/release/zero-to-map-checkpoint.d.ts +34 -0
- package/dist/src/release/zero-to-map-checkpoint.d.ts.map +1 -0
- package/dist/src/release/zero-to-map-checkpoint.js +369 -0
- package/dist/src/release/zero-to-map-cli.d.ts +51 -0
- package/dist/src/release/zero-to-map-cli.d.ts.map +1 -0
- package/dist/src/release/zero-to-map-cli.js +881 -0
- package/dist/src/release/zero-to-map-provision.d.ts +37 -0
- package/dist/src/release/zero-to-map-provision.d.ts.map +1 -0
- package/dist/src/release/zero-to-map-provision.js +177 -0
- package/dist/src/release/zero-to-map.d.ts +296 -0
- package/dist/src/release/zero-to-map.d.ts.map +1 -0
- package/dist/src/release/zero-to-map.js +1069 -0
- package/dist/src/tools/admin-install-local.d.ts +30 -0
- package/dist/src/tools/admin-install-local.d.ts.map +1 -0
- package/dist/src/tools/admin-install-local.js +22 -0
- package/dist/src/tools/control-plane-command.d.ts +99 -0
- package/dist/src/tools/control-plane-command.d.ts.map +1 -0
- package/dist/src/tools/control-plane-command.js +180 -0
- package/dist/src/tools/docs-search.d.ts +100 -0
- package/dist/src/tools/docs-search.d.ts.map +1 -0
- package/dist/src/tools/docs-search.js +381 -0
- package/package.json +8 -3
- package/release/zero-to-map/README.md +174 -0
- package/release/zero-to-map/configs/claude-code.mcp.json +11 -0
- package/release/zero-to-map/configs/claude-desktop.json +11 -0
- package/release/zero-to-map/configs/cursor.json +11 -0
- package/release/zero-to-map/contracts/aws-ecs-provision-binding.schema.json +59 -0
- package/release/zero-to-map/contracts/checkpoint.schema.json +152 -0
- package/release/zero-to-map/contracts/console-receipt.schema.json +150 -0
- package/release/zero-to-map/contracts/esri-gp-mcp.v1.json +73 -0
- package/release/zero-to-map/fixtures/parcels.geojson +33 -0
- package/release/zero-to-map/fixtures/zoning.geojson +24 -0
- package/release/zero-to-map/journey.v1.json +1248 -0
package/dist/src/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { HonuaClient } from "@honua/sdk-js";
|
|
5
4
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { isMainEntrypoint } from "./entrypoint.js";
|
|
7
7
|
import { registerNlMapControlMcpTools } from "./nl-map-control.js";
|
|
8
8
|
import * as layerSchemaResource from "./resources/layer-schema.js";
|
|
9
9
|
import * as servicesResource from "./resources/services.js";
|
|
10
10
|
import * as stylesResource from "./resources/styles.js";
|
|
11
|
+
import * as adminInstallLocal from "./tools/admin-install-local.js";
|
|
11
12
|
import * as applyStylePreset from "./tools/apply-style-preset.js";
|
|
13
|
+
import * as controlPlaneCommand from "./tools/control-plane-command.js";
|
|
12
14
|
import * as countFeatures from "./tools/count-features.js";
|
|
13
15
|
import * as describeLayer from "./tools/describe-layer.js";
|
|
16
|
+
import * as docsSearch from "./tools/docs-search.js";
|
|
14
17
|
import * as explainCapabilityGap from "./tools/explain-capability-gap.js";
|
|
15
18
|
import * as getExtent from "./tools/get-extent.js";
|
|
16
19
|
import * as getStyle from "./tools/get-style.js";
|
|
@@ -19,6 +22,7 @@ import * as listSources from "./tools/list-sources.js";
|
|
|
19
22
|
import * as queryFeatures from "./tools/query-features.js";
|
|
20
23
|
import * as statistics from "./tools/statistics.js";
|
|
21
24
|
export { createNlMapControlMcpHost, registerNlMapControlMcpTools, } from "./nl-map-control.js";
|
|
25
|
+
export { CONTROL_PLANE_COMMAND_TOOL_PREFIX, controlPlaneCommandToolName, controlPlaneCommandToolSchema, registerControlPlaneCommandTools, } from "./tools/control-plane-command.js";
|
|
22
26
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
23
27
|
const DEFAULT_MAX_RETRIES = 2;
|
|
24
28
|
const DEFAULT_SERVER_VERSION = "0.0.3-alpha.0";
|
|
@@ -146,6 +150,15 @@ export function createClientFromEnv(env = process.env) {
|
|
|
146
150
|
* mutation, jobs, and publishing — is a superset reached over streamable-HTTP and
|
|
147
151
|
* mirrored to stdio by `honua-mcp-proxy` ({@link runProxy}); it requires a Honua
|
|
148
152
|
* deployment. Standalone is the front door; the proxy is the upgrade path.
|
|
153
|
+
* Local installation is intentionally absent from this catalog and is exposed
|
|
154
|
+
* only by {@link createBootstrapServer}.
|
|
155
|
+
*
|
|
156
|
+
* Control-plane MUTATION is likewise absent by default, and when a host does
|
|
157
|
+
* enable it ({@link CreateServerOptions.controlPlaneCommands}) MCP does not get
|
|
158
|
+
* a workflow of its own: the tools are generated from the shared command
|
|
159
|
+
* catalog in `@honua/sdk-js/control-plane`, so an MCP invocation and the
|
|
160
|
+
* equivalent `honua` CLI, Studio, or direct-JS invocation share one
|
|
161
|
+
* implementation and one receipt `auditKey`.
|
|
149
162
|
*/
|
|
150
163
|
export function createServer(client, options = {}) {
|
|
151
164
|
const server = new McpServer({
|
|
@@ -166,6 +179,12 @@ export function createServer(client, options = {}) {
|
|
|
166
179
|
server.tool("honua_explain_capability_gap", "Explain whether a source/protocol supports a capability and suggest a safe fallback when it does not.", explainCapabilityGap.schema.shape, async (args) => explainCapabilityGap.execute(client, explainCapabilityGap.schema.parse(args)));
|
|
167
180
|
server.tool("honua_get_style", "Resolve a style from a server's OGC API – Styles surface. Pass styleId for the canonical StyleRef (style_id, title, encodings incl. inlined MapLibre); omit it to list available styles. On a plain FeatureServer with no styling surface, reports a structured 'not available on this target' result.", getStyle.schema.shape, async (args) => getStyle.execute(client, getStyle.schema.parse(args)));
|
|
168
181
|
server.tool("honua_apply_style_preset", "Resolve a style preset and its MapLibre stylesheet for client-side application. Read-only: returns the stylesheet to apply locally; it does not mutate server state. Degrades gracefully on a target with no styling surface.", applyStylePreset.schema.shape, async (args) => applyStylePreset.execute(client, applyStylePreset.schema.parse(args)));
|
|
182
|
+
if (options.controlPlaneCommands) {
|
|
183
|
+
// Thin adapters generated from the shared command catalog. Registered last
|
|
184
|
+
// so the platform-free read-only surface keeps its stable tool order.
|
|
185
|
+
controlPlaneCommand.registerControlPlaneCommandTools(server, options.controlPlaneCommands);
|
|
186
|
+
}
|
|
187
|
+
server.tool("honua_docs_search", "Answer a Honua documentation question from the local versioned documentation corpus (llms-full.txt), returning ranked excerpts with a source-file and release-version citation for each hit. Local and offline: reads committed docs, never the network. Use it before guessing at an API, a CLI flag, or a contract. Reports a structured 'not available' result when the corpus is not present in this installation.", docsSearch.schema.shape, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async (args) => docsSearch.execute(client, docsSearch.schema.parse(args)));
|
|
169
188
|
// ── Resources ──────────────────────────────────────────────────
|
|
170
189
|
server.resource("services-catalog", servicesResource.uri, async (_uri) => servicesResource.read(client));
|
|
171
190
|
server.resource("layer-schema", new ResourceTemplate(layerSchemaResource.uriTemplate, { list: undefined }), async (_uri, params) => layerSchemaResource.read(client, params.encodedServiceId, params.layerId));
|
|
@@ -173,6 +192,15 @@ export function createServer(client, options = {}) {
|
|
|
173
192
|
server.resource("style", new ResourceTemplate(stylesResource.uriTemplate, { list: undefined }), async (_uri, params) => stylesResource.read(client, params.styleId));
|
|
174
193
|
return server;
|
|
175
194
|
}
|
|
195
|
+
/** Bootstrap-only MCP surface for a laptop that does not have Honua yet. */
|
|
196
|
+
export function createBootstrapServer() {
|
|
197
|
+
const server = new McpServer({ name: "honua-bootstrap", version: SERVER_VERSION });
|
|
198
|
+
registerLocalInstallTool(server);
|
|
199
|
+
return server;
|
|
200
|
+
}
|
|
201
|
+
function registerLocalInstallTool(server) {
|
|
202
|
+
server.tool("honua_admin_install_local", "Install the manifest-pinned Honua Server, PostGIS, and Redis on this laptop, wait for readiness, bootstrap a scoped admin key, and write proxy MCP configuration. Requires confirm=true.", adminInstallLocal.schema.shape, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async (args) => adminInstallLocal.execute(adminInstallLocal.schema.parse(args)));
|
|
203
|
+
}
|
|
176
204
|
/* v8 ignore start -- live-process entry: wires the stdio transport to a real
|
|
177
205
|
SDK client against HONUA_BASE_URL; exercised by running the bin, not unit tests.
|
|
178
206
|
The unit-testable logic (option resolution, client construction, server wiring)
|
|
@@ -181,14 +209,14 @@ export function createServer(client, options = {}) {
|
|
|
181
209
|
* Run the platform-free standalone server end-to-end over stdio.
|
|
182
210
|
*
|
|
183
211
|
* Builds a `HonuaClient` from the environment (`HONUA_BASE_URL` + optional
|
|
184
|
-
* transport/auth) and exposes {@link createServer} — the
|
|
212
|
+
* transport/auth) and exposes {@link createServer} — the ten read-only tools —
|
|
185
213
|
* to a stdio MCP client (Claude Desktop / Claude Code / any MCP agent). Point
|
|
186
214
|
* `HONUA_BASE_URL` at any public FeatureServer/OGC endpoint; no Honua server is
|
|
187
215
|
* required. For the Honua-enhanced `/mcp` catalog, use `honua-mcp-proxy` instead.
|
|
188
216
|
*/
|
|
189
217
|
export async function runStandalone(env = process.env) {
|
|
190
|
-
const
|
|
191
|
-
const server = createServer(
|
|
218
|
+
const bootstrapOnly = env.HONUA_MCP_BOOTSTRAP === "1" || env.HONUA_MCP_BOOTSTRAP === "true";
|
|
219
|
+
const server = bootstrapOnly ? createBootstrapServer() : createServer(createClientFromEnv(env));
|
|
192
220
|
const transport = new StdioServerTransport();
|
|
193
221
|
await server.connect(transport);
|
|
194
222
|
}
|
|
@@ -196,7 +224,7 @@ export async function runStandalone(env = process.env) {
|
|
|
196
224
|
async function main() {
|
|
197
225
|
await runStandalone();
|
|
198
226
|
}
|
|
199
|
-
if (
|
|
227
|
+
if (isMainEntrypoint(import.meta.url)) {
|
|
200
228
|
main().catch((err) => {
|
|
201
229
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
202
230
|
process.exit(1);
|
package/dist/src/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAEnE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAmBnE;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAsBD,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAqBtF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUlF;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+C1D;AAOD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBlF"}
|
package/dist/src/proxy.js
CHANGED
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
3
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
6
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, PromptListChangedNotificationSchema, ReadResourceRequestSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { requireSecureCredentialEndpoint } from "./credential-endpoint.js";
|
|
8
|
+
import { isMainEntrypoint } from "./entrypoint.js";
|
|
8
9
|
import { SERVER_VERSION } from "./index.js";
|
|
10
|
+
function isConfigured(value) {
|
|
11
|
+
return value !== undefined && value.length > 0;
|
|
12
|
+
}
|
|
13
|
+
function validateAuthentication(options) {
|
|
14
|
+
const hasBearer = isConfigured(options.authToken);
|
|
15
|
+
const hasApiKey = isConfigured(options.apiKey);
|
|
16
|
+
if (hasBearer && hasApiKey) {
|
|
17
|
+
throw new Error("Configure exactly one upstream authentication scheme: unset either authToken/HONUA_MCP_AUTH_TOKEN or apiKey/HONUA_ADMIN_KEY/HONUA_API_KEY");
|
|
18
|
+
}
|
|
19
|
+
return hasBearer ? "bearer" : hasApiKey ? "api-key" : "anonymous";
|
|
20
|
+
}
|
|
21
|
+
function validateProxyOptions(options) {
|
|
22
|
+
const authMode = validateAuthentication(options);
|
|
23
|
+
return requireSecureCredentialEndpoint(options.remoteUrl, "remoteUrl", authMode !== "anonymous");
|
|
24
|
+
}
|
|
9
25
|
export function resolveProxyOptions(env = process.env) {
|
|
10
26
|
const remoteUrl = env.HONUA_MCP_REMOTE_URL ?? env.HONUA_MCP_URL;
|
|
11
27
|
if (!remoteUrl) {
|
|
12
28
|
throw new Error("HONUA_MCP_REMOTE_URL environment variable is required (the remote honua /mcp endpoint to proxy).");
|
|
13
29
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
catch {
|
|
19
|
-
throw new Error(`HONUA_MCP_REMOTE_URL must be a valid absolute URL: ${remoteUrl}`);
|
|
20
|
-
}
|
|
21
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
22
|
-
throw new Error(`HONUA_MCP_REMOTE_URL must use http or https: ${remoteUrl}`);
|
|
30
|
+
const hasAdminKey = isConfigured(env.HONUA_ADMIN_KEY);
|
|
31
|
+
const hasApiKey = isConfigured(env.HONUA_API_KEY);
|
|
32
|
+
if (hasAdminKey && hasApiKey) {
|
|
33
|
+
throw new Error("Configure one API-key source: unset either HONUA_ADMIN_KEY or HONUA_API_KEY; credential precedence is not allowed");
|
|
23
34
|
}
|
|
24
|
-
|
|
25
|
-
remoteUrl
|
|
35
|
+
const options = {
|
|
36
|
+
remoteUrl,
|
|
26
37
|
authToken: env.HONUA_MCP_AUTH_TOKEN,
|
|
27
|
-
apiKey: env.HONUA_API_KEY,
|
|
38
|
+
apiKey: hasAdminKey ? env.HONUA_ADMIN_KEY : hasApiKey ? env.HONUA_API_KEY : undefined,
|
|
28
39
|
};
|
|
40
|
+
const parsed = validateProxyOptions(options);
|
|
41
|
+
return { ...options, remoteUrl: parsed.toString() };
|
|
29
42
|
}
|
|
30
43
|
/** Build request headers for the upstream connection from the resolved options. */
|
|
31
44
|
export function buildUpstreamHeaders(options) {
|
|
45
|
+
validateProxyOptions(options);
|
|
32
46
|
const headers = {};
|
|
33
47
|
if (options.authToken) {
|
|
34
48
|
headers.Authorization = `Bearer ${options.authToken}`;
|
|
@@ -41,8 +55,9 @@ export function buildUpstreamHeaders(options) {
|
|
|
41
55
|
/** Connect an upstream MCP client to the remote honua /mcp over streamable HTTP. */
|
|
42
56
|
export async function connectUpstream(options) {
|
|
43
57
|
const headers = buildUpstreamHeaders(options);
|
|
44
|
-
const
|
|
45
|
-
|
|
58
|
+
const remoteUrl = validateProxyOptions(options);
|
|
59
|
+
const transport = new StreamableHTTPClientTransport(remoteUrl, {
|
|
60
|
+
requestInit: { ...(Object.keys(headers).length > 0 ? { headers } : {}), redirect: "manual" },
|
|
46
61
|
});
|
|
47
62
|
const client = new Client({ name: "honua-mcp-stdio-proxy", version: SERVER_VERSION });
|
|
48
63
|
await client.connect(transport);
|
|
@@ -115,7 +130,7 @@ export async function runProxy(env = process.env) {
|
|
|
115
130
|
const transport = new StdioServerTransport();
|
|
116
131
|
await server.connect(transport);
|
|
117
132
|
}
|
|
118
|
-
if (
|
|
133
|
+
if (isMainEntrypoint(import.meta.url)) {
|
|
119
134
|
runProxy().catch((err) => {
|
|
120
135
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
121
136
|
process.exit(1);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type JourneyPauseSnapshot, type JourneyReceiptRequest, type JourneyResumeState } from "./zero-to-map.js";
|
|
2
|
+
export declare const ZERO_TO_MAP_CHECKPOINT_SCHEMA: "honua.zero-to-map.checkpoint/v1";
|
|
3
|
+
export declare const ZERO_TO_MAP_CHECKPOINT_MAX_AGE_MS: number;
|
|
4
|
+
export interface ZeroToMapCheckpointBindings {
|
|
5
|
+
readonly journeyId: string;
|
|
6
|
+
readonly releaseContract: string;
|
|
7
|
+
readonly target: "local-docker" | "aws-ecs";
|
|
8
|
+
readonly planSha256: string;
|
|
9
|
+
readonly sourceRevision: string;
|
|
10
|
+
readonly mcpEndpointSha256: string;
|
|
11
|
+
readonly candidateId: string;
|
|
12
|
+
readonly releaseId: string;
|
|
13
|
+
readonly provisionReceiptSha256?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ZeroToMapCheckpoint extends ZeroToMapCheckpointBindings {
|
|
16
|
+
readonly schemaVersion: typeof ZERO_TO_MAP_CHECKPOINT_SCHEMA;
|
|
17
|
+
readonly state: "paused" | "consumed";
|
|
18
|
+
readonly createdAt: string;
|
|
19
|
+
readonly resume: JourneyResumeState;
|
|
20
|
+
readonly consoleReceiptRequest: JourneyReceiptRequest;
|
|
21
|
+
readonly consumedAt?: string;
|
|
22
|
+
readonly finalReceiptSha256?: string;
|
|
23
|
+
readonly integrity: {
|
|
24
|
+
readonly algorithm: "sha256";
|
|
25
|
+
readonly digest: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function createZeroToMapCheckpoint(bindings: ZeroToMapCheckpointBindings, snapshot: JourneyPauseSnapshot, createdAt?: string): ZeroToMapCheckpoint;
|
|
29
|
+
export declare function consumeZeroToMapCheckpoint(checkpoint: ZeroToMapCheckpoint, finalReceiptSha256: string, consumedAt?: string): ZeroToMapCheckpoint;
|
|
30
|
+
export declare function parseZeroToMapCheckpoint(value: unknown): ZeroToMapCheckpoint;
|
|
31
|
+
export declare function assertZeroToMapCheckpointFresh(checkpoint: ZeroToMapCheckpoint, now?: Date): void;
|
|
32
|
+
export declare function assertZeroToMapCheckpointDigest(checkpoint: ZeroToMapCheckpoint, expectedDigest: string): void;
|
|
33
|
+
export declare function assertZeroToMapCheckpointBindings(checkpoint: ZeroToMapCheckpoint, expected: ZeroToMapCheckpointBindings): void;
|
|
34
|
+
//# sourceMappingURL=zero-to-map-checkpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zero-to-map-checkpoint.d.ts","sourceRoot":"","sources":["../../../src/release/zero-to-map-checkpoint.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAE1B,KAAK,kBAAkB,EAMxB,MAAM,kBAAkB,CAAC;AAE1B,eAAO,MAAM,6BAA6B,EAAG,iCAA0C,CAAC;AACxF,eAAO,MAAM,iCAAiC,QAAuB,CAAC;AAEtE,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAC1C;AAED,MAAM,WAAW,mBAAoB,SAAQ,2BAA2B;IACtE,QAAQ,CAAC,aAAa,EAAE,OAAO,6BAA6B,CAAC;IAC7D,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,UAAU,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE;QAClB,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC;QAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAID,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,2BAA2B,EACrC,QAAQ,EAAE,oBAAoB,EAC9B,SAAS,SAA2B,GACnC,mBAAmB,CAqBrB;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,mBAAmB,EAC/B,kBAAkB,EAAE,MAAM,EAC1B,UAAU,SAA2B,GACpC,mBAAmB,CAIrB;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAkF5E;AAED,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,mBAAmB,EAAE,GAAG,OAAa,GAAG,IAAI,CAKtG;AAED,wBAAgB,+BAA+B,CAAC,UAAU,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAI7G;AAED,wBAAgB,iCAAiC,CAC/C,UAAU,EAAE,mBAAmB,EAC/B,QAAQ,EAAE,2BAA2B,GACpC,IAAI,CAIN"}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ZERO_TO_MAP_CATALOG_RECEIPT_SCHEMA, ZERO_TO_MAP_CONSOLE_RECEIPT_REQUEST_SCHEMA, } from "./zero-to-map.js";
|
|
3
|
+
export const ZERO_TO_MAP_CHECKPOINT_SCHEMA = "honua.zero-to-map.checkpoint/v1";
|
|
4
|
+
export const ZERO_TO_MAP_CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
5
|
+
export function createZeroToMapCheckpoint(bindings, snapshot, createdAt = new Date().toISOString()) {
|
|
6
|
+
assertTargetBinding(bindings);
|
|
7
|
+
const payload = {
|
|
8
|
+
schemaVersion: ZERO_TO_MAP_CHECKPOINT_SCHEMA,
|
|
9
|
+
state: "paused",
|
|
10
|
+
createdAt,
|
|
11
|
+
...bindings,
|
|
12
|
+
resume: {
|
|
13
|
+
startedAt: snapshot.startedAt,
|
|
14
|
+
capturedVariables: snapshot.capturedVariables,
|
|
15
|
+
completedStages: snapshot.completedStages,
|
|
16
|
+
resumeAt: snapshot.resumeAt,
|
|
17
|
+
// Carried across the Console boundary because no post-checkpoint stage
|
|
18
|
+
// can re-derive it; the resumed run's final receipt would otherwise
|
|
19
|
+
// overwrite the pre-pause receipt with one that has no catalog evidence.
|
|
20
|
+
...(snapshot.catalog ? { catalog: snapshot.catalog } : {}),
|
|
21
|
+
},
|
|
22
|
+
consoleReceiptRequest: snapshot.consoleReceiptRequest,
|
|
23
|
+
};
|
|
24
|
+
assertSecretFree(payload);
|
|
25
|
+
return seal(payload);
|
|
26
|
+
}
|
|
27
|
+
export function consumeZeroToMapCheckpoint(checkpoint, finalReceiptSha256, consumedAt = new Date().toISOString()) {
|
|
28
|
+
if (checkpoint.state !== "paused")
|
|
29
|
+
throw new Error("checkpoint has already been consumed");
|
|
30
|
+
const { integrity: _integrity, ...prior } = checkpoint;
|
|
31
|
+
return seal({ ...prior, state: "consumed", consumedAt, finalReceiptSha256 });
|
|
32
|
+
}
|
|
33
|
+
export function parseZeroToMapCheckpoint(value) {
|
|
34
|
+
const checkpoint = object(value, "checkpoint");
|
|
35
|
+
exactKeys(checkpoint, [
|
|
36
|
+
"schemaVersion",
|
|
37
|
+
"state",
|
|
38
|
+
"createdAt",
|
|
39
|
+
"journeyId",
|
|
40
|
+
"releaseContract",
|
|
41
|
+
"target",
|
|
42
|
+
"planSha256",
|
|
43
|
+
"sourceRevision",
|
|
44
|
+
"mcpEndpointSha256",
|
|
45
|
+
"candidateId",
|
|
46
|
+
"releaseId",
|
|
47
|
+
"provisionReceiptSha256",
|
|
48
|
+
"resume",
|
|
49
|
+
"consoleReceiptRequest",
|
|
50
|
+
"consumedAt",
|
|
51
|
+
"finalReceiptSha256",
|
|
52
|
+
"integrity",
|
|
53
|
+
], "checkpoint");
|
|
54
|
+
if (checkpoint.schemaVersion !== ZERO_TO_MAP_CHECKPOINT_SCHEMA) {
|
|
55
|
+
throw new Error(`checkpoint.schemaVersion must be ${ZERO_TO_MAP_CHECKPOINT_SCHEMA}`);
|
|
56
|
+
}
|
|
57
|
+
if (checkpoint.state !== "paused" && checkpoint.state !== "consumed") {
|
|
58
|
+
throw new Error("checkpoint.state must be paused or consumed");
|
|
59
|
+
}
|
|
60
|
+
const createdAt = timestamp(checkpoint.createdAt, "checkpoint.createdAt");
|
|
61
|
+
const bindings = {
|
|
62
|
+
journeyId: text(checkpoint.journeyId, "checkpoint.journeyId"),
|
|
63
|
+
releaseContract: text(checkpoint.releaseContract, "checkpoint.releaseContract"),
|
|
64
|
+
target: target(checkpoint.target),
|
|
65
|
+
planSha256: sha256(checkpoint.planSha256, "checkpoint.planSha256"),
|
|
66
|
+
sourceRevision: revision(checkpoint.sourceRevision, "checkpoint.sourceRevision"),
|
|
67
|
+
mcpEndpointSha256: sha256(checkpoint.mcpEndpointSha256, "checkpoint.mcpEndpointSha256"),
|
|
68
|
+
candidateId: text(checkpoint.candidateId, "checkpoint.candidateId"),
|
|
69
|
+
releaseId: text(checkpoint.releaseId, "checkpoint.releaseId"),
|
|
70
|
+
...(checkpoint.provisionReceiptSha256 === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: {
|
|
73
|
+
provisionReceiptSha256: sha256(checkpoint.provisionReceiptSha256, "checkpoint.provisionReceiptSha256"),
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
assertTargetBinding(bindings);
|
|
77
|
+
const resume = parseResume(checkpoint.resume);
|
|
78
|
+
const consoleReceiptRequest = parseReceiptRequest(checkpoint.consoleReceiptRequest);
|
|
79
|
+
const integrity = object(checkpoint.integrity, "checkpoint.integrity");
|
|
80
|
+
exactKeys(integrity, ["algorithm", "digest"], "checkpoint.integrity");
|
|
81
|
+
if (integrity.algorithm !== "sha256")
|
|
82
|
+
throw new Error("checkpoint.integrity.algorithm must be sha256");
|
|
83
|
+
const digest = sha256(integrity.digest, "checkpoint.integrity.digest");
|
|
84
|
+
const state = checkpoint.state;
|
|
85
|
+
const consumedAt = checkpoint.consumedAt;
|
|
86
|
+
const finalReceiptSha256 = checkpoint.finalReceiptSha256;
|
|
87
|
+
if (state === "paused" && (consumedAt !== undefined || finalReceiptSha256 !== undefined)) {
|
|
88
|
+
throw new Error("paused checkpoint cannot carry consumed receipt fields");
|
|
89
|
+
}
|
|
90
|
+
if (state === "consumed" && (consumedAt === undefined || finalReceiptSha256 === undefined)) {
|
|
91
|
+
throw new Error("consumed checkpoint must carry consumedAt and finalReceiptSha256");
|
|
92
|
+
}
|
|
93
|
+
const payload = {
|
|
94
|
+
schemaVersion: ZERO_TO_MAP_CHECKPOINT_SCHEMA,
|
|
95
|
+
state,
|
|
96
|
+
createdAt,
|
|
97
|
+
...bindings,
|
|
98
|
+
resume,
|
|
99
|
+
consoleReceiptRequest,
|
|
100
|
+
...(state === "consumed"
|
|
101
|
+
? {
|
|
102
|
+
consumedAt: timestamp(consumedAt, "checkpoint.consumedAt"),
|
|
103
|
+
finalReceiptSha256: sha256(finalReceiptSha256, "checkpoint.finalReceiptSha256"),
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
107
|
+
assertSecretFree(payload);
|
|
108
|
+
const expected = digestPayload(payload);
|
|
109
|
+
if (digest !== expected)
|
|
110
|
+
throw new Error("checkpoint integrity digest does not match its canonical payload");
|
|
111
|
+
return { ...payload, integrity: { algorithm: "sha256", digest } };
|
|
112
|
+
}
|
|
113
|
+
export function assertZeroToMapCheckpointFresh(checkpoint, now = new Date()) {
|
|
114
|
+
const age = now.getTime() - Date.parse(checkpoint.createdAt);
|
|
115
|
+
if (age < 0 || age > ZERO_TO_MAP_CHECKPOINT_MAX_AGE_MS) {
|
|
116
|
+
throw new Error("checkpoint is stale or has a future creation time");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function assertZeroToMapCheckpointDigest(checkpoint, expectedDigest) {
|
|
120
|
+
if (!/^[0-9a-f]{64}$/.test(expectedDigest) || checkpoint.integrity.digest !== expectedDigest) {
|
|
121
|
+
throw new Error("externally carried checkpoint digest does not match");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function assertZeroToMapCheckpointBindings(checkpoint, expected) {
|
|
125
|
+
for (const key of Object.keys(expected)) {
|
|
126
|
+
if (checkpoint[key] !== expected[key])
|
|
127
|
+
throw new Error(`checkpoint binding mismatch: ${key}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function assertTargetBinding(bindings) {
|
|
131
|
+
if (bindings.target === "aws-ecs" && bindings.provisionReceiptSha256 === undefined) {
|
|
132
|
+
throw new Error("AWS ECS checkpoint must bind provisionReceiptSha256");
|
|
133
|
+
}
|
|
134
|
+
if (bindings.target === "local-docker" && bindings.provisionReceiptSha256 !== undefined) {
|
|
135
|
+
throw new Error("local Docker checkpoint cannot bind an AWS provision receipt");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function seal(payload) {
|
|
139
|
+
return { ...payload, integrity: { algorithm: "sha256", digest: digestPayload(payload) } };
|
|
140
|
+
}
|
|
141
|
+
function digestPayload(payload) {
|
|
142
|
+
return createHash("sha256").update(canonicalJson(payload)).digest("hex");
|
|
143
|
+
}
|
|
144
|
+
/** Checkpoints are an allowlisted release handoff, never a credential transport. */
|
|
145
|
+
function assertSecretFree(value, path = "checkpoint") {
|
|
146
|
+
if (Array.isArray(value)) {
|
|
147
|
+
value.forEach((item, index) => assertSecretFree(item, `${path}[${index}]`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!value || typeof value !== "object")
|
|
151
|
+
return;
|
|
152
|
+
for (const [key, item] of Object.entries(value)) {
|
|
153
|
+
if (/(?:password|authorization|api[-_]?key|access[-_]?key|secret(?:[-_]?key)?|bearer|token|material)/i.test(key)) {
|
|
154
|
+
throw new Error(`checkpoint contains a forbidden credential field at ${path}.${key}`);
|
|
155
|
+
}
|
|
156
|
+
assertSecretFree(item, `${path}.${key}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function canonicalJson(value) {
|
|
160
|
+
if (value === null)
|
|
161
|
+
return "null";
|
|
162
|
+
if (Array.isArray(value))
|
|
163
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
164
|
+
if (typeof value === "object") {
|
|
165
|
+
const candidate = value;
|
|
166
|
+
return `{${Object.keys(candidate)
|
|
167
|
+
.filter((key) => candidate[key] !== undefined)
|
|
168
|
+
.sort()
|
|
169
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(candidate[key])}`)
|
|
170
|
+
.join(",")}}`;
|
|
171
|
+
}
|
|
172
|
+
const encoded = JSON.stringify(value);
|
|
173
|
+
if (encoded === undefined)
|
|
174
|
+
throw new Error("checkpoint contains a non-JSON value");
|
|
175
|
+
return encoded;
|
|
176
|
+
}
|
|
177
|
+
function parseResume(value) {
|
|
178
|
+
const resume = object(value, "checkpoint.resume");
|
|
179
|
+
exactKeys(resume, ["startedAt", "capturedVariables", "completedStages", "resumeAt", "catalog"], "checkpoint.resume");
|
|
180
|
+
const capturedVariables = object(resume.capturedVariables, "checkpoint.resume.capturedVariables");
|
|
181
|
+
if (!Array.isArray(resume.completedStages))
|
|
182
|
+
throw new Error("checkpoint.resume.completedStages must be an array");
|
|
183
|
+
return {
|
|
184
|
+
startedAt: timestamp(resume.startedAt, "checkpoint.resume.startedAt"),
|
|
185
|
+
capturedVariables,
|
|
186
|
+
completedStages: resume.completedStages.map(parseStageReceipt),
|
|
187
|
+
resumeAt: parseResumePoint(resume.resumeAt),
|
|
188
|
+
...(resume.catalog === undefined ? {} : { catalog: parseCatalogReceipt(resume.catalog) }),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Catalog preflight evidence restored from the checkpoint. Validated as
|
|
193
|
+
* strictly as the rest of the payload: it is re-emitted onto the final release
|
|
194
|
+
* receipt, so a malformed or padded catalog block must not survive the parse.
|
|
195
|
+
*/
|
|
196
|
+
function parseCatalogReceipt(value) {
|
|
197
|
+
const path = "checkpoint.resume.catalog";
|
|
198
|
+
const catalog = object(value, path);
|
|
199
|
+
exactKeys(catalog, [
|
|
200
|
+
"schemaVersion",
|
|
201
|
+
"activeProfiles",
|
|
202
|
+
"expectedTotalTools",
|
|
203
|
+
"advertisedTotalTools",
|
|
204
|
+
"baseStaticTools",
|
|
205
|
+
"baseAdminTools",
|
|
206
|
+
"auditedExclusions",
|
|
207
|
+
"profiles",
|
|
208
|
+
"catalogSha256",
|
|
209
|
+
"adminRosterSha256",
|
|
210
|
+
"staticRosterSha256",
|
|
211
|
+
"exclusionRosterSha256",
|
|
212
|
+
], path);
|
|
213
|
+
if (catalog.schemaVersion !== ZERO_TO_MAP_CATALOG_RECEIPT_SCHEMA) {
|
|
214
|
+
throw new Error(`${path}.schemaVersion must be ${ZERO_TO_MAP_CATALOG_RECEIPT_SCHEMA}`);
|
|
215
|
+
}
|
|
216
|
+
if (!Array.isArray(catalog.profiles))
|
|
217
|
+
throw new Error(`${path}.profiles must be an array`);
|
|
218
|
+
return {
|
|
219
|
+
schemaVersion: ZERO_TO_MAP_CATALOG_RECEIPT_SCHEMA,
|
|
220
|
+
activeProfiles: stringList(catalog.activeProfiles, `${path}.activeProfiles`),
|
|
221
|
+
expectedTotalTools: count(catalog.expectedTotalTools, `${path}.expectedTotalTools`),
|
|
222
|
+
advertisedTotalTools: count(catalog.advertisedTotalTools, `${path}.advertisedTotalTools`),
|
|
223
|
+
baseStaticTools: count(catalog.baseStaticTools, `${path}.baseStaticTools`),
|
|
224
|
+
baseAdminTools: count(catalog.baseAdminTools, `${path}.baseAdminTools`),
|
|
225
|
+
auditedExclusions: count(catalog.auditedExclusions, `${path}.auditedExclusions`),
|
|
226
|
+
profiles: catalog.profiles.map((profile, index) => parseProfileReceipt(profile, `${path}.profiles[${index}]`)),
|
|
227
|
+
catalogSha256: sha256(catalog.catalogSha256, `${path}.catalogSha256`),
|
|
228
|
+
adminRosterSha256: sha256(catalog.adminRosterSha256, `${path}.adminRosterSha256`),
|
|
229
|
+
staticRosterSha256: sha256(catalog.staticRosterSha256, `${path}.staticRosterSha256`),
|
|
230
|
+
exclusionRosterSha256: sha256(catalog.exclusionRosterSha256, `${path}.exclusionRosterSha256`),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function parseProfileReceipt(value, path) {
|
|
234
|
+
const profile = object(value, path);
|
|
235
|
+
exactKeys(profile, ["id", "expectedMembers", "advertisedMembers", "confirmedMembers", "nameResolvedMembers", "rosterSha256"], path);
|
|
236
|
+
return {
|
|
237
|
+
id: text(profile.id, `${path}.id`),
|
|
238
|
+
expectedMembers: count(profile.expectedMembers, `${path}.expectedMembers`),
|
|
239
|
+
advertisedMembers: count(profile.advertisedMembers, `${path}.advertisedMembers`),
|
|
240
|
+
confirmedMembers: stringList(profile.confirmedMembers, `${path}.confirmedMembers`),
|
|
241
|
+
nameResolvedMembers: stringList(profile.nameResolvedMembers, `${path}.nameResolvedMembers`),
|
|
242
|
+
rosterSha256: sha256(profile.rosterSha256, `${path}.rosterSha256`),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function parseResumePoint(value) {
|
|
246
|
+
const point = object(value, "checkpoint.resume.resumeAt");
|
|
247
|
+
exactKeys(point, ["stageId", "actionId"], "checkpoint.resume.resumeAt");
|
|
248
|
+
return {
|
|
249
|
+
stageId: text(point.stageId, "checkpoint.resume.resumeAt.stageId"),
|
|
250
|
+
actionId: text(point.actionId, "checkpoint.resume.resumeAt.actionId"),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function parseReceiptRequest(value) {
|
|
254
|
+
const request = object(value, "checkpoint.consoleReceiptRequest");
|
|
255
|
+
exactKeys(request, ["schemaVersion", "actionId", "receiptSchema", "matches", "requiredPointers", "equalPointers"], "checkpoint.consoleReceiptRequest");
|
|
256
|
+
if (request.schemaVersion !== ZERO_TO_MAP_CONSOLE_RECEIPT_REQUEST_SCHEMA) {
|
|
257
|
+
throw new Error(`checkpoint.consoleReceiptRequest.schemaVersion must be ${ZERO_TO_MAP_CONSOLE_RECEIPT_REQUEST_SCHEMA}`);
|
|
258
|
+
}
|
|
259
|
+
const matches = object(request.matches, "checkpoint.consoleReceiptRequest.matches");
|
|
260
|
+
const requiredPointers = stringList(request.requiredPointers, "checkpoint.consoleReceiptRequest.requiredPointers");
|
|
261
|
+
if (!Array.isArray(request.equalPointers)) {
|
|
262
|
+
throw new Error("checkpoint.consoleReceiptRequest.equalPointers must be an array");
|
|
263
|
+
}
|
|
264
|
+
const equalPointers = request.equalPointers.map((value, index) => {
|
|
265
|
+
if (!Array.isArray(value) || value.length !== 2 || value.some((item) => typeof item !== "string" || !item)) {
|
|
266
|
+
throw new Error(`checkpoint.consoleReceiptRequest.equalPointers[${index}] must be a pointer pair`);
|
|
267
|
+
}
|
|
268
|
+
return [value[0], value[1]];
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
schemaVersion: ZERO_TO_MAP_CONSOLE_RECEIPT_REQUEST_SCHEMA,
|
|
272
|
+
actionId: text(request.actionId, "checkpoint.consoleReceiptRequest.actionId"),
|
|
273
|
+
receiptSchema: text(request.receiptSchema, "checkpoint.consoleReceiptRequest.receiptSchema"),
|
|
274
|
+
matches,
|
|
275
|
+
requiredPointers,
|
|
276
|
+
equalPointers,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function parseStageReceipt(value, index) {
|
|
280
|
+
const path = `checkpoint.resume.completedStages[${index}]`;
|
|
281
|
+
const stage = object(value, path);
|
|
282
|
+
exactKeys(stage, ["number", "id", "title", "status", "actions"], path);
|
|
283
|
+
if (!Number.isInteger(stage.number) || stage.number < 1)
|
|
284
|
+
throw new Error(`${path}.number must be positive`);
|
|
285
|
+
if (!Array.isArray(stage.actions))
|
|
286
|
+
throw new Error(`${path}.actions must be an array`);
|
|
287
|
+
return {
|
|
288
|
+
number: stage.number,
|
|
289
|
+
id: text(stage.id, `${path}.id`),
|
|
290
|
+
title: text(stage.title, `${path}.title`),
|
|
291
|
+
status: actionStatus(stage.status, `${path}.status`),
|
|
292
|
+
actions: stage.actions.map((action, actionIndex) => parseActionReceipt(action, `${path}.actions[${actionIndex}]`)),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function parseActionReceipt(value, path) {
|
|
296
|
+
const action = object(value, path);
|
|
297
|
+
exactKeys(action, ["id", "kind", "status", "startedAt", "finishedAt", "code", "message", "evidence", "captures"], path);
|
|
298
|
+
const kind = action.kind;
|
|
299
|
+
if (!["cli", "mcp", "mcp-resource", "gpserver", "receipt", "http"].includes(String(kind))) {
|
|
300
|
+
throw new Error(`${path}.kind is invalid`);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
id: text(action.id, `${path}.id`),
|
|
304
|
+
kind: kind,
|
|
305
|
+
status: actionStatus(action.status, `${path}.status`),
|
|
306
|
+
startedAt: timestamp(action.startedAt, `${path}.startedAt`),
|
|
307
|
+
finishedAt: timestamp(action.finishedAt, `${path}.finishedAt`),
|
|
308
|
+
...(action.code === undefined ? {} : { code: text(action.code, `${path}.code`) }),
|
|
309
|
+
...(action.message === undefined ? {} : { message: text(action.message, `${path}.message`) }),
|
|
310
|
+
...(action.evidence === undefined ? {} : { evidence: object(action.evidence, `${path}.evidence`) }),
|
|
311
|
+
...(action.captures === undefined ? {} : { captures: object(action.captures, `${path}.captures`) }),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function actionStatus(value, path) {
|
|
315
|
+
if (value !== "passed" && value !== "blocked" && value !== "failed" && value !== "skipped") {
|
|
316
|
+
throw new Error(`${path} is invalid`);
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
function object(value, path) {
|
|
321
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
322
|
+
throw new Error(`${path} must be an object`);
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
function exactKeys(value, allowed, path) {
|
|
326
|
+
const extras = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
327
|
+
if (extras.length > 0)
|
|
328
|
+
throw new Error(`${path} has unknown fields: ${extras.join(", ")}`);
|
|
329
|
+
}
|
|
330
|
+
function text(value, path) {
|
|
331
|
+
if (typeof value !== "string" || !value)
|
|
332
|
+
throw new Error(`${path} must be a non-empty string`);
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
function timestamp(value, path) {
|
|
336
|
+
const result = text(value, path);
|
|
337
|
+
if (!Number.isFinite(Date.parse(result)))
|
|
338
|
+
throw new Error(`${path} must be an ISO timestamp`);
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
function sha256(value, path) {
|
|
342
|
+
const result = text(value, path);
|
|
343
|
+
if (!/^[0-9a-f]{64}$/.test(result))
|
|
344
|
+
throw new Error(`${path} must be a lowercase SHA-256 digest`);
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
function revision(value, path) {
|
|
348
|
+
const result = text(value, path);
|
|
349
|
+
if (!/^[0-9a-f]{40}$/.test(result))
|
|
350
|
+
throw new Error(`${path} must be a full lowercase git SHA`);
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
function target(value) {
|
|
354
|
+
if (value !== "local-docker" && value !== "aws-ecs") {
|
|
355
|
+
throw new Error("checkpoint.target must be local-docker or aws-ecs");
|
|
356
|
+
}
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
function count(value, path) {
|
|
360
|
+
if (!Number.isInteger(value) || value < 0)
|
|
361
|
+
throw new Error(`${path} must be a non-negative integer`);
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
function stringList(value, path) {
|
|
365
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) {
|
|
366
|
+
throw new Error(`${path} must be an array of non-empty strings`);
|
|
367
|
+
}
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type AwsEcsProvisionBinding } from "./zero-to-map-provision.js";
|
|
3
|
+
import { type JourneyExecutionResult, type JourneyMcpResourceAction } from "./zero-to-map.js";
|
|
4
|
+
type JourneyTarget = "local-docker" | "aws-ecs";
|
|
5
|
+
export interface CliOptions {
|
|
6
|
+
readonly execute: boolean;
|
|
7
|
+
readonly confirmed: boolean;
|
|
8
|
+
readonly planPath: string;
|
|
9
|
+
readonly outputPath: string;
|
|
10
|
+
readonly honuaCommand: string;
|
|
11
|
+
readonly target: JourneyTarget;
|
|
12
|
+
readonly mcpUrl?: string;
|
|
13
|
+
readonly checkpointPath?: string;
|
|
14
|
+
readonly checkpointDigest?: string;
|
|
15
|
+
readonly provisionReceiptPath?: string;
|
|
16
|
+
readonly consoleReceiptPath?: string;
|
|
17
|
+
readonly provisionBinding?: AwsEcsProvisionBinding;
|
|
18
|
+
readonly provisionReceiptSha256?: string;
|
|
19
|
+
readonly sourceRevision?: string;
|
|
20
|
+
readonly variables: Readonly<Record<string, unknown>>;
|
|
21
|
+
readonly receiptPaths: Readonly<Record<string, string>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function runZeroToMapCli(argv?: readonly string[], env?: NodeJS.ProcessEnv): Promise<number>;
|
|
24
|
+
export declare function parseZeroToMapCliArgs(argv: readonly string[], env: NodeJS.ProcessEnv): CliOptions;
|
|
25
|
+
/** Resolve the live mutating CLI without permitting Windows' cwd-first executable search. */
|
|
26
|
+
export declare function resolveLiveExecutable(command: string, env: NodeJS.ProcessEnv, excludedDirectory: string): Promise<string>;
|
|
27
|
+
/** Atomically transfer one paused checkpoint to a single resume owner. */
|
|
28
|
+
export declare function claimZeroToMapCheckpoint(path: string, digest: string): Promise<string>;
|
|
29
|
+
export declare function parseInstallAccessCredential(stdout: string): {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly status: "active";
|
|
33
|
+
readonly requestedGrants: readonly string[];
|
|
34
|
+
readonly effectiveGrants: readonly string[];
|
|
35
|
+
readonly canAuthenticate: true;
|
|
36
|
+
readonly referenceType: "private-env-file";
|
|
37
|
+
readonly referenceDigestSha256: string;
|
|
38
|
+
readonly provisioned: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** Poll one MCP job resource to its declared successful terminal state. */
|
|
41
|
+
export declare function readJourneyMcpResource(action: JourneyMcpResourceAction, read: (uri: string) => Promise<unknown>): Promise<JourneyExecutionResult>;
|
|
42
|
+
export declare function requireCompletedPublishedOperation(tool: string, value: unknown): {
|
|
43
|
+
operationId: string;
|
|
44
|
+
status: string;
|
|
45
|
+
handleId?: string;
|
|
46
|
+
approvalLane?: string;
|
|
47
|
+
jobId?: string;
|
|
48
|
+
metadataRevision?: number;
|
|
49
|
+
} | undefined;
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=zero-to-map-cli.d.ts.map
|