cursedbelt-server 4.40.2 → 4.40.4
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/dist/server/d1/pullD1.d.ts +21 -0
- package/dist/server/d1/pullD1.js +37 -11
- package/dist/server/incidents/store.js +1 -1
- package/dist/server/sharing/router.js +1 -1
- package/package.json +8 -4
- package/src/server/d1/pullD1.spec.ts +23 -2
- package/src/server/d1/pullD1.ts +39 -11
- package/src/server/incidents/incidents.spec.ts +0 -1
- package/src/server/incidents/store.ts +1 -1
- package/src/server/master-lock/guard.spec.ts +0 -1
- package/src/server/master-lock/sessions.spec.ts +3 -1
- package/src/server/sharing/router.ts +1 -1
- package/src/server/storage/files/catalogue.media-host.spec.ts +0 -2
- package/src/server/storage/storage.spec.ts +2 -2
|
@@ -86,6 +86,27 @@ export type Wrangler = (args: string[]) => {
|
|
|
86
86
|
* line until 4.22.0.
|
|
87
87
|
*/
|
|
88
88
|
export declare const wranglerArgv: (bun?: string) => string[];
|
|
89
|
+
/**
|
|
90
|
+
* Run `argv` to completion with its stdout written to a FILE, never a pipe, and return what it
|
|
91
|
+
* wrote.
|
|
92
|
+
*
|
|
93
|
+
* 🔴 A Bun child that ends with `process.exit()` drops whatever of its stdout the pipe had not
|
|
94
|
+
* taken yet, at a 64 KiB boundary — and wrangler is a Bun child whenever `node` is not on PATH
|
|
95
|
+
* (`bun x` then runs it under Bun), which is every launchd job and the station's recovery
|
|
96
|
+
* backup. Measured 2026-10-01: desk's `rows` pages came back cut at exactly 655,360 and 786,432
|
|
97
|
+
* bytes (`JSON Parse error: Unterminated string`) on every run under a launchd PATH and never in
|
|
98
|
+
* a shell with nvm's node; roms' `battery_saves` was halved to 3-row pages and flix's `films`
|
|
99
|
+
* refused 50 times in its log. A page that was cut halved and retried until it fit in the pipe,
|
|
100
|
+
* so backups stayed green but did 80 wrangler calls where 31 do. A file has no buffer to drop.
|
|
101
|
+
*/
|
|
102
|
+
export declare function spawnToFile(argv: string[], options?: {
|
|
103
|
+
cwd?: string;
|
|
104
|
+
env?: Record<string, string | undefined>;
|
|
105
|
+
}): {
|
|
106
|
+
code: number;
|
|
107
|
+
stdout: string;
|
|
108
|
+
stderr: string;
|
|
109
|
+
};
|
|
89
110
|
/** The real {@link Wrangler}: `bun x wrangler …` in `cwd`, with `env` added to this process's. */
|
|
90
111
|
export declare function spawnWrangler(options: {
|
|
91
112
|
cwd: string;
|
package/dist/server/d1/pullD1.js
CHANGED
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
* below still refuses a load short of what was read.
|
|
45
45
|
*/
|
|
46
46
|
import { Database } from 'bun:sqlite';
|
|
47
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
47
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
48
|
+
import { tmpdir } from 'node:os';
|
|
49
|
+
import { join } from 'node:path';
|
|
48
50
|
/** Which side a backup must read. Pure, so every branch is testable without a network. */
|
|
49
51
|
export function chooseBackupSource(input) {
|
|
50
52
|
if (input.fromFileFlag) {
|
|
@@ -112,17 +114,34 @@ export const PAGE_ROWS = 500;
|
|
|
112
114
|
* line until 4.22.0.
|
|
113
115
|
*/
|
|
114
116
|
export const wranglerArgv = (bun = process.execPath) => [bun, 'x', 'wrangler'];
|
|
117
|
+
/**
|
|
118
|
+
* Run `argv` to completion with its stdout written to a FILE, never a pipe, and return what it
|
|
119
|
+
* wrote.
|
|
120
|
+
*
|
|
121
|
+
* 🔴 A Bun child that ends with `process.exit()` drops whatever of its stdout the pipe had not
|
|
122
|
+
* taken yet, at a 64 KiB boundary — and wrangler is a Bun child whenever `node` is not on PATH
|
|
123
|
+
* (`bun x` then runs it under Bun), which is every launchd job and the station's recovery
|
|
124
|
+
* backup. Measured 2026-10-01: desk's `rows` pages came back cut at exactly 655,360 and 786,432
|
|
125
|
+
* bytes (`JSON Parse error: Unterminated string`) on every run under a launchd PATH and never in
|
|
126
|
+
* a shell with nvm's node; roms' `battery_saves` was halved to 3-row pages and flix's `films`
|
|
127
|
+
* refused 50 times in its log. A page that was cut halved and retried until it fit in the pipe,
|
|
128
|
+
* so backups stayed green but did 80 wrangler calls where 31 do. A file has no buffer to drop.
|
|
129
|
+
*/
|
|
130
|
+
export function spawnToFile(argv, options = {}) {
|
|
131
|
+
const dir = mkdtempSync(join(tmpdir(), 'cursedbelt-spawn-'));
|
|
132
|
+
const out = join(dir, 'stdout');
|
|
133
|
+
try {
|
|
134
|
+
const r = Bun.spawnSync(argv, { cwd: options.cwd, env: options.env, stdout: Bun.file(out), stderr: 'pipe' });
|
|
135
|
+
const stdout = existsSync(out) ? readFileSync(out, 'utf8') : '';
|
|
136
|
+
return { code: r.exitCode ?? 1, stdout, stderr: r.stderr.toString() };
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
115
142
|
/** The real {@link Wrangler}: `bun x wrangler …` in `cwd`, with `env` added to this process's. */
|
|
116
143
|
export function spawnWrangler(options) {
|
|
117
|
-
return (args) => {
|
|
118
|
-
const r = Bun.spawnSync([...wranglerArgv(), ...args], {
|
|
119
|
-
cwd: options.cwd,
|
|
120
|
-
env: { ...process.env, ...options.env },
|
|
121
|
-
stdout: 'pipe',
|
|
122
|
-
stderr: 'pipe',
|
|
123
|
-
});
|
|
124
|
-
return { code: r.exitCode ?? 1, stdout: r.stdout.toString(), stderr: r.stderr.toString() };
|
|
125
|
-
};
|
|
144
|
+
return (args) => spawnToFile([...wranglerArgv(), ...args], { cwd: options.cwd, env: { ...process.env, ...options.env } });
|
|
126
145
|
}
|
|
127
146
|
/**
|
|
128
147
|
* 🔴 Trailing NUL bytes off a `wrangler d1 export` file, in place. Returns how many were removed.
|
|
@@ -174,7 +193,14 @@ function execute(wrangler, database, sql) {
|
|
|
174
193
|
const r = wrangler(['d1', 'execute', database, '--remote', '--env=', '--json', '--command', sql]);
|
|
175
194
|
if (r.code !== 0)
|
|
176
195
|
throw new Error(r.stderr.trim() || r.stdout.trim() || `wrangler exited ${r.code}`);
|
|
177
|
-
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(r.stdout.slice(r.stdout.indexOf('[')));
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
// The byte count is the diagnosis: a multiple of 65,536 is a cut pipe (see `spawnToFile`).
|
|
202
|
+
throw new Error(`${error.message} (${r.stdout.length} bytes of stdout)`);
|
|
203
|
+
}
|
|
178
204
|
return parsed.map((statement) => statement.results ?? []);
|
|
179
205
|
}
|
|
180
206
|
/**
|
|
@@ -165,7 +165,7 @@ export function createIncidentStore(db) {
|
|
|
165
165
|
},
|
|
166
166
|
acknowledge(fp, now = Date.now()) {
|
|
167
167
|
const row = selectOne.get(fp);
|
|
168
|
-
if (
|
|
168
|
+
if (row?.state !== "open")
|
|
169
169
|
return false;
|
|
170
170
|
db.prepare(`UPDATE ${INCIDENTS_TABLE} SET state = 'acknowledged', acknowledged_at = ?
|
|
171
171
|
WHERE fingerprint = ?`).run(now, fp);
|
|
@@ -26,7 +26,7 @@ const isLevel = (v) => v === 'read' || v === 'write';
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
export function mountShareRoutes(
|
|
29
|
-
//
|
|
29
|
+
// `any` on purpose: the caller's Hono env is theirs, and this
|
|
30
30
|
// module reads nothing off it — narrowing it would force every app to name its own Env
|
|
31
31
|
// here for no gain.
|
|
32
32
|
app, options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.40.
|
|
3
|
+
"version": "4.40.4",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"verify": "bun run gate",
|
|
26
26
|
"prepublishOnly": "bun run gate --force",
|
|
27
27
|
"test:pg": "bun run scripts/testPg.ts",
|
|
28
|
-
"gate": "bun run scripts/paths.ts --only gate.ts"
|
|
28
|
+
"gate": "bun run scripts/paths.ts --only gate.ts",
|
|
29
|
+
"lint": "biome check --error-on-warnings ."
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"dist",
|
|
@@ -352,7 +353,7 @@
|
|
|
352
353
|
"dependencies": {
|
|
353
354
|
"argon2id": "1.0.1",
|
|
354
355
|
"cursedbelt-core": "^2.1.1",
|
|
355
|
-
"cursedops": ">=0.8.1 <0.
|
|
356
|
+
"cursedops": ">=0.8.1 <0.14.0",
|
|
356
357
|
"cwip": "^4.6.0",
|
|
357
358
|
"jose": "^6.2.3"
|
|
358
359
|
},
|
|
@@ -387,7 +388,7 @@
|
|
|
387
388
|
}
|
|
388
389
|
},
|
|
389
390
|
"devDependencies": {
|
|
390
|
-
"@biomejs/biome": "^2.5.
|
|
391
|
+
"@biomejs/biome": "^2.5.14",
|
|
391
392
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
392
393
|
"@node-rs/argon2": "^2.0.2",
|
|
393
394
|
"@types/bun": "^1.3.14",
|
|
@@ -408,6 +409,9 @@
|
|
|
408
409
|
{
|
|
409
410
|
"script": "paths"
|
|
410
411
|
},
|
|
412
|
+
{
|
|
413
|
+
"script": "lint"
|
|
414
|
+
},
|
|
411
415
|
{
|
|
412
416
|
"script": "typecheck"
|
|
413
417
|
},
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type Wrangler,
|
|
13
13
|
wranglerArgv,
|
|
14
14
|
} from './localBackup.js';
|
|
15
|
-
import { PAGE_ROWS, pageSql, tablePlan } from './pullD1.js';
|
|
15
|
+
import { PAGE_ROWS, pageSql, spawnToFile, tablePlan } from './pullD1.js';
|
|
16
16
|
|
|
17
17
|
const dirs: string[] = [];
|
|
18
18
|
const scratch = (): string => {
|
|
@@ -58,7 +58,7 @@ describe('🔴 chooseBackupSource — the one whose failure looks like a green b
|
|
|
58
58
|
|
|
59
59
|
describe('servedRuntime', () => {
|
|
60
60
|
const serve = (body: unknown, status = 200) =>
|
|
61
|
-
Bun.serve({ port: 0, fetch: () => Response.json(body, { status }) });
|
|
61
|
+
Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => Response.json(body, { status }) });
|
|
62
62
|
|
|
63
63
|
test('reads `runtime` off /healthz', async () => {
|
|
64
64
|
const server = serve({ runtime: 'worker' });
|
|
@@ -361,6 +361,27 @@ END;`;
|
|
|
361
361
|
expect(wranglerArgv('/x/bun')).toEqual(['/x/bun', 'x', 'wrangler']);
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
+
test('🔴 spawnToFile keeps ALL of a Bun child\'s stdout — a pipe loses the tail at a 64 KiB boundary', async () => {
|
|
365
|
+
// wrangler under `bun x` with no node on PATH (every launchd job) is exactly this child.
|
|
366
|
+
const dir = scratch();
|
|
367
|
+
const child = join(dir, 'big.js');
|
|
368
|
+
writeFileSync(child, "process.stdout.write('[' + JSON.stringify('x'.repeat(2_000_000)) + ']'); process.exit(0);");
|
|
369
|
+
const proc = Bun.spawn([process.execPath, child], { stdout: 'pipe' });
|
|
370
|
+
const piped = (await new Response(proc.stdout).text()).length;
|
|
371
|
+
await proc.exited;
|
|
372
|
+
// The failure this exists for: if Bun ever stops cutting the pipe, this line says so.
|
|
373
|
+
expect(piped).toBeLessThan(2_000_004);
|
|
374
|
+
const r = spawnToFile([process.execPath, child]);
|
|
375
|
+
expect(r.code).toBe(0);
|
|
376
|
+
expect(r.stdout.length).toBe(2_000_004);
|
|
377
|
+
expect(JSON.parse(r.stdout)).toHaveLength(1);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('a cut page names its byte count, so a 64 KiB multiple is visible in the log', async () => {
|
|
381
|
+
const cut: Wrangler = () => ({ code: 0, stdout: '[{"results":[{"name":"ite', stderr: '' });
|
|
382
|
+
await expect(pull(scratch(), cut)).rejects.toThrow(/\(25 bytes of stdout\)/);
|
|
383
|
+
});
|
|
384
|
+
|
|
364
385
|
test('🔴 a pull with no rows at all THROWS — never an empty library dated today', async () => {
|
|
365
386
|
await expect(pull(scratch(), d1(live()).run)).rejects.toThrow(/no rows/);
|
|
366
387
|
});
|
package/src/server/d1/pullD1.ts
CHANGED
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
import { Database } from 'bun:sqlite';
|
|
48
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
48
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
49
|
+
import { tmpdir } from 'node:os';
|
|
50
|
+
import { join } from 'node:path';
|
|
49
51
|
|
|
50
52
|
export type BackupSourceChoice = { kind: 'file' | 'd1' | 'refuse'; why: string };
|
|
51
53
|
|
|
@@ -132,17 +134,37 @@ export type Wrangler = (args: string[]) => { code: number; stdout: string; stder
|
|
|
132
134
|
*/
|
|
133
135
|
export const wranglerArgv = (bun: string = process.execPath): string[] => [bun, 'x', 'wrangler'];
|
|
134
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Run `argv` to completion with its stdout written to a FILE, never a pipe, and return what it
|
|
139
|
+
* wrote.
|
|
140
|
+
*
|
|
141
|
+
* 🔴 A Bun child that ends with `process.exit()` drops whatever of its stdout the pipe had not
|
|
142
|
+
* taken yet, at a 64 KiB boundary — and wrangler is a Bun child whenever `node` is not on PATH
|
|
143
|
+
* (`bun x` then runs it under Bun), which is every launchd job and the station's recovery
|
|
144
|
+
* backup. Measured 2026-10-01: desk's `rows` pages came back cut at exactly 655,360 and 786,432
|
|
145
|
+
* bytes (`JSON Parse error: Unterminated string`) on every run under a launchd PATH and never in
|
|
146
|
+
* a shell with nvm's node; roms' `battery_saves` was halved to 3-row pages and flix's `films`
|
|
147
|
+
* refused 50 times in its log. A page that was cut halved and retried until it fit in the pipe,
|
|
148
|
+
* so backups stayed green but did 80 wrangler calls where 31 do. A file has no buffer to drop.
|
|
149
|
+
*/
|
|
150
|
+
export function spawnToFile(
|
|
151
|
+
argv: string[],
|
|
152
|
+
options: { cwd?: string; env?: Record<string, string | undefined> } = {},
|
|
153
|
+
): { code: number; stdout: string; stderr: string } {
|
|
154
|
+
const dir = mkdtempSync(join(tmpdir(), 'cursedbelt-spawn-'));
|
|
155
|
+
const out = join(dir, 'stdout');
|
|
156
|
+
try {
|
|
157
|
+
const r = Bun.spawnSync(argv, { cwd: options.cwd, env: options.env, stdout: Bun.file(out), stderr: 'pipe' });
|
|
158
|
+
const stdout = existsSync(out) ? readFileSync(out, 'utf8') : '';
|
|
159
|
+
return { code: r.exitCode ?? 1, stdout, stderr: r.stderr.toString() };
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(dir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
135
165
|
/** The real {@link Wrangler}: `bun x wrangler …` in `cwd`, with `env` added to this process's. */
|
|
136
166
|
export function spawnWrangler(options: { cwd: string; env?: Record<string, string | undefined> }): Wrangler {
|
|
137
|
-
return (args) => {
|
|
138
|
-
const r = Bun.spawnSync([...wranglerArgv(), ...args], {
|
|
139
|
-
cwd: options.cwd,
|
|
140
|
-
env: { ...process.env, ...options.env },
|
|
141
|
-
stdout: 'pipe',
|
|
142
|
-
stderr: 'pipe',
|
|
143
|
-
});
|
|
144
|
-
return { code: r.exitCode ?? 1, stdout: r.stdout.toString(), stderr: r.stderr.toString() };
|
|
145
|
-
};
|
|
167
|
+
return (args) => spawnToFile([...wranglerArgv(), ...args], { cwd: options.cwd, env: { ...process.env, ...options.env } });
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
/**
|
|
@@ -216,7 +238,13 @@ const sqlText = (text: string): string => `'${text.replaceAll("'", "''")}'`;
|
|
|
216
238
|
function execute(wrangler: Wrangler, database: string, sql: string): Array<Array<Record<string, unknown>>> {
|
|
217
239
|
const r = wrangler(['d1', 'execute', database, '--remote', '--env=', '--json', '--command', sql]);
|
|
218
240
|
if (r.code !== 0) throw new Error(r.stderr.trim() || r.stdout.trim() || `wrangler exited ${r.code}`);
|
|
219
|
-
|
|
241
|
+
let parsed: Array<{ results?: Array<Record<string, unknown>> }>;
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(r.stdout.slice(r.stdout.indexOf('[')));
|
|
244
|
+
} catch (error) {
|
|
245
|
+
// The byte count is the diagnosis: a multiple of 65,536 is a cut pipe (see `spawnToFile`).
|
|
246
|
+
throw new Error(`${(error as Error).message} (${r.stdout.length} bytes of stdout)`);
|
|
247
|
+
}
|
|
220
248
|
return parsed.map((statement) => statement.results ?? []);
|
|
221
249
|
}
|
|
222
250
|
|
|
@@ -208,7 +208,6 @@ describe("what a message may not contain or travel down", () => {
|
|
|
208
208
|
store.reconcile("ingest", [
|
|
209
209
|
{
|
|
210
210
|
...stuckIngest(),
|
|
211
|
-
// biome-ignore lint/suspicious/noExplicitAny: the point of the test is the wrong type
|
|
212
211
|
facts: { failed: 42, firstFile: "Mom's 60th birthday.mov" as any },
|
|
213
212
|
},
|
|
214
213
|
]),
|
|
@@ -316,7 +316,7 @@ export function createIncidentStore(db: Database): IncidentStore {
|
|
|
316
316
|
|
|
317
317
|
acknowledge(fp, now = Date.now()) {
|
|
318
318
|
const row = selectOne.get(fp) as Row | null;
|
|
319
|
-
if (
|
|
319
|
+
if (row?.state !== "open") return false;
|
|
320
320
|
db.prepare(
|
|
321
321
|
`UPDATE ${INCIDENTS_TABLE} SET state = 'acknowledged', acknowledged_at = ?
|
|
322
322
|
WHERE fingerprint = ?`,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
MASTER_LOCK_STATE_HEADER,
|
|
12
12
|
type MasterLockKdfParams,
|
|
13
13
|
deriveMasterLockVerifier,
|
|
14
|
-
newMasterLockKdfParams,
|
|
15
14
|
} from "cursedbelt-core/master-lock";
|
|
16
15
|
import { createMasterLockGuard } from "./guard.js";
|
|
17
16
|
import { MasterLock } from "./masterLock.js";
|
|
@@ -107,7 +107,9 @@ describe("🔴 two devices in flight together (task 2138)", () => {
|
|
|
107
107
|
let pending: string | null = null;
|
|
108
108
|
const lock = new MasterLock({
|
|
109
109
|
store: record,
|
|
110
|
-
state: { read: () => seen, write: (json) =>
|
|
110
|
+
state: { read: () => seen, write: (json) => {
|
|
111
|
+
pending = json;
|
|
112
|
+
} },
|
|
111
113
|
seedJson: SEED,
|
|
112
114
|
});
|
|
113
115
|
return { lock, settle: () => pending !== null && createKvMasterLockStore(kv, MASTER_LOCK_STATE_KEY).write(pending) };
|
|
@@ -126,7 +126,7 @@ const isLevel = (v: unknown): v is ShareLevel => v === 'read' || v === 'write';
|
|
|
126
126
|
* ```
|
|
127
127
|
*/
|
|
128
128
|
export function mountShareRoutes<V extends ShareRouterViewer>(
|
|
129
|
-
//
|
|
129
|
+
// `any` on purpose: the caller's Hono env is theirs, and this
|
|
130
130
|
// module reads nothing off it — narrowing it would force every app to name its own Env
|
|
131
131
|
// here for no gain.
|
|
132
132
|
app: Hono<any>,
|
|
@@ -17,8 +17,6 @@ import { bytesStream } from 'cursedbelt-core/testing';
|
|
|
17
17
|
const CTX: MediaCtx = { userId: 'u1', tenantId: 't1' };
|
|
18
18
|
const OWNER = CTX.userId;
|
|
19
19
|
const stream = bytesStream;
|
|
20
|
-
const drain = async (s: ReadableStream<Uint8Array>): Promise<Uint8Array> =>
|
|
21
|
-
new Uint8Array(await new Response(s).arrayBuffer());
|
|
22
20
|
|
|
23
21
|
let db: Database;
|
|
24
22
|
let root: string;
|
|
@@ -131,9 +131,9 @@ describe('diskStore + serveRange', () => {
|
|
|
131
131
|
await store.putStream('fs/tenant1/live.bin', body, 'application/octet-stream');
|
|
132
132
|
return c.json({ ok: true });
|
|
133
133
|
});
|
|
134
|
-
const server = Bun.serve({ fetch: app.fetch, port: 0 });
|
|
134
|
+
const server = Bun.serve({ hostname: "127.0.0.1", fetch: app.fetch, port: 0 });
|
|
135
135
|
try {
|
|
136
|
-
const res = await fetch(`http://
|
|
136
|
+
const res = await fetch(`http://127.0.0.1:${server.port}/u`, {
|
|
137
137
|
method: 'PUT',
|
|
138
138
|
body: 'live request bytes',
|
|
139
139
|
});
|