From 649f9b41202a635c1d266c4e3cabfa30bbb3fe5f Mon Sep 17 00:00:00 2001 From: jiasheng Date: Sat, 30 May 2026 09:54:15 +0800 Subject: [PATCH 1/8] feat: add public API key support and request signature verification --- README.md | 14 ++++ package.json | 1 + src/index.ts | 2 + src/server.ts | 57 ++++++++++++++- src/signature-verifier.test.ts | 78 +++++++++++++++++++++ src/signature-verifier.ts | 122 +++++++++++++++++++++++++++++++++ 6 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 src/signature-verifier.test.ts create mode 100644 src/signature-verifier.ts diff --git a/README.md b/README.md index 22e3b57..cbfc7e2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ zenstack-proxy [options] - `-p, --port ` Port number for the server (default: `8008`) - `-s, --schema ` - Path to ZModel schema file (default: "schema.zmodel") - `-d, --datasource-url ` Datasource URL (overrides schema configuration) +- `--public-api-key ` Public API key used to verify `X-ZenStack-Signature` request headers - `-l, --log ` Query log levels (e.g., query, info, warn, error) ### Examples @@ -46,6 +47,19 @@ zenstack-proxy -s ./schema/schema.zmodel -z ./generated/zenstack zenstack-proxy -p 8888 ``` +#### Enable signed requests + +```bash +zenstack-proxy --public-api-key "MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=" +``` + +When `--public-api-key` is provided, every incoming request must include an `X-ZenStack-Signature` header in the format `t=,v1=`. +The signed message format matches ZenStack Studio: `payload + timestamp`. + +- For `GET` and `DELETE` requests, `payload` is the raw query string without the leading `?`. +- For body-based requests, `payload` is the exact JSON request body string. +- For requests without query params or a request body, `payload` is an empty string. + ## Server Endpoints The server provides the following endpoints: diff --git a/package.json b/package.json index 99b0e13..d58e300 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "scripts": { "clean": "rimraf dist", + "test": "node --import tsx --test", "build": "pnpm clean && tsc && copyfiles -F \"bin/*\" dist && copyfiles ./README.md ./LICENSE ./package.json dist && pnpm pack dist --pack-destination ../build" }, "keywords": [ diff --git a/src/index.ts b/src/index.ts index d9645ad..fa7f0c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export function createProgram() { .option('-p, --port ', 'Port number for the server', '2311') .option('-s, --schema ', 'Path to ZModel schema file', 'schema.zmodel') .option('-d, --datasource-url ', 'Datasource URL (overrides schema configuration)') + .option('--public-api-key ', 'Public API key used to verify request signatures') .option('-l, --log ', 'Query log levels (e.g., query, info, warn, error)') .action(async (options) => { // Determine ZModel schema path @@ -47,6 +48,7 @@ export function createProgram() { zmodelConfig: zmodelConfig, zmodelSchemaDir: zmodelSchemaDir, logLevel: options.log, + publicAPIKey: options.publicApiKey, }) }) diff --git a/src/server.ts b/src/server.ts index 251c1e0..27d57d6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,6 +8,11 @@ import { blue, grey, red } from 'colors' import semver from 'semver' import { CliError } from './cli-error' import SuperJSON from 'superjson' +import { + buildSignedPayload, + verifySignedRequest, + type RequestSignatureHeader, +} from './signature-verifier' export interface ServerOptions { zenstackPath: string | undefined @@ -15,6 +20,11 @@ export interface ServerOptions { zmodelConfig: ZModelConfig zmodelSchemaDir: string logLevel?: string[] + publicAPIKey?: string +} + +type RequestWithRawBody = express.Request & { + rawBody?: string } type EnhancementKind = 'password' | 'omit' | 'policy' | 'validation' | 'delegate' | 'encryption' @@ -321,7 +331,7 @@ async function handleTransaction(modelMeta: any, client: any, requestBody: unkno * Start the Express server with ZenStack proxy */ export async function startServer(options: ServerOptions) { - const { zenstackPath, port, zmodelConfig, zmodelSchemaDir } = options + const { zenstackPath, port, zmodelConfig, zmodelSchemaDir, publicAPIKey } = options const { PrismaClient, modelMeta, enums, zenstackVersion, enhanceFunc } = await loadZenStackModules(zmodelConfig, zmodelSchemaDir, zenstackPath) @@ -347,8 +357,49 @@ export async function startServer(options: ServerOptions) { const app = express() app.use(cors()) - app.use(express.json({ limit: '5mb' })) - app.use(express.urlencoded({ extended: true, limit: '5mb' })) + app.use( + express.json({ + limit: '5mb', + verify: (req, _res, buf) => { + ;(req as RequestWithRawBody).rawBody = buf.toString('utf8') + }, + }) + ) + app.use( + express.urlencoded({ + extended: true, + limit: '5mb', + verify: (req, _res, buf) => { + ;(req as RequestWithRawBody).rawBody = buf.toString('utf8') + }, + }) + ) + + if (publicAPIKey) { + console.log(grey('Request signature verification is enabled')) + app.use((req, res, next) => { + const payload = buildSignedPayload({ + method: req.method, + rawQuery: req.originalUrl.includes('?') + ? req.originalUrl.slice(req.originalUrl.indexOf('?') + 1) + : '', + rawBody: (req as RequestWithRawBody).rawBody, + }) + + const verification = verifySignedRequest({ + publicAPIKey, + payload, + header: req.header('x-zenstack-signature') as RequestSignatureHeader, + }) + + if (!verification.ok) { + res.status(401).json({ error: verification.error }) + return + } + + next() + }) + } // ZenStack API endpoint diff --git a/src/signature-verifier.test.ts b/src/signature-verifier.test.ts new file mode 100644 index 0000000..1a2d5b4 --- /dev/null +++ b/src/signature-verifier.test.ts @@ -0,0 +1,78 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { buildSignedPayload, verifySignedRequest } from './signature-verifier' + +test('verifies sample GET signature using base64 DER public key', () => { + const payload = buildSignedPayload({ + method: 'GET', + rawQuery: 'q=%7B%22where%22%3A%7B%7D%2C%22take%22%3A100%2C%22skip%22%3A0%7D', + }) + + const result = verifySignedRequest({ + publicAPIKey: 'MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=', + payload, + header: + 't=1777590674,v1=_Mbr9a-X24ZBUWQLGiPnehh99yBGXoMmfJ9Jh5N99E1uz4NpjZrCzdoKQZGCsYLRgYYJmCElsZ_6YT4FwlhZBQ', + }) + + assert.equal(result.ok, true) +}) + +test('verifies sample signature using PEM public key', () => { + const payload = buildSignedPayload({ + method: 'GET', + rawQuery: 'q=%7B%22where%22%3A%7B%7D%2C%22take%22%3A100%2C%22skip%22%3A0%7D', + }) + + const result = verifySignedRequest({ + publicAPIKey: `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs= +-----END PUBLIC KEY-----`, + payload, + header: + 't=1777590674,v1=_Mbr9a-X24ZBUWQLGiPnehh99yBGXoMmfJ9Jh5N99E1uz4NpjZrCzdoKQZGCsYLRgYYJmCElsZ_6YT4FwlhZBQ', + }) + + assert.equal(result.ok, true) +}) + +test('verifies sample PUT signature using request body payload', () => { + const payload = buildSignedPayload({ + method: 'PUT', + rawBody: '{"data":{"meta":{"sessionNumber":15}},"where":{"id":"cmkhoq1t1000cb6av8sq9ql12"}}', + }) + + const result = verifySignedRequest({ + publicAPIKey: 'MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=', + payload, + header: + 't=1777590954,v1=L_b8qi55lLv5XHfopAhC15qbJ_GAc-zGs8CUakXxnDh3Xce0seAC3Ri5mbEwVx27ckYV821wmaxJZt8fvFNnDA', + }) + + assert.equal(result.ok, true) +}) + +test('rejects invalid signatures', () => { + const result = verifySignedRequest({ + publicAPIKey: 'MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=', + payload: '{}', + header: + 't=1777590674,v1=_Mbr9a-X24ZBUWQLGiPnehh99yBGXoMmfJ9Jh5N99E1uz4NpjZrCzdoKQZGCsYLRgYYJmCElsZ_6YT4FwlhZBQ', + }) + + assert.deepEqual(result, { + ok: false, + error: 'Invalid request signature.', + }) +}) + +test('uses raw body for body-based requests', () => { + assert.equal( + buildSignedPayload({ method: 'POST', rawBody: '{"data":{"id":1}}' }), + '{"data":{"id":1}}' + ) + assert.equal( + buildSignedPayload({ method: 'PUT', rawBody: '{"where":{"id":1}}' }), + '{"where":{"id":1}}' + ) +}) diff --git a/src/signature-verifier.ts b/src/signature-verifier.ts new file mode 100644 index 0000000..6f405cd --- /dev/null +++ b/src/signature-verifier.ts @@ -0,0 +1,122 @@ +import { createPublicKey, type KeyObject, verify } from 'node:crypto' + +export type RequestSignatureHeader = string | undefined + +type ParsedSignatureHeader = { + timestamp: string + signature: string +} + +type VerifySignedRequestOptions = { + publicAPIKey: string + payload: string + header: RequestSignatureHeader +} + +type VerifySignedRequestResult = + | { + ok: true + } + | { + ok: false + error: string + } + +export function buildSignedPayload({ + method, + rawQuery, + rawBody, +}: { + method: string + rawQuery?: string + rawBody?: string +}) { + const normalizedMethod = method.toUpperCase() + + if (normalizedMethod === 'GET' || normalizedMethod === 'DELETE') { + return rawQuery ?? '' + } + + return rawBody ?? '' +} + +export function verifySignedRequest({ + publicAPIKey, + payload, + header, +}: VerifySignedRequestOptions): VerifySignedRequestResult { + const parsedHeader = parseSignatureHeader(header) + if (!parsedHeader) { + return { + ok: false, + error: + 'Missing or invalid x-zenstack-signature header. Expected format: t=,v1=.', + } + } + + const publicKey = parsePublicKey(publicAPIKey) + if (!publicKey) { + return { + ok: false, + error: + 'Invalid public API key. Expected a PEM public key or base64-encoded Ed25519 SPKI key.', + } + } + + const { timestamp, signature } = parsedHeader + const message = `${payload}${timestamp}` + + try { + const isValid = verify( + null, + Buffer.from(message, 'utf8'), + publicKey, + Buffer.from(signature, 'base64url') + ) + + return isValid ? { ok: true } : { ok: false, error: 'Invalid request signature.' } + } catch { + return { ok: false, error: 'Invalid request signature.' } + } +} + +function parseSignatureHeader(header: RequestSignatureHeader): ParsedSignatureHeader | null { + if (!header) { + return null + } + + const values = header.split(',').reduce>((result, item) => { + const [key, ...valueParts] = item.trim().split('=') + if (key && valueParts.length > 0) { + result[key] = valueParts.join('=') + } + return result + }, {}) + + if (!values.t || !values.v1 || !/^\d+$/.test(values.t)) { + return null + } + + return { + timestamp: values.t, + signature: values.v1, + } +} + +function parsePublicKey(publicAPIKey: string): KeyObject | null { + const trimmedKey = publicAPIKey.trim() + + try { + if (trimmedKey.includes('BEGIN PUBLIC KEY')) { + return createPublicKey(trimmedKey) + } + + return createPublicKey({ + key: Buffer.from(trimmedKey, 'base64'), + format: 'der', + type: 'spki', + }) + } catch { + return null + } +} From febf9601805a2db8f763f9f099dca2244629d2ed Mon Sep 17 00:00:00 2001 From: jiasheng Date: Tue, 16 Jun 2026 22:02:39 +0800 Subject: [PATCH 2/8] feat: add signature verification middleware and related options - Introduced `--studioAuthKey` option for authentication key from ZenStack Studio. - Added `--signatureToleranceSecs` option to configure maximum age of signed requests. - Implemented `createSignatureMiddleware` to verify ed25519 signatures on incoming requests. - Normalized public key format for signature verification. - Enhanced server options to include signature tolerance and public API key. - Updated request handling to enforce signature verification for protected routes. - Added comprehensive tests for signature verification and middleware functionality. --- package.json | 8 +- pnpm-lock.yaml | 862 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 21 +- src/server.ts | 135 ++++--- src/signature.ts | 112 ++++++ test/signature.test.ts | 401 +++++++++++++++++++ 6 files changed, 1478 insertions(+), 61 deletions(-) create mode 100644 src/signature.ts create mode 100644 test/signature.test.ts diff --git a/package.json b/package.json index d58e300..c6cffab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/proxy", - "version": "0.4.2", + "version": "0.4.3", "description": "A CLI tool to run an Express server that proxies CRUD requests to a ZenStack backend", "main": "index.js", "publishConfig": { @@ -18,8 +18,8 @@ }, "scripts": { "clean": "rimraf dist", - "test": "node --import tsx --test", - "build": "pnpm clean && tsc && copyfiles -F \"bin/*\" dist && copyfiles ./README.md ./LICENSE ./package.json dist && pnpm pack dist --pack-destination ../build" + "build": "pnpm clean && tsc && copyfiles -F \"bin/*\" dist && copyfiles ./README.md ./LICENSE ./package.json dist && pnpm pack dist --pack-destination ../build", + "test": "node --require tsx/cjs --test test/signature.test.ts" }, "keywords": [ "zenstack", @@ -50,8 +50,10 @@ "@types/express": "^5.0.0", "@types/node": "^20.0.0", "@types/semver": "^7.7.1", + "@types/supertest": "^6.0.2", "copyfiles": "^2.4.1", "rimraf": "^4.0.0", + "supertest": "^7.0.0", "typescript": "^5.0.0" }, "exports": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2374ff5..e2c6be2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,15 +63,24 @@ importers: '@types/semver': specifier: ^7.7.1 version: 7.7.1 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.3 copyfiles: specifier: ^2.4.1 version: 2.4.1 rimraf: specifier: ^4.0.0 version: 4.4.1 + supertest: + specifier: ^7.0.0 + version: 7.2.2 typescript: specifier: ^5.0.0 version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@20.19.27)(tsx@4.21.0) publishDirectory: dist packages: @@ -232,6 +241,16 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@prisma/adapter-better-sqlite3@6.19.2': resolution: {integrity: sha512-SCDyUS30NlHjgfghEfech1GYScxlDzedFBgNrlQk1bb9N/vGLwvtDwsMqaHlhusnrm2w1eMllTThBZ5vlIsEOQ==} @@ -268,15 +287,155 @@ packages: '@prisma/driver-adapter-utils@6.19.2': resolution: {integrity: sha512-tkHsL3jhx81eXg2oqtJH/1IEs8uEeUb1RpqHtwYqdNb176u9D0mnHRZM1/cKca/XhLpq49Nnd9XDxdMfWcKAYA==} + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@5.1.0': resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} @@ -289,6 +448,9 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/node@20.19.27': resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} @@ -310,6 +472,41 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/superagent@8.1.10': + resolution: {integrity: sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@zenstackhq/runtime@2.22.2': resolution: {integrity: sha512-GgWSyU1nL1q89ZOS7O7cn9GWUcQ2ywavTp73RJvqRw0HSQ2r5sCi+wAyh2eAxqGTSnBob4jg2kq7CcFqcaze9A==} peerDependencies: @@ -340,6 +537,16 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -378,6 +585,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -386,6 +597,14 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -403,10 +622,17 @@ packages: resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} engines: {node: '>=0.1.90'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -421,10 +647,17 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} @@ -471,6 +704,10 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -479,6 +716,10 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -495,6 +736,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} @@ -524,10 +768,17 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -540,6 +791,9 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -548,10 +802,26 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -559,6 +829,14 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -615,6 +893,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -670,12 +952,21 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + logic-solver@2.0.1: resolution: {integrity: sha512-F1oCywXUzvAF4Z98mMyXySUCpUU3hNyc+JfYV3g2x/4BupC/xv94iPJuHh9us2XX5UrvY5lnKUXNvjcJNQBJ/g==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mariadb@3.4.5: resolution: {integrity: sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==} engines: {node: '>= 14'} @@ -708,6 +999,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -748,6 +1044,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -792,6 +1093,13 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pg-cloudflare@1.2.7: resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} @@ -826,10 +1134,21 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -904,6 +1223,11 @@ packages: engines: {node: '>=14'} hasBin: true + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -948,20 +1272,33 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -983,6 +1320,13 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + superjson@1.13.3: resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==} engines: {node: '>=10'} @@ -991,6 +1335,10 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -1001,6 +1349,28 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -1072,6 +1442,84 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1184,6 +1632,14 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@noble/hashes@1.8.0': {} + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + '@prisma/adapter-better-sqlite3@6.19.2': dependencies: '@prisma/driver-adapter-utils': 6.19.2 @@ -1222,19 +1678,107 @@ snapshots: dependencies: '@prisma/debug': 6.19.2 + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 '@types/node': 20.19.27 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 20.19.27 + '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.19': dependencies: '@types/node': 20.19.27 + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + '@types/express-serve-static-core@5.1.0': dependencies: '@types/node': 20.19.27 @@ -1252,6 +1796,8 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/methods@1.1.4': {} + '@types/node@20.19.27': dependencies: undici-types: 6.21.0 @@ -1275,6 +1821,60 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 20.19.27 + '@types/superagent@8.1.10': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.19.27 + form-data: 4.0.5 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.10 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.3(@types/node@20.19.27)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.3(@types/node@20.19.27)(tsx@4.21.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@zenstackhq/runtime@2.22.2(@prisma/client@7.2.0(typescript@5.9.3))(zod@4.3.5)': dependencies: '@prisma/client': 7.2.0(typescript@5.9.3) @@ -1319,6 +1919,12 @@ snapshots: array-flatten@1.1.1: {} + asap@2.0.6: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -1378,6 +1984,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -1388,6 +1996,16 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + chownr@1.1.4: {} cliui@7.0.4: @@ -1404,8 +2022,14 @@ snapshots: colors@1.4.0: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.1.0: {} + component-emitter@1.3.1: {} + concat-map@0.0.1: {} content-disposition@0.5.4: @@ -1416,8 +2040,12 @@ snapshots: cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} + cookiejar@2.1.4: {} + copy-anything@3.0.5: dependencies: is-what: 4.1.16 @@ -1459,10 +2087,14 @@ snapshots: dependencies: mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deepmerge@4.3.1: {} + delayed-stream@1.0.0: {} + denque@2.1.0: {} depd@2.0.0: {} @@ -1471,6 +2103,11 @@ snapshots: detect-libc@2.1.2: {} + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dotenv@17.2.3: {} dunder-proto@1.0.1: @@ -1493,10 +2130,19 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -1530,10 +2176,16 @@ snapshots: escape-html@1.0.3: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + etag@1.8.1: {} expand-template@2.0.3: {} + expect-type@1.3.0: {} + express@4.22.1: dependencies: accepts: 1.3.8 @@ -1570,6 +2222,12 @@ snapshots: transitivePeerDependencies: - supports-color + fast-safe-stringify@2.1.1: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + file-uri-to-path@1.0.0: {} finalhandler@1.3.2: @@ -1584,6 +2242,20 @@ snapshots: transitivePeerDependencies: - supports-color + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -1643,6 +2315,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -1693,12 +2369,20 @@ snapshots: isarray@1.0.0: {} + js-tokens@9.0.1: {} + logic-solver@2.0.1: dependencies: underscore: 1.13.7 + loupe@3.2.1: {} + lru-cache@10.4.3: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + mariadb@3.4.5: dependencies: '@types/geojson': 7946.0.16 @@ -1723,6 +2407,8 @@ snapshots: mime@1.6.0: {} + mime@2.6.0: {} + mimic-response@3.1.0: {} minimatch@3.1.2: @@ -1753,6 +2439,8 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.12: {} + napi-build-utils@2.0.0: {} negotiator@0.6.3: {} @@ -1789,6 +2477,10 @@ snapshots: path-to-regexp@0.1.12: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + pg-cloudflare@1.2.7: optional: true @@ -1824,8 +2516,18 @@ snapshots: dependencies: split2: 4.2.0 + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + pluralize@8.0.0: {} + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-array@3.0.4: {} @@ -1916,6 +2618,37 @@ snapshots: dependencies: glob: 9.3.5 + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -1983,6 +2716,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + simple-concat@1.0.1: {} simple-get@4.0.1: @@ -1991,10 +2726,16 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + source-map-js@1.2.1: {} + split2@4.2.0: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -2017,6 +2758,24 @@ snapshots: strip-json-comments@2.0.1: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3 + 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 + transitivePeerDependencies: + - supports-color + superjson@1.13.3: dependencies: copy-anything: 3.0.5 @@ -2025,6 +2784,14 @@ snapshots: dependencies: copy-anything: 4.0.5 + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -2045,6 +2812,21 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + toidentifier@1.0.1: {} ts-japi@1.12.1: {} @@ -2093,6 +2875,86 @@ snapshots: vary@1.1.2: {} + vite-node@3.2.4(@types/node@20.19.27)(tsx@4.21.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.3(@types/node@20.19.27)(tsx@4.21.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.3(@types/node@20.19.27)(tsx@4.21.0): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.27 + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@3.2.4(@types/node@20.19.27)(tsx@4.21.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.3(@types/node@20.19.27)(tsx@4.21.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.3(@types/node@20.19.27)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@20.19.27)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.27 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/src/index.ts b/src/index.ts index fa7f0c5..5a4e6ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { Command, CommanderError } from 'commander' +import { Command, CommanderError, Option } from 'commander' import * as path from 'path' import * as fs from 'fs' import { grey, red } from 'colors' @@ -25,6 +25,24 @@ export function createProgram() { .option('-d, --datasource-url ', 'Datasource URL (overrides schema configuration)') .option('--public-api-key ', 'Public API key used to verify request signatures') .option('-l, --log ', 'Query log levels (e.g., query, info, warn, error)') + .option( + '--studioAuthKey ', + 'Authentication key from ZenStack Studio. When set, the proxy will only accept requests signed by your Studio project.\nCan also be set via the ZENSTACK_STUDIO_AUTH_KEY environment variable.' + ) + .addOption( + new Option( + '--signatureToleranceSecs ', + 'Maximum age (in seconds) of a signed request before it is rejected as a replay. Defaults to 60.' + ) + .default(60) + .argParser((v) => { + const parsed = parseInt(v, 10) + if (isNaN(parsed) || parsed < 0) { + throw new CliError(`--signatureToleranceSecs must be a positive integer, got: ${v}`) + } + return parsed + }) + ) .action(async (options) => { // Determine ZModel schema path const zmodelPath = path.isAbsolute(options.schema) @@ -49,6 +67,7 @@ export function createProgram() { zmodelSchemaDir: zmodelSchemaDir, logLevel: options.log, publicAPIKey: options.publicApiKey, + signatureToleranceSecs: options.signatureToleranceSecs, }) }) diff --git a/src/server.ts b/src/server.ts index 27d57d6..8398d93 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,15 +4,11 @@ import cors from 'cors' import { ZenStackMiddleware } from '@zenstackhq/server/express' import { ZModelConfig } from './zmodel-parser' import { getNodeModulesFolder, getPrismaVersion, getZenStackVersion } from './utils/version-utils' -import { blue, grey, red } from 'colors' +import { blue, grey, red, yellow } from 'colors' import semver from 'semver' import { CliError } from './cli-error' import SuperJSON from 'superjson' -import { - buildSignedPayload, - verifySignedRequest, - type RequestSignatureHeader, -} from './signature-verifier' +import { createSignatureMiddleware, normalizePublicKey } from './signature' export interface ServerOptions { zenstackPath: string | undefined @@ -21,11 +17,14 @@ export interface ServerOptions { zmodelSchemaDir: string logLevel?: string[] publicAPIKey?: string + signatureToleranceSecs: number } -type RequestWithRawBody = express.Request & { - rawBody?: string -} +/** + * Represents the identity claim embedded in the Authorization header. + * The bearer token is a plain base64-encoded JSON string. + */ +type UserClaim = { type: 'superUser' } | { type: 'user'; data: Record } type EnhancementKind = 'password' | 'omit' | 'policy' | 'validation' | 'delegate' | 'encryption' // enable all enhancements except policy @@ -262,6 +261,56 @@ function processRequestPayload(args: any) { } } +/** + * Resolves the appropriate enhanced Prisma client for a request based on the Authorization header. + * + * - No publicAPIKey configured (authEnabled=false): return the standard enhanced client. + * - superUser claim: return the standard enhanced client (no policy enforcement). + * - Regular user claim: return the policy-enhanced client with the user identity. + * - No / invalid token: return the standard enhanced client. + */ +function resolveEnhancedClient( + prisma: any, + enhanceFunc: (prisma: any, ctx: any, opts: any) => any, + req: express.Request, + authEnabled: boolean +): any { + const baseClient = enhanceFunc(prisma, {}, { kinds: Enhancements }) + + const authHeader = req.headers['authorization'] + + if (!authEnabled && !authHeader) { + return baseClient + } + + if (!authHeader?.startsWith('Bearer ')) { + return baseClient + } + + const token = authHeader.substring(7) + let claim: UserClaim + try { + claim = JSON.parse(Buffer.from(token, 'base64').toString('utf8')) as UserClaim + } catch { + return baseClient + } + + if (claim.type === 'superUser') { + return baseClient + } + + if (claim.type === 'user') { + // Enable policy enforcement with the user's identity context. + return enhanceFunc( + prisma, + { user: claim.data }, + { kinds: [...Enhancements, 'policy'] as EnhancementKind[] } + ) + } + + return baseClient +} + async function handleTransaction(modelMeta: any, client: any, requestBody: unknown) { const processedOps: Array<{ model: string; op: string; args: unknown }> = [] if (!requestBody || !Array.isArray(requestBody) || requestBody.length === 0) { @@ -354,6 +403,17 @@ export async function startServer(options: ServerOptions) { throw new CliError('Database connection failed: ' + err) } + // Warn when running without authentication. + const publicAPIKey = options.publicAPIKey ?? process.env['ZENSTACK_STUDIO_AUTH_KEY'] + if (!publicAPIKey) { + console.warn( + yellow( + 'Warning: This proxy has no authentication. Do not expose it to the public network.\n' + + 'To secure it, get an API key from ZenStack Studio and set it via the ZENSTACK_STUDIO_AUTH_KEY environment variable.' + ) + ) + } + const app = express() app.use(cors()) @@ -361,44 +421,17 @@ export async function startServer(options: ServerOptions) { express.json({ limit: '5mb', verify: (req, _res, buf) => { - ;(req as RequestWithRawBody).rawBody = buf.toString('utf8') - }, - }) - ) - app.use( - express.urlencoded({ - extended: true, - limit: '5mb', - verify: (req, _res, buf) => { - ;(req as RequestWithRawBody).rawBody = buf.toString('utf8') + // Capture the raw body string for use in signature verification. + ;(req as express.Request & { rawBody?: string }).rawBody = buf.toString('utf8') }, }) ) + app.use(express.urlencoded({ extended: true, limit: '5mb' })) if (publicAPIKey) { - console.log(grey('Request signature verification is enabled')) - app.use((req, res, next) => { - const payload = buildSignedPayload({ - method: req.method, - rawQuery: req.originalUrl.includes('?') - ? req.originalUrl.slice(req.originalUrl.indexOf('?') + 1) - : '', - rawBody: (req as RequestWithRawBody).rawBody, - }) - - const verification = verifySignedRequest({ - publicAPIKey, - payload, - header: req.header('x-zenstack-signature') as RequestSignatureHeader, - }) - - if (!verification.ok) { - res.status(401).json({ error: verification.error }) - return - } - - next() - }) + const toleranceSecs = options.signatureToleranceSecs ?? 60 + const normalizedKey = normalizePublicKey(publicAPIKey) + app.use(['/api/model', '/api/schema'], createSignatureMiddleware(normalizedKey, toleranceSecs)) } // ZenStack API endpoint @@ -406,13 +439,7 @@ export async function startServer(options: ServerOptions) { app.post('/api/model/\\$transaction/sequential', async (_req, res) => { const response = await handleTransaction( modelMeta, - enhanceFunc( - prisma, - {}, - { - kinds: Enhancements, - } - ), + resolveEnhancedClient(prisma, enhanceFunc, _req, !!publicAPIKey), _req.body ) res.status(response.status).json(response.body) @@ -421,14 +448,8 @@ export async function startServer(options: ServerOptions) { app.use( '/api/model', ZenStackMiddleware({ - getPrisma: () => { - return enhanceFunc( - prisma, - {}, - { - kinds: Enhancements, - } - ) + getPrisma: (req) => { + return resolveEnhancedClient(prisma, enhanceFunc, req, !!publicAPIKey) }, }) ) diff --git a/src/signature.ts b/src/signature.ts new file mode 100644 index 0000000..452ca29 --- /dev/null +++ b/src/signature.ts @@ -0,0 +1,112 @@ +import { verify } from 'node:crypto' +import express from 'express' +import { yellow } from 'colors' + +/** + * Accepts a public key in either PEM format or as a raw base64 / base64url DER string + * (without the `-----BEGIN PUBLIC KEY-----` markers) and always returns a PEM string. + */ +export function normalizePublicKey(key: string): string { + key = key.trim() + if (key.startsWith('-----BEGIN PUBLIC KEY-----')) { + return key + } + // Convert base64url → standard base64, then wrap in PEM markers. + const b64 = key.replace(/-/g, '+').replace(/_/g, '/') + return `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----` +} + +/** + * Verifies an ed25519 signature. + * + * @param publicKey PEM-encoded public key (use normalizePublicKey first) + * @param message The message that was signed + * @param sig The base64url-encoded signature (the value after `v1=`) + * @returns true if the signature is valid, false otherwise + */ +export function verifyEd25519Signature(publicKey: string, message: string, sig: string): boolean { + try { + return verify(null, Buffer.from(message, 'utf8'), publicKey, Buffer.from(sig, 'base64url')) + } catch { + return false + } +} + +/** + * Creates an Express middleware that verifies the ed25519 signature on every request. + * + * Signature header format: `x-zenstack-signature: t=,v1=` + * + * The signed message is constructed as: + * - GET / DELETE requests: `[]` + * - Other methods: `[]` + * + * `authorizationToken` is the bearer token value from the `Authorization` header (if present). + */ +export function createSignatureMiddleware(publicKey: string, toleranceSeconds: number) { + // Throttle invalid-signature warnings to at most once per 60 seconds. + let lastInvalidSigWarnAt = 0 + const WARN_THROTTLE_SECS = 60 + + function warnInvalidSignature() { + const now = Math.floor(Date.now() / 1000) + if (now - lastInvalidSigWarnAt >= WARN_THROTTLE_SECS) { + lastInvalidSigWarnAt = now + console.warn( + yellow( + 'Warning: Received a request with an invalid signature. ' + + 'Please double-check whether you have the correct public API key configured.' + ) + ) + } + } + + return (req: express.Request, res: express.Response, next: express.NextFunction) => { + const signatureHeader = req.headers['x-zenstack-signature'] + if (!signatureHeader || typeof signatureHeader !== 'string') { + return res.status(401).json({ message: 'Missing x-zenstack-signature header' }) + } + + const parts = signatureHeader.split(',') + const timestampPart = parts.find((p) => p.startsWith('t=')) + const sigPart = parts.find((p) => p.startsWith('v1=')) + if (!timestampPart || !sigPart) { + return res.status(401).json({ message: 'Invalid x-zenstack-signature format' }) + } + const timestamp = timestampPart.substring(2) + const sig = sigPart.substring(3) + + // Replay-attack prevention: reject requests whose timestamp deviates + // from server time by more than the configured tolerance. + const requestTime = parseInt(timestamp, 10) + const now = Math.floor(Date.now() / 1000) + if (isNaN(requestTime) || Math.abs(now - requestTime) > toleranceSeconds) { + return res.status(401).json({ message: 'Request timestamp is expired or invalid' }) + } + + // Payload: raw query string for GET/DELETE, raw body for other methods. + let payload: string + if (req.method === 'GET' || req.method === 'DELETE') { + const qMark = req.originalUrl.indexOf('?') + payload = qMark >= 0 ? req.originalUrl.substring(qMark + 1) : '' + } else { + payload = (req as express.Request & { rawBody?: string }).rawBody ?? '' + } + + // authorizationToken is the bearer token value (if present). + const authHeader = req.headers['authorization'] + const authorizationToken = + authHeader && authHeader.startsWith('Bearer ') ? authHeader.substring(7) : undefined + + const message = authorizationToken + ? `${payload}${timestamp}${authorizationToken}` + : `${payload}${timestamp}` + + if (!verifyEd25519Signature(publicKey, message, sig)) { + warnInvalidSignature() + return res.status(401).json({ message: 'Invalid signature' }) + } + + return next() + } +} diff --git a/test/signature.test.ts b/test/signature.test.ts new file mode 100644 index 0000000..56487ca --- /dev/null +++ b/test/signature.test.ts @@ -0,0 +1,401 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import express from 'express' +import { sign } from 'node:crypto' +import request from 'supertest' +import { + createSignatureMiddleware, + normalizePublicKey, + verifyEd25519Signature, +} from '../src/signature' + +// ─── Ed25519 key pair for tests ─────────────────────────────────────────────── + +const TEST_PRIVATE_KEY = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIHIlHXhk+zc9ziuvrYAnZZgGL36H1GXwfsYchM9dM8gR +-----END PRIVATE KEY-----` + +const TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs= +-----END PUBLIC KEY-----` + +/** Raw base64 DER — the same key without PEM markers. */ +const TEST_PUBLIC_KEY_DER = 'MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=' + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** + * Builds the `x-zenstack-signature` header value for a request. + */ +function buildSignatureHeader(options: { + privateKey: string + method: string + pathWithQuery: string + body?: unknown + authorizationToken?: string + timestamp?: string +}): string { + const timestamp = options.timestamp ?? String(Math.floor(Date.now() / 1000)) + const method = options.method.toUpperCase() + let payload: string + if (method === 'GET' || method === 'DELETE') { + const qMark = options.pathWithQuery.indexOf('?') + payload = qMark >= 0 ? options.pathWithQuery.substring(qMark + 1) : '' + } else { + payload = options.body != null ? JSON.stringify(options.body) : '' + } + + const message = options.authorizationToken + ? `${payload}${timestamp}${options.authorizationToken}` + : `${payload}${timestamp}` + + const sig = sign(null, Buffer.from(message, 'utf8'), options.privateKey).toString('base64url') + return `t=${timestamp},v1=${sig}` +} + +// ─── normalizePublicKey ──────────────────────────────────────────────────────── + +describe('normalizePublicKey', () => { + it('returns PEM key unchanged', () => { + const result = normalizePublicKey(TEST_PUBLIC_KEY) + assert.strictEqual(result, TEST_PUBLIC_KEY) + }) + + it('wraps raw base64 DER in PEM markers', () => { + const result = normalizePublicKey(TEST_PUBLIC_KEY_DER) + assert.ok(result.includes('-----BEGIN PUBLIC KEY-----')) + assert.ok(result.includes('-----END PUBLIC KEY-----')) + assert.ok(result.includes(TEST_PUBLIC_KEY_DER)) + }) + + it('converts base64url to standard base64 before wrapping', () => { + const base64url = TEST_PUBLIC_KEY_DER.replace(/\+/g, '-').replace(/\//g, '_') + const result = normalizePublicKey(base64url) + // After normalization, the body should be standard base64 (no `-` or `_`) + const body = result + .replace('-----BEGIN PUBLIC KEY-----\n', '') + .replace('\n-----END PUBLIC KEY-----', '') + assert.doesNotMatch(body, /[-_]/) + }) + + it('trims leading/trailing whitespace', () => { + const result = normalizePublicKey(` ${TEST_PUBLIC_KEY_DER} `) + assert.ok(result.includes('-----BEGIN PUBLIC KEY-----')) + assert.ok(result.includes('-----END PUBLIC KEY-----')) + }) +}) + +// ─── verifyEd25519Signature ──────────────────────────────────────────────────── + +describe('verifyEd25519Signature', () => { + const normalizedKey = normalizePublicKey(TEST_PUBLIC_KEY) + + it('returns true for a valid signature', () => { + const message = 'hello world' + const sig = sign(null, Buffer.from(message, 'utf8'), TEST_PRIVATE_KEY).toString('base64url') + assert.ok(verifyEd25519Signature(normalizedKey, message, sig)) + }) + + it('returns false for a tampered message', () => { + const message = 'hello world' + const sig = sign(null, Buffer.from(message, 'utf8'), TEST_PRIVATE_KEY).toString('base64url') + assert.ok(!verifyEd25519Signature(normalizedKey, 'tampered message', sig)) + }) + + it('returns false for a garbage signature', () => { + assert.ok(!verifyEd25519Signature(normalizedKey, 'hello', 'notavalidsig')) + }) + + it('returns false for an empty signature', () => { + assert.ok(!verifyEd25519Signature(normalizedKey, 'hello', '')) + }) + + it('works with a raw DER key after normalization', () => { + const message = 'test-message' + const sig = sign(null, Buffer.from(message, 'utf8'), TEST_PRIVATE_KEY).toString('base64url') + assert.ok(verifyEd25519Signature(normalizePublicKey(TEST_PUBLIC_KEY_DER), message, sig)) + }) +}) + +// ─── createSignatureMiddleware ───────────────────────────────────────────────── + +describe('createSignatureMiddleware', () => { + function buildApp(publicKey: string, toleranceSecs = 60) { + const app = express() + app.use( + express.json({ + verify: (req, _res, buf) => { + ;(req as express.Request & { rawBody?: string }).rawBody = buf.toString('utf8') + }, + }) + ) + app.use(createSignatureMiddleware(normalizePublicKey(publicKey), toleranceSecs)) + app.get('/ping', (_req, res) => res.json({ ok: true })) + app.post('/ping', (_req, res) => res.json({ ok: true })) + app.put('/ping', (_req, res) => res.json({ ok: true })) + return app + } + + describe('missing / malformed header', () => { + it('returns 401 when x-zenstack-signature header is absent', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const res = await request(app).get('/ping') + assert.strictEqual(res.status, 401) + assert.match(res.body.message, /missing/i) + }) + + it('returns 401 when the header format is invalid', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const res = await request(app).get('/ping').set('x-zenstack-signature', 'garbage') + assert.strictEqual(res.status, 401) + assert.match(res.body.message, /invalid.*format/i) + }) + + it('returns 401 when t= part is missing', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const res = await request(app).get('/ping').set('x-zenstack-signature', 'v1=abc') + assert.strictEqual(res.status, 401) + }) + + it('returns 401 when v1= part is missing', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const res = await request(app) + .get('/ping') + .set('x-zenstack-signature', `t=${Math.floor(Date.now() / 1000)}`) + assert.strictEqual(res.status, 401) + }) + }) + + describe('timestamp validation', () => { + it('returns 401 when timestamp is too old (default 60s window)', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const expiredTimestamp = String(Math.floor(Date.now() / 1000) - 120) + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + timestamp: expiredTimestamp, + }) + const res = await request(app).get('/ping').set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 401) + assert.match(res.body.message, /expired/i) + }) + + it('returns 401 when timestamp is too far in the future', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const futureTimestamp = String(Math.floor(Date.now() / 1000) + 120) + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + timestamp: futureTimestamp, + }) + const res = await request(app).get('/ping').set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 401) + assert.match(res.body.message, /expired/i) + }) + + it('accepts a request within the custom tolerance window', async () => { + const app = buildApp(TEST_PUBLIC_KEY, 300) + const timestamp = String(Math.floor(Date.now() / 1000) - 120) + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + timestamp, + }) + const res = await request(app).get('/ping').set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 200) + }) + + it('rejects a request outside a tight custom tolerance', async () => { + const app = buildApp(TEST_PUBLIC_KEY, 5) + const timestamp = String(Math.floor(Date.now() / 1000) - 10) + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + timestamp, + }) + const res = await request(app).get('/ping').set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 401) + }) + }) + + describe('GET request signature', () => { + it('accepts a valid GET request with no query params', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + }) + const res = await request(app).get('/ping').set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 200) + }) + + it('accepts a valid GET request with query params', async () => { + const appInner = express() + appInner.use( + express.json({ + verify: (req, _res, buf) => { + ;(req as express.Request & { rawBody?: string }).rawBody = buf.toString('utf8') + }, + }) + ) + appInner.use(createSignatureMiddleware(normalizePublicKey(TEST_PUBLIC_KEY), 60)) + appInner.get('/search', (_req, res) => res.json({ ok: true })) + + const pathWithQuery = '/search?q=hello%20world&page=1' + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery, + }) + const res = await request(appInner).get(pathWithQuery).set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 200) + }) + + it('rejects a GET request when query string is tampered', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + // Sign with original query, then send a different query + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping?foo=bar', + }) + const res = await request(app).get('/ping?foo=tampered').set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 401) + }) + }) + + describe('POST request signature', () => { + it('accepts a valid POST request with a JSON body', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const body = { data: { email: 'alice@example.com' } } + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'POST', + pathWithQuery: '/ping', + body, + }) + const res = await request(app) + .post('/ping') + .set('x-zenstack-signature', sig) + .set('Content-Type', 'application/json') + .send(body) + assert.strictEqual(res.status, 200) + }) + + it('rejects a POST request when the body is tampered', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const originalBody = { data: { email: 'alice@example.com' } } + const tamperedBody = { data: { email: 'evil@example.com' } } + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'POST', + pathWithQuery: '/ping', + body: originalBody, + }) + const res = await request(app) + .post('/ping') + .set('x-zenstack-signature', sig) + .set('Content-Type', 'application/json') + .send(tamperedBody) + assert.strictEqual(res.status, 401) + }) + }) + + describe('PUT request signature', () => { + it('accepts a valid PUT request', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const body = { where: { id: 'u1' }, data: { email: 'new@example.com' } } + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'PUT', + pathWithQuery: '/ping', + body, + }) + const res = await request(app) + .put('/ping') + .set('x-zenstack-signature', sig) + .set('Content-Type', 'application/json') + .send(body) + assert.strictEqual(res.status, 200) + }) + }) + + describe('Authorization header is included in the signed message', () => { + it('rejects a request when the signature does not cover the Authorization header', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const authToken = Buffer.from(JSON.stringify({ type: 'superUser' })).toString('base64') + // Sign WITHOUT including the auth token + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + }) + const res = await request(app) + .get('/ping') + .set('x-zenstack-signature', sig) + .set('Authorization', `Bearer ${authToken}`) + assert.strictEqual(res.status, 401) + }) + + it('accepts a request when the signature covers the Authorization header', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const authToken = Buffer.from(JSON.stringify({ type: 'superUser' })).toString('base64') + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + authorizationToken: authToken, + }) + const res = await request(app) + .get('/ping') + .set('x-zenstack-signature', sig) + .set('Authorization', `Bearer ${authToken}`) + assert.strictEqual(res.status, 200) + }) + + it('rejects when the auth token is swapped after signing', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const originalToken = Buffer.from(JSON.stringify({ type: 'superUser' })).toString('base64') + const differentToken = Buffer.from( + JSON.stringify({ type: 'user', data: { id: 'u1' } }) + ).toString('base64') + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + authorizationToken: originalToken, + }) + const res = await request(app) + .get('/ping') + .set('x-zenstack-signature', sig) + .set('Authorization', `Bearer ${differentToken}`) + assert.strictEqual(res.status, 401) + }) + }) + + describe('public key format', () => { + it('accepts a raw base64 DER key (without PEM markers)', async () => { + const app = buildApp(TEST_PUBLIC_KEY_DER) + const sig = buildSignatureHeader({ + privateKey: TEST_PRIVATE_KEY, + method: 'GET', + pathWithQuery: '/ping', + }) + const res = await request(app).get('/ping').set('x-zenstack-signature', sig) + assert.strictEqual(res.status, 200) + }) + + it('rejects an invalid signature with the correct key format', async () => { + const app = buildApp(TEST_PUBLIC_KEY) + const res = await request(app) + .get('/ping') + .set('x-zenstack-signature', `t=${Math.floor(Date.now() / 1000)},v1=invalidsignature`) + assert.strictEqual(res.status, 401) + }) + }) +}) From 1c63ae5416838c61db64aaf81156903d659b93c1 Mon Sep 17 00:00:00 2001 From: jiasheng Date: Tue, 16 Jun 2026 22:16:08 +0800 Subject: [PATCH 3/8] feat: update version to 0.5.0 and rename publicAPIKey to studioAuthKey --- package.json | 2 +- src/index.ts | 3 +-- src/server.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index c6cffab..607d271 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/proxy", - "version": "0.4.3", + "version": "0.5.0", "description": "A CLI tool to run an Express server that proxies CRUD requests to a ZenStack backend", "main": "index.js", "publishConfig": { diff --git a/src/index.ts b/src/index.ts index 5a4e6ca..35f7825 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,6 @@ export function createProgram() { .option('-p, --port ', 'Port number for the server', '2311') .option('-s, --schema ', 'Path to ZModel schema file', 'schema.zmodel') .option('-d, --datasource-url ', 'Datasource URL (overrides schema configuration)') - .option('--public-api-key ', 'Public API key used to verify request signatures') .option('-l, --log ', 'Query log levels (e.g., query, info, warn, error)') .option( '--studioAuthKey ', @@ -66,7 +65,7 @@ export function createProgram() { zmodelConfig: zmodelConfig, zmodelSchemaDir: zmodelSchemaDir, logLevel: options.log, - publicAPIKey: options.publicApiKey, + studioAuthKey: options.studioAuthKey, signatureToleranceSecs: options.signatureToleranceSecs, }) }) diff --git a/src/server.ts b/src/server.ts index 8398d93..631e53f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,7 +16,7 @@ export interface ServerOptions { zmodelConfig: ZModelConfig zmodelSchemaDir: string logLevel?: string[] - publicAPIKey?: string + studioAuthKey?: string signatureToleranceSecs: number } @@ -380,7 +380,7 @@ async function handleTransaction(modelMeta: any, client: any, requestBody: unkno * Start the Express server with ZenStack proxy */ export async function startServer(options: ServerOptions) { - const { zenstackPath, port, zmodelConfig, zmodelSchemaDir, publicAPIKey } = options + const { zenstackPath, port, zmodelConfig, zmodelSchemaDir } = options const { PrismaClient, modelMeta, enums, zenstackVersion, enhanceFunc } = await loadZenStackModules(zmodelConfig, zmodelSchemaDir, zenstackPath) @@ -404,8 +404,8 @@ export async function startServer(options: ServerOptions) { } // Warn when running without authentication. - const publicAPIKey = options.publicAPIKey ?? process.env['ZENSTACK_STUDIO_AUTH_KEY'] - if (!publicAPIKey) { + const studioAuthKey = options.studioAuthKey ?? process.env['ZENSTACK_STUDIO_AUTH_KEY'] + if (!studioAuthKey) { console.warn( yellow( 'Warning: This proxy has no authentication. Do not expose it to the public network.\n' + @@ -428,9 +428,9 @@ export async function startServer(options: ServerOptions) { ) app.use(express.urlencoded({ extended: true, limit: '5mb' })) - if (publicAPIKey) { + if (studioAuthKey) { const toleranceSecs = options.signatureToleranceSecs ?? 60 - const normalizedKey = normalizePublicKey(publicAPIKey) + const normalizedKey = normalizePublicKey(studioAuthKey) app.use(['/api/model', '/api/schema'], createSignatureMiddleware(normalizedKey, toleranceSecs)) } @@ -439,7 +439,7 @@ export async function startServer(options: ServerOptions) { app.post('/api/model/\\$transaction/sequential', async (_req, res) => { const response = await handleTransaction( modelMeta, - resolveEnhancedClient(prisma, enhanceFunc, _req, !!publicAPIKey), + resolveEnhancedClient(prisma, enhanceFunc, _req, !!studioAuthKey), _req.body ) res.status(response.status).json(response.body) @@ -449,7 +449,7 @@ export async function startServer(options: ServerOptions) { '/api/model', ZenStackMiddleware({ getPrisma: (req) => { - return resolveEnhancedClient(prisma, enhanceFunc, req, !!publicAPIKey) + return resolveEnhancedClient(prisma, enhanceFunc, req, !!studioAuthKey) }, }) ) From 4f26d5853d6b9381a4cff533303d2b8d339d0a6a Mon Sep 17 00:00:00 2001 From: jiasheng Date: Tue, 16 Jun 2026 22:17:59 +0800 Subject: [PATCH 4/8] fix: update pnpm-lock.yaml --- pnpm-lock.yaml | 684 ------------------------------------------------- 1 file changed, 684 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2c6be2..38261d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,9 +78,6 @@ importers: typescript: specifier: ^5.0.0 version: 5.9.3 - vitest: - specifier: ^3.0.0 - version: 3.2.4(@types/node@20.19.27)(tsx@4.21.0) publishDirectory: dist packages: @@ -241,9 +238,6 @@ packages: cpu: [x64] os: [win32] - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -287,137 +281,9 @@ packages: '@prisma/driver-adapter-utils@6.19.2': resolution: {integrity: sha512-tkHsL3jhx81eXg2oqtJH/1IEs8uEeUb1RpqHtwYqdNb176u9D0mnHRZM1/cKca/XhLpq49Nnd9XDxdMfWcKAYA==} - '@rollup/rollup-android-arm-eabi@4.60.4': - resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.4': - resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.4': - resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.4': - resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.4': - resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.4': - resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.60.4': - resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.60.4': - resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.60.4': - resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.60.4': - resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.60.4': - resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.60.4': - resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.60.4': - resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.60.4': - resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.60.4': - resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.60.4': - resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.4': - resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.4': - resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.4': - resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.4': - resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.4': - resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} - cpu: [x64] - os: [win32] - '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -427,15 +293,6 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@5.1.0': resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} @@ -478,35 +335,6 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@zenstackhq/runtime@2.22.2': resolution: {integrity: sha512-GgWSyU1nL1q89ZOS7O7cn9GWUcQ2ywavTp73RJvqRw0HSQ2r5sCi+wAyh2eAxqGTSnBob4jg2kq7CcFqcaze9A==} peerDependencies: @@ -540,10 +368,6 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -585,10 +409,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -597,14 +417,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} - - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -704,10 +516,6 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -768,9 +576,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -791,9 +596,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -802,10 +604,6 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} @@ -813,15 +611,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -952,21 +741,12 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - logic-solver@2.0.1: resolution: {integrity: sha512-F1oCywXUzvAF4Z98mMyXySUCpUU3hNyc+JfYV3g2x/4BupC/xv94iPJuHh9us2XX5UrvY5lnKUXNvjcJNQBJ/g==} - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - mariadb@3.4.5: resolution: {integrity: sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==} engines: {node: '>= 14'} @@ -1044,11 +824,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -1093,13 +868,6 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - pg-cloudflare@1.2.7: resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} @@ -1134,21 +902,10 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -1223,11 +980,6 @@ packages: engines: {node: '>=14'} hasBin: true - rollup@4.60.4: - resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -1272,33 +1024,20 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1320,9 +1059,6 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - superagent@10.3.0: resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} @@ -1349,28 +1085,6 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} - toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -1442,84 +1156,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite@7.3.3: - resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.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 - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1632,8 +1268,6 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} - '@noble/hashes@1.8.0': {} '@paralleldrive/cuid2@2.3.1': @@ -1678,91 +1312,11 @@ snapshots: dependencies: '@prisma/debug': 6.19.2 - '@rollup/rollup-android-arm-eabi@4.60.4': - optional: true - - '@rollup/rollup-android-arm64@4.60.4': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.4': - optional: true - - '@rollup/rollup-darwin-x64@4.60.4': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.4': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.4': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.4': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.4': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.4': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.4': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.4': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.4': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.4': - optional: true - '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 '@types/node': 20.19.27 - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - '@types/connect@3.4.38': dependencies: '@types/node': 20.19.27 @@ -1773,12 +1327,6 @@ snapshots: dependencies: '@types/node': 20.19.27 - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.8': {} - - '@types/estree@1.0.9': {} - '@types/express-serve-static-core@5.1.0': dependencies: '@types/node': 20.19.27 @@ -1833,48 +1381,6 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.10 - '@vitest/expect@3.2.4': - dependencies: - '@types/chai': 5.2.3 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.2.4(vite@7.3.3(@types/node@20.19.27)(tsx@4.21.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.3(@types/node@20.19.27)(tsx@4.21.0) - - '@vitest/pretty-format@3.2.4': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/runner@3.2.4': - dependencies: - '@vitest/utils': 3.2.4 - pathe: 2.0.3 - strip-literal: 3.1.0 - - '@vitest/snapshot@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 - - '@vitest/utils@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 - '@zenstackhq/runtime@2.22.2(@prisma/client@7.2.0(typescript@5.9.3))(zod@4.3.5)': dependencies: '@prisma/client': 7.2.0(typescript@5.9.3) @@ -1921,8 +1427,6 @@ snapshots: asap@2.0.6: {} - assertion-error@2.0.1: {} - asynckit@0.4.0: {} balanced-match@1.0.2: {} @@ -1984,8 +1488,6 @@ snapshots: bytes@3.1.2: {} - cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -1996,16 +1498,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - check-error@2.1.3: {} - chownr@1.1.4: {} cliui@7.0.4: @@ -2087,8 +1579,6 @@ snapshots: dependencies: mimic-response: 3.1.0 - deep-eql@5.0.2: {} - deep-extend@0.6.0: {} deepmerge@4.3.1: {} @@ -2130,8 +1620,6 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} - es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -2176,16 +1664,10 @@ snapshots: escape-html@1.0.3: {} - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - etag@1.8.1: {} expand-template@2.0.3: {} - expect-type@1.3.0: {} - express@4.22.1: dependencies: accepts: 1.3.8 @@ -2224,10 +1706,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - file-uri-to-path@1.0.0: {} finalhandler@1.3.2: @@ -2369,20 +1847,12 @@ snapshots: isarray@1.0.0: {} - js-tokens@9.0.1: {} - logic-solver@2.0.1: dependencies: underscore: 1.13.7 - loupe@3.2.1: {} - lru-cache@10.4.3: {} - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - mariadb@3.4.5: dependencies: '@types/geojson': 7946.0.16 @@ -2439,8 +1909,6 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.12: {} - napi-build-utils@2.0.0: {} negotiator@0.6.3: {} @@ -2477,10 +1945,6 @@ snapshots: path-to-regexp@0.1.12: {} - pathe@2.0.3: {} - - pathval@2.0.1: {} - pg-cloudflare@1.2.7: optional: true @@ -2516,18 +1980,8 @@ snapshots: dependencies: split2: 4.2.0 - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - pluralize@8.0.0: {} - postcss@8.5.15: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postgres-array@2.0.0: {} postgres-array@3.0.4: {} @@ -2618,37 +2072,6 @@ snapshots: dependencies: glob: 9.3.5 - rollup@4.60.4: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.4 - '@rollup/rollup-android-arm64': 4.60.4 - '@rollup/rollup-darwin-arm64': 4.60.4 - '@rollup/rollup-darwin-x64': 4.60.4 - '@rollup/rollup-freebsd-arm64': 4.60.4 - '@rollup/rollup-freebsd-x64': 4.60.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 - '@rollup/rollup-linux-arm-musleabihf': 4.60.4 - '@rollup/rollup-linux-arm64-gnu': 4.60.4 - '@rollup/rollup-linux-arm64-musl': 4.60.4 - '@rollup/rollup-linux-loong64-gnu': 4.60.4 - '@rollup/rollup-linux-loong64-musl': 4.60.4 - '@rollup/rollup-linux-ppc64-gnu': 4.60.4 - '@rollup/rollup-linux-ppc64-musl': 4.60.4 - '@rollup/rollup-linux-riscv64-gnu': 4.60.4 - '@rollup/rollup-linux-riscv64-musl': 4.60.4 - '@rollup/rollup-linux-s390x-gnu': 4.60.4 - '@rollup/rollup-linux-x64-gnu': 4.60.4 - '@rollup/rollup-linux-x64-musl': 4.60.4 - '@rollup/rollup-openbsd-x64': 4.60.4 - '@rollup/rollup-openharmony-arm64': 4.60.4 - '@rollup/rollup-win32-arm64-msvc': 4.60.4 - '@rollup/rollup-win32-ia32-msvc': 4.60.4 - '@rollup/rollup-win32-x64-gnu': 4.60.4 - '@rollup/rollup-win32-x64-msvc': 4.60.4 - fsevents: 2.3.3 - safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -2716,8 +2139,6 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - simple-concat@1.0.1: {} simple-get@4.0.1: @@ -2726,16 +2147,10 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - source-map-js@1.2.1: {} - split2@4.2.0: {} - stackback@0.0.2: {} - statuses@2.0.2: {} - std-env@3.10.0: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -2758,10 +2173,6 @@ snapshots: strip-json-comments@2.0.1: {} - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - superagent@10.3.0: dependencies: component-emitter: 1.3.1 @@ -2812,21 +2223,6 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} - toidentifier@1.0.1: {} ts-japi@1.12.1: {} @@ -2875,86 +2271,6 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@20.19.27)(tsx@4.21.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.3(@types/node@20.19.27)(tsx@4.21.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@7.3.3(@types/node@20.19.27)(tsx@4.21.0): - dependencies: - esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.15 - rollup: 4.60.4 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 20.19.27 - fsevents: 2.3.3 - tsx: 4.21.0 - - vitest@3.2.4(@types/node@20.19.27)(tsx@4.21.0): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.3(@types/node@20.19.27)(tsx@4.21.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.17 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.3.3(@types/node@20.19.27)(tsx@4.21.0) - vite-node: 3.2.4(@types/node@20.19.27)(tsx@4.21.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.19.27 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 From 7466feaea7c81a6ee9d311a9eb4908e6359d86e6 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Tue, 16 Jun 2026 22:23:51 +0800 Subject: [PATCH 5/8] Update package.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 607d271..c166813 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "clean": "rimraf dist", "build": "pnpm clean && tsc && copyfiles -F \"bin/*\" dist && copyfiles ./README.md ./LICENSE ./package.json dist && pnpm pack dist --pack-destination ../build", - "test": "node --require tsx/cjs --test test/signature.test.ts" + "test": "node --require tsx/cjs --test test/**/*.test.ts src/**/*.test.ts" }, "keywords": [ "zenstack", From d7fcb3c2182f8fff9b357d0a59515ba9fd5e775c Mon Sep 17 00:00:00 2001 From: jiasheng Date: Tue, 16 Jun 2026 22:32:43 +0800 Subject: [PATCH 6/8] feat: update authentication key in README to studioAuthKey and adjust related examples --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cbfc7e2..627c7d9 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ zenstack-proxy [options] - `-p, --port ` Port number for the server (default: `8008`) - `-s, --schema ` - Path to ZModel schema file (default: "schema.zmodel") - `-d, --datasource-url ` Datasource URL (overrides schema configuration) -- `--public-api-key ` Public API key used to verify `X-ZenStack-Signature` request headers +- `--studioAuthKey ` Authentication key from ZenStack Studio. When set, the proxy will only accept requests signed by your Studio project. - `-l, --log ` Query log levels (e.g., query, info, warn, error) +- `--signature-tolerance-secs ` Time tolerance in seconds for signed requests (default: `60`) ### Examples @@ -50,15 +51,16 @@ zenstack-proxy -p 8888 #### Enable signed requests ```bash -zenstack-proxy --public-api-key "MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=" +zenstack-proxy --studioAuthKey "MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=" ``` -When `--public-api-key` is provided, every incoming request must include an `X-ZenStack-Signature` header in the format `t=,v1=`. -The signed message format matches ZenStack Studio: `payload + timestamp`. +When `--studioAuthKey` is provided, every incoming request must include an `X-ZenStack-Signature` header in the format `t=,v1=`. +The signed message format matches ZenStack Studio: `payload + timestamp [+ authorizationToken]`. - For `GET` and `DELETE` requests, `payload` is the raw query string without the leading `?`. - For body-based requests, `payload` is the exact JSON request body string. - For requests without query params or a request body, `payload` is an empty string. +- If an `Authorization: Bearer ` header is present, append `` to the signed message. ## Server Endpoints From 30ad1a683a533c43cc92b4663c7aac9a9b067313 Mon Sep 17 00:00:00 2001 From: jiasheng Date: Thu, 18 Jun 2026 21:17:22 +0800 Subject: [PATCH 7/8] feat: add test step to build workflow and remove obsolete signature verifier files --- .github/workflows/build-test.yml | 3 + src/signature-verifier.test.ts | 78 -------------------- src/signature-verifier.ts | 122 ------------------------------- 3 files changed, 3 insertions(+), 200 deletions(-) delete mode 100644 src/signature-verifier.test.ts delete mode 100644 src/signature-verifier.ts diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 26166d3..7c7f801 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -57,3 +57,6 @@ jobs: run: | pnpm run build pnpm tsx scripts/post-build.ts + + - name: Test + run: pnpm test diff --git a/src/signature-verifier.test.ts b/src/signature-verifier.test.ts deleted file mode 100644 index 1a2d5b4..0000000 --- a/src/signature-verifier.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' -import { buildSignedPayload, verifySignedRequest } from './signature-verifier' - -test('verifies sample GET signature using base64 DER public key', () => { - const payload = buildSignedPayload({ - method: 'GET', - rawQuery: 'q=%7B%22where%22%3A%7B%7D%2C%22take%22%3A100%2C%22skip%22%3A0%7D', - }) - - const result = verifySignedRequest({ - publicAPIKey: 'MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=', - payload, - header: - 't=1777590674,v1=_Mbr9a-X24ZBUWQLGiPnehh99yBGXoMmfJ9Jh5N99E1uz4NpjZrCzdoKQZGCsYLRgYYJmCElsZ_6YT4FwlhZBQ', - }) - - assert.equal(result.ok, true) -}) - -test('verifies sample signature using PEM public key', () => { - const payload = buildSignedPayload({ - method: 'GET', - rawQuery: 'q=%7B%22where%22%3A%7B%7D%2C%22take%22%3A100%2C%22skip%22%3A0%7D', - }) - - const result = verifySignedRequest({ - publicAPIKey: `-----BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs= ------END PUBLIC KEY-----`, - payload, - header: - 't=1777590674,v1=_Mbr9a-X24ZBUWQLGiPnehh99yBGXoMmfJ9Jh5N99E1uz4NpjZrCzdoKQZGCsYLRgYYJmCElsZ_6YT4FwlhZBQ', - }) - - assert.equal(result.ok, true) -}) - -test('verifies sample PUT signature using request body payload', () => { - const payload = buildSignedPayload({ - method: 'PUT', - rawBody: '{"data":{"meta":{"sessionNumber":15}},"where":{"id":"cmkhoq1t1000cb6av8sq9ql12"}}', - }) - - const result = verifySignedRequest({ - publicAPIKey: 'MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=', - payload, - header: - 't=1777590954,v1=L_b8qi55lLv5XHfopAhC15qbJ_GAc-zGs8CUakXxnDh3Xce0seAC3Ri5mbEwVx27ckYV821wmaxJZt8fvFNnDA', - }) - - assert.equal(result.ok, true) -}) - -test('rejects invalid signatures', () => { - const result = verifySignedRequest({ - publicAPIKey: 'MCowBQYDK2VwAyEAFSJV7wjdFuDz2CqYX7hGnITQvcmJYy7OJQq2Cy2Eiqs=', - payload: '{}', - header: - 't=1777590674,v1=_Mbr9a-X24ZBUWQLGiPnehh99yBGXoMmfJ9Jh5N99E1uz4NpjZrCzdoKQZGCsYLRgYYJmCElsZ_6YT4FwlhZBQ', - }) - - assert.deepEqual(result, { - ok: false, - error: 'Invalid request signature.', - }) -}) - -test('uses raw body for body-based requests', () => { - assert.equal( - buildSignedPayload({ method: 'POST', rawBody: '{"data":{"id":1}}' }), - '{"data":{"id":1}}' - ) - assert.equal( - buildSignedPayload({ method: 'PUT', rawBody: '{"where":{"id":1}}' }), - '{"where":{"id":1}}' - ) -}) diff --git a/src/signature-verifier.ts b/src/signature-verifier.ts deleted file mode 100644 index 6f405cd..0000000 --- a/src/signature-verifier.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { createPublicKey, type KeyObject, verify } from 'node:crypto' - -export type RequestSignatureHeader = string | undefined - -type ParsedSignatureHeader = { - timestamp: string - signature: string -} - -type VerifySignedRequestOptions = { - publicAPIKey: string - payload: string - header: RequestSignatureHeader -} - -type VerifySignedRequestResult = - | { - ok: true - } - | { - ok: false - error: string - } - -export function buildSignedPayload({ - method, - rawQuery, - rawBody, -}: { - method: string - rawQuery?: string - rawBody?: string -}) { - const normalizedMethod = method.toUpperCase() - - if (normalizedMethod === 'GET' || normalizedMethod === 'DELETE') { - return rawQuery ?? '' - } - - return rawBody ?? '' -} - -export function verifySignedRequest({ - publicAPIKey, - payload, - header, -}: VerifySignedRequestOptions): VerifySignedRequestResult { - const parsedHeader = parseSignatureHeader(header) - if (!parsedHeader) { - return { - ok: false, - error: - 'Missing or invalid x-zenstack-signature header. Expected format: t=,v1=.', - } - } - - const publicKey = parsePublicKey(publicAPIKey) - if (!publicKey) { - return { - ok: false, - error: - 'Invalid public API key. Expected a PEM public key or base64-encoded Ed25519 SPKI key.', - } - } - - const { timestamp, signature } = parsedHeader - const message = `${payload}${timestamp}` - - try { - const isValid = verify( - null, - Buffer.from(message, 'utf8'), - publicKey, - Buffer.from(signature, 'base64url') - ) - - return isValid ? { ok: true } : { ok: false, error: 'Invalid request signature.' } - } catch { - return { ok: false, error: 'Invalid request signature.' } - } -} - -function parseSignatureHeader(header: RequestSignatureHeader): ParsedSignatureHeader | null { - if (!header) { - return null - } - - const values = header.split(',').reduce>((result, item) => { - const [key, ...valueParts] = item.trim().split('=') - if (key && valueParts.length > 0) { - result[key] = valueParts.join('=') - } - return result - }, {}) - - if (!values.t || !values.v1 || !/^\d+$/.test(values.t)) { - return null - } - - return { - timestamp: values.t, - signature: values.v1, - } -} - -function parsePublicKey(publicAPIKey: string): KeyObject | null { - const trimmedKey = publicAPIKey.trim() - - try { - if (trimmedKey.includes('BEGIN PUBLIC KEY')) { - return createPublicKey(trimmedKey) - } - - return createPublicKey({ - key: Buffer.from(trimmedKey, 'base64'), - format: 'der', - type: 'spki', - }) - } catch { - return null - } -} From b903a43bbabf7b80f50764437f85700d689ee31a Mon Sep 17 00:00:00 2001 From: jiasheng Date: Thu, 18 Jun 2026 22:06:49 +0800 Subject: [PATCH 8/8] fix: update test script to target specific test files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c166813..d8b38b5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "clean": "rimraf dist", "build": "pnpm clean && tsc && copyfiles -F \"bin/*\" dist && copyfiles ./README.md ./LICENSE ./package.json dist && pnpm pack dist --pack-destination ../build", - "test": "node --require tsx/cjs --test test/**/*.test.ts src/**/*.test.ts" + "test": "node --require tsx/cjs --test test/*.test.ts " }, "keywords": [ "zenstack",