Skip to content

Commit 8a89440

Browse files
Include vendored traverse-embedder-web dist for CI installs
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent adb8177 commit 8a89440

19 files changed

Lines changed: 1568 additions & 0 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Dependencies
22
node_modules/
33
dist/
4+
!vendor/**/dist/
5+
!vendor/**/dist/**
46

57
# Test coverage
68
coverage/
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { BundleLoader } from "./bundleLoader.js";
2+
import type { CompatibleLifecycleOutcome, CompatibleStartOutcome, EventCallback, JsonValue, ShutdownOutcome, SubmitOutcome, TraverseEmbedderApi } from "./types.js";
3+
/** Configuration for `BundleEmbedder.init` (`runtime.init` input). */
4+
export interface BundleEmbedderConfig {
5+
/** Path or URL to the application bundle's `app.manifest.json`. */
6+
readonly manifestPath: string;
7+
/** Loader used to fetch bundle files (browser: `FetchBundleLoader`). */
8+
readonly loader: BundleLoader;
9+
/** Workspace identity recorded on events. Defaults to `local-default`. */
10+
readonly workspaceId?: string;
11+
/** Platform identity checked against compatible-capability allowlists. */
12+
readonly platform?: string;
13+
}
14+
export declare class BundleEmbedder implements TraverseEmbedderApi {
15+
private readonly core;
16+
private readonly wasmTargets;
17+
private readonly workflowTargets;
18+
private readonly wasmComponentEvidence;
19+
private constructor();
20+
/**
21+
* `runtime.init`: load, digest-verify, host-ABI-validate, and compile the
22+
* application bundle. Rejects deterministically with a `BundleRejectedError`
23+
* and never falls back to a sidecar (spec 068 NFR-001).
24+
*/
25+
static init(config: BundleEmbedderConfig): Promise<BundleEmbedder>;
26+
submit(targetId: string, input: JsonValue): SubmitOutcome;
27+
private submitCapability;
28+
private submitWorkflow;
29+
subscribe(callback: EventCallback): void;
30+
startCompatible(capabilityId: string, input: JsonValue): CompatibleStartOutcome;
31+
stopCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
32+
killCompatible(capabilityId: string, instanceId?: string | null): CompatibleLifecycleOutcome;
33+
shutdown(): ShutdownOutcome;
34+
releaseEvidence(): JsonValue;
35+
}

vendor/traverse-embedder-web/dist/bundleEmbedder.js

