@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,14 @@
1
+ import type { VendoStore } from "@vendoai/store";
2
+ export interface ToolImpact {
3
+ tool: string;
4
+ apps: {
5
+ id: string;
6
+ title: string;
7
+ }[];
8
+ automations: {
9
+ id: string;
10
+ title: string;
11
+ }[];
12
+ grants: number;
13
+ }
14
+ export declare function computeImpact(store: VendoStore, tools: string[]): Promise<ToolImpact[]>;
@@ -0,0 +1,75 @@
1
+ import { VENDO_TREE_FORMAT_V2 } from "@vendoai/core";
2
+ async function allRecords(store, collection) {
3
+ const records = [];
4
+ let cursor;
5
+ do {
6
+ const page = await store.records(collection).list({ limit: 1_000, cursor });
7
+ records.push(...page.records);
8
+ cursor = page.cursor;
9
+ } while (cursor !== undefined);
10
+ return records;
11
+ }
12
+ function collectActions(value, tools) {
13
+ if (Array.isArray(value)) {
14
+ for (const item of value)
15
+ collectActions(item, tools);
16
+ return;
17
+ }
18
+ if (typeof value !== "object" || value === null)
19
+ return;
20
+ const record = value;
21
+ for (const key of ["action", "$action"]) {
22
+ const reference = record[key];
23
+ if (typeof reference === "string" && !reference.startsWith("fn:"))
24
+ tools.add(reference);
25
+ }
26
+ for (const nested of Object.values(record))
27
+ collectActions(nested, tools);
28
+ }
29
+ function referencedTools(doc) {
30
+ const tools = new Set();
31
+ if (doc.tree?.formatVersion === VENDO_TREE_FORMAT_V2) {
32
+ const tree = doc.tree;
33
+ for (const query of tree.queries ?? []) {
34
+ if (typeof query.tool === "string" && !query.tool.startsWith("fn:"))
35
+ tools.add(query.tool);
36
+ }
37
+ for (const node of tree.nodes ?? [])
38
+ collectActions(node.props, tools);
39
+ }
40
+ if (doc.trigger?.run.kind === "steps") {
41
+ for (const step of doc.trigger.run.steps) {
42
+ if (!step.tool.startsWith("fn:"))
43
+ tools.add(step.tool);
44
+ }
45
+ }
46
+ return tools;
47
+ }
48
+ function activeGrant(grant, now) {
49
+ return grant.revokedAt === undefined && (grant.expiresAt === undefined || grant.expiresAt > now);
50
+ }
51
+ export async function computeImpact(store, tools) {
52
+ const [appRecords, grantRecords] = await Promise.all([
53
+ allRecords(store, "vendo_apps"),
54
+ allRecords(store, "vendo_grants"),
55
+ ]);
56
+ const apps = appRecords.map((record) => record.data).filter((app) => app.enabled);
57
+ const now = new Date().toISOString();
58
+ const grants = grantRecords
59
+ .map((record) => record.data)
60
+ .filter((grant) => activeGrant(grant, now));
61
+ return tools.map((tool) => {
62
+ const impact = { tool, apps: [], automations: [], grants: 0 };
63
+ for (const app of apps) {
64
+ if (!referencedTools(app.doc).has(tool))
65
+ continue;
66
+ const reference = { id: app.doc.id, title: app.doc.name };
67
+ if (app.doc.trigger === undefined)
68
+ impact.apps.push(reference);
69
+ else
70
+ impact.automations.push(reference);
71
+ }
72
+ impact.grants = grants.filter((grant) => grant.tool === tool).length;
73
+ return impact;
74
+ });
75
+ }
@@ -0,0 +1,33 @@
1
+ /** ENG-353 — server-side turn liveness: the idle-abort fallback for client
2
+ * disconnects the runtime never surfaces.
3
+ *
4
+ * The fast path is unchanged: `request.signal` cancels the turn the moment the
5
+ * runtime propagates a fetch abort (wave-5 AGENT-3). But under `next dev` a
6
+ * real browser's graceful tab-close/navigate-away fires neither the signal nor
7
+ * a stream cancel, so an abandoned turn runs to completion. The fallback is
8
+ * liveness by heartbeat: the panel beats `POST /threads/:id/heartbeat` while
9
+ * it consumes the stream (08 — `withTurnHeartbeat`); the FIRST beat arms the
10
+ * watchdog, and from then on `IDLE_ABORT_MS` of silence aborts the turn.
11
+ * Arming is opt-in by construction: consumers that never beat (curl drills,
12
+ * scripted clients, older panels) keep run-to-completion semantics.
13
+ *
14
+ * The registry lives on globalThis (Symbol.for) so HMR copies of this module
15
+ * under a dev server share one view: a turn registered before an edit is still
16
+ * beatable after it.
17
+ */
18
+ /** Track one streaming turn; returns its unregister. Registration alone never
19
+ * arms the watchdog — only a first heartbeat does. */
20
+ export declare function registerActiveTurn(turn: {
21
+ threadId: string;
22
+ subject: string;
23
+ abort: () => void;
24
+ }): () => void;
25
+ /** A heartbeat for `threadId` from `subject`. Refreshes (and on first beat
26
+ * arms) the idle watchdog of every matching in-flight turn. Foreign or
27
+ * unknown ids answer false — no oracle, and a beat can never keep (or end)
28
+ * another principal's turn. */
29
+ export declare function touchActiveTurn(threadId: string, subject: string): boolean;
30
+ /** Wrap a turn response so `onSettled` runs exactly once when its stream
31
+ * finishes, errors, or is cancelled — the turn's registry entry must not
32
+ * outlive the stream. Mirrors the wire's inflight-bracket wrapper. */
33
+ export declare function trackTurnResponse(response: Response, onSettled: () => void): Response;
@@ -0,0 +1,105 @@
1
+ /** ENG-353 — server-side turn liveness: the idle-abort fallback for client
2
+ * disconnects the runtime never surfaces.
3
+ *
4
+ * The fast path is unchanged: `request.signal` cancels the turn the moment the
5
+ * runtime propagates a fetch abort (wave-5 AGENT-3). But under `next dev` a
6
+ * real browser's graceful tab-close/navigate-away fires neither the signal nor
7
+ * a stream cancel, so an abandoned turn runs to completion. The fallback is
8
+ * liveness by heartbeat: the panel beats `POST /threads/:id/heartbeat` while
9
+ * it consumes the stream (08 — `withTurnHeartbeat`); the FIRST beat arms the
10
+ * watchdog, and from then on `IDLE_ABORT_MS` of silence aborts the turn.
11
+ * Arming is opt-in by construction: consumers that never beat (curl drills,
12
+ * scripted clients, older panels) keep run-to-completion semantics.
13
+ *
14
+ * The registry lives on globalThis (Symbol.for) so HMR copies of this module
15
+ * under a dev server share one view: a turn registered before an edit is still
16
+ * beatable after it.
17
+ */
18
+ import { environment } from "./wire/shared.js";
19
+ const IDLE_ABORT_MS = 15_000;
20
+ const ACTIVE_TURNS_KEY = Symbol.for("vendoai.vendo.active-turns@1");
21
+ function activeTurns() {
22
+ const holder = globalThis;
23
+ return (holder[ACTIVE_TURNS_KEY] ??= new Set());
24
+ }
25
+ /** Test seam only: the idle window, overridable per call site via env. The
26
+ * process-guarded `environment` helper keeps every beat working on
27
+ * edge/Worker targets with no `process` global. */
28
+ function idleAbortMs() {
29
+ const configured = Number(environment("VENDO_TURN_IDLE_ABORT_MS"));
30
+ return Number.isFinite(configured) && configured > 0 ? configured : IDLE_ABORT_MS;
31
+ }
32
+ /** Track one streaming turn; returns its unregister. Registration alone never
33
+ * arms the watchdog — only a first heartbeat does. */
34
+ export function registerActiveTurn(turn) {
35
+ const entry = { ...turn };
36
+ activeTurns().add(entry);
37
+ return () => {
38
+ if (entry.idleTimer !== undefined)
39
+ clearTimeout(entry.idleTimer);
40
+ activeTurns().delete(entry);
41
+ };
42
+ }
43
+ /** A heartbeat for `threadId` from `subject`. Refreshes (and on first beat
44
+ * arms) the idle watchdog of every matching in-flight turn. Foreign or
45
+ * unknown ids answer false — no oracle, and a beat can never keep (or end)
46
+ * another principal's turn. */
47
+ export function touchActiveTurn(threadId, subject) {
48
+ let active = false;
49
+ for (const turn of activeTurns()) {
50
+ if (turn.threadId !== threadId || turn.subject !== subject)
51
+ continue;
52
+ active = true;
53
+ if (turn.idleTimer !== undefined)
54
+ clearTimeout(turn.idleTimer);
55
+ turn.idleTimer = setTimeout(() => {
56
+ console.warn(`[vendo] turn on thread ${turn.threadId} lost its client heartbeat for ${idleAbortMs()}ms — aborting the abandoned turn.`);
57
+ // Drop the entry now (idempotent with the stream-settled unregister):
58
+ // an idle-aborted turn is over, and a late beat must see it inactive
59
+ // even before the runtime drains the closing stream.
60
+ activeTurns().delete(turn);
61
+ turn.abort();
62
+ }, idleAbortMs());
63
+ turn.idleTimer.unref?.();
64
+ }
65
+ return active;
66
+ }
67
+ /** Wrap a turn response so `onSettled` runs exactly once when its stream
68
+ * finishes, errors, or is cancelled — the turn's registry entry must not
69
+ * outlive the stream. Mirrors the wire's inflight-bracket wrapper. */
70
+ export function trackTurnResponse(response, onSettled) {
71
+ if (response.body === null) {
72
+ onSettled();
73
+ return response;
74
+ }
75
+ let settled = false;
76
+ const settle = () => {
77
+ if (settled)
78
+ return;
79
+ settled = true;
80
+ onSettled();
81
+ };
82
+ const reader = response.body.getReader();
83
+ const tracked = new ReadableStream({
84
+ async pull(controller) {
85
+ try {
86
+ const { done, value } = await reader.read();
87
+ if (done) {
88
+ settle();
89
+ controller.close();
90
+ return;
91
+ }
92
+ controller.enqueue(value);
93
+ }
94
+ catch (error) {
95
+ settle();
96
+ controller.error(error);
97
+ }
98
+ },
99
+ cancel(reason) {
100
+ settle();
101
+ return reader.cancel(reason);
102
+ },
103
+ });
104
+ return new Response(tracked, response);
105
+ }
@@ -0,0 +1,10 @@
1
+ import { type RouteEntry } from "./shared.js";
2
+ /** 05-guard / 09 §3 — the approvals wire area. Org-scoped approvals
3
+ (`?org=<id>` / body `org`) were a Vendo Cloud capability (block-actions
4
+ design §C); orgs are cut from OSS (kill-list A5), so a request carrying an
5
+ org param here now gets the same cloud-required error the /orgs routes
6
+ answer, rather than silently ignoring it. */
7
+ export declare const approvalRoutes: RouteEntry[];
8
+ /** Same cloud-required `?org=` scoping as approvals: standing grants scoping
9
+ to an org is a Vendo Cloud capability. */
10
+ export declare const grantRoutes: RouteEntry[];
@@ -0,0 +1,59 @@
1
+ import { VendoError, approvalDecisionSchema } from "@vendoai/core";
2
+ import { json, orgsCloudRequired, requestJson, route, string } from "./shared.js";
3
+ /** 05-guard / 09 §3 — the approvals wire area. Org-scoped approvals
4
+ (`?org=<id>` / body `org`) were a Vendo Cloud capability (block-actions
5
+ design §C); orgs are cut from OSS (kill-list A5), so a request carrying an
6
+ org param here now gets the same cloud-required error the /orgs routes
7
+ answer, rather than silently ignoring it. */
8
+ export const approvalRoutes = [
9
+ route("GET", "/approvals", async ({ url, deps, context }) => {
10
+ const ctx = await context("chat");
11
+ if (url.searchParams.get("org") !== null)
12
+ orgsCloudRequired();
13
+ return json(await deps.guard.approvals.pending(ctx.principal));
14
+ }),
15
+ // Existing-agents Lane B — the read `<VendoApprovalEmbed>` polls for a
16
+ // parked BYO guarded call: the frozen VendoApprovalEmbedState vocabulary,
17
+ // carrying the full request while pending (the consent card shows real
18
+ // inputs) and the resumed call's outcome once executed. Owner-scoped;
19
+ // unknown and foreign ids both answer not-found. Registered before the
20
+ // decide route only textually — decide's exact-path POST never collides
21
+ // with this GET segment pattern.
22
+ route("GET", "/approvals/:id", async ({ url, deps, context, params }) => {
23
+ const ctx = await context("chat");
24
+ if (url.searchParams.get("org") !== null)
25
+ orgsCloudRequired();
26
+ return json(await deps.byoApprovals.read(string(params["id"], "approval id"), ctx.principal));
27
+ }),
28
+ route("POST", "/approvals/decide", async ({ request, deps, context }) => {
29
+ const body = await requestJson(request);
30
+ const ids = Array.isArray(body["ids"]) ? body["ids"].map((id) => string(id, "approval id")) : [];
31
+ if (ids.length === 0)
32
+ throw new VendoError("validation", "ids must contain at least one approval id");
33
+ const decision = approvalDecisionSchema.safeParse(body["decision"]);
34
+ if (!decision.success)
35
+ throw new VendoError("validation", "decision is invalid");
36
+ const ctx = await context("chat");
37
+ if (body["org"] !== undefined)
38
+ orgsCloudRequired();
39
+ await deps.guard.approvals.decide(ids, decision.data, ctx.principal);
40
+ return json({});
41
+ }),
42
+ ];
43
+ /** Same cloud-required `?org=` scoping as approvals: standing grants scoping
44
+ to an org is a Vendo Cloud capability. */
45
+ export const grantRoutes = [
46
+ route("GET", "/grants", async ({ url, deps, context }) => {
47
+ const ctx = await context("chat");
48
+ if (url.searchParams.get("org") !== null)
49
+ orgsCloudRequired();
50
+ return json(await deps.guard.grants.list(ctx.principal));
51
+ }),
52
+ route("DELETE", "/grants/:id", async ({ url, deps, context, params }) => {
53
+ const ctx = await context("chat");
54
+ if (url.searchParams.get("org") !== null)
55
+ orgsCloudRequired();
56
+ await deps.guard.grants.revoke(string(params["id"], "grant id"), ctx.principal);
57
+ return json({});
58
+ }),
59
+ ];
@@ -0,0 +1,5 @@
1
+ import { type RouteEntry } from "./shared.js";
2
+ /** 06-apps / 09 §3 — the /apps wire area: CRUD, open/call/edit, history,
3
+ ship-diff, pin drift/rebase, the gesture fork (fork-pin), export/import,
4
+ fork (whole-app copy — a different feature from fork-pin). */
5
+ export declare const appRoutes: RouteEntry[];
@@ -0,0 +1,155 @@
1
+ import { VendoError } from "@vendoai/core";
2
+ import { json, requestJson, route, string } from "./shared.js";
3
+ /** 06-apps / 09 §3 — the /apps wire area: CRUD, open/call/edit, history,
4
+ ship-diff, pin drift/rebase, the gesture fork (fork-pin), export/import,
5
+ fork (whole-app copy — a different feature from fork-pin). */
6
+ export const appRoutes = [
7
+ // Grouped like the old if-chain arm: ANY method on /apps resolves context
8
+ // first; an unhandled method falls through to the table's not-found.
9
+ route("*", "/apps", async ({ request, deps, context }) => {
10
+ const ctx = await context("app");
11
+ if (request.method === "GET") {
12
+ return json(await deps.apps.list(ctx));
13
+ }
14
+ if (request.method === "POST") {
15
+ const body = await requestJson(request);
16
+ return json(await deps.apps.create({ prompt: string(body["prompt"], "prompt") }, ctx));
17
+ }
18
+ return undefined;
19
+ }),
20
+ // 06-apps §8 — gesture-owned forking (2026-07-21): the deterministic fork
21
+ // the user's Remix gesture on an EMPTY slot invokes. The engine mints a
22
+ // minimal app, copies the captured baseline, and records the pin — no model
23
+ // call. An optional instruction then rides the ordinary edit path, already
24
+ // scoped to the forked component. The model never decides to fork.
25
+ // ORDER IS LOAD-BEARING: this entry (and /apps/import below) must stay
26
+ // ahead of the "/apps/:appId/*" catch-all, whose rest pattern would
27
+ // otherwise capture appId="fork-pin".
28
+ route("POST", "/apps/fork-pin", async ({ request, deps, context }) => {
29
+ const ctx = await context("app");
30
+ const body = await requestJson(request);
31
+ return json(await deps.apps.pins.fork({
32
+ slot: string(body["slot"], "slot"),
33
+ ...(body["instruction"] === undefined ? {} : { instruction: string(body["instruction"], "instruction") }),
34
+ }, ctx));
35
+ }),
36
+ route("POST", "/apps/import", async ({ request, deps, context }) => {
37
+ // The CSRF floor exempts import (binary body), so it must instead require
38
+ // a non-CORS-safelisted media type — forcing a cross-origin preflight so
39
+ // a simple credentialed form/text POST cannot silently import (09 §3).
40
+ const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
41
+ if (contentType !== "application/octet-stream" && contentType !== "application/vnd.vendo.app") {
42
+ throw new VendoError("validation", "import requires Content-Type: application/octet-stream");
43
+ }
44
+ const ctx = await context("app");
45
+ return json(await deps.apps.importApp(new Uint8Array(await request.arrayBuffer()), ctx));
46
+ }),
47
+ // The old `head === "apps" && segments.length >= 2` grouped arm, verbatim:
48
+ // context resolves for ANY /apps/:appId[/...] request before the method and
49
+ // operation checks, and an unmatched combination falls through to not-found.
50
+ route("*", "/apps/:appId/*", async (wire) => {
51
+ const { request, deps, params, segments } = wire;
52
+ const appId = string(params["appId"], "app id");
53
+ const ctx = await wire.context("app");
54
+ const operation = segments[2];
55
+ if (segments.length === 2) {
56
+ if (request.method === "GET") {
57
+ const app = await deps.apps.get(appId, ctx);
58
+ if (app === null)
59
+ throw new VendoError("not-found", `app not found: ${appId}`);
60
+ return json(app);
61
+ }
62
+ if (request.method === "DELETE") {
63
+ await deps.apps.delete(appId, ctx);
64
+ return json({});
65
+ }
66
+ }
67
+ if (request.method === "GET" && operation === "open" && segments.length === 3) {
68
+ // Existing-agents polish — the embed's build-window poll. The app record
69
+ // lands only at build completion, so until then open() (and the meta
70
+ // route alike) answers not-found, and every 1.2s poll logged a browser
71
+ // console 404. Under the additive ?pending=1 flag, ONLY that expected
72
+ // pre-servable miss becomes a quiet 200 {kind:"pending"}; unflagged
73
+ // callers keep the contracted 404, and every other failure keeps its
74
+ // envelope and status either way.
75
+ if (wire.url.searchParams.get("pending") === "1") {
76
+ try {
77
+ return json(await deps.apps.open(appId, ctx));
78
+ }
79
+ catch (reason) {
80
+ if (reason instanceof VendoError && reason.code === "not-found") {
81
+ return json({ kind: "pending" });
82
+ }
83
+ throw reason;
84
+ }
85
+ }
86
+ return json(await deps.apps.open(appId, ctx));
87
+ }
88
+ if (request.method === "POST" && operation === "call" && segments.length === 3) {
89
+ const body = await requestJson(request);
90
+ return json(await deps.apps.call(appId, string(body["ref"], "ref"), body["args"], ctx));
91
+ }
92
+ if (request.method === "POST" && operation === "edit" && segments.length === 3) {
93
+ const body = await requestJson(request);
94
+ return json(await deps.apps.edit(appId, string(body["instruction"], "instruction"), ctx));
95
+ }
96
+ if (operation === "history" && segments.length === 3) {
97
+ if (await deps.apps.get(appId, ctx) === null)
98
+ throw new VendoError("not-found", `app not found: ${appId}`);
99
+ if (request.method === "GET")
100
+ return json(await deps.apps.history(appId).list());
101
+ if (request.method === "POST") {
102
+ const body = await requestJson(request);
103
+ if (body["op"] !== "undo")
104
+ throw new VendoError("validation", "history op must be undo");
105
+ return json(await deps.apps.history(appId).undo());
106
+ }
107
+ }
108
+ // 06-apps §8–§9 — additive: the reviewable diff of what this app ships
109
+ // relative to the captured host baselines, hash-pinned to the version
110
+ // an in-client approval would cover. Owner-scoped like every app route.
111
+ if (request.method === "GET" && operation === "ship-diff" && segments.length === 3) {
112
+ return json(await deps.apps.inClient.shipDiff(appId, ctx));
113
+ }
114
+ // 06-apps §8 — additive drift→rebase surface, owner-scoped like every
115
+ // app route. A rebase rewrites content, so it is only ever invoked
116
+ // explicitly here or via the vendo_apps_rebase_pin agent tool — drift
117
+ // detection never auto-rebases.
118
+ if (request.method === "GET" && operation === "pin-drift" && segments.length === 3) {
119
+ return json(await deps.apps.pins.drift(appId, ctx));
120
+ }
121
+ if (request.method === "POST" && operation === "rebase-pin" && segments.length === 3) {
122
+ const body = await requestJson(request);
123
+ return json(await deps.apps.pins.rebase({ appId, slot: string(body["slot"], "slot") }, ctx));
124
+ }
125
+ // 06-apps §8 — the same gesture fork landing in an EXISTING app (the
126
+ // filled-slot / driver surface). Owner-scoped like every app route.
127
+ if (request.method === "POST" && operation === "fork-pin" && segments.length === 3) {
128
+ const body = await requestJson(request);
129
+ return json(await deps.apps.pins.fork({
130
+ appId,
131
+ slot: string(body["slot"], "slot"),
132
+ ...(body["instruction"] === undefined ? {} : { instruction: string(body["instruction"], "instruction") }),
133
+ }, ctx));
134
+ }
135
+ // Wave 7 H2 — the embed surface's keepalive: user activity on an embedded
136
+ // served app rides one host-proxied HEAD through the machine (re-arming
137
+ // the idle timer); "woke" tells the embed its URL is stale — re-open.
138
+ if (request.method === "POST" && operation === "machine" && segments[3] === "ping" && segments.length === 4) {
139
+ return json(await deps.apps.machine.ping(appId, ctx));
140
+ }
141
+ if (request.method === "GET" && operation === "export" && segments.length === 3) {
142
+ const bytes = await deps.apps.exportApp(appId, ctx);
143
+ return new Response(bytes, {
144
+ headers: {
145
+ "content-type": "application/octet-stream",
146
+ "content-disposition": `attachment; filename="${appId}.vendoapp"`,
147
+ },
148
+ });
149
+ }
150
+ if (request.method === "POST" && operation === "fork" && segments.length === 3) {
151
+ return json(await deps.apps.fork(appId, ctx));
152
+ }
153
+ return undefined;
154
+ }),
155
+ ];
@@ -0,0 +1,5 @@
1
+ import { type RouteEntry } from "./shared.js";
2
+ /** 07-automations / 09 §3 — the /automations wire area. */
3
+ export declare const automationRoutes: RouteEntry[];
4
+ /** 07-automations / 09 §3 — the /runs wire area. */
5
+ export declare const runRoutes: RouteEntry[];
@@ -0,0 +1,57 @@
1
+ import { VendoError } from "@vendoai/core";
2
+ import { json, route, string } from "./shared.js";
3
+ /** 07-automations / 09 §3 — the /automations wire area. */
4
+ export const automationRoutes = [
5
+ route("GET", "/automations", async ({ deps, context }) => {
6
+ return json(await deps.automations.list(await context("automation")));
7
+ }),
8
+ // Grouped like the old if-chain arm (`segments.length === 3 && POST`):
9
+ // context resolves before the operation check, and an unknown operation
10
+ // falls through to the table's not-found.
11
+ route("POST", "/automations/:appId/:op", async ({ deps, context, params, segments }) => {
12
+ const appId = string(params["appId"], "app id");
13
+ const ctx = await context("automation");
14
+ if (segments[2] === "enable")
15
+ return json(await deps.automations.enable(appId, ctx));
16
+ if (segments[2] === "disable") {
17
+ await deps.automations.disable(appId, ctx);
18
+ return json({});
19
+ }
20
+ if (segments[2] === "dry-run")
21
+ return json(await deps.automations.dryRun(appId, ctx));
22
+ return undefined;
23
+ }),
24
+ ];
25
+ /** 07-automations / 09 §3 — the /runs wire area. */
26
+ export const runRoutes = [
27
+ route("GET", "/runs", async ({ url, deps, context }) => {
28
+ const status = url.searchParams.get("status") ?? undefined;
29
+ const allowed = ["running", "ok", "error", "stopped", "pending-approval"];
30
+ if (status !== undefined && !allowed.includes(status)) {
31
+ throw new VendoError("validation", "run status is invalid");
32
+ }
33
+ const filter = {
34
+ ...(url.searchParams.get("appId") === null ? {} : { appId: url.searchParams.get("appId") }),
35
+ ...(status === undefined ? {} : { status: status }),
36
+ ...(url.searchParams.get("cursor") === null ? {} : { cursor: url.searchParams.get("cursor") }),
37
+ };
38
+ return json(await deps.automations.runs.list(filter, await context("automation")));
39
+ }),
40
+ // Grouped like the old `head === "runs" && segments.length >= 2` arm: ANY
41
+ // method/depth resolves context first; unmatched shapes fall through.
42
+ route("*", "/runs/:runId/*", async ({ request, deps, context, params, segments }) => {
43
+ const ctx = await context("automation");
44
+ const runId = string(params["runId"], "run id");
45
+ if (request.method === "GET" && segments.length === 2) {
46
+ const run = await deps.automations.runs.get(runId, ctx);
47
+ if (run === null)
48
+ throw new VendoError("not-found", `run not found: ${runId}`);
49
+ return json(run);
50
+ }
51
+ if (request.method === "POST" && segments[2] === "stop" && segments.length === 3) {
52
+ await deps.automations.runs.stop(runId, ctx);
53
+ return json({});
54
+ }
55
+ return undefined;
56
+ }),
57
+ ];
@@ -0,0 +1,3 @@
1
+ import { type RouteEntry } from "./shared.js";
2
+ export declare const fnProxyRoutes: RouteEntry[];
3
+ export declare const boxRoutes: RouteEntry[];