sproutboat 0.2.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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/SURFACE.md +49 -0
- package/package.json +50 -0
- package/src/assets.ts +77 -0
- package/src/broker.ts +593 -0
- package/src/build.ts +103 -0
- package/src/compile.ts +132 -0
- package/src/config.ts +241 -0
- package/src/credentials.ts +79 -0
- package/src/main.ts +322 -0
- package/src/manifest.ts +80 -0
- package/src/native-fetch-prelude.js +685 -0
- package/src/patch-porffor.ts +43 -0
- package/src/report.ts +65 -0
- package/src/source.ts +26 -0
- package/src/surface.ts +41 -0
- package/src/toolchain.ts +125 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
// Prepended to every handler by tools/compile.ts, before Porffor's native-fetch
|
|
2
|
+
// esbuild bundle. Porffor's runtime/fetch-globals.js (checked through alpha-4)
|
|
3
|
+
// gives URL (href/origin/pathname/search only) and Response without a static
|
|
4
|
+
// json(). This adds, additively, the rest of the WHATWG surface Worker code
|
|
5
|
+
// expects: URLSearchParams (read + write), URL.prototype.searchParams and the
|
|
6
|
+
// protocol/host/hostname/port/hash accessors, static Response.json,
|
|
7
|
+
// crypto.randomUUID / crypto.getRandomValues, and structuredClone. Each is
|
|
8
|
+
// feature-detected; delete a block once Porffor ships that global.
|
|
9
|
+
// Tracked upstream in patches/UPSTREAM.md.
|
|
10
|
+
//
|
|
11
|
+
// Declared before it is referenced: a getter body that names a later top-level
|
|
12
|
+
// class throws ReferenceError in Porffor (see patches/UPSTREAM.md draft B).
|
|
13
|
+
|
|
14
|
+
class __SproutboatURLSearchParams {
|
|
15
|
+
constructor(init) {
|
|
16
|
+
this._keys = [];
|
|
17
|
+
this._vals = [];
|
|
18
|
+
let raw = init == null ? '' : String(init);
|
|
19
|
+
if (raw.charCodeAt(0) === 63) raw = raw.slice(1); // strip a leading '?'
|
|
20
|
+
if (raw.length === 0) return;
|
|
21
|
+
const pairs = raw.split('&');
|
|
22
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
23
|
+
const pair = pairs[i];
|
|
24
|
+
if (pair.length === 0) continue;
|
|
25
|
+
const eq = pair.indexOf('=');
|
|
26
|
+
const k = eq === -1 ? pair : pair.slice(0, eq);
|
|
27
|
+
const v = eq === -1 ? '' : pair.slice(eq + 1);
|
|
28
|
+
this._keys.push(decodeURIComponent(k.split('+').join(' ')));
|
|
29
|
+
this._vals.push(decodeURIComponent(v.split('+').join(' ')));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
get(name) { for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) return this._vals[i]; return null; }
|
|
33
|
+
getAll(name) { const out = []; for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) out.push(this._vals[i]); return out; }
|
|
34
|
+
has(name) { for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) return true; return false; }
|
|
35
|
+
forEach(cb) { for (let i = 0; i < this._keys.length; i++) cb(this._vals[i], this._keys[i], this); }
|
|
36
|
+
// Mutators: standalone `new URLSearchParams()` building works. They do NOT
|
|
37
|
+
// write back into a URL's `search` (Porffor's URL has no setter) — build the
|
|
38
|
+
// string with toString() and assign it yourself.
|
|
39
|
+
append(name, value) { this._keys.push(String(name)); this._vals.push(String(value)); }
|
|
40
|
+
set(name, value) {
|
|
41
|
+
let found = false;
|
|
42
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
43
|
+
if (this._keys[i] !== name) continue;
|
|
44
|
+
if (found) { this._keys.splice(i, 1); this._vals.splice(i, 1); i--; }
|
|
45
|
+
else { this._vals[i] = String(value); found = true; }
|
|
46
|
+
}
|
|
47
|
+
if (!found) this.append(name, value);
|
|
48
|
+
}
|
|
49
|
+
delete(name) {
|
|
50
|
+
for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) { this._keys.splice(i, 1); this._vals.splice(i, 1); i--; }
|
|
51
|
+
}
|
|
52
|
+
sort() {
|
|
53
|
+
const idx = this._keys.map((_, i) => i).sort((a, b) => (this._keys[a] < this._keys[b] ? -1 : this._keys[a] > this._keys[b] ? 1 : 0));
|
|
54
|
+
this._keys = idx.map((i) => this._keys[i]);
|
|
55
|
+
this._vals = idx.map((i) => this._vals[i]);
|
|
56
|
+
}
|
|
57
|
+
keys() { return this._keys.slice(); }
|
|
58
|
+
values() { return this._vals.slice(); }
|
|
59
|
+
get size() { return this._keys.length; }
|
|
60
|
+
toString() {
|
|
61
|
+
let out = '';
|
|
62
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
63
|
+
if (i > 0) out += '&';
|
|
64
|
+
out += encodeURIComponent(this._keys[i]) + '=' + encodeURIComponent(this._vals[i]);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// URL and Response are always defined by Porffor's fetch-globals.js banner.
|
|
71
|
+
if (globalThis.URLSearchParams == null) globalThis.URLSearchParams = __SproutboatURLSearchParams;
|
|
72
|
+
|
|
73
|
+
if (!('searchParams' in URL.prototype)) {
|
|
74
|
+
Object.defineProperty(URL.prototype, 'searchParams', {
|
|
75
|
+
configurable: true,
|
|
76
|
+
get() {
|
|
77
|
+
if (this.__sbSearchParams == null) this.__sbSearchParams = new __SproutboatURLSearchParams(this.search);
|
|
78
|
+
return this.__sbSearchParams;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Response.json == null) {
|
|
84
|
+
Response.json = function (data, init) {
|
|
85
|
+
const response = new Response(JSON.stringify(data), init);
|
|
86
|
+
if (!response.headers.has('content-type')) response.headers.set('content-type', 'application/json;charset=utf-8');
|
|
87
|
+
return response;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Porffor's URL exposes href / origin / pathname / search only. Add the rest of
|
|
92
|
+
// the WHATWG read surface, derived from `origin` (scheme://host[:port]).
|
|
93
|
+
// `hash` is always '' server-side — browsers strip the fragment before the
|
|
94
|
+
// request, so there is nothing to recover. Tracked upstream (patches/UPSTREAM.md).
|
|
95
|
+
function __sbDefineURLAccessor(name, get) {
|
|
96
|
+
if (!(name in URL.prototype)) Object.defineProperty(URL.prototype, name, { configurable: true, get });
|
|
97
|
+
}
|
|
98
|
+
__sbDefineURLAccessor('protocol', function () {
|
|
99
|
+
const i = this.origin.indexOf('://');
|
|
100
|
+
return i === -1 ? '' : this.origin.slice(0, i + 1);
|
|
101
|
+
});
|
|
102
|
+
__sbDefineURLAccessor('host', function () {
|
|
103
|
+
const i = this.origin.indexOf('://');
|
|
104
|
+
return i === -1 ? '' : this.origin.slice(i + 3);
|
|
105
|
+
});
|
|
106
|
+
__sbDefineURLAccessor('hostname', function () {
|
|
107
|
+
const h = this.host;
|
|
108
|
+
const c = h.indexOf(':');
|
|
109
|
+
return c === -1 ? h : h.slice(0, c);
|
|
110
|
+
});
|
|
111
|
+
__sbDefineURLAccessor('port', function () {
|
|
112
|
+
const h = this.host;
|
|
113
|
+
const c = h.indexOf(':');
|
|
114
|
+
return c === -1 ? '' : h.slice(c + 1);
|
|
115
|
+
});
|
|
116
|
+
__sbDefineURLAccessor('hash', function () { return ''; });
|
|
117
|
+
__sbDefineURLAccessor('username', function () { return ''; });
|
|
118
|
+
__sbDefineURLAccessor('password', function () { return ''; });
|
|
119
|
+
|
|
120
|
+
// crypto.randomUUID / getRandomValues are absent in native-fetch. Provide them
|
|
121
|
+
// so Worker code (request ids, cache keys, idempotency keys) runs.
|
|
122
|
+
// ponytail: Math.random() is NOT cryptographically strong. Swap for a real
|
|
123
|
+
// CSPRNG the moment Porffor exposes one — do not use these for tokens/secrets.
|
|
124
|
+
if (globalThis.crypto == null) globalThis.crypto = {};
|
|
125
|
+
if (globalThis.crypto.getRandomValues == null) {
|
|
126
|
+
globalThis.crypto.getRandomValues = function (view) {
|
|
127
|
+
for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
|
|
128
|
+
return view;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// structuredClone: JSON round-trip. Lossy (no Map/Set/Date/typed arrays), but
|
|
132
|
+
// covers the common "deep-copy a plain object" case Worker code relies on.
|
|
133
|
+
if (globalThis.structuredClone == null) {
|
|
134
|
+
// The suggested fix (use structuredClone) is circular — this IS the polyfill,
|
|
135
|
+
// and Porffor exposes no other deep-clone primitive.
|
|
136
|
+
// react-doctor-disable-next-line react-doctor/no-json-parse-stringify-clone
|
|
137
|
+
globalThis.structuredClone = function (value) { return JSON.parse(JSON.stringify(value)); };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (globalThis.crypto.randomUUID == null) {
|
|
141
|
+
globalThis.crypto.randomUUID = function () {
|
|
142
|
+
const b = new Uint8Array(16);
|
|
143
|
+
globalThis.crypto.getRandomValues(b);
|
|
144
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
145
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
146
|
+
const h = [];
|
|
147
|
+
for (let i = 0; i < 16; i++) h.push((b[i] + 0x100).toString(16).slice(1));
|
|
148
|
+
return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Bindings: env.<KV>, env.<SECRET>, env.<D1>, env.<R2>, and globalThis.fetch,
|
|
154
|
+
// backed by a Bun broker on a loopback TCP port. The transport is inline C — blocking
|
|
155
|
+
// socket/connect/write/read per call (http-sync-v0: one worker event-loop turn
|
|
156
|
+
// per request, so a blocking roundtrip is acceptable). Wire frame:
|
|
157
|
+
// [u32 LE len][ <token> "\n" <json> ] reply: [u32 LE len][ <json> ]
|
|
158
|
+
// SB_BROKER_PORT / SB_BROKER_TOKEN are set by the supervisor next to $PORT.
|
|
159
|
+
// If SB_BROKER_PORT is unset the shims below are never installed (compile.ts
|
|
160
|
+
// only emits the __sbInstallBindings call when the project declares bindings),
|
|
161
|
+
// so a plain worker is byte-for-byte unchanged.
|
|
162
|
+
// ponytail: blocking IO, fresh connection per call, text values only. Connection
|
|
163
|
+
// pooling + binary values + non-blocking = v2.
|
|
164
|
+
|
|
165
|
+
Porffor.c`
|
|
166
|
+
#include <sys/socket.h>
|
|
167
|
+
#include <netinet/in.h>
|
|
168
|
+
#include <unistd.h>
|
|
169
|
+
#include <string.h>
|
|
170
|
+
#include <stdlib.h>
|
|
171
|
+
#include <stdio.h>
|
|
172
|
+
|
|
173
|
+
u32 porf_native_fetch_alloc_bytestring(const char* input, size_t len);
|
|
174
|
+
int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned);
|
|
175
|
+
|
|
176
|
+
static int sb_io_all(int fd, unsigned char* buf, size_t len, int writing) {
|
|
177
|
+
size_t done = 0;
|
|
178
|
+
while (done < len) {
|
|
179
|
+
long n = writing ? write(fd, buf + done, len - done) : read(fd, buf + done, len - done);
|
|
180
|
+
if (n <= 0) return -1;
|
|
181
|
+
done += (size_t)n;
|
|
182
|
+
}
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static int sb_broker_roundtrip(const char* req, size_t req_len, char** resp_out, size_t* resp_len_out) {
|
|
187
|
+
*resp_out = NULL;
|
|
188
|
+
*resp_len_out = 0;
|
|
189
|
+
|
|
190
|
+
const char* port_s = getenv("SB_BROKER_PORT");
|
|
191
|
+
if (!port_s) return -10;
|
|
192
|
+
int port = atoi(port_s);
|
|
193
|
+
const char* tok = getenv("SB_BROKER_TOKEN");
|
|
194
|
+
size_t tok_len = tok ? strlen(tok) : 0;
|
|
195
|
+
|
|
196
|
+
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
197
|
+
if (fd < 0) return -1;
|
|
198
|
+
|
|
199
|
+
struct sockaddr_in addr;
|
|
200
|
+
memset(&addr, 0, sizeof(addr));
|
|
201
|
+
addr.sin_family = AF_INET;
|
|
202
|
+
addr.sin_port = htons((unsigned short)port);
|
|
203
|
+
addr.sin_addr.s_addr = htonl(0x7f000001u); // 127.0.0.1
|
|
204
|
+
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { close(fd); return -2; }
|
|
205
|
+
|
|
206
|
+
// frame body: token "\n" json
|
|
207
|
+
size_t body_len = tok_len + 1 + req_len;
|
|
208
|
+
unsigned char* frame = (unsigned char*)malloc(4 + body_len);
|
|
209
|
+
if (!frame) { close(fd); return -5; }
|
|
210
|
+
frame[0] = (unsigned char)(body_len & 0xff);
|
|
211
|
+
frame[1] = (unsigned char)((body_len >> 8) & 0xff);
|
|
212
|
+
frame[2] = (unsigned char)((body_len >> 16) & 0xff);
|
|
213
|
+
frame[3] = (unsigned char)((body_len >> 24) & 0xff);
|
|
214
|
+
if (tok_len) memcpy(frame + 4, tok, tok_len);
|
|
215
|
+
frame[4 + tok_len] = '\n';
|
|
216
|
+
if (req_len) memcpy(frame + 4 + tok_len + 1, req, req_len);
|
|
217
|
+
int wr = sb_io_all(fd, frame, 4 + body_len, 1);
|
|
218
|
+
free(frame);
|
|
219
|
+
if (wr != 0) { close(fd); return -3; }
|
|
220
|
+
|
|
221
|
+
unsigned char rhdr[4];
|
|
222
|
+
if (sb_io_all(fd, rhdr, 4, 0) != 0) { close(fd); return -4; }
|
|
223
|
+
size_t rlen = (size_t)rhdr[0] | ((size_t)rhdr[1] << 8) | ((size_t)rhdr[2] << 16) | ((size_t)rhdr[3] << 24);
|
|
224
|
+
|
|
225
|
+
char* buf = (char*)malloc(rlen ? rlen : 1);
|
|
226
|
+
if (!buf) { close(fd); return -5; }
|
|
227
|
+
if (rlen && sb_io_all(fd, (unsigned char*)buf, rlen, 0) != 0) { free(buf); close(fd); return -6; }
|
|
228
|
+
close(fd);
|
|
229
|
+
|
|
230
|
+
*resp_out = buf;
|
|
231
|
+
*resp_len_out = rlen;
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
`;
|
|
235
|
+
|
|
236
|
+
// One request string in, one reply string out. `reqJson` is a parameter, so the
|
|
237
|
+
// generated C names it directly in the RawC block below.
|
|
238
|
+
function __sbCall(reqJson) {
|
|
239
|
+
let res = '';
|
|
240
|
+
Porffor.c`
|
|
241
|
+
const char* __req; size_t __reqlen; char* __reqowned = 0;
|
|
242
|
+
porf_native_fetch_read_value(reqJson, &__req, &__reqlen, &__reqowned);
|
|
243
|
+
char* __resp = 0; size_t __resplen = 0;
|
|
244
|
+
int __rc = sb_broker_roundtrip(__req, __reqlen, &__resp, &__resplen);
|
|
245
|
+
if (__reqowned) free(__reqowned);
|
|
246
|
+
if (__rc == 0) {
|
|
247
|
+
res = porf_box((f64)porf_native_fetch_alloc_bytestring(__resp, __resplen), 195);
|
|
248
|
+
free(__resp);
|
|
249
|
+
} else {
|
|
250
|
+
char __e[40];
|
|
251
|
+
int __n = snprintf(__e, sizeof(__e), "{\"ok\":false,\"error\":\"broker rc %d\"}", __rc);
|
|
252
|
+
res = porf_box((f64)porf_native_fetch_alloc_bytestring(__e, (size_t)__n), 195);
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
return res;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function __sbRpc(op, extra) {
|
|
259
|
+
const req = { op };
|
|
260
|
+
if (extra) for (const k in extra) req[k] = extra[k];
|
|
261
|
+
const reply = JSON.parse(__sbCall(JSON.stringify(req)));
|
|
262
|
+
if (reply && reply.ok === false) throw new Error(`sproutboat ${op}: ${reply.error || 'failed'}`);
|
|
263
|
+
return reply;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// D1: a Cloudflare-shaped `env.<DB>` (prepare / bind / all / run / raw / first,
|
|
267
|
+
// plus batch and exec). Every call is one broker roundtrip.
|
|
268
|
+
function __sbMakeD1(dbName) {
|
|
269
|
+
function stmt(sql, params) {
|
|
270
|
+
const s = {
|
|
271
|
+
__sql: sql,
|
|
272
|
+
__params: params,
|
|
273
|
+
bind() { return stmt(sql, Array.prototype.slice.call(arguments)); },
|
|
274
|
+
all() {
|
|
275
|
+
const r = __sbRpc('d1.query', { db: dbName, sql, params });
|
|
276
|
+
return { results: r.results || [], success: true, meta: r.meta || {} };
|
|
277
|
+
},
|
|
278
|
+
run() { return s.all(); },
|
|
279
|
+
raw() {
|
|
280
|
+
const rows = s.all().results;
|
|
281
|
+
const out = [];
|
|
282
|
+
for (let i = 0; i < rows.length; i++) {
|
|
283
|
+
const cols = [];
|
|
284
|
+
for (const k in rows[i]) cols.push(rows[i][k]);
|
|
285
|
+
out.push(cols);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
},
|
|
289
|
+
first(column) {
|
|
290
|
+
const rows = s.all().results;
|
|
291
|
+
if (rows.length === 0) return null;
|
|
292
|
+
return column == null ? rows[0] : rows[0][column];
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
return s;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
prepare(sql) { return stmt(String(sql), []); },
|
|
299
|
+
batch(statements) {
|
|
300
|
+
const list = [];
|
|
301
|
+
for (let i = 0; i < (statements || []).length; i++) list.push({ sql: statements[i].__sql, params: statements[i].__params });
|
|
302
|
+
const r = __sbRpc('d1.batch', { db: dbName, statements: list });
|
|
303
|
+
const out = [];
|
|
304
|
+
for (let i = 0; i < (r.results || []).length; i++) out.push({ results: r.results[i].results || [], success: true, meta: r.results[i].meta || {} });
|
|
305
|
+
return out;
|
|
306
|
+
},
|
|
307
|
+
exec(sql) {
|
|
308
|
+
__sbRpc('d1.exec', { db: dbName, sql: String(sql) });
|
|
309
|
+
return { count: (String(sql).match(/;/g) || []).length, duration: 0 };
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// R2: a Cloudflare-shaped object. When `body` is present the sync accessors
|
|
315
|
+
// mirror R2ObjectBody's async ones (a worker may `await` them harmlessly).
|
|
316
|
+
function __sbR2Object(meta, body) {
|
|
317
|
+
const obj = {
|
|
318
|
+
key: meta.key,
|
|
319
|
+
size: meta.size,
|
|
320
|
+
etag: meta.etag,
|
|
321
|
+
httpEtag: '"' + meta.etag + '"',
|
|
322
|
+
uploaded: meta.uploaded,
|
|
323
|
+
httpMetadata: meta.httpMetadata || {},
|
|
324
|
+
customMetadata: meta.customMetadata || {},
|
|
325
|
+
};
|
|
326
|
+
if (body != null) {
|
|
327
|
+
obj.body = body;
|
|
328
|
+
obj.text = function () { return body; };
|
|
329
|
+
obj.json = function () { return JSON.parse(body); };
|
|
330
|
+
}
|
|
331
|
+
return obj;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Installed only when the project declares bindings. `env` is the module-scoped
|
|
335
|
+
// object from compile.ts (a `const`, but mutable); we add the binding accessors
|
|
336
|
+
// to it in place. compile.ts emits `__sbInstallBindings(env, {...})` right after
|
|
337
|
+
// the `const env = {...}` line.
|
|
338
|
+
globalThis.__sbInstallBindings = function (target, bindings) {
|
|
339
|
+
if (!target) return;
|
|
340
|
+
|
|
341
|
+
for (let i = 0; i < (bindings.kv || []).length; i++) {
|
|
342
|
+
const ns = bindings.kv[i];
|
|
343
|
+
target[ns] = {
|
|
344
|
+
get(key) {
|
|
345
|
+
const r = __sbRpc('kv.get', { ns, key: String(key) });
|
|
346
|
+
return r.found ? r.value : null;
|
|
347
|
+
},
|
|
348
|
+
put(key, value) {
|
|
349
|
+
__sbRpc('kv.put', { ns, key: String(key), value: String(value) });
|
|
350
|
+
},
|
|
351
|
+
delete(key) {
|
|
352
|
+
__sbRpc('kv.delete', { ns, key: String(key) });
|
|
353
|
+
},
|
|
354
|
+
list(prefix) {
|
|
355
|
+
return __sbRpc('kv.list', { ns, prefix: prefix == null ? '' : String(prefix) }).keys || [];
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < (bindings.secrets || []).length; i++) {
|
|
361
|
+
const name = bindings.secrets[i];
|
|
362
|
+
Object.defineProperty(target, name, {
|
|
363
|
+
configurable: true,
|
|
364
|
+
get() { return __sbRpc('secret.get', { name }).value; },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (let i = 0; i < (bindings.d1 || []).length; i++) {
|
|
369
|
+
const name = bindings.d1[i];
|
|
370
|
+
target[name] = __sbMakeD1(name);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < (bindings.r2 || []).length; i++) {
|
|
374
|
+
const name = bindings.r2[i];
|
|
375
|
+
target[name] = {
|
|
376
|
+
put(key, value, options) {
|
|
377
|
+
const o = options || {};
|
|
378
|
+
return __sbRpc('r2.put', {
|
|
379
|
+
bucket: name,
|
|
380
|
+
key: String(key),
|
|
381
|
+
body: value == null ? '' : String(value),
|
|
382
|
+
httpMetadata: o.httpMetadata || {},
|
|
383
|
+
customMetadata: o.customMetadata || {},
|
|
384
|
+
}).object;
|
|
385
|
+
},
|
|
386
|
+
get(key) {
|
|
387
|
+
const r = __sbRpc('r2.get', { bucket: name, key: String(key) });
|
|
388
|
+
return r.found ? __sbR2Object(r.object, r.body == null ? '' : r.body) : null;
|
|
389
|
+
},
|
|
390
|
+
head(key) {
|
|
391
|
+
const r = __sbRpc('r2.head', { bucket: name, key: String(key) });
|
|
392
|
+
return r.found ? __sbR2Object(r.object, null) : null;
|
|
393
|
+
},
|
|
394
|
+
delete(key) {
|
|
395
|
+
__sbRpc('r2.delete', { bucket: name, key: String(key) });
|
|
396
|
+
},
|
|
397
|
+
list(options) {
|
|
398
|
+
const o = options || {};
|
|
399
|
+
const r = __sbRpc('r2.list', {
|
|
400
|
+
bucket: name,
|
|
401
|
+
prefix: o.prefix == null ? '' : String(o.prefix),
|
|
402
|
+
cursor: o.cursor == null ? '' : String(o.cursor),
|
|
403
|
+
limit: o.limit == null ? 1000 : o.limit,
|
|
404
|
+
});
|
|
405
|
+
const objects = [];
|
|
406
|
+
for (let j = 0; j < (r.objects || []).length; j++) objects.push(__sbR2Object(r.objects[j], null));
|
|
407
|
+
return { objects, truncated: !!r.truncated, cursor: r.cursor || undefined };
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < (bindings.queues || []).length; i++) {
|
|
413
|
+
const name = bindings.queues[i];
|
|
414
|
+
target[name] = {
|
|
415
|
+
send(body, options) {
|
|
416
|
+
const o = options || {};
|
|
417
|
+
__sbRpc('queue.send', { queue: name, body: typeof body === 'string' ? body : JSON.stringify(body), delaySeconds: o.delaySeconds || 0 });
|
|
418
|
+
},
|
|
419
|
+
sendBatch(messages) {
|
|
420
|
+
const list = [];
|
|
421
|
+
for (let j = 0; j < (messages || []).length; j++) {
|
|
422
|
+
const m = messages[j];
|
|
423
|
+
list.push({ body: typeof m.body === 'string' ? m.body : JSON.stringify(m.body), delaySeconds: (m.delaySeconds || 0) });
|
|
424
|
+
}
|
|
425
|
+
__sbRpc('queue.send_batch', { queue: name, messages: list });
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (let i = 0; i < (bindings.analytics || []).length; i++) {
|
|
431
|
+
const name = bindings.analytics[i];
|
|
432
|
+
target[name] = {
|
|
433
|
+
writeDataPoint(event) {
|
|
434
|
+
const e = event || {};
|
|
435
|
+
__sbRpc('ae.write', {
|
|
436
|
+
dataset: name,
|
|
437
|
+
indexes: e.indexes || [],
|
|
438
|
+
blobs: e.blobs || [],
|
|
439
|
+
doubles: e.doubles || [],
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
// Sproutboat extension (Cloudflare AE is write-only from a Worker — you
|
|
443
|
+
// query it via the SQL API). Returns { count, rows }.
|
|
444
|
+
query(options) {
|
|
445
|
+
const o = options || {};
|
|
446
|
+
return __sbRpc('ae.query', { dataset: name, limit: o.limit || 20 });
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (let i = 0; i < (bindings.do || []).length; i++) {
|
|
452
|
+
const b = bindings.do[i];
|
|
453
|
+
target[b.binding] = __sbMakeDONamespace(b.binding, b.className);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Static assets: env.<ASSETS>.fetch(request) -> broker `assets.get`. The edge
|
|
457
|
+
// already serves matching files directly; the worker only calls this for paths
|
|
458
|
+
// it wants to own (SPA fallback, auth-gated files). Text assets only — binary
|
|
459
|
+
// files go through the edge (the broker frame is UTF-8 JSON).
|
|
460
|
+
if (bindings.assets) {
|
|
461
|
+
target[bindings.assets] = {
|
|
462
|
+
fetch(input) {
|
|
463
|
+
let path = typeof input === 'string' ? input : String(input && input.url || '/');
|
|
464
|
+
try { path = new URL(path, 'http://a').pathname; } catch (_e) { /* use as-is */ }
|
|
465
|
+
const r = __sbRpc('assets.get', { path });
|
|
466
|
+
const headers = {};
|
|
467
|
+
if (r.type) headers['content-type'] = r.type;
|
|
468
|
+
if (r.found) headers['etag'] = '"' + r.hash + '"';
|
|
469
|
+
return new Response(r.body == null ? '' : r.body, { status: r.status || (r.found ? 200 : 404), headers });
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if ((bindings.outbound || []).length > 0) {
|
|
475
|
+
globalThis.fetch = function (input, init) {
|
|
476
|
+
const url = typeof input === 'string' ? input : String(input.url);
|
|
477
|
+
const opts = init || {};
|
|
478
|
+
const headers = [];
|
|
479
|
+
if (opts.headers) {
|
|
480
|
+
if (typeof opts.headers.forEach === 'function') opts.headers.forEach((v, k) => headers.push([k, v]));
|
|
481
|
+
else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
|
|
482
|
+
}
|
|
483
|
+
const r = __sbRpc('fetch', {
|
|
484
|
+
url,
|
|
485
|
+
method: opts.method || 'GET',
|
|
486
|
+
headers,
|
|
487
|
+
body: opts.body == null ? null : String(opts.body),
|
|
488
|
+
});
|
|
489
|
+
const respHeaders = new Headers();
|
|
490
|
+
for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
|
|
491
|
+
return new Response(r.body == null ? '' : r.body, { status: r.status || 502, headers: respHeaders });
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// Durable Objects. The class runs here in the sandboxed worker. There is exactly
|
|
498
|
+
// one worker process per deployment (the supervisor model) and the native-fetch
|
|
499
|
+
// runtime processes one turn at a time, so calls to a given object id are
|
|
500
|
+
// already serialized — `env.<NS>.get(id).fetch()` invokes the instance directly,
|
|
501
|
+
// no round-trip. Only `state.storage.*` goes to the broker (so object state
|
|
502
|
+
// outlives a worker restart), scoped to (class, id).
|
|
503
|
+
// ponytail: serialization relies on the single worker process; a multi-worker
|
|
504
|
+
// deployment needs the broker to hold a per-id lock (cloud). Storage ops are one
|
|
505
|
+
// key at a time; blockConcurrencyWhile just runs the fn.
|
|
506
|
+
|
|
507
|
+
function __sbMakeDONamespace(binding, className) {
|
|
508
|
+
return {
|
|
509
|
+
idFromName(name) { return { toString() { return 'name:' + String(name); }, name: String(name) }; },
|
|
510
|
+
idFromString(hex) { return { toString() { return String(hex); } }; },
|
|
511
|
+
newUniqueId() {
|
|
512
|
+
const id = (globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : String(Math.random()).slice(2));
|
|
513
|
+
return { toString() { return 'uid:' + id; } };
|
|
514
|
+
},
|
|
515
|
+
get(id) {
|
|
516
|
+
const idStr = typeof id === 'string' ? id : id.toString();
|
|
517
|
+
return {
|
|
518
|
+
fetch(input, init) {
|
|
519
|
+
let req;
|
|
520
|
+
if (input && typeof input === 'object' && typeof input.url === 'string' && !init) {
|
|
521
|
+
req = input;
|
|
522
|
+
} else {
|
|
523
|
+
const url = typeof input === 'string' ? input : String((input && input.url) || 'https://do/');
|
|
524
|
+
const opts = init || {};
|
|
525
|
+
const headers = new Headers();
|
|
526
|
+
if (opts.headers) {
|
|
527
|
+
if (typeof opts.headers.forEach === 'function') opts.headers.forEach((v, k) => headers.set(k, v));
|
|
528
|
+
else for (const k in opts.headers) headers.set(k, opts.headers[k]);
|
|
529
|
+
}
|
|
530
|
+
req = new Request(url, { method: opts.method || 'GET', headers });
|
|
531
|
+
if (opts.body != null) req.body = String(opts.body);
|
|
532
|
+
}
|
|
533
|
+
return __sbGetDOInstance(className, idStr).fetch(req);
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const __sbDOClasses = {};
|
|
541
|
+
const __sbDOInstances = {};
|
|
542
|
+
globalThis.__sbRegisterDO = function (map) {
|
|
543
|
+
for (const k in map) __sbDOClasses[k] = map[k];
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
function __sbGetDOInstance(cls, id) {
|
|
547
|
+
const Ctor = __sbDOClasses[cls];
|
|
548
|
+
if (!Ctor) throw new Error('no such Durable Object class: ' + cls);
|
|
549
|
+
const cacheKey = cls + ' ' + id;
|
|
550
|
+
let inst = __sbDOInstances[cacheKey];
|
|
551
|
+
if (!inst) {
|
|
552
|
+
const state = {
|
|
553
|
+
id: { toString() { return id; } },
|
|
554
|
+
storage: __sbDOStorage(cls, id),
|
|
555
|
+
blockConcurrencyWhile(fn) { return fn(); },
|
|
556
|
+
waitUntil() {},
|
|
557
|
+
};
|
|
558
|
+
inst = new Ctor(state, globalThis.env);
|
|
559
|
+
__sbDOInstances[cacheKey] = inst;
|
|
560
|
+
}
|
|
561
|
+
return inst;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function __sbDOStorage(cls, id) {
|
|
565
|
+
return {
|
|
566
|
+
get(key) {
|
|
567
|
+
if (Array.isArray(key)) {
|
|
568
|
+
const out = new Map();
|
|
569
|
+
for (let i = 0; i < key.length; i++) {
|
|
570
|
+
const r = __sbRpc('do.storage.get', { cls, id, key: String(key[i]) });
|
|
571
|
+
if (r.found) out.set(key[i], JSON.parse(r.value));
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
const r = __sbRpc('do.storage.get', { cls, id, key: String(key) });
|
|
576
|
+
return r.found ? JSON.parse(r.value) : undefined;
|
|
577
|
+
},
|
|
578
|
+
put(key, value) {
|
|
579
|
+
if (key != null && typeof key === 'object') {
|
|
580
|
+
for (const k in key) __sbRpc('do.storage.put', { cls, id, key: String(k), value: JSON.stringify(key[k]) });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
__sbRpc('do.storage.put', { cls, id, key: String(key), value: JSON.stringify(value) });
|
|
584
|
+
},
|
|
585
|
+
delete(key) {
|
|
586
|
+
if (Array.isArray(key)) {
|
|
587
|
+
let n = 0;
|
|
588
|
+
for (let i = 0; i < key.length; i++) n += __sbRpc('do.storage.delete', { cls, id, key: String(key[i]) }).deleted ? 1 : 0;
|
|
589
|
+
return n;
|
|
590
|
+
}
|
|
591
|
+
return !!__sbRpc('do.storage.delete', { cls, id, key: String(key) }).deleted;
|
|
592
|
+
},
|
|
593
|
+
deleteAll() { __sbRpc('do.storage.delete_all', { cls, id }); },
|
|
594
|
+
list(options) {
|
|
595
|
+
const o = options || {};
|
|
596
|
+
const r = __sbRpc('do.storage.list', { cls, id, prefix: o.prefix == null ? '' : String(o.prefix), limit: o.limit == null ? 1000 : o.limit });
|
|
597
|
+
const out = new Map();
|
|
598
|
+
for (let i = 0; i < (r.entries || []).length; i++) out.set(r.entries[i][0], JSON.parse(r.entries[i][1]));
|
|
599
|
+
return out;
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// Trigger dispatch. The compiled server only ever calls `fetch(request)`; this
|
|
606
|
+
// routes the internal `x-sb-trigger` requests (sent by the broker, authenticated
|
|
607
|
+
// with SB_BROKER_TOKEN) to the right user handler, and everything else to
|
|
608
|
+
// `handlers.fetch`.
|
|
609
|
+
|
|
610
|
+
function __sbEnv(name) {
|
|
611
|
+
let res = '';
|
|
612
|
+
Porffor.c`
|
|
613
|
+
const char* __n; size_t __nl; char* __no = 0;
|
|
614
|
+
porf_native_fetch_read_value(name, &__n, &__nl, &__no);
|
|
615
|
+
char __key[128];
|
|
616
|
+
size_t __kn = __nl < 127 ? __nl : 127;
|
|
617
|
+
memcpy(__key, __n, __kn); __key[__kn] = 0;
|
|
618
|
+
if (__no) free(__no);
|
|
619
|
+
const char* __v = getenv(__key);
|
|
620
|
+
if (__v) res = porf_box((f64)porf_native_fetch_alloc_bytestring(__v, strlen(__v)), 195);
|
|
621
|
+
`;
|
|
622
|
+
return res;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function __sbTriggerAuthed(request) {
|
|
626
|
+
const want = __sbEnv('SB_BROKER_TOKEN');
|
|
627
|
+
if (!want) return true; // no token configured (local/dev)
|
|
628
|
+
return request.headers.get('x-sb-token') === want;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
globalThis.__sbEntry = function (handlers, request) {
|
|
632
|
+
const trigger = request.headers.get('x-sb-trigger');
|
|
633
|
+
if (!trigger) return handlers.fetch(request);
|
|
634
|
+
if (!__sbTriggerAuthed(request)) return new Response('forbidden', { status: 403 });
|
|
635
|
+
|
|
636
|
+
if (trigger === 'scheduled') {
|
|
637
|
+
if (typeof handlers.scheduled !== 'function') return new Response('no scheduled handler', { status: 404 });
|
|
638
|
+
const body = __sbReadJson(request);
|
|
639
|
+
handlers.scheduled({ cron: body.cron || '', scheduledTime: body.scheduledTime || Date.now(), noRetry() {} });
|
|
640
|
+
return new Response('', { status: 204 });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (trigger === 'queue') {
|
|
644
|
+
if (typeof handlers.queue !== 'function') return new Response('no queue handler', { status: 404 });
|
|
645
|
+
const body = __sbReadJson(request);
|
|
646
|
+
const acked = [];
|
|
647
|
+
const retried = [];
|
|
648
|
+
const raw = body.messages || [];
|
|
649
|
+
const messages = [];
|
|
650
|
+
for (let i = 0; i < raw.length; i++) {
|
|
651
|
+
const m = raw[i];
|
|
652
|
+
const msg = {
|
|
653
|
+
id: m.id,
|
|
654
|
+
timestamp: m.timestamp,
|
|
655
|
+
attempts: m.attempts || 1,
|
|
656
|
+
body: __sbTryParse(m.body),
|
|
657
|
+
ack() { if (acked.indexOf(m.id) === -1) acked.push(m.id); },
|
|
658
|
+
retry() { if (retried.indexOf(m.id) === -1) retried.push(m.id); },
|
|
659
|
+
};
|
|
660
|
+
messages.push(msg);
|
|
661
|
+
}
|
|
662
|
+
const batch = {
|
|
663
|
+
queue: body.queue || '',
|
|
664
|
+
messages,
|
|
665
|
+
ackAll() { for (let i = 0; i < messages.length; i++) messages[i].ack(); },
|
|
666
|
+
retryAll() { for (let i = 0; i < messages.length; i++) messages[i].retry(); },
|
|
667
|
+
};
|
|
668
|
+
handlers.queue(batch);
|
|
669
|
+
// default: any message neither acked nor retried is treated as acked
|
|
670
|
+
for (let i = 0; i < messages.length; i++) {
|
|
671
|
+
if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1) acked.push(messages[i].id);
|
|
672
|
+
}
|
|
673
|
+
return new Response(JSON.stringify({ ack: acked, retry: retried }), { headers: { 'content-type': 'application/json' } });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
return new Response('unknown trigger', { status: 400 });
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
function __sbReadJson(request) {
|
|
681
|
+
try { return JSON.parse(request.body == null ? '{}' : String(request.body)); } catch (e) { return {}; }
|
|
682
|
+
}
|
|
683
|
+
function __sbTryParse(s) {
|
|
684
|
+
try { return JSON.parse(s); } catch (e) { return s; }
|
|
685
|
+
}
|