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.
- package/README.md +12 -1
- package/bin/openzoo.js +21 -46
- package/lib/chatgpt-install.js +106 -0
- package/lib/chatgpt.js +108 -90
- package/lib/config.js +34 -0
- package/lib/modelcommand.js +443 -0
- package/lib/pay.js +60 -4
- package/lib/proxy.js +39 -0
- package/package.json +4 -7
- package/transmute/README.md +0 -385
- package/transmute/bin/openzoo-transmute.js +0 -7
- package/transmute/docs/OPENZOO_CLI_PATCH.md +0 -139
- package/transmute/docs/VERCEL_TO_SOLANA.md +0 -381
- package/transmute/lib/build.js +0 -381
- package/transmute/lib/cli.js +0 -293
- package/transmute/lib/compile/bytecode.js +0 -591
- package/transmute/lib/compile/hostids.js +0 -11
- package/transmute/lib/compile/index.js +0 -159
- package/transmute/lib/compile/ir.js +0 -1634
- package/transmute/lib/compile/parse.js +0 -753
- package/transmute/lib/compile/rust.js +0 -781
- package/transmute/lib/deploy.js +0 -357
- package/transmute/lib/eligibility.js +0 -211
- package/transmute/lib/gateway.js +0 -710
- package/transmute/lib/hub.js +0 -290
- package/transmute/lib/solana.js +0 -387
- package/transmute/lib/vercel.js +0 -296
- package/transmute/lib/wallet.js +0 -41
- package/transmute/lib/wire.js +0 -208
- package/transmute/prebuilt/zoo_vm.v0.so +0 -0
- package/transmute/runtime/zoo-host/Cargo.lock +0 -588
- package/transmute/runtime/zoo-host/Cargo.toml +0 -30
- package/transmute/runtime/zoo-host/examples/vmrun.rs +0 -43
- package/transmute/runtime/zoo-host/src/assets.rs +0 -214
- package/transmute/runtime/zoo-host/src/ctx.rs +0 -350
- package/transmute/runtime/zoo-host/src/fmt.rs +0 -188
- package/transmute/runtime/zoo-host/src/helpers.rs +0 -245
- package/transmute/runtime/zoo-host/src/json.rs +0 -263
- package/transmute/runtime/zoo-host/src/kv.rs +0 -206
- package/transmute/runtime/zoo-host/src/lib.rs +0 -137
- package/transmute/runtime/zoo-host/src/site.rs +0 -94
- package/transmute/runtime/zoo-host/src/val.rs +0 -1050
- package/transmute/runtime/zoo-host/src/vm.rs +0 -902
- package/transmute/runtime/zoo-host/src/wire.rs +0 -239
- package/transmute/runtime/zoo-vm/Cargo.lock +0 -596
- package/transmute/runtime/zoo-vm/Cargo.toml +0 -23
- package/transmute/runtime/zoo-vm/src/lib.rs +0 -92
- package/transmute/site/.env.production +0 -2
- package/transmute/site/api/echo.js +0 -10
- package/transmute/site/api/hello.js +0 -12
- package/transmute/site/api/hits.js +0 -13
- package/transmute/site/api/time.js +0 -5
- package/transmute/site/package.json +0 -6
- package/transmute/site/public/app.js +0 -16
- package/transmute/site/public/index.html +0 -96
package/transmute/lib/hub.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
// The hosted explorer: one process, any number of transmuted sites.
|
|
2
|
-
//
|
|
3
|
-
// `openzoo-transmute serve` fronts ONE program on localhost. The hub fronts
|
|
4
|
-
// EVERY program on a cluster from one public host, read-only:
|
|
5
|
-
//
|
|
6
|
-
// GET /s/<programId> pin that site (cookie) and go to its /
|
|
7
|
-
// GET /s/<programId>/<path> one page of it, absolute
|
|
8
|
-
// GET /<path> the pinned site's <path> (so a site's own
|
|
9
|
-
// root-relative links — /app.js, /api/x — work)
|
|
10
|
-
// GET /.hub the landing page (paste a program id)
|
|
11
|
-
// GET /.hub/leave unpin
|
|
12
|
-
// GET /.hub/sites.json what this hub has served lately
|
|
13
|
-
//
|
|
14
|
-
// Reads are free simulations, exactly like the local gateway. Writes need a
|
|
15
|
-
// signer, and a public host must never sign with a shared wallet, so they
|
|
16
|
-
// answer 402 with the same message the local gateway gives — the seam where
|
|
17
|
-
// x402 pays per request later. Everything a site is comes from the chain:
|
|
18
|
-
// the manifest at /.zoo/manifest.json, the assets, the program; the hub only
|
|
19
|
-
// caches.
|
|
20
|
-
import http from 'node:http';
|
|
21
|
-
import { PublicKey } from '@solana/web3.js';
|
|
22
|
-
import { makeState, normalizeManifest, handleRequest, safeResponseHeaders, PACKET_DATA_SIZE, BODY_LIMIT } from './gateway.js';
|
|
23
|
-
import { connect, readManifest, getSiteInfo } from './solana.js';
|
|
24
|
-
import { rpcUrl } from './wallet.js';
|
|
25
|
-
import { MANIFEST_PATH } from './wire.js';
|
|
26
|
-
|
|
27
|
-
export const HUB_PREFIX = '/.hub';
|
|
28
|
-
export const SITE_PREFIX = '/s/';
|
|
29
|
-
export const COOKIE = 'zoo_site';
|
|
30
|
-
export const DEFAULT_HUB_PORT = 8080;
|
|
31
|
-
|
|
32
|
-
const JSON_CT = 'application/json; charset=utf-8';
|
|
33
|
-
const HTML_CT = 'text/html; charset=utf-8';
|
|
34
|
-
|
|
35
|
-
function isProgramId(s) {
|
|
36
|
-
if (typeof s !== 'string' || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(s)) return false;
|
|
37
|
-
try { new PublicKey(s); return true; } catch { return false; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function cookieOf(req) {
|
|
41
|
-
const raw = req.headers?.cookie || '';
|
|
42
|
-
for (const part of String(raw).split(';')) {
|
|
43
|
-
const [k, ...v] = part.trim().split('=');
|
|
44
|
-
if (k === COOKIE) { const id = decodeURIComponent(v.join('=')); return isProgramId(id) ? id : null; }
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const setCookie = (id) => `${COOKIE}=${id}; Path=/; Max-Age=2592000; SameSite=Lax`;
|
|
50
|
-
const clearCookie = () => `${COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`;
|
|
51
|
-
|
|
52
|
-
function json(status, body, headers = {}) {
|
|
53
|
-
return { status, headers: { 'content-type': JSON_CT, ...headers }, body: Buffer.from(JSON.stringify(body, null, 2)) };
|
|
54
|
-
}
|
|
55
|
-
function html(status, body, headers = {}) {
|
|
56
|
-
return { status, headers: { 'content-type': HTML_CT, ...headers }, body: Buffer.from(body) };
|
|
57
|
-
}
|
|
58
|
-
function redirect(location, headers = {}) {
|
|
59
|
-
return { status: 302, headers: { location, 'content-type': 'text/plain; charset=utf-8', ...headers }, body: Buffer.from(`→ ${location}\n`) };
|
|
60
|
-
}
|
|
61
|
-
const escape = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Hub state: the cluster connection plus an LRU of per-site gateway states.
|
|
65
|
-
* `o.makeSite(programId)` can be injected for tests.
|
|
66
|
-
*/
|
|
67
|
-
export function makeHub(o = {}) {
|
|
68
|
-
const cluster = o.cluster || process.env.OPENZOO_CLUSTER || 'mainnet';
|
|
69
|
-
const rpc = o.connection?.rpcEndpoint || rpcUrl(cluster);
|
|
70
|
-
const hub = {
|
|
71
|
-
cluster,
|
|
72
|
-
rpc,
|
|
73
|
-
connection: o.connection ?? connect(rpc),
|
|
74
|
-
keypair: o.keypair ?? null,
|
|
75
|
-
sites: new Map(), // programId -> { state, at, lastSeen, hits }
|
|
76
|
-
maxSites: o.maxSites ?? 64,
|
|
77
|
-
manifestTtlMs: o.manifestTtlMs ?? 60_000,
|
|
78
|
-
log: o.log ?? (() => {}),
|
|
79
|
-
makeSite: o.makeSite ?? null,
|
|
80
|
-
startedAt: Date.now(),
|
|
81
|
-
stats: { requests: 0, sitesLoaded: 0, writes: 0, writesRefused: 0, spentLamports: 0 },
|
|
82
|
-
publicUrl: o.publicUrl || null,
|
|
83
|
-
// The shared runtime program on this cluster: ids that are site accounts
|
|
84
|
-
// under it are served as shared sites; anything else as a compiled program.
|
|
85
|
-
runtime: o.runtime || process.env.OPENZOO_VM_PROGRAM || null,
|
|
86
|
-
// write governor: per-IP token bucket + a daily lamport budget for the signer
|
|
87
|
-
writesPerMin: o.writesPerMin ?? Number(process.env.OPENZOO_HUB_WRITES_PER_MIN || 3),
|
|
88
|
-
budgetLamports: Math.round((o.writeBudgetSol ?? Number(process.env.OPENZOO_HUB_WRITE_BUDGET_SOL || 0.05)) * 1e9),
|
|
89
|
-
ipHits: new Map(),
|
|
90
|
-
dayStart: Date.now(),
|
|
91
|
-
};
|
|
92
|
-
return hub;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const READ_METHODS = ['GET', 'HEAD', 'OPTIONS'];
|
|
96
|
-
|
|
97
|
-
/** Allow a signed write? Refills per minute per IP; budget resets daily. */
|
|
98
|
-
export function allowWrite(hub, ip, now = Date.now()) {
|
|
99
|
-
if (!hub.keypair) return { ok: false, reason: 'no signer' };
|
|
100
|
-
if (now - hub.dayStart > 86_400_000) { hub.dayStart = now; hub.stats.spentLamports = 0; }
|
|
101
|
-
if (hub.stats.spentLamports >= hub.budgetLamports) return { ok: false, reason: 'daily write budget spent' };
|
|
102
|
-
const key = ip || 'unknown';
|
|
103
|
-
const win = hub.ipHits.get(key) || { t: now, n: 0 };
|
|
104
|
-
if (now - win.t > 60_000) { win.t = now; win.n = 0; }
|
|
105
|
-
if (win.n >= hub.writesPerMin) return { ok: false, reason: `rate limit: ${hub.writesPerMin} writes/min` };
|
|
106
|
-
win.n++; hub.ipHits.set(key, win);
|
|
107
|
-
if (hub.ipHits.size > 10_000) hub.ipHits.clear();
|
|
108
|
-
return { ok: true };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function siteFor(hub, programId) {
|
|
112
|
-
const hit = hub.sites.get(programId);
|
|
113
|
-
if (hit) {
|
|
114
|
-
hit.lastSeen = Date.now(); hit.hits++;
|
|
115
|
-
hub.sites.delete(programId); hub.sites.set(programId, hit); // LRU order
|
|
116
|
-
return hit;
|
|
117
|
-
}
|
|
118
|
-
let state;
|
|
119
|
-
if (hub.makeSite) state = await hub.makeSite(programId);
|
|
120
|
-
else {
|
|
121
|
-
let shared = false;
|
|
122
|
-
if (hub.runtime) {
|
|
123
|
-
try { shared = (await getSiteInfo(hub.connection, hub.runtime, programId)).exists; } catch { /* fall through to program mode */ }
|
|
124
|
-
}
|
|
125
|
-
state = shared
|
|
126
|
-
? makeState({ programId: hub.runtime, site: programId, cluster: hub.cluster, connection: hub.connection, keypair: hub.keypair, log: hub.log, cacheTtlMs: hub.manifestTtlMs })
|
|
127
|
-
: makeState({ programId, cluster: hub.cluster, connection: hub.connection, keypair: hub.keypair, log: hub.log, cacheTtlMs: hub.manifestTtlMs });
|
|
128
|
-
try {
|
|
129
|
-
const m = await readManifest(hub.connection, state.programId, state.site);
|
|
130
|
-
if (m) { state.manifest = normalizeManifest(m); state.manifestAt = Date.now(); }
|
|
131
|
-
else state.noManifest = true;
|
|
132
|
-
} catch (e) { state.manifestError = String(e?.message || e); }
|
|
133
|
-
}
|
|
134
|
-
const entry = { state, at: Date.now(), lastSeen: Date.now(), hits: 1 };
|
|
135
|
-
// An RPC failure is not a fact about the site: answer it, but do not cache it.
|
|
136
|
-
if (state.manifestError) return entry;
|
|
137
|
-
hub.sites.set(programId, entry);
|
|
138
|
-
hub.stats.sitesLoaded++;
|
|
139
|
-
while (hub.sites.size > hub.maxSites) hub.sites.delete(hub.sites.keys().next().value);
|
|
140
|
-
return entry;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function siteSummary(id, entry) {
|
|
144
|
-
const m = entry.state.manifest || {};
|
|
145
|
-
return {
|
|
146
|
-
programId: id,
|
|
147
|
-
mode: entry.state.site ? 'shared' : 'program',
|
|
148
|
-
name: m.name || null,
|
|
149
|
-
framework: m.framework || null,
|
|
150
|
-
routes: (m.routes || []).length,
|
|
151
|
-
assets: (m.static || []).length,
|
|
152
|
-
deployedAt: m.deployedAt || null,
|
|
153
|
-
manifest: entry.state.noManifest ? 'missing' : entry.state.manifestError ? `error: ${entry.state.manifestError}` : 'ok',
|
|
154
|
-
hits: entry.hits,
|
|
155
|
-
lastSeen: new Date(entry.lastSeen).toISOString(),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function landingHtml(hub, pinned = null) {
|
|
160
|
-
const rows = [...hub.sites.entries()].reverse().map(([id, e]) => {
|
|
161
|
-
const s = siteSummary(id, e);
|
|
162
|
-
return `<tr><td><a href="/s/${escape(id)}">${escape(s.name || id.slice(0, 8) + '…')}</a></td><td><code>${escape(id)}</code></td><td>${s.routes}</td><td>${s.assets}</td><td>${escape(s.manifest)}</td></tr>`;
|
|
163
|
-
}).join('\n');
|
|
164
|
-
return `<!doctype html>
|
|
165
|
-
<meta charset="utf-8">
|
|
166
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
167
|
-
<title>openzoo sites · ${escape(hub.cluster)}</title>
|
|
168
|
-
<style>
|
|
169
|
-
:root{color-scheme:light dark;font-family:ui-sans-serif,system-ui,sans-serif}
|
|
170
|
-
body{max-width:56rem;margin:2rem auto;padding:0 1rem;line-height:1.45}
|
|
171
|
-
h1{font-size:1.5rem;margin:.2rem 0} .muted{opacity:.7} code{font-size:.9em}
|
|
172
|
-
form{display:flex;gap:.5rem;margin:1rem 0} input{flex:1;padding:.55rem .7rem;font:inherit;border:1px solid #8884;border-radius:.4rem;background:transparent;color:inherit}
|
|
173
|
-
button{padding:.55rem 1rem;font:inherit;border:1px solid #8886;border-radius:.4rem;background:#3b82f6;color:#fff;cursor:pointer}
|
|
174
|
-
table{border-collapse:collapse;width:100%;font-size:.92rem} td,th{padding:.35rem .5rem;border-bottom:1px solid #8883;text-align:left;vertical-align:top}
|
|
175
|
-
pre{background:#8881;padding:.7rem;border-radius:.4rem;overflow:auto;font-size:.85rem}
|
|
176
|
-
.pin{padding:.6rem .8rem;border:1px solid #3b82f6;border-radius:.4rem;margin:1rem 0}
|
|
177
|
-
</style>
|
|
178
|
-
<h1>openzoo sites</h1>
|
|
179
|
-
<p class="muted">Vercel apps transmuted onto Solana <b>${escape(hub.cluster)}</b>: the frontend lives in asset accounts, the <code>/api/*</code> Lambdas run as a Pinocchio program. This host reads them straight off the chain. Reads are free simulations; writes need a signer, so they answer 402 here — run <code>npx openzoo serve <programId></code> locally to write with your own wallet.</p>
|
|
180
|
-
${pinned ? `<div class="pin">Pinned site: <code>${escape(pinned)}</code> — <a href="/">open</a> · <a href="/.zoo/">explorer</a> · <a href="/.hub/leave">unpin</a></div>` : ''}
|
|
181
|
-
<form action="/.hub/go" method="get"><input name="program" placeholder="program id (base58)" autocomplete="off" required pattern="[1-9A-HJ-NP-Za-km-z]{32,44}"><button type="submit">open</button></form>
|
|
182
|
-
<h2 style="font-size:1.1rem">recently served</h2>
|
|
183
|
-
${rows ? `<table><tr><th>site</th><th>program</th><th>routes</th><th>assets</th><th>manifest</th></tr>${rows}</table>` : '<p class="muted">nothing yet — paste a program id above.</p>'}
|
|
184
|
-
<h2 style="font-size:1.1rem">put your app here</h2>
|
|
185
|
-
<pre>npx openzoo inspect . # your Next.js / Vite app, in Vercel terms + eligibility
|
|
186
|
-
npx openzoo build . # → Pinocchio Rust program + asset plan (cargo build-sbf)
|
|
187
|
-
npx openzoo deploy . --cluster ${escape(hub.cluster)} --yes # rent is quoted first; paid by ~/.openzoo/wallet.json
|
|
188
|
-
# then: ${escape(hub.publicUrl || 'https://<this host>')}/s/<programId></pre>
|
|
189
|
-
<p class="muted">Each site's <code>/.zoo/manifest.json</code> and <code>/.zoo/status</code> are public; <code>/.hub/sites.json</code> lists what this hub has served.</p>
|
|
190
|
-
`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function readBody(req, max) {
|
|
194
|
-
return new Promise((resolve) => {
|
|
195
|
-
const chunks = []; let length = 0, truncated = false;
|
|
196
|
-
req.on('data', (c) => { length += c.length; if (length > max) { truncated = true; req.destroy?.(); return; } chunks.push(c); });
|
|
197
|
-
req.on('end', () => resolve({ body: Buffer.concat(chunks), truncated, length }));
|
|
198
|
-
req.on('error', () => resolve({ body: Buffer.concat(chunks), truncated, length }));
|
|
199
|
-
req.on('close', () => resolve({ body: Buffer.concat(chunks), truncated, length }));
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Route one request. `req` = { method, url, headers, body, remoteAddress }. */
|
|
204
|
-
export async function handleHub(hub, req) {
|
|
205
|
-
hub.stats.requests++;
|
|
206
|
-
const method = String(req.method || 'GET').toUpperCase();
|
|
207
|
-
const url = new URL(req.url || '/', 'http://hub.local');
|
|
208
|
-
const pathname = url.pathname;
|
|
209
|
-
const pinned = cookieOf(req);
|
|
210
|
-
|
|
211
|
-
// ---- hub pages
|
|
212
|
-
if (pathname === HUB_PREFIX || pathname === `${HUB_PREFIX}/`) return html(200, landingHtml(hub, pinned));
|
|
213
|
-
if (pathname === `${HUB_PREFIX}/go`) {
|
|
214
|
-
const id = (url.searchParams.get('program') || '').trim();
|
|
215
|
-
if (!isProgramId(id)) return json(400, { error: 'not a program id', program: id });
|
|
216
|
-
return redirect(`${SITE_PREFIX}${id}`);
|
|
217
|
-
}
|
|
218
|
-
if (pathname === `${HUB_PREFIX}/leave`) return redirect(HUB_PREFIX, { 'set-cookie': clearCookie() });
|
|
219
|
-
if (pathname === `${HUB_PREFIX}/sites.json`) {
|
|
220
|
-
return json(200, { cluster: hub.cluster, rpc: hub.rpc.replace(/\?.*$/, ''), sites: [...hub.sites.entries()].map(([id, e]) => siteSummary(id, e)), uptimeS: Math.round((Date.now() - hub.startedAt) / 1000), stats: hub.stats });
|
|
221
|
-
}
|
|
222
|
-
if (pathname === `${HUB_PREFIX}/health`) return json(200, { ok: true, sites: hub.sites.size, cluster: hub.cluster });
|
|
223
|
-
|
|
224
|
-
// ---- /s/<programId>[/<path>]
|
|
225
|
-
let target = null, rest = null, pin = false;
|
|
226
|
-
if (pathname.startsWith(SITE_PREFIX)) {
|
|
227
|
-
const after = pathname.slice(SITE_PREFIX.length);
|
|
228
|
-
const slash = after.indexOf('/');
|
|
229
|
-
const id = slash < 0 ? after : after.slice(0, slash);
|
|
230
|
-
if (!isProgramId(id)) return json(400, { error: 'not a program id', program: id });
|
|
231
|
-
target = id; pin = true;
|
|
232
|
-
rest = slash < 0 ? null : after.slice(slash) || '/';
|
|
233
|
-
if (rest === null) return redirect('/', { 'set-cookie': setCookie(id) }); // pin, then the site's own /
|
|
234
|
-
} else if (pinned) {
|
|
235
|
-
target = pinned; rest = pathname;
|
|
236
|
-
} else {
|
|
237
|
-
if (pathname === '/') return html(200, landingHtml(hub, null));
|
|
238
|
-
return json(404, { error: 'no site pinned', hint: `open /s/<programId> first, or /.hub` });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const entry = await siteFor(hub, target);
|
|
242
|
-
const state = entry.state;
|
|
243
|
-
if (state.manifestError) {
|
|
244
|
-
return json(502, { error: 'rpc error', message: state.manifestError, cluster: hub.cluster, rpc: hub.rpc.replace(/\?.*$/, ''), program: target }, pin ? { 'set-cookie': setCookie(target) } : {});
|
|
245
|
-
}
|
|
246
|
-
if (state.noManifest && (rest === '/' || rest === MANIFEST_PATH)) {
|
|
247
|
-
return json(404, { error: 'no site at this program id', program: target, hint: `no manifest at ${MANIFEST_PATH}; is this an openzoo-transmute deployment on ${hub.cluster}?` }, pin ? { 'set-cookie': setCookie(target) } : {});
|
|
248
|
-
}
|
|
249
|
-
if (!READ_METHODS.includes(method) && hub.keypair) {
|
|
250
|
-
const gate = allowWrite(hub, req.remoteAddress || req.headers?.['fly-client-ip'] || req.headers?.['x-forwarded-for']);
|
|
251
|
-
if (!gate.ok) { hub.stats.writesRefused++; return json(429, { error: 'write refused', reason: gate.reason, hint: 'run `npx openzoo serve <programId>` locally to write with your own wallet' }, { 'x-zoo-site': target }); }
|
|
252
|
-
}
|
|
253
|
-
const before = hub.keypair && !READ_METHODS.includes(method) ? await hub.connection.getBalance(hub.keypair.publicKey).catch(() => null) : null;
|
|
254
|
-
const r = await handleRequest(state, { ...req, method, url: rest + (url.search || '') });
|
|
255
|
-
if (before != null) {
|
|
256
|
-
hub.stats.writes++;
|
|
257
|
-
const after = await hub.connection.getBalance(hub.keypair.publicKey).catch(() => before);
|
|
258
|
-
hub.stats.spentLamports += Math.max(0, before - after);
|
|
259
|
-
}
|
|
260
|
-
const headers = { ...(r.headers || {}), 'x-zoo-site': target };
|
|
261
|
-
if (pin) headers['set-cookie'] = setCookie(target);
|
|
262
|
-
return { ...r, headers };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export async function startHub({ cluster, port = DEFAULT_HUB_PORT, host = '0.0.0.0', keypair = null, connection, log = console.log, quiet = false, publicUrl, maxSites } = {}) {
|
|
266
|
-
const hub = makeHub({ cluster, connection, keypair, log: quiet ? () => {} : log, publicUrl, maxSites });
|
|
267
|
-
const server = http.createServer(async (req, res) => {
|
|
268
|
-
const t0 = Date.now();
|
|
269
|
-
try {
|
|
270
|
-
const { body, truncated, length } = await readBody(req, PACKET_DATA_SIZE);
|
|
271
|
-
const r = truncated
|
|
272
|
-
? json(413, { error: 'payload too large', limit: BODY_LIMIT, received: length })
|
|
273
|
-
: await handleHub(hub, { method: req.method, url: req.url, headers: req.headers, body, remoteAddress: req.socket?.remoteAddress });
|
|
274
|
-
const out = Buffer.isBuffer(r.body) ? r.body : Buffer.from(String(r.body ?? ''));
|
|
275
|
-
const headers = safeResponseHeaders(r.headers);
|
|
276
|
-
headers['content-length'] = String(out.length);
|
|
277
|
-
res.writeHead(r.status, headers);
|
|
278
|
-
res.end(req.method === 'HEAD' ? undefined : out);
|
|
279
|
-
if (!quiet) log(`${req.method} ${req.url} → ${r.status} ${out.length}B ${Date.now() - t0}ms${headers['x-zoo-site'] ? ' site=' + headers['x-zoo-site'].slice(0, 8) : ''}`);
|
|
280
|
-
} catch (e) {
|
|
281
|
-
if (!quiet) log(`${req.method} ${req.url} → 500 ${e?.stack || e}`);
|
|
282
|
-
if (!res.headersSent) res.writeHead(500, { 'content-type': JSON_CT });
|
|
283
|
-
res.end(JSON.stringify({ error: 'hub error', message: String(e?.message || e) }));
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, () => { server.off('error', reject); resolve(); }); });
|
|
287
|
-
const actualPort = server.address().port;
|
|
288
|
-
const url = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${actualPort}`;
|
|
289
|
-
return { hub, server, port: actualPort, url, close: () => new Promise((r) => server.close(() => r())) };
|
|
290
|
-
}
|