openzoo 0.51.4 → 0.51.6

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 (55) hide show
  1. package/README.md +12 -1
  2. package/bin/openzoo.js +21 -46
  3. package/lib/chatgpt-install.js +106 -0
  4. package/lib/chatgpt.js +108 -90
  5. package/lib/config.js +34 -0
  6. package/lib/modelcommand.js +443 -0
  7. package/lib/pay.js +60 -4
  8. package/lib/proxy.js +39 -0
  9. package/package.json +4 -7
  10. package/transmute/README.md +0 -385
  11. package/transmute/bin/openzoo-transmute.js +0 -7
  12. package/transmute/docs/OPENZOO_CLI_PATCH.md +0 -139
  13. package/transmute/docs/VERCEL_TO_SOLANA.md +0 -381
  14. package/transmute/lib/build.js +0 -381
  15. package/transmute/lib/cli.js +0 -293
  16. package/transmute/lib/compile/bytecode.js +0 -591
  17. package/transmute/lib/compile/hostids.js +0 -11
  18. package/transmute/lib/compile/index.js +0 -159
  19. package/transmute/lib/compile/ir.js +0 -1634
  20. package/transmute/lib/compile/parse.js +0 -753
  21. package/transmute/lib/compile/rust.js +0 -781
  22. package/transmute/lib/deploy.js +0 -357
  23. package/transmute/lib/eligibility.js +0 -211
  24. package/transmute/lib/gateway.js +0 -710
  25. package/transmute/lib/hub.js +0 -290
  26. package/transmute/lib/solana.js +0 -387
  27. package/transmute/lib/vercel.js +0 -296
  28. package/transmute/lib/wallet.js +0 -41
  29. package/transmute/lib/wire.js +0 -208
  30. package/transmute/prebuilt/zoo_vm.v0.so +0 -0
  31. package/transmute/runtime/zoo-host/Cargo.lock +0 -588
  32. package/transmute/runtime/zoo-host/Cargo.toml +0 -30
  33. package/transmute/runtime/zoo-host/examples/vmrun.rs +0 -43
  34. package/transmute/runtime/zoo-host/src/assets.rs +0 -214
  35. package/transmute/runtime/zoo-host/src/ctx.rs +0 -350
  36. package/transmute/runtime/zoo-host/src/fmt.rs +0 -188
  37. package/transmute/runtime/zoo-host/src/helpers.rs +0 -245
  38. package/transmute/runtime/zoo-host/src/json.rs +0 -263
  39. package/transmute/runtime/zoo-host/src/kv.rs +0 -206
  40. package/transmute/runtime/zoo-host/src/lib.rs +0 -137
  41. package/transmute/runtime/zoo-host/src/site.rs +0 -94
  42. package/transmute/runtime/zoo-host/src/val.rs +0 -1050
  43. package/transmute/runtime/zoo-host/src/vm.rs +0 -902
  44. package/transmute/runtime/zoo-host/src/wire.rs +0 -239
  45. package/transmute/runtime/zoo-vm/Cargo.lock +0 -596
  46. package/transmute/runtime/zoo-vm/Cargo.toml +0 -23
  47. package/transmute/runtime/zoo-vm/src/lib.rs +0 -92
  48. package/transmute/site/.env.production +0 -2
  49. package/transmute/site/api/echo.js +0 -10
  50. package/transmute/site/api/hello.js +0 -12
  51. package/transmute/site/api/hits.js +0 -13
  52. package/transmute/site/api/time.js +0 -5
  53. package/transmute/site/package.json +0 -6
  54. package/transmute/site/public/app.js +0 -16
  55. package/transmute/site/public/index.html +0 -96