Lines changed: 499 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Loads application bundle files (manifests, contracts, WASM artifacts) by
3+
* relative path. `BundleEmbedder` depends only on this interface, so a
4+
* browser host supplies `FetchBundleLoader` (production) and a Node host
5+
* (tests, Electron main-process embedding) supplies `NodeFsBundleLoader` —
6+
* neither is baked into the public API surface (spec 068: no `traverse-cli
7+
* serve` dependency in the production path).
8+
*/
9+
export interface BundleLoader {
10+
/** Resolves `relativePath` against the directory containing `basePath`. */
11+
resolve(basePath: string, relativePath: string): string;
12+
/** Loads a UTF-8 text file (manifests, contracts, workflow definitions). */
13+
loadText(path: string): Promise<string>;
14+
/** Loads raw bytes (WASM artifacts). */
15+
loadBytes(path: string): Promise<Uint8Array>;
16+
}
17+
/**
18+
* Browser bundle loader: resolves paths as URLs relative to the manifest
19+
* URL and loads them via `fetch`. This is the production loader — no
20+
* `traverse-cli serve` sidecar is involved.
21+
*/
22+
export declare class FetchBundleLoader implements BundleLoader {
23+
resolve(basePath: string, relativePath: string): string;
24+
loadText(path: string): Promise<string>;
25+
loadBytes(path: string): Promise<Uint8Array>;
26+
}
27+
/**
28+
* Node.js filesystem bundle loader, for tests and non-browser hosts
29+
* (Electron main process, server-side prerendering). Uses dynamic `import`
30+
* so `node:fs` is never a static dependency of the browser entry point.
31+
*/
32+
export declare class NodeFsBundleLoader implements BundleLoader {
33+
resolve(basePath: string, relativePath: string): string;
34+
loadText(path: string): Promise<string>;
35+
loadBytes(path: string): Promise<Uint8Array>;
36+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Loads application bundle files (manifests, contracts, WASM artifacts) by
3+
* relative path. `BundleEmbedder` depends only on this interface, so a
4+
* browser host supplies `FetchBundleLoader` (production) and a Node host
5+
* (tests, Electron main-process embedding) supplies `NodeFsBundleLoader` —
6+
* neither is baked into the public API surface (spec 068: no `traverse-cli
7+
* serve` dependency in the production path).
8+
*/
9+
function dirname(path) {
10+
const index = path.lastIndexOf("/");
11+
return index === -1 ? "" : path.slice(0, index);
12+
}
13+
function joinPosix(base, relative) {
14+
if (relative.startsWith("/") || /^[a-zA-Z]+:/.test(relative)) {
15+
return relative;
16+
}
17+
const segments = `${base}/${relative}`.split("/");
18+
const resolved = [];
19+
for (const segment of segments) {
20+
if (segment === "" || segment === ".") {
21+
continue;
22+
}
23+
if (segment === "..") {
24+
resolved.pop();
25+
continue;
26+
}
27+
resolved.push(segment);
28+
}
29+
return (base.startsWith("/") ? "/" : "") + resolved.join("/");
30+
}
31+
/**
32+
* Browser bundle loader: resolves paths as URLs relative to the manifest
33+
* URL and loads them via `fetch`. This is the production loader — no
34+
* `traverse-cli serve` sidecar is involved.
35+
*/
36+
export class FetchBundleLoader {
37+
resolve(basePath, relativePath) {
38+
const documentBase = typeof document !== "undefined" ? document.baseURI : globalThis.location.href;
39+
return new URL(relativePath, new URL(basePath, documentBase)).toString();
40+
}
41+
async loadText(path) {
42+
const response = await fetch(path);
43+
if (!response.ok) {
44+
throw new Error(`failed to fetch '${path}': HTTP ${response.status}`);
45+
}
46+
return response.text();
47+
}
48+
async loadBytes(path) {
49+
const response = await fetch(path);
50+
if (!response.ok) {
51+
throw new Error(`failed to fetch '${path}': HTTP ${response.status}`);
52+
}
53+
return new Uint8Array(await response.arrayBuffer());
54+
}
55+
}
56+
/**
57+
* Node.js filesystem bundle loader, for tests and non-browser hosts
58+
* (Electron main process, server-side prerendering). Uses dynamic `import`
59+
* so `node:fs` is never a static dependency of the browser entry point.
60+
*/
61+
export class NodeFsBundleLoader {
62+
resolve(basePath, relativePath) {
63+
return joinPosix(dirname(basePath), relativePath);
64+
}
65+
async loadText(path) {
66+
const { readFile } = await import("node:fs/promises");
67+
return readFile(path, "utf8");
68+
}
69+
async loadBytes(path) {
70+
const { readFile } = await import("node:fs/promises");
71+
const buffer = await readFile(path);
72+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
73+
}
74+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { EmbedderError, JsonValue } from "./types.js";
2+
/** Thrown when a bundle is rejected at the embedder boundary. */
3+
export declare class BundleRejectedError extends Error {
4+
readonly embedderError: EmbedderError;
5+
constructor(error: EmbedderError);
6+
}
7+
/** One bundled component reference parsed from the app manifest. */
8+
export interface BundleComponentSummary {
9+
readonly componentId: string;
10+
readonly version: string;
11+
readonly digest: string;
12+
readonly manifestPath: string;
13+
}
14+
/** One bundled workflow reference parsed from the app manifest. */
15+
export interface BundleWorkflowSummary {
16+
readonly workflowId: string;
17+
readonly workflowVersion: string;
18+
readonly path: string;
19+
}
20+
/** Deterministic bundle compatibility summary (spec 068 NFR-001). */
21+
export interface BundleCompatibility {
22+
readonly appId: string;
23+
readonly appVersion: string;
24+
readonly schemaVersion: string;
25+
readonly components: readonly BundleComponentSummary[];
26+
readonly workflowIds: readonly string[];
27+
readonly workflows: readonly BundleWorkflowSummary[];
28+
}
29+
export declare function asRecord(value: JsonValue | undefined): {
30+
[key: string]: JsonValue;
31+
} | null;
32+
export declare function requiredString(record: {
33+
[key: string]: JsonValue;
34+
}, key: string, context: string): string;
35+
export declare function optionalString(record: {
36+
[key: string]: JsonValue;
37+
}, key: string): string | null;
38+
export declare const SHA256_DIGEST_PATTERN: RegExp;
39+
/**
40+
* Parses and deterministically validates an application bundle manifest
41+
* (spec `044-application-bundle-manifest`) for embedder compatibility:
42+
* schema version support, component identity, and sha-256 digest metadata.
43+
* Rejection never falls back to a sidecar (spec 068 NFR-001).
44+
*
45+
* @throws {BundleRejectedError} with a stable `EmbedderErrorCode`.
46+
*/
47+
export declare function validateBundleCompatibility(appManifest: string | JsonValue): BundleCompatibility;
48+
/**
49+
* Verifies bundled artifact bytes against declared sha-256 digest metadata
50+
* using WebCrypto (browser) or the Node.js webcrypto implementation.
51+
*
52+
* @throws {BundleRejectedError} with `bundle_load_failed` on mismatch.
53+
*/
54+
export declare function verifyArtifactDigest(bytes: Uint8Array, declaredDigest: string, artifactLabel: string): Promise<void>;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Deterministic application bundle manifest validation (spec
3+
* `044-application-bundle-manifest`) and artifact digest verification.
4+
* Rejection never falls back to a sidecar (spec 068 NFR-001).
5+
*/
6+
import { SUPPORTED_BUNDLE_SCHEMA_VERSIONS, embedderError } from "./types.js";
7+
/** Thrown when a bundle is rejected at the embedder boundary. */
8+
export class BundleRejectedError extends Error {
9+
embedderError;
10+
constructor(error) {
11+
super(`${error.code}: ${error.message}`);
12+
this.name = "BundleRejectedError";
13+
this.embedderError = error;
14+
}
15+
}
16+
export function asRecord(value) {
17+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
18+
return value;
19+
}
20+
return null;
21+
}
22+
export function requiredString(record, key, context) {
23+
const value = record[key];
24+
if (typeof value !== "string" || value.trim() === "") {
25+
throw new BundleRejectedError(embedderError("bundle_load_failed", `${context} requires a non-empty string '${key}'`));
26+
}
27+
return value;
28+
}
29+
export function optionalString(record, key) {
30+
const value = record[key];
31+
return typeof value === "string" ? value : null;
32+
}
33+
export const SHA256_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
34+
/**
35+
* Parses and deterministically validates an application bundle manifest
36+
* (spec `044-application-bundle-manifest`) for embedder compatibility:
37+
* schema version support, component identity, and sha-256 digest metadata.
38+
* Rejection never falls back to a sidecar (spec 068 NFR-001).
39+
*
40+
* @throws {BundleRejectedError} with a stable `EmbedderErrorCode`.
41+
*/
42+
export function validateBundleCompatibility(appManifest) {
43+
let parsed;
44+
if (typeof appManifest === "string") {
45+
try {
46+
parsed = JSON.parse(appManifest);
47+
}
48+
catch (error) {
49+
throw new BundleRejectedError(embedderError("bundle_load_failed", `application bundle manifest is not valid JSON: ${String(error)}`));
50+
}
51+
}
52+
else {
53+
parsed = appManifest;
54+
}
55+
const manifest = asRecord(parsed);
56+
if (manifest === null) {
57+
throw new BundleRejectedError(embedderError("bundle_load_failed", "application bundle manifest must be a JSON object"));
58+
}
59+
const appId = requiredString(manifest, "app_id", "application bundle manifest");
60+
const appVersion = requiredString(manifest, "version", "application bundle manifest");
61+
const schemaVersion = requiredString(manifest, "schema_version", "application bundle manifest");
62+
if (!SUPPORTED_BUNDLE_SCHEMA_VERSIONS.includes(schemaVersion)) {
63+
throw new BundleRejectedError(embedderError("unsupported_bundle_schema", `bundle declares schema_version '${schemaVersion}' but this package supports ` +
64+
`[${SUPPORTED_BUNDLE_SCHEMA_VERSIONS.join(", ")}]; no sidecar fallback is attempted`));
65+
}
66+
const componentsValue = manifest["components"];
67+
if (!Array.isArray(componentsValue)) {
68+
throw new BundleRejectedError(embedderError("bundle_load_failed", "application bundle manifest requires a 'components' array"));
69+
}
70+
const components = componentsValue.map((entry, index) => {
71+
const component = asRecord(entry);
72+
if (component === null) {
73+
throw new BundleRejectedError(embedderError("bundle_load_failed", `components[${index}] must be a JSON object`));
74+
}
75+
const context = `components[${index}]`;
76+
const digest = requiredString(component, "digest", context);
77+
if (!SHA256_DIGEST_PATTERN.test(digest)) {
78+
throw new BundleRejectedError(embedderError("bundle_load_failed", `${context} declares invalid digest metadata '${digest}'; ` +
79+
"expected sha256:<64 hex characters>"));
80+
}
81+
return {
82+
componentId: requiredString(component, "component_id", context),
83+
version: requiredString(component, "version", context),
84+
digest,
85+
manifestPath: requiredString(component, "manifest_path", context),
86+
};
87+
});
88+
const workflowsValue = manifest["workflows"];
89+
const workflowIds = [];
90+
const workflows = [];
91+
if (Array.isArray(workflowsValue)) {
92+
for (const [index, entry] of workflowsValue.entries()) {
93+
const workflow = asRecord(entry);
94+
if (workflow === null) {
95+
throw new BundleRejectedError(embedderError("bundle_load_failed", `workflows[${index}] must be a JSON object`));
96+
}
97+
const context = `workflows[${index}]`;
98+
const workflowId = requiredString(workflow, "workflow_id", context);
99+
workflowIds.push(workflowId);
100+
workflows.push({
101+
workflowId,
102+
workflowVersion: requiredString(workflow, "workflow_version", context),
103+
path: requiredString(workflow, "path", context),
104+
});
105+
}
106+
}
107+
return { appId, appVersion, schemaVersion, components, workflowIds, workflows };
108+
}
109+
/**
110+
* Verifies bundled artifact bytes against declared sha-256 digest metadata
111+
* using WebCrypto (browser) or the Node.js webcrypto implementation.
112+
*
113+
* @throws {BundleRejectedError} with `bundle_load_failed` on mismatch.
114+
*/
115+
export async function verifyArtifactDigest(bytes, declaredDigest, artifactLabel) {
116+
if (!SHA256_DIGEST_PATTERN.test(declaredDigest)) {
117+
throw new BundleRejectedError(embedderError("bundle_load_failed", `${artifactLabel} declares invalid digest metadata '${declaredDigest}'`));
118+
}
119+
const digestBytes = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
120+
const actual = [...new Uint8Array(digestBytes)]
121+
.map((byte) => byte.toString(16).padStart(2, "0"))
122+
.join("");
123+
const expected = declaredDigest.slice("sha256:".length);
124+
if (actual !== expected) {
125+
throw new BundleRejectedError(embedderError("bundle_load_failed", `${artifactLabel} digest mismatch: manifest declares sha256:${expected} ` +
126+
`but the bundled artifact hashes to sha256:${actual}; ` +
127+
"no sidecar fallback is attempted"));
128+
}
129+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { CompatibleLifecycleOutcome, CompatibleStartOutcome, EmbedderError, EmbedderEvent, EventCallback, JsonValue, SubmitOutcome } from "./types.js";
2+
export declare class EmbedderCore {
3+
readonly workspaceId: string;
4+
readonly appId: string;
5+
readonly appVersion: string;
6+
readonly platform: string;
7+
readonly compatibleTargets: Map<string, readonly string[]>;
8+
private readonly instances;
9+
private readonly subscribers;
10+
private readonly history;
11+
private nextEvent;
12+
private nextSession;
13+
private nextRequest;
14+
private nextInstance;
15+
stopped: boolean;
16+
constructor(workspaceId: string, appId: string, appVersion: string, platform: string, compatibleTargets: Map<string, readonly string[]>);
17+
nextSessionId(): string;
18+
nextRequestId(): string;
19+
private nextInstanceId;
20+
emit(eventType: EmbedderEvent["event_type"], sessionId: string | null, data: JsonValue): void;
21+
subscribe(callback: EventCallback): void;
22+
emitErrorEvent(sessionId: string | null, error: EmbedderError, data: {
23+
[key: string]: JsonValue;
24+
}): void;
25+
rejectedSubmit(targetId: string, error: EmbedderError): SubmitOutcome;
26+
startCompatible(capabilityId: string, input: JsonValue): CompatibleStartOutcome;
27+
transitionCompatible(capabilityId: string, instanceId: string | null, targetState: "stopped" | "killed"): CompatibleLifecycleOutcome;
28+
private setInstanceState;
29+
shutdown(): {
30+
killedInstances: number;
31+
};
32+
evidence(runtimeImplementation: string, wasmComponents: JsonValue): JsonValue;
33+
}

0 commit comments

Comments
 (0)