@fastls/fastls 0.0.0-stage → 0.10.1

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/native.js ADDED
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Native = void 0;
7
+ exports.parseNativeJSON = parseNativeJSON;
8
+ exports.encodeNativeJSON = encodeNativeJSON;
9
+ exports.asHandle = asHandle;
10
+ const koffi_1 = __importDefault(require("koffi"));
11
+ const lossless_json_1 = require("lossless-json");
12
+ const library_path_js_1 = require("./library-path.js");
13
+ const node_worker_threads_1 = require("node:worker_threads");
14
+ const errors_js_1 = require("./errors.js");
15
+ function parseNativeJSON(text) {
16
+ return (0, lossless_json_1.parse)(text, undefined, (token) => {
17
+ const number = Number(token);
18
+ return /^-?\d+$/.test(token) && !Number.isSafeInteger(number) ? BigInt(token) : number;
19
+ });
20
+ }
21
+ function encodeNativeJSON(value, limit = 1024 * 1024) {
22
+ const raw = Buffer.from((0, lossless_json_1.stringify)(value), "utf8");
23
+ if (!raw.length || raw.length > limit)
24
+ throw new RangeError(`native JSON exceeds ${limit} bytes`);
25
+ return raw;
26
+ }
27
+ function asHandle(value) {
28
+ if (typeof value !== "bigint" && !(typeof value === "number" && Number.isSafeInteger(value))) {
29
+ throw new errors_js_1.FastlsError("native library returned an invalid handle");
30
+ }
31
+ const handle = BigInt(value);
32
+ if (handle < 1n || handle > 0xffffffffffffffffn)
33
+ throw new errors_js_1.FastlsError("invalid uint64 handle");
34
+ return handle;
35
+ }
36
+ // Keep successful and partially loaded libraries alive until process exit.
37
+ // Worker teardown may unload a Go runtime, so worker_threads are not yet supported.
38
+ const libraries = new Map();
39
+ const bindings = new Map();
40
+ const pinned = new Set();
41
+ let systemLoader;
42
+ function pinLibrary(path) {
43
+ if (pinned.has(path))
44
+ return;
45
+ // N-API finalizers can unload libraries during environment teardown even when
46
+ // JavaScript retained them. Go runtime threads require OS-level process lifetime.
47
+ systemLoader ??= koffi_1.default.load(process.platform === "win32" ? "kernel32.dll" : null);
48
+ if (process.platform === "win32") {
49
+ const module = [null];
50
+ const pin = systemLoader.func("int __stdcall GetModuleHandleExW(uint32_t flags, str16 name, _Out_ void **module)");
51
+ if (!pin(1, path, module))
52
+ throw new errors_js_1.FastlsError("cannot pin the Go runtime library for process lifetime");
53
+ }
54
+ else {
55
+ const dlopen = systemLoader.func("void *dlopen(str path, int flags)");
56
+ // RTLD_NOW | RTLD_NODELETE; values from Darwin / glibc dlfcn.h.
57
+ if (!dlopen(path, 2 | (process.platform === "darwin" ? 0x80 : 0x1000))) {
58
+ throw new errors_js_1.FastlsError("cannot pin the Go runtime library for process lifetime");
59
+ }
60
+ }
61
+ pinned.add(path);
62
+ }
63
+ class Native {
64
+ functions = {};
65
+ notificationsAvailable;
66
+ websocketAvailable;
67
+ path;
68
+ static load(library) {
69
+ if (!node_worker_threads_1.isMainThread)
70
+ throw new errors_js_1.FastlsError("Fastls must be loaded on the main thread; worker_threads are not supported yet");
71
+ const path = (0, library_path_js_1.libraryPath)(library);
72
+ let native = bindings.get(path);
73
+ if (!native) {
74
+ native = new Native(path);
75
+ bindings.set(path, native);
76
+ }
77
+ return native;
78
+ }
79
+ constructor(path) {
80
+ this.path = path;
81
+ try {
82
+ let lib = libraries.get(path);
83
+ if (!lib) {
84
+ lib = koffi_1.default.load(path);
85
+ libraries.set(path, lib);
86
+ }
87
+ pinLibrary(path);
88
+ if (lib.func("uint32_t fastls_abi_version(void)")() !== 1) {
89
+ throw new errors_js_1.FastlsError("unsupported Fastls ABI version (expected 1)");
90
+ }
91
+ const signatures = {
92
+ profiles: [], stats: [], features: [], session_create: [],
93
+ session_close: ["uint64_t"], session_close_idle: ["uint64_t"],
94
+ session_cookies: ["uint64_t", "void *", "size_t"],
95
+ start: ["void *", "size_t"], info: ["uint64_t"],
96
+ cancel: ["uint64_t"], close: ["uint64_t"],
97
+ };
98
+ for (const [name, args] of Object.entries(signatures)) {
99
+ // Return an owned pointer, never an auto-converted string: we must free it.
100
+ this.functions[name] = lib.func(`fastls_${name}`, "void *", args);
101
+ }
102
+ this.functions.free = lib.func("void fastls_free(void *ptr)");
103
+ this.functions.read = lib.func("int fastls_read(uint64_t handle, _Out_ void *buffer, size_t capacity, _Out_ size_t *length)");
104
+ this.functions.write = lib.func("int fastls_write(uint64_t handle, const void *buffer, size_t length, int end)");
105
+ const features = new Set(this.call("features"));
106
+ if (features.has("transport_context_v1")) {
107
+ this.functions.transport_create = lib.func("void *fastls_transport_create(void)");
108
+ }
109
+ let websocketAvailable = false;
110
+ if (features.has("websocket_v1")) {
111
+ try {
112
+ const start = lib.func("void *fastls_ws_start(void *json, size_t length)");
113
+ const send = lib.func("int fastls_ws_send(uint64_t handle, int opcode, const void *data, size_t length)");
114
+ const read = lib.func("int fastls_ws_read(uint64_t handle, _Out_ void *buffer, size_t capacity, _Out_ size_t *length, _Out_ int *opcode, _Out_ int *end)");
115
+ Object.assign(this.functions, { ws_start: start, ws_send: send, ws_read: read });
116
+ websocketAvailable = true;
117
+ }
118
+ catch { /* Optional as a group; HTTP stays available with old libraries. */ }
119
+ }
120
+ this.websocketAvailable = websocketAvailable;
121
+ let notificationsAvailable = false;
122
+ if (features.has("session_notifications")) {
123
+ try {
124
+ for (const [name, args] of Object.entries({ session_watch: ["uint64_t", "uint16_t", "void *", "size_t"], session_events: ["uint64_t"], session_unwatch: ["uint64_t"] })) {
125
+ this.functions[name] = lib.func(`fastls_${name}`, "void *", args);
126
+ }
127
+ this.functions.wait = lib.func("int fastls_wait(uint64_t handle, uint32_t events, uint32_t timeout_ms)");
128
+ notificationsAvailable = true;
129
+ }
130
+ catch { /* Old ABI-v1 libraries can still use polling. */ }
131
+ }
132
+ this.notificationsAvailable = notificationsAvailable;
133
+ for (const feature of ["stream_decode", "phase_timeouts", "stream_upload", "write_timeout"]) {
134
+ if (!features.has(feature))
135
+ throw new errors_js_1.FastlsError(`native feature required: ${feature}`);
136
+ }
137
+ }
138
+ catch (cause) {
139
+ throw new errors_js_1.FastlsError(`Cannot bind Fastls library at ${path}; ABI v1 with decoding, phase timeouts and streaming uploads is required`, { cause });
140
+ }
141
+ }
142
+ call(name, ...args) {
143
+ const pointer = this.functions[name](...args);
144
+ if (!pointer)
145
+ throw new errors_js_1.FastlsError("native library returned a null result");
146
+ let envelope;
147
+ try {
148
+ const json = koffi_1.default.decode.string(pointer);
149
+ // Starts, sessions and notification batches contain uint64 handles. Catalogs, headers
150
+ // and info metadata use strings and bounded numeric fields; avoid the
151
+ // allocating JS numeric parser on every poll and catalog lookup.
152
+ envelope = (name === "start" || name === "ws_start" || name === "session_create" || name === "session_events" ? parseNativeJSON(json) : JSON.parse(json));
153
+ }
154
+ finally {
155
+ this.functions.free(pointer);
156
+ }
157
+ if (!envelope?.ok)
158
+ throw new errors_js_1.FastlsError(envelope?.error ?? "invalid native result");
159
+ return envelope.result;
160
+ }
161
+ start(input) {
162
+ const raw = encodeNativeJSON(input);
163
+ return asHandle(this.call("start", raw, raw.length));
164
+ }
165
+ wsStart(input) {
166
+ if (!this.websocketAvailable)
167
+ throw new errors_js_1.FastlsError("WebSocket requires a newer Fastls native library");
168
+ const raw = encodeNativeJSON(input);
169
+ return asHandle(this.call("ws_start", raw, raw.length));
170
+ }
171
+ wsSend(handle, opcode, data) {
172
+ return this.functions.ws_send(handle, opcode, data, data.byteLength);
173
+ }
174
+ wsRead(handle, buffer) {
175
+ const length = [0], opcode = [0], end = [0];
176
+ const code = this.functions.ws_read(handle, buffer, buffer.length, length, opcode, end);
177
+ if (![0, 1, 2].includes(code) && code >= 0)
178
+ throw new errors_js_1.FastlsError("invalid WebSocket read status");
179
+ if (!Number.isSafeInteger(Number(length[0])) || Number(length[0]) < 0 || Number(length[0]) > buffer.length || ![0, 1].includes(end[0]))
180
+ throw new errors_js_1.FastlsError("invalid WebSocket read result");
181
+ return { code, length: Number(length[0]), opcode: opcode[0], end: end[0] === 1 };
182
+ }
183
+ read(handle, buffer) {
184
+ const out = [0];
185
+ const code = this.functions.read(handle, buffer, buffer.length, out);
186
+ const length = Number(out[0]);
187
+ if (!Number.isSafeInteger(length) || length < 0 || length > buffer.length) {
188
+ throw new errors_js_1.FastlsError("native library returned an invalid read length");
189
+ }
190
+ if (code < 0) {
191
+ const info = this.call("info", handle);
192
+ throw (0, errors_js_1.responseError)(info);
193
+ }
194
+ if (![0, 1, 2].includes(code))
195
+ throw new errors_js_1.FastlsError("native library returned an invalid read status");
196
+ return { code, length };
197
+ }
198
+ write(handle, buffer, end = false) {
199
+ const code = this.functions.write(handle, buffer, buffer.byteLength, Number(end));
200
+ if (code < 0) {
201
+ const info = this.call("info", handle);
202
+ if (info.state === "error")
203
+ throw (0, errors_js_1.responseError)(info);
204
+ if (info.state === "headers" || info.state === "eof")
205
+ return 2;
206
+ throw new errors_js_1.FastlsError("upload is closed or invalid");
207
+ }
208
+ return code;
209
+ }
210
+ }
211
+ exports.Native = Native;
@@ -0,0 +1,40 @@
1
+ import { type Socket } from "node:net";
2
+ import { type Native } from "./native.js";
3
+ type State = {
4
+ generation: number;
5
+ waiters: Set<() => void>;
6
+ };
7
+ /** One authenticated wakeup socket per session; bytes and state stay in the ABI. */
8
+ export declare class Notifier {
9
+ private readonly native;
10
+ private readonly session;
11
+ readonly states: Map<bigint, State>;
12
+ readonly sockets: Set<Socket>;
13
+ readonly token: NonSharedBuffer;
14
+ private server?;
15
+ private writer?;
16
+ private registered;
17
+ private closed;
18
+ private failure?;
19
+ private readonly pendingClose;
20
+ private resolveReady;
21
+ private rejectReady;
22
+ readonly ready: Promise<void>;
23
+ isReady: boolean;
24
+ setupWaiters: number;
25
+ constructor(native: Native, session: bigint);
26
+ start(): Promise<void>;
27
+ private accept;
28
+ check(): void;
29
+ track(handle: bigint): void;
30
+ generation(handle: bigint): number;
31
+ pulse(handle: bigint): void;
32
+ forget(handle: bigint): void;
33
+ wait(handle: bigint, generation: number, signal: AbortSignal): Promise<void>;
34
+ probe<T>(handle: bigint, probe: () => T | undefined, signal: AbortSignal): Promise<T>;
35
+ private dispose;
36
+ private fail;
37
+ close(): void;
38
+ closeAsync(): Promise<void>;
39
+ }
40
+ export {};
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Notifier = void 0;
4
+ const node_net_1 = require("node:net");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const promises_1 = require("node:timers/promises");
7
+ const errors_js_1 = require("./errors.js");
8
+ const native_js_1 = require("./native.js");
9
+ let budget = 32, resetScheduled = false;
10
+ function fairTurn() {
11
+ if (!resetScheduled) {
12
+ resetScheduled = true;
13
+ setImmediate(() => { budget = 32; resetScheduled = false; });
14
+ }
15
+ return budget-- > 0 ? undefined : (0, promises_1.setImmediate)();
16
+ }
17
+ /** One authenticated wakeup socket per session; bytes and state stay in the ABI. */
18
+ class Notifier {
19
+ native;
20
+ session;
21
+ states = new Map();
22
+ sockets = new Set();
23
+ token = (0, node_crypto_1.randomBytes)(32);
24
+ server;
25
+ writer;
26
+ registered = false;
27
+ closed = false;
28
+ failure;
29
+ pendingClose = new Set();
30
+ resolveReady;
31
+ rejectReady;
32
+ ready = new Promise((resolve, reject) => { this.resolveReady = resolve; this.rejectReady = reject; });
33
+ isReady = false;
34
+ setupWaiters = 0;
35
+ constructor(native, session) {
36
+ this.native = native;
37
+ this.session = session;
38
+ void this.ready.catch(() => { });
39
+ }
40
+ start() {
41
+ const timeout = setTimeout(() => {
42
+ try {
43
+ if (this.registered)
44
+ this.native.call("session_events", this.session);
45
+ this.fail(new errors_js_1.FastlsError("could not start native notifications within 3 seconds"));
46
+ }
47
+ catch (cause) {
48
+ this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause }));
49
+ }
50
+ }, 3000);
51
+ void this.ready.then(() => clearTimeout(timeout), () => clearTimeout(timeout));
52
+ try {
53
+ const server = this.server = (0, node_net_1.createServer)(socket => this.accept(socket));
54
+ server.on("error", error => this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause: error })));
55
+ server.listen({ host: "127.0.0.1", port: 0, backlog: 2 }, () => {
56
+ if (this.closed || this.failure) {
57
+ server.close();
58
+ return;
59
+ }
60
+ try {
61
+ const address = server.address();
62
+ if (!address || typeof address === "string")
63
+ throw new errors_js_1.FastlsError("invalid notification endpoint");
64
+ this.native.call("session_watch", this.session, address.port, this.token, this.token.length);
65
+ this.registered = true;
66
+ }
67
+ catch (error) {
68
+ this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause: error }));
69
+ }
70
+ });
71
+ }
72
+ catch (error) {
73
+ this.fail(new errors_js_1.FastlsError("could not start native notifications", { cause: error }));
74
+ }
75
+ return this.ready;
76
+ }
77
+ accept(socket) {
78
+ socket.on("error", error => { if (socket === this.writer)
79
+ this.fail(new errors_js_1.FastlsError("native notification connection failed", { cause: error })); });
80
+ if (this.closed || this.failure || this.writer || this.sockets.size >= 2) {
81
+ socket.destroy();
82
+ return;
83
+ }
84
+ this.sockets.add(socket);
85
+ const closing = new Promise(resolve => socket.once("close", () => { this.sockets.delete(socket); resolve(); if (socket === this.writer && !this.closed && !this.failure)
86
+ this.fail(new errors_js_1.FastlsError("native notification connection closed")); }));
87
+ this.pendingClose.add(closing);
88
+ void closing.then(() => this.pendingClose.delete(closing));
89
+ const handshake = Buffer.alloc(32);
90
+ let length = 0;
91
+ socket.setTimeout(1000, () => socket.destroy());
92
+ socket.on("data", chunk => {
93
+ try {
94
+ if (this.closed || this.failure)
95
+ return;
96
+ if (socket !== this.writer) {
97
+ const take = Math.min(32 - length, chunk.length);
98
+ chunk.copy(handshake, length, 0, take);
99
+ length += take;
100
+ chunk = chunk.subarray(take);
101
+ if (length !== 32)
102
+ return;
103
+ if (!(0, node_crypto_1.timingSafeEqual)(handshake, this.token) || this.writer) {
104
+ socket.destroy();
105
+ return;
106
+ }
107
+ this.writer = socket;
108
+ socket.setTimeout(0);
109
+ this.server?.close();
110
+ for (const candidate of this.sockets)
111
+ if (candidate !== socket)
112
+ candidate.destroy();
113
+ this.isReady = true;
114
+ this.resolveReady();
115
+ }
116
+ if (!chunk.length)
117
+ return;
118
+ if (chunk.some(byte => byte !== 1))
119
+ throw new errors_js_1.FastlsError("invalid native notification packet");
120
+ const batch = this.native.call("session_events", this.session);
121
+ if (!Array.isArray(batch.handles) || batch.handles.length > 256 || typeof batch.overflow !== "boolean")
122
+ throw new errors_js_1.FastlsError("invalid native notification batch");
123
+ for (const handle of batch.overflow ? [...this.states.keys()] : batch.handles.map(native_js_1.asHandle))
124
+ this.pulse(handle);
125
+ }
126
+ catch (error) {
127
+ this.fail(new errors_js_1.FastlsError("native notifications failed", { cause: error }));
128
+ }
129
+ });
130
+ }
131
+ check() {
132
+ if (this.failure)
133
+ throw this.failure;
134
+ if (this.closed)
135
+ throw new errors_js_1.FastlsError("notification session is closed");
136
+ }
137
+ track(handle) { this.check(); this.states.set(handle, { generation: 0, waiters: new Set() }); }
138
+ generation(handle) { this.check(); return this.states.get(handle)?.generation ?? 0; }
139
+ pulse(handle) {
140
+ const state = this.states.get(handle);
141
+ if (!state)
142
+ return;
143
+ state.generation++;
144
+ for (const wake of [...state.waiters])
145
+ wake();
146
+ }
147
+ forget(handle) { const state = this.states.get(handle); this.states.delete(handle); if (state)
148
+ for (const wake of [...state.waiters])
149
+ wake(); }
150
+ wait(handle, generation, signal) {
151
+ this.check();
152
+ if (signal.aborted)
153
+ return Promise.reject(signal.reason);
154
+ const state = this.states.get(handle);
155
+ if (!state || state.generation !== generation)
156
+ return Promise.resolve();
157
+ return new Promise((resolve, reject) => {
158
+ const clean = () => { state.waiters.delete(wake); signal.removeEventListener("abort", abort); };
159
+ const wake = () => { clean(); try {
160
+ this.check();
161
+ resolve();
162
+ }
163
+ catch (error) {
164
+ reject(error);
165
+ } };
166
+ const abort = () => { clean(); reject(signal.reason); };
167
+ state.waiters.add(wake);
168
+ signal.addEventListener("abort", abort, { once: true });
169
+ });
170
+ }
171
+ async probe(handle, probe, signal) {
172
+ while (true) {
173
+ const pause = fairTurn();
174
+ if (pause)
175
+ await pause;
176
+ this.check();
177
+ if (signal.aborted)
178
+ throw signal.reason;
179
+ const generation = this.generation(handle), value = probe();
180
+ if (value !== undefined)
181
+ return value;
182
+ await this.wait(handle, generation, signal);
183
+ }
184
+ }
185
+ dispose() {
186
+ if (this.registered) {
187
+ this.registered = false;
188
+ this.native.call("session_unwatch", this.session);
189
+ }
190
+ if (this.server?.listening)
191
+ this.server.close();
192
+ for (const socket of this.sockets)
193
+ socket.destroy();
194
+ }
195
+ fail(error) {
196
+ if (this.closed || this.failure)
197
+ return;
198
+ this.failure = error;
199
+ this.rejectReady(error);
200
+ for (const handle of this.states.keys())
201
+ this.pulse(handle);
202
+ // A failing ABI cleanup must not throw out of a socket event callback.
203
+ try {
204
+ this.dispose();
205
+ }
206
+ catch {
207
+ for (const socket of this.sockets)
208
+ socket.destroy();
209
+ }
210
+ }
211
+ close() {
212
+ if (this.closed)
213
+ return;
214
+ this.closed = true;
215
+ this.rejectReady(new errors_js_1.FastlsError("notification session is closed"));
216
+ for (const handle of [...this.states.keys()])
217
+ this.forget(handle);
218
+ this.dispose();
219
+ }
220
+ async closeAsync() { this.close(); await Promise.all([...this.pendingClose]); }
221
+ }
222
+ exports.Notifier = Notifier;
@@ -0,0 +1,28 @@
1
+ import type { UploadBody } from "./upload.js";
2
+ export type QueryScalar = string | Uint8Array | number | boolean | null | undefined;
3
+ export type FormFields = Record<string, QueryScalar | readonly QueryScalar[]> | ReadonlyMap<string, QueryScalar | readonly QueryScalar[]> | readonly (readonly [string | Uint8Array, QueryScalar | readonly QueryScalar[]])[];
4
+ export type QueryParams = FormFields | string;
5
+ export type JSONValue = null | boolean | number | string | JSONValue[] | {
6
+ [key: string]: JSONValue;
7
+ };
8
+ /** Caller-owned binary reader. Return an empty array at EOF; never more than maxBytes. */
9
+ export interface MultipartReader {
10
+ name?: string;
11
+ read(maxBytes: number, signal: AbortSignal): Uint8Array | Promise<Uint8Array>;
12
+ }
13
+ export type MultipartBody = Uint8Array | MultipartReader;
14
+ export type MultipartFile = MultipartBody | readonly [string | null, MultipartBody, string?];
15
+ export type MultipartFiles = Record<string, MultipartFile> | ReadonlyMap<string, MultipartFile> | readonly (readonly [string, MultipartFile])[];
16
+ export interface EncodingOptions {
17
+ params?: QueryParams;
18
+ json?: JSONValue;
19
+ form?: FormFields;
20
+ files?: MultipartFiles;
21
+ body?: UploadBody;
22
+ duplex?: boolean;
23
+ }
24
+ /** Called before native handle allocation, with headers already merged. No file reads. */
25
+ export declare function prepare(url: string, options: EncodingOptions, headers: Record<string, string>): {
26
+ url: string;
27
+ body: UploadBody | undefined;
28
+ };
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepare = prepare;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_path_1 = require("node:path");
6
+ const upload_js_1 = require("./upload.js");
7
+ function pairs(value, name) {
8
+ if (value instanceof Map)
9
+ return [...value];
10
+ if (Array.isArray(value) && value.every(p => Array.isArray(p) && p.length === 2))
11
+ return value;
12
+ if (value && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null))
13
+ return Object.entries(value);
14
+ throw new TypeError(`${name} must be an object, Map or array of pairs`);
15
+ }
16
+ function scalar(value) {
17
+ if (typeof value === "string" || value instanceof Uint8Array)
18
+ return value;
19
+ if (typeof value === "boolean")
20
+ return value ? "True" : "False";
21
+ if (typeof value === "number" && Number.isFinite(value))
22
+ return String(value);
23
+ throw new TypeError("form/query values must be strings, bytes, finite numbers or booleans");
24
+ }
25
+ function values(value, name) {
26
+ const result = [];
27
+ for (const [key, item] of pairs(value, name)) {
28
+ if (typeof key !== "string" && !(key instanceof Uint8Array))
29
+ throw new TypeError(`${name} names must be strings or bytes`);
30
+ for (const part of Array.isArray(item) ? item : [item])
31
+ if (part !== null && part !== undefined)
32
+ result.push([key, scalar(part)]);
33
+ }
34
+ return result;
35
+ }
36
+ function quote(value) {
37
+ const bytes = typeof value === "string" ? Buffer.from(value) : value;
38
+ let encoded = "";
39
+ for (const b of bytes)
40
+ encoded += b === 32 ? "+" : (b >= 65 && b <= 90) || (b >= 97 && b <= 122) || (b >= 48 && b <= 57) || [45, 46, 95, 126].includes(b) ? String.fromCharCode(b) : `%${b.toString(16).toUpperCase().padStart(2, "0")}`;
41
+ return encoded;
42
+ }
43
+ function form(value) { return values(value, "form").map(([k, v]) => `${quote(k)}=${quote(v)}`).join("&"); }
44
+ function metadata(value) {
45
+ if (typeof value !== "string" || /[\x00-\x1f\x7f]/.test(value))
46
+ throw new TypeError("multipart metadata must be strings without control characters");
47
+ return value.replaceAll("\\", "\\\\").replaceAll('"', "%22");
48
+ }
49
+ function validateJSON(value, stack = new Set()) {
50
+ if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value)))
51
+ return;
52
+ if (!value || typeof value !== "object" || (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null))
53
+ throw new TypeError("json must contain only JSON values and finite numbers");
54
+ if (stack.has(value))
55
+ throw new TypeError("json must not contain cycles");
56
+ if ("toJSON" in value)
57
+ throw new TypeError("json does not support toJSON hooks");
58
+ stack.add(value);
59
+ if (Array.isArray(value)) {
60
+ for (let i = 0; i < value.length; i++)
61
+ validateJSON(value[i], stack);
62
+ }
63
+ else {
64
+ for (const entry of Object.values(value))
65
+ validateJSON(entry, stack);
66
+ }
67
+ stack.delete(value);
68
+ }
69
+ class Multipart {
70
+ boundary = "fastls-" + (0, node_crypto_1.randomBytes)(24).toString("hex");
71
+ parts = [];
72
+ used = false;
73
+ constructor(fields, files) {
74
+ for (const [key, value] of fields === undefined ? [] : values(fields, "form")) {
75
+ const name = typeof key === "string" ? key : new TextDecoder("utf-8", { fatal: true }).decode(key);
76
+ this.add(`Content-Disposition: form-data; name="${metadata(name)}"\r\n`, typeof value === "string" ? Buffer.from(value) : value);
77
+ }
78
+ for (const [key, spec] of pairs(files, "files")) {
79
+ let filename, body, kind;
80
+ if (Array.isArray(spec)) {
81
+ if (spec.length !== 2 && spec.length !== 3)
82
+ throw new TypeError("files tuples must be [filename, body, contentType?]");
83
+ [filename, body, kind] = spec;
84
+ }
85
+ else {
86
+ body = spec;
87
+ filename = (0, node_path_1.basename)(String(body?.name ?? key).replaceAll("\\", "/"));
88
+ }
89
+ if (!(body instanceof Uint8Array) && (!body || typeof body.read !== "function"))
90
+ throw new TypeError("files bodies must be bytes or binary readers");
91
+ let header = `Content-Disposition: form-data; name="${metadata(key)}"`;
92
+ if (filename !== null)
93
+ header += `; filename="${metadata(filename)}"`;
94
+ // Stable cross-platform subset; callers supply other media types explicitly.
95
+ const extension = typeof filename === "string" ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
96
+ kind ??= { ".txt": "text/plain", ".json": "application/json", ".html": "text/html", ".csv": "text/csv", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".pdf": "application/pdf" }[extension] ?? "application/octet-stream";
97
+ metadata(kind);
98
+ this.add(header + `\r\nContent-Type: ${kind}\r\n`, body);
99
+ }
100
+ }
101
+ add(header, body) { this.parts.push({ header: Buffer.from(`--${this.boundary}\r\n${header}\r\n`), body }); }
102
+ async *chunks(signal) {
103
+ if (this.used)
104
+ throw new TypeError("multipart bodies cannot be replayed");
105
+ this.used = true;
106
+ for (const { header, body } of this.parts) {
107
+ signal.throwIfAborted();
108
+ yield header;
109
+ if (body instanceof Uint8Array) {
110
+ for (let offset = 0; offset < body.byteLength; offset += 65536)
111
+ yield body.subarray(offset, offset + 65536);
112
+ }
113
+ else {
114
+ while (true) {
115
+ signal.throwIfAborted();
116
+ const block = await body.read(65536, signal);
117
+ if (!(block instanceof Uint8Array) || block.byteLength > 65536)
118
+ throw new TypeError("multipart readers must return at most 64KiB of bytes");
119
+ if (!block.byteLength)
120
+ break;
121
+ yield block;
122
+ }
123
+ }
124
+ yield Buffer.from("\r\n");
125
+ }
126
+ const terminal = Buffer.from(`--${this.boundary}--\r\n`);
127
+ upload_js_1.terminalUploadChunks.add(terminal);
128
+ yield terminal;
129
+ }
130
+ }
131
+ /** Called before native handle allocation, with headers already merged. No file reads. */
132
+ function prepare(url, options, headers) {
133
+ const hasJSON = Object.hasOwn(options, "json");
134
+ if (hasJSON && (options.body !== undefined || options.form !== undefined || options.files !== undefined))
135
+ throw new TypeError("json cannot be combined with body, form or files");
136
+ if (options.body !== undefined && (options.form !== undefined || options.files !== undefined))
137
+ throw new TypeError("body cannot be combined with form or files");
138
+ if (options.params !== undefined) {
139
+ const query = typeof options.params === "string" ? options.params : form(options.params);
140
+ if (/[^\x00-\x7f]|#/.test(query))
141
+ throw new TypeError("raw params must be an ASCII query without a fragment");
142
+ if (query) {
143
+ const fragment = url.indexOf("#"), base = fragment < 0 ? url : url.slice(0, fragment), hash = fragment < 0 ? "" : url.slice(fragment);
144
+ url = base + (base.includes("?") ? base.endsWith("?") ? "" : "&" : "?") + query + hash;
145
+ }
146
+ }
147
+ let body = options.body, kind;
148
+ if (options.files !== undefined) {
149
+ if (options.duplex)
150
+ throw new TypeError("multipart cannot be combined with duplex");
151
+ if ("content-type" in headers)
152
+ throw new TypeError("multipart manages Content-Type and its boundary");
153
+ const multipart = new Multipart(options.form, options.files);
154
+ body = signal => multipart.chunks(signal);
155
+ kind = `multipart/form-data; boundary=${multipart.boundary}`;
156
+ }
157
+ else if (hasJSON) {
158
+ validateJSON(options.json);
159
+ body = JSON.stringify(options.json);
160
+ kind = "application/json";
161
+ }
162
+ else if (options.form !== undefined) {
163
+ body = form(options.form);
164
+ kind = "application/x-www-form-urlencoded";
165
+ }
166
+ if (kind) {
167
+ if ("content-length" in headers || "transfer-encoding" in headers)
168
+ throw new TypeError("encoded bodies manage Content-Length and Transfer-Encoding");
169
+ headers["content-type"] ??= kind;
170
+ }
171
+ return { url, body };
172
+ }