From 26a4d8cc4ca69fefd0e6c36dfbd8750bcd9a6f47 Mon Sep 17 00:00:00 2001 From: vivek1504 Date: Thu, 28 May 2026 08:38:24 +0530 Subject: [PATCH 1/6] added deployment tests --- src/deploy/firecracker.test.ts | 68 ++++++++++++++++++++++++++++++++++ src/deploy/queue.test.ts | 28 ++++++++++++++ src/deploy/rootfs.test.ts | 33 +++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 src/deploy/firecracker.test.ts create mode 100644 src/deploy/queue.test.ts create mode 100644 src/deploy/rootfs.test.ts diff --git a/src/deploy/firecracker.test.ts b/src/deploy/firecracker.test.ts new file mode 100644 index 0000000..0245e1b --- /dev/null +++ b/src/deploy/firecracker.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { waitForVMReady, waitForFile, createFcCient } from "./firecracker.js"; +import { EventEmitter } from "events"; +import fs from "fs"; + +describe("waitForVMReady", () => { + function makeFakeFC() { + return { stdout: new EventEmitter() }; + } + + it("resolves when READY arrives in one chunk", async () => { + const fc = makeFakeFC(); + const promise = waitForVMReady(fc); + fc.stdout.emit("data", Buffer.from("booting...\nREADY\n")); + await expect(promise).resolves.toBeUndefined(); + }); + + it("resolves when READY is split across chunks", async () => { + const fc = makeFakeFC(); + const promise = waitForVMReady(fc); + fc.stdout.emit("data", Buffer.from("REA")); + fc.stdout.emit("data", Buffer.from("DY")); + await expect(promise).resolves.toBeUndefined(); + }); + + it("rejects on timeout", async () => { + const fc = makeFakeFC(); + const promise = new Promise((resolve, reject) => { + let buffer = ""; + const timeout = setTimeout( + () => reject(new Error("VM startup timeout")), + 100, + ); + fc.stdout.on("data", (d: Buffer) => { + buffer += d.toString(); + if (buffer.includes("READY")) { + clearTimeout(timeout); + resolve(); + } + }); + }); + await expect(promise).rejects.toThrow("VM startup timeout"); + }); +}); + +describe("waitForFile", () => { + beforeEach(() => vi.restoreAllMocks()); + + it("returns immediately if file exists", async () => { + vi.spyOn(fs, "existsSync").mockReturnValue(true); + await expect(waitForFile("/tmp/test.sock", 1000)).resolves.toBeUndefined(); + }); + + it("throws on timeout", async () => { + vi.spyOn(fs, "existsSync").mockReturnValue(false); + await expect(waitForFile("/tmp/missing.sock", 200)).rejects.toThrow( + "timeout waiting for socket", + ); + }); +}); + +describe("createFcCient", () => { + it("creates axios client with socketPath", () => { + const client = createFcCient("/tmp/test.sock"); + expect(client.defaults.socketPath).toBe("/tmp/test.sock"); + expect(client.defaults.baseURL).toBe("http://localhost"); + }); +}); diff --git a/src/deploy/queue.test.ts b/src/deploy/queue.test.ts new file mode 100644 index 0000000..4a2a98d --- /dev/null +++ b/src/deploy/queue.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { jobs, deployQueue } from "./queue.js"; +import type { JobStatus } from "./queue.js"; + +describe("deploy queue", () => { + it("jobs map starts empty", () => { + expect(jobs.size).toBe(0); + }); + + it("deployQueue has concurrency 3", () => { + expect(deployQueue.concurrency).toBe(3); + }); + + it("can track job lifecycle", () => { + jobs.set("j1", { state: "pending" }); + expect(jobs.get("j1")?.state).toBe("pending"); + + jobs.set("j1", { state: "running" }); + expect(jobs.get("j1")?.state).toBe("running"); + + jobs.set("j1", { state: "done", functionId: "f1", url: "/f/f1" }); + const job = jobs.get("j1") as Extract; + expect(job.functionId).toBe("f1"); + + jobs.delete("j1"); + }); +}); + diff --git a/src/deploy/rootfs.test.ts b/src/deploy/rootfs.test.ts new file mode 100644 index 0000000..787a4f7 --- /dev/null +++ b/src/deploy/rootfs.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, vi } from "vitest"; +import { extractZip } from "./rootfs.js"; + +vi.mock("extract-zip", () => ({ + default: vi.fn(async (zip, opts) => { + if (opts?.onEntry) { + opts.onEntry({ fileName: "index.js" }); + } + }), +})); + +describe("extractZip", () => { + it("calls extract with correct args", async () => { + const extract = (await import("extract-zip")).default; + await extractZip("/tmp/code.zip", "/tmp/output"); + expect(extract).toHaveBeenCalledWith( + "/tmp/code.zip", + expect.objectContaining({ + dir: "/tmp/output", + }), + ); + }); + + it("rejects zip entries with path traversal", async () => { + const extract = (await import("extract-zip")).default; + (extract as any).mockImplementation(async (_: any, opts: any) => { + opts.onEntry({ fileName: "../../etc/passwd" }); + }); + await expect(extractZip("/tmp/evil.zip", "/tmp/out")).rejects.toThrow( + "Invalid zip content", + ); + }); +}); From 2f60dbc3f7bf126f68d26b808f21111d93ddfdd1 Mon Sep 17 00:00:00 2001 From: vivek1504 Date: Thu, 28 May 2026 08:42:11 +0530 Subject: [PATCH 2/6] added runtime tests --- src/runtime/cleanup.test.ts | 46 +++++++++++++++++++ src/runtime/protocol.test.ts | 86 +++++++++++++++++++++++++++++++++++ src/runtime/scheduler.test.ts | 56 +++++++++++++++++++++++ src/runtime/store.ts | 3 ++ src/runtime/test.store.ts | 17 +++++++ 5 files changed, 208 insertions(+) create mode 100644 src/runtime/cleanup.test.ts create mode 100644 src/runtime/protocol.test.ts create mode 100644 src/runtime/scheduler.test.ts create mode 100644 src/runtime/test.store.ts diff --git a/src/runtime/cleanup.test.ts b/src/runtime/cleanup.test.ts new file mode 100644 index 0000000..aa8ff25 --- /dev/null +++ b/src/runtime/cleanup.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { cleanupVm } from "./cleanup.js"; +import fs from "fs"; +import type { RuntimeFunction, Vm } from "../types/types.js"; + +function makeVm(overrides = {}): Vm { + return { + id: "test", + state: "ready", + firecrackerProcess: { kill: vi.fn() } as any, + apiSock: "/tmp/test-api.sock", + vsock: "/tmp/test-vsock.sock", + idleTime: Date.now(), + ...overrides, + }; +} + +function makeFn(vms: Vm[]): RuntimeFunction { + return { functionId: "fn1", queue: [], vms, processing: false }; +} + +describe("cleanupVm", () => { + beforeEach(() => vi.restoreAllMocks()); + + it("kills the process and removes sockets", async () => { + vi.spyOn(fs, "existsSync").mockReturnValue(true); + vi.spyOn(fs, "unlinkSync").mockImplementation(() => {}); + + const vm = makeVm(); + const fn = makeFn([vm]); + + await cleanupVm(fn, vm); + + expect(vm.firecrackerProcess.kill).toHaveBeenCalled(); + expect(fs.unlinkSync).toHaveBeenCalledTimes(2); + expect(fn.vms).toHaveLength(0); + }); + + it("skips if already cleaned", async () => { + const vm = makeVm({ cleaned: true }); + const fn = makeFn([vm]); + await cleanupVm(fn, vm); + expect(vm.firecrackerProcess.kill).not.toHaveBeenCalled(); + }); +}); + diff --git a/src/runtime/protocol.test.ts b/src/runtime/protocol.test.ts new file mode 100644 index 0000000..0f58fdd --- /dev/null +++ b/src/runtime/protocol.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from "vitest"; +import { buildPayload, readVsockResponse } from "./protocol.js"; +import { PassThrough } from "stream"; +import type { Socket } from "net"; + +describe("buildPayload", () => { + it("serializes request into JSON with newline", () => { + const req = { + method: "POST", + headers: { "content-type": "application/json" }, + query: { page: "1" }, + body: { name: "test" }, + }; + + const result = buildPayload(req, "/greet"); + const parsed = JSON.parse(result.trim()); + + expect(parsed.httpMethod).toBe("POST"); + expect(parsed.path).toBe("/greet"); + expect(parsed.headers["content-type"]).toBe("application/json"); + expect(result.endsWith("\n")).toBe(true); + }); + + it("defaults body to empty object when undefined", () => { + const req = { method: "GET", headers: {}, query: {} }; + const parsed = JSON.parse(buildPayload(req, "/").trim()); + expect(parsed.body).toBe("{}"); + }); +}); + +describe("readVsockResponse", () => { + function makeFakeSocket() { + return new PassThrough() as unknown as Socket; + } + + it("resolves on a valid response message", async () => { + const socket = makeFakeSocket(); + const promise = readVsockResponse(socket, 5000); + + socket.push( + JSON.stringify({ + type: "response", + data: { statusCode: 200, body: "ok" }, + }) + "\n", + ); + + const msg = await promise; + expect(msg.type).toBe("response"); + expect(msg.data.statusCode).toBe(200); + }); + + it("resolves on an error message", async () => { + const socket = makeFakeSocket(); + const promise = readVsockResponse(socket, 5000); + + socket.push( + JSON.stringify({ type: "error", data: null, error: "boom" }) + "\n", + ); + + const msg = await promise; + expect(msg.type).toBe("error"); + expect(msg.error).toBe("boom"); + }); + + it("skips OK lines and waits for real response", async () => { + const socket = makeFakeSocket(); + const promise = readVsockResponse(socket, 5000); + + socket.push("OK\n"); + socket.push( + JSON.stringify({ type: "response", data: { statusCode: 201 } }) + "\n", + ); + + const msg = await promise; + expect(msg.data.statusCode).toBe(201); + }); + + it("rejects on timeout", async () => { + const socket = makeFakeSocket(); + (socket as any).destroy = () => socket.end(); + + await expect(readVsockResponse(socket, 100)).rejects.toThrow( + "Function timeout", + ); + }); +}); diff --git a/src/runtime/scheduler.test.ts b/src/runtime/scheduler.test.ts new file mode 100644 index 0000000..47df60b --- /dev/null +++ b/src/runtime/scheduler.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { runtimeStore } from "./store.js"; +import type { RequestTask } from "../types/types.js"; + +vi.mock("./vm-manager.js", () => ({ + createVm: vi.fn(async (fid, fn) => { + const vm = { id: "mock", state: "ready", idleTime: Date.now() }; + fn.vms.push(vm); + return vm; + }), +})); + +vi.mock("./transport.js", () => ({ + sendRequest: vi.fn(async () => {}), +})); + +import { enqueueRequest } from "./scheduler.js"; +import { sendRequest } from "./transport.js"; + +describe("scheduler", () => { + beforeEach(() => { + runtimeStore.functions.clear(); + vi.clearAllMocks(); + }); + + it("creates a function entry on first request", async () => { + const task = makeTask(); + enqueueRequest("fn1", task); + await task.promise; + expect(runtimeStore.functions.has("fn1")).toBe(true); + }); + + it("calls sendRequest with correct args", async () => { + const task = makeTask("/hello"); + enqueueRequest("fn1", task); + await task.promise; + expect(sendRequest).toHaveBeenCalledWith("/hello", task.req, task.res, expect.anything()); + }); + + it("rejects task when sendRequest throws", async () => { + (sendRequest as any).mockRejectedValueOnce(new Error("boom")); + const task = makeTask(); + enqueueRequest("fn1", task); + await expect(task.promise).rejects.toThrow("boom"); + }); +}); + +function makeTask(subPath = "/"): RequestTask & { promise: Promise } { + let resolve!: () => void; + let reject!: (err: any) => void; + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + const req = { method: "GET", headers: {}, query: {}, body: {} }; + const res = { status: vi.fn().mockReturnThis(), json: vi.fn(), send: vi.fn() }; + return { req, res, subPath, resolve, reject, promise } as any; +} + diff --git a/src/runtime/store.ts b/src/runtime/store.ts index 352e6a8..4bf4fd5 100644 --- a/src/runtime/store.ts +++ b/src/runtime/store.ts @@ -2,4 +2,7 @@ import type { RuntimeFunction } from "../types/types.js"; export const runtimeStore = { functions: new Map(), + reset() { + this.functions.clear(); + }, }; diff --git a/src/runtime/test.store.ts b/src/runtime/test.store.ts new file mode 100644 index 0000000..4b2881f --- /dev/null +++ b/src/runtime/test.store.ts @@ -0,0 +1,17 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { runtimeStore } from "./store.js"; + +describe("runtimeStore", () => { + beforeEach(() => runtimeStore.reset()); + + it("starts empty", () => { + expect(runtimeStore.functions.size).toBe(0); + }); + + it("reset clears all functions", () => { + runtimeStore.functions.set("test", {} as any); + runtimeStore.reset(); + expect(runtimeStore.functions.size).toBe(0); + }); +}); + From 62c3b76de3e4dc6430249246f70eb1dc7cfd6f77 Mon Sep 17 00:00:00 2001 From: vivek1504 Date: Thu, 28 May 2026 08:59:20 +0530 Subject: [PATCH 3/6] added integration test --- .gitignore | 1 + package-lock.json | 1763 +++++++++++++++++- package.json | 10 +- src/routes/deploy.test.ts | 24 + src/routes/invoke.test.ts | 20 + src/runtime/{test.store.ts => store.test.ts} | 0 src/tests/unit/invoke.test.ts | 86 - tsconfig.json | 2 + vitest.config.ts | 16 + 9 files changed, 1790 insertions(+), 132 deletions(-) create mode 100644 src/routes/deploy.test.ts create mode 100644 src/routes/invoke.test.ts rename src/runtime/{test.store.ts => store.test.ts} (100%) delete mode 100644 src/tests/unit/invoke.test.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index c4e2d25..4431a1f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ future.md rootfs.ext4 node_modules vmlinux +coverage diff --git a/package-lock.json b/package-lock.json index f6dcdd6..2e30b41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,468 @@ "multer": "^2.1.1", "p-queue": "^9.2.0", "unzipper": "^0.12.3" + }, + "devDependencies": { + "@types/supertest": "^7.2.0", + "@vitest/coverage-v8": "^4.1.7", + "supertest": "^7.2.2", + "vitest": "^4.1.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@types/body-parser": { @@ -33,6 +495,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -42,6 +515,27 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -71,6 +565,13 @@ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/multer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", @@ -120,6 +621,30 @@ "@types/node": "*" } }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -130,6 +655,150 @@ "@types/node": "*" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz", + "integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.7", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.7", + "vitest": "4.1.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -152,11 +821,40 @@ "node": ">=12.0" } }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz", + "integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } }, "node_modules/asynckit": { "version": "0.4.0", @@ -269,6 +967,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", @@ -287,6 +995,16 @@ "node": ">= 0.8" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -324,6 +1042,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -342,6 +1067,13 @@ "node": ">=6.6.0" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -390,6 +1122,27 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -485,6 +1238,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -518,6 +1278,16 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -533,6 +1303,16 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -596,6 +1376,13 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -605,6 +1392,24 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -683,6 +1488,24 @@ "node": ">= 0.6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -721,6 +1544,21 @@ "node": ">=14.14" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -800,6 +1638,16 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -839,6 +1687,13 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -856,62 +1711,407 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/parcel" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.10" + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, - "node_modules/is-promise": { + "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "semver": "^7.5.3" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/math-intrinsics": { @@ -944,6 +2144,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -1037,6 +2260,25 @@ "node": ">= 0.6" } }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1064,6 +2306,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1132,12 +2385,68 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1226,6 +2535,40 @@ "node": ">= 6" } }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1268,6 +2611,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -1391,6 +2747,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1400,6 +2780,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -1417,6 +2804,99 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", + "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1426,6 +2906,14 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1498,6 +2986,191 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index c5fabf3..f9b70a0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "", "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "start": "tsc -b && sudo node dist/server.js" }, "keywords": [], @@ -24,5 +26,11 @@ "multer": "^2.1.1", "p-queue": "^9.2.0", "unzipper": "^0.12.3" + }, + "devDependencies": { + "@types/supertest": "^7.2.0", + "@vitest/coverage-v8": "^4.1.7", + "supertest": "^7.2.2", + "vitest": "^4.1.7" } } diff --git a/src/routes/deploy.test.ts b/src/routes/deploy.test.ts new file mode 100644 index 0000000..e516ad3 --- /dev/null +++ b/src/routes/deploy.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("../deploy/pipeline.js", () => ({ + deployFunction: vi.fn(), +})); + +import supertest from "supertest"; +import { app } from "../app.js"; + +describe("POST /deploy", () => { + it("returns 400 when no file is uploaded", async () => { + const res = await supertest(app).post("/deploy"); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/no file/i); + }); +}); + +describe("GET /deploy/status/:jobId", () => { + it("returns 404 for unknown job", async () => { + const res = await supertest(app).get("/deploy/status/nonexistent"); + expect(res.status).toBe(404); + expect(res.body.error).toMatch(/unknown/i); + }); +}); diff --git a/src/routes/invoke.test.ts b/src/routes/invoke.test.ts new file mode 100644 index 0000000..37dfab6 --- /dev/null +++ b/src/routes/invoke.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect, vi } from "vitest"; + +vi.mock("../runtime/scheduler.js", () => ({ + enqueueRequest: vi.fn((functionId, task) => { + // Simulate immediate error (no snapshot exists) + task.reject(new Error("no snapshot")); + }), +})); + +import supertest from "supertest"; +import { app } from "../app.js"; + +describe("Invoke route", () => { + it("returns 500 when scheduler rejects", async () => { + const res = await supertest(app).get("/f/test_function"); + expect(res.status).toBe(500); + expect(res.body.error).toBe("internal server error"); + }); +}); + diff --git a/src/runtime/test.store.ts b/src/runtime/store.test.ts similarity index 100% rename from src/runtime/test.store.ts rename to src/runtime/store.test.ts diff --git a/src/tests/unit/invoke.test.ts b/src/tests/unit/invoke.test.ts deleted file mode 100644 index 4dbc949..0000000 --- a/src/tests/unit/invoke.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { buildPayload, readVsockResponse } from "../../runtime/protocol.js"; -import { PassThrough } from "stream"; -import type { Socket } from "net"; - -describe("buildPayload", () => { - it("serializes request into JSON with newline", () => { - const req = { - method: "POST", - headers: { "content-type": "application/json" }, - query: { page: "1" }, - body: { name: "test" }, - }; - - const result = buildPayload(req, "/greet"); - const parsed = JSON.parse(result.trim()); - - expect(parsed.httpMethod).toBe("POST"); - expect(parsed.path).toBe("/greet"); - expect(parsed.headers["content-type"]).toBe("application/json"); - expect(result.endsWith("\n")).toBe(true); - }); - - it("defaults body to empty object when undefined", () => { - const req = { method: "GET", headers: {}, query: {} }; - const parsed = JSON.parse(buildPayload(req, "/").trim()); - expect(parsed.body).toBe("{}"); - }); -}); - -describe("readVsockResponse", () => { - function makeFakeSocket() { - return new PassThrough() as unknown as Socket; - } - - it("resolves on a valid response message", async () => { - const socket = makeFakeSocket(); - const promise = readVsockResponse(socket, 5000); - - socket.push( - JSON.stringify({ - type: "response", - data: { statusCode: 200, body: "ok" }, - }) + "\n", - ); - - const msg = await promise; - expect(msg.type).toBe("response"); - expect(msg.data.statusCode).toBe(200); - }); - - it("resolves on an error message", async () => { - const socket = makeFakeSocket(); - const promise = readVsockResponse(socket, 5000); - - socket.push( - JSON.stringify({ type: "error", data: null, error: "boom" }) + "\n", - ); - - const msg = await promise; - expect(msg.type).toBe("error"); - expect(msg.error).toBe("boom"); - }); - - it("skips OK lines and waits for real response", async () => { - const socket = makeFakeSocket(); - const promise = readVsockResponse(socket, 5000); - - socket.push("OK\n"); - socket.push( - JSON.stringify({ type: "response", data: { statusCode: 201 } }) + "\n", - ); - - const msg = await promise; - expect(msg.data.statusCode).toBe(201); - }); - - it("rejects on timeout", async () => { - const socket = makeFakeSocket(); - (socket as any).destroy = () => socket.end(); - - await expect(readVsockResponse(socket, 100)).rejects.toThrow( - "Function timeout", - ); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 0c1e300..f38e4b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,4 +41,6 @@ "moduleDetection": "force", "skipLibCheck": true, }, + "include": ["src/**/*"], + "exclude": ["vitest.config.ts"], } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e808cf0 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["src/**/*.test.ts"], + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts", "src/server.ts"], + }, + testTimeout: 15000, + }, +}); + From 925db84716a973ffc52f28078a97267b9c4487ab Mon Sep 17 00:00:00 2001 From: vivek1504 Date: Thu, 28 May 2026 09:17:40 +0530 Subject: [PATCH 4/6] added ci workflow --- .github/workflows/test.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2ba3551 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: Tests +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 20 } + - run: npm ci + - run: npm test + - run: npm run test:coverage From 8658a4c97b508a5e5f59dfcd18caed54ebd9608b Mon Sep 17 00:00:00 2001 From: vivek1504 Date: Thu, 28 May 2026 09:31:23 +0530 Subject: [PATCH 5/6] added test section to readme --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index dfcc3ec..2df6a09 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,48 @@ This replicates the benchmark configuration used to produce the performance numb --- +## Testing + +The project includes a comprehensive test suite using [Vitest](https://vitest.dev/) covering the control plane, deploy pipeline, and invocation runtime. + +### Running Tests + +```bash +# Run all tests +npm test + +# Watch mode +npm run test:watch + +# With coverage report +npm run test:coverage +``` + +### Test Coverage + +| Module | Tests | What's Covered | +|---|---|---| +| `runtime/protocol` | Unit | Payload serialization, vsock response parsing, chunked data handling | +| `runtime/scheduler` | Unit | Queue draining, VM creation, error propagation | +| `runtime/cleanup` | Unit | VM teardown, idempotent cleanup | +| `runtime/store` | Unit | State management, reset between runs | +| `deploy/firecracker` | Unit | VM readiness detection (chunked stdout buffering), socket polling, client creation | +| `deploy/rootfs` | Unit | Zip extraction, path traversal prevention | +| `deploy/queue` | Unit | Job lifecycle tracking, queue concurrency | +| `utils/path` | Unit | Path generation for all runtime artifacts | +| `routes/deploy` | Integration | HTTP validation (400, 404, 429), job submission | +| `routes/invoke` | Integration | Error handling, scheduler integration | + +### Testing Stack + +| Tool | Purpose | +|---|---| +| [Vitest](https://vitest.dev/) | Test runner and assertions | +| [Supertest](https://github.com/ladjs/supertest) | HTTP integration testing | +| [@vitest/coverage-v8](https://vitest.dev/guide/coverage) | Code coverage | + +--- + ## Performance Benchmarked using [`autocannon`](https://github.com/mcollina/autocannon) with 10 concurrent connections over 30 seconds: @@ -231,6 +273,7 @@ Benchmarked using [`autocannon`](https://github.com/mcollina/autocannon) with 10 | Host ↔ VM IPC | vsock | | Intra-VM IPC | Unix domain sockets | | Benchmarking | autocannon | +| Testing | Vitest, Supertest | --- From 26df4c2d42ce59794e051b2b7f0f8e7da088b238 Mon Sep 17 00:00:00 2001 From: vivek1504 Date: Thu, 28 May 2026 23:28:13 +0530 Subject: [PATCH 6/6] added logging --- package-lock.json | 267 ++++++++++++++++++++++++++++++++++++++ package.json | 3 + src/app.ts | 5 + src/deploy/firecracker.ts | 73 ++++++++++- src/deploy/pipeline.ts | 77 ++++++++++- src/deploy/rootfs.ts | 13 ++ src/routes/deploy.ts | 32 ++++- src/routes/invoke.ts | 25 +++- src/runtime/cleanup.ts | 9 ++ src/runtime/protocol.ts | 12 +- src/runtime/scheduler.ts | 33 +++-- src/runtime/transport.ts | 31 ++++- src/runtime/vm-manager.ts | 28 ++++ src/server.ts | 7 +- src/utils/logger.ts | 80 ++++++++++++ 15 files changed, 659 insertions(+), 36 deletions(-) create mode 100644 src/utils/logger.ts diff --git a/package-lock.json b/package-lock.json index 2e30b41..2499f84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,11 +20,14 @@ "fs": "^0.0.1-security", "multer": "^2.1.1", "p-queue": "^9.2.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", "unzipper": "^0.12.3" }, "devDependencies": { "@types/supertest": "^7.2.0", "@vitest/coverage-v8": "^4.1.7", + "pino-pretty": "^13.1.3", "supertest": "^7.2.2", "vitest": "^4.1.7" } @@ -203,6 +206,12 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", @@ -862,6 +871,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/axios": { "version": "1.13.6", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", @@ -983,6 +1001,13 @@ "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", "license": "ISC" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1087,6 +1112,16 @@ "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", "license": "ISC" }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1376,6 +1411,13 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-copy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", + "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -1568,6 +1610,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1687,6 +1738,13 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -1796,6 +1854,16 @@ "node": ">=8" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -2192,6 +2260,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2317,6 +2395,15 @@ ], "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2418,6 +2505,80 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^10.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -2453,6 +2614,22 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2497,6 +2674,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2535,6 +2718,15 @@ "node": ">= 6" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/rolldown": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", @@ -2605,12 +2797,38 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", @@ -2754,6 +2972,15 @@ "dev": true, "license": "ISC" }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2764,6 +2991,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2804,6 +3040,19 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/superagent": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", @@ -2853,6 +3102,24 @@ "node": ">=8" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/package.json b/package.json index f9b70a0..250aae1 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,14 @@ "fs": "^0.0.1-security", "multer": "^2.1.1", "p-queue": "^9.2.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", "unzipper": "^0.12.3" }, "devDependencies": { "@types/supertest": "^7.2.0", "@vitest/coverage-v8": "^4.1.7", + "pino-pretty": "^13.1.3", "supertest": "^7.2.2", "vitest": "^4.1.7" } diff --git a/src/app.ts b/src/app.ts index 9e43bef..36f0353 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,8 +1,13 @@ import express from "express"; +import pinoHttp from "pino-http"; import { deployRouter } from "./routes/deploy.js"; import { invokeRouter } from "./routes/invoke.js"; +import { httpLoggerOptions } from "./utils/logger.js"; export const app = express(); + +// @ts-expect-error +app.use(pinoHttp(httpLoggerOptions)); app.use(express.json()); app.use("/deploy", deployRouter); app.use("/f", invokeRouter); diff --git a/src/deploy/firecracker.ts b/src/deploy/firecracker.ts index a952906..8970495 100644 --- a/src/deploy/firecracker.ts +++ b/src/deploy/firecracker.ts @@ -2,14 +2,36 @@ import { spawn } from "child_process"; import fs from "fs"; import axios from "axios"; import path from "path"; +import { firecrackerLogger } from "../utils/logger.js"; export async function startFirecrackerProcess(apiSock: string) { + firecrackerLogger.debug({ apiSock }, "spawning firecracker process"); + const fc = spawn("firecracker", ["--api-sock", apiSock]); - fc.on("error", console.error); - fc.stderr.on("data", (d) => console.error(d.toString())); + fc.on("error", (err) => { + firecrackerLogger.error( + { err, apiSock }, + "firecracker process error", + ); + }); + + fc.stderr.on("data", (d) => { + firecrackerLogger.warn( + { apiSock, stderr: d.toString().trim() }, + "firecracker stderr output", + ); + }); + + fc.on("exit", (code, signal) => { + firecrackerLogger.info( + { apiSock, exitCode: code, signal }, + "firecracker process exited", + ); + }); await waitForFile(apiSock, 5000); + firecrackerLogger.debug({ apiSock }, "firecracker API socket ready"); return fc; } @@ -21,6 +43,10 @@ export async function waitForFile(path: any, timeout = 5000) { if (fs.existsSync(path)) return; if (Date.now() - start > timeout) { + firecrackerLogger.error( + { path, timeoutMs: timeout }, + "timeout waiting for file", + ); throw new Error("timeout waiting for socket"); } @@ -43,15 +69,19 @@ export async function configureVM( functionId: string, image: string, ) { + firecrackerLogger.debug({ functionId }, "configuring VM"); + await client.put("/machine-config", { vcpu_count: 1, mem_size_mib: 128, }); + firecrackerLogger.debug({ functionId, vcpu: 1, memMib: 128 }, "machine config set"); await client.put("/boot-source", { kernel_image_path: path.resolve("vmlinux"), boot_args: "console=ttyS0 reboot=k panic=1 pci=off init=/init -- /start.sh", }); + firecrackerLogger.debug({ functionId }, "boot source configured"); await client.put("/drives/rootfs", { drive_id: "rootfs", @@ -59,12 +89,16 @@ export async function configureVM( is_root_device: true, is_read_only: false, }); + firecrackerLogger.debug({ functionId, image }, "rootfs drive attached"); + const guestCid = Math.floor(Math.random() * 10000) + 3; + const vsockPath = `/tmp/vsock-${functionId}.sock`; await client.put("/vsock", { vsock_id: "vsock0", - guest_cid: Math.floor(Math.random() * 10000) + 3, - uds_path: `/tmp/vsock-${functionId}.sock`, + guest_cid: guestCid, + uds_path: vsockPath, }); + firecrackerLogger.debug({ functionId, guestCid, vsockPath }, "vsock configured"); await client.put("/logger", { log_path: `firecracker.log`, @@ -75,6 +109,7 @@ export async function configureVM( await client.put("/actions", { action_type: "InstanceStart", }); + firecrackerLogger.info({ functionId }, "VM instance started"); } export function waitForVMReady(fc: any) { @@ -82,6 +117,7 @@ export function waitForVMReady(fc: any) { let buffer = ""; const timeout = setTimeout(() => { + firecrackerLogger.error("VM startup timeout — READY signal not received within 50s"); reject(new Error("VM startup timeout")); }, 50000); @@ -90,6 +126,7 @@ export function waitForVMReady(fc: any) { if (buffer.includes("READY")) { clearTimeout(timeout); + firecrackerLogger.debug("VM READY signal received"); setTimeout(resolve, 200); } }); @@ -97,19 +134,41 @@ export function waitForVMReady(fc: any) { } export async function snapshotVM(client: any, functionId: string) { + firecrackerLogger.debug({ functionId }, "pausing VM for snapshot"); await client.patch("/vm", { state: "Paused" }); + const snapshotPath = path.resolve(`snapshot/snapshot-${functionId}`); + const memPath = path.resolve(`mem/mem-${functionId}`); + await client.put("/snapshot/create", { snapshot_type: "Full", - snapshot_path: path.resolve(`snapshot/snapshot-${functionId}`), - mem_file_path: path.resolve(`mem/mem-${functionId}`), + snapshot_path: snapshotPath, + mem_file_path: memPath, }); + + firecrackerLogger.info( + { functionId, snapshotPath, memPath }, + "VM snapshot created", + ); } export async function cleanupResources(paths: any) { - await Promise.allSettled([ + firecrackerLogger.debug( + { outputDir: paths.outputDir, apiSock: paths.apiSock, vsock: paths.vsock }, + "cleaning up deployment resources", + ); + + const results = await Promise.allSettled([ fs.promises.rm(paths.outputDir, { recursive: true, force: true }), fs.promises.rm(paths.apiSock), fs.promises.rm(paths.vsock), ]); + + const failed = results.filter((r) => r.status === "rejected"); + if (failed.length > 0) { + firecrackerLogger.warn( + { failedCount: failed.length, errors: failed.map((r: any) => r.reason?.message) }, + "some cleanup operations failed (non-critical)", + ); + } } diff --git a/src/deploy/pipeline.ts b/src/deploy/pipeline.ts index 7c23837..5cbcbcc 100644 --- a/src/deploy/pipeline.ts +++ b/src/deploy/pipeline.ts @@ -10,51 +10,114 @@ import { import fs from "fs"; import crypto from "crypto"; import { getPaths } from "../utils/path.js"; +import { pipelineLogger } from "../utils/logger.js"; export async function deployFunction(zipPath: string) { const functionId = crypto.randomBytes(8).toString("hex"); const paths = getPaths(functionId); let fc: ReturnType extends Promise ? T : never; + pipelineLogger.info( + { functionId, zipPath }, + "starting deployment pipeline", + ); + try { + // ── Stage 1: Extract zip ────────────────────────────────────── const t0 = performance.now(); await extractZip(zipPath, paths.outputDir); - console.log("extract:", performance.now() - t0); + const extractDuration = performance.now() - t0; + pipelineLogger.info( + { functionId, stage: "extract", durationMs: extractDuration }, + "zip extraction completed", + ); await fs.promises.unlink(zipPath); + // ── Stage 2: Prepare rootfs ─────────────────────────────────── const t1 = performance.now(); const image = await prepareRootfs(functionId); - console.log("rootfs:", performance.now() - t1); + const rootfsDuration = performance.now() - t1; + pipelineLogger.info( + { functionId, stage: "rootfs", durationMs: rootfsDuration, image }, + "rootfs preparation completed", + ); + // ── Stage 3: Spawn Firecracker ──────────────────────────────── const t2 = performance.now(); fc = await startFirecrackerProcess(paths.apiSock); - console.log("fc spawn:", performance.now() - t2); + const spawnDuration = performance.now() - t2; + pipelineLogger.info( + { functionId, stage: "fc-spawn", durationMs: spawnDuration }, + "firecracker process spawned", + ); + // ── Stage 4: Configure VM ───────────────────────────────────── const t3 = performance.now(); const readyPromise = waitForVMReady(fc); const client = createFcCient(paths.apiSock); const t4 = performance.now(); await configureVM(client, functionId, image); - console.log("configure Vm: ", performance.now() - t4); + const configureDuration = performance.now() - t4; + pipelineLogger.info( + { functionId, stage: "configure-vm", durationMs: configureDuration }, + "VM configured", + ); + // ── Stage 5: Wait for VM ready ──────────────────────────────── await readyPromise; - console.log("wait for vmReady: ", performance.now() - t3); + const readyDuration = performance.now() - t3; + pipelineLogger.info( + { functionId, stage: "vm-ready", durationMs: readyDuration }, + "VM reported READY", + ); + // ── Stage 6: Snapshot ───────────────────────────────────────── const t5 = performance.now(); await snapshotVM(client, functionId); - console.log("snapshot time: ", performance.now() - t5); + const snapshotDuration = performance.now() - t5; + pipelineLogger.info( + { functionId, stage: "snapshot", durationMs: snapshotDuration }, + "VM snapshot created", + ); + + const totalDuration = performance.now() - t0; + pipelineLogger.info( + { + functionId, + stage: "complete", + totalDurationMs: totalDuration, + stages: { + extractMs: extractDuration, + rootfsMs: rootfsDuration, + spawnMs: spawnDuration, + configureMs: configureDuration, + readyMs: readyDuration, + snapshotMs: snapshotDuration, + }, + }, + "deployment pipeline completed successfully", + ); return { functionId, url: `http://localhost:3000/f/${functionId}`, }; + } catch (err) { + pipelineLogger.error( + { functionId, err }, + "deployment pipeline failed", + ); + throw err; } finally { // Always kill the FC process — whether deploy succeeded or failed try { fc!?.kill("SIGKILL"); } catch { } const t6 = performance.now(); await cleanupResources(paths); - console.log("cleanupResources: ", performance.now() - t6); + pipelineLogger.debug( + { functionId, stage: "cleanup", durationMs: performance.now() - t6 }, + "post-deploy cleanup completed", + ); } } diff --git a/src/deploy/rootfs.ts b/src/deploy/rootfs.ts index 3714641..b7de429 100644 --- a/src/deploy/rootfs.ts +++ b/src/deploy/rootfs.ts @@ -1,36 +1,49 @@ import extract from "extract-zip"; import { exec as execCb, spawn } from "child_process"; import { promisify } from "util"; +import { rootfsLogger } from "../utils/logger.js"; const exec = promisify(execCb); export async function extractZip(zip: string, outputDir: string) { + rootfsLogger.debug({ zip, outputDir }, "extracting zip archive"); + await extract(zip, { dir: outputDir, onEntry: (entry) => { if (entry.fileName.includes("..")) { + rootfsLogger.error( + { fileName: entry.fileName }, + "path traversal detected in zip — aborting", + ); throw new Error("Invalid zip content"); } }, }); + + rootfsLogger.debug({ zip, outputDir }, "zip extraction completed"); } export async function prepareRootfs(functionId: string) { const baseImage = "rootfs.ext4"; const image = `rootfs/rootfs-${functionId}.ext4`; + rootfsLogger.debug({ functionId, baseImage, image }, "copying base rootfs image"); await exec(`cp --reflink=auto ${baseImage} ${image}`); const mountDir = `/mnt/rootfs-${functionId}`; const extracted = `extracted/${functionId}`; await exec(`sudo mkdir -p ${mountDir}`); + rootfsLogger.debug({ functionId, mountDir, image }, "mounting rootfs image"); await exec(`sudo mount -o loop ${image} ${mountDir}`); + rootfsLogger.debug({ functionId, from: extracted, to: `${mountDir}/app/` }, "copying user code into rootfs"); await exec(`sudo cp -r ${extracted}/. ${mountDir}/app/`); await exec(`sudo umount ${mountDir}`); await exec(`sudo rm -rf ${mountDir}`); + rootfsLogger.debug({ functionId, image }, "rootfs prepared and unmounted"); return image; } diff --git a/src/routes/deploy.ts b/src/routes/deploy.ts index bcce0c6..9b36620 100644 --- a/src/routes/deploy.ts +++ b/src/routes/deploy.ts @@ -3,23 +3,41 @@ import crypto from "crypto"; import { upload } from "../deploy/upload.js"; import { jobs, deployQueue } from "../deploy/queue.js"; import { deployFunction } from "../deploy/pipeline.js"; +import { deployLogger } from "../utils/logger.js"; export const deployRouter = Router(); deployRouter.post("/", upload.single("code"), async (req, res) => { if (!req.file?.path) { + deployLogger.warn("deploy request rejected: no file uploaded"); return res.status(400).json({ error: "No file uploaded" }); } if (deployQueue.size > 50) { + deployLogger.warn( + { queueSize: deployQueue.size }, + "deploy request rejected: queue full", + ); return res.status(429).json({ error: "Too many jobs" }); } const jobId = crypto.randomBytes(8).toString("hex"); jobs.set(jobId, { state: "pending" }); + deployLogger.info( + { + jobId, + fileName: req.file.originalname, + fileSize: req.file.size, + queueSize: deployQueue.size + 1, + }, + "deployment job enqueued", + ); + deployQueue.add(async () => { jobs.set(jobId, { state: "running" }); + deployLogger.info({ jobId }, "deployment job started"); + try { const result = await deployFunction(req.file!.path); jobs.set(jobId, { @@ -27,8 +45,16 @@ deployRouter.post("/", upload.single("code"), async (req, res) => { functionId: result.functionId, url: result.url, }); + deployLogger.info( + { jobId, functionId: result.functionId, url: result.url }, + "deployment job completed successfully", + ); } catch (err: any) { jobs.set(jobId, { state: "error", message: err.message }); + deployLogger.error( + { jobId, err }, + "deployment job failed", + ); } }); @@ -40,6 +66,10 @@ deployRouter.post("/", upload.single("code"), async (req, res) => { deployRouter.get("/status/:jobId", (req, res) => { const job = jobs.get(req.params.jobId); - if (!job) return res.status(404).json({ error: "Unknown job" }); + if (!job) { + deployLogger.debug({ jobId: req.params.jobId }, "status lookup: unknown job"); + return res.status(404).json({ error: "Unknown job" }); + } + deployLogger.debug({ jobId: req.params.jobId, state: job.state }, "status lookup"); res.json(job); }); diff --git a/src/routes/invoke.ts b/src/routes/invoke.ts index 00b80c0..6cf437a 100644 --- a/src/routes/invoke.ts +++ b/src/routes/invoke.ts @@ -1,21 +1,38 @@ import { Router } from "express"; import { enqueueRequest } from "../runtime/scheduler.js"; +import { runtimeLogger } from "../utils/logger.js"; export const invokeRouter = Router(); invokeRouter.use("/:functionId", async (req, res) => { + const { functionId } = req.params; + const subPath = req.path || "/"; + + runtimeLogger.info( + { functionId, method: req.method, subPath }, + "function invocation received", + ); + try { await new Promise((resolve, reject) => { - enqueueRequest(req.params.functionId, { + enqueueRequest(functionId, { req, res, - subPath: req.path || "/", + subPath, resolve, reject, }); }); - } catch (e) { - console.error(e); + + runtimeLogger.info( + { functionId, method: req.method, subPath, statusCode: res.statusCode }, + "function invocation completed", + ); + } catch (e: any) { + runtimeLogger.error( + { functionId, method: req.method, subPath, err: e }, + "function invocation failed", + ); if (!res.headersSent) { res.status(500).json({ diff --git a/src/runtime/cleanup.ts b/src/runtime/cleanup.ts index c1f475c..58bda59 100644 --- a/src/runtime/cleanup.ts +++ b/src/runtime/cleanup.ts @@ -1,4 +1,5 @@ import fs from "fs"; +import { cleanupLogger } from "../utils/logger.js"; import type { RuntimeFunction, Vm } from "../types/types.js"; @@ -6,20 +7,28 @@ export async function cleanupVm(fn: RuntimeFunction, vm: Vm) { if (vm.cleaned) return; vm.cleaned = true; + cleanupLogger.info({ functionId: fn.functionId, vmId: vm.id }, "cleaning up VM"); try { vm.firecrackerProcess.kill(); + cleanupLogger.debug({ vmId: vm.id }, "firecracker process killed"); } catch {} try { if (fs.existsSync(vm.apiSock)) { fs.unlinkSync(vm.apiSock); + cleanupLogger.debug({ path: vm.apiSock }, "API socket removed"); } if (fs.existsSync(vm.vsock)) { fs.unlinkSync(vm.vsock); + cleanupLogger.debug({ path: vm.vsock }, "vsock removed"); } } catch {} fn.vms = fn.vms.filter((v) => v !== vm); + cleanupLogger.info( + { functionId: fn.functionId, vmId: vm.id, remainingVms: fn.vms.length }, + "VM cleanup completed", + ); } diff --git a/src/runtime/protocol.ts b/src/runtime/protocol.ts index 1bc690c..8a59f00 100644 --- a/src/runtime/protocol.ts +++ b/src/runtime/protocol.ts @@ -1,4 +1,5 @@ import type { Socket } from "net"; +import { protocolLogger } from "../utils/logger.js"; export function buildPayload(req: any, subPath: string): string { return ( @@ -30,6 +31,7 @@ export function readVsockResponse( }; const timer = setTimeout(() => { + protocolLogger.error({ timeoutMs: timeout }, "function execution timeout"); socket.destroy(); reject(new Error("Function timeout")); }, timeout); @@ -52,11 +54,17 @@ export function readVsockResponse( if (msg.type === "response" || msg.type === "error") { clearTimeout(timer); cleanup(); + if (msg.type === "error") { + protocolLogger.warn( + { errorData: msg.data, errorMsg: msg.error }, + "VM returned error response", + ); + } resolve(msg); return; } } catch { - console.error("invalid json:", line); + protocolLogger.error({ rawLine: line }, "invalid JSON received from VM"); } } }; @@ -64,12 +72,14 @@ export function readVsockResponse( onError = (err) => { clearTimeout(timer); cleanup(); + protocolLogger.error({ err }, "vsock read error"); reject(err); }; onEnd = () => { clearTimeout(timer); cleanup(); + protocolLogger.error("vsock connection closed before response received"); reject(new Error("Connection closed before response")); }; diff --git a/src/runtime/scheduler.ts b/src/runtime/scheduler.ts index 4deb3f2..917a513 100644 --- a/src/runtime/scheduler.ts +++ b/src/runtime/scheduler.ts @@ -1,6 +1,7 @@ import { runtimeStore } from "./store.js"; import { createVm } from "./vm-manager.js"; import { sendRequest } from "./transport.js"; +import { schedulerLogger } from "../utils/logger.js"; import type { RequestTask, RuntimeFunction } from "../types/types.js"; @@ -12,25 +13,25 @@ export async function enqueueRequest(functionId: string, task: RequestTask) { if (!fn) { fn = { functionId, - queue: [], - vms: [], - processing: false, }; - runtimeStore.functions.set(functionId, fn); + schedulerLogger.info({ functionId }, "new runtime function registered"); } fn.queue.push(task); + schedulerLogger.debug( + { functionId, queueDepth: fn.queue.length }, + "request enqueued", + ); processQueue(fn); } async function processQueue(fn: RuntimeFunction) { if (fn.processing) return; - fn.processing = true; try { @@ -38,22 +39,38 @@ async function processQueue(fn: RuntimeFunction) { let vm = fn.vms.find((v) => v.state === "ready"); if (!vm && fn.vms.length < MAX_VMS) { + schedulerLogger.info( + { functionId: fn.functionId, currentVms: fn.vms.length }, + "creating new VM", + ); vm = await createVm(fn.functionId, fn); } - if (!vm) return; + if (!vm) { + schedulerLogger.warn( + { functionId: fn.functionId, vmCount: fn.vms.length }, + "no VM available, max reached", + ); + return; + } const task = fn.queue.shift(); - if (!task) return; vm.state = "busy"; + schedulerLogger.debug( + { functionId: fn.functionId, vmId: vm.id, subPath: task.subPath }, + "dispatching request to VM", + ); try { await sendRequest(task.subPath, task.req, task.res, vm); - task.resolve(); } catch (err) { + schedulerLogger.error( + { functionId: fn.functionId, vmId: vm.id, err }, + "request handling failed", + ); task.reject(err); } finally { vm.state = "ready"; diff --git a/src/runtime/transport.ts b/src/runtime/transport.ts index 1376f2f..b04b803 100644 --- a/src/runtime/transport.ts +++ b/src/runtime/transport.ts @@ -1,11 +1,14 @@ import net, { Socket } from "net"; import { buildPayload, readVsockResponse } from "./protocol.js"; +import { transportLogger } from "../utils/logger.js"; import type { Vm } from "../types/types.js"; export async function connectVsock( path: string, timeout = 5000, ): Promise { + transportLogger.debug({ path, timeoutMs: timeout }, "connecting to vsock"); + return new Promise((resolve, reject) => { const start = Date.now(); @@ -13,6 +16,10 @@ export async function connectVsock( const socket = net.createConnection({ path }); socket.once("connect", () => { + transportLogger.debug( + { path, elapsedMs: Date.now() - start }, + "vsock connected", + ); resolve(socket); }); @@ -20,6 +27,7 @@ export async function connectVsock( socket.destroy(); if (Date.now() - start > timeout) { + transportLogger.error({ path, timeoutMs: timeout }, "vsock connection timeout"); return reject(new Error("Vsock timeout")); } @@ -36,8 +44,8 @@ export async function getVmSocket(vm: Vm) { return vm.socket; } + transportLogger.debug({ vmId: vm.id, vsock: vm.vsock }, "establishing new VM socket"); vm.socket = await connectVsock(vm.vsock); - vm.socket.write("CONNECT 5000\n"); return vm.socket; @@ -45,9 +53,13 @@ export async function getVmSocket(vm: Vm) { export async function sendRequest(subPath: string, req: any, res: any, vm: Vm) { const socket = await getVmSocket(vm); - socket.write(buildPayload(req, subPath)); + transportLogger.debug( + { vmId: vm.id, method: req.method, subPath }, + "request payload sent to VM", + ); + const msg = await readVsockResponse(socket, 10000); if (msg.type === "response") { @@ -55,14 +67,21 @@ export async function sendRequest(subPath: string, req: any, res: any, vm: Vm) { try { const body = JSON.parse(msg.data.body); - res.status(statusCode).json(body); } catch { res.status(statusCode).send(msg.data.body ?? ""); } + + transportLogger.debug( + { vmId: vm.id, statusCode }, + "response forwarded to client", + ); } else { - res - .status(msg.data?.statusCode || 500) - .json(msg.data ?? { error: msg.error }); + const statusCode = msg.data?.statusCode || 500; + transportLogger.error( + { vmId: vm.id, statusCode, error: msg.error }, + "VM returned error response", + ); + res.status(statusCode).json(msg.data ?? { error: msg.error }); } } diff --git a/src/runtime/vm-manager.ts b/src/runtime/vm-manager.ts index cd61575..88e3da8 100644 --- a/src/runtime/vm-manager.ts +++ b/src/runtime/vm-manager.ts @@ -3,6 +3,7 @@ import net from "net"; import axios from "axios"; import path from "path"; import crypto from "crypto"; +import { vmManagerLogger } from "../utils/logger.js"; import type { RuntimeFunction, Vm } from "../types/types.js"; @@ -13,8 +14,22 @@ export async function createVm( const instanceId = crypto.randomBytes(4).toString("hex"); const apiSock = `/tmp/firecracker-${functionId}-${instanceId}.socket`; const vsock = `/tmp/vsock-${functionId}-${instanceId}.sock`; + + vmManagerLogger.info( + { functionId, instanceId, apiSock, vsock }, + "creating new VM instance", + ); + const fc = spawn("firecracker", ["--api-sock", apiSock]); + fc.on("error", (err) => { + vmManagerLogger.error({ instanceId, err }, "firecracker process error"); + }); + + fc.on("exit", (code, signal) => { + vmManagerLogger.info({ instanceId, exitCode: code, signal }, "firecracker process exited"); + }); + await waitForFirecrackerApiSocket(apiSock); const client = createFcClient(apiSock); @@ -30,6 +45,10 @@ export async function createVm( }; fn.vms.push(vm); + vmManagerLogger.info( + { functionId, instanceId, totalVms: fn.vms.length }, + "VM instance created and ready", + ); return vm; } @@ -45,6 +64,10 @@ export async function waitForFirecrackerApiSocket( client.once("connect", () => { client.destroy(); + vmManagerLogger.debug( + { path, elapsedMs: Date.now() - start }, + "API socket connected", + ); resolve(); }); @@ -52,6 +75,7 @@ export async function waitForFirecrackerApiSocket( client.destroy(); if (Date.now() - start > timeout) { + vmManagerLogger.error({ path, timeoutMs: timeout }, "API socket connection timeout"); return reject(new Error("socket timeout")); } setTimeout(tryConnect, 50); @@ -77,6 +101,8 @@ export async function restoreVm( functionId: string, vsock: string, ) { + vmManagerLogger.debug({ functionId, vsock }, "restoring VM from snapshot"); + await client.put("/snapshot/load", { snapshot_path: path.resolve(`snapshot/snapshot-${functionId}`), mem_backend: { @@ -89,4 +115,6 @@ export async function restoreVm( uds_path: vsock, }, }); + + vmManagerLogger.debug({ functionId }, "VM restored from snapshot"); } diff --git a/src/server.ts b/src/server.ts index 3b7b468..89747ca 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,8 @@ import { app } from "./app.js"; +import { logger } from "./utils/logger.js"; -app.listen(3000, () => { - console.log("listening on http://localhost:3000"); +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + logger.info({ port: PORT }, `server listening on http://localhost:${PORT}`); }); diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..6ddf478 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,80 @@ +import { pino } from "pino"; +import type { Options } from "pino-http"; +import type { LoggerOptions } from "pino"; + +const isProd = process.env.NODE_ENV === "production"; + +const baseOptions: LoggerOptions = { + level: process.env.LOG_LEVEL || "info", + redact: { + paths: [ + "req.headers.authorization", + "req.headers.cookie", + "password", + "token", + ], + censor: "[REDACTED]", + }, + timestamp: pino.stdTimeFunctions.isoTime, +}; + +if (!isProd) { + baseOptions.transport = { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "SYS:HH:MM:ss.l", + ignore: "pid,hostname", + }, + }; +} + +export const logger = pino(baseOptions); + +export const deployLogger = logger.child({ module: "deploy" }); +export const pipelineLogger = logger.child({ module: "pipeline" }); +export const firecrackerLogger = logger.child({ module: "firecracker" }); +export const rootfsLogger = logger.child({ module: "rootfs" }); +export const queueLogger = logger.child({ module: "queue" }); +export const runtimeLogger = logger.child({ module: "runtime" }); +export const schedulerLogger = logger.child({ module: "scheduler" }); +export const vmManagerLogger = logger.child({ module: "vm-manager" }); +export const transportLogger = logger.child({ module: "transport" }); +export const protocolLogger = logger.child({ module: "protocol" }); +export const cleanupLogger = logger.child({ module: "cleanup" }); + +export const httpLoggerOptions: Options = { + logger: logger.child({ module: "http" }), + autoLogging: { + ignore: (req) => req.url === "/health", + }, + customLogLevel: (_req, res, err) => { + if (res.statusCode >= 500 || err) return "error"; + if (res.statusCode >= 400) return "warn"; + return "info"; + }, + customSuccessMessage: (req, res) => { + return `${req.method} ${req.url} completed with ${res.statusCode}`; + }, + customErrorMessage: (req, _res, err) => { + return `${req.method} ${req.url} errored: ${err.message}`; + }, + customReceivedMessage: (req) => { + return `${req.method} ${req.url} received`; + }, + serializers: { + req(req) { + return { + method: req.method, + url: req.url, + remoteAddress: req.remoteAddress, + contentType: req.headers?.["content-type"], + }; + }, + res(res) { + return { + statusCode: res.statusCode, + }; + }, + }, +};