@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,229 @@
1
+ import { supabasePreset, verifyHs256 } from "@vendoai/actions/presets";
2
+ import { environment } from "../wire/shared.js";
3
+ import { actAsClaimsFromUser, bearerToken, claimString, composeHostAuthPreset, lazyActAs, lazyModule, loginRedirect, makeUserResolver, requestCookies, resolvePresetSecret, } from "./identity.js";
4
+ const MISSING_VERIFIER_MESSAGE = "supabase() has no way to verify sessions: set SUPABASE_JWT_SECRET (the project's legacy JWT signing secret — verifies HS256 access tokens offline; the same one supabasePreset mints with, never the anon key) and/or SUPABASE_URL (the project URL — ES256 logins verify against its GoTrue JWKS at /auth/v1/.well-known/jwks.json), or pass supabase({ secret }) / supabase({ jwks }).";
5
+ /** Same optional-dependency strategy as auth0's jose: ES256 login tokens
6
+ (Supabase's newer signing keys) verify against GoTrue's JWKS through jose,
7
+ an optional peerDependency loaded lazily on first use. HS256-only hosts
8
+ never hit this path and need nothing installed. */
9
+ const MISSING_JOSE_MESSAGE = "supabase() verifies ES256 Supabase session tokens through jose, which is not installed. Install it next to your Supabase setup: npm install jose";
10
+ const loadJose = lazyModule(() => import("jose").then((module) => module), MISSING_JOSE_MESSAGE);
11
+ /** jose's remote JWKS resolvers cache fetched keys per instance — keep one per
12
+ URL so verification never refetches per request (auth0's pattern). */
13
+ const jwksByUrl = new Map();
14
+ function remoteJwks(jose, url) {
15
+ let jwks = jwksByUrl.get(url);
16
+ if (jwks === undefined) {
17
+ jwks = jose.createRemoteJWKSet(new URL(url));
18
+ jwksByUrl.set(url, jwks);
19
+ }
20
+ return jwks;
21
+ }
22
+ /** Supabase's auth cookie: `sb-<project-ref>-auth-token`, optionally chunked
23
+ across `.0`, `.1`, ... suffixes by @supabase/ssr. */
24
+ const SUPABASE_AUTH_COOKIE = /^(sb-.+-auth-token)(?:\.(\d+))?$/;
25
+ /** A compact JWS: three non-empty base64url segments. */
26
+ const RAW_JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
27
+ function decodeBase64Url(value) {
28
+ if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1)
29
+ return undefined;
30
+ const padding = "=".repeat((4 - (value.length % 4)) % 4);
31
+ try {
32
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/") + padding);
33
+ return new TextDecoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0)));
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ /** Extract the access token from a reassembled cookie value. Four shapes ship
40
+ in the wild: @supabase/ssr's `base64-` + base64url(JSON session), the
41
+ legacy plain-JSON session object / `[access_token, ...]` array, and the
42
+ raw access token itself (hand-rolled hosts, the old auth-helpers). */
43
+ function accessTokenFrom(value) {
44
+ if (RAW_JWT.test(value))
45
+ return value;
46
+ const raw = value.startsWith("base64-") ? decodeBase64Url(value.slice("base64-".length)) : value;
47
+ if (raw === undefined)
48
+ return undefined;
49
+ let session;
50
+ try {
51
+ session = JSON.parse(raw);
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ if (Array.isArray(session)) {
57
+ const token = session[0];
58
+ return typeof token === "string" && token.length > 0 ? token : undefined;
59
+ }
60
+ if (session !== null && typeof session === "object") {
61
+ const token = session.access_token;
62
+ return typeof token === "string" && token.length > 0 ? token : undefined;
63
+ }
64
+ return undefined;
65
+ }
66
+ /** The session token off a plain Request, per Supabase's own conventions:
67
+ `Authorization: Bearer <access token>` (how Supabase clients call the API —
68
+ and what the actAs half mints), else the `sb-*-auth-token` cookie with its
69
+ chunks reassembled in order. */
70
+ function sessionTokenFrom(request) {
71
+ const fromHeader = bearerToken(request);
72
+ if (fromHeader !== undefined)
73
+ return fromHeader;
74
+ const chunks = new Map();
75
+ for (const [name, value] of requestCookies(request)) {
76
+ const match = SUPABASE_AUTH_COOKIE.exec(name);
77
+ if (match === null)
78
+ continue;
79
+ const base = match[1];
80
+ const parts = chunks.get(base) ?? [];
81
+ parts.push({ index: match[2] === undefined ? 0 : Number(match[2]), value });
82
+ chunks.set(base, parts);
83
+ }
84
+ for (const parts of chunks.values()) {
85
+ const joined = parts.sort((left, right) => left.index - right.index).map((part) => part.value).join("");
86
+ const token = accessTokenFrom(joined);
87
+ if (token !== undefined)
88
+ return token;
89
+ }
90
+ return undefined;
91
+ }
92
+ /** The token's protected-header `alg`, decoded without verification — only to
93
+ route the token to the verifier that can possibly accept it. */
94
+ function tokenAlg(token) {
95
+ const header = decodeBase64Url(token.split(".")[0] ?? "");
96
+ if (header === undefined)
97
+ return undefined;
98
+ try {
99
+ const parsed = JSON.parse(header);
100
+ return parsed !== null && typeof parsed === "object" && typeof parsed.alg === "string"
101
+ ? parsed.alg
102
+ : undefined;
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
108
+ /** GoTrue's JWKS URL: the `jwks` option (thunks resolved per call), else
109
+ SUPABASE_URL's well-known path. */
110
+ function jwksUrlFrom(jwks) {
111
+ const resolved = typeof jwks === "function" ? jwks() : jwks;
112
+ if (resolved !== undefined)
113
+ return resolved.toString();
114
+ const projectUrl = environment("SUPABASE_URL");
115
+ if (projectUrl === undefined)
116
+ return undefined;
117
+ try {
118
+ return new URL("/auth/v1/.well-known/jwks.json", projectUrl).toString();
119
+ }
120
+ catch {
121
+ return undefined;
122
+ }
123
+ }
124
+ /** Supabase's claims→user defaults: identity lives in `user_metadata`
125
+ (name/full_name) with the email as a top-level claim. */
126
+ function supabaseUser(claims) {
127
+ const metadata = claims["user_metadata"];
128
+ const metadataClaims = metadata !== null && typeof metadata === "object" && !Array.isArray(metadata)
129
+ ? metadata
130
+ : {};
131
+ const email = claimString(claims, "email");
132
+ const display = claimString(metadataClaims, "name")
133
+ ?? claimString(metadataClaims, "full_name")
134
+ ?? claimString(claims, "name")
135
+ ?? email;
136
+ return {
137
+ ...(display === undefined ? {} : { display }),
138
+ ...(email === undefined ? {} : { email }),
139
+ };
140
+ }
141
+ /**
142
+ * 09-vendo §2.1 — the Supabase Auth host-identity preset. Zero-argument in
143
+ * the standard case: it reads Supabase's own env, the session resolves off a
144
+ * plain Request per Supabase's own formats (Authorization: Bearer, or the
145
+ * `sb-*-auth-token` cookie — @supabase/ssr's `base64-`/chunked shape, the
146
+ * legacy JSON shapes, and the raw access token), and display derives from
147
+ * user_metadata.name/full_name/email. The optional subject→user resolver has
148
+ * the same semantics as authJs (null = subject unknown → decline/null).
149
+ *
150
+ * Session verification is Supabase's documented HYBRID (the same one a
151
+ * project with JWT signing keys needs), routed by the token's own `alg`:
152
+ *
153
+ * 1. HS256 first — the project's legacy JWT secret (SUPABASE_JWT_SECRET or
154
+ * `secret`), verified OFFLINE through the SAME shared `verifyHs256` the
155
+ * minting half targets: no network, no optional SDK, and every actAs-minted
156
+ * away token stays verifiable with no Supabase stack running.
157
+ * 2. ES256 fallback — what `supabase start` ≥ v2.71 and hosted projects on
158
+ * the new key system sign interactive logins with, verified against
159
+ * GoTrue's JWKS (SUPABASE_URL → /auth/v1/.well-known/jwks.json, or the
160
+ * `jwks` option) through a lazily-imported jose (optional peer, cached
161
+ * remote key set per URL).
162
+ *
163
+ * Both paths enforce Supabase's `authenticated` audience, which is also what
164
+ * keeps the project's API keys (anon/service_role — HS256 JWTs under the same
165
+ * secret, but without that audience) from ever counting as a signed-in user.
166
+ * Tokens neither path can verify resolve to null (no session); construction
167
+ * with NEITHER a secret NOR a JWKS source fails loud, naming both.
168
+ *
169
+ * The actAs half IS the shipped `@vendoai/actions/presets` supabasePreset —
170
+ * one minting story (04 §2.1), configured from these same options. Tokens
171
+ * mint (and verify) under Supabase's `authenticated` audience convention.
172
+ */
173
+ export function supabase(options = {}) {
174
+ const { secret, user, jwks } = options;
175
+ const sessionClaims = async (request) => {
176
+ const token = sessionTokenFrom(request);
177
+ if (token === undefined)
178
+ return null;
179
+ const jwtSecret = secret === undefined && environment("SUPABASE_JWT_SECRET") === undefined
180
+ ? undefined
181
+ : await resolvePresetSecret(secret, "SUPABASE_JWT_SECRET", MISSING_VERIFIER_MESSAGE);
182
+ const jwksUrl = jwksUrlFrom(jwks);
183
+ if (jwtSecret === undefined && jwksUrl === undefined) {
184
+ throw new Error(MISSING_VERIFIER_MESSAGE);
185
+ }
186
+ const alg = tokenAlg(token);
187
+ // HS256 first: offline, no network — and the only path a JWKS could never
188
+ // serve. ES256 is the fallback for logins under the newer signing keys.
189
+ if (alg === "HS256" && jwtSecret !== undefined) {
190
+ try {
191
+ return (await verifyHs256(token, jwtSecret, { audience: "authenticated" })).payload;
192
+ }
193
+ catch {
194
+ return null; // unverifiable/expired/foreign token = no session
195
+ }
196
+ }
197
+ if (alg === "ES256" && jwksUrl !== undefined) {
198
+ const jose = await loadJose();
199
+ try {
200
+ const { payload } = await jose.jwtVerify(token, remoteJwks(jose, jwksUrl), {
201
+ audience: "authenticated",
202
+ algorithms: ["ES256"],
203
+ });
204
+ return payload;
205
+ }
206
+ catch {
207
+ return null; // unverifiable/expired/foreign token = no session
208
+ }
209
+ }
210
+ return null; // an alg (or config) neither verifier serves = no session
211
+ };
212
+ return composeHostAuthPreset({
213
+ sessionClaims,
214
+ resolveUser: makeUserResolver(user, supabaseUser),
215
+ // Away + MCP execution: the shipped Supabase minting preset (04 §2.1),
216
+ // fed the same secret and identity this preset resolves sessions with —
217
+ // user identity rides the token as Supabase's own claim shape.
218
+ actAs: lazyActAs(() => supabasePreset({
219
+ ...(secret === undefined ? {} : { secret }),
220
+ ...(user === undefined ? {} : {
221
+ claims: actAsClaimsFromUser(user, (resolved) => ({
222
+ ...(resolved.email === undefined ? {} : { email: resolved.email }),
223
+ ...(resolved.display === undefined ? {} : { user_metadata: { name: resolved.display } }),
224
+ })),
225
+ }),
226
+ })),
227
+ login: (request, returnTo) => loginRedirect(request, returnTo),
228
+ });
229
+ }
@@ -0,0 +1,37 @@
1
+ import { type ApprovalRequest, type Principal, type StoreAdapter, type ToolOutcome, type ToolRegistry } from "@vendoai/core";
2
+ import type { VendoGuard } from "@vendoai/guard";
3
+ /** The wire's answer to `GET /approvals/:id` — the frozen
4
+ * `VendoApprovalEmbedState` vocabulary, plus what each state needs to render:
5
+ * the full request while pending (the consent card shows real inputs), the
6
+ * executed outcome after resume. */
7
+ export type ByoApprovalResolution = {
8
+ state: "pending";
9
+ request: ApprovalRequest;
10
+ } | {
11
+ state: "executed";
12
+ outcome: ToolOutcome;
13
+ } | {
14
+ state: "declined";
15
+ } | {
16
+ state: "expired";
17
+ };
18
+ export interface ByoApprovals {
19
+ /** The guard-bound registry with approval parking — the registry the BYO
20
+ * tool pack executes through. Same decisions, same audit; the only
21
+ * addition is the parked record behind a `pending-approval` outcome. */
22
+ registry: ToolRegistry;
23
+ /** Resolve one approval's state for its owner; not-found for unknown or
24
+ * foreign ids (indistinguishable on purpose). */
25
+ read(approvalId: string, principal: Principal): Promise<ByoApprovalResolution>;
26
+ /** Deny every parked call idle past `ttlMs` through the existing
27
+ * abandonment path. No-op when `ttlMs` is 0 or negative. */
28
+ sweepExpired(ttlMs: number, now?: number): Promise<void>;
29
+ }
30
+ export interface ByoApprovalsConfig {
31
+ guard: VendoGuard;
32
+ /** The guard-bound registry (the SAME binding chat, apps, and automations
33
+ * execute through) — both the parked call and its resume dispatch ride it. */
34
+ tools: ToolRegistry;
35
+ store: StoreAdapter;
36
+ }
37
+ export declare function createByoApprovals({ guard, tools, store }: ByoApprovalsConfig): ByoApprovals;
@@ -0,0 +1,189 @@
1
+ import { VendoError, } from "@vendoai/core";
2
+ /**
3
+ * Existing-agents Lane B — parked guarded calls with NO Vendo thread and NO app.
4
+ *
5
+ * A `vendo_*` pack tool executing in a BYO agent loop returns the
6
+ * `vendo/approval-ref@1` envelope the instant the guard answers
7
+ * `pending-approval` — no throw, no block. But nothing in the host's loop ever
8
+ * re-dispatches the call: the thread resume path (`data-vendo-approval` stream
9
+ * parts) needs Vendo's conversation, and the apps runtime's `ParkedAction`
10
+ * pins an `appId` and lives with the app. This seam is the venue-neutral
11
+ * third venue, riding the same three existing mechanisms end to end:
12
+ *
13
+ * - PARK: the {@link ByoApprovals.registry} decorator records the EXACT call
14
+ * (guard-minted id, tool, args) plus its `RunContext` when a guarded execute
15
+ * returns `pending-approval` — the same shape as `ParkedAction`, minus the
16
+ * app pin.
17
+ * - RESUME: an umbrella-level `guard.onApprovalDecision` subscriber (the SAME
18
+ * seam the apps runtime and automations ride) re-dispatches the parked call
19
+ * byte-for-byte through the guard-bound registry on approve — the guard's
20
+ * one-shot approved replay pins subject, call id, args hash, descriptor
21
+ * hash, venue, presence, and appId, so the stored ctx is reused verbatim.
22
+ * Deny clears the record and never executes (fail closed).
23
+ * - EXPIRE: {@link ByoApprovals.sweepExpired} denies parked calls older than
24
+ * the TTL through the existing abandonment path (`guard.abandonApprovals`
25
+ * semantics: deny + clear, idempotent) — a new trigger, not new semantics.
26
+ *
27
+ * The resume outcome persists keyed by approvalId so the wire can answer
28
+ * "what happened to apr_x?" for `<VendoApprovalEmbed>` — in-thread that answer
29
+ * rides the thread stream; there is no thread here.
30
+ */
31
+ const PARKED_COLLECTION = "vendo_parked_call";
32
+ const OUTCOME_COLLECTION = "vendo_parked_call_outcome";
33
+ function now() {
34
+ return new Date().toISOString();
35
+ }
36
+ function cloneJson(value) {
37
+ return globalThis.structuredClone(value);
38
+ }
39
+ async function listAll(store) {
40
+ const records = [];
41
+ let cursor;
42
+ do {
43
+ const page = await store.list({ ...(cursor === undefined ? {} : { cursor }) });
44
+ records.push(...page.records);
45
+ if (page.cursor === undefined || page.cursor === cursor)
46
+ break;
47
+ cursor = page.cursor;
48
+ } while (cursor !== undefined);
49
+ return records;
50
+ }
51
+ export function createByoApprovals({ guard, tools, store }) {
52
+ const parked = store.records(PARKED_COLLECTION);
53
+ const outcomes = store.records(OUTCOME_COLLECTION);
54
+ const putParked = async (record) => {
55
+ await parked.put({
56
+ id: record.approvalId,
57
+ data: record,
58
+ refs: { subject: record.owner, approval: record.approvalId },
59
+ });
60
+ };
61
+ const putOutcome = async (record) => {
62
+ await outcomes.put({
63
+ id: record.approvalId,
64
+ data: record,
65
+ refs: { subject: record.owner, state: record.state },
66
+ });
67
+ };
68
+ // RESUME — the decision subscriber. `decide` fires callbacks exactly once
69
+ // per approval (the pending→decided transition has a single atomic winner)
70
+ // and awaits them, so the outcome row has one writer and lands before the
71
+ // decide call returns to the wire.
72
+ guard.onApprovalDecision(async (approvalId, approved) => {
73
+ const record = await parked.get(approvalId);
74
+ if (record === null)
75
+ return;
76
+ const data = record.data;
77
+ try {
78
+ if (approved) {
79
+ // Byte-for-byte re-dispatch: the one-shot approved replay executes it;
80
+ // the guard binding folds a downstream throw into an error outcome.
81
+ const outcome = await tools.execute(data.call, data.ctx);
82
+ await putOutcome({ approvalId, owner: data.owner, state: "executed", outcome, at: now() });
83
+ }
84
+ else {
85
+ await putOutcome({
86
+ approvalId,
87
+ owner: data.owner,
88
+ state: data.expiring === true ? "expired" : "declined",
89
+ at: now(),
90
+ });
91
+ }
92
+ }
93
+ finally {
94
+ // Cleared either way: approve ran it, deny fails closed. A parked record
95
+ // exists exactly while its approval is undecided.
96
+ await parked.delete(approvalId);
97
+ }
98
+ });
99
+ // EXPIRE — abandonApprovals is the guard's idempotent deny wrapper (already-
100
+ // decided and unknown ids already hold the state abandonment wants). Older
101
+ // Guard implementations may omit the optional method; the fallback applies
102
+ // the same semantics through the plain decide path.
103
+ const abandon = async (approvalId, ctx) => {
104
+ if (guard.abandonApprovals !== undefined) {
105
+ await guard.abandonApprovals([approvalId], ctx);
106
+ return;
107
+ }
108
+ try {
109
+ await guard.approvals.decide(approvalId, { approve: false }, ctx.principal);
110
+ }
111
+ catch (error) {
112
+ if (error instanceof VendoError && (error.code === "conflict" || error.code === "not-found"))
113
+ return;
114
+ throw error;
115
+ }
116
+ };
117
+ return {
118
+ registry: {
119
+ descriptors: () => tools.descriptors(),
120
+ async execute(call, ctx) {
121
+ const outcome = await tools.execute(call, ctx);
122
+ if (outcome.status === "pending-approval") {
123
+ // PARK — written right before the pack tool returns the
124
+ // vendo/approval-ref@1 envelope to the foreign loop. The request
125
+ // snapshot keeps `read` answering "pending" through the resume
126
+ // window, after the decision has already left the guard's queue.
127
+ const requests = await guard.approvals.pending(ctx.principal);
128
+ const request = requests.find((candidate) => candidate.id === outcome.approvalId);
129
+ await putParked({
130
+ approvalId: outcome.approvalId,
131
+ owner: ctx.principal.subject,
132
+ call: cloneJson(call),
133
+ ctx: cloneJson(ctx),
134
+ parkedAt: now(),
135
+ ...(request === undefined ? {} : { request: cloneJson(request) }),
136
+ });
137
+ }
138
+ return outcome;
139
+ },
140
+ },
141
+ async read(approvalId, principal) {
142
+ const record = await outcomes.get(approvalId);
143
+ if (record !== null) {
144
+ const data = record.data;
145
+ if (data.owner === principal.subject) {
146
+ if (data.state === "executed" && data.outcome !== undefined) {
147
+ return { state: "executed", outcome: data.outcome };
148
+ }
149
+ if (data.state === "declined" || data.state === "expired") {
150
+ return { state: data.state };
151
+ }
152
+ }
153
+ }
154
+ const pending = await guard.approvals.pending(principal);
155
+ const request = pending.find((candidate) => candidate.id === approvalId);
156
+ if (request !== undefined)
157
+ return { state: "pending", request };
158
+ // Mid-resume window: the decision already left the guard's pending queue
159
+ // but the subscriber has not written the outcome row yet. The parked
160
+ // record exists exactly until that write, so serve its request snapshot
161
+ // as still-pending rather than a terminal not-found (which the embed
162
+ // renders as expired and stops polling).
163
+ const stillParked = await parked.get(approvalId);
164
+ if (stillParked !== null) {
165
+ const data = stillParked.data;
166
+ if (data.owner === principal.subject && data.request !== undefined) {
167
+ return { state: "pending", request: data.request };
168
+ }
169
+ }
170
+ throw new VendoError("not-found", `Approval ${approvalId} was not found`);
171
+ },
172
+ async sweepExpired(ttlMs, at = Date.now()) {
173
+ if (ttlMs <= 0)
174
+ return;
175
+ for (const record of await listAll(parked)) {
176
+ const data = record.data;
177
+ const parkedAt = Date.parse(data.parkedAt);
178
+ if (Number.isFinite(parkedAt) && parkedAt + ttlMs > at)
179
+ continue;
180
+ // Mark first, so the deny lands as "expired" — the subscriber is the
181
+ // outcome's single writer and reads the flag when the decision fires.
182
+ // A concurrent user approve that wins the atomic decide still executes
183
+ // and records "executed"; this abandon then no-ops (conflict).
184
+ await putParked({ ...data, expiring: true });
185
+ await abandon(data.approvalId, data.ctx);
186
+ }
187
+ },
188
+ };
189
+ }
@@ -0,0 +1,38 @@
1
+ import { type CapabilityMissEvent, type RiskLabel, type ToolDescriptor } from "@vendoai/core";
2
+ import { type TelemetryConfig } from "@vendoai/telemetry";
3
+ export interface CapabilitySurfaceSnapshot {
4
+ hash: string;
5
+ tools: Array<{
6
+ name: string;
7
+ risk: RiskLabel;
8
+ }>;
9
+ }
10
+ interface AppendOptions {
11
+ dataDir?: string;
12
+ }
13
+ type AppendMiss = (event: CapabilityMissEvent) => Promise<void>;
14
+ interface CaptureOptions {
15
+ dataDir?: string;
16
+ env?: Record<string, string | undefined>;
17
+ telemetryHome?: string;
18
+ telemetryConfig?: Pick<TelemetryConfig, "anonymousId" | "optedOut">;
19
+ surface: Promise<CapabilitySurfaceSnapshot>;
20
+ append?: AppendMiss;
21
+ fetchImpl?: typeof fetch;
22
+ batchSize?: number;
23
+ queueLimit?: number;
24
+ batchDelayMs?: number;
25
+ requestTimeoutMs?: number;
26
+ retryDelaysMs?: readonly number[];
27
+ }
28
+ export interface CapabilityMissCapture {
29
+ /** Stable host-installation identity, shared with telemetry by contract. */
30
+ hostId: string;
31
+ record(event: CapabilityMissEvent): void;
32
+ /** Drain hook for tests and orderly host shutdown; agent turns never await it. */
33
+ flush(): Promise<void>;
34
+ }
35
+ export declare function appendCapabilityMiss(event: CapabilityMissEvent, options?: AppendOptions): Promise<void>;
36
+ export declare function capabilitySurfaceSnapshot(descriptors: ToolDescriptor[]): CapabilitySurfaceSnapshot;
37
+ export declare function createCapabilityMissCapture(options: CaptureOptions): CapabilityMissCapture;
38
+ export {};
@@ -0,0 +1,172 @@
1
+ import { canonicalJson, sha256Hex, } from "@vendoai/core";
2
+ import { envOptOut, loadConfig } from "@vendoai/telemetry";
3
+ import { cloudFetch } from "./cli/cloud/client.js";
4
+ const DEFAULT_DATA_DIR = ".vendo/data";
5
+ const DEFAULT_BATCH_SIZE = 100;
6
+ const DEFAULT_QUEUE_LIMIT = 1_000;
7
+ const DEFAULT_BATCH_DELAY_MS = 250;
8
+ const DEFAULT_REQUEST_TIMEOUT_MS = 1_500;
9
+ const DEFAULT_RETRY_DELAYS_MS = [250, 1_000];
10
+ function runtimeEnv() {
11
+ return typeof process === "undefined" ? {} : process.env;
12
+ }
13
+ function nodeFs() {
14
+ const proc = globalThis.process;
15
+ const fs = proc?.getBuiltinModule?.("node:fs");
16
+ if (!fs)
17
+ throw new Error("Capability-miss local persistence requires the Node filesystem");
18
+ return fs;
19
+ }
20
+ export async function appendCapabilityMiss(event, options = {}) {
21
+ const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
22
+ const fs = nodeFs();
23
+ await fs.promises.mkdir(dataDir, { recursive: true });
24
+ // appendFile opens with O_APPEND. Each event is serialized into one write so
25
+ // concurrent processes cannot race a read/modify/write cycle.
26
+ await fs.promises.appendFile(`${dataDir.replace(/[\\/]$/, "")}/misses.jsonl`, `${JSON.stringify(event)}\n`, { encoding: "utf8", flag: "a" });
27
+ }
28
+ export function capabilitySurfaceSnapshot(descriptors) {
29
+ const tools = descriptors
30
+ .map(({ name, risk }) => ({ name, risk }))
31
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
32
+ const canonical = canonicalJson({ format: "vendo/tools@1", tools });
33
+ return { hash: `sha256:${sha256Hex(canonical)}`, tools };
34
+ }
35
+ function delay(ms) {
36
+ return new Promise((resolve) => {
37
+ const timer = setTimeout(resolve, ms);
38
+ unrefTimer(timer);
39
+ });
40
+ }
41
+ function unrefTimer(timer) {
42
+ if (typeof timer === "object" && timer !== null && "unref" in timer) {
43
+ timer.unref?.();
44
+ }
45
+ }
46
+ function validUploadResponse(value) {
47
+ if (typeof value !== "object" || value === null)
48
+ return false;
49
+ const response = value;
50
+ return Number.isInteger(response.accepted) && Number.isInteger(response.duplicates);
51
+ }
52
+ function createMissUploader(options) {
53
+ const queue = [];
54
+ let timer;
55
+ let active;
56
+ const send = async (events) => {
57
+ for (let attempt = 0; attempt <= options.retryDelaysMs.length; attempt += 1) {
58
+ const controller = new AbortController();
59
+ const timeout = setTimeout(() => controller.abort(), options.requestTimeoutMs);
60
+ unrefTimer(timeout);
61
+ try {
62
+ const surface = await options.surface;
63
+ const response = await cloudFetch("/api/v1/misses", {
64
+ auth: "key",
65
+ apiKey: options.apiKey,
66
+ env: options.env,
67
+ fetchImpl: options.fetchImpl,
68
+ signal: controller.signal,
69
+ body: { surface, events },
70
+ });
71
+ if (!validUploadResponse(response))
72
+ throw new Error("Invalid capability-miss upload response");
73
+ return;
74
+ }
75
+ catch {
76
+ const retryDelay = options.retryDelaysMs[attempt];
77
+ if (retryDelay === undefined)
78
+ return;
79
+ await delay(retryDelay);
80
+ }
81
+ finally {
82
+ clearTimeout(timeout);
83
+ }
84
+ }
85
+ };
86
+ const drain = async () => {
87
+ while (queue.length > 0) {
88
+ const batch = queue.splice(0, options.batchSize);
89
+ await send(batch);
90
+ }
91
+ };
92
+ const flush = async () => {
93
+ if (timer !== undefined) {
94
+ clearTimeout(timer);
95
+ timer = undefined;
96
+ }
97
+ if (active) {
98
+ await active;
99
+ if (queue.length === 0)
100
+ return;
101
+ }
102
+ active = drain().finally(() => {
103
+ active = undefined;
104
+ });
105
+ await active;
106
+ };
107
+ const schedule = () => {
108
+ if (timer !== undefined || active !== undefined)
109
+ return;
110
+ timer = setTimeout(() => {
111
+ timer = undefined;
112
+ void flush().catch(() => undefined);
113
+ }, options.batchDelayMs);
114
+ unrefTimer(timer);
115
+ };
116
+ return {
117
+ enqueue(event) {
118
+ if (queue.length >= options.queueLimit)
119
+ return;
120
+ queue.push(event);
121
+ if (queue.length >= options.batchSize)
122
+ void flush().catch(() => undefined);
123
+ else
124
+ schedule();
125
+ },
126
+ flush,
127
+ };
128
+ }
129
+ export function createCapabilityMissCapture(options) {
130
+ const env = options.env ?? runtimeEnv();
131
+ const telemetryConfig = options.telemetryConfig
132
+ ?? loadConfig(options.telemetryHome, env);
133
+ const apiKey = env.VENDO_API_KEY?.trim();
134
+ let uploader;
135
+ if (apiKey) {
136
+ // Contract (01-core §17): upload is gated by the key plus envOptOut only.
137
+ // The persisted telemetry opt-out and the NODE_ENV fail-close are
138
+ // product-telemetry-only; a non-empty VENDO_API_KEY is the host's opt-in.
139
+ if (!envOptOut(env)) {
140
+ uploader = createMissUploader({
141
+ apiKey,
142
+ env,
143
+ surface: options.surface,
144
+ fetchImpl: options.fetchImpl,
145
+ batchSize: options.batchSize ?? DEFAULT_BATCH_SIZE,
146
+ queueLimit: options.queueLimit ?? DEFAULT_QUEUE_LIMIT,
147
+ batchDelayMs: options.batchDelayMs ?? DEFAULT_BATCH_DELAY_MS,
148
+ requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
149
+ retryDelaysMs: options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
150
+ });
151
+ }
152
+ }
153
+ const append = options.append
154
+ ?? ((event) => appendCapabilityMiss(event, { dataDir: options.dataDir }));
155
+ const pendingLocal = new Set();
156
+ return {
157
+ hostId: telemetryConfig.anonymousId,
158
+ record(event) {
159
+ const local = Promise.resolve()
160
+ .then(() => append(event))
161
+ .catch(() => undefined)
162
+ .finally(() => pendingLocal.delete(local));
163
+ pendingLocal.add(local);
164
+ uploader?.enqueue(event);
165
+ },
166
+ async flush() {
167
+ while (pendingLocal.size > 0)
168
+ await Promise.all([...pendingLocal]);
169
+ await uploader?.flush();
170
+ },
171
+ };
172
+ }