@@ -1,296 +0,0 @@
1
- // The Vercel deployment model, in Vercel's own terms.
2
- //
3
- // `vercel build` produces the Build Output API v3 tree:
4
- //
5
- // .vercel/output/
6
- // config.json { version: 3, routes, overrides, images, crons, wildcard, framework }
7
- // static/** served as-is (highest precedence after `routes` w/ handle:filesystem)
8
- // functions/<name>.func/
9
- // .vc-config.json { runtime, handler | entrypoint, launcherType, shouldAddHelpers,
10
- // maxDuration, memory, regions, environment, supportsResponseStreaming,
11
- // operationType, framework, architecture, ... }
12
- // <bundle files>
13
- // functions/<name>.prerender-config.json (ISR)
14
- //
15
- // A `nodejs*.x` function is an AWS Lambda whose handler is wrapped by Vercel's
16
- // node-bridge launcher: the Lambda receives `{Action:"Invoke", body:
17
- // JSON{method, path, headers, encoding, body}}` and returns `{statusCode,
18
- // headers, encoding, body}`. That bridge contract is what this package
19
- // re-hosts on Solana (see wire.js).
20
- //
21
- // This module reads a real `.vercel/output` when present, and otherwise
22
- // synthesizes the same model straight from a Next.js (pages/ or app/) or a
23
- // Vite-style repo with an `api/` directory — the same routes `@vercel/next`
24
- // and `@vercel/node` would emit, minus the bundling.
25
- import fs from 'node:fs';
26
- import path from 'node:path';
27
-
28
- export const NODE_RUNTIMES = /^nodejs\d+\.x$/;
29
- export const HANDLER_EXTS = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx'];
30
-
31
- /** One Lambda in Vercel terms. */
32
- export class VercelFunction {
33
- constructor(o) {
34
- this.name = o.name; // "api/hello" — the .func dir name
35
- this.routePath = o.routePath; // "/api/hello" or "/api/users/[id]"
36
- this.pattern = o.pattern; // regex source matching the route path
37
- this.params = o.params || []; // ["id"] for dynamic segments
38
- this.runtime = o.runtime || 'nodejs20.x';
39
- this.launcherType = o.launcherType || 'Nodejs';
40
- this.shouldAddHelpers = o.shouldAddHelpers ?? true; // Vercel's req.query/req.body/res.json helpers
41
- this.handler = o.handler || 'index.js'; // Lambda handler file
42
- this.entrypoint = o.entrypoint; // edge functions
43
- this.sourceFile = o.sourceFile; // absolute path to the source we transmute
44
- this.maxDuration = o.maxDuration ?? 10; // seconds (Hobby default)
45
- this.memory = o.memory ?? 1024; // MB
46
- this.regions = o.regions || ['iad1'];
47
- this.environment = o.environment || {};
48
- this.supportsResponseStreaming = !!o.supportsResponseStreaming;
49
- this.operationType = o.operationType || 'API';
50
- this.methods = o.methods || null; // app-router exports (GET/POST...) or null = any
51
- this.style = o.style; // 'pages' | 'app' | 'vercel-node'
52
- this.framework = o.framework || null;
53
- this.prerender = o.prerender || null; // ISR config, if any
54
- this.middleware = !!o.middleware;
55
- this.isEdge = this.runtime === 'edge';
56
- }
57
- }
58
-
59
- export class VercelDeployment {
60
- constructor(o) {
61
- this.root = o.root;
62
- this.source = o.source; // 'build-output' | 'nextjs' | 'vite' | 'vercel-node'
63
- this.framework = o.framework || null;
64
- this.config = o.config; // Build Output API config.json (routes, overrides, ...)
65
- this.staticDir = o.staticDir; // absolute dir served as static/, or null
66
- this.staticFiles = o.staticFiles || []; // [{ path: '/index.html', file: abs, contentType }]
67
- this.functions = o.functions || []; // VercelFunction[]
68
- this.crons = o.crons || [];
69
- this.notes = o.notes || [];
70
- }
71
- }
72
-
73
- // ---------------------------------------------------------------- content types
74
-
75
- export const CONTENT_TYPES = {
76
- '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8',
77
- '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8',
78
- '.map': 'application/json', '.txt': 'text/plain; charset=utf-8', '.md': 'text/markdown; charset=utf-8',
79
- '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
80
- '.webp': 'image/webp', '.avif': 'image/avif', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2',
81
- '.ttf': 'font/ttf', '.otf': 'font/otf', '.wasm': 'application/wasm', '.xml': 'application/xml', '.pdf': 'application/pdf',
82
- '.webmanifest': 'application/manifest+json', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4', '.webm': 'video/webm',
83
- };
84
- export function contentTypeFor(p) {
85
- return CONTENT_TYPES[path.extname(p).toLowerCase()] || 'application/octet-stream';
86
- }
87
-
88
- // ---------------------------------------------------------------- route paths
89
-
90
- /** Turn a file-system route ("api/users/[id]/index.js", "api/[...slug].ts") into Vercel's route shape. */
91
- export function routeFromFile(rel, style) {
92
- let p = rel.replace(/\\/g, '/');
93
- p = p.replace(/\.(js|mjs|cjs|ts|mts|cts|jsx|tsx)$/, '');
94
- if (style === 'app') p = p.replace(/\/route$/, '');
95
- p = p.replace(/\/index$/, '');
96
- if (p === 'index') p = '';
97
- const params = [];
98
- const segs = p.split('/').filter(Boolean).map((seg) => {
99
- if (style === 'app' && /^\(.*\)$/.test(seg)) return null; // route groups
100
- let m;
101
- if ((m = seg.match(/^\[\[\.\.\.(.+)\]\]$/))) { params.push(m[1]); return { re: '(?:/(.*))?', optional: true }; }
102
- if ((m = seg.match(/^\[\.\.\.(.+)\]$/))) { params.push(m[1]); return { re: '/(.+)' }; }
103
- if ((m = seg.match(/^\[(.+)\]$/))) { params.push(m[1]); return { re: '/([^/]+)' }; }
104
- return { re: '/' + seg.replace(/[.*+?^${}()|\\]/g, '\\$&') };
105
- }).filter(Boolean);
106
- const routePath = '/' + p.split('/').filter((s) => !(style === 'app' && /^\(.*\)$/.test(s))).join('/');
107
- const pattern = '^' + (segs.length ? segs.map((s) => s.re).join('') : '/?') + '/?$';
108
- return { routePath: routePath === '/' ? '/' : routePath.replace(/\/$/, ''), pattern, params };
109
- }
110
-
111
- function walk(dir, { skip = [] } = {}) {
112
- const out = [];
113
- if (!fs.existsSync(dir)) return out;
114
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
115
- if (skip.includes(e.name) || e.name.startsWith('.')) continue;
116
- const p = path.join(dir, e.name);
117
- if (e.isDirectory()) out.push(...walk(p, { skip }));
118
- else out.push(p);
119
- }
120
- return out;
121
- }
122
-
123
- function readJson(p, fallback = null) {
124
- try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
125
- }
126
-
127
- /** Detect app-router HTTP method exports without parsing (cheap; the compiler re-checks). */
128
- export function detectMethods(src) {
129
- const m = new Set();
130
- for (const x of src.matchAll(/export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\b/g)) m.add(x[1]);
131
- for (const x of src.matchAll(/export\s+(?:const|let)\s+(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s*=/g)) m.add(x[1]);
132
- for (const x of src.matchAll(/export\s*\{([^}]+)\}/g)) for (const n of x[1].split(',')) { const t = n.trim().split(/\s+as\s+/).pop(); if (/^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)$/.test(t)) m.add(t); }
133
- return m.size ? [...m] : null;
134
- }
135
-
136
- export function detectRuntimeConfig(src) {
137
- const edge = /export\s+const\s+(?:config\s*=\s*\{[^}]*runtime\s*:\s*['"]edge['"]|runtime\s*=\s*['"]edge['"])/.test(src);
138
- const md = src.match(/export\s+const\s+maxDuration\s*=\s*(\d+)/);
139
- return { runtime: edge ? 'edge' : 'nodejs20.x', maxDuration: md ? Number(md[1]) : undefined };
140
- }
141
-
142
- // ---------------------------------------------------------------- readers
143
-
144
- /** Read a real `.vercel/output` (Build Output API v3). */
145
- export function readBuildOutput(root) {
146
- const out = path.join(root, '.vercel', 'output');
147
- const config = readJson(path.join(out, 'config.json'), { version: 3, routes: [] });
148
- const staticDir = path.join(out, 'static');
149
- const staticFiles = walk(staticDir).map((f) => ({
150
- path: '/' + path.relative(staticDir, f).replace(/\\/g, '/'), file: f, contentType: contentTypeFor(f), size: fs.statSync(f).size,
151
- }));
152
- const functions = [];
153
- const fdir = path.join(out, 'functions');
154
- const notes = [];
155
- const findFunc = (dir) => {
156
- if (!fs.existsSync(dir)) return;
157
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
158
- const p = path.join(dir, e.name);
159
- if (e.isDirectory() && e.name.endsWith('.func')) {
160
- const vc = readJson(path.join(p, '.vc-config.json'), {});
161
- const name = path.relative(fdir, p).replace(/\\/g, '/').replace(/\.func$/, '');
162
- const prerender = readJson(path.join(dir, e.name.replace(/\.func$/, '.prerender-config.json')));
163
- const handlerFile = vc.handler || vc.entrypoint;
164
- const { routePath, pattern, params } = routeFromFile(name, 'pages');
165
- functions.push(new VercelFunction({
166
- name, routePath, pattern, params, runtime: vc.runtime, launcherType: vc.launcherType, shouldAddHelpers: vc.shouldAddHelpers,
167
- handler: vc.handler, entrypoint: vc.entrypoint, sourceFile: handlerFile ? path.join(p, handlerFile) : null,
168
- maxDuration: vc.maxDuration, memory: vc.memory, regions: vc.regions, environment: vc.environment,
169
- supportsResponseStreaming: vc.supportsResponseStreaming, operationType: vc.operationType, framework: vc.framework,
170
- prerender, style: 'build-output', middleware: name === 'middleware' || name === '_middleware',
171
- }));
172
- } else if (e.isDirectory()) findFunc(p);
173
- }
174
- };
175
- findFunc(fdir);
176
- notes.push('read from .vercel/output (Build Output API v3); bundled function handlers are transmuted from their handler file');
177
- return new VercelDeployment({ root, source: 'build-output', framework: config.framework?.slug || null, config, staticDir, staticFiles, functions, crons: config.crons || [], notes });
178
- }
179
-
180
- /** Synthesize the model from a Next.js repo (pages/api and app router route files). */
181
- export function readNextjs(root) {
182
- const pkg = readJson(path.join(root, 'package.json'), {});
183
- const srcRoots = [root, path.join(root, 'src')];
184
- const functions = [];
185
- const notes = [];
186
- for (const base of srcRoots) {
187
- const pagesApi = path.join(base, 'pages', 'api');
188
- for (const f of walk(pagesApi)) {
189
- if (!HANDLER_EXTS.includes(path.extname(f))) continue;
190
- const rel = 'api/' + path.relative(pagesApi, f).replace(/\\/g, '/');
191
- const src = fs.readFileSync(f, 'utf8');
192
- const rc = detectRuntimeConfig(src);
193
- const { routePath, pattern, params } = routeFromFile(rel, 'pages');
194
- functions.push(new VercelFunction({ name: rel.replace(/\.[^.]+$/, ''), routePath, pattern, params, sourceFile: f, style: 'pages', runtime: rc.runtime, maxDuration: rc.maxDuration, framework: { slug: 'nextjs', version: pkg.dependencies?.next } }));
195
- }
196
- const app = path.join(base, 'app');
197
- for (const f of walk(app)) {
198
- const bn = path.basename(f).replace(/\.[^.]+$/, '');
199
- if (bn !== 'route' || !HANDLER_EXTS.includes(path.extname(f))) continue;
200
- const rel = path.relative(app, f).replace(/\\/g, '/');
201
- const src = fs.readFileSync(f, 'utf8');
202
- const rc = detectRuntimeConfig(src);
203
- const { routePath, pattern, params } = routeFromFile(rel, 'app');
204
- functions.push(new VercelFunction({ name: rel.replace(/\/route\.[^.]+$/, ''), routePath, pattern, params, sourceFile: f, style: 'app', methods: detectMethods(src), runtime: rc.runtime, maxDuration: rc.maxDuration, framework: { slug: 'nextjs', version: pkg.dependencies?.next } }));
205
- }
206
- for (const mw of ['middleware.js', 'middleware.ts']) {
207
- const f = path.join(base, mw);
208
- if (fs.existsSync(f)) functions.push(new VercelFunction({ name: 'middleware', routePath: '/', pattern: '^/.*$', sourceFile: f, style: 'app', middleware: true, runtime: 'edge' }));
209
- }
210
- }
211
- // A top-level `api/` next to a Next.js app is built by @vercel/node exactly
212
- // as it is in a Vite repo (Vercel's docs call these "Serverless Functions"
213
- // outside the framework); `next build` ignores it, `vercel build` does not.
214
- // A pages/api route with the same name wins, as it does on Vercel.
215
- for (const f of readVercelNode(root).functions) {
216
- if (!functions.some((g) => g.name === f.name)) functions.push(f);
217
- }
218
- // A top-level `api/` next to pages/app is built by @vercel/node too (Vite-style functions in a Next repo).
219
- for (const f of walk(path.join(root, 'api'))) {
220
- if (!HANDLER_EXTS.includes(path.extname(f))) continue;
221
- const rel = 'api/' + path.relative(path.join(root, 'api'), f).replace(/\\/g, '/');
222
- const name = rel.replace(/\.[^.]+$/, '');
223
- if (functions.some((fn) => fn.name === name)) continue;
224
- const src = fs.readFileSync(f, 'utf8');
225
- const rc = detectRuntimeConfig(src);
226
- const { routePath, pattern, params } = routeFromFile(rel, 'pages');
227
- functions.push(new VercelFunction({ name, routePath, pattern, params, sourceFile: f, style: 'vercel-node', methods: detectMethods(src), runtime: rc.runtime, maxDuration: rc.maxDuration }));
228
- }
229
- // Static: `public/` is copied verbatim by @vercel/next; `out/` exists after `next build` with output:'export'.
230
- const staticDir = fs.existsSync(path.join(root, 'out')) ? path.join(root, 'out') : path.join(root, 'public');
231
- if (!fs.existsSync(path.join(root, 'out'))) notes.push("no `out/` directory: only `public/` is treated as static. Run `next build` with `output: 'export'` in next.config to ship the rendered pages.");
232
- const staticFiles = walk(staticDir).map((f) => ({ path: '/' + path.relative(staticDir, f).replace(/\\/g, '/'), file: f, contentType: contentTypeFor(f), size: fs.statSync(f).size }));
233
- const vercelJson = readJson(path.join(root, 'vercel.json'), {});
234
- const config = {
235
- version: 3,
236
- routes: [...(vercelJson.rewrites || []).map((r) => ({ src: r.source, dest: r.destination })), { handle: 'filesystem' }, ...functions.filter((f) => !f.middleware).map((f) => ({ src: f.pattern, dest: f.routePath }))],
237
- overrides: {},
238
- framework: { slug: 'nextjs', version: pkg.dependencies?.next || null },
239
- };
240
- applyEnv(functions, root);
241
- return new VercelDeployment({ root, source: 'nextjs', framework: 'nextjs', config, staticDir, staticFiles, functions, crons: vercelJson.crons || [], notes });
242
- }
243
-
244
- /** Vite / static repos with a top-level `api/` (what @vercel/node builds). */
245
- export function readVercelNode(root) {
246
- const pkg = readJson(path.join(root, 'package.json'), {});
247
- const api = path.join(root, 'api');
248
- const functions = walk(api).filter((f) => HANDLER_EXTS.includes(path.extname(f))).map((f) => {
249
- const rel = 'api/' + path.relative(api, f).replace(/\\/g, '/');
250
- const src = fs.readFileSync(f, 'utf8');
251
- const rc = detectRuntimeConfig(src);
252
- const { routePath, pattern, params } = routeFromFile(rel, 'pages');
253
- return new VercelFunction({ name: rel.replace(/\.[^.]+$/, ''), routePath, pattern, params, sourceFile: f, style: 'vercel-node', methods: detectMethods(src), runtime: rc.runtime, maxDuration: rc.maxDuration });
254
- });
255
- const vercelJson = readJson(path.join(root, 'vercel.json'), {});
256
- const candidates = [vercelJson.outputDirectory, 'dist', 'build', 'public', '.'].filter(Boolean);
257
- const staticDir = candidates.map((d) => path.join(root, d)).find((d) => fs.existsSync(path.join(d, 'index.html'))) || path.join(root, 'public');
258
- const staticFiles = walk(staticDir, { skip: ['node_modules', 'api'] }).map((f) => ({ path: '/' + path.relative(staticDir, f).replace(/\\/g, '/'), file: f, contentType: contentTypeFor(f), size: fs.statSync(f).size }));
259
- const notes = [];
260
- if (!fs.existsSync(path.join(staticDir, 'index.html'))) notes.push(`no built frontend found (looked for index.html in ${candidates.join(', ')}); run your \`vite build\` first`);
261
- const config = {
262
- version: 3,
263
- routes: [...(vercelJson.rewrites || []).map((r) => ({ src: r.source, dest: r.destination })), { handle: 'filesystem' }, ...functions.map((f) => ({ src: f.pattern, dest: f.routePath }))],
264
- overrides: {},
265
- framework: { slug: pkg.devDependencies?.vite || pkg.dependencies?.vite ? 'vite' : null },
266
- };
267
- applyEnv(functions, root);
268
- return new VercelDeployment({ root, source: 'vercel-node', framework: config.framework.slug, config, staticDir, staticFiles, functions, crons: vercelJson.crons || [], notes });
269
- }
270
-
271
- /** `.env.production` / `.env` become the Lambda `environment` table (baked into the program!). */
272
- export function applyEnv(functions, root) {
273
- const env = {};
274
- for (const f of ['.env', '.env.production', '.env.local', '.env.production.local']) {
275
- const p = path.join(root, f);
276
- if (!fs.existsSync(p)) continue;
277
- for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
278
- const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
279
- if (!m) continue;
280
- let v = m[2].trim();
281
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
282
- env[m[1]] = v;
283
- }
284
- }
285
- for (const fn of functions) fn.environment = { ...env, ...fn.environment };
286
- }
287
-
288
- /** Pick the reader for a repo. */
289
- export function readDeployment(root) {
290
- root = path.resolve(root);
291
- if (fs.existsSync(path.join(root, '.vercel', 'output', 'config.json'))) return readBuildOutput(root);
292
- const pkg = readJson(path.join(root, 'package.json'), {});
293
- const hasNext = !!(pkg.dependencies?.next || pkg.devDependencies?.next) || fs.existsSync(path.join(root, 'next.config.js')) || fs.existsSync(path.join(root, 'next.config.mjs')) || fs.existsSync(path.join(root, 'next.config.ts'));
294
- if (hasNext || fs.existsSync(path.join(root, 'pages', 'api')) || fs.existsSync(path.join(root, 'app'))) return readNextjs(root);
295
- return readVercelNode(root);
296
- }
@@ -1,41 +0,0 @@
1
- // Signer discovery. Order: --keypair / OPENZOO_KEYPAIR path → the openzoo
2
- // burner wallet (~/.openzoo/wallet.json, {solana:[64 bytes]} or a bare
3
- // solana-keygen array) → the Solana CLI keypair (~/.config/solana/id.json).
4
- import fs from 'node:fs';
5
- import os from 'node:os';
6
- import path from 'node:path';
7
- import { Keypair } from '@solana/web3.js';
8
-
9
- export function keypairFromFile(p) {
10
- const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
11
- const bytes = Array.isArray(raw) ? raw : raw.solana;
12
- if (!bytes) throw new Error(`${p}: not a keypair file`);
13
- return Keypair.fromSecretKey(Uint8Array.from(bytes));
14
- }
15
-
16
- export function loadWallet({ keypair } = {}) {
17
- const candidates = [
18
- keypair,
19
- process.env.OPENZOO_KEYPAIR,
20
- process.env.OPENZOO_WALLET || path.join(os.homedir(), '.openzoo', 'wallet.json'),
21
- path.join(os.homedir(), '.config', 'solana', 'id.json'),
22
- ].filter(Boolean);
23
- for (const p of candidates) {
24
- if (fs.existsSync(p)) return { keypair: keypairFromFile(p), path: p };
25
- }
26
- throw new Error(`no wallet found (tried ${candidates.join(', ')}); run \`npx openzoo address\` to create the burner wallet or pass --keypair`);
27
- }
28
-
29
- export function rpcUrl(cluster) {
30
- const c = cluster || process.env.OPENZOO_CLUSTER || 'mainnet';
31
- if (/^https?:\/\//.test(c)) return c;
32
- switch (c) {
33
- case 'localnet': case 'localhost': case 'local': return 'http://127.0.0.1:8899';
34
- case 'devnet': return 'https://api.devnet.solana.com';
35
- case 'testnet': return 'https://api.testnet.solana.com';
36
- case 'mainnet': case 'mainnet-beta':
37
- // Same default as the openzoo proxy (public mainnet RPC rate-limits hard).
38
- return process.env.OPENZOO_RPC || 'https://eu.fluxrpc.com?key=ab9278e1-6430-41ab-aee0-ac6b759a1fe4';
39
- default: throw new Error(`unknown cluster ${c}`);
40
- }
41
- }
@@ -1,208 +0,0 @@
1
- // The Vercel node-bridge contract as bytes — mirror of runtime/zoo-host/src/wire.rs.
2
- //
3
- // Vercel's Lambda launcher hands a function `{method, path, headers, body}`
4
- // (the bridge "Invoke" event) and expects `{statusCode, headers, body}`. On
5
- // Solana that event is instruction data and the response is return data plus
6
- // `sol_log_data` chunks. Keep this file and wire.rs in lock-step.
7
- import { createHash } from 'node:crypto';
8
- import { PublicKey } from '@solana/web3.js';
9
-
10
- export const TAG = { INVOKE: 0, ASSET_INIT: 1, ASSET_WRITE: 2, ASSET_CLOSE: 3, SITE_INIT: 16, SITE_SET_AUTHORITY: 17, VM_INVOKE: 18, VM_ASSET_INIT: 19, VM_ASSET_WRITE: 20, VM_ASSET_CLOSE: 21 };
11
- /** Path of a site's bytecode module under the shared runtime. */
12
- export const CODE_PATH = '/.zoo/code.bin';
13
- export const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
14
- export const ERR_KV_MISSING = 0x4b560001;
15
- export const ERR_NOT_AUTHORITY = 0x4b560002;
16
- export const ERR_BAD_WIRE = 0x4b560003;
17
- export const MAX_RETURN_DATA = 1024;
18
- export const BPF_LOADER_UPGRADEABLE = new PublicKey('BPFLoaderUpgradeab1e11111111111111111111111');
19
-
20
- /** Headers the gateway forwards on chain (everything else is dropped; a tx is 1232 bytes). */
21
- export const FORWARDED_HEADERS = ['content-type', 'authorization', 'accept', 'cookie', 'user-agent', 'x-forwarded-for', 'x-real-ip', 'x-vercel-id'];
22
-
23
- function u16(n) { const b = Buffer.alloc(2); b.writeUInt16LE(n); return b; }
24
- function u32(n) { const b = Buffer.alloc(4); b.writeUInt32LE(n); return b; }
25
-
26
- export function methodIndex(method) {
27
- const i = METHODS.indexOf(String(method || 'GET').toUpperCase());
28
- if (i < 0) throw new Error(`unsupported method ${method}`);
29
- return i;
30
- }
31
-
32
- /** Serialize headers as "name:value\n" with lowercase names, prefix-forwarded only. */
33
- export function encodeHeaders(headers = {}, { forwardAll = false } = {}) {
34
- const lines = [];
35
- for (const [k, v] of Object.entries(headers)) {
36
- const name = k.toLowerCase();
37
- if (!forwardAll && !FORWARDED_HEADERS.includes(name) && !name.startsWith('x-')) continue;
38
- const val = Array.isArray(v) ? v.join(', ') : String(v ?? '');
39
- if (val.includes('\n')) continue;
40
- lines.push(`${name}:${val}\n`);
41
- }
42
- return lines.join('');
43
- }
44
-
45
- /**
46
- * Build the instruction data for one bridge Invoke event.
47
- * @param {{route:number, method?:string, path:string, query?:string, headers?:object, body?:Buffer|string}} ev
48
- */
49
- export function encodeInvoke(ev) {
50
- const path = Buffer.from(ev.path || '/', 'utf8');
51
- const query = Buffer.from(ev.query || '', 'utf8');
52
- const headers = Buffer.from(typeof ev.headers === 'string' ? ev.headers : encodeHeaders(ev.headers), 'utf8');
53
- const body = ev.body == null ? Buffer.alloc(0) : Buffer.isBuffer(ev.body) ? ev.body : Buffer.from(String(ev.body), 'utf8');
54
- if (ev.route < 0 || ev.route > 255) throw new Error('route index out of range');
55
- return Buffer.concat([
56
- Buffer.from([TAG.INVOKE, ev.route, methodIndex(ev.method)]),
57
- u16(path.length), path,
58
- u16(query.length), query,
59
- u16(headers.length), headers,
60
- u32(body.length), body,
61
- ]);
62
- }
63
-
64
- /** Decode the bridge response bytes into {status, headers, body}. */
65
- export function decodeResponse(buf) {
66
- let i = 0;
67
- const need = (n) => { if (i + n > buf.length) throw new Error('short response'); };
68
- need(2); const status = buf.readUInt16LE(i); i += 2;
69
- need(2); const hl = buf.readUInt16LE(i); i += 2;
70
- need(hl); const hdr = buf.subarray(i, i + hl).toString('utf8'); i += hl;
71
- need(4); const bl = buf.readUInt32LE(i); i += 4;
72
- need(bl); const body = buf.subarray(i, i + bl); i += bl;
73
- const headers = {};
74
- for (const line of hdr.split('\n')) {
75
- const c = line.indexOf(':');
76
- if (c > 0) headers[line.slice(0, c)] = line.slice(c + 1);
77
- }
78
- return { status, headers, body };
79
- }
80
-
81
- /**
82
- * Reassemble a response from simulateTransaction/getTransaction logs.
83
- * Prefers the `ZOOR` log chunks; falls back to `returnData` (≤1024 bytes).
84
- * Also collects `ZOOK` (missing KV account) lines.
85
- */
86
- export function parseLogs(logs = [], returnData = null) {
87
- const chunks = new Map();
88
- const missing = [];
89
- for (const line of logs) {
90
- if (!line.startsWith('Program data: ')) continue;
91
- const parts = line.slice('Program data: '.length).split(' ').map((p) => Buffer.from(p, 'base64'));
92
- const tag = parts[0]?.toString('latin1');
93
- if (tag === 'ZOOR' && parts.length >= 2) {
94
- const idx = parts[1].length >= 2 ? parts[1].readUInt16LE(0) : 0;
95
- chunks.set(idx, parts[2] ?? Buffer.alloc(0));
96
- } else if (tag === 'ZOOK' && parts[1]?.length === 32) {
97
- missing.push(new PublicKey(parts[1]));
98
- }
99
- }
100
- let bytes = null;
101
- if (chunks.size) {
102
- const ordered = [...chunks.keys()].sort((a, b) => a - b).map((k) => chunks.get(k));
103
- bytes = Buffer.concat(ordered);
104
- } else if (returnData?.data) {
105
- bytes = Buffer.from(returnData.data[0], returnData.data[1] || 'base64');
106
- }
107
- return { bytes, missing, truncated: bytes == null ? false : (!chunks.size && bytes.length >= MAX_RETURN_DATA) };
108
- }
109
-
110
- // ---- PDAs (mirror kv.rs / assets.rs) ----
111
-
112
- export function sha256(...parts) {
113
- const h = createHash('sha256');
114
- for (const p of parts) h.update(p);
115
- return h.digest();
116
- }
117
-
118
- export function kvHash(key) { return sha256(Buffer.from('zoo-kv'), Buffer.from(String(key), 'utf8')); }
119
- export function kvPda(programId, key) {
120
- return PublicKey.findProgramAddressSync([Buffer.from('kv'), kvHash(key)], new PublicKey(programId));
121
- }
122
-
123
- export function assetHash(path) { return sha256(Buffer.from('zoo-asset'), Buffer.from(path, 'utf8')); }
124
- export function assetPda(programId, path) {
125
- return PublicKey.findProgramAddressSync([Buffer.from('asset'), assetHash(path)], new PublicKey(programId));
126
- }
127
- export function assetPdaFromHash(programId, hash) {
128
- return PublicKey.findProgramAddressSync([Buffer.from('asset'), hash], new PublicKey(programId));
129
- }
130
-
131
- export function programDataPda(programId) {
132
- return PublicKey.findProgramAddressSync([new PublicKey(programId).toBuffer()], BPF_LOADER_UPGRADEABLE)[0];
133
- }
134
-
135
- /** Reserved path of the site manifest (served publicly, like /.well-known). */
136
- export const MANIFEST_PATH = '/.zoo/manifest.json';
137
-
138
- export const ASSET_FIXED_HEADER = 7;
139
- export const ASSET_MAX_INITIAL = 10240;
140
-
141
- export function encodeAssetInit(path, totalLen, contentType) {
142
- const ct = Buffer.from(contentType.slice(0, 120), 'utf8');
143
- return Buffer.concat([Buffer.from([TAG.ASSET_INIT]), assetHash(path), u32(totalLen), Buffer.from([ct.length]), ct]);
144
- }
145
- export function encodeAssetWrite(path, offset, bytes) {
146
- return Buffer.concat([Buffer.from([TAG.ASSET_WRITE]), assetHash(path), u32(offset), bytes]);
147
- }
148
- export function encodeAssetClose(path) {
149
- return Buffer.concat([Buffer.from([TAG.ASSET_CLOSE]), assetHash(path)]);
150
- }
151
-
152
- /** Decode an asset account: {total, contentType, data (may be partial), complete}. */
153
- export function decodeAsset(data) {
154
- if (!data || data.length < ASSET_FIXED_HEADER || data[0] !== 1) return null;
155
- const total = data.readUInt32LE(2);
156
- const ctLen = data[6];
157
- const contentType = data.subarray(7, 7 + ctLen).toString('utf8');
158
- const start = 7 + ctLen;
159
- const body = data.subarray(start, Math.min(data.length, start + total));
160
- return { total, contentType, data: body, complete: body.length === total, bump: data[1] };
161
- }
162
-
163
- export const KV_HEADER = 6;
164
- export function decodeKv(data) {
165
- if (!data || data.length < KV_HEADER || data[0] !== 1) return null;
166
- const len = data.readUInt32LE(2);
167
- if (len === 0) return null;
168
- try { return JSON.parse(data.subarray(KV_HEADER, KV_HEADER + len).toString('utf8')); } catch { return null; }
169
- }
170
-
171
- // ---- shared runtime (zoo-vm): sites are accounts namespaced by a 32-byte site id
172
-
173
- export function siteIdBytes(site) { return site instanceof PublicKey ? site.toBuffer() : new PublicKey(site).toBuffer(); }
174
-
175
- export function sitePda(runtime, site) {
176
- return PublicKey.findProgramAddressSync([Buffer.from('site'), siteIdBytes(site)], new PublicKey(runtime));
177
- }
178
- export function kvPdaNs(runtime, site, key) {
179
- return PublicKey.findProgramAddressSync([Buffer.from('kv'), siteIdBytes(site), kvHash(key)], new PublicKey(runtime));
180
- }
181
- export function assetPdaNs(runtime, site, path) {
182
- return PublicKey.findProgramAddressSync([Buffer.from('asset'), siteIdBytes(site), assetHash(path)], new PublicKey(runtime));
183
- }
184
- /** Any-mode asset PDA: program-flavour when `site` is null. */
185
- export function assetPdaFor(programId, site, path) {
186
- return site ? assetPdaNs(programId, site, path) : assetPda(programId, path);
187
- }
188
- export function kvPdaFor(programId, site, key) {
189
- return site ? kvPdaNs(programId, site, key) : kvPda(programId, key);
190
- }
191
-
192
- export function encodeSiteInit(site) { return Buffer.concat([Buffer.from([TAG.SITE_INIT]), siteIdBytes(site)]); }
193
- export function encodeSiteSetAuthority(site, newAuthority) { return Buffer.concat([Buffer.from([TAG.SITE_SET_AUTHORITY]), siteIdBytes(site), new PublicKey(newAuthority).toBuffer()]); }
194
- /** The VM invoke: same bridge event, prefixed by the site id. */
195
- export function encodeVmInvoke(site, ev) {
196
- const inner = encodeInvoke(ev); // [TAG.INVOKE][...]
197
- return Buffer.concat([Buffer.from([TAG.VM_INVOKE]), siteIdBytes(site), inner.subarray(1)]);
198
- }
199
- export function encodeVmAssetInit(site, path, totalLen, contentType) { return Buffer.concat([Buffer.from([TAG.VM_ASSET_INIT]), siteIdBytes(site), encodeAssetInit(path, totalLen, contentType).subarray(1)]); }
200
- export function encodeVmAssetWrite(site, path, offset, bytes) { return Buffer.concat([Buffer.from([TAG.VM_ASSET_WRITE]), siteIdBytes(site), encodeAssetWrite(path, offset, bytes).subarray(1)]); }
201
- export function encodeVmAssetClose(site, path) { return Buffer.concat([Buffer.from([TAG.VM_ASSET_CLOSE]), siteIdBytes(site), encodeAssetClose(path).subarray(1)]); }
202
-
203
- export const SITE_LEN = 66;
204
- /** Decode a site account: { authority, bump } or null. */
205
- export function decodeSite(data) {
206
- if (!data || data.length < SITE_LEN || data[0] !== 1) return null;
207
- return { bump: data[1], authority: new PublicKey(data.subarray(2, 34)) };
208
- }
Binary file