@vendoai/vendo 0.4.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/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- package/skills/vendo-setup/SKILL.md +128 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { VendoError, principalSchema } from "@vendoai/core";
|
|
2
|
+
import { BASE_PATH, environment, json, prefixRoute, route } from "./shared.js";
|
|
3
|
+
/** The doctor probe surface (CLI `vendo doctor` targets a running dev server):
|
|
4
|
+
the synthetic credential/actAs round-trip constants and tool descriptors,
|
|
5
|
+
and the /doctor wire routes. server.ts keeps only the deps.doctor
|
|
6
|
+
probe-executor wiring (the probes run through a real createActions). */
|
|
7
|
+
const DOCTOR_PRESENT_AUTHORIZATION = "Bearer vendo-doctor-present";
|
|
8
|
+
const DOCTOR_PRESENT_COOKIE = "vendo_doctor_present=1";
|
|
9
|
+
export const DOCTOR_ACT_AS_PRINCIPAL = { kind: "user", subject: "vendo_doctor_act_as" };
|
|
10
|
+
export const DOCTOR_ACT_AS_APP_ID = "app_vendo_doctor";
|
|
11
|
+
export const doctorPresentTool = {
|
|
12
|
+
name: "vendo_doctor_present",
|
|
13
|
+
description: "Vendo doctor present credential round-trip",
|
|
14
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
15
|
+
risk: "read",
|
|
16
|
+
binding: { kind: "route", method: "GET", path: `${BASE_PATH}/doctor/present/echo`, argsIn: "query" },
|
|
17
|
+
};
|
|
18
|
+
export const doctorActAsTool = {
|
|
19
|
+
name: "vendo_doctor_act_as",
|
|
20
|
+
description: "Vendo doctor actAs mint and verification round-trip",
|
|
21
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
22
|
+
risk: "read",
|
|
23
|
+
binding: { kind: "route", method: "GET", path: `${BASE_PATH}/doctor/act-as/echo`, argsIn: "query" },
|
|
24
|
+
};
|
|
25
|
+
function doctorProbeOk(outcome) {
|
|
26
|
+
if (outcome.status !== "ok" || typeof outcome.output !== "object" || outcome.output === null)
|
|
27
|
+
return false;
|
|
28
|
+
return "ok" in outcome.output && outcome.output.ok === true;
|
|
29
|
+
}
|
|
30
|
+
/** Doctor targets a running dev server. The prefix gate keeps its synthetic
|
|
31
|
+
mint/echo routes out of production entirely (and falls through in
|
|
32
|
+
development); the echo halves expose no credential material — booleans
|
|
33
|
+
only.
|
|
34
|
+
|
|
35
|
+
`/doctor/base-url` (09-vendo §2 install-dx wave 1.1) is listed BEFORE that
|
|
36
|
+
gate on purpose: unlike the synthetic credential round-trips, it reports a
|
|
37
|
+
static composition fact (is VENDO_BASE_URL set?) that reveals no secret
|
|
38
|
+
material, and it is exactly the environment — production — the gate
|
|
39
|
+
otherwise silences. Placing it first lets it win the route-table scan in
|
|
40
|
+
every environment, gate included. */
|
|
41
|
+
export const doctorRoutes = [
|
|
42
|
+
route("GET", "/doctor/base-url", async () => {
|
|
43
|
+
const missingInProduction = environment("NODE_ENV") === "production" && environment("VENDO_BASE_URL") === undefined;
|
|
44
|
+
if (missingInProduction) {
|
|
45
|
+
return json({
|
|
46
|
+
ok: false,
|
|
47
|
+
error: {
|
|
48
|
+
code: "base-url-not-set-in-production",
|
|
49
|
+
message: "VENDO_BASE_URL is not set in production. Present-mode host tool calls that need to forward the caller's credentials fail closed instead of running unauthenticated. Set VENDO_BASE_URL to this deployment's public origin and restart the server.",
|
|
50
|
+
},
|
|
51
|
+
}, 409);
|
|
52
|
+
}
|
|
53
|
+
return json({ ok: true });
|
|
54
|
+
}),
|
|
55
|
+
prefixRoute("*", "/doctor/", async () => {
|
|
56
|
+
if (environment("NODE_ENV") === "production") {
|
|
57
|
+
throw new VendoError("not-found", "unknown Vendo route");
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}),
|
|
61
|
+
// execution-v2 Lane D — dev-only machine/schedule reporting (sits AFTER the
|
|
62
|
+
// production gate above, like every probe route). Reporting only: which apps
|
|
63
|
+
// carry a machine, whether a schedule caller (VENDO_TICK_SECRET) is
|
|
64
|
+
// configured for the /tick surface, and each schedule's last-fired state.
|
|
65
|
+
route("GET", "/doctor/machines", async ({ deps }) => {
|
|
66
|
+
return json({
|
|
67
|
+
scheduleCallerConfigured: environment("VENDO_TICK_SECRET") !== undefined,
|
|
68
|
+
machines: await deps.apps.schedules.report(),
|
|
69
|
+
});
|
|
70
|
+
}),
|
|
71
|
+
route("GET", "/doctor/present/echo", async ({ request }) => {
|
|
72
|
+
return json({
|
|
73
|
+
ok: request.headers.get("authorization") === DOCTOR_PRESENT_AUTHORIZATION
|
|
74
|
+
&& request.headers.get("cookie") === DOCTOR_PRESENT_COOKIE,
|
|
75
|
+
});
|
|
76
|
+
}),
|
|
77
|
+
route("GET", "/doctor/act-as/echo", async ({ request, deps }) => {
|
|
78
|
+
const resolved = await deps.principal(request);
|
|
79
|
+
const parsed = principalSchema.safeParse(resolved);
|
|
80
|
+
const accepted = parsed.success && parsed.data.subject === DOCTOR_ACT_AS_PRINCIPAL.subject;
|
|
81
|
+
return json({ ok: accepted }, accepted ? 200 : 401);
|
|
82
|
+
}),
|
|
83
|
+
route("POST", "/doctor/present", async ({ deps, context }) => {
|
|
84
|
+
const outcome = await deps.doctor.present(await context("chat"));
|
|
85
|
+
if (doctorProbeOk(outcome))
|
|
86
|
+
return json({ ok: true });
|
|
87
|
+
return json({
|
|
88
|
+
ok: false,
|
|
89
|
+
error: {
|
|
90
|
+
code: "present-credentials-not-forwarded",
|
|
91
|
+
message: "Present credentials did not reach the host API. Set VENDO_BASE_URL to the running host origin and restart the dev server.",
|
|
92
|
+
},
|
|
93
|
+
}, 409);
|
|
94
|
+
}),
|
|
95
|
+
route("POST", "/doctor/act-as", async ({ deps }) => {
|
|
96
|
+
const outcome = await deps.doctor.actAs();
|
|
97
|
+
if (doctorProbeOk(outcome))
|
|
98
|
+
return json({ ok: true });
|
|
99
|
+
if (outcome.status === "error" && outcome.error.code === "not-implemented") {
|
|
100
|
+
return json({
|
|
101
|
+
ok: false,
|
|
102
|
+
error: {
|
|
103
|
+
code: "act-as-not-configured",
|
|
104
|
+
message: "actAs is not configured; pass createVendo({ actAs }) before enabling away host actions.",
|
|
105
|
+
},
|
|
106
|
+
}, 501);
|
|
107
|
+
}
|
|
108
|
+
return json({
|
|
109
|
+
ok: false,
|
|
110
|
+
error: {
|
|
111
|
+
code: "act-as-verification-failed",
|
|
112
|
+
message: "actAs returned no usable AuthMaterial, or the host API did not accept it. Check the matching verifier middleware and principal resolver.",
|
|
113
|
+
},
|
|
114
|
+
}, 409);
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type RouteEntry } from "./shared.js";
|
|
2
|
+
/** The development-only injection seams. Each handler guards on its composed
|
|
3
|
+
dependency and falls through otherwise: production handlers receive no
|
|
4
|
+
runtimeCapture dependency / no development flag, so these answer the
|
|
5
|
+
ordinary 404 — there is no guarded-but-mounted production endpoint. */
|
|
6
|
+
export declare const devRoutes: RouteEntry[];
|
|
7
|
+
/** The machine-facing surfaces: webhook ingress, the authenticated scheduler
|
|
8
|
+
tick, and the dev-only sync impact probe. All match on the RAW path
|
|
9
|
+
(prefix or exact) ahead of any segment decoding, exactly like the old
|
|
10
|
+
chain. (The v1 run-token apps proxy mount died with execution-v2 Wave 1.5;
|
|
11
|
+
the box callback surface at /box/ is its replacement.) */
|
|
12
|
+
export declare const systemRoutes: RouteEntry[];
|
|
13
|
+
/** The decoded first segment matches only /orgs and /orgs/* (any depth, any
|
|
14
|
+
method), never a lookalike like /organizations; the rest wildcard also
|
|
15
|
+
covers a trailing-slash `/orgs/`. */
|
|
16
|
+
export declare const orgsRoutes: RouteEntry[];
|
|
17
|
+
export declare const activityRoutes: RouteEntry[];
|
|
18
|
+
export declare const statusRoutes: RouteEntry[];
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { VendoError, inferToolSemantics } from "@vendoai/core";
|
|
2
|
+
import { computeImpact } from "../sync-impact.js";
|
|
3
|
+
import { VERSION, environment, hex, json, orgsCloudRequired, prefixRoute, requestJson, route, string, } from "./shared.js";
|
|
4
|
+
/** Lazily-minted random per-process HMAC key for constant-time secret compares
|
|
5
|
+
(WebCrypto only — NO node:crypto — so the module keeps bundling for edge/
|
|
6
|
+
Worker targets; cf. dotVendoFile). */
|
|
7
|
+
let compareKeyPromise;
|
|
8
|
+
function compareKey() {
|
|
9
|
+
compareKeyPromise ??= (() => {
|
|
10
|
+
const raw = new Uint8Array(32);
|
|
11
|
+
globalThis.crypto.getRandomValues(raw);
|
|
12
|
+
return globalThis.crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
13
|
+
})();
|
|
14
|
+
return compareKeyPromise;
|
|
15
|
+
}
|
|
16
|
+
/** Length-independent-leak-free digest compare for timingSafeEqual's HMAC
|
|
17
|
+
digests (always equal-length hex; unequal lengths simply fail). */
|
|
18
|
+
function constantTimeEqual(a, b) {
|
|
19
|
+
if (a.length !== b.length)
|
|
20
|
+
return false;
|
|
21
|
+
let diff = 0;
|
|
22
|
+
for (let i = 0; i < a.length; i++)
|
|
23
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
24
|
+
return diff === 0;
|
|
25
|
+
}
|
|
26
|
+
/** Constant-time string equality via WebCrypto, matching the webhook HMAC path
|
|
27
|
+
(which leans on crypto.subtle.verify for the same guarantee). HMACs both
|
|
28
|
+
inputs under a random per-process key so the digests are equal-length 32-byte
|
|
29
|
+
values regardless of input length — equal digests iff equal inputs (SHA-256
|
|
30
|
+
collision resistance) — and the byte compare leaks neither length nor content
|
|
31
|
+
through timing. Replaces the `===` bearer compare, a classic timing oracle. */
|
|
32
|
+
async function timingSafeEqual(a, b) {
|
|
33
|
+
const key = await compareKey();
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const [da, db] = await Promise.all([
|
|
36
|
+
globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(a)),
|
|
37
|
+
globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(b)),
|
|
38
|
+
]);
|
|
39
|
+
return constantTimeEqual(hex(da), hex(db));
|
|
40
|
+
}
|
|
41
|
+
async function tickAuthorized(request) {
|
|
42
|
+
const secret = environment("VENDO_TICK_SECRET");
|
|
43
|
+
if (secret === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
return timingSafeEqual(request.headers.get("authorization") ?? "", `Bearer ${secret}`);
|
|
46
|
+
}
|
|
47
|
+
/** The development-only injection seams. Each handler guards on its composed
|
|
48
|
+
dependency and falls through otherwise: production handlers receive no
|
|
49
|
+
runtimeCapture dependency / no development flag, so these answer the
|
|
50
|
+
ordinary 404 — there is no guarded-but-mounted production endpoint. */
|
|
51
|
+
export const devRoutes = [
|
|
52
|
+
route("POST", "/dev/remixable-source", async ({ request, deps, context }) => {
|
|
53
|
+
if (deps.runtimeCapture === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
const body = await requestJson(request);
|
|
56
|
+
// Capture writes .vendo/remixable baselines on the developer's disk, so
|
|
57
|
+
// it requires a HOST-resolved principal — an anonymous visitor's minted
|
|
58
|
+
// ephemeral session is not enough, even in a development composition.
|
|
59
|
+
const captureContext = await context("app");
|
|
60
|
+
if (captureContext.principal.ephemeral === true) {
|
|
61
|
+
return json({ error: { code: "blocked", message: "runtime capture requires a host-resolved principal" } }, 401);
|
|
62
|
+
}
|
|
63
|
+
if (typeof body["exportable"] !== "boolean") {
|
|
64
|
+
throw new VendoError("validation", "exportable must be a boolean");
|
|
65
|
+
}
|
|
66
|
+
return json(await deps.runtimeCapture.capture({
|
|
67
|
+
slot: string(body["slot"], "slot"),
|
|
68
|
+
source: string(body["source"], "source"),
|
|
69
|
+
exportable: body["exportable"],
|
|
70
|
+
}));
|
|
71
|
+
}),
|
|
72
|
+
// 06-apps §9 — the documented LOCAL injection seam for in-client approval
|
|
73
|
+
// records (demos and dev; Cloud's review console mints these in
|
|
74
|
+
// production). Development compositions only: production handlers fall
|
|
75
|
+
// through to the ordinary 404, exactly like /dev/remixable-source, so no
|
|
76
|
+
// production surface can self-approve an app into the host page.
|
|
77
|
+
route("POST", "/dev/inclient-approval", async ({ request, deps, context }) => {
|
|
78
|
+
if (!deps.development)
|
|
79
|
+
return undefined;
|
|
80
|
+
const body = await requestJson(request);
|
|
81
|
+
// Approving a host-page mount is a HOST trust decision — an anonymous
|
|
82
|
+
// visitor's minted ephemeral session is not enough, even in dev.
|
|
83
|
+
const approvalContext = await context("app");
|
|
84
|
+
if (approvalContext.principal.ephemeral === true) {
|
|
85
|
+
return json({ error: { code: "blocked", message: "in-client approval injection requires a host-resolved principal" } }, 401);
|
|
86
|
+
}
|
|
87
|
+
const approvedBy = body["approvedBy"] === undefined
|
|
88
|
+
? "local-dev"
|
|
89
|
+
: string(body["approvedBy"], "approvedBy");
|
|
90
|
+
return json(await deps.apps.inClient.approve({
|
|
91
|
+
appId: string(body["appId"], "appId"),
|
|
92
|
+
approvedBy,
|
|
93
|
+
}, approvalContext));
|
|
94
|
+
}),
|
|
95
|
+
];
|
|
96
|
+
/** The machine-facing surfaces: webhook ingress, the authenticated scheduler
|
|
97
|
+
tick, and the dev-only sync impact probe. All match on the RAW path
|
|
98
|
+
(prefix or exact) ahead of any segment decoding, exactly like the old
|
|
99
|
+
chain. (The v1 run-token apps proxy mount died with execution-v2 Wave 1.5;
|
|
100
|
+
the box callback surface at /box/ is its replacement.) */
|
|
101
|
+
export const systemRoutes = [
|
|
102
|
+
prefixRoute("POST", "/webhooks/", async ({ request, deps }) => {
|
|
103
|
+
return await deps.automations.webhook(request);
|
|
104
|
+
}),
|
|
105
|
+
route("POST", "/tick", async ({ request, deps }) => {
|
|
106
|
+
if (!await tickAuthorized(request)) {
|
|
107
|
+
return json({ error: { code: "blocked", message: "invalid tick credential" } }, 401);
|
|
108
|
+
}
|
|
109
|
+
// execution-v2 Lane D — one authenticated tick drives BOTH schedulers: the
|
|
110
|
+
// automations engine and the machine-app vendo.json schedules (additive
|
|
111
|
+
// `schedules` field). Point any external cron here (Vercel cron, GitHub
|
|
112
|
+
// Actions, crontab); the Cloud broker calls this same surface. The engines
|
|
113
|
+
// settle independently so one failing can never suppress the other; any
|
|
114
|
+
// failure still answers 500 so a retrying cron comes back (both engines
|
|
115
|
+
// are idempotent within their windows).
|
|
116
|
+
const [runs, schedules] = await Promise.allSettled([
|
|
117
|
+
deps.automations.tick(),
|
|
118
|
+
deps.apps.schedules.tick(),
|
|
119
|
+
]);
|
|
120
|
+
const errors = [
|
|
121
|
+
...(runs.status === "rejected" ? [`automations: ${runs.reason instanceof Error ? runs.reason.message : "tick failed"}`] : []),
|
|
122
|
+
...(schedules.status === "rejected" ? [`schedules: ${schedules.reason instanceof Error ? schedules.reason.message : "tick failed"}`] : []),
|
|
123
|
+
];
|
|
124
|
+
return json({
|
|
125
|
+
...(runs.status === "fulfilled" ? { runIds: runs.value } : {}),
|
|
126
|
+
...(schedules.status === "fulfilled" ? { schedules: schedules.value } : {}),
|
|
127
|
+
...(errors.length === 0 ? {} : { errors }),
|
|
128
|
+
}, errors.length === 0 ? 200 : 500);
|
|
129
|
+
}),
|
|
130
|
+
route("POST", "/sync/impact", async ({ request, deps }) => {
|
|
131
|
+
if (environment("NODE_ENV") === "production") {
|
|
132
|
+
throw new VendoError("blocked", "sync impact is only available on a dev server");
|
|
133
|
+
}
|
|
134
|
+
const body = await requestJson(request);
|
|
135
|
+
const tools = body["tools"];
|
|
136
|
+
if (!Array.isArray(tools) || tools.length > 200 || tools.some((tool) => typeof tool !== "string")) {
|
|
137
|
+
throw new VendoError("validation", "tools must be an array of at most 200 strings");
|
|
138
|
+
}
|
|
139
|
+
return json({ impact: await computeImpact(deps.store, tools) });
|
|
140
|
+
}),
|
|
141
|
+
// W3 (v3 spec §Context) — the `vendo sync` semantics seam: sample each
|
|
142
|
+
// zero-required-input READ tool once (through the guard-bound registry,
|
|
143
|
+
// with this request's own context — approvals/audit see it like any call)
|
|
144
|
+
// and return the INFERRED field semantics per tool. Values never leave the
|
|
145
|
+
// server: only classifications (and enum vocabularies) do. Dev-only, like
|
|
146
|
+
// /sync/impact.
|
|
147
|
+
route("POST", "/sync/semantics", async ({ deps, context }) => {
|
|
148
|
+
if (environment("NODE_ENV") === "production") {
|
|
149
|
+
throw new VendoError("blocked", "sync semantics is only available on a dev server");
|
|
150
|
+
}
|
|
151
|
+
const ctx = await context("chat");
|
|
152
|
+
const descriptors = await deps.tools.descriptors();
|
|
153
|
+
const requiresInput = (schema) => {
|
|
154
|
+
const required = schema?.required;
|
|
155
|
+
return Array.isArray(required) && required.length > 0;
|
|
156
|
+
};
|
|
157
|
+
const tools = {};
|
|
158
|
+
await Promise.all(descriptors
|
|
159
|
+
.filter((descriptor) => descriptor.risk === "read" && !requiresInput(descriptor.inputSchema))
|
|
160
|
+
.map(async (descriptor) => {
|
|
161
|
+
try {
|
|
162
|
+
const outcome = await deps.tools.execute({ id: `call_${globalThis.crypto.randomUUID()}`, tool: descriptor.name, args: {} }, ctx);
|
|
163
|
+
if (outcome.status !== "ok")
|
|
164
|
+
return;
|
|
165
|
+
const inferred = inferToolSemantics([outcome.output]);
|
|
166
|
+
if (Object.keys(inferred).length > 0)
|
|
167
|
+
tools[descriptor.name] = inferred;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// A failing sample leaves that tool without inferred semantics —
|
|
171
|
+
// the CLI writes what it can; hosts annotate the rest.
|
|
172
|
+
}
|
|
173
|
+
}));
|
|
174
|
+
return json({ tools });
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
/** The decoded first segment matches only /orgs and /orgs/* (any depth, any
|
|
178
|
+
method), never a lookalike like /organizations; the rest wildcard also
|
|
179
|
+
covers a trailing-slash `/orgs/`. */
|
|
180
|
+
export const orgsRoutes = [
|
|
181
|
+
route("*", "/orgs/*", async () => orgsCloudRequired()),
|
|
182
|
+
];
|
|
183
|
+
export const activityRoutes = [
|
|
184
|
+
route("GET", "/activity", async ({ url, deps, context }) => {
|
|
185
|
+
const ctx = await context("chat");
|
|
186
|
+
const limitValue = url.searchParams.get("limit");
|
|
187
|
+
const limit = limitValue === null ? undefined : Number(limitValue);
|
|
188
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
|
|
189
|
+
throw new VendoError("validation", "activity limit must be a positive integer");
|
|
190
|
+
}
|
|
191
|
+
const activity = await deps.guard.audit.query({
|
|
192
|
+
principal: ctx.principal,
|
|
193
|
+
...(url.searchParams.get("cursor") === null ? {} : { cursor: url.searchParams.get("cursor") }),
|
|
194
|
+
...(limit === undefined ? {} : { limit }),
|
|
195
|
+
});
|
|
196
|
+
// 09 §3: the wire returns AuditEvent[] — the block's {events,cursor}
|
|
197
|
+
// envelope stays internal (the client pages by last event id).
|
|
198
|
+
return json(activity.events);
|
|
199
|
+
}),
|
|
200
|
+
];
|
|
201
|
+
export const statusRoutes = [
|
|
202
|
+
route("GET", "/status", async ({ deps, context }) => {
|
|
203
|
+
await context("chat");
|
|
204
|
+
return json({
|
|
205
|
+
posture: deps.guard.status().posture,
|
|
206
|
+
version: VERSION,
|
|
207
|
+
blocks: {
|
|
208
|
+
store: true,
|
|
209
|
+
agent: true,
|
|
210
|
+
actions: true,
|
|
211
|
+
guard: true,
|
|
212
|
+
apps: true,
|
|
213
|
+
automations: true,
|
|
214
|
+
sandbox: deps.sandbox,
|
|
215
|
+
// Inference seam (cloud definition 2026-07-17): "custom" (host-passed
|
|
216
|
+
// model) or "ladder" (the composed devModel env default).
|
|
217
|
+
model: deps.model,
|
|
218
|
+
// 10-mcp §1 — the door is off by default; true only when
|
|
219
|
+
// createVendo({ mcp: true }) opened it.
|
|
220
|
+
mcp: deps.mcp,
|
|
221
|
+
// 04-actions §3 — how per-user connected accounts are brokered:
|
|
222
|
+
// "byo" (host's own Composio key), "cloud" (VENDO_API_KEY), or off.
|
|
223
|
+
connections: deps.connections.posture,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}),
|
|
227
|
+
];
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { AppsRuntime, AppTokens } from "@vendoai/apps";
|
|
2
|
+
import type { AutomationsEngine } from "@vendoai/automations";
|
|
3
|
+
import { VendoError, type Principal, type RunContext, type ToolOutcome, type ToolRegistry } from "@vendoai/core";
|
|
4
|
+
import type { VendoGuard } from "@vendoai/guard";
|
|
5
|
+
import type { McpDoor } from "@vendoai/mcp";
|
|
6
|
+
import type { SubjectMergeReport, VendoStore } from "@vendoai/store";
|
|
7
|
+
import type { Telemetry } from "@vendoai/telemetry";
|
|
8
|
+
import type { VendoAgent } from "@vendoai/agent";
|
|
9
|
+
import type { ByoApprovalResolution } from "../byo-approvals.js";
|
|
10
|
+
import type { ConnectionsService } from "../connections.js";
|
|
11
|
+
import type { RuntimeCaptureHandler } from "../runtime-capture.js";
|
|
12
|
+
/** The shared wire toolkit (kill-list B4): the route-table types and matcher,
|
|
13
|
+
the JSON/error envelope helpers, and the param validators every wire area
|
|
14
|
+
shares. The anonymous-session + RunContext resolution lives in
|
|
15
|
+
wire/context.ts; server.ts assembles the table from the per-area modules
|
|
16
|
+
under src/wire/. */
|
|
17
|
+
export declare const VERSION = "0.4.0";
|
|
18
|
+
export declare const BASE_PATH = "/api/vendo";
|
|
19
|
+
export type SandboxVenue = "e2b" | "cloud" | "custom" | false;
|
|
20
|
+
/** How inference is served: "custom" (a host-passed model) or "ladder" (the
|
|
21
|
+
composed devModel default — provider env key, then VENDO_API_KEY via the
|
|
22
|
+
Cloud model gateway, then the honest keyless failure; the ladder resolves
|
|
23
|
+
lazily, so /status cannot name the rung without forcing a resolution). */
|
|
24
|
+
export type ModelVenue = "custom" | "ladder";
|
|
25
|
+
export interface WireDeps {
|
|
26
|
+
principal: (req: Request) => Promise<Principal | null>;
|
|
27
|
+
ready: Promise<void>;
|
|
28
|
+
/** VENDO_BASE_URL is https → TLS terminates upstream; see secureRequest. */
|
|
29
|
+
trustedBaseIsHttps: boolean;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
store: VendoStore;
|
|
32
|
+
telemetry?: Telemetry;
|
|
33
|
+
agent: VendoAgent;
|
|
34
|
+
guard: VendoGuard;
|
|
35
|
+
apps: AppsRuntime;
|
|
36
|
+
/** execution-v2 Lane C — the guard-bound registry (the SAME binding chat and
|
|
37
|
+
automations execute through); the /box tools callback rides it so
|
|
38
|
+
approvals and audit see box-originated calls like any other. */
|
|
39
|
+
tools: ToolRegistry;
|
|
40
|
+
/** execution-v2 Lane C — verify a presented per-app box bearer
|
|
41
|
+
(createAppTokens over the composed store; mint lives with provision). */
|
|
42
|
+
appTokens: Pick<AppTokens, "verify">;
|
|
43
|
+
automations: AutomationsEngine;
|
|
44
|
+
/** Existing-agents Lane B — the per-approval state read `<VendoApprovalEmbed>`
|
|
45
|
+
polls: pending (with the full request for the consent card), executed
|
|
46
|
+
(with the resumed call's outcome), declined, or expired. */
|
|
47
|
+
byoApprovals: {
|
|
48
|
+
read(approvalId: string, principal: Principal): Promise<ByoApprovalResolution>;
|
|
49
|
+
};
|
|
50
|
+
connections: ConnectionsService;
|
|
51
|
+
sandbox: SandboxVenue;
|
|
52
|
+
model: ModelVenue;
|
|
53
|
+
doctor: {
|
|
54
|
+
present(ctx: RunContext): Promise<ToolOutcome>;
|
|
55
|
+
actAs(): Promise<ToolOutcome>;
|
|
56
|
+
};
|
|
57
|
+
mcp: boolean;
|
|
58
|
+
door?: McpDoor;
|
|
59
|
+
/** True only in a development composition — gates the local injection seams. */
|
|
60
|
+
development: boolean;
|
|
61
|
+
runtimeCapture?: RuntimeCaptureHandler;
|
|
62
|
+
onRequestOrigin?: (origin: string) => void;
|
|
63
|
+
/** 02-store §4 (kill-list B3) ephemeral-session policy. `now` reads the
|
|
64
|
+
(possibly injected) session clock; `sweep` runs the store TTL sweep and
|
|
65
|
+
cascades swept subjects into the agent. */
|
|
66
|
+
sessions: {
|
|
67
|
+
ttlMs: number;
|
|
68
|
+
sweepIntervalMs: number;
|
|
69
|
+
now: () => number;
|
|
70
|
+
};
|
|
71
|
+
/** True when any sweep leg is active (session TTL, parked-approval TTL) —
|
|
72
|
+
gates the amortized on-request sweep; each leg still no-ops itself. */
|
|
73
|
+
sweepEnabled: boolean;
|
|
74
|
+
/** The session doors bound to the composed store (selectStore): the local
|
|
75
|
+
engine's SQL registry, or the hosted store's wire doors. */
|
|
76
|
+
sessionStore: {
|
|
77
|
+
register(subject: string, now: number): Promise<void>;
|
|
78
|
+
adopt(from: string, to: string): Promise<SubjectMergeReport | null>;
|
|
79
|
+
};
|
|
80
|
+
sweep: () => Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
/** The per-request view a route handler receives: the raw request, its parsed
|
|
83
|
+
URL, the wire-relative path, lazily decoded segments, the matched entry's
|
|
84
|
+
`:param` captures, the anon-session-aware RunContext resolver, and the
|
|
85
|
+
composed deps. */
|
|
86
|
+
export interface WireContext {
|
|
87
|
+
request: Request;
|
|
88
|
+
url: URL;
|
|
89
|
+
/** Wire-relative raw path (output of the server's relativePath). */
|
|
90
|
+
path: string;
|
|
91
|
+
/** Decoded path segments — computed lazily on first access so raw-matched
|
|
92
|
+
routes (exact/prefix) never decode; malformed encoding throws the same
|
|
93
|
+
validation error the old eager routeSegments call threw. */
|
|
94
|
+
readonly segments: string[];
|
|
95
|
+
/** `:param` captures from the matched pattern (decoded segment values). */
|
|
96
|
+
params: Record<string, string>;
|
|
97
|
+
/** Resolve this request's RunContext for a venue. */
|
|
98
|
+
context(venue: RunContext["venue"]): Promise<RunContext>;
|
|
99
|
+
deps: WireDeps;
|
|
100
|
+
}
|
|
101
|
+
/** A handler answers with a Response, or returns undefined to FALL THROUGH to
|
|
102
|
+
the next entry — mirroring the old if-chain, where a matched-path block
|
|
103
|
+
whose method/operation checks all missed simply fell out the bottom (any
|
|
104
|
+
side effects it ran, e.g. context resolution, stand). */
|
|
105
|
+
export type RouteHandler = (wire: WireContext) => Promise<Response | undefined>;
|
|
106
|
+
type RoutePattern =
|
|
107
|
+
/** Raw-path equality — no decoding, matching the old `path === "/x"` arms. */
|
|
108
|
+
{
|
|
109
|
+
kind: "exact";
|
|
110
|
+
path: string;
|
|
111
|
+
}
|
|
112
|
+
/** Raw-path prefix — matching the old `path.startsWith("/x/")` arms. */
|
|
113
|
+
| {
|
|
114
|
+
kind: "prefix";
|
|
115
|
+
prefix: string;
|
|
116
|
+
}
|
|
117
|
+
/** Decoded-segment match: literals compare against decoded values, `:name`
|
|
118
|
+
captures, a trailing rest wildcard allows ZERO or more extra segments —
|
|
119
|
+
matching the old `head === "x" && segments.length >= n` arms. */
|
|
120
|
+
| {
|
|
121
|
+
kind: "segments";
|
|
122
|
+
parts: string[];
|
|
123
|
+
rest: boolean;
|
|
124
|
+
};
|
|
125
|
+
export interface RouteEntry {
|
|
126
|
+
/** Exact method, or "*" for grouped handlers that dispatch methods inside. */
|
|
127
|
+
method: string;
|
|
128
|
+
pattern: RoutePattern;
|
|
129
|
+
handler: RouteHandler;
|
|
130
|
+
}
|
|
131
|
+
/** Table entry from a pattern string: no `:param` and no trailing `/*` means
|
|
132
|
+
raw-path equality; otherwise decoded-segment matching (trailing `/*` = rest
|
|
133
|
+
wildcard, zero or more segments). */
|
|
134
|
+
export declare function route(method: string, pattern: string, handler: RouteHandler): RouteEntry;
|
|
135
|
+
/** Table entry matching on a raw path prefix (webhooks, proxy, the doctor
|
|
136
|
+
production gate) — never decodes, exactly like the old startsWith arms.
|
|
137
|
+
Raw string match, no segment boundary — include the trailing slash. */
|
|
138
|
+
export declare function prefixRoute(method: string, prefix: string, handler: RouteHandler): RouteEntry;
|
|
139
|
+
/** Scan the table in order; a handler returning undefined keeps scanning
|
|
140
|
+
(fall-through). No match → undefined; the caller answers not-found. */
|
|
141
|
+
export declare function dispatchRoutes(routes: readonly RouteEntry[], wire: WireContext): Promise<Response | undefined>;
|
|
142
|
+
export declare function json(body: unknown, status?: number): Response;
|
|
143
|
+
export declare function errorResponse(error: VendoError): Response;
|
|
144
|
+
export declare function internalError(): Response;
|
|
145
|
+
/** Orgs are a Vendo Cloud capability, not an OSS one (kill-list A5): every
|
|
146
|
+
/orgs route and every org-scoped param on /approvals and /grants answers
|
|
147
|
+
this, unconditionally — there is no key-gated activation path left in the
|
|
148
|
+
OSS wire (contrast the old block-actions design §C org machinery, which
|
|
149
|
+
this seam replaces). */
|
|
150
|
+
export declare function orgsCloudRequired(): never;
|
|
151
|
+
export declare function string(value: unknown, label: string): string;
|
|
152
|
+
export declare function requestJson(request: Request): Promise<Record<string, unknown>>;
|
|
153
|
+
export declare function environment(name: string): string | undefined;
|
|
154
|
+
export declare function routeSegments(path: string): string[];
|
|
155
|
+
/** Bytes → lowercase hex. Used by wire/context.ts's session-id mint and
|
|
156
|
+
wire/misc.ts's timing-safe digest compare. */
|
|
157
|
+
export declare function hex(bytes: ArrayBuffer | Uint8Array): string;
|
|
158
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { VendoError, } from "@vendoai/core";
|
|
2
|
+
/** The shared wire toolkit (kill-list B4): the route-table types and matcher,
|
|
3
|
+
the JSON/error envelope helpers, and the param validators every wire area
|
|
4
|
+
shares. The anonymous-session + RunContext resolution lives in
|
|
5
|
+
wire/context.ts; server.ts assembles the table from the per-area modules
|
|
6
|
+
under src/wire/. */
|
|
7
|
+
export const VERSION = "0.4.0";
|
|
8
|
+
export const BASE_PATH = "/api/vendo";
|
|
9
|
+
const STATUS_BY_CODE = {
|
|
10
|
+
validation: 400,
|
|
11
|
+
"not-found": 404,
|
|
12
|
+
blocked: 403,
|
|
13
|
+
conflict: 409,
|
|
14
|
+
"cloud-required": 402,
|
|
15
|
+
"sandbox-unavailable": 501,
|
|
16
|
+
"not-implemented": 501,
|
|
17
|
+
};
|
|
18
|
+
/** Table entry from a pattern string: no `:param` and no trailing `/*` means
|
|
19
|
+
raw-path equality; otherwise decoded-segment matching (trailing `/*` = rest
|
|
20
|
+
wildcard, zero or more segments). */
|
|
21
|
+
export function route(method, pattern, handler) {
|
|
22
|
+
if (!pattern.includes(":") && !pattern.endsWith("/*")) {
|
|
23
|
+
return { method, pattern: { kind: "exact", path: pattern }, handler };
|
|
24
|
+
}
|
|
25
|
+
const rest = pattern.endsWith("/*");
|
|
26
|
+
const parts = (rest ? pattern.slice(0, -2) : pattern).split("/").filter(Boolean);
|
|
27
|
+
return { method, pattern: { kind: "segments", parts, rest }, handler };
|
|
28
|
+
}
|
|
29
|
+
/** Table entry matching on a raw path prefix (webhooks, proxy, the doctor
|
|
30
|
+
production gate) — never decodes, exactly like the old startsWith arms.
|
|
31
|
+
Raw string match, no segment boundary — include the trailing slash. */
|
|
32
|
+
export function prefixRoute(method, prefix, handler) {
|
|
33
|
+
return { method, pattern: { kind: "prefix", prefix }, handler };
|
|
34
|
+
}
|
|
35
|
+
function matchRoute(entry, wire) {
|
|
36
|
+
if (entry.method !== "*" && entry.method !== wire.request.method)
|
|
37
|
+
return null;
|
|
38
|
+
const pattern = entry.pattern;
|
|
39
|
+
if (pattern.kind === "exact")
|
|
40
|
+
return pattern.path === wire.path ? {} : null;
|
|
41
|
+
if (pattern.kind === "prefix")
|
|
42
|
+
return wire.path.startsWith(pattern.prefix) ? {} : null;
|
|
43
|
+
// Segment access may throw the invalid-encoding validation error — only ever
|
|
44
|
+
// reached after every raw pre-route entry has had its chance, preserving the
|
|
45
|
+
// old chain's ordering (prefix routes served /proxy/%zz; /threads/%zz threw).
|
|
46
|
+
const segments = wire.segments;
|
|
47
|
+
if (pattern.rest ? segments.length < pattern.parts.length : segments.length !== pattern.parts.length) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const params = {};
|
|
51
|
+
for (let i = 0; i < pattern.parts.length; i++) {
|
|
52
|
+
const part = pattern.parts[i];
|
|
53
|
+
if (part.startsWith(":"))
|
|
54
|
+
params[part.slice(1)] = segments[i];
|
|
55
|
+
else if (part !== segments[i])
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return params;
|
|
59
|
+
}
|
|
60
|
+
/** Scan the table in order; a handler returning undefined keeps scanning
|
|
61
|
+
(fall-through). No match → undefined; the caller answers not-found. */
|
|
62
|
+
export async function dispatchRoutes(routes, wire) {
|
|
63
|
+
for (const entry of routes) {
|
|
64
|
+
const params = matchRoute(entry, wire);
|
|
65
|
+
if (params === null)
|
|
66
|
+
continue;
|
|
67
|
+
wire.params = params;
|
|
68
|
+
const response = await entry.handler(wire);
|
|
69
|
+
if (response !== undefined)
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
export function json(body, status = 200) {
|
|
75
|
+
return Response.json(body, { status });
|
|
76
|
+
}
|
|
77
|
+
export function errorResponse(error) {
|
|
78
|
+
return json({ error: { code: error.code, message: error.message } }, STATUS_BY_CODE[error.code]);
|
|
79
|
+
}
|
|
80
|
+
export function internalError() {
|
|
81
|
+
return errorResponse(new VendoError("not-implemented", "Internal Vendo error"));
|
|
82
|
+
}
|
|
83
|
+
/** Orgs are a Vendo Cloud capability, not an OSS one (kill-list A5): every
|
|
84
|
+
/orgs route and every org-scoped param on /approvals and /grants answers
|
|
85
|
+
this, unconditionally — there is no key-gated activation path left in the
|
|
86
|
+
OSS wire (contrast the old block-actions design §C org machinery, which
|
|
87
|
+
this seam replaces). */
|
|
88
|
+
export function orgsCloudRequired() {
|
|
89
|
+
throw new VendoError("cloud-required", "orgs are a Vendo Cloud capability");
|
|
90
|
+
}
|
|
91
|
+
function object(value, label) {
|
|
92
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
93
|
+
throw new VendoError("validation", `${label} must be an object`);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
export function string(value, label) {
|
|
98
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
99
|
+
throw new VendoError("validation", `${label} must be a non-empty string`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
export async function requestJson(request) {
|
|
104
|
+
try {
|
|
105
|
+
return object(await request.json(), "request body");
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof VendoError)
|
|
109
|
+
throw error;
|
|
110
|
+
throw new VendoError("validation", "request body must be valid JSON");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function environment(name) {
|
|
114
|
+
if (typeof process === "undefined")
|
|
115
|
+
return undefined;
|
|
116
|
+
const value = process.env[name];
|
|
117
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
118
|
+
}
|
|
119
|
+
export function routeSegments(path) {
|
|
120
|
+
try {
|
|
121
|
+
return path.split("/").filter(Boolean).map(decodeURIComponent);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new VendoError("validation", "route contains invalid URL encoding");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Bytes → lowercase hex. Used by wire/context.ts's session-id mint and
|
|
128
|
+
wire/misc.ts's timing-safe digest compare. */
|
|
129
|
+
export function hex(bytes) {
|
|
130
|
+
let out = "";
|
|
131
|
+
for (const b of bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes))
|
|
132
|
+
out += b.toString(16).padStart(2, "0");
|
|
133
|
+
return out;
|
|
134
|
+
}
|