From af1e0dc9c5195e737906714c1695b68e7f380ca4 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 17:01:44 -0700 Subject: [PATCH 01/12] examples/chat: scaffold + SqlStorage probe (Phase 0 of the DO chat demo) A Cloudflare Durable Object chat app whose data layer is typegres running inside the DO's SQLite, driven end-to-end over Cap'n Web (see plan). Phase 0 scaffolds the worker/DO + vitest-pool-workers (workerd runs locally here) and pins the SqlStorage binding/result contract as a living artifact. Key findings that shape the coming DoSqliteDriver: - numbers bind as REAL (7/2 = 3.5), same as better-sqlite3 -> integer affinity comes from the dialect's CAST, which works in workerd too; - SqlStorage REJECTS BigInt (opposite of better-sqlite3) -> the driver must map typegres's boolean 0n/1n to plain 0/1; - blobs bind from Uint8Array, return as ArrayBuffer. Co-Authored-By: Claude Fable 5 --- examples/chat/package-lock.json | 2691 +++++++++++++++++++ examples/chat/package.json | 20 + examples/chat/src/index.ts | 24 + examples/chat/test/env.d.ts | 6 + examples/chat/test/sqlstorage.probe.test.ts | 86 + examples/chat/tsconfig.json | 15 + examples/chat/vitest.config.ts | 13 + examples/chat/wrangler.jsonc | 12 + 8 files changed, 2867 insertions(+) create mode 100644 examples/chat/package-lock.json create mode 100644 examples/chat/package.json create mode 100644 examples/chat/src/index.ts create mode 100644 examples/chat/test/env.d.ts create mode 100644 examples/chat/test/sqlstorage.probe.test.ts create mode 100644 examples/chat/tsconfig.json create mode 100644 examples/chat/vitest.config.ts create mode 100644 examples/chat/wrangler.jsonc diff --git a/examples/chat/package-lock.json b/examples/chat/package-lock.json new file mode 100644 index 00000000..02ddfc40 --- /dev/null +++ b/examples/chat/package-lock.json @@ -0,0 +1,2691 @@ +{ + "name": "typegres-chat", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typegres-chat", + "version": "0.0.0", + "dependencies": { + "capnweb": "file:../../packages/capnweb", + "typegres": "file:../.." + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.12.21", + "@cloudflare/workers-types": "^4.20260310.1", + "typescript": "^6.0.3", + "vitest": "~3.2.4" + } + }, + "../..": { + "version": "0.2.0", + "dependencies": { + "camelcase": "^9.0.0", + "capnweb": "file:packages/capnweb" + }, + "bin": { + "tg": "dist/cli.mjs" + }, + "devDependencies": { + "@electric-sql/pglite": "^0.4.4", + "@eslint/js": "^10.0.1", + "@standard-schema/spec": "^1.1.0", + "@swc/core": "^1.15.32", + "@types/acorn": "^4.0.6", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^25.6.0", + "@types/pg": "^8.20.0", + "@typescript-eslint/eslint-plugin": "^8.59.0", + "@typescript-eslint/parser": "^8.59.0", + "@typescript/native-preview": "^7.0.0-dev.20260422.1", + "acorn": "^8.16.0", + "better-sqlite3": "^12.11.1", + "eslint": "^10.2.1", + "fast-check": "^4.9.0", + "pg": "^8.20.0", + "prettier": "^3.8.3", + "secure-json-parse": "^4.1.0", + "tsdown": "^0.21.10", + "typescript": "^6.0.3", + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.5", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@electric-sql/pglite": "^0.4.4", + "pg": "^8.20.0" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "pg": { + "optional": true + } + } + }, + "../../packages/capnweb": { + "version": "0.6.1", + "license": "MIT", + "devDependencies": { + "@changesets/changelog-github": "^0.5.2", + "@changesets/cli": "^2.29.8", + "@cloudflare/vitest-pool-workers": "^0.12.10", + "@cloudflare/workers-types": "^4.20260205.0", + "@types/ws": "^8.18.1", + "@vitest/browser": "^3.2.4", + "pkg-pr-new": "^0.0.60", + "playwright": "^1.56.1", + "tsup": "^8.5.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^3.2.4", + "ws": "^8.19.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.15.0.tgz", + "integrity": "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vitest-pool-workers": { + "version": "0.12.21", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.12.21.tgz", + "integrity": "sha512-xqvqVR+qAhekXWaTNY36UtFFmHrz13yGUoWVGOu6LDC2ABiQqI1E1lQ3eUZY8KVB+1FXY/mP5dB6oD07XUGnPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cjs-module-lexer": "^1.2.3", + "esbuild": "0.27.3", + "miniflare": "4.20260310.0", + "wrangler": "4.72.0" + }, + "peerDependencies": { + "@vitest/runner": "2.0.x - 3.2.x", + "@vitest/snapshot": "2.0.x - 3.2.x", + "vitest": "2.0.x - 3.2.x" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260310.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260310.1.tgz", + "integrity": "sha512-hF2VpoWaMb1fiGCQJqCY6M8I+2QQqjkyY4LiDYdTL5D/w6C1l5v1zhc0/jrjdD1DXfpJtpcSMSmEPjHse4p9Ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260310.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260310.1.tgz", + "integrity": "sha512-h/Vl3XrYYPI6yFDE27XO1QPq/1G1lKIM8tzZGIWYpntK3IN5XtH3Ee/sLaegpJ49aIJoqhF2mVAZ6Yw+Vk2gJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260310.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260310.1.tgz", + "integrity": "sha512-XzQ0GZ8G5P4d74bQYOIP2Su4CLdNPpYidrInaSOuSxMw+HamsHaFrjVsrV2mPy/yk2hi6SY2yMbgKFK9YjA7vw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260310.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260310.1.tgz", + "integrity": "sha512-sxv4CxnN4ZR0uQGTFVGa0V4KTqwdej/czpIc5tYS86G8FQQoGIBiAIs2VvU7b8EROPcandxYHDBPTb+D9HIMPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260310.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260310.1.tgz", + "integrity": "sha512-+1ZTViWKJypLfgH/luAHCqkent0DEBjAjvO40iAhOMHRLYP/SPphLvr4Jpi6lb+sIocS8Q1QZL4uM5Etg1Wskg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260702.1.tgz", + "integrity": "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/capnweb": { + "resolved": "../../packages/capnweb", + "link": true + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/miniflare": { + "version": "4.20260310.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260310.0.tgz", + "integrity": "sha512-uC5vNPenFpDSj5aUU3wGSABG6UUqMr+Xs1m4AkCrTHo37F4Z6xcQw5BXqViTfPDVT/zcYH1UgTVoXhr1l6ZMXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260310.1", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typegres": { + "resolved": "../..", + "link": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "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 + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "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 + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20260310.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260310.1.tgz", + "integrity": "sha512-yawXhypXXHtArikJj15HOMknNGikpBbSg2ZDe6lddUbqZnJXuCVSkgc/0ArUeVMG1jbbGvpst+REFtKwILvRTQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260310.1", + "@cloudflare/workerd-darwin-arm64": "1.20260310.1", + "@cloudflare/workerd-linux-64": "1.20260310.1", + "@cloudflare/workerd-linux-arm64": "1.20260310.1", + "@cloudflare/workerd-windows-64": "1.20260310.1" + } + }, + "node_modules/wrangler": { + "version": "4.72.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.72.0.tgz", + "integrity": "sha512-bKkb8150JGzJZJWiNB2nu/33smVfawmfYiecA6rW4XH7xS23/jqMbgpdelM34W/7a1IhR66qeQGVqTRXROtAZg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.15.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260310.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260310.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260310.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/examples/chat/package.json b/examples/chat/package.json new file mode 100644 index 00000000..efbbf134 --- /dev/null +++ b/examples/chat/package.json @@ -0,0 +1,20 @@ +{ + "name": "typegres-chat", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "capnweb": "file:../../packages/capnweb", + "typegres": "file:../.." + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.12.21", + "@cloudflare/workers-types": "^4.20260310.1", + "typescript": "^6.0.3", + "vitest": "~3.2.4" + } +} diff --git a/examples/chat/src/index.ts b/examples/chat/src/index.ts new file mode 100644 index 00000000..9fac8d2c --- /dev/null +++ b/examples/chat/src/index.ts @@ -0,0 +1,24 @@ +import { DurableObject } from "cloudflare:workers"; + +export interface Env { + CHAT: DurableObjectNamespace; +} + +// The single chat Durable Object. Phase 0: just a SQLite-backed DO whose +// storage the probe tests exercise directly. Later phases add the typegres +// schema, the @expose capability graph, and the Cap'n Web WebSocket server. +export class ChatDo extends DurableObject { + readonly sql: SqlStorage; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.sql = ctx.storage.sql; + } +} + +export default { + async fetch(_req: Request, _env: Env): Promise { + // Phase 4 replaces this with a WebSocket upgrade handed to Cap'n Web. + return new Response("typegres chat", { status: 200 }); + }, +}; diff --git a/examples/chat/test/env.d.ts b/examples/chat/test/env.d.ts new file mode 100644 index 00000000..db77a029 --- /dev/null +++ b/examples/chat/test/env.d.ts @@ -0,0 +1,6 @@ +import type { Env } from "../src/index"; + +// Types the `env` exported from cloudflare:test with our worker's bindings. +declare module "cloudflare:test" { + interface ProvidedEnv extends Env {} +} diff --git a/examples/chat/test/sqlstorage.probe.test.ts b/examples/chat/test/sqlstorage.probe.test.ts new file mode 100644 index 00000000..6a259da3 --- /dev/null +++ b/examples/chat/test/sqlstorage.probe.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from "vitest"; +import { env, runInDurableObject } from "cloudflare:test"; +import type { ChatDo } from "../src/index"; + +// Phase 0 -- the SqlStorage contract, as a living artifact. +// +// The DoSqliteDriver maps typegres's compiled { text, ?-values } onto +// Cloudflare's SqlStorage. These tests pin the binding/result semantics that +// shape the driver, contrasted with the better-sqlite3 SqliteDriver. +// +// HEADLINE (proven below): +// - Numbers bind as REAL (7/2 = 3.5), SAME as better-sqlite3 -- so the sqlite +// dialect's CAST(? AS INTEGER/REAL) is what makes typed positions correct, +// for both drivers. +// - SqlStorage REJECTS BigInt, the OPPOSITE of better-sqlite3 (which requires +// it for INTEGER). typegres's compile step turns booleans into 0n/1n +// (BigInt), so the DO driver must map BigInt -> Number before exec(). +// - Blobs bind from Uint8Array and come back as ArrayBuffer. + +let n = 0; +const withSql = async (fn: (sql: SqlStorage) => T | Promise): Promise => { + const stub = env.CHAT.get(env.CHAT.idFromName(`probe-${n++}`)); + return runInDurableObject(stub, (_instance: ChatDo, state) => fn(state.storage.sql)); +}; +const one = (sql: SqlStorage, query: string, ...binds: unknown[]): Record => + sql.exec(query, ...binds).toArray()[0] as Record; + +describe("SqlStorage binding semantics", () => { + it("binds every JS number as REAL (like better-sqlite3)", async () => { + const k = await withSql((sql) => ({ + int: one(sql, "SELECT typeof(?) t", 7).t, + frac: one(sql, "SELECT typeof(?) t", 2.5).t, + str: one(sql, "SELECT typeof(?) t", "hi").t, + nul: one(sql, "SELECT typeof(?) t", null).t, + blob: one(sql, "SELECT typeof(?) t", new Uint8Array([1, 2, 3])).t, + })); + expect(k).toEqual({ int: "real", frac: "real", str: "text", nul: "null", blob: "blob" }); + }); + + it("number division is REAL: 7/2 = 3.5 (so integer affinity must come from CAST)", async () => { + const v = await withSql((sql) => one(sql, "SELECT ? / ? AS v", 7, 2).v); + expect(v).toBe(3.5); + }); + + it("CAST(? AS INTEGER) forces integer affinity -- the dialect's mechanism works here too", async () => { + const v = await withSql((sql) => one(sql, "SELECT CAST(? AS INTEGER) / CAST(? AS INTEGER) AS v", 7, 2).v); + expect(v).toBe(3); + }); + + it("REJECTS BigInt bindings (the driver must down-convert typegres's 0n/1n booleans)", async () => { + await expect(withSql((sql) => one(sql, "SELECT typeof(?) t", 7n))).rejects.toThrow(/BigInt/); + }); + + it("plain 0/1 numbers bind fine (what the driver sends instead of 0n/1n)", async () => { + const v = await withSql((sql) => one(sql, "SELECT CAST(? AS INTEGER) AS v", 1).v); + expect(v).toBe(1); + }); +}); + +describe("SqlStorage result value types", () => { + it("returns native JS types per storage class (driver must stringify for deserialize)", async () => { + const row = await withSql((sql) => { + sql.exec("CREATE TABLE t (i INTEGER, r REAL, s TEXT, b BLOB, n INTEGER)"); + sql.exec("INSERT INTO t VALUES (CAST(? AS INTEGER), ?, ?, ?, ?)", 42, 2.5, "hey", new Uint8Array([9, 8]), null); + return one(sql, "SELECT i, r, s, b, n FROM t"); + }); + expect(typeof row.i).toBe("number"); + expect(row.i).toBe(42); + expect(row.r).toBe(2.5); + expect(row.s).toBe("hey"); + expect(row.n).toBeNull(); + // blobs return as ArrayBuffer here (better-sqlite3 gives Buffer/Uint8Array) + expect(row.b).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(row.b as ArrayBuffer)).toEqual(new Uint8Array([9, 8])); + }); + + it("INTEGER results are JS number, not bigint (fine for chat-scale ids)", async () => { + const v = await withSql((sql) => { + sql.exec("CREATE TABLE t (v INTEGER)"); + sql.exec("INSERT INTO t VALUES (CAST(? AS INTEGER))", 1000); + return one(sql, "SELECT v FROM t").v; + }); + expect(typeof v).toBe("number"); + expect(v).toBe(1000); + }); +}); diff --git a/examples/chat/tsconfig.json b/examples/chat/tsconfig.json new file mode 100644 index 00000000..d248f0fe --- /dev/null +++ b/examples/chat/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["esnext"], + "types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "experimentalDecorators": false + }, + "include": ["src", "test", "vitest.config.ts"] +} diff --git a/examples/chat/vitest.config.ts b/examples/chat/vitest.config.ts new file mode 100644 index 00000000..a582bdef --- /dev/null +++ b/examples/chat/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; + +// Runs tests inside real workerd (the same runtime the DO ships on), with +// the DO + SQLite storage wired up from wrangler.jsonc. +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.jsonc" }, + }, + }, + }, +}); diff --git a/examples/chat/wrangler.jsonc b/examples/chat/wrangler.jsonc new file mode 100644 index 00000000..a858475a --- /dev/null +++ b/examples/chat/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "name": "typegres-chat", + "main": "src/index.ts", + "compatibility_date": "2026-02-05", + // A single Durable Object holds all chat state; rooms are rows, not + // instances. new_sqlite_classes makes it SQLite-backed so ctx.storage.sql + // is available (typegres runs against it via DoSqliteDriver). + "durable_objects": { + "bindings": [{ "name": "CHAT", "class_name": "ChatDo" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatDo"] }] +} From 1912fcc96046dd1f267c8bb345b0765cc0cd6663 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 17:03:48 -0700 Subject: [PATCH 02/12] examples/chat: pin SqlStorage int64-precision limitation in the probe SqlStorage always marshals INTEGER results to a JS number (never bigint), so values above 2^53 come back lossy (9223372036854775807 -> ...776000) -- worse than better-sqlite3, which can return bigint. It stores int64 correctly, so CAST(col AS TEXT) reads the exact value. Chat ids are small, but the limitation is now on record with its workaround. Co-Authored-By: Claude Fable 5 --- examples/chat/test/sqlstorage.probe.test.ts | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/examples/chat/test/sqlstorage.probe.test.ts b/examples/chat/test/sqlstorage.probe.test.ts index 6a259da3..f6e28205 100644 --- a/examples/chat/test/sqlstorage.probe.test.ts +++ b/examples/chat/test/sqlstorage.probe.test.ts @@ -83,4 +83,33 @@ describe("SqlStorage result value types", () => { expect(typeof v).toBe("number"); expect(v).toBe(1000); }); + + // int64 precision: SqlStorage always marshals INTEGER results to a JS + // number, never bigint -- so values above 2^53 come back LOSSY (a real + // footgun, worse than better-sqlite3 which can return bigint). It stores + // int64 correctly, though: CAST(col AS TEXT) reads the exact value. So a + // full-precision bigint column would need the driver/dialect to select it + // as text. Chat ids are small autoincrement, so unaffected -- but pinned + // here so the limitation is on record. + it("int64 above 2^53 is LOSSY as a number result", async () => { + const v = await withSql((sql) => { + sql.exec("CREATE TABLE t (v INTEGER)"); + sql.exec("INSERT INTO t VALUES (9223372036854775807)"); // max int64, SQL literal + return one(sql, "SELECT v FROM t").v; + }); + expect(typeof v).toBe("number"); + // Compare as strings: the true value 9223372036854775807 can't even be + // written as a JS number literal (it rounds), so string form shows the loss. + expect(String(v)).toBe("9223372036854776000"); // rounded to the nearest double + expect(String(v)).not.toBe("9223372036854775807"); // the true int64 is lost + }); + + it("CAST(col AS TEXT) preserves int64 precision losslessly (the workaround)", async () => { + const v = await withSql((sql) => { + sql.exec("CREATE TABLE t (v INTEGER)"); + sql.exec("INSERT INTO t VALUES (9223372036854775807)"); + return one(sql, "SELECT CAST(v AS TEXT) AS v FROM t").v; + }); + expect(v).toBe("9223372036854775807"); + }); }); From 30f8433239d22fe9bb3b78728968e6be641f02f6 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 17:08:28 -0700 Subject: [PATCH 03/12] =?UTF-8?q?examples/chat:=20DoSqliteDriver=20?= =?UTF-8?q?=E2=80=94=20typegres=20over=20a=20DO's=20SQLite=20(Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A typegres Driver over Cloudflare's SqlStorage. Compiles typegres queries to {text, ?-values}, down-converts BigInt→Number (SqlStorage rejects BigInt), runs ctx.storage.sql.exec, and normalizes native results (number/string/ ArrayBuffer/null) to the string form deserialize expects (blobs as \x-hex). Single-connection; no-op close. Built/tested in the example; promote to core once proven. Test: a real typegres select (.upper()/.orderBy()) runs against the DO SQLite inside workerd and deserializes correctly. Co-Authored-By: Claude Fable 5 --- examples/chat/src/do-sqlite-driver.ts | 80 +++++++++++++++++++++++++++ examples/chat/test/do-driver.test.ts | 38 +++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 examples/chat/src/do-sqlite-driver.ts create mode 100644 examples/chat/test/do-driver.test.ts diff --git a/examples/chat/src/do-sqlite-driver.ts b/examples/chat/src/do-sqlite-driver.ts new file mode 100644 index 00000000..99c8f3f0 --- /dev/null +++ b/examples/chat/src/do-sqlite-driver.ts @@ -0,0 +1,80 @@ +import type { Driver } from "typegres"; + +// A typegres Driver over Cloudflare's SqlStorage (a Durable Object's SQLite). +// Built and tested here in the example; promote to typegres core once proven. +// +// The contract is nailed down by test/sqlstorage.probe.test.ts. In short: +// - SqlStorage binds numbers as REAL (integer affinity comes from the +// sqlite dialect's CAST, same as the better-sqlite3 driver); +// - it REJECTS BigInt, so typegres's boolean 0n/1n must be down-converted +// to plain 0/1 before exec(); +// - results are native JS values (number / string / ArrayBuffer / null), +// which we normalize to strings for typegres's deserialize layer. + +// Derive the driver's sub-types from the exported Driver interface, so the +// example needs no deep imports into typegres internals. +type CompiledSql = Parameters[0]; +type QueryResult = Awaited>; +type ExecuteFn = Driver["execute"]; + +// Duck-typed SqlStorage so this file needs no @cloudflare/workers-types. +export interface SqlStorageLike { + exec(query: string, ...bindings: unknown[]): { toArray(): Record[] }; +} + +// QueryBuilder.bind() wraps its output in `(...)` for subquery splicing; +// SQLite refuses to prepare a top-level parenthesized statement, so unwrap a +// single *matched* outer pair (mirrors the better-sqlite3 driver). +const stripMatchedOuterParens = (s: string): string => { + const t = s.trim(); + if (!t.startsWith("(") || !t.endsWith(")")) return s; + let depth = 0; + for (let i = 0; i < t.length; i++) { + if (t[i] === "(") depth++; + else if (t[i] === ")") { + depth--; + if (depth === 0 && i !== t.length - 1) return s; + } + } + return t.slice(1, -1); +}; + +const toHex = (bytes: Uint8Array): string => { + let s = "\\x"; + for (const b of bytes) s += b.toString(16).padStart(2, "0"); + return s; +}; + +// Native SqlStorage value -> the string form typegres's deserialize expects +// (PG text-protocol contract; blobs as \x-hex, matching the sqlite driver). +const normalizeValue = (v: unknown): string | null => { + if (v === null || v === undefined) return null; + if (typeof v === "string") return v; + if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") return String(v); + if (v instanceof ArrayBuffer) return toHex(new Uint8Array(v)); + if (v instanceof Uint8Array) return toHex(v); + return String(v); +}; + +const normalizeRow = (row: Record): Record => + Object.fromEntries(Object.entries(row).map(([k, v]) => [k, normalizeValue(v)])); + +export class DoSqliteDriver implements Driver { + readonly dialect = "sqlite" as const; + + constructor(private readonly sql: SqlStorageLike) {} + + execute: ExecuteFn = ({ text, values }: CompiledSql): Promise => { + const query = stripMatchedOuterParens(text); + // SqlStorage rejects BigInt; typegres emits 0n/1n for booleans. Down-convert. + const bound = values.map((v) => (typeof v === "bigint" ? Number(v) : v)); + const rows = this.sql.exec(query, ...bound).toArray().map(normalizeRow); + return Promise.resolve({ rows }); + }; + + // A Durable Object is single-threaded and single-connection. + runInSingleConnection = (cb: (execute: ExecuteFn) => Promise): Promise => cb(this.execute); + + // Storage lifecycle belongs to the DO; nothing to close. + close = (): Promise => Promise.resolve(); +} diff --git a/examples/chat/test/do-driver.test.ts b/examples/chat/test/do-driver.test.ts new file mode 100644 index 00000000..0356dc13 --- /dev/null +++ b/examples/chat/test/do-driver.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { env, runInDurableObject } from "cloudflare:test"; +import { Database, sql } from "typegres"; +import { Integer, Text } from "typegres/sqlite"; +import type { ChatDo } from "../src/index"; +import { DoSqliteDriver } from "../src/do-sqlite-driver"; + +// Phase 1 -- typegres queries run against the Durable Object's SQLite through +// the DoSqliteDriver, inside real workerd. + +describe("DoSqliteDriver", () => { + it("runs a typegres-built query against the DO's SQLite", async () => { + const stub = env.CHAT.get(env.CHAT.idFromName("phase1")); + const rows = await runInDurableObject(stub, async (_instance: ChatDo, state) => { + const db = new Database({ dialect: "sqlite" }); + const conn = db.attach(new DoSqliteDriver(state.storage.sql)); + + await conn.execute(sql`CREATE TABLE dogs (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`); + await conn.execute(sql`INSERT INTO dogs (name) VALUES ('Rex'), ('Fido')`); + + class Dogs extends db.Table("dogs") { + id = Integer.column({ nonNull: true, generated: true }); + name = Text.column({ nonNull: true }); + } + + return conn.execute( + Dogs.from() + .select(({ dogs }) => ({ id: dogs.id, name: dogs.name.upper() })) + .orderBy(({ dogs }) => dogs.id), + ); + }); + + expect(rows).toEqual([ + { id: 1, name: "REX" }, + { id: 2, name: "FIDO" }, + ]); + }); +}); From 8522a9478190824b9ce83fa3a5e2ecec93d7769c Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 17:12:47 -0700 Subject: [PATCH 04/12] examples/chat: chat schema + DO migration (Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 4-table schema (users, rooms, room_members, messages) as module-level typegres tables bound to a driverless Database; each DO attaches its own DoSqliteDriver. The DO runs the migration in blockConcurrencyWhile on init. Relation methods (room.messages(), message.author(), ...) use the codegen's scope propagation. Test: inserts across all four tables + a messages⋈users join run through the DO's SQLite in workerd. Co-Authored-By: Claude Fable 5 --- examples/chat/src/index.ts | 15 ++++--- examples/chat/src/schema.ts | 73 +++++++++++++++++++++++++++++++ examples/chat/test/schema.test.ts | 38 ++++++++++++++++ 3 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 examples/chat/src/schema.ts create mode 100644 examples/chat/test/schema.test.ts diff --git a/examples/chat/src/index.ts b/examples/chat/src/index.ts index 9fac8d2c..3946f68c 100644 --- a/examples/chat/src/index.ts +++ b/examples/chat/src/index.ts @@ -1,18 +1,23 @@ import { DurableObject } from "cloudflare:workers"; +import type { Connection } from "typegres"; +import { db, migrate } from "./schema"; +import { DoSqliteDriver, type SqlStorageLike } from "./do-sqlite-driver"; export interface Env { CHAT: DurableObjectNamespace; } -// The single chat Durable Object. Phase 0: just a SQLite-backed DO whose -// storage the probe tests exercise directly. Later phases add the typegres -// schema, the @expose capability graph, and the Cap'n Web WebSocket server. +// The single chat Durable Object. Holds one typegres Connection over its own +// SQLite storage and runs the schema migration on init. Later phases add the +// @expose capability graph and the Cap'n Web WebSocket server. export class ChatDo extends DurableObject { - readonly sql: SqlStorage; + readonly conn: Connection; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); - this.sql = ctx.storage.sql; + this.conn = db.attach(new DoSqliteDriver(ctx.storage.sql as SqlStorageLike)); + // blockConcurrencyWhile: no request is served until the schema exists. + ctx.blockConcurrencyWhile(() => migrate(this.conn)); } } diff --git a/examples/chat/src/schema.ts b/examples/chat/src/schema.ts new file mode 100644 index 00000000..8e49f613 --- /dev/null +++ b/examples/chat/src/schema.ts @@ -0,0 +1,73 @@ +import { Database, sql, type Connection } from "typegres"; +import { Integer, Text } from "typegres/sqlite"; + +// The chat schema. `db` is dialect + table definitions only (no driver) — each +// Durable Object attaches its own DoSqliteDriver to get a Connection over its +// own storage. Table classes are module-level (bound to this `db`); relations +// use the same scope-propagation the codegen emits. +export const db = new Database({ dialect: "sqlite" }); + +export class Users extends db.Table("users") { + id = Integer.column({ nonNull: true, generated: true }); + name = Text.column({ nonNull: true }); +} + +export class Rooms extends db.Table("rooms") { + id = Integer.column({ nonNull: true, generated: true }); + name = Text.column({ nonNull: true }); + created_by = Integer.column({ nonNull: true }); + + creator() { + return Users.scope(Rooms.contextOf(this)).where(({ users }) => users.id.eq(this.created_by)).cardinality("one"); + } + messages() { + return Messages.scope(Rooms.contextOf(this)).where(({ messages }) => messages.room_id.eq(this.id)).cardinality("many"); + } +} + +export class RoomMembers extends db.Table("room_members") { + room_id = Integer.column({ nonNull: true }); + user_id = Integer.column({ nonNull: true }); +} + +export class Messages extends db.Table("messages") { + id = Integer.column({ nonNull: true, generated: true }); + room_id = Integer.column({ nonNull: true }); + user_id = Integer.column({ nonNull: true }); + body = Text.column({ nonNull: true }); + // DB-defaulted (see the DDL below) → optional on insert. + created_at = Text.column({ nonNull: true, default: sql`(datetime('now'))` }); + + author() { + return Users.scope(Messages.contextOf(this)).where(({ users }) => users.id.eq(this.user_id)).cardinality("one"); + } + room() { + return Rooms.scope(Messages.contextOf(this)).where(({ rooms }) => rooms.id.eq(this.room_id)).cardinality("one"); + } +} + +// Idempotent schema creation. SqlStorage.exec runs one statement, so each +// CREATE TABLE is issued separately. Run once per DO in its init. +export const migrate = async (conn: Connection): Promise => { + await conn.execute(sql`CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )`); + await conn.execute(sql`CREATE TABLE IF NOT EXISTS rooms ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + created_by INTEGER NOT NULL REFERENCES users(id) + )`); + await conn.execute(sql`CREATE TABLE IF NOT EXISTS room_members ( + room_id INTEGER NOT NULL REFERENCES rooms(id), + user_id INTEGER NOT NULL REFERENCES users(id), + PRIMARY KEY (room_id, user_id) + )`); + await conn.execute(sql`CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY, + room_id INTEGER NOT NULL REFERENCES rooms(id), + user_id INTEGER NOT NULL REFERENCES users(id), + body TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`); +}; diff --git a/examples/chat/test/schema.test.ts b/examples/chat/test/schema.test.ts new file mode 100644 index 00000000..93108966 --- /dev/null +++ b/examples/chat/test/schema.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { env, runInDurableObject } from "cloudflare:test"; +import type { ChatDo } from "../src/index"; +import { Users, Rooms, RoomMembers, Messages } from "../src/schema"; + +// Phase 2 -- the 4-table chat schema, migrated by the DO on init, exercised +// through typegres inserts + a multi-table join, all inside the DO's SQLite. + +describe("chat schema in the DO", () => { + it("inserts users/rooms/members/messages and joins messages to their authors", async () => { + const stub = env.CHAT.get(env.CHAT.idFromName("phase2")); + const rows = await runInDurableObject(stub, async (instance: ChatDo) => { + const conn = instance.conn; // migrated in the DO constructor + + const [alice] = await Users.insert({ name: "alice" }).returning(({ users }) => ({ id: users.id })).execute(conn); + const [bob] = await Users.insert({ name: "bob" }).returning(({ users }) => ({ id: users.id })).execute(conn); + const [room] = await Rooms.insert({ name: "general", created_by: alice!.id }).returning(({ rooms }) => ({ id: rooms.id })).execute(conn); + + await RoomMembers.insert({ room_id: room!.id, user_id: alice!.id }, { room_id: room!.id, user_id: bob!.id }).execute(conn); + await Messages.insert( + { room_id: room!.id, user_id: alice!.id, body: "hi" }, + { room_id: room!.id, user_id: bob!.id, body: "yo" }, + ).execute(conn); + + return Messages.from() + .join(Users, ({ messages, users }) => messages.user_id.eq(users.id)) + .where(({ messages }) => messages.room_id.eq(room!.id)) + .select(({ messages, users }) => ({ author: users.name, body: messages.body })) + .orderBy(({ messages }) => messages.id) + .execute(conn); + }); + + expect(rows).toEqual([ + { author: "alice", body: "hi" }, + { author: "bob", body: "yo" }, + ]); + }); +}); From b571a8876b9e62b86f657ee667fe11c1c4ccc59b Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 17:17:06 -0700 Subject: [PATCH 05/12] examples/chat: @expose capability graph (Phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User (the main cap) and Room, with @expose gating as the authorization story: reads return client-refinable query builders (createRoom/joinRoom/room(id) hand back Room caps; room(id) enforces membership at grant time), mutations (post) execute server-side. authenticate() is the auth entry. Test (in-process, no wire): createRoom + post + read, and a non-member is denied a Room cap until joinRoom — all against the DO's SQLite. Cap'n Web (Phase 4) gates the same graph over the wire. Co-Authored-By: Claude Fable 5 --- examples/chat/package-lock.json | 12 ++- examples/chat/package.json | 3 +- examples/chat/src/capabilities.ts | 119 ++++++++++++++++++++++++ examples/chat/test/capabilities.test.ts | 58 ++++++++++++ 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 examples/chat/src/capabilities.ts create mode 100644 examples/chat/test/capabilities.test.ts diff --git a/examples/chat/package-lock.json b/examples/chat/package-lock.json index 02ddfc40..bcff956e 100644 --- a/examples/chat/package-lock.json +++ b/examples/chat/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "capnweb": "file:../../packages/capnweb", - "typegres": "file:../.." + "typegres": "file:../..", + "zod": "^4.4.3" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.12.21", @@ -2686,6 +2687,15 @@ "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/examples/chat/package.json b/examples/chat/package.json index efbbf134..3c5b468e 100644 --- a/examples/chat/package.json +++ b/examples/chat/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "capnweb": "file:../../packages/capnweb", - "typegres": "file:../.." + "typegres": "file:../..", + "zod": "^4.4.3" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.12.21", diff --git a/examples/chat/src/capabilities.ts b/examples/chat/src/capabilities.ts new file mode 100644 index 00000000..feee0cd8 --- /dev/null +++ b/examples/chat/src/capabilities.ts @@ -0,0 +1,119 @@ +import { expose, type Connection } from "typegres"; +import z from "zod"; +import { Users, Rooms, RoomMembers, Messages } from "./schema"; + +// The @expose capability graph. @expose gating is the entire authorization +// story: only exposed members are reachable over Cap'n Web, and a capability +// is only handed out when the server decides you may have it. Reads return +// query builders the client refines and runs (the Cap'n Web showcase); +// mutations execute server-side. + +// A room the current user is authorized for — you only get one via User. +export class Room { + readonly #conn: Connection; + readonly #userId: number; + readonly #id: number; + readonly #name: string; + + constructor(conn: Connection, userId: number, id: number, name: string) { + this.#conn = conn; + this.#userId = userId; + this.#id = id; + this.#name = name; + } + + @expose() get id() { + return this.#id; + } + @expose() get name() { + return this.#name; + } + + // Client-refinable: messages in this room (authorized by holding the cap). + @expose() messages() { + return Messages.from().where(({ messages }) => messages.room_id.eq(this.#id)); + } + @expose() members() { + return Users.from() + .join(RoomMembers, ({ users, room_members }) => users.id.eq(room_members.user_id)) + .where(({ room_members }) => room_members.room_id.eq(this.#id)); + } + + // Mutation: post a message as the current user. + @expose(z.string().min(1)) + async post(body: string) { + const [msg] = await Messages.insert({ room_id: this.#id, user_id: this.#userId, body }) + .returning(({ messages }) => ({ id: messages.id, body: messages.body })) + .execute(this.#conn); + return msg; + } +} + +// The authenticated principal — the Cap'n Web main capability. +export class User { + readonly #conn: Connection; + readonly #id: number; + readonly #name: string; + + constructor(conn: Connection, id: number, name: string) { + this.#conn = conn; + this.#id = id; + this.#name = name; + } + + @expose() get id() { + return this.#id; + } + @expose() get name() { + return this.#name; + } + // Exposed so client-authored read builders can .execute() against it. + @expose() get conn() { + return this.#conn; + } + + // Client-refinable: the rooms I'm a member of. + @expose() rooms() { + return Rooms.from() + .join(RoomMembers, ({ rooms, room_members }) => rooms.id.eq(room_members.room_id)) + .where(({ room_members }) => room_members.user_id.eq(this.#id)); + } + + @expose(z.string().min(1)) + async createRoom(name: string): Promise { + const [room] = await Rooms.insert({ name, created_by: this.#id }) + .returning(({ rooms }) => ({ id: rooms.id })) + .execute(this.#conn); + await RoomMembers.insert({ room_id: room!.id, user_id: this.#id }).execute(this.#conn); + return new Room(this.#conn, this.#id, room!.id, name); + } + + @expose(z.number().int()) + async joinRoom(id: number): Promise { + const [room] = await Rooms.from().where(({ rooms }) => rooms.id.eq(id)).select(({ rooms }) => ({ name: rooms.name })).execute(this.#conn); + if (!room) throw new Error(`no such room ${id}`); + // INSERT OR IGNORE via the join-table PK; idempotent join. + await RoomMembers.insert({ room_id: id, user_id: this.#id }).execute(this.#conn); + return new Room(this.#conn, this.#id, id, room.name); + } + + // Authorization at grant time: only a member gets a Room cap. + @expose(z.number().int()) + async room(id: number): Promise { + const [membership] = await RoomMembers.from() + .where(({ room_members }) => room_members.room_id.eq(id).and(room_members.user_id.eq(this.#id))) + .select(({ room_members }) => ({ room_id: room_members.room_id })) + .execute(this.#conn); + if (!membership) throw new Error(`not a member of room ${id}`); + const [room] = await Rooms.from().where(({ rooms }) => rooms.id.eq(id)).select(({ rooms }) => ({ name: rooms.name })).execute(this.#conn); + return new Room(this.#conn, this.#id, id, room!.name); + } +} + +// Auth entry: resolve or create a user by name, return their capability. +// (A real app validates a token; PoC keys off the name.) +export const authenticate = async (conn: Connection, name: string): Promise => { + const [existing] = await Users.from().where(({ users }) => users.name.eq(name)).select(({ users }) => ({ id: users.id })).execute(conn); + const id = existing?.id ?? (await Users.insert({ name }).returning(({ users }) => ({ id: users.id })).execute(conn))[0]!.id; + return new User(conn, id, name); +}; diff --git a/examples/chat/test/capabilities.test.ts b/examples/chat/test/capabilities.test.ts new file mode 100644 index 00000000..fd7376d4 --- /dev/null +++ b/examples/chat/test/capabilities.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import { env, runInDurableObject } from "cloudflare:test"; +import type { ChatDo } from "../src/index"; +import { authenticate } from "../src/capabilities"; + +// Phase 3 -- the capability graph, exercised in-process (no wire yet). Proves +// the authorization logic: you only reach a Room through User, and only as a +// member. Cap'n Web (Phase 4) then gates the SAME graph over the wire. + +describe("capability graph (in-process)", () => { + it("createRoom + post + read; a non-member is denied a room cap until they join", async () => { + const stub = env.CHAT.get(env.CHAT.idFromName("phase3")); + const result = await runInDurableObject(stub, async (i: ChatDo) => { + const conn = i.conn; + const alice = await authenticate(conn, "alice"); + const bob = await authenticate(conn, "bob"); + + const room = await alice.createRoom("general"); + await room.post("hi from alice"); + await room.post("still alice"); + + const aliceView = await room + .messages() + .select(({ messages }) => ({ body: messages.body })) + .orderBy(({ messages }) => messages.id) + .execute(conn); + const aliceRooms = await alice.rooms().select(({ rooms }) => ({ name: rooms.name })).execute(conn); + + // bob is not a member -> room(id) rejects (authorization at grant time) + let bobDenied = false; + try { + await bob.room(room.id); + } catch { + bobDenied = true; + } + + // bob joins, then can post + read + const bobRoom = await bob.joinRoom(room.id); + await bobRoom.post("hi from bob"); + const bobView = await bobRoom + .messages() + .select(({ messages }) => ({ body: messages.body })) + .orderBy(({ messages }) => messages.id) + .execute(conn); + + return { aliceView, aliceRooms, bobDenied, bobView }; + }); + + expect(result.aliceView).toEqual([{ body: "hi from alice" }, { body: "still alice" }]); + expect(result.aliceRooms).toEqual([{ name: "general" }]); + expect(result.bobDenied).toBe(true); + expect(result.bobView).toEqual([ + { body: "hi from alice" }, + { body: "still alice" }, + { body: "hi from bob" }, + ]); + }); +}); From b3823d2e00c1c75c1afab64b9a5bb37bf61b206e Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 17:27:15 -0700 Subject: [PATCH 06/12] =?UTF-8?q?examples/chat:=20Cap'n=20Web=20over=20Web?= =?UTF-8?q?Socket=20=E2=80=94=20the=20end-to-end=20showcase=20(Phase=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DO serves its @expose capability graph to clients over a real WebSocket via Cap'n Web (newWorkersWebSocketRpcResponse + toRpc(user)); clients author typegres queries as record-replay closures that run server-side against the DO's SQLite. Auth is a ?user= token → the authenticated User is the main cap. typegres now exports the shim at `typegres/capnweb` (toRpc/fromRpc/doRpc/ ShimStub); tsdown builds it with swc (decorators) and capnweb external. Tests (real workerd, WebSocket transport): - client authors createRoom + a .rooms().select(...).execute(conn) query, replayed in the DO; - @expose gating holds over the wire: a non-member's room(id) rejects. Notes: schema columns/relations are @expose'd so client-authored selects can read them; isolated storage is off (a live WS keeps the DO's SQLite open), so each test uses its own DO id. Co-Authored-By: Claude Fable 5 --- examples/chat/src/index.ts | 24 ++++++++--- examples/chat/src/schema.ts | 34 ++++++++-------- examples/chat/test/capnweb.test.ts | 64 ++++++++++++++++++++++++++++++ examples/chat/vitest.config.ts | 4 ++ package.json | 6 ++- tsdown.config.ts | 4 +- 6 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 examples/chat/test/capnweb.test.ts diff --git a/examples/chat/src/index.ts b/examples/chat/src/index.ts index 3946f68c..5979412b 100644 --- a/examples/chat/src/index.ts +++ b/examples/chat/src/index.ts @@ -1,6 +1,9 @@ import { DurableObject } from "cloudflare:workers"; +import { newWorkersWebSocketRpcResponse } from "capnweb"; +import { toRpc } from "typegres/capnweb"; import type { Connection } from "typegres"; import { db, migrate } from "./schema"; +import { authenticate } from "./capabilities"; import { DoSqliteDriver, type SqlStorageLike } from "./do-sqlite-driver"; export interface Env { @@ -8,22 +11,31 @@ export interface Env { } // The single chat Durable Object. Holds one typegres Connection over its own -// SQLite storage and runs the schema migration on init. Later phases add the -// @expose capability graph and the Cap'n Web WebSocket server. +// SQLite storage, and serves the @expose capability graph to clients over +// Cap'n Web on a WebSocket -- clients author typegres queries that replay here. export class ChatDo extends DurableObject { readonly conn: Connection; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.conn = db.attach(new DoSqliteDriver(ctx.storage.sql as SqlStorageLike)); - // blockConcurrencyWhile: no request is served until the schema exists. ctx.blockConcurrencyWhile(() => migrate(this.conn)); } + + // WebSocket upgrade -> Cap'n Web session whose main capability is the + // authenticated User (the @expose shim gates the reachable surface). Auth is + // a ?user= token for the PoC; a real app validates a signed credential. + async fetch(req: Request): Promise { + const name = new URL(req.url).searchParams.get("user"); + if (!name) return new Response("missing ?user", { status: 400 }); + const user = await authenticate(this.conn, name); + return newWorkersWebSocketRpcResponse(req, toRpc(user)); + } } export default { - async fetch(_req: Request, _env: Env): Promise { - // Phase 4 replaces this with a WebSocket upgrade handed to Cap'n Web. - return new Response("typegres chat", { status: 200 }); + // One DO for all rooms; every client routes to it. + async fetch(req: Request, env: Env): Promise { + return env.CHAT.get(env.CHAT.idFromName("global")).fetch(req); }, }; diff --git a/examples/chat/src/schema.ts b/examples/chat/src/schema.ts index 8e49f613..e132c8a8 100644 --- a/examples/chat/src/schema.ts +++ b/examples/chat/src/schema.ts @@ -1,4 +1,4 @@ -import { Database, sql, type Connection } from "typegres"; +import { Database, expose, sql, type Connection } from "typegres"; import { Integer, Text } from "typegres/sqlite"; // The chat schema. `db` is dialect + table definitions only (no driver) — each @@ -8,40 +8,40 @@ import { Integer, Text } from "typegres/sqlite"; export const db = new Database({ dialect: "sqlite" }); export class Users extends db.Table("users") { - id = Integer.column({ nonNull: true, generated: true }); - name = Text.column({ nonNull: true }); + @expose() id = Integer.column({ nonNull: true, generated: true }); + @expose() name = Text.column({ nonNull: true }); } export class Rooms extends db.Table("rooms") { - id = Integer.column({ nonNull: true, generated: true }); - name = Text.column({ nonNull: true }); - created_by = Integer.column({ nonNull: true }); + @expose() id = Integer.column({ nonNull: true, generated: true }); + @expose() name = Text.column({ nonNull: true }); + @expose() created_by = Integer.column({ nonNull: true }); - creator() { + @expose() creator() { return Users.scope(Rooms.contextOf(this)).where(({ users }) => users.id.eq(this.created_by)).cardinality("one"); } - messages() { + @expose() messages() { return Messages.scope(Rooms.contextOf(this)).where(({ messages }) => messages.room_id.eq(this.id)).cardinality("many"); } } export class RoomMembers extends db.Table("room_members") { - room_id = Integer.column({ nonNull: true }); - user_id = Integer.column({ nonNull: true }); + @expose() room_id = Integer.column({ nonNull: true }); + @expose() user_id = Integer.column({ nonNull: true }); } export class Messages extends db.Table("messages") { - id = Integer.column({ nonNull: true, generated: true }); - room_id = Integer.column({ nonNull: true }); - user_id = Integer.column({ nonNull: true }); - body = Text.column({ nonNull: true }); + @expose() id = Integer.column({ nonNull: true, generated: true }); + @expose() room_id = Integer.column({ nonNull: true }); + @expose() user_id = Integer.column({ nonNull: true }); + @expose() body = Text.column({ nonNull: true }); // DB-defaulted (see the DDL below) → optional on insert. - created_at = Text.column({ nonNull: true, default: sql`(datetime('now'))` }); + @expose() created_at = Text.column({ nonNull: true, default: sql`(datetime('now'))` }); - author() { + @expose() author() { return Users.scope(Messages.contextOf(this)).where(({ users }) => users.id.eq(this.user_id)).cardinality("one"); } - room() { + @expose() room() { return Rooms.scope(Messages.contextOf(this)).where(({ rooms }) => rooms.id.eq(this.room_id)).cardinality("one"); } } diff --git a/examples/chat/test/capnweb.test.ts b/examples/chat/test/capnweb.test.ts new file mode 100644 index 00000000..87c48d3a --- /dev/null +++ b/examples/chat/test/capnweb.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +import { env } from "cloudflare:test"; +import { newWebSocketRpcSession } from "capnweb"; +import { doRpc, type ShimStub } from "typegres/capnweb"; +import type { User } from "../src/capabilities"; + +// Phase 4 -- the end-to-end showcase. A client connects to the DO over a real +// WebSocket, and authors typegres queries as Cap'n Web closures that replay +// server-side against the DO's SQLite, gated by the @expose capability graph. + +const connect = async (doId: string, name: string): Promise> => { + const resp = await env.CHAT.get(env.CHAT.idFromName(doId)).fetch( + `http://chat/?user=${name}`, + { headers: { Upgrade: "websocket" } }, + ); + const ws = resp.webSocket!; + ws.accept(); + return newWebSocketRpcSession(ws) as unknown as ShimStub; +}; + +describe("Cap'n Web over WebSocket", () => { + it("client authors typegres queries that replay in the DO", async () => { + using alice = await connect("room-demo", "alice"); + + // Hand out a Room capability (createRoom runs server-side in the DO). + using room = await doRpc(alice, (u) => u.createRoom("general")); + + // Post via the Room cap, then read back with a CLIENT-AUTHORED query: + // .rooms().select(...).execute(conn) is one closure, replayed in the DO. + const posted = await room.post("hello from alice"); + expect(posted?.body).toBe("hello from alice"); + + const rooms = await doRpc(alice, (u) => + u.rooms().select(({ rooms }) => ({ name: rooms.name })).execute(u.conn), + ); + expect(rooms).toEqual([{ name: "general" }]); + + const messages = await doRpc(alice, (u) => + u.rooms().select(({ rooms }) => ({ id: rooms.id })).execute(u.conn), + ).then(([r]) => + doRpc(alice, (u) => + u + .room(r!.id) + .messages() + .select(({ messages }) => ({ body: messages.body })) + .execute(u.conn), + ), + ); + expect(messages).toEqual([{ body: "hello from alice" }]); + }); + + it("@expose gates the surface over the wire: a non-member can't reach a room", async () => { + using alice = await connect("gating", "alice"); + using room = await doRpc(alice, (u) => u.createRoom("private")); + await room.post("secret"); + const [{ id: roomId }] = await doRpc(alice, (u) => + u.rooms().select(({ rooms }) => ({ id: rooms.id })).where(({ rooms }) => rooms.name.eq("private")).execute(u.conn), + ); + + using bob = await connect("gating", "bob"); + // bob is not a member -> room(id) rejects at the server, over the wire. + await expect(doRpc(bob, (u) => u.room(roomId))).rejects.toThrow(/not a member/); + }); +}); diff --git a/examples/chat/vitest.config.ts b/examples/chat/vitest.config.ts index a582bdef..b1e69dfb 100644 --- a/examples/chat/vitest.config.ts +++ b/examples/chat/vitest.config.ts @@ -6,6 +6,10 @@ export default defineWorkersConfig({ test: { poolOptions: { workers: { + // A long-lived Cap'n Web WebSocket keeps the DO's SQLite open, which + // the per-test isolated-storage reset can't snapshot. Disable it; each + // test uses its own DO id instead, so they stay independent. + isolatedStorage: false, wrangler: { configPath: "./wrangler.jsonc" }, }, }, diff --git a/package.json b/package.json index 989506b5..231bdf99 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,10 @@ "./exoeval": { "import": "./dist/exoeval/index.mjs", "types": "./dist/exoeval/index.d.mts" + }, + "./capnweb": { + "import": "./dist/capnweb/shim.mjs", + "types": "./dist/capnweb/shim.d.mts" } }, "bin": { @@ -54,7 +58,7 @@ "scripts": { "build": "tsdown", "codegen": "node --experimental-strip-types src/types/postgres/emit.ts && node --experimental-strip-types src/types/sqlite/emit.ts", - "codegen:check": "tmp=$(mktemp -d) && node --experimental-strip-types src/types/postgres/emit.ts --out-dir \"$tmp/pg\" && { diff -r \"$tmp/pg\" src/types/postgres/generated || { echo 'src/types/postgres/generated is stale — run `npm run codegen` and commit.' >&2; rm -rf \"$tmp\"; exit 1; }; } && node --experimental-strip-types src/types/sqlite/emit.ts --out-dir \"$tmp/sqlite\" && { diff -r \"$tmp/sqlite/generated\" src/types/sqlite/generated || { echo 'src/types/sqlite/generated is stale — run `npm run codegen` and commit.' >&2; rm -rf \"$tmp\"; exit 1; }; } && rm -rf \"$tmp\"", + "codegen:check": "tmp=$(mktemp -d) && node --experimental-strip-types src/types/postgres/emit.ts --out-dir \"$tmp/pg\" && { diff -r \"$tmp/pg\" src/types/postgres/generated || { echo 'src/types/postgres/generated is stale \u2014 run `npm run codegen` and commit.' >&2; rm -rf \"$tmp\"; exit 1; }; } && node --experimental-strip-types src/types/sqlite/emit.ts --out-dir \"$tmp/sqlite\" && { diff -r \"$tmp/sqlite/generated\" src/types/sqlite/generated || { echo 'src/types/sqlite/generated is stale \u2014 run `npm run codegen` and commit.' >&2; rm -rf \"$tmp\"; exit 1; }; } && rm -rf \"$tmp\"", "lint": "eslint src", "typecheck": "tsgo --noEmit", "format": "prettier --write src", diff --git a/tsdown.config.ts b/tsdown.config.ts index a45d0cd2..c0b91779 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -20,10 +20,10 @@ export default defineConfig([ // loaders — those use `require` / `__filename` and would trip Node's // CJS/ESM mixed-mode check when a consumer runs the CLI. { - entry: ["src/index.ts", "src/config.ts", "src/builder/sql.ts", "src/types/postgres/index.ts", "src/types/sqlite/index.ts", "src/cli.ts", "src/exoeval/index.ts"], + entry: ["src/index.ts", "src/config.ts", "src/builder/sql.ts", "src/types/postgres/index.ts", "src/types/sqlite/index.ts", "src/cli.ts", "src/exoeval/index.ts", "src/capnweb/shim.ts"], format: ["esm"], clean: true, - deps: { neverBundle: ["pg", "@electric-sql/pglite", "better-sqlite3"] }, + deps: { neverBundle: ["pg", "@electric-sql/pglite", "better-sqlite3", "capnweb"] }, plugins: [swcPlugin()], }, // Playground single-file bundle for the site's Monaco + esbuild-wasm From f54859e6acbb2f8148a2d41f892c9c3ffb870912 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 17:50:14 -0700 Subject: [PATCH 07/12] examples/chat: React UI + deployable CF app (Phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A React chat client (client/) over the Cap'n Web capability graph: login → create/join rooms → post + read messages, every call authoring a typegres query that runs in the DO. client/api.ts is the framework-agnostic doRpc glue. Made the example a real deployable Cloudflare app: - vite builds client/ → dist/client, served as static assets; - wrangler.jsonc gains an `assets` binding + the Worker routes /ws to the DO and everything else to ASSETS; - the node-only typegres drivers (pg/better-sqlite3/pglite) are aliased to an empty stub so their native deps stay out of the workerd bundle (the DO uses only DoSqliteDriver); - scripts: dev (vite watch + wrangler dev), build, deploy, preview. `wrangler deploy --dry-run` bundles clean; 14 tests still green; README added. Co-Authored-By: Claude Fable 5 --- examples/chat/README.md | 59 ++ examples/chat/client/App.tsx | 137 ++++ examples/chat/client/api.ts | 33 + examples/chat/client/index.html | 13 + examples/chat/client/main.tsx | 9 + examples/chat/client/styles.css | 54 ++ examples/chat/package-lock.json | 1009 +++++++++++++++++++++++++++- examples/chat/package.json | 16 +- examples/chat/src/capabilities.ts | 13 + examples/chat/src/empty.ts | 6 + examples/chat/src/index.ts | 9 +- examples/chat/test/capnweb.test.ts | 7 +- examples/chat/tsconfig.json | 5 +- examples/chat/vite.config.ts | 14 + examples/chat/wrangler.jsonc | 19 +- 15 files changed, 1384 insertions(+), 19 deletions(-) create mode 100644 examples/chat/README.md create mode 100644 examples/chat/client/App.tsx create mode 100644 examples/chat/client/api.ts create mode 100644 examples/chat/client/index.html create mode 100644 examples/chat/client/main.tsx create mode 100644 examples/chat/client/styles.css create mode 100644 examples/chat/src/empty.ts create mode 100644 examples/chat/vite.config.ts diff --git a/examples/chat/README.md b/examples/chat/README.md new file mode 100644 index 00000000..fd3c4b24 --- /dev/null +++ b/examples/chat/README.md @@ -0,0 +1,59 @@ +# typegres chat — Durable Object + Cap'n Web + +A chat app whose entire data layer is [typegres](../../) running **inside a +Cloudflare Durable Object's SQLite**. The browser authors typegres queries that +replay server-side over [Cap'n Web](https://github.com/cloudflare/capnweb) RPC — +there is no REST anywhere. + +``` +Browser (React) ──WebSocket / Cap'n Web──▶ Worker ──▶ Durable Object + authors queries as closures typegres → DoSqliteDriver + doRpc(user => user.rooms() → ctx.storage.sql + .select(...).execute(user.conn)) +``` + +- **`src/do-sqlite-driver.ts`** — a typegres `Driver` over Cloudflare's + `SqlStorage`. +- **`src/schema.ts`** — the tables (users, rooms, room_members, messages). +- **`src/capabilities.ts`** — the `@expose` capability graph (`User` → `Room`). + `@expose` gating is the authorization: you only reach a `Room` you're a member + of, enforced at grant time. A hostile client can't forge a capability. +- **`src/index.ts`** — the Worker + Durable Object; serves the cap graph over a + WebSocket and the React client as static assets. +- **`client/`** — the React UI (`api.ts` is the framework-agnostic Cap'n Web glue). + +## Develop + +```sh +npm install +npm run dev # vite (client, watched) + wrangler dev (worker + DO) +``` + +Open the printed `wrangler dev` URL, pick a name, create a room, chat. + +## Deploy + +```sh +npm run deploy # vite build + wrangler deploy +``` + +Requires a Cloudflare account (`wrangler login`). SQLite-backed Durable Objects +are available on the Workers paid plan. + +## Test + +```sh +npm test # vitest in real workerd (@cloudflare/vitest-pool-workers) +``` + +The tests run against a real Durable Object in local workerd — including the +`SqlStorage` contract, the driver, the capability graph, and the full Cap'n Web +round trip over a WebSocket. + +## Notes + +- `SqlStorage` rejects `BigInt` bindings and marshals `INTEGER` results to JS + `number` (lossy above 2^53) — see `test/sqlstorage.probe.test.ts`. Fine for + chat-scale ids; the driver down-converts typegres's boolean `0n/1n`. +- The node-only typegres drivers (pg / better-sqlite3 / pglite) are aliased to an + empty stub in `wrangler.jsonc` — the DO only uses `DoSqliteDriver`. diff --git a/examples/chat/client/App.tsx b/examples/chat/client/App.tsx new file mode 100644 index 00000000..7c7c2095 --- /dev/null +++ b/examples/chat/client/App.tsx @@ -0,0 +1,137 @@ +import { useEffect, useRef, useState } from "react"; +import { + connect, + createRoom, + feed, + joinRoom, + listRooms, + post, + type Client, + type Msg, + type RoomInfo, +} from "./api"; + +export function App() { + const [name, setName] = useState(""); + const [client, setClient] = useState(null); + + if (!client) { + return ( +
+

