@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.
Files changed (212) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +28 -0
  3. package/bin/vendo.mjs +4 -0
  4. package/dist/ai-sdk.d.ts +35 -0
  5. package/dist/ai-sdk.js +62 -0
  6. package/dist/auth-presets/auth-js.d.ts +14 -0
  7. package/dist/auth-presets/auth-js.js +91 -0
  8. package/dist/auth-presets/auth0.d.ts +23 -0
  9. package/dist/auth-presets/auth0.js +98 -0
  10. package/dist/auth-presets/clerk.d.ts +23 -0
  11. package/dist/auth-presets/clerk.js +65 -0
  12. package/dist/auth-presets/conformance.d.ts +42 -0
  13. package/dist/auth-presets/conformance.js +139 -0
  14. package/dist/auth-presets/identity.d.ts +56 -0
  15. package/dist/auth-presets/identity.js +152 -0
  16. package/dist/auth-presets/index.d.ts +14 -0
  17. package/dist/auth-presets/index.js +12 -0
  18. package/dist/auth-presets/jwt.d.ts +16 -0
  19. package/dist/auth-presets/jwt.js +54 -0
  20. package/dist/auth-presets/shared.d.ts +35 -0
  21. package/dist/auth-presets/shared.js +1 -0
  22. package/dist/auth-presets/supabase.d.ts +42 -0
  23. package/dist/auth-presets/supabase.js +229 -0
  24. package/dist/byo-approvals.d.ts +37 -0
  25. package/dist/byo-approvals.js +189 -0
  26. package/dist/capability-misses.d.ts +38 -0
  27. package/dist/capability-misses.js +172 -0
  28. package/dist/catalog.d.ts +19 -0
  29. package/dist/catalog.js +152 -0
  30. package/dist/cli/cloud/args.d.ts +3 -0
  31. package/dist/cli/cloud/args.js +32 -0
  32. package/dist/cli/cloud/auth.d.ts +17 -0
  33. package/dist/cli/cloud/auth.js +123 -0
  34. package/dist/cli/cloud/client.d.ts +28 -0
  35. package/dist/cli/cloud/client.js +133 -0
  36. package/dist/cli/cloud/command.d.ts +22 -0
  37. package/dist/cli/cloud/command.js +73 -0
  38. package/dist/cli/cloud/deploy.d.ts +26 -0
  39. package/dist/cli/cloud/deploy.js +187 -0
  40. package/dist/cli/cloud/device-login.d.ts +32 -0
  41. package/dist/cli/cloud/device-login.js +259 -0
  42. package/dist/cli/cloud/index.d.ts +5 -0
  43. package/dist/cli/cloud/index.js +59 -0
  44. package/dist/cli/cloud/keys.d.ts +2 -0
  45. package/dist/cli/cloud/keys.js +31 -0
  46. package/dist/cli/cloud/members.d.ts +3 -0
  47. package/dist/cli/cloud/members.js +25 -0
  48. package/dist/cli/cloud/output.d.ts +4 -0
  49. package/dist/cli/cloud/output.js +12 -0
  50. package/dist/cli/cloud/pending-claim.d.ts +29 -0
  51. package/dist/cli/cloud/pending-claim.js +38 -0
  52. package/dist/cli/cloud/read.d.ts +3 -0
  53. package/dist/cli/cloud/read.js +12 -0
  54. package/dist/cli/cloud/services.d.ts +2 -0
  55. package/dist/cli/cloud/services.js +9 -0
  56. package/dist/cli/cloud/session.d.ts +13 -0
  57. package/dist/cli/cloud/session.js +41 -0
  58. package/dist/cli/cloud-init.d.ts +60 -0
  59. package/dist/cli/cloud-init.js +149 -0
  60. package/dist/cli/dep-versions.d.ts +26 -0
  61. package/dist/cli/dep-versions.js +77 -0
  62. package/dist/cli/doctor-codes.d.ts +58 -0
  63. package/dist/cli/doctor-codes.js +60 -0
  64. package/dist/cli/doctor-live.d.ts +67 -0
  65. package/dist/cli/doctor-live.js +169 -0
  66. package/dist/cli/doctor.d.ts +52 -0
  67. package/dist/cli/doctor.js +467 -0
  68. package/dist/cli/eject.d.ts +29 -0
  69. package/dist/cli/eject.js +141 -0
  70. package/dist/cli/extract/apply.d.ts +26 -0
  71. package/dist/cli/extract/apply.js +123 -0
  72. package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
  73. package/dist/cli/extract/claude-cli-harness.js +132 -0
  74. package/dist/cli/extract/claude-harness.d.ts +14 -0
  75. package/dist/cli/extract/claude-harness.js +127 -0
  76. package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
  77. package/dist/cli/extract/codex-cli-harness.js +154 -0
  78. package/dist/cli/extract/delegate.d.ts +21 -0
  79. package/dist/cli/extract/delegate.js +77 -0
  80. package/dist/cli/extract/extraction.d.ts +65 -0
  81. package/dist/cli/extract/extraction.js +300 -0
  82. package/dist/cli/extract/gateway-fuel.d.ts +69 -0
  83. package/dist/cli/extract/gateway-fuel.js +69 -0
  84. package/dist/cli/extract/harness.d.ts +114 -0
  85. package/dist/cli/extract/harness.js +84 -0
  86. package/dist/cli/extract/index.d.ts +9 -0
  87. package/dist/cli/extract/index.js +9 -0
  88. package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
  89. package/dist/cli/extract/npx-engine-harness.js +225 -0
  90. package/dist/cli/extract/stages.d.ts +149 -0
  91. package/dist/cli/extract/stages.js +398 -0
  92. package/dist/cli/framework.d.ts +8 -0
  93. package/dist/cli/framework.js +36 -0
  94. package/dist/cli/init-auth.d.ts +67 -0
  95. package/dist/cli/init-auth.js +142 -0
  96. package/dist/cli/init-scaffolds.d.ts +28 -0
  97. package/dist/cli/init-scaffolds.js +235 -0
  98. package/dist/cli/init.d.ts +115 -0
  99. package/dist/cli/init.js +1008 -0
  100. package/dist/cli/mcp/index.d.ts +7 -0
  101. package/dist/cli/mcp/index.js +59 -0
  102. package/dist/cli/mcp/registry.d.ts +9 -0
  103. package/dist/cli/mcp/registry.js +117 -0
  104. package/dist/cli/mcp/server-json.d.ts +12 -0
  105. package/dist/cli/mcp/server-json.js +65 -0
  106. package/dist/cli/mcp/server.schema.json +574 -0
  107. package/dist/cli/mcp/verify-domain.d.ts +11 -0
  108. package/dist/cli/mcp/verify-domain.js +48 -0
  109. package/dist/cli/playground/app/embed-entry.d.ts +20 -0
  110. package/dist/cli/playground/app/embed-entry.js +46 -0
  111. package/dist/cli/playground/app/fake-client.d.ts +3 -0
  112. package/dist/cli/playground/app/fake-client.js +215 -0
  113. package/dist/cli/playground/app/fixtures.d.ts +47 -0
  114. package/dist/cli/playground/app/fixtures.js +472 -0
  115. package/dist/cli/playground/app/main.d.ts +1 -0
  116. package/dist/cli/playground/app/main.js +108 -0
  117. package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
  118. package/dist/cli/playground/app/scenario-mount.js +48 -0
  119. package/dist/cli/playground/app/scenarios.d.ts +25 -0
  120. package/dist/cli/playground/app/scenarios.js +152 -0
  121. package/dist/cli/playground/app/theme-editor.d.ts +13 -0
  122. package/dist/cli/playground/app/theme-editor.js +149 -0
  123. package/dist/cli/playground/app/theme-state.d.ts +29 -0
  124. package/dist/cli/playground/app/theme-state.js +151 -0
  125. package/dist/cli/playground/bundle.gen.d.ts +2 -0
  126. package/dist/cli/playground/bundle.gen.js +2 -0
  127. package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
  128. package/dist/cli/playground/embed-bundle.gen.js +2 -0
  129. package/dist/cli/playground.d.ts +33 -0
  130. package/dist/cli/playground.js +120 -0
  131. package/dist/cli/pretty.d.ts +48 -0
  132. package/dist/cli/pretty.js +330 -0
  133. package/dist/cli/refine.d.ts +43 -0
  134. package/dist/cli/refine.js +181 -0
  135. package/dist/cli/semantics.d.ts +31 -0
  136. package/dist/cli/semantics.js +122 -0
  137. package/dist/cli/shared.d.ts +68 -0
  138. package/dist/cli/shared.js +184 -0
  139. package/dist/cli/sync.d.ts +36 -0
  140. package/dist/cli/sync.js +202 -0
  141. package/dist/cli/theme/color.d.ts +15 -0
  142. package/dist/cli/theme/color.js +188 -0
  143. package/dist/cli/theme/css-vars.d.ts +13 -0
  144. package/dist/cli/theme/css-vars.js +40 -0
  145. package/dist/cli/theme/entry-candidates.d.ts +5 -0
  146. package/dist/cli/theme/entry-candidates.js +22 -0
  147. package/dist/cli/theme/extract-theme.d.ts +184 -0
  148. package/dist/cli/theme/extract-theme.js +358 -0
  149. package/dist/cli/theme/walk.d.ts +2 -0
  150. package/dist/cli/theme/walk.js +24 -0
  151. package/dist/cli.d.ts +2 -0
  152. package/dist/cli.js +333 -0
  153. package/dist/cloud-apps.d.ts +17 -0
  154. package/dist/cloud-apps.js +46 -0
  155. package/dist/cloud-console.d.ts +32 -0
  156. package/dist/cloud-console.js +72 -0
  157. package/dist/cloud-tools.d.ts +23 -0
  158. package/dist/cloud-tools.js +196 -0
  159. package/dist/connections.d.ts +71 -0
  160. package/dist/connections.js +192 -0
  161. package/dist/deployment-identity.d.ts +12 -0
  162. package/dist/deployment-identity.js +69 -0
  163. package/dist/dev-creds/model.d.ts +65 -0
  164. package/dist/dev-creds/model.js +159 -0
  165. package/dist/dev-creds/resolve.d.ts +36 -0
  166. package/dist/dev-creds/resolve.js +57 -0
  167. package/dist/hosted-store.d.ts +52 -0
  168. package/dist/hosted-store.js +322 -0
  169. package/dist/index.d.ts +13 -0
  170. package/dist/index.js +1 -0
  171. package/dist/mastra.d.ts +46 -0
  172. package/dist/mastra.js +88 -0
  173. package/dist/react.d.ts +10 -0
  174. package/dist/react.js +32 -0
  175. package/dist/refine.d.ts +244 -0
  176. package/dist/refine.js +599 -0
  177. package/dist/remixable.d.ts +18 -0
  178. package/dist/remixable.js +42 -0
  179. package/dist/runtime-capture.d.ts +18 -0
  180. package/dist/runtime-capture.js +111 -0
  181. package/dist/sandbox-wire.d.ts +101 -0
  182. package/dist/sandbox-wire.js +101 -0
  183. package/dist/sandbox.d.ts +38 -0
  184. package/dist/sandbox.js +389 -0
  185. package/dist/server.d.ts +216 -0
  186. package/dist/server.js +1320 -0
  187. package/dist/sync-impact.d.ts +14 -0
  188. package/dist/sync-impact.js +75 -0
  189. package/dist/turn-liveness.d.ts +33 -0
  190. package/dist/turn-liveness.js +105 -0
  191. package/dist/wire/approvals.d.ts +10 -0
  192. package/dist/wire/approvals.js +59 -0
  193. package/dist/wire/apps.d.ts +5 -0
  194. package/dist/wire/apps.js +155 -0
  195. package/dist/wire/automations.d.ts +5 -0
  196. package/dist/wire/automations.js +57 -0
  197. package/dist/wire/box.d.ts +3 -0
  198. package/dist/wire/box.js +268 -0
  199. package/dist/wire/connections.d.ts +5 -0
  200. package/dist/wire/connections.js +47 -0
  201. package/dist/wire/context.d.ts +22 -0
  202. package/dist/wire/context.js +190 -0
  203. package/dist/wire/doctor.d.ts +19 -0
  204. package/dist/wire/doctor.js +116 -0
  205. package/dist/wire/misc.d.ts +18 -0
  206. package/dist/wire/misc.js +227 -0
  207. package/dist/wire/shared.d.ts +158 -0
  208. package/dist/wire/shared.js +134 -0
  209. package/dist/wire/threads.d.ts +3 -0
  210. package/dist/wire/threads.js +68 -0
  211. package/package.json +151 -0
  212. 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
+ }
@@ -0,0 +1,3 @@
1
+ import { type RouteEntry } from "./shared.js";
2
+ /** 09 §3 — the /threads wire area: chat streaming plus thread list/get/delete. */
3
+ export declare const threadRoutes: RouteEntry[];