typegres chat

+

Queries you send run inside a Cloudflare Durable Object's SQLite, over Cap'n Web.

+
{ + e.preventDefault(); + if (name.trim()) setClient(connect(name.trim())); + }} + > + setName(e.target.value)} placeholder="your name" autoFocus /> + +
+
+ ); + } + return ; +} + +function Chat({ client, me }: { client: Client; me: string }) { + const [rooms, setRooms] = useState([]); + const [roomId, setRoomId] = useState(null); + const [messages, setMessages] = useState([]); + const [draft, setDraft] = useState(""); + const [newRoom, setNewRoom] = useState(""); + + const refreshRooms = () => listRooms(client).then(setRooms); + useEffect(() => { + void refreshRooms(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Poll the current room's feed (Phase 6 will push instead). + useEffect(() => { + if (roomId == null) return; + let alive = true; + const tick = () => feed(client, roomId).then((m) => alive && setMessages(m)).catch(() => {}); + void tick(); + const t = setInterval(tick, 1000); + return () => { + alive = false; + clearInterval(t); + }; + }, [client, roomId]); + + const open = async (id: number) => { + await joinRoom(client, id); // idempotent; makes `me` a member so the feed is authorized + setRoomId(id); + setMessages([]); + }; + + const send = async (e: React.FormEvent) => { + e.preventDefault(); + if (roomId == null || !draft.trim()) return; + await post(client, roomId, draft.trim()); + setDraft(""); + setMessages(await feed(client, roomId)); + }; + + const addRoom = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newRoom.trim()) return; + await createRoom(client, newRoom.trim()); + setNewRoom(""); + await refreshRooms(); + }; + + return ( +
+ + +
+ {roomId == null ? ( +
pick or create a room
+ ) : ( + <> + +
+ setDraft(e.target.value)} placeholder="message" autoFocus /> + +
+ + )} +
+
+ ); +} + +function Feed({ messages }: { messages: Msg[] }) { + const end = useRef(null); + useEffect(() => end.current?.scrollIntoView(), [messages]); + return ( +
+ {messages.map((m) => ( +
+ {m.author} + {m.body} +
+ ))} +
+
+ ); +} diff --git a/examples/chat/client/api.ts b/examples/chat/client/api.ts new file mode 100644 index 00000000..1ac033bc --- /dev/null +++ b/examples/chat/client/api.ts @@ -0,0 +1,33 @@ +import { newWebSocketRpcSession } from "capnweb"; +import { doRpc, type ShimStub } from "typegres/capnweb"; +import type { Room, User } from "../src/capabilities"; + +// The browser's view of the chat backend: a Cap'n Web session to the DO whose +// main capability is the authenticated User. Every call below authors a +// typegres query as a Cap'n Web closure that runs server-side in the DO. +export type Client = ShimStub; + +export const connect = (user: string): Client => { + const proto = location.protocol === "https:" ? "wss:" : "ws:"; + const url = `${proto}//${location.host}/ws?user=${encodeURIComponent(user)}`; + return newWebSocketRpcSession(url) as unknown as Client; +}; + +export type RoomInfo = { id: number; name: string }; +export type Msg = { id: number; author: string; body: string }; + +// Client-AUTHORED query: the browser writes the select, it runs in the DO. +export const listRooms = (c: Client): Promise => + doRpc(c, (u: User) => + u.rooms().select(({ rooms }) => ({ id: rooms.id, name: rooms.name })).orderBy(({ rooms }) => rooms.id).execute(u.conn), + ); + +export const createRoom = (c: Client, name: string): Promise => doRpc(c, (u: User) => u.createRoom(name)); + +export const joinRoom = (c: Client, id: number): Promise => doRpc(c, (u: User) => u.joinRoom(id)); + +// room(id) enforces membership at the server before feed()/post() run. +export const feed = (c: Client, roomId: number): Promise => doRpc(c, (u: User) => (u.room(roomId) as unknown as Room).feed()); + +export const post = (c: Client, roomId: number, body: string): Promise => + doRpc(c, (u: User) => (u.room(roomId) as unknown as Room).post(body)); diff --git a/examples/chat/client/index.html b/examples/chat/client/index.html new file mode 100644 index 00000000..adde34a7 --- /dev/null +++ b/examples/chat/client/index.html @@ -0,0 +1,13 @@ + + + + + + typegres chat + + + +
+ + + diff --git a/examples/chat/client/main.tsx b/examples/chat/client/main.tsx new file mode 100644 index 00000000..1c711a7b --- /dev/null +++ b/examples/chat/client/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/chat/client/styles.css b/examples/chat/client/styles.css new file mode 100644 index 00000000..dde7b73c --- /dev/null +++ b/examples/chat/client/styles.css @@ -0,0 +1,54 @@ +:root { + --bg: #0f1115; + --panel: #171a21; + --line: #262b36; + --text: #e6e8ee; + --muted: #8a91a0; + --accent: #6ea8fe; + font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; +} +* { box-sizing: border-box; } +body { margin: 0; background: var(--bg); color: var(--text); } +input, button { font: inherit; } +input { + background: var(--bg); color: var(--text); + border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; +} +button { + background: var(--accent); color: #06122b; border: 0; + border-radius: 8px; padding: 8px 12px; cursor: pointer; font-weight: 600; +} +button:hover { filter: brightness(1.08); } + +.login { + max-width: 420px; margin: 12vh auto; padding: 24px; text-align: center; +} +.login p { color: var(--muted); } +.login form { display: flex; gap: 8px; margin-top: 16px; } +.login input { flex: 1; } + +.chat { display: grid; grid-template-columns: 220px 1fr; height: 100vh; } +aside { + background: var(--panel); border-right: 1px solid var(--line); + padding: 16px; display: flex; flex-direction: column; gap: 12px; +} +aside .me { color: var(--muted); font-size: 13px; } +aside h2 { margin: 0; font-size: 12px; text-transform: uppercase; color: var(--muted); letter-spacing: 0.05em; } +aside ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; flex: 1; overflow: auto; } +aside li button { + width: 100%; text-align: left; background: transparent; color: var(--text); + font-weight: 500; padding: 8px 10px; +} +aside li button:hover { background: var(--line); } +aside li button.active { background: var(--accent); color: #06122b; } +aside form { display: flex; gap: 6px; } +aside form input { flex: 1; min-width: 0; } + +main { display: flex; flex-direction: column; min-width: 0; } +.empty { margin: auto; color: var(--muted); } +.feed { flex: 1; overflow: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px; } +.msg { display: flex; gap: 8px; align-items: baseline; } +.msg .author { color: var(--accent); font-weight: 600; min-width: 64px; } +.msg .body { white-space: pre-wrap; } +.composer { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--line); } +.composer input { flex: 1; } diff --git a/examples/chat/package-lock.json b/examples/chat/package-lock.json index bcff956e..d5eac173 100644 --- a/examples/chat/package-lock.json +++ b/examples/chat/package-lock.json @@ -9,14 +9,22 @@ "version": "0.0.0", "dependencies": { "capnweb": "file:../../packages/capnweb", + "react": "^19.2.0", + "react-dom": "^19.2.0", "typegres": "file:../..", "zod": "^4.4.3" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.12.21", "@cloudflare/workers-types": "^4.20260310.1", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.4", + "concurrently": "^9.2.1", "typescript": "^6.0.3", - "vitest": "~3.2.4" + "vite": "^7.3.6", + "vitest": "~3.2.4", + "wrangler": "^4.72.0" } }, "../..": { @@ -88,6 +96,326 @@ "ws": "^8.19.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", @@ -1180,6 +1508,50 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1237,6 +1609,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", @@ -1607,6 +1986,51 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1632,6 +2056,47 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/@vitest/expect": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", @@ -1747,6 +2212,32 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1757,6 +2248,19 @@ "node": ">=12" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1764,16 +2268,71 @@ "dev": true, "license": "MIT" }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/capnweb": { "resolved": "../../packages/capnweb", "link": true @@ -1795,6 +2354,36 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -1812,6 +2401,89 @@ "dev": true, "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -1826,6 +2498,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1864,6 +2543,20 @@ "node": ">=8" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.391", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.391.tgz", + "integrity": "sha512-YmCu4856jkgKT1Nh6fwRdeVrM6Ydf/fBnq51tpmSfX+jOcUMTxh31yH6hjKScRenhB2oDSvA9oooxcpjogPeig==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -1923,6 +2616,16 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1976,6 +2679,46 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1983,6 +2726,32 @@ "dev": true, "license": "MIT" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2000,6 +2769,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2057,6 +2836,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -2130,6 +2919,47 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -2175,6 +3005,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -2233,6 +3079,19 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2264,6 +3123,34 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -2351,13 +3238,22 @@ "node": ">=14.0.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/typegres": { "resolved": "../..", @@ -2397,6 +3293,37 @@ "pathe": "^2.0.3" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -2641,6 +3568,24 @@ } } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -2663,6 +3608,52 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/examples/chat/package.json b/examples/chat/package.json index 3c5b468e..5dc479c6 100644 --- a/examples/chat/package.json +++ b/examples/chat/package.json @@ -4,18 +4,30 @@ "private": true, "type": "module", "scripts": { + "dev": "concurrently -k -n ui,worker \"vite build --watch --emptyOutDir=false\" \"wrangler dev\"", + "build": "vite build", + "preview": "npm run build && wrangler dev", + "deploy": "npm run build && wrangler deploy", "test": "vitest run", "typecheck": "tsc --noEmit" }, "dependencies": { "capnweb": "file:../../packages/capnweb", "typegres": "file:../..", - "zod": "^4.4.3" + "zod": "^4.4.3", + "react": "^19.2.0", + "react-dom": "^19.2.0" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.12.21", "@cloudflare/workers-types": "^4.20260310.1", "typescript": "^6.0.3", - "vitest": "~3.2.4" + "vitest": "~3.2.4", + "@vitejs/plugin-react": "^5.0.4", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "vite": "^7.3.6", + "wrangler": "^4.72.0", + "concurrently": "^9.2.1" } } diff --git a/examples/chat/src/capabilities.ts b/examples/chat/src/capabilities.ts index feee0cd8..e5e2948e 100644 --- a/examples/chat/src/capabilities.ts +++ b/examples/chat/src/capabilities.ts @@ -39,6 +39,19 @@ export class Room { .where(({ room_members }) => room_members.room_id.eq(this.#id)); } + // Convenience read for the UI: messages joined with their authors, newest + // last. Executes server-side (the client can't reference the Users table to + // author the join itself) -- authorized by holding the Room cap. + @expose() + async feed() { + return Messages.from() + .join(Users, ({ messages, users }) => messages.user_id.eq(users.id)) + .where(({ messages }) => messages.room_id.eq(this.#id)) + .select(({ messages, users }) => ({ id: messages.id, author: users.name, body: messages.body })) + .orderBy(({ messages }) => messages.id) + .execute(this.#conn); + } + // Mutation: post a message as the current user. @expose(z.string().min(1)) async post(body: string) { diff --git a/examples/chat/src/empty.ts b/examples/chat/src/empty.ts new file mode 100644 index 00000000..caaf45e6 --- /dev/null +++ b/examples/chat/src/empty.ts @@ -0,0 +1,6 @@ +// Stub for the node-only typegres drivers (pg / better-sqlite3 / pglite). +// The Durable Object talks to SQLite through DoSqliteDriver only, so those +// drivers are never imported at runtime — aliasing them here (see +// wrangler.jsonc `alias`) keeps their native/node dependencies out of the +// workerd bundle. +export default {}; diff --git a/examples/chat/src/index.ts b/examples/chat/src/index.ts index 5979412b..54a9ff4d 100644 --- a/examples/chat/src/index.ts +++ b/examples/chat/src/index.ts @@ -8,6 +8,7 @@ import { DoSqliteDriver, type SqlStorageLike } from "./do-sqlite-driver"; export interface Env { CHAT: DurableObjectNamespace; + ASSETS: Fetcher; } // The single chat Durable Object. Holds one typegres Connection over its own @@ -34,8 +35,12 @@ export class ChatDo extends DurableObject { } export default { - // One DO for all rooms; every client routes to it. async fetch(req: Request, env: Env): Promise { - return env.CHAT.get(env.CHAT.idFromName("global")).fetch(req); + // /ws -> the single chat DO (Cap'n Web WebSocket). Everything else is the + // static React client. + if (new URL(req.url).pathname === "/ws") { + return env.CHAT.get(env.CHAT.idFromName("global")).fetch(req); + } + return env.ASSETS.fetch(req); }, }; diff --git a/examples/chat/test/capnweb.test.ts b/examples/chat/test/capnweb.test.ts index 87c48d3a..eca28894 100644 --- a/examples/chat/test/capnweb.test.ts +++ b/examples/chat/test/capnweb.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import { env } from "cloudflare:test"; import { newWebSocketRpcSession } from "capnweb"; import { doRpc, type ShimStub } from "typegres/capnweb"; -import type { User } from "../src/capabilities"; +import type { Room, User } from "../src/capabilities"; // Phase 4 -- the end-to-end showcase. A client connects to the DO over a real // WebSocket, and authors typegres queries as Cap'n Web closures that replay @@ -39,8 +39,9 @@ describe("Cap'n Web over WebSocket", () => { u.rooms().select(({ rooms }) => ({ id: rooms.id })).execute(u.conn), ).then(([r]) => doRpc(alice, (u) => - u - .room(r!.id) + // room(id) returns Promise in the type; capnweb pipelines the + // call on the eventual Room, so bridge the type with a cast. + (u.room(r!.id) as unknown as Room) .messages() .select(({ messages }) => ({ body: messages.body })) .execute(u.conn), diff --git a/examples/chat/tsconfig.json b/examples/chat/tsconfig.json index d248f0fe..bfdc5031 100644 --- a/examples/chat/tsconfig.json +++ b/examples/chat/tsconfig.json @@ -3,13 +3,14 @@ "target": "esnext", "module": "esnext", "moduleResolution": "bundler", - "lib": ["esnext"], + "lib": ["esnext", "dom", "dom.iterable"], "types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers"], + "jsx": "react-jsx", "strict": true, "noEmit": true, "skipLibCheck": true, "verbatimModuleSyntax": true, "experimentalDecorators": false }, - "include": ["src", "test", "vitest.config.ts"] + "include": ["src", "client", "test", "vite.config.ts", "vitest.config.ts"] } diff --git a/examples/chat/vite.config.ts b/examples/chat/vite.config.ts new file mode 100644 index 00000000..c689ab74 --- /dev/null +++ b/examples/chat/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Builds the browser client (client/) to dist/client, which wrangler serves as +// static assets. The Worker + Durable Object are built/served by wrangler; this +// config is only the frontend. +export default defineConfig({ + root: "client", + plugins: [react()], + build: { + outDir: "../dist/client", + emptyOutDir: true, + }, +}); diff --git a/examples/chat/wrangler.jsonc b/examples/chat/wrangler.jsonc index a858475a..350ab0ac 100644 --- a/examples/chat/wrangler.jsonc +++ b/examples/chat/wrangler.jsonc @@ -8,5 +8,22 @@ "durable_objects": { "bindings": [{ "name": "CHAT", "class_name": "ChatDo" }] }, - "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatDo"] }] + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatDo"] }], + // typegres re-exports node-only drivers (pg / better-sqlite3 / pglite) that + // the DO never uses (it goes through DoSqliteDriver). Alias them to an empty + // stub so their native/node deps stay out of the workerd bundle. + "alias": { + "pg": "./src/empty.ts", + "better-sqlite3": "./src/empty.ts", + "@electric-sql/pglite": "./src/empty.ts" + }, + // The built React client (vite build -> dist/client), served as static + // assets. run_worker_first routes every request through the Worker, which + // handles the /ws Cap'n Web upgrade and delegates the rest to ASSETS. + "assets": { + "directory": "./dist/client", + "binding": "ASSETS", + "not_found_handling": "single-page-application", + "run_worker_first": true + } } From 88377c3b4ece4e54683e11e6bc60287e4b707b1d Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 18:14:58 -0700 Subject: [PATCH 08/12] examples/chat: fix three bugs that broke the browser UI end to end Verified against a real workerd (wrangler dev) driven by a headless browser; all three are browser/React-runtime bugs the node and in-process workerd tests could not surface. 1. Login sent the capnweb stub through setState. connect() returns a capnweb stub, which is a *callable* Proxy. `setClient(connect(name))` made React treat it as a functional updater and invoke `stub(prevState)` -> the wire showed a spurious `main(null)` call and every later query pipelined off its broken result ("'' is not a function"). Store it via an initializer: `setClient(() => c)`. 2. joinRoom was not idempotent. Opening a room you already belong to (every room in your sidebar) ran a plain INSERT into room_members and hit a UNIQUE constraint. Skip the insert when membership already exists (the DO is single-threaded; typegres has no ON CONFLICT yet). Regression test added. 3. Feed auto-scroll effect used a concise arrow body, so useEffect returned the value of scrollIntoView() as its "cleanup" -> React later called it and threw "destroy is not a function" on every feed update. Use a block body. Full create -> open -> post -> read round trip now works in the minified production bundle with zero console errors; 15 workerd tests pass. Co-Authored-By: Claude Fable 5 --- examples/chat/.gitignore | 2 ++ examples/chat/client/App.tsx | 11 +++++++++-- examples/chat/src/capabilities.ts | 11 +++++++++-- examples/chat/test/capnweb.test.ts | 12 ++++++++++++ 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 examples/chat/.gitignore diff --git a/examples/chat/.gitignore b/examples/chat/.gitignore new file mode 100644 index 00000000..ce4f67d9 --- /dev/null +++ b/examples/chat/.gitignore @@ -0,0 +1,2 @@ +# Local wrangler dev state (DO SQLite, caches) +.wrangler/ diff --git a/examples/chat/client/App.tsx b/examples/chat/client/App.tsx index 7c7c2095..3b53ad74 100644 --- a/examples/chat/client/App.tsx +++ b/examples/chat/client/App.tsx @@ -23,7 +23,12 @@ export function App() {
{ e.preventDefault(); - if (name.trim()) setClient(connect(name.trim())); + // A capnweb stub is a callable Proxy; React's setState would treat + // it as a functional updater and invoke it. Wrap so it's stored, not called. + if (name.trim()) { + const c = connect(name.trim()); + setClient(() => c); + } }} > setName(e.target.value)} placeholder="your name" autoFocus /> @@ -122,7 +127,9 @@ function Chat({ client, me }: { client: Client; me: string }) { function Feed({ messages }: { messages: Msg[] }) { const end = useRef(null); - useEffect(() => end.current?.scrollIntoView(), [messages]); + useEffect(() => { + end.current?.scrollIntoView(); + }, [messages]); return (
{messages.map((m) => ( diff --git a/examples/chat/src/capabilities.ts b/examples/chat/src/capabilities.ts index e5e2948e..48a15794 100644 --- a/examples/chat/src/capabilities.ts +++ b/examples/chat/src/capabilities.ts @@ -105,8 +105,15 @@ export class User { async joinRoom(id: number): Promise { const [room] = await Rooms.from().where(({ rooms }) => rooms.id.eq(id)).select(({ rooms }) => ({ name: rooms.name })).execute(this.#conn); if (!room) throw new Error(`no such room ${id}`); - // INSERT OR IGNORE via the join-table PK; idempotent join. - await RoomMembers.insert({ room_id: id, user_id: this.#id }).execute(this.#conn); + // Idempotent join: skip the insert if already a member (the DO is + // single-threaded, so no membership race). typegres has no ON CONFLICT yet. + const [existing] = await RoomMembers.from() + .where(({ room_members }) => room_members.room_id.eq(id).and(room_members.user_id.eq(this.#id))) + .select(({ room_members }) => ({ room_id: room_members.room_id })) + .execute(this.#conn); + if (!existing) { + await RoomMembers.insert({ room_id: id, user_id: this.#id }).execute(this.#conn); + } return new Room(this.#conn, this.#id, id, room.name); } diff --git a/examples/chat/test/capnweb.test.ts b/examples/chat/test/capnweb.test.ts index eca28894..bfcf8959 100644 --- a/examples/chat/test/capnweb.test.ts +++ b/examples/chat/test/capnweb.test.ts @@ -50,6 +50,18 @@ describe("Cap'n Web over WebSocket", () => { expect(messages).toEqual([{ body: "hello from alice" }]); }); + it("joinRoom is idempotent: re-joining a room you already belong to doesn't throw", async () => { + using alice = await connect("rejoin", "alice"); + using room = await doRpc(alice, (u) => u.createRoom("general")); + const [{ id }] = await doRpc(alice, (u) => + u.rooms().select(({ rooms }) => ({ id: rooms.id })).execute(u.conn), + ); + // createRoom already made alice a member; joining again must be a no-op, + // not a UNIQUE-constraint failure on room_members. + await expect(doRpc(alice, (u) => u.joinRoom(id))).resolves.toBeDefined(); + void room; + }); + it("@expose gates the surface over the wire: a non-member can't reach a room", async () => { using alice = await connect("gating", "alice"); using room = await doRpc(alice, (u) => u.createRoom("private")); From 9ddff14373ad2d5f3194a7e629fbc0d39d4b3169 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Tue, 14 Jul 2026 18:29:36 -0700 Subject: [PATCH 09/12] examples/chat: public room directory so you can join others' rooms Previously the sidebar only listed rooms you'd already joined, and nothing surfaced the id of a room someone else created -- so there was no way to join one through the UI (only the plumbing, User.joinRoom, existed). Add a public directory: - User.allRooms() -- a client-refinable builder over every room. Any authenticated user may list it; a Room *capability* is still only granted once you're a member, so the @expose authorization story is unchanged. - Client lists allRooms() alongside rooms() and diffs them: rooms you're in are clickable, rooms you're not in show a [Join] button that joins then opens. Verified in a real browser: a second user sees another's room in the directory, joins it, reads the existing message, and both users exchange messages. Added a workerd regression test for discover -> join -> read. 16 tests pass. Co-Authored-By: Claude Fable 5 --- examples/chat/README.md | 2 ++ examples/chat/client/App.tsx | 44 ++++++++++++++++++++++-------- examples/chat/client/api.ts | 6 ++++ examples/chat/client/styles.css | 8 ++++++ examples/chat/src/capabilities.ts | 8 ++++++ examples/chat/test/capnweb.test.ts | 22 +++++++++++++++ 6 files changed, 79 insertions(+), 11 deletions(-) diff --git a/examples/chat/README.md b/examples/chat/README.md index fd3c4b24..77a7bd13 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -18,6 +18,8 @@ Browser (React) ──WebSocket / Cap'n Web──▶ Worker ──▶ Durable - **`src/capabilities.ts`** — the `@expose` capability graph (`User` → `Room`). `@expose` gating is the authorization: you only reach a `Room` you're a member of, enforced at grant time. A hostile client can't forge a capability. + `allRooms()` lists the public directory (any room is joinable), while a `Room` + *capability* is still only handed out once you're a member. - **`src/index.ts`** — the Worker + Durable Object; serves the cap graph over a WebSocket and the React client as static assets. - **`client/`** — the React UI (`api.ts` is the framework-agnostic Cap'n Web glue). diff --git a/examples/chat/client/App.tsx b/examples/chat/client/App.tsx index 3b53ad74..cdf64771 100644 --- a/examples/chat/client/App.tsx +++ b/examples/chat/client/App.tsx @@ -4,6 +4,7 @@ import { createRoom, feed, joinRoom, + listAllRooms, listRooms, post, type Client, @@ -41,13 +42,18 @@ export function App() { } function Chat({ client, me }: { client: Client; me: string }) { - const [rooms, setRooms] = useState([]); + const [rooms, setRooms] = useState([]); // the public directory (all rooms) + const [mine, setMine] = useState>(new Set()); // ids of rooms I've joined const [roomId, setRoomId] = useState(null); const [messages, setMessages] = useState([]); const [draft, setDraft] = useState(""); const [newRoom, setNewRoom] = useState(""); - const refreshRooms = () => listRooms(client).then(setRooms); + const refreshRooms = () => + Promise.all([listAllRooms(client), listRooms(client)]).then(([all, joined]) => { + setRooms(all); + setMine(new Set(joined.map((r) => r.id))); + }); useEffect(() => { void refreshRooms(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -66,12 +72,19 @@ function Chat({ client, me }: { client: Client; me: string }) { }; }, [client, roomId]); - const open = async (id: number) => { - await joinRoom(client, id); // idempotent; makes `me` a member so the feed is authorized + // Open a room I'm already a member of (feed/post are authorized by membership). + const open = (id: number) => { setRoomId(id); setMessages([]); }; + // Join a room from the directory, then open it. + const join = async (id: number) => { + await joinRoom(client, id); + await refreshRooms(); + open(id); + }; + const send = async (e: React.FormEvent) => { e.preventDefault(); if (roomId == null || !draft.trim()) return; @@ -94,13 +107,22 @@ function Chat({ client, me }: { client: Client; me: string }) {
signed in as {me}

rooms

    - {rooms.map((r) => ( -
  • - -
  • - ))} + {rooms.map((r) => + mine.has(r.id) ? ( +
  • + +
  • + ) : ( +
  • + #{r.name} + +
  • + ), + )}
setNewRoom(e.target.value)} placeholder="new room" /> diff --git a/examples/chat/client/api.ts b/examples/chat/client/api.ts index 1ac033bc..2588d1c4 100644 --- a/examples/chat/client/api.ts +++ b/examples/chat/client/api.ts @@ -22,6 +22,12 @@ export const listRooms = (c: Client): Promise => u.rooms().select(({ rooms }) => ({ id: rooms.id, name: rooms.name })).orderBy(({ rooms }) => rooms.id).execute(u.conn), ); +// The public room directory (all rooms, joinable or already joined). +export const listAllRooms = (c: Client): Promise => + doRpc(c, (u: User) => + u.allRooms().select(({ rooms }) => ({ id: rooms.id, name: rooms.name })).orderBy(({ rooms }) => rooms.id).execute(u.conn), + ); + export const createRoom = (c: Client, name: string): Promise => doRpc(c, (u: User) => u.createRoom(name)); export const joinRoom = (c: Client, id: number): Promise => doRpc(c, (u: User) => u.joinRoom(id)); diff --git a/examples/chat/client/styles.css b/examples/chat/client/styles.css index dde7b73c..c7d1b6c1 100644 --- a/examples/chat/client/styles.css +++ b/examples/chat/client/styles.css @@ -41,6 +41,14 @@ aside li button { } aside li button:hover { background: var(--line); } aside li button.active { background: var(--accent); color: #06122b; } +aside li.joinable { + display: flex; align-items: center; justify-content: space-between; + padding: 4px 10px; color: var(--muted); +} +aside li.joinable span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +aside li.joinable .join { + width: auto; padding: 3px 10px; font-size: 12px; flex: none; +} aside form { display: flex; gap: 6px; } aside form input { flex: 1; min-width: 0; } diff --git a/examples/chat/src/capabilities.ts b/examples/chat/src/capabilities.ts index 48a15794..e9b7f52d 100644 --- a/examples/chat/src/capabilities.ts +++ b/examples/chat/src/capabilities.ts @@ -92,6 +92,14 @@ export class User { .where(({ room_members }) => room_members.user_id.eq(this.#id)); } + // Client-refinable: the public room directory -- every room is joinable, so + // any authenticated user may list them (membership is enforced later, when a + // Room capability is actually handed out). The client diffs this against + // rooms() to know which ones it still needs to join. + @expose() allRooms() { + return Rooms.from(); + } + @expose(z.string().min(1)) async createRoom(name: string): Promise { const [room] = await Rooms.insert({ name, created_by: this.#id }) diff --git a/examples/chat/test/capnweb.test.ts b/examples/chat/test/capnweb.test.ts index bfcf8959..8fd29055 100644 --- a/examples/chat/test/capnweb.test.ts +++ b/examples/chat/test/capnweb.test.ts @@ -62,6 +62,28 @@ describe("Cap'n Web over WebSocket", () => { void room; }); + it("allRooms lists the public directory so another user can discover and join", async () => { + using alice = await connect("directory", "alice"); + using room = await doRpc(alice, (u) => u.createRoom("general")); + await room.post("hi, anyone here?"); + + using bob = await connect("directory", "bob"); + // bob isn't a member, so rooms() is empty but allRooms() shows the room. + const mine = await doRpc(bob, (u) => u.rooms().select(({ rooms }) => ({ id: rooms.id })).execute(u.conn)); + expect(mine).toEqual([]); + const dir = await doRpc(bob, (u) => + u.allRooms().select(({ rooms }) => ({ id: rooms.id, name: rooms.name })).execute(u.conn), + ); + expect(dir).toEqual([{ id: expect.any(Number), name: "general" }]); + + // bob joins the discovered room, then can read its feed. + await doRpc(bob, (u) => u.joinRoom(dir[0]!.id)); + const feed = await doRpc(bob, (u) => + (u.room(dir[0]!.id) as unknown as Room).messages().select(({ messages }) => ({ body: messages.body })).execute(u.conn), + ); + expect(feed).toEqual([{ body: "hi, anyone here?" }]); + }); + it("@expose gates the surface over the wire: a non-member can't reach a room", async () => { using alice = await connect("gating", "alice"); using room = await doRpc(alice, (u) => u.createRoom("private")); From 6d9d7a5f0e77bb6fb1437387792439c836ba89a7 Mon Sep 17 00:00:00 2001 From: Ryan Rasti Date: Wed, 15 Jul 2026 15:48:01 -0700 Subject: [PATCH 10/12] Address chat review: core DO driver, caps-first API, generate schema Promote DoSqliteDriver into typegres with typegres/core and typegres/do-sqlite entries so Workers avoid optional node peers (drop empty.ts aliases). Reshape the chat demo around ChatApi.userByName, client-authored doRpc queries (no api/feed wrappers), Tailwind, and tg generate from migrate DDL. Add LLM-oriented guidelines.md. --- examples/chat/.gitignore | 2 + examples/chat/README.md | 36 +- examples/chat/client/App.tsx | 198 +++- examples/chat/client/api.ts | 42 +- examples/chat/client/index.css | 27 + examples/chat/client/index.html | 1 - examples/chat/client/main.tsx | 1 + examples/chat/client/styles.css | 62 -- examples/chat/package-lock.json | 1063 ++++++++++++++++++++- examples/chat/package.json | 22 +- examples/chat/scripts/generate-schema.mjs | 78 ++ examples/chat/src/capabilities.ts | 62 +- examples/chat/src/db.ts | 5 + examples/chat/src/do-sqlite-driver.ts | 80 -- examples/chat/src/empty.ts | 6 - examples/chat/src/index.ts | 20 +- examples/chat/src/migrate.ts | 56 ++ examples/chat/src/schema.ts | 73 -- examples/chat/src/tables/messages.ts | 18 + examples/chat/src/tables/room_members.ts | 15 + examples/chat/src/tables/rooms.ts | 18 + examples/chat/src/tables/users.ts | 17 + examples/chat/test/capabilities.test.ts | 13 +- examples/chat/test/capnweb.test.ts | 47 +- examples/chat/test/do-driver.test.ts | 4 +- examples/chat/test/schema.test.ts | 5 +- examples/chat/typegres.config.ts | 11 + examples/chat/vite.config.ts | 3 +- examples/chat/wrangler.jsonc | 8 - guidelines.md | 249 +++++ package.json | 12 + site/.gitignore | 1 + src/core.ts | 13 + src/database.ts | 2 +- src/driver-do-sqlite.ts | 44 + src/driver-shared.ts | 65 ++ src/driver.ts | 66 +- src/index.ts | 9 +- tsdown.config.ts | 2 +- 39 files changed, 1988 insertions(+), 468 deletions(-) create mode 100644 examples/chat/client/index.css delete mode 100644 examples/chat/client/styles.css create mode 100644 examples/chat/scripts/generate-schema.mjs create mode 100644 examples/chat/src/db.ts delete mode 100644 examples/chat/src/do-sqlite-driver.ts delete mode 100644 examples/chat/src/empty.ts create mode 100644 examples/chat/src/migrate.ts delete mode 100644 examples/chat/src/schema.ts create mode 100644 examples/chat/src/tables/messages.ts create mode 100644 examples/chat/src/tables/room_members.ts create mode 100644 examples/chat/src/tables/rooms.ts create mode 100644 examples/chat/src/tables/users.ts create mode 100644 examples/chat/typegres.config.ts create mode 100644 guidelines.md create mode 100644 src/core.ts create mode 100644 src/driver-do-sqlite.ts create mode 100644 src/driver-shared.ts diff --git a/examples/chat/.gitignore b/examples/chat/.gitignore index ce4f67d9..1e110047 100644 --- a/examples/chat/.gitignore +++ b/examples/chat/.gitignore @@ -1,2 +1,4 @@ # Local wrangler dev state (DO SQLite, caches) .wrangler/ +# Temp sqlite file used by `npm run generate` (tg generate introspection) +.tg-schema.sqlite diff --git a/examples/chat/README.md b/examples/chat/README.md index 77a7bd13..fb0e4b4f 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -7,28 +7,32 @@ there is no REST anywhere. ``` Browser (React) ──WebSocket / Cap'n Web──▶ Worker ──▶ Durable Object - authors queries as closures typegres → DoSqliteDriver - doRpc(user => user.rooms() → ctx.storage.sql + connect() → ChatApi typegres/core + DoSqliteDriver + doRpc(api => api.userByName(name)) → ctx.storage.sql + doRpc(user => user.rooms() .select(...).execute(user.conn)) ``` -- **`src/do-sqlite-driver.ts`** — a typegres `Driver` over Cloudflare's - `SqlStorage`. -- **`src/schema.ts`** — the tables (users, rooms, room_members, messages). -- **`src/capabilities.ts`** — the `@expose` capability graph (`User` → `Room`). - `@expose` gating is the authorization: you only reach a `Room` you're a member - of, enforced at grant time. A hostile client can't forge a capability. - `allRooms()` lists the public directory (any room is joinable), while a `Room` - *capability* is still only handed out once you're a member. -- **`src/index.ts`** — the Worker + Durable Object; serves the cap graph over a - WebSocket and the React client as static assets. -- **`client/`** — the React UI (`api.ts` is the framework-agnostic Cap'n Web glue). +- **`typegres/do-sqlite`** — `DoSqliteDriver` over Cloudflare `SqlStorage`. +- **`typegres/core`** — schema/SQL/@expose without node driver peers (so the + worker never resolves `pg` / `better-sqlite3` / pglite). +- **`src/tables/`** — generated table classes (`npm run generate`). +- **`src/migrate.ts`** — DDL applied once per DO on init (and by generate). +- **`src/capabilities.ts`** — `@expose` graph: `ChatApi` → `User` → `Room`. + Reads return query builders the client refines; mutations run server-side. + `@expose` gating is authorization: you only get a `Room` cap when you're a + member. +- **`src/index.ts`** — Worker + Durable Object; serves Cap'n Web on `/ws` and + the React client as static assets. +- **`client/`** — React UI. `api.ts` is transport only (`connect`); the UI + authors queries inline via `doRpc`. ## Develop ```sh npm install -npm run dev # vite (client, watched) + wrangler dev (worker + DO) +npm run generate # migrate DDL → temp sqlite → tg generate → src/tables +npm run dev # vite (client, watched) + wrangler dev (worker + DO) ``` Open the printed `wrangler dev` URL, pick a name, create a room, chat. @@ -57,5 +61,5 @@ round trip over a WebSocket. - `SqlStorage` rejects `BigInt` bindings and marshals `INTEGER` results to JS `number` (lossy above 2^53) — see `test/sqlstorage.probe.test.ts`. Fine for chat-scale ids; the driver down-converts typegres's boolean `0n/1n`. -- The node-only typegres drivers (pg / better-sqlite3 / pglite) are aliased to an - empty stub in `wrangler.jsonc` — the DO only uses `DoSqliteDriver`. +- Auth is PoC find-or-create via `ChatApi.userByName(name)`. A real app verifies + a token in that method and never creates users on login. diff --git a/examples/chat/client/App.tsx b/examples/chat/client/App.tsx index cdf64771..3287d899 100644 --- a/examples/chat/client/App.tsx +++ b/examples/chat/client/App.tsx @@ -1,40 +1,54 @@ import { useEffect, useRef, useState } from "react"; -import { - connect, - createRoom, - feed, - joinRoom, - listAllRooms, - listRooms, - post, - type Client, - type Msg, - type RoomInfo, -} from "./api"; +import { doRpc, type ShimStub } from "typegres/capnweb"; +import type { Room, User } from "../src/capabilities"; +import { connect, type Root } from "./api"; + +type Client = ShimStub; +type RoomInfo = { id: number; name: string }; +type Msg = { id: number; author: string; body: string }; export function App() { const [name, setName] = useState(""); const [client, setClient] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); if (!client) { return ( -
-

typegres chat

-

Queries you send run inside a Cloudflare Durable Object's SQLite, over Cap'n Web.

+
+

typegres chat

+

+ Queries you send run inside a Cloudflare Durable Object's SQLite, over Cap'n Web. +

{ e.preventDefault(); - // A capnweb stub is a callable Proxy; React's setState would treat - // it as a functional updater and invoke it. Wrap so it's stored, not called. - if (name.trim()) { - const c = connect(name.trim()); - setClient(() => c); - } + const n = name.trim(); + if (!n || busy) return; + setBusy(true); + setError(null); + // Root is ChatApi; userByName is on the capability graph (not ?user=). + // A capnweb stub is a callable Proxy — wrap for React setState. + const root: Root = connect(); + void doRpc(root, (api) => api.userByName(n)) + .then((user) => setClient(() => user as unknown as Client)) + .catch((err: unknown) => setError(err instanceof Error ? err.message : String(err))) + .finally(() => setBusy(false)); }} > - setName(e.target.value)} placeholder="your name" autoFocus /> - + setName(e.target.value)} + placeholder="your name" + autoFocus + /> + + {error &&

{error}

}
); } @@ -48,9 +62,21 @@ function Chat({ client, me }: { client: Client; me: string }) { const [messages, setMessages] = useState([]); const [draft, setDraft] = useState(""); const [newRoom, setNewRoom] = useState(""); + // member id → name, filled when a room is opened so the client can label messages + // without a server-side join wrapper (Room.members() + Room.messages() are both + // client-refined builders). + const namesRef = useRef>(new Map()); + // Client-authored queries: the browser writes the select; it runs in the DO. const refreshRooms = () => - Promise.all([listAllRooms(client), listRooms(client)]).then(([all, joined]) => { + Promise.all([ + doRpc(client, (u) => + u.allRooms().select(({ rooms }) => ({ id: rooms.id, name: rooms.name })).orderBy(({ rooms }) => rooms.id).execute(u.conn), + ), + doRpc(client, (u) => + u.rooms().select(({ rooms }) => ({ id: rooms.id, name: rooms.name })).orderBy(({ rooms }) => rooms.id).execute(u.conn), + ), + ]).then(([all, joined]) => { setRooms(all); setMine(new Set(joined.map((r) => r.id))); }); @@ -59,20 +85,49 @@ function Chat({ client, me }: { client: Client; me: string }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Poll the current room's feed (Phase 6 will push instead). + // Load member names once per open room, then poll a client-authored messages query. useEffect(() => { if (roomId == null) return; let alive = true; - const tick = () => feed(client, roomId).then((m) => alive && setMessages(m)).catch(() => {}); - void tick(); - const t = setInterval(tick, 1000); + + const loadNames = doRpc(client, (u) => + (u.room(roomId) as unknown as Room) + .members() + .select(({ users }) => ({ id: users.id, name: users.name })) + .execute(u.conn), + ).then((rows) => { + if (!alive) return; + namesRef.current = new Map(rows.map((r) => [r.id, r.name])); + }); + + const loadFeed = (): Promise => + doRpc(client, (u) => + (u.room(roomId) as unknown as Room) + .messages() + .select(({ messages }) => ({ id: messages.id, userId: messages.user_id, body: messages.body })) + .orderBy(({ messages }) => messages.id) + .execute(u.conn), + ).then((rows) => { + if (!alive) return; + const names = namesRef.current; + setMessages( + rows.map((r) => ({ + id: r.id, + author: names.get(r.userId) ?? `#${r.userId}`, + body: r.body, + })), + ); + }); + + void loadNames.then(() => loadFeed()).catch(() => {}); + const t = setInterval(() => void loadFeed().catch(() => {}), 1000); return () => { alive = false; clearInterval(t); }; }, [client, roomId]); - // Open a room I'm already a member of (feed/post are authorized by membership). + // Open a room I'm already a member of (messages/post are authorized by membership). const open = (id: number) => { setRoomId(id); setMessages([]); @@ -80,7 +135,7 @@ function Chat({ client, me }: { client: Client; me: string }) { // Join a room from the directory, then open it. const join = async (id: number) => { - await joinRoom(client, id); + await doRpc(client, (u) => u.joinRoom(id)); await refreshRooms(); open(id); }; @@ -88,56 +143,95 @@ function Chat({ client, me }: { client: Client; me: string }) { const send = async (e: React.FormEvent) => { e.preventDefault(); if (roomId == null || !draft.trim()) return; - await post(client, roomId, draft.trim()); + const body = draft.trim(); + await doRpc(client, (u) => (u.room(roomId) as unknown as Room).post(body)); setDraft(""); - setMessages(await feed(client, roomId)); + // Immediate re-read with the same client-authored query the poll uses. + const rows = await doRpc(client, (u) => + (u.room(roomId) as unknown as Room) + .messages() + .select(({ messages }) => ({ id: messages.id, userId: messages.user_id, body: messages.body })) + .orderBy(({ messages }) => messages.id) + .execute(u.conn), + ); + const names = namesRef.current; + setMessages( + rows.map((r) => ({ + id: r.id, + author: names.get(r.userId) ?? `#${r.userId}`, + body: r.body, + })), + ); }; const addRoom = async (e: React.FormEvent) => { e.preventDefault(); if (!newRoom.trim()) return; - await createRoom(client, newRoom.trim()); + await doRpc(client, (u) => u.createRoom(newRoom.trim())); setNewRoom(""); await refreshRooms(); }; return ( -
-