diff --git a/.gitignore b/.gitignore index 40b911851..75d71a3ab 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ tests/repo.git/ .env logs/agent_* data/family/ +github-cors-proxy/node_modules/* diff --git a/Cargo.lock b/Cargo.lock index 405980a3e..1ea86ab29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,17 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -141,6 +152,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.59" @@ -556,6 +573,23 @@ dependencies = [ "url", ] +[[package]] +name = "grit-wasm" +version = "0.1.2" +dependencies = [ + "base64", + "grit-lib", + "js-sys", + "serde", + "serde_json", + "sha1", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -884,9 +918,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -906,6 +940,12 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.16" @@ -978,6 +1018,16 @@ dependencies = [ "difflib", ] +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1024,6 +1074,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-complex" version = "0.2.4" @@ -1056,6 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1070,6 +1130,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1421,6 +1487,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "semver" version = "1.0.27" @@ -1870,6 +1945,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1905,9 +1990,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -1918,9 +2003,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -1928,9 +2013,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1938,9 +2023,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -1951,13 +2036,52 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb55e2540ad1c56eec35fd63e2aea15f83b11ce487fd2de9ad11578dfc047ea" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf0ca1bd612b988616bac1ab34c4e4290ef18f7148a1d8b7f31c150080e9295" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cda5ecc67248c48d3e705d3e03e00af905769b78b9d2a1678b663b8b9d4472" + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1994,9 +2118,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2046,6 +2170,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 45144693d..a18fd5a42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["grit", "grit-lib", "git-cloud"] +members = ["grit", "grit-lib", "git-cloud", "grit-wasm"] resolver = "2" [workspace.package] diff --git a/github-cors-proxy/README.md b/github-cors-proxy/README.md new file mode 100644 index 000000000..aaa859079 --- /dev/null +++ b/github-cors-proxy/README.md @@ -0,0 +1,92 @@ +# GitHub CORS Proxy + +This is a small Vercel-deployable CORS proxy for Git smart HTTP traffic. It lets +the browser/WASM client talk to a Git host that does not emit browser CORS +headers itself. + +The proxy is intentionally not an open proxy. By default it only forwards to: + +- `github.com` +- `www.github.com` +- `gist.github.com` + +## Deploy + +```bash +npm install +npm run deploy:prod +``` + +Or run locally: + +```bash +npm install +npm run dev +``` + +## URL Shape + +Use the deployed proxy as the repository base URL: + +```text +https://.vercel.app/OWNER/REPO.git +``` + +The WASM Git client will append normal smart HTTP paths: + +```text +/info/refs?service=git-upload-pack +/git-upload-pack +/info/refs?service=git-receive-pack +/git-receive-pack +``` + +For example, in the demo page enter: + +```text +https://.vercel.app/rust-lang/rust.git +``` + +With the custom domain discussed for this project: + +```text +https://github-proxy.io/rust-lang/rust.git +``` + +The original explicit API form still works too: + +```text +https://.vercel.app/api/git/github.com/OWNER/REPO.git +``` + +## Authentication + +Prefer sending credentials as an `Authorization` header to the proxy. The proxy +forwards `Authorization` to GitHub. + +For private GitHub repositories, use a token with the minimal scopes needed for +the operation. Avoid logging request headers in production. + +## Security Notes + +Before using this publicly: + +- Keep the host allowlist restrictive. +- Add authentication or rate limiting for your proxy. +- Consider restricting repository owners/names. +- Be aware of Vercel function time and response size limits. Blobless fetches + help, but large repos or pushes can still exceed serverless limits. + +## CORS Headers + +The proxy answers preflight requests and emits: + +```http +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Authorization, Content-Type, Git-Protocol, Accept +Access-Control-Expose-Headers: Content-Type, Git-Protocol, WWW-Authenticate +``` + +Set `ALLOW_ORIGIN=https://your-app.example` in Vercel if you want to restrict +the allowed origin instead of using `*`. diff --git a/github-cors-proxy/api/git/[...path].ts b/github-cors-proxy/api/git/[...path].ts new file mode 100644 index 000000000..5b300027a --- /dev/null +++ b/github-cors-proxy/api/git/[...path].ts @@ -0,0 +1,172 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +const ALLOWED_HOSTS = new Set([ + "github.com", + "www.github.com", + "gist.github.com", +]); + +const HOP_BY_HOP_HEADERS = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "host", +]); + +const FORWARDED_REQUEST_HEADERS = [ + "accept", + "accept-encoding", + "authorization", + "content-type", + "git-protocol", + "user-agent", +]; + +function setCors(res: VercelResponse): void { + res.setHeader("Access-Control-Allow-Origin", process.env.ALLOW_ORIGIN ?? "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.setHeader( + "Access-Control-Allow-Headers", + "Authorization, Content-Type, Git-Protocol, Accept" + ); + res.setHeader( + "Access-Control-Expose-Headers", + "Content-Type, Git-Protocol, WWW-Authenticate" + ); + res.setHeader("Vary", "Origin"); +} + +function pathSegments(req: VercelRequest): string[] { + const value = req.query.path; + if (Array.isArray(value)) { + return value.flatMap((part) => part.split("/")).filter(Boolean); + } + if (typeof value === "string") { + return value.split("/").filter(Boolean); + } + return []; +} + +function upstreamUrl(req: VercelRequest): URL { + const segments = pathSegments(req); + if (segments.length === 0) { + throw new Error("missing repository path"); + } + + let normalizedHost = "github.com"; + let pathParts = segments; + const first = segments[0].toLowerCase(); + if (ALLOWED_HOSTS.has(first)) { + normalizedHost = first; + pathParts = segments.slice(1); + } + + const url = new URL(`https://${normalizedHost}/${pathParts.map(encodeURIComponent).join("/")}`); + for (const [key, value] of Object.entries(req.query)) { + if (key === "path") { + continue; + } + if (Array.isArray(value)) { + for (const item of value) { + url.searchParams.append(key, item); + } + } else if (typeof value === "string") { + url.searchParams.set(key, value); + } + } + return url; +} + +function requestHeaders(req: VercelRequest): Headers { + const headers = new Headers(); + for (const name of FORWARDED_REQUEST_HEADERS) { + const value = req.headers[name]; + if (Array.isArray(value)) { + headers.set(name, value.join(", ")); + } else if (value) { + headers.set(name, value); + } + } + + if (!headers.has("user-agent")) { + headers.set("user-agent", "github-cors-proxy"); + } + return headers; +} + +function responseHeaders(upstream: Response): Record { + const headers: Record = {}; + upstream.headers.forEach((value, key) => { + const lower = key.toLowerCase(); + if (!HOP_BY_HOP_HEADERS.has(lower)) { + headers[key] = value; + } + }); + return headers; +} + +function requestBody(req: VercelRequest): BodyInit | undefined { + if (req.method === "GET" || req.method === "HEAD") { + return undefined; + } + if (Buffer.isBuffer(req.body)) { + return new Uint8Array(req.body); + } + if (typeof req.body === "string") { + return req.body; + } + if (req.body == null) { + return undefined; + } + return JSON.stringify(req.body); +} + +export default async function handler(req: VercelRequest, res: VercelResponse) { + setCors(res); + + if (req.method === "OPTIONS") { + res.status(204).end(); + return; + } + + if (req.method !== "GET" && req.method !== "POST") { + res.setHeader("Allow", "GET, POST, OPTIONS"); + res.status(405).send("method not allowed\n"); + return; + } + + let url: URL; + try { + url = upstreamUrl(req); + } catch (error) { + res.status(400).send(`${error instanceof Error ? error.message : String(error)}\n`); + return; + } + + try { + const upstream = await fetch(url, { + method: req.method, + headers: requestHeaders(req), + body: requestBody(req), + redirect: "manual", + }); + + for (const [key, value] of Object.entries(responseHeaders(upstream))) { + res.setHeader(key, value); + } + setCors(res); + res.status(upstream.status); + + const body = Buffer.from(await upstream.arrayBuffer()); + res.send(body); + } catch (error) { + res + .status(502) + .send(`upstream fetch failed: ${error instanceof Error ? error.message : String(error)}\n`); + } +} diff --git a/github-cors-proxy/package-lock.json b/github-cors-proxy/package-lock.json new file mode 100644 index 000000000..72f6fab42 --- /dev/null +++ b/github-cors-proxy/package-lock.json @@ -0,0 +1,4134 @@ +{ + "name": "github-cors-proxy", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "github-cors-proxy", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.17.0", + "@vercel/node": "^3.2.0", + "typescript": "^5.6.0", + "vercel": "^39.0.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/@edge-runtime/format": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@edge-runtime/format/-/format-2.2.1.tgz", + "integrity": "sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/node-utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/node-utils/-/node-utils-2.3.0.tgz", + "integrity": "sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/ponyfill": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@edge-runtime/ponyfill/-/ponyfill-2.4.2.tgz", + "integrity": "sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/primitives": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-4.1.0.tgz", + "integrity": "sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/vm": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.2.0.tgz", + "integrity": "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@edge-runtime/primitives": "4.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.1.tgz", + "integrity": "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "minimatch": "^3.0.4", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/build-utils": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-8.7.0.tgz", + "integrity": "sha512-ofZX+ABiW76u5khIyYyH5xK5KSuiAteqRu5hz2k1a2WHLwF7VpeBg8gdFR+HwbVnNkHtkMA64ya5Dd/lNoABkw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/error-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vercel/error-utils/-/error-utils-2.0.3.tgz", + "integrity": "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/fun": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vercel/fun/-/fun-1.1.2.tgz", + "integrity": "sha512-n13RO1BUy8u6+kzDQ2++BRj4Y5EAiQPt+aV+Tb2HNTmToNr4Mu3dE1kFlaTVTxQzAT3hvIRlVEU/OMvF8LCFJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@tootallnate/once": "2.0.0", + "async-listen": "1.2.0", + "debug": "4.3.4", + "execa": "3.2.0", + "fs-extra": "8.1.0", + "generic-pool": "3.4.2", + "micro": "9.3.5-canary.3", + "ms": "2.1.1", + "node-fetch": "2.6.7", + "path-match": "1.2.4", + "promisepipe": "3.0.0", + "semver": "7.5.4", + "stat-mode": "0.3.0", + "stream-to-promise": "2.2.0", + "tar": "4.4.18", + "tree-kill": "1.2.2", + "uid-promise": "1.0.0", + "uuid": "3.3.2", + "xdg-app-paths": "5.1.0", + "yauzl-promise": "2.1.3" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@vercel/fun/node_modules/async-listen": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz", + "integrity": "sha512-CcEtRh/oc9Jc4uWeUwdpG/+Mb2YUHKmdaTf0gUr7Wa+bfp4xx70HOb3RuSTJMvqKNB1TkdTfjLdrcz2X4rkkZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/fun/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vercel/fun/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@vercel/fun/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/fun/node_modules/fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^2.6.0" + } + }, + "node_modules/@vercel/fun/node_modules/minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/@vercel/fun/node_modules/minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^2.9.0" + } + }, + "node_modules/@vercel/fun/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/@vercel/fun/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/fun/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@vercel/fun/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vercel/fun/node_modules/tar": { + "version": "4.4.18", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.18.tgz", + "integrity": "sha512-ZuOtqqmkV9RE1+4odd+MhBpibmCxNP6PJhH/h2OqNuotTX7/XHPZQJv2pKvWMplFH9SIZZhitehh6vBH6LO8Pg==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/@vercel/fun/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/@vercel/gatsby-plugin-vercel-analytics": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-analytics/-/gatsby-plugin-vercel-analytics-1.0.11.tgz", + "integrity": "sha512-iTEA0vY6RBPuEzkwUTVzSHDATo1aF6bdLLspI68mQ/BTbi5UQEGjpjyzdKOVcSYApDtFU6M6vypZ1t4vIEnHvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "web-vitals": "0.2.4" + } + }, + "node_modules/@vercel/gatsby-plugin-vercel-builder": { + "version": "2.0.65", + "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.0.65.tgz", + "integrity": "sha512-MQX56fuL4WHDhT/fvKy9FMJigOymTAcCqw8rteF1wpRBAGhapSJkhT34I4mkfRRMFk1kIV7ijwuX+w1mpRrLjA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "0.25.24", + "@vercel/build-utils": "9.1.0", + "@vercel/routing-utils": "5.0.1", + "esbuild": "0.14.47", + "etag": "1.8.1", + "fs-extra": "11.1.0" + } + }, + "node_modules/@vercel/gatsby-plugin-vercel-builder/node_modules/@vercel/build-utils": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-9.1.0.tgz", + "integrity": "sha512-ccknvdKH6LDB9ZzZaX8a8cOvFbI441APLHvKrunJE/wezY0skmfuEUK1qnfPApXMs4FMWzZQj2LO9qpzfgBPsQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/gatsby-plugin-vercel-builder/node_modules/fs-extra": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", + "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@vercel/gatsby-plugin-vercel-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@vercel/gatsby-plugin-vercel-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@vercel/go": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vercel/go/-/go-3.2.1.tgz", + "integrity": "sha512-ezjmuUvLigH9V4egEaX0SZ+phILx8lb+Zkp1iTqKI+yl/ibPAtVo5o+dLSRAXU9U01LBmaLu3O8Oxd/JpWYCOw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/hydrogen": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@vercel/hydrogen/-/hydrogen-1.0.11.tgz", + "integrity": "sha512-nkSQ0LC7rFRdfkTUGm9pIbAfRb2Aat05u8ouN0FoUl7/I/YVgd0G6iRBN9bOMFUIiBiaKB4KqaZEFzVfUHpwYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/static-config": "3.0.0", + "ts-morph": "12.0.0" + } + }, + "node_modules/@vercel/next": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@vercel/next/-/next-4.4.4.tgz", + "integrity": "sha512-/xMzlOMY8UHzCehRZzx8TIdzVRCu3O2O+Gb7R8uRX0/ci9cLIjJvi0WfLyR06Ny4fMqMzzUuRADp5ezfJjaO1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/nft": "0.27.10" + } + }, + "node_modules/@vercel/next/node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/next/node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@vercel/next/node_modules/@vercel/nft": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.10.tgz", + "integrity": "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0-rc.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vercel/next/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@vercel/next/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vercel/next/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/next/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vercel/next/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@vercel/next/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@vercel/next/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@vercel/next/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vercel/next/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/next/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/nft": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.3.tgz", + "integrity": "sha512-oySTdDSzUAFDXpsSLk9Q943o+/Yu/+TCFxnehpFQEf/3khi2stMpTHPVNwFdvZq/Z4Ky93lE+MGHpXCRpMkSCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.5", + "@rollup/pluginutils": "^4.0.0", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.2", + "node-gyp-build": "^4.2.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vercel/node": { + "version": "3.2.29", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-3.2.29.tgz", + "integrity": "sha512-WRVYidBqtRyYUw36v/WyUB2v97PsiV2+LepUiOPWcW9UpszQGGT2DAzsXOYqWveXMJKFhx0aETR6Nn6i+Yps1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@edge-runtime/node-utils": "2.3.0", + "@edge-runtime/primitives": "4.1.0", + "@edge-runtime/vm": "3.2.0", + "@types/node": "16.18.11", + "@vercel/build-utils": "8.7.0", + "@vercel/error-utils": "2.0.3", + "@vercel/nft": "0.27.3", + "@vercel/static-config": "3.0.0", + "async-listen": "3.0.0", + "cjs-module-lexer": "1.2.3", + "edge-runtime": "2.5.9", + "es-module-lexer": "1.4.1", + "esbuild": "0.14.47", + "etag": "1.8.1", + "node-fetch": "2.6.9", + "path-to-regexp": "6.2.1", + "ts-morph": "12.0.0", + "ts-node": "10.9.1", + "typescript": "4.9.5", + "undici": "5.28.4" + } + }, + "node_modules/@vercel/node/node_modules/@types/node": { + "version": "16.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.11.tgz", + "integrity": "sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/node/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@vercel/python": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@vercel/python/-/python-4.7.1.tgz", + "integrity": "sha512-H4g/5e8unII4oQ+KN5IUvTZSzHmj+lLYDkAK15QGYgAxBtE/mHUvEZpPPo7DPUDIyfq8ybWB1bmk7H5kEahubQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/redwood": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@vercel/redwood/-/redwood-2.1.13.tgz", + "integrity": "sha512-e+4odfP2akWQq3WQ8mBkjqqwUcOvjhYmAhfg66IqTdIG15tIY6EOTMx/DhqXlvSDCyBbZPcqHb4/Xe662yPiEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/nft": "0.27.10", + "@vercel/routing-utils": "5.0.1", + "@vercel/static-config": "3.0.0", + "semver": "6.3.1", + "ts-morph": "12.0.0" + } + }, + "node_modules/@vercel/redwood/node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/redwood/node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vercel/redwood/node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@vercel/redwood/node_modules/@vercel/nft": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.10.tgz", + "integrity": "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0-rc.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vercel/redwood/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@vercel/redwood/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vercel/redwood/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/redwood/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vercel/redwood/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@vercel/redwood/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@vercel/redwood/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@vercel/redwood/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vercel/redwood/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/@vercel/redwood/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/redwood/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/remix-builder": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vercel/remix-builder/-/remix-builder-5.1.1.tgz", + "integrity": "sha512-OP1f6GI8MdylL4aUrX6n7OkN93jqmkWyLzQMeQMapVOXKvRFj05STZ4SQ/kNJkXdh3rEzjJWuCsJ6bklTHkJ7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/error-utils": "2.0.3", + "@vercel/nft": "0.27.10", + "@vercel/static-config": "3.0.0", + "ts-morph": "12.0.0" + } + }, + "node_modules/@vercel/remix-builder/node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/remix-builder/node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@vercel/remix-builder/node_modules/@vercel/nft": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.10.tgz", + "integrity": "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0-rc.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vercel/remix-builder/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@vercel/remix-builder/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vercel/remix-builder/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/remix-builder/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vercel/remix-builder/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@vercel/remix-builder/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@vercel/remix-builder/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@vercel/remix-builder/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vercel/remix-builder/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/remix-builder/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/routing-utils": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vercel/routing-utils/-/routing-utils-5.0.1.tgz", + "integrity": "sha512-CH8sulzI8VNySWyJP+536fEX+oBnRuIVpw79jrn/0JwgCl7xb6E2JkKrMBT/mUCkZXh4vZZIOt23/QiIRK9Dyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "path-to-regexp": "6.1.0", + "path-to-regexp-updated": "npm:path-to-regexp@6.3.0" + }, + "optionalDependencies": { + "ajv": "^6.12.3" + } + }, + "node_modules/@vercel/routing-utils/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@vercel/routing-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@vercel/routing-utils/node_modules/path-to-regexp": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", + "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/ruby": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vercel/ruby/-/ruby-2.2.0.tgz", + "integrity": "sha512-FJF9gKVNHAljGOgV6zS5ou2N7ZgjOqMMtcPA5lsJEUI5/AZzVDWCmtcowTP80wEtHuupkd7d7M399FA082kXYQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/static-build": { + "version": "2.5.43", + "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.5.43.tgz", + "integrity": "sha512-r6Pi/yC1nUCuq6V7xDxfMKDkwla4qnqpJVohd7cTsWRDKlRzHJJX/YaDp/6yKrDaNH9UY6cBhj9ryL8QJWY63w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", + "@vercel/gatsby-plugin-vercel-builder": "2.0.65", + "@vercel/static-config": "3.0.0", + "ts-morph": "12.0.0" + } + }, + "node_modules/@vercel/static-config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.0.0.tgz", + "integrity": "sha512-2qtvcBJ1bGY0dYGYh3iM7yGKkk971FujLEDXzuW5wcZsPr1GSEjO/w2iSr3qve6nDDtBImsGoDEnus5FI4+fIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "8.6.3", + "json-schema-to-ts": "1.6.4", + "ts-morph": "12.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-listen": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.0.tgz", + "integrity": "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/chokidar": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.0.tgz", + "integrity": "sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/code-block-writer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", + "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-hrtime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-3.0.0.tgz", + "integrity": "sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.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/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/edge-runtime": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz", + "integrity": "sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@edge-runtime/format": "2.2.1", + "@edge-runtime/ponyfill": "2.4.2", + "@edge-runtime/vm": "3.2.0", + "async-listen": "3.0.1", + "mri": "1.2.0", + "picocolors": "1.0.0", + "pretty-ms": "7.0.1", + "signal-exit": "4.0.2", + "time-span": "4.0.0" + }, + "bin": { + "edge-runtime": "dist/cli/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/edge-runtime/node_modules/async-listen": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.1.tgz", + "integrity": "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.47.tgz", + "integrity": "sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.47", + "esbuild-android-arm64": "0.14.47", + "esbuild-darwin-64": "0.14.47", + "esbuild-darwin-arm64": "0.14.47", + "esbuild-freebsd-64": "0.14.47", + "esbuild-freebsd-arm64": "0.14.47", + "esbuild-linux-32": "0.14.47", + "esbuild-linux-64": "0.14.47", + "esbuild-linux-arm": "0.14.47", + "esbuild-linux-arm64": "0.14.47", + "esbuild-linux-mips64le": "0.14.47", + "esbuild-linux-ppc64le": "0.14.47", + "esbuild-linux-riscv64": "0.14.47", + "esbuild-linux-s390x": "0.14.47", + "esbuild-netbsd-64": "0.14.47", + "esbuild-openbsd-64": "0.14.47", + "esbuild-sunos-64": "0.14.47", + "esbuild-windows-32": "0.14.47", + "esbuild-windows-64": "0.14.47", + "esbuild-windows-arm64": "0.14.47" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz", + "integrity": "sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz", + "integrity": "sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz", + "integrity": "sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz", + "integrity": "sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz", + "integrity": "sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz", + "integrity": "sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz", + "integrity": "sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz", + "integrity": "sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz", + "integrity": "sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz", + "integrity": "sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz", + "integrity": "sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz", + "integrity": "sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz", + "integrity": "sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz", + "integrity": "sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz", + "integrity": "sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz", + "integrity": "sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz", + "integrity": "sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz", + "integrity": "sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz", + "integrity": "sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.47", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz", + "integrity": "sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-intercept": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/events-intercept/-/events-intercept-2.0.0.tgz", + "integrity": "sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.2.0.tgz", + "integrity": "sha512-kJJfVbI/lZE1PZYDI5VPxp8zXPO9rtxOkhpZ0jMKha56AI9y2gGVC6bkukStQf0ka5Rh15BA5m7cCCH4jmHqkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": "^8.12.0 || >=9.7.0" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/generic-pool": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz", + "integrity": "sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-errors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz", + "integrity": "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "2.0.1", + "statuses": ">= 1.2.1 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", + "dev": true, + "license": "ISC" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-to-ts": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-1.6.4.tgz", + "integrity": "sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.6", + "ts-toolbelt": "^6.15.5" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micro": { + "version": "9.3.5-canary.3", + "resolved": "https://registry.npmjs.org/micro/-/micro-9.3.5-canary.3.tgz", + "integrity": "sha512-viYIo9PefV+w9dvoIBh1gI44Mvx1BOk67B4BpC2QK77qdY0xZF0Q+vWLt/BII6cLkIc8rLmSIcJaB/OrXXKe1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "arg": "4.1.0", + "content-type": "1.0.4", + "raw-body": "2.4.1" + }, + "bin": { + "micro": "bin/micro.js" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/micro/node_modules/arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-paths": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/os-paths/-/os-paths-4.4.0.tgz", + "integrity": "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0" + } + }, + "node_modules/p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-match": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/path-match/-/path-match-1.2.4.tgz", + "integrity": "sha512-UWlehEdqu36jmh4h5CWJ7tARp1OEVKGHKm6+dg9qMq5RKUTV5WJrGgaZ3dN2m7WFAXDbjlHzvJvL/IUpy84Ktw==", + "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "dev": true, + "license": "MIT", + "dependencies": { + "http-errors": "~1.4.0", + "path-to-regexp": "^1.0.0" + } + }, + "node_modules/path-match/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp-updated": { + "name": "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/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/promisepipe": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/promisepipe/-/promisepipe-3.0.0.tgz", + "integrity": "sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stat-mode": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.3.0.tgz", + "integrity": "sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-to-array": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", + "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.1.0" + } + }, + "node_modules/stream-to-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-to-promise/-/stream-to-promise-2.2.0.tgz", + "integrity": "sha512-HAGUASw8NT0k8JvIVutB2Y/9iBk7gpgEyAudXwNJmZERdMITGdajOa4VJfD/kNiA3TppQpTP4J+CtcHwdzKBAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "~1.3.0", + "end-of-stream": "~1.1.0", + "stream-to-array": "~2.3.0" + } + }, + "node_modules/stream-to-promise/node_modules/end-of-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz", + "integrity": "sha512-EoulkdKF/1xa92q25PbjuDcgJ9RDHYU2Rs3SCIvs2/dSQ3BpmxneNHmA/M7fe60M3PrV7nNGTTNbkK62l6vXiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/stream-to-promise/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/time-span": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-4.0.0.tgz", + "integrity": "sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-hrtime": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "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/ts-morph": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-12.0.0.tgz", + "integrity": "sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.11.0", + "code-block-writer": "^10.1.1" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-toolbelt": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", + "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uid-promise/-/uid-promise-1.0.0.tgz", + "integrity": "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vercel": { + "version": "39.4.2", + "resolved": "https://registry.npmjs.org/vercel/-/vercel-39.4.2.tgz", + "integrity": "sha512-A3ilkwJ83xLwAYAI733hHthJ4DO0zXjQOvCWS9QYklWQTBEj0RllyRkrfGd2jypgNDZuAbDS/iFMsV+GuuaTHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/build-utils": "9.1.0", + "@vercel/fun": "1.1.2", + "@vercel/go": "3.2.1", + "@vercel/hydrogen": "1.0.11", + "@vercel/next": "4.4.4", + "@vercel/node": "5.0.4", + "@vercel/python": "4.7.1", + "@vercel/redwood": "2.1.13", + "@vercel/remix-builder": "5.1.1", + "@vercel/ruby": "2.2.0", + "@vercel/static-build": "2.5.43", + "chokidar": "4.0.0" + }, + "bin": { + "vc": "dist/vc.js", + "vercel": "dist/vc.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/vercel/node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/vercel/node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/vercel/node_modules/@types/node": { + "version": "16.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.11.tgz", + "integrity": "sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vercel/node_modules/@vercel/build-utils": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-9.1.0.tgz", + "integrity": "sha512-ccknvdKH6LDB9ZzZaX8a8cOvFbI441APLHvKrunJE/wezY0skmfuEUK1qnfPApXMs4FMWzZQj2LO9qpzfgBPsQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/vercel/node_modules/@vercel/nft": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.10.tgz", + "integrity": "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0-rc.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/vercel/node_modules/@vercel/node": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.0.4.tgz", + "integrity": "sha512-AXpTFDzomabvi/FmxDDTwmnuqRBDfy2i0nzjKwVPM3ch94EucPbiAk3+18iZOX/A+o2mBO4jKc1DmB0ifQF2Rw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@edge-runtime/node-utils": "2.3.0", + "@edge-runtime/primitives": "4.1.0", + "@edge-runtime/vm": "3.2.0", + "@types/node": "16.18.11", + "@vercel/build-utils": "9.1.0", + "@vercel/error-utils": "2.0.3", + "@vercel/nft": "0.27.10", + "@vercel/static-config": "3.0.0", + "async-listen": "3.0.0", + "cjs-module-lexer": "1.2.3", + "edge-runtime": "2.5.9", + "es-module-lexer": "1.4.1", + "esbuild": "0.14.47", + "etag": "1.8.1", + "node-fetch": "2.6.9", + "path-to-regexp": "6.2.1", + "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", + "ts-morph": "12.0.0", + "ts-node": "10.9.1", + "typescript": "4.9.5", + "undici": "5.28.4" + } + }, + "node_modules/vercel/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vercel/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/vercel/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/vercel/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/vercel/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/vercel/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/vercel/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vercel/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vercel/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/vercel/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vercel/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/web-vitals": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz", + "integrity": "sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xdg-app-paths": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-app-paths/-/xdg-app-paths-5.1.0.tgz", + "integrity": "sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xdg-portable": "^7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/xdg-portable": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/xdg-portable/-/xdg-portable-7.3.0.tgz", + "integrity": "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-paths": "^4.0.1" + }, + "engines": { + "node": ">= 6.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl-clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/yauzl-clone/-/yauzl-clone-1.0.4.tgz", + "integrity": "sha512-igM2RRCf3k8TvZoxR2oguuw4z1xasOnA31joCqHIyLkeWrvAc2Jgay5ISQ2ZplinkoGaJ6orCz56Ey456c5ESA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-intercept": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yauzl-promise": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yauzl-promise/-/yauzl-promise-2.1.3.tgz", + "integrity": "sha512-A1pf6fzh6eYkK0L4Qp7g9jzJSDrM6nN0bOn5T0IbY4Yo3w+YkWlHFkJP7mzknMXjqusHFHlKsK2N+4OLsK2MRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "yauzl": "^2.9.1", + "yauzl-clone": "^1.0.4" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/github-cors-proxy/package.json b/github-cors-proxy/package.json new file mode 100644 index 000000000..e6a6188c1 --- /dev/null +++ b/github-cors-proxy/package.json @@ -0,0 +1,18 @@ +{ + "name": "github-cors-proxy", + "version": "0.1.0", + "private": true, + "description": "Vercel-deployable CORS proxy for Git smart HTTP requests.", + "scripts": { + "dev": "vercel dev", + "deploy": "vercel deploy", + "deploy:prod": "vercel deploy --prod", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "^20.17.0", + "@vercel/node": "^3.2.0", + "typescript": "^5.6.0", + "vercel": "^39.0.0" + } +} diff --git a/github-cors-proxy/tsconfig.json b/github-cors-proxy/tsconfig.json new file mode 100644 index 000000000..8e42bfaa7 --- /dev/null +++ b/github-cors-proxy/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["api/**/*.ts"] +} diff --git a/github-cors-proxy/vercel.json b/github-cors-proxy/vercel.json new file mode 100644 index 000000000..5a07ec4ed --- /dev/null +++ b/github-cors-proxy/vercel.json @@ -0,0 +1,13 @@ +{ + "rewrites": [ + { + "source": "/((?!api/).*)", + "destination": "/api/git/:path*" + } + ], + "functions": { + "api/**/*.ts": { + "maxDuration": 60 + } + } +} diff --git a/grit-lib/src/commit.rs b/grit-lib/src/commit.rs new file mode 100644 index 000000000..707e14fbc --- /dev/null +++ b/grit-lib/src/commit.rs @@ -0,0 +1,77 @@ +//! Commit object writing helpers. + +use crate::error::{Error, Result}; +use crate::objects::{serialize_commit, CommitData, ObjectId, ObjectKind}; +use crate::storage::ObjectWriter; + +/// Write a commit object to `store` and return its object ID. +/// +/// This validates that the referenced tree exists and is a tree object, and +/// that every listed parent exists and is a commit object. The caller provides +/// complete author and committer identity lines, including timestamp and +/// timezone. +/// +/// # Errors +/// +/// Returns an error when the tree or a parent is missing, has the wrong object +/// kind, or when the store cannot write the new commit object. +pub fn write_commit(store: &mut impl ObjectWriter, commit: &CommitData) -> Result { + ensure_object_kind(store, &commit.tree, ObjectKind::Tree, "commit tree")?; + for parent in &commit.parents { + ensure_object_kind(store, parent, ObjectKind::Commit, "commit parent")?; + } + store.write_object(ObjectKind::Commit, &serialize_commit(commit)) +} + +fn ensure_object_kind( + store: &impl ObjectWriter, + oid: &ObjectId, + expected: ObjectKind, + label: &str, +) -> Result<()> { + let object = store.read_object(oid)?; + if object.kind != expected { + return Err(Error::CorruptObject(format!( + "{label} {oid} is {}, expected {expected}", + object.kind + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use tempfile::TempDir; + + use super::*; + use crate::odb::Odb; + use crate::storage::ObjectReader; + + #[test] + fn writes_commit_through_object_store() { + let temp_dir = TempDir::new().expect("temp dir"); + let mut odb = Odb::new(temp_dir.path()); + let tree = odb + .write_object(ObjectKind::Tree, b"") + .expect("write empty tree"); + let commit = CommitData { + tree, + parents: Vec::new(), + author: "A U Thor 1 +0000".to_string(), + committer: "C O Mitter 2 +0000".to_string(), + author_raw: Vec::new(), + committer_raw: Vec::new(), + encoding: None, + message: "initial\n".to_string(), + raw_message: None, + }; + + let oid = write_commit(&mut odb, &commit).expect("write commit"); + let stored = odb.read_object(&oid).expect("read commit"); + + assert_eq!(stored.kind, ObjectKind::Commit); + assert!(String::from_utf8_lossy(&stored.data).contains("initial\n")); + } +} diff --git a/grit-lib/src/lib.rs b/grit-lib/src/lib.rs index facc5ab90..62b2f765b 100644 --- a/grit-lib/src/lib.rs +++ b/grit-lib/src/lib.rs @@ -15,90 +15,200 @@ //! - [`ignore`] — ignore/exclude pattern matching for check-ignore //! - [`refs`] — reference storage (files backend) +#[cfg(target_arch = "wasm32")] +pub mod commit; +#[cfg(target_arch = "wasm32")] +pub mod commit_encoding; +#[cfg(target_arch = "wasm32")] +pub mod error; +#[cfg(target_arch = "wasm32")] +pub mod objects; +#[cfg(target_arch = "wasm32")] +pub mod pack_write; +#[cfg(target_arch = "wasm32")] +pub mod pkt_line; +#[cfg(target_arch = "wasm32")] +pub mod smart_protocol; +#[cfg(target_arch = "wasm32")] +pub mod storage; +#[cfg(target_arch = "wasm32")] +pub mod unpack_objects; + +#[cfg(not(target_arch = "wasm32"))] pub mod attributes; +#[cfg(not(target_arch = "wasm32"))] pub mod bloom; +#[cfg(not(target_arch = "wasm32"))] pub mod check_ref_format; +#[cfg(not(target_arch = "wasm32"))] pub mod combined_diff_patch; +#[cfg(not(target_arch = "wasm32"))] pub mod combined_tree_diff; +#[cfg(not(target_arch = "wasm32"))] +pub mod commit; +#[cfg(not(target_arch = "wasm32"))] pub mod commit_encoding; +#[cfg(not(target_arch = "wasm32"))] pub mod commit_graph_file; +#[cfg(not(target_arch = "wasm32"))] pub mod commit_graph_write; +#[cfg(not(target_arch = "wasm32"))] pub mod commit_pretty; +#[cfg(not(target_arch = "wasm32"))] pub mod commit_trailers; +#[cfg(not(target_arch = "wasm32"))] pub mod config; +#[cfg(not(target_arch = "wasm32"))] pub mod connectivity; +#[cfg(not(target_arch = "wasm32"))] pub mod crlf; +#[cfg(not(target_arch = "wasm32"))] pub mod delta_encode; +#[cfg(not(target_arch = "wasm32"))] pub mod diff; +#[cfg(not(target_arch = "wasm32"))] mod diff_indent_heuristic; +#[cfg(not(target_arch = "wasm32"))] pub mod diffstat; +#[cfg(not(target_arch = "wasm32"))] pub mod error; +#[cfg(not(target_arch = "wasm32"))] mod ewah_bitmap; +#[cfg(not(target_arch = "wasm32"))] pub mod fast_export; +#[cfg(not(target_arch = "wasm32"))] pub mod fast_import; +#[cfg(not(target_arch = "wasm32"))] pub mod fetch_head; +#[cfg(not(target_arch = "wasm32"))] pub mod fetch_negotiator; +#[cfg(not(target_arch = "wasm32"))] pub mod fetch_submodules; +#[cfg(not(target_arch = "wasm32"))] pub mod filter_process; +#[cfg(not(target_arch = "wasm32"))] pub mod fmt_merge_msg; +#[cfg(not(target_arch = "wasm32"))] pub mod fsck_standalone; +#[cfg(not(target_arch = "wasm32"))] pub mod git_date; +#[cfg(not(target_arch = "wasm32"))] pub mod gitmodules; +#[cfg(not(target_arch = "wasm32"))] pub mod hide_refs; +#[cfg(not(target_arch = "wasm32"))] pub mod hooks; +#[cfg(not(target_arch = "wasm32"))] pub mod ident; +#[cfg(not(target_arch = "wasm32"))] pub mod ident_config; +#[cfg(not(target_arch = "wasm32"))] pub mod ignore; +#[cfg(not(target_arch = "wasm32"))] pub mod index; +#[cfg(not(target_arch = "wasm32"))] pub mod index_name_hash_lazy; +#[cfg(not(target_arch = "wasm32"))] pub mod interpret_trailers; +#[cfg(not(target_arch = "wasm32"))] pub mod line_log; +#[cfg(not(target_arch = "wasm32"))] pub mod ls_remote; +#[cfg(not(target_arch = "wasm32"))] pub mod mailinfo; +#[cfg(not(target_arch = "wasm32"))] pub mod mailmap; +#[cfg(not(target_arch = "wasm32"))] pub mod merge_base; +#[cfg(not(target_arch = "wasm32"))] pub mod merge_diff; +#[cfg(not(target_arch = "wasm32"))] pub mod merge_file; +#[cfg(not(target_arch = "wasm32"))] pub mod merge_tree_trivial; +#[cfg(not(target_arch = "wasm32"))] pub mod merge_trees; +#[cfg(not(target_arch = "wasm32"))] pub mod mergetool_vimdiff; +#[cfg(not(target_arch = "wasm32"))] pub mod midx; +#[cfg(not(target_arch = "wasm32"))] pub mod name_rev; +#[cfg(not(target_arch = "wasm32"))] pub mod objects; +#[cfg(not(target_arch = "wasm32"))] pub mod odb; +#[cfg(not(target_arch = "wasm32"))] pub mod pack; +#[cfg(not(target_arch = "wasm32"))] pub mod pack_geometry; +#[cfg(not(target_arch = "wasm32"))] pub mod pack_name_hash; +#[cfg(not(target_arch = "wasm32"))] pub mod pack_rev; +#[cfg(not(target_arch = "wasm32"))] +pub mod pack_write; +#[cfg(not(target_arch = "wasm32"))] pub mod parse_options_test_tool; +#[cfg(not(target_arch = "wasm32"))] pub mod patch_ids; +#[cfg(not(target_arch = "wasm32"))] pub mod path_walk; +#[cfg(not(target_arch = "wasm32"))] pub mod pathspec; +#[cfg(not(target_arch = "wasm32"))] +pub mod pkt_line; +#[cfg(not(target_arch = "wasm32"))] pub mod precompose_config; +#[cfg(not(target_arch = "wasm32"))] pub mod promisor; +#[cfg(not(target_arch = "wasm32"))] pub mod prune_packed; +#[cfg(not(target_arch = "wasm32"))] pub mod push_submodules; +#[cfg(not(target_arch = "wasm32"))] pub mod quote_path; +#[cfg(not(target_arch = "wasm32"))] pub mod ref_exclusions; +#[cfg(not(target_arch = "wasm32"))] pub mod ref_namespace; +#[cfg(not(target_arch = "wasm32"))] pub mod reflog; +#[cfg(not(target_arch = "wasm32"))] pub mod refs; +#[cfg(not(target_arch = "wasm32"))] pub mod refs_fsck; +#[cfg(not(target_arch = "wasm32"))] pub mod reftable; +#[cfg(not(target_arch = "wasm32"))] pub mod repo; +#[cfg(not(target_arch = "wasm32"))] pub mod rerere; +#[cfg(not(target_arch = "wasm32"))] pub mod resolve_undo; +#[cfg(not(target_arch = "wasm32"))] pub mod rev_list; +#[cfg(not(target_arch = "wasm32"))] pub mod rev_parse; +#[cfg(not(target_arch = "wasm32"))] pub mod shallow; +#[cfg(not(target_arch = "wasm32"))] pub mod shared_repo; -#[cfg(unix)] +#[cfg(all(unix, not(target_arch = "wasm32")))] pub mod simple_ipc; +#[cfg(not(target_arch = "wasm32"))] +pub mod smart_protocol; +#[cfg(not(target_arch = "wasm32"))] pub mod sparse_checkout; +#[cfg(not(target_arch = "wasm32"))] pub mod split_index; +#[cfg(not(target_arch = "wasm32"))] +pub mod storage; +#[cfg(not(target_arch = "wasm32"))] pub mod unicode_normalization; +#[cfg(not(target_arch = "wasm32"))] pub mod untracked_cache; -#[cfg(not(unix))] +#[cfg(all(not(unix), not(target_arch = "wasm32")))] pub mod simple_ipc { /// Whether simple IPC is supported on this platform. #[must_use] @@ -112,22 +222,39 @@ pub mod simple_ipc { 1 } } +#[cfg(not(target_arch = "wasm32"))] pub mod state; +#[cfg(not(target_arch = "wasm32"))] pub mod stripspace; +#[cfg(not(target_arch = "wasm32"))] pub mod submodule_active; +#[cfg(not(target_arch = "wasm32"))] pub mod submodule_config; +#[cfg(not(target_arch = "wasm32"))] pub mod submodule_config_cache; +#[cfg(not(target_arch = "wasm32"))] pub mod submodule_gitdir; +#[cfg(not(target_arch = "wasm32"))] pub mod tab_expand; +#[cfg(not(target_arch = "wasm32"))] pub mod test_tool_progress; +#[cfg(not(target_arch = "wasm32"))] pub mod textconv_cache; +#[cfg(not(target_arch = "wasm32"))] pub mod tree_path_follow; -#[cfg(unix)] +#[cfg(all(unix, not(target_arch = "wasm32")))] pub mod unix_process; +#[cfg(not(target_arch = "wasm32"))] pub mod unpack_objects; +#[cfg(not(target_arch = "wasm32"))] pub mod userdiff; +#[cfg(not(target_arch = "wasm32"))] pub mod whitespace_rule; +#[cfg(not(target_arch = "wasm32"))] pub mod wildmatch; +#[cfg(not(target_arch = "wasm32"))] pub mod worktree_cwd; +#[cfg(not(target_arch = "wasm32"))] pub mod write_tree; +#[cfg(not(target_arch = "wasm32"))] pub mod ws; diff --git a/grit-lib/src/pack_write.rs b/grit-lib/src/pack_write.rs new file mode 100644 index 000000000..cca0d1abd --- /dev/null +++ b/grit-lib/src/pack_write.rs @@ -0,0 +1,332 @@ +//! Minimal PACK v2 writing. +//! +//! This module intentionally starts with correctness-first full-object packs: +//! no deltas, no thin packs, and no reachability walk. Higher-level callers can +//! choose the object IDs to include. + +use std::collections::HashSet; +use std::io::Write; + +use flate2::write::ZlibEncoder; +use flate2::Compression; +use sha1::{Digest, Sha1}; + +use crate::error::{Error, Result}; +use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind}; +use crate::storage::ObjectReader; + +/// Options for writing PACK files. +#[derive(Clone, Copy, Debug, Default)] +pub struct PackWriteOptions { + /// Placeholder for future delta support. The current writer always emits + /// full objects and returns an error when this is set. + pub use_deltas: bool, + /// Placeholder for future thin-pack support. The current writer always emits + /// self-contained packs and returns an error when this is set. + pub thin: bool, +} + +/// Summary of a written pack. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PackWriteSummary { + /// Number of objects written to the pack. + pub object_count: usize, + /// SHA-1 trailer of the pack contents. + pub trailer: [u8; 20], +} + +/// Write a PACK v2 containing the full objects named by `object_ids`. +/// +/// Duplicate object IDs are skipped while preserving first-seen order. +/// +/// # Errors +/// +/// Returns an error when an object is missing, writing fails, or unsupported +/// options such as deltas/thin packs are requested. +pub fn write_pack( + store: &impl ObjectReader, + object_ids: &[ObjectId], + out: &mut W, + options: PackWriteOptions, +) -> Result { + if options.use_deltas { + return Err(Error::Message( + "delta pack writing is not implemented".to_string(), + )); + } + if options.thin { + return Err(Error::Message( + "thin pack writing is not implemented".to_string(), + )); + } + + let mut seen = HashSet::new(); + let mut objects = Vec::new(); + for oid in object_ids { + if seen.insert(*oid) { + let object = store.read_object(oid)?; + objects.push(object); + } + } + + let mut bytes = Vec::new(); + bytes.extend_from_slice(b"PACK"); + bytes.extend_from_slice(&2u32.to_be_bytes()); + bytes.extend_from_slice(&(objects.len() as u32).to_be_bytes()); + for object in &objects { + write_object_entry(&mut bytes, object.kind, &object.data)?; + } + let trailer = Sha1::digest(&bytes); + bytes.extend_from_slice(&trailer); + out.write_all(&bytes).map_err(Error::Io)?; + + let mut trailer_bytes = [0u8; 20]; + trailer_bytes.copy_from_slice(&trailer); + Ok(PackWriteSummary { + object_count: objects.len(), + trailer: trailer_bytes, + }) +} + +/// Select objects reachable from `tips` and not reachable from `exclude_tips`. +/// +/// The result is in traversal order with duplicates removed. It includes the +/// starting tip objects themselves. +/// +/// # Errors +/// +/// Returns an error when any traversed object is missing or malformed. +pub fn objects_for_push_pack( + store: &impl ObjectReader, + tips: &[ObjectId], + exclude_tips: &[ObjectId], +) -> Result> { + let excluded = reachable_set(store, exclude_tips)?; + let mut seen = HashSet::new(); + let mut out = Vec::new(); + for tip in tips { + collect_reachable(store, *tip, &excluded, &mut seen, &mut out)?; + } + Ok(out) +} + +fn reachable_set(store: &impl ObjectReader, tips: &[ObjectId]) -> Result> { + let excluded = HashSet::new(); + let mut seen = HashSet::new(); + let mut out = Vec::new(); + for tip in tips { + collect_reachable(store, *tip, &excluded, &mut seen, &mut out)?; + } + Ok(seen) +} + +fn collect_reachable( + store: &impl ObjectReader, + oid: ObjectId, + excluded: &HashSet, + seen: &mut HashSet, + out: &mut Vec, +) -> Result<()> { + if excluded.contains(&oid) || !seen.insert(oid) { + return Ok(()); + } + let object = store.read_object(&oid)?; + out.push(oid); + match object.kind { + ObjectKind::Commit => { + let commit = parse_commit(&object.data)?; + collect_reachable(store, commit.tree, excluded, seen, out)?; + for parent in commit.parents { + collect_reachable(store, parent, excluded, seen, out)?; + } + } + ObjectKind::Tree => { + for entry in parse_tree(&object.data)? { + collect_reachable(store, entry.oid, excluded, seen, out)?; + } + } + ObjectKind::Tag => { + let tag = parse_tag(&object.data)?; + collect_reachable(store, tag.object, excluded, seen, out)?; + } + ObjectKind::Blob => {} + } + Ok(()) +} + +fn write_object_entry(out: &mut Vec, kind: ObjectKind, data: &[u8]) -> Result<()> { + write_type_and_size(out, object_type_code(kind), data.len()); + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder + .write_all(data) + .map_err(|err| Error::Zlib(err.to_string()))?; + let compressed = encoder + .finish() + .map_err(|err| Error::Zlib(err.to_string()))?; + out.extend_from_slice(&compressed); + Ok(()) +} + +fn object_type_code(kind: ObjectKind) -> u8 { + match kind { + ObjectKind::Commit => 1, + ObjectKind::Tree => 2, + ObjectKind::Blob => 3, + ObjectKind::Tag => 4, + } +} + +fn write_type_and_size(out: &mut Vec, type_code: u8, mut size: usize) { + let mut byte = ((type_code & 0x07) << 4) | ((size & 0x0f) as u8); + size >>= 4; + if size != 0 { + byte |= 0x80; + } + out.push(byte); + while size != 0 { + let mut next = (size & 0x7f) as u8; + size >>= 7; + if size != 0 { + next |= 0x80; + } + out.push(next); + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::io::Cursor; + + use crate::objects::{serialize_commit, serialize_tree, CommitData, Object, TreeEntry}; + use crate::storage::{object_id_for, ObjectWriter}; + use crate::unpack_objects::{unpack_objects_into_store, UnpackOptions}; + + use super::*; + + #[derive(Default)] + struct MemoryStore { + objects: BTreeMap, + } + + impl ObjectReader for MemoryStore { + fn read_object(&self, oid: &ObjectId) -> Result { + self.objects + .get(oid) + .cloned() + .ok_or_else(|| Error::ObjectNotFound(oid.to_hex())) + } + } + + impl ObjectWriter for MemoryStore { + fn write_object(&mut self, kind: ObjectKind, data: &[u8]) -> Result { + let oid = object_id_for(kind, data); + self.objects.insert(oid, Object::new(kind, data.to_vec())); + Ok(oid) + } + } + + #[test] + fn writes_pack_that_unpack_objects_can_read() { + let mut source = MemoryStore::default(); + let blob = source.write_object(ObjectKind::Blob, b"hello").unwrap(); + let tree = source.write_object(ObjectKind::Tree, b"").unwrap(); + let mut pack = Vec::new(); + + let summary = write_pack( + &source, + &[blob, tree, blob], + &mut pack, + PackWriteOptions::default(), + ) + .unwrap(); + + assert_eq!(summary.object_count, 2); + assert_eq!(&pack[0..4], b"PACK"); + assert_eq!(u32::from_be_bytes(pack[4..8].try_into().unwrap()), 2); + assert_eq!(u32::from_be_bytes(pack[8..12].try_into().unwrap()), 2); + + let mut dest = MemoryStore::default(); + let count = + unpack_objects_into_store(&mut Cursor::new(pack), &mut dest, &UnpackOptions::default()) + .unwrap(); + assert_eq!(count, 2); + assert_eq!(dest.read_object(&blob).unwrap().data, b"hello"); + assert_eq!(dest.read_object(&tree).unwrap().kind, ObjectKind::Tree); + } + + #[test] + fn selects_reachable_objects_excluding_old_tip() { + let mut source = MemoryStore::default(); + let old_blob = source.write_object(ObjectKind::Blob, b"old").unwrap(); + let old_tree = source + .write_object( + ObjectKind::Tree, + &serialize_tree(&[TreeEntry { + mode: 0o100644, + name: b"old.txt".to_vec(), + oid: old_blob, + }]), + ) + .unwrap(); + let old_commit = source + .write_object( + ObjectKind::Commit, + &serialize_commit(&CommitData { + tree: old_tree, + parents: Vec::new(), + author: "A 1 +0000".to_string(), + committer: "C 1 +0000".to_string(), + author_raw: Vec::new(), + committer_raw: Vec::new(), + encoding: None, + message: "old\n".to_string(), + raw_message: None, + }), + ) + .unwrap(); + let new_blob = source.write_object(ObjectKind::Blob, b"new").unwrap(); + let new_tree = source + .write_object( + ObjectKind::Tree, + &serialize_tree(&[ + TreeEntry { + mode: 0o100644, + name: b"old.txt".to_vec(), + oid: old_blob, + }, + TreeEntry { + mode: 0o100644, + name: b"new.txt".to_vec(), + oid: new_blob, + }, + ]), + ) + .unwrap(); + let new_commit = source + .write_object( + ObjectKind::Commit, + &serialize_commit(&CommitData { + tree: new_tree, + parents: vec![old_commit], + author: "A 2 +0000".to_string(), + committer: "C 2 +0000".to_string(), + author_raw: Vec::new(), + committer_raw: Vec::new(), + encoding: None, + message: "new\n".to_string(), + raw_message: None, + }), + ) + .unwrap(); + + let selected = objects_for_push_pack(&source, &[new_commit], &[old_commit]).unwrap(); + + assert!(selected.contains(&new_commit)); + assert!(selected.contains(&new_tree)); + assert!(selected.contains(&new_blob)); + assert!(!selected.contains(&old_commit)); + assert!(!selected.contains(&old_tree)); + assert!(!selected.contains(&old_blob)); + } +} diff --git a/grit-lib/src/pkt_line.rs b/grit-lib/src/pkt_line.rs new file mode 100644 index 000000000..9e2f4072a --- /dev/null +++ b/grit-lib/src/pkt_line.rs @@ -0,0 +1,214 @@ +//! Git pkt-line and sideband wire-format helpers. + +use std::io::{self, Read, Write}; + +/// Flush packet marker. +pub const FLUSH: &str = "0000"; +/// Delimiter packet marker. +pub const DELIM: &str = "0001"; +/// Response-end packet marker. +pub const RESPONSE_END: &str = "0002"; + +/// A single packet read from the wire. +#[derive(Debug, PartialEq, Eq)] +pub enum Packet { + /// A data line, decoded lossily as UTF-8 and without a trailing newline. + Data(String), + /// Flush packet (`0000`). + Flush, + /// Delimiter packet (`0001`). + Delim, + /// Response-end packet (`0002`). + ResponseEnd, +} + +/// Write a single pkt-line to `w`, appending a trailing newline. +pub fn write_line(w: &mut impl Write, data: &str) -> io::Result<()> { + let len = 4 + data.len() + 1; + writeln!(w, "{len:04x}{data}") +} + +/// Append a pkt-line encoding of `data` to `buf`, appending a trailing newline. +pub fn write_line_to_vec(buf: &mut Vec, data: &str) -> io::Result<()> { + let len = 4 + data.len() + 1; + let line = format!("{len:04x}{data}\n"); + buf.extend_from_slice(line.as_bytes()); + Ok(()) +} + +/// Write a flush packet. +pub fn write_flush(w: &mut impl Write) -> io::Result<()> { + write!(w, "{FLUSH}") +} + +/// Write a delimiter packet. +pub fn write_delim(w: &mut impl Write) -> io::Result<()> { + write!(w, "{DELIM}") +} + +/// Write one pkt-line with arbitrary bytes and no appended newline. +pub fn write_packet_raw(w: &mut impl Write, payload: &[u8]) -> io::Result<()> { + let total = payload + .len() + .checked_add(4) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "pkt-line payload too large"))?; + if total > 65520 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "pkt-line exceeds maximum size", + )); + } + write!(w, "{total:04x}")?; + w.write_all(payload) +} + +/// Read one pkt-line from `r`, returning `None` at EOF. +pub fn read_packet(r: &mut impl Read) -> io::Result> { + let mut len_buf = [0u8; 4]; + match r.read_exact(&mut len_buf) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e), + } + let len = parse_hex_len(&len_buf)?; + + match len { + 0 => Ok(Some(Packet::Flush)), + 1 => Ok(Some(Packet::Delim)), + 2 => Ok(Some(Packet::ResponseEnd)), + n if n <= 4 => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid pkt-line length: {n}"), + )), + n => { + let payload_len = n - 4; + let mut buf = vec![0u8; payload_len]; + r.read_exact(&mut buf)?; + let s = String::from_utf8_lossy(&buf).into_owned(); + Ok(Some(Packet::Data( + s.strip_suffix('\n').unwrap_or(&s).to_owned(), + ))) + } + } +} + +/// Read packets until a flush, delimiter, response-end, or EOF. +pub fn read_until_flush_or_delim(r: &mut impl Read) -> io::Result<(Vec, Option)> { + let mut lines = Vec::new(); + loop { + match read_packet(r)? { + None => return Ok((lines, None)), + Some(Packet::Flush) => return Ok((lines, Some(Packet::Flush))), + Some(Packet::Delim) => return Ok((lines, Some(Packet::Delim))), + Some(Packet::ResponseEnd) => return Ok((lines, Some(Packet::ResponseEnd))), + Some(Packet::Data(s)) => lines.push(s), + } + } +} + +/// Read data pkt-lines until a flush packet. +pub fn read_data_lines_until_flush( + r: &mut impl Read, + err_not_flush: &str, +) -> io::Result> { + let mut lines = Vec::new(); + loop { + match read_packet(r)? { + None => { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + err_not_flush.to_string(), + )); + } + Some(Packet::Flush) => return Ok(lines), + Some(Packet::Delim | Packet::ResponseEnd) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + err_not_flush.to_string(), + )); + } + Some(Packet::Data(s)) => lines.push(s), + } + } +} + +/// Write payload on sideband channel 1 using Git's side-band-64k chunking. +pub fn write_sideband_channel1_64k(w: &mut impl Write, payload: &[u8]) -> io::Result<()> { + const MAX_PAYLOAD: usize = 65515; + for chunk in payload.chunks(MAX_PAYLOAD) { + write_sideband_packet(w, 1, chunk)?; + } + Ok(()) +} + +/// Decode a sideband pkt-line stream into the raw primary channel payload. +pub fn decode_sideband_primary(mut input: &[u8]) -> io::Result> { + let mut out = Vec::new(); + while !input.is_empty() { + if input.len() < 4 { + break; + } + let len = parse_hex_len(&input[..4])?; + input = &input[4..]; + if len == 0 { + break; + } + if len <= 4 || input.len() < len - 4 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "truncated sideband packet", + )); + } + let payload_len = len - 4; + let payload = &input[..payload_len]; + input = &input[payload_len..]; + if let Some((&1, data)) = payload.split_first() { + out.extend_from_slice(data); + } + } + Ok(out) +} + +fn write_sideband_packet(w: &mut impl Write, band: u8, payload: &[u8]) -> io::Result<()> { + let len = 4 + 1 + payload.len(); + write!(w, "{len:04x}")?; + w.write_all(&[band])?; + w.write_all(payload) +} + +fn parse_hex_len(prefix: &[u8]) -> io::Result { + let s = std::str::from_utf8(prefix) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{e}")))?; + usize::from_str_radix(s, 16) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("invalid length: {e}"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trips_basic_packets() { + let mut out = Vec::new(); + write_line_to_vec(&mut out, "hello").unwrap(); + write_delim(&mut out).unwrap(); + write_flush(&mut out).unwrap(); + + let mut cur = out.as_slice(); + assert_eq!( + read_packet(&mut cur).unwrap(), + Some(Packet::Data("hello".into())) + ); + assert_eq!(read_packet(&mut cur).unwrap(), Some(Packet::Delim)); + assert_eq!(read_packet(&mut cur).unwrap(), Some(Packet::Flush)); + } + + #[test] + fn decodes_sideband_primary() { + let mut out = Vec::new(); + write_sideband_channel1_64k(&mut out, b"abc").unwrap(); + write_flush(&mut out).unwrap(); + + assert_eq!(decode_sideband_primary(&out).unwrap(), b"abc"); + } +} diff --git a/grit-lib/src/smart_protocol.rs b/grit-lib/src/smart_protocol.rs new file mode 100644 index 000000000..b74cbc7f2 --- /dev/null +++ b/grit-lib/src/smart_protocol.rs @@ -0,0 +1,989 @@ +//! Transport-neutral smart Git protocol helpers. +//! +//! These helpers build request bodies and parse response bodies. HTTP, SSH, +//! browser `fetch`, and other transports remain responsible for moving bytes. + +use std::collections::HashSet; +use std::io::Cursor; + +use crate::error::{Error, Result}; +use crate::objects::ObjectId; +use crate::pkt_line::{self, Packet}; + +/// A single advertised reference from a smart protocol response. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AdvertisedRef { + /// Fully qualified ref name, or `HEAD`. + pub name: String, + /// Object ID currently stored at the reference. + pub oid: ObjectId, + /// Symbolic ref target advertised by protocol-v2 `ls-refs`, if present. + pub symref_target: Option, +} + +/// Parsed upload-pack discovery response. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum UploadPackAdvertisement { + /// Protocol v2 capability advertisement. + V2 { + /// Capability lines, including `version 2`. + caps: Vec, + /// Negotiated object format, usually `sha1`. + object_format: String, + }, + /// Protocol v0/v1 ref advertisement. + V0V1 { + /// Protocol version observed (`0` or `1`). + protocol_version: u8, + /// Advertised refs. + refs: Vec, + /// Capability strings from the first advertised ref. + capabilities: HashSet, + /// Negotiated object format, usually `sha1`. + object_format: String, + }, +} + +/// Parsed receive-pack discovery response. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReceivePackAdvertisement { + /// Protocol version observed (`0`, `1`, or `2`). + pub protocol_version: u8, + /// Advertised refs. Empty for protocol-v2 capability advertisements. + pub refs: Vec, + /// Capability strings. + pub capabilities: HashSet, + /// Negotiated object format, usually `sha1`. + pub object_format: String, +} + +impl ReceivePackAdvertisement { + /// Return true when an exact capability or key-value capability is advertised. + #[must_use] + pub fn supports(&self, capability: &str) -> bool { + self.capabilities + .iter() + .any(|c| c == capability || c.starts_with(&format!("{capability}="))) + } + + /// Return the advertised object ID for `refname`, if present. + #[must_use] + pub fn advertised_oid(&self, refname: &str) -> Option { + self.refs + .iter() + .find(|advertised| advertised.name == refname) + .map(|advertised| advertised.oid) + } +} + +/// Strip a smart-HTTP `# service=...` advertisement prefix when present. +/// +/// Smart HTTP v0/v1 discovery may start with a service header followed by a +/// flush packet. Protocol v2 responses start directly with `version 2`. +pub fn strip_http_service_advertisement_if_present(body: &[u8]) -> Result<&[u8]> { + let mut cur = Cursor::new(body); + let first = match pkt_line::read_packet(&mut cur)? { + None => return Ok(body), + Some(Packet::Data(line)) => line, + Some(Packet::Flush) => return Ok(body), + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected first smart-http packet: {other:?}" + ))); + } + }; + if first.starts_with("# service=") { + loop { + match pkt_line::read_packet(&mut cur)? { + None => { + return Err(Error::CorruptObject( + "unexpected EOF in smart-http service advertisement".to_owned(), + )); + } + Some(Packet::Flush) => { + let pos = cur.position() as usize; + return Ok(&body[pos..]); + } + Some(Packet::Data(_)) => {} + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected packet in smart-http service block: {other:?}" + ))); + } + } + } + } + Ok(body) +} + +/// Parse an upload-pack smart protocol discovery body after any HTTP service +/// advertisement prefix has been stripped. +pub fn parse_upload_pack_advertisement(body: &[u8]) -> Result { + let mut cur = Cursor::new(body); + let first = match pkt_line::read_packet(&mut cur)? { + None => { + return Err(Error::CorruptObject( + "empty smart protocol advertisement".to_owned(), + )); + } + Some(Packet::Data(line)) => line, + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected first advertisement packet: {other:?}" + ))); + } + }; + if first == "version 2" { + let caps = read_v2_caps_from_first(first, &mut cur)?; + let object_format = object_format_from_caps(caps.iter().map(String::as_str)); + return Ok(UploadPackAdvertisement::V2 { + caps, + object_format, + }); + } + + let (refs, capabilities) = parse_v0_v1_advertisement_from_first(first.clone(), &mut cur)?; + let protocol_version = if first == "version 1" { 1 } else { 0 }; + let object_format = object_format_from_caps(capabilities.iter().map(String::as_str)); + Ok(UploadPackAdvertisement::V0V1 { + protocol_version, + refs, + capabilities, + object_format, + }) +} + +/// Parse a receive-pack smart protocol discovery body after any HTTP service +/// advertisement prefix has been stripped. +pub fn parse_receive_pack_advertisement(body: &[u8]) -> Result { + let mut cur = Cursor::new(body); + let first = match pkt_line::read_packet(&mut cur)? { + None => { + return Err(Error::CorruptObject( + "empty receive-pack advertisement".to_owned(), + )); + } + Some(Packet::Data(line)) => line, + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected first receive-pack advertisement packet: {other:?}" + ))); + } + }; + if first == "version 2" { + let caps = read_v2_caps_from_first(first, &mut cur)?; + let object_format = object_format_from_caps(caps.iter().map(String::as_str)); + return Ok(ReceivePackAdvertisement { + protocol_version: 2, + refs: Vec::new(), + capabilities: caps.into_iter().collect(), + object_format, + }); + } + + let (refs, capabilities) = parse_v0_v1_advertisement_from_first(first.clone(), &mut cur)?; + let protocol_version = if first == "version 1" { 1 } else { 0 }; + let object_format = object_format_from_caps(capabilities.iter().map(String::as_str)); + Ok(ReceivePackAdvertisement { + protocol_version, + refs, + capabilities, + object_format, + }) +} + +/// Parse a protocol-v2 `ls-refs` response body. +pub fn parse_ls_refs_v2_response(data: &[u8]) -> Result> { + let mut cur = Cursor::new(data); + let mut out = Vec::new(); + loop { + let pkt = match pkt_line::read_packet(&mut cur)? { + None | Some(Packet::Flush) => break, + Some(Packet::Data(line)) => line, + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected ls-refs packet: {other:?}" + ))); + } + }; + let (oid_hex, rest) = pkt + .split_once(' ') + .ok_or_else(|| Error::CorruptObject(format!("bad ls-refs line: {pkt}")))?; + let oid = ObjectId::from_hex(oid_hex.trim())?; + let name = rest.split_whitespace().next().unwrap_or(rest).to_string(); + if !name.is_empty() { + let symref_target = rest + .split_whitespace() + .skip(1) + .find_map(|attr| attr.strip_prefix("symref-target:")) + .map(ToOwned::to_owned); + out.push(AdvertisedRef { + name, + oid, + symref_target, + }); + } + } + Ok(out) +} + +fn read_v2_caps_from_first(first: String, cur: &mut Cursor<&[u8]>) -> Result> { + if first != "version 2" { + return Err(Error::CorruptObject(format!( + "expected 'version 2', got {first:?}" + ))); + } + let mut caps = vec![first]; + loop { + match pkt_line::read_packet(cur)? { + None => { + return Err(Error::CorruptObject( + "unexpected EOF in v2 capabilities".to_owned(), + )); + } + Some(Packet::Flush) => break, + Some(Packet::Data(line)) => caps.push(line), + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected packet in v2 capabilities: {other:?}" + ))); + } + } + } + Ok(caps) +} + +fn parse_v0_v1_advertisement_from_first( + first: String, + cur: &mut Cursor<&[u8]>, +) -> Result<(Vec, HashSet)> { + let mut refs = Vec::new(); + let mut capabilities = HashSet::new(); + let mut first_ref_line = true; + let mut pending = Some(first); + loop { + let line = match pending.take() { + Some(line) => line, + None => match pkt_line::read_packet(cur)? { + None | Some(Packet::Flush) => break, + Some(Packet::Data(line)) => line, + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected packet in v0/v1 advertisement: {other:?}" + ))); + } + }, + }; + let line = line.trim_end_matches('\n'); + if line.starts_with("version ") { + continue; + } + let (payload, cap_part) = match line.split_once('\0') { + Some((payload, caps)) => (payload.trim(), Some(caps)), + None => (line.trim(), None), + }; + let (oid_hex, refname) = payload + .split_once('\t') + .or_else(|| payload.split_once(' ')) + .ok_or_else(|| { + Error::CorruptObject(format!("malformed v0/v1 advertisement: {line}")) + })?; + let oid = ObjectId::from_hex(oid_hex.trim())?; + let refname = refname.trim(); + if first_ref_line { + if let Some(raw_caps) = cap_part { + for cap in raw_caps.split_whitespace() { + capabilities.insert(cap.to_string()); + } + } + first_ref_line = false; + } + if !refname.is_empty() { + refs.push(AdvertisedRef { + name: refname.to_string(), + oid, + symref_target: None, + }); + } + } + Ok((refs, capabilities)) +} + +fn object_format_from_caps<'a>(caps: impl Iterator) -> String { + caps.filter_map(|cap| cap.strip_prefix("object-format=")) + .next() + .unwrap_or("sha1") + .to_string() +} + +/// Return capability lines that a client should echo before the command delimiter. +#[must_use] +pub fn capability_lines_for_client_request(caps: &[String]) -> Vec { + let mut out = Vec::new(); + for line in caps { + if line.starts_with("agent=") { + out.push(line.clone()); + } else if let Some(fmt) = line.strip_prefix("object-format=") { + out.push(format!("object-format={fmt}")); + } + } + out +} + +/// Extract feature names from a protocol-v2 `fetch=` capability. +#[must_use] +pub fn fetch_features_from_caps(caps: &[String]) -> HashSet { + let mut features = HashSet::new(); + for line in caps { + if let Some(rest) = line.strip_prefix("fetch=") { + for feature in rest.split_whitespace() { + features.insert(feature.to_string()); + } + } + } + features +} + +/// Options for protocol-v2 upload-pack `fetch` requests. +#[derive(Clone, Debug, Default)] +pub struct FetchRequestOptions { + /// Object format to request, usually `sha1`. + pub object_format: String, + /// Capabilities to echo before the delimiter, such as `agent=...`. + pub capability_lines: Vec, + /// Object IDs requested with `want`. + pub wants: Vec, + /// Local object IDs sent as `have`. + pub haves: Vec, + /// Whether to include `done`. + pub include_done: bool, + /// Absolute depth requested by `deepen`. + pub depth: Option, + /// Date boundary requested by `deepen-since`. + pub shallow_since: Option, + /// Exclusion revisions requested by `deepen-not`. + pub shallow_exclude: Vec, + /// Partial clone filter specification, such as `blob:none`. + pub filter_spec: Option, +} + +/// Build a protocol-v2 `ls-refs` request body. +/// +/// `capability_lines` are written before the delimiter. `object_format` +/// defaults to `sha1` when empty. +pub fn build_ls_refs_v2_request( + object_format: &str, + capability_lines: &[String], + peel: bool, + symrefs: bool, +) -> Result> { + let object_format = if object_format.trim().is_empty() { + "sha1" + } else { + object_format.trim() + }; + let mut req = Vec::new(); + pkt_line::write_line_to_vec(&mut req, "command=ls-refs")?; + pkt_line::write_line_to_vec(&mut req, &format!("object-format={object_format}"))?; + for line in capability_lines { + if !line.trim().is_empty() { + pkt_line::write_line_to_vec(&mut req, line)?; + } + } + pkt_line::write_delim(&mut req)?; + if peel { + pkt_line::write_line_to_vec(&mut req, "peel")?; + } + if symrefs { + pkt_line::write_line_to_vec(&mut req, "symrefs")?; + } + pkt_line::write_flush(&mut req)?; + Ok(req) +} + +/// Build a protocol-v2 upload-pack `fetch` request body. +/// +/// `server_fetch_features` should contain feature names advertised in the +/// server's `fetch=` capability, for example `filter` or `shallow`. +pub fn build_fetch_v2_request( + server_fetch_features: &HashSet, + options: &FetchRequestOptions, +) -> Result> { + if options.wants.is_empty() { + return Err(Error::Message( + "cannot build fetch request without wants".to_owned(), + )); + } + let object_format = if options.object_format.trim().is_empty() { + "sha1" + } else { + options.object_format.trim() + }; + let mut req = Vec::new(); + pkt_line::write_line_to_vec(&mut req, "command=fetch")?; + pkt_line::write_line_to_vec(&mut req, &format!("object-format={object_format}"))?; + for line in &options.capability_lines { + if !line.trim().is_empty() { + pkt_line::write_line_to_vec(&mut req, line)?; + } + } + pkt_line::write_delim(&mut req)?; + for want in &options.wants { + pkt_line::write_line_to_vec(&mut req, &format!("want {}", want.to_hex()))?; + } + append_fetch_extensions(&mut req, server_fetch_features, options)?; + for have in &options.haves { + pkt_line::write_line_to_vec(&mut req, &format!("have {}", have.to_hex()))?; + } + if options.include_done { + pkt_line::write_line_to_vec(&mut req, "done")?; + } + pkt_line::write_flush(&mut req)?; + Ok(req) +} + +/// Extract pack bytes from a protocol-v2 upload-pack `fetch` response. +/// +/// The response may include acknowledgement sections before the `packfile` +/// marker. Once `packfile` is found, the remainder is decoded as sideband and +/// channel 1 is returned. +pub fn extract_packfile_from_fetch_response(data: &[u8]) -> Result> { + let mut cur = Cursor::new(data); + loop { + let pkt = match pkt_line::read_packet(&mut cur)? { + None | Some(Packet::Flush) => break, + Some(Packet::Delim | Packet::ResponseEnd) => continue, + Some(Packet::Data(line)) => line, + }; + if pkt == "packfile" { + let pos = cur.position() as usize; + let pack = pkt_line::decode_sideband_primary(&data[pos..])?; + if pack.is_empty() { + return Err(Error::CorruptObject( + "fetch response did not contain pack data".to_string(), + )); + } + return Ok(pack); + } + skip_section(&mut cur)?; + } + Err(Error::CorruptObject( + "fetch response did not contain a packfile section".to_string(), + )) +} + +fn skip_section(cur: &mut Cursor<&[u8]>) -> Result<()> { + loop { + match pkt_line::read_packet(cur)? { + None | Some(Packet::Flush) => return Ok(()), + Some(Packet::Data(_)) | Some(Packet::Delim | Packet::ResponseEnd) => {} + } + } +} + +fn append_fetch_extensions( + req: &mut Vec, + server_fetch_features: &HashSet, + options: &FetchRequestOptions, +) -> Result<()> { + if let Some(depth) = options.depth.filter(|depth| *depth > 0) { + pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?; + } + if let Some(since) = options.shallow_since.as_deref() { + if server_fetch_features.contains("deepen-since") + || server_fetch_features.contains("shallow") + { + pkt_line::write_line_to_vec(req, &format!("deepen-since {since}"))?; + } + } + if server_fetch_features.contains("deepen-not") || server_fetch_features.contains("shallow") { + for excl in &options.shallow_exclude { + let excl = excl.trim(); + if !excl.is_empty() { + pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?; + } + } + } + if server_fetch_features.contains("filter") { + if let Some(filter_spec) = options.filter_spec.as_deref() { + let filter_spec = filter_spec.trim(); + if !filter_spec.is_empty() { + pkt_line::write_line_to_vec(req, &format!("filter {filter_spec}"))?; + } + } + } + Ok(()) +} + +/// One reference update command sent to `git-receive-pack`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PushCommand { + /// Current old value expected on the remote (`None` means all-zero object id). + pub old_oid: Option, + /// New value to update (`None` means delete). + pub new_oid: Option, + /// Fully qualified destination reference name. + pub refname: String, +} + +/// Receive-pack capabilities used by the client request builder. +#[derive(Clone, Debug, Default)] +pub struct ReceivePackCapabilities { + /// Raw advertised capability strings. + pub advertised: HashSet, + /// Agent value to send when the remote advertises `agent`. + pub agent: Option, + /// Session ID value to send when the remote advertises `session-id`. + pub session_id: Option, +} + +impl ReceivePackCapabilities { + /// Return whether an exact capability or key-value capability is advertised. + #[must_use] + pub fn supports(&self, capability: &str) -> bool { + self.advertised + .iter() + .any(|c| c == capability || c.starts_with(&format!("{capability}="))) + } +} + +/// One per-ref status line returned by the remote. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PushStatusEntry { + /// Updated reference. + pub refname: String, + /// Whether the update succeeded. + pub ok: bool, + /// Optional error text for rejected updates. + pub message: Option, +} + +/// Parsed `report-status` response for a push request. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PushStatusReport { + /// Whether the remote unpack phase succeeded. + pub unpack_ok: bool, + /// Unpack status message returned by remote. + pub unpack_message: String, + /// Per-reference status entries. + pub statuses: Vec, + /// Sideband progress/error bytes from remote channels 2 and 3. + pub sideband_stderr: Vec, +} + +/// Build a `git-receive-pack` request and return `(request_bytes, use_sideband)`. +pub fn build_receive_pack_request( + capabilities: &ReceivePackCapabilities, + commands: &[PushCommand], + push_options: &[String], + pack_data: &[u8], + atomic: bool, +) -> Result<(Vec, bool)> { + if commands.is_empty() { + return Err(Error::Message( + "cannot push without update commands".to_owned(), + )); + } + let caps = client_push_capabilities(capabilities, atomic, push_options)?; + let mut request = Vec::new(); + for (idx, cmd) in commands.iter().enumerate() { + let old_hex = format_push_old_new(cmd.old_oid); + let new_hex = format_push_old_new(cmd.new_oid); + let mut payload = format!("{old_hex} {new_hex} {}", cmd.refname); + if idx == 0 && !caps.is_empty() { + payload.push('\0'); + payload.push_str(&caps.join(" ")); + } + payload.push('\n'); + pkt_line::write_packet_raw(&mut request, payload.as_bytes())?; + } + pkt_line::write_flush(&mut request)?; + + if !push_options.is_empty() { + for opt in push_options { + pkt_line::write_line_to_vec(&mut request, opt)?; + } + pkt_line::write_flush(&mut request)?; + } + + let delete_only = commands.iter().all(|cmd| cmd.new_oid.is_none()); + if !delete_only { + request.extend_from_slice(pack_data); + } + + let use_sideband = caps + .iter() + .any(|cap| cap == "side-band-64k" || cap == "side-band"); + Ok((request, use_sideband)) +} + +/// Parse a receive-pack response body into a status report. +pub fn parse_receive_pack_response( + response: &[u8], + use_sideband: bool, +) -> Result { + let (primary, sideband_stderr) = if use_sideband { + decode_sideband_stream(response)? + } else { + (response.to_vec(), Vec::new()) + }; + let mut status = parse_report_status_body(&primary)?; + status.sideband_stderr = sideband_stderr; + Ok(status) +} + +fn client_push_capabilities( + capabilities: &ReceivePackCapabilities, + atomic: bool, + push_options: &[String], +) -> Result> { + let mut out = Vec::new(); + if capabilities.supports("report-status-v2") { + out.push("report-status-v2".to_string()); + } else if capabilities.supports("report-status") { + out.push("report-status".to_string()); + } else { + return Err(Error::Message( + "remote does not support report-status".to_owned(), + )); + } + if capabilities.supports("ofs-delta") { + out.push("ofs-delta".to_string()); + } + if capabilities.supports("side-band-64k") { + out.push("side-band-64k".to_string()); + } else if capabilities.supports("side-band") { + out.push("side-band".to_string()); + } + if atomic { + if !capabilities.supports("atomic") { + return Err(Error::Message( + "the receiving end does not support --atomic push".to_owned(), + )); + } + out.push("atomic".to_string()); + } + if !push_options.is_empty() { + if !capabilities.supports("push-options") { + return Err(Error::Message( + "the receiving end does not support push options".to_owned(), + )); + } + out.push("push-options".to_string()); + } + if capabilities.supports("agent") { + if let Some(agent) = capabilities.agent.as_deref() { + out.push(format!("agent={agent}")); + } + } + if capabilities.supports("object-format") { + out.push("object-format=sha1".to_string()); + } + if capabilities.supports("session-id") { + if let Some(session_id) = capabilities.session_id.as_deref() { + out.push(format!("session-id={session_id}")); + } + } + Ok(out) +} + +fn format_push_old_new(oid: Option) -> String { + oid.map(|oid| oid.to_hex()) + .unwrap_or_else(|| "0".repeat(40)) +} + +fn decode_sideband_stream(body: &[u8]) -> Result<(Vec, Vec)> { + let mut i = 0usize; + let mut primary = Vec::new(); + let mut stderr = Vec::new(); + while i + 4 <= body.len() { + let len_str = std::str::from_utf8(&body[i..i + 4]) + .map_err(|e| Error::CorruptObject(format!("invalid sideband length: {e}")))?; + let pkt_len = usize::from_str_radix(len_str, 16) + .map_err(|e| Error::CorruptObject(format!("invalid sideband length: {e}")))?; + i += 4; + if pkt_len == 0 { + break; + } + if pkt_len < 5 || i + (pkt_len - 4) > body.len() { + return Err(Error::CorruptObject( + "truncated sideband packet in push response".to_owned(), + )); + } + let payload = &body[i..i + (pkt_len - 4)]; + i += pkt_len - 4; + let (band, data) = (payload[0], &payload[1..]); + match band { + 1 => primary.extend_from_slice(data), + 2 | 3 => stderr.extend_from_slice(data), + _ => {} + } + } + Ok((primary, stderr)) +} + +fn parse_report_status_body(body: &[u8]) -> Result { + let mut cur = Cursor::new(body); + let unpack_line = match pkt_line::read_packet(&mut cur)? { + Some(Packet::Data(line)) => line, + Some(other) => { + return Err(Error::CorruptObject(format!( + "unexpected first report-status packet: {other:?}" + ))); + } + None => { + return Err(Error::CorruptObject( + "empty report-status response".to_owned(), + )) + } + }; + let unpack_line = unpack_line.trim_end_matches('\n').to_string(); + let unpack_ok = unpack_line == "unpack ok"; + let unpack_message = unpack_line + .strip_prefix("unpack ") + .unwrap_or(unpack_line.as_str()) + .to_string(); + + let mut statuses = Vec::new(); + loop { + match pkt_line::read_packet(&mut cur)? { + Some(Packet::Data(line)) => { + let line = line.trim_end_matches('\n'); + if let Some(rest) = line.strip_prefix("ok ") { + statuses.push(PushStatusEntry { + refname: rest.trim().to_string(), + ok: true, + message: None, + }); + continue; + } + if let Some(rest) = line.strip_prefix("ng ") { + let (refname, message) = rest + .split_once(' ') + .map(|(r, m)| (r.trim(), Some(m.trim().to_string()))) + .unwrap_or((rest.trim(), None)); + statuses.push(PushStatusEntry { + refname: refname.to_string(), + ok: false, + message, + }); + } + } + Some(Packet::Flush) | None => break, + Some(Packet::Delim | Packet::ResponseEnd) => {} + } + } + + Ok(PushStatusReport { + unpack_ok, + unpack_message, + statuses, + sideband_stderr: Vec::new(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn oid(hex: &str) -> ObjectId { + ObjectId::from_hex(hex).unwrap() + } + + #[test] + fn builds_blobless_fetch_request() { + let features = HashSet::from(["filter".to_string(), "shallow".to_string()]); + let request = build_fetch_v2_request( + &features, + &FetchRequestOptions { + object_format: "sha1".to_string(), + wants: vec![oid("67bf698f3ab735e92fb011a99cff3497c44d30c1")], + include_done: true, + filter_spec: Some("blob:none".to_string()), + ..FetchRequestOptions::default() + }, + ) + .unwrap(); + let text = String::from_utf8_lossy(&request); + + assert!(text.contains("command=fetch")); + assert!(text.contains("want 67bf698f3ab735e92fb011a99cff3497c44d30c1")); + assert!(text.contains("filter blob:none")); + assert!(text.contains("done")); + } + + #[test] + fn extracts_packfile_from_fetch_response() { + let mut response = Vec::new(); + pkt_line::write_line_to_vec(&mut response, "packfile").unwrap(); + pkt_line::write_sideband_channel1_64k(&mut response, b"PACK bytes").unwrap(); + pkt_line::write_flush(&mut response).unwrap(); + + let pack = extract_packfile_from_fetch_response(&response).unwrap(); + + assert_eq!(pack, b"PACK bytes"); + } + + #[test] + fn parses_upload_pack_v2_advertisement() { + let mut body = Vec::new(); + pkt_line::write_line_to_vec(&mut body, "version 2").unwrap(); + pkt_line::write_line_to_vec(&mut body, "agent=git/test").unwrap(); + pkt_line::write_line_to_vec(&mut body, "object-format=sha1").unwrap(); + pkt_line::write_line_to_vec(&mut body, "fetch=shallow filter").unwrap(); + pkt_line::write_flush(&mut body).unwrap(); + + let parsed = parse_upload_pack_advertisement(&body).unwrap(); + + match parsed { + UploadPackAdvertisement::V2 { + caps, + object_format, + } => { + assert_eq!(object_format, "sha1"); + assert!(caps.iter().any(|cap| cap == "fetch=shallow filter")); + } + UploadPackAdvertisement::V0V1 { .. } => panic!("expected v2 advertisement"), + } + } + + #[test] + fn strips_http_service_advertisement() { + let mut body = Vec::new(); + pkt_line::write_line_to_vec(&mut body, "# service=git-upload-pack").unwrap(); + pkt_line::write_flush(&mut body).unwrap(); + pkt_line::write_line_to_vec(&mut body, "version 2").unwrap(); + pkt_line::write_flush(&mut body).unwrap(); + + let stripped = strip_http_service_advertisement_if_present(&body).unwrap(); + + assert!(String::from_utf8_lossy(stripped).contains("version 2")); + } + + #[test] + fn parses_receive_pack_v0_advertisement() { + let mut body = Vec::new(); + pkt_line::write_packet_raw( + &mut body, + b"67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/heads/main\0report-status side-band-64k\n", + ) + .unwrap(); + pkt_line::write_flush(&mut body).unwrap(); + + let parsed = parse_receive_pack_advertisement(&body).unwrap(); + + assert_eq!(parsed.protocol_version, 0); + assert_eq!( + parsed.advertised_oid("refs/heads/main"), + Some(oid("67bf698f3ab735e92fb011a99cff3497c44d30c1")) + ); + assert!(parsed.supports("report-status")); + assert!(parsed.supports("side-band-64k")); + } + + #[test] + fn parses_ls_refs_response() { + let mut body = Vec::new(); + pkt_line::write_line_to_vec( + &mut body, + "67bf698f3ab735e92fb011a99cff3497c44d30c1 HEAD symref-target:refs/heads/main", + ) + .unwrap(); + pkt_line::write_flush(&mut body).unwrap(); + + let refs = parse_ls_refs_v2_response(&body).unwrap(); + + assert_eq!(refs.len(), 1); + assert_eq!(refs[0].name, "HEAD"); + assert_eq!(refs[0].symref_target.as_deref(), Some("refs/heads/main")); + } + + #[test] + fn builds_receive_pack_request_and_parses_status() { + let caps = ReceivePackCapabilities { + advertised: HashSet::from([ + "report-status".to_string(), + "side-band-64k".to_string(), + "agent".to_string(), + ]), + agent: Some("grit-test".to_string()), + session_id: None, + }; + let new_oid = oid("67bf698f3ab735e92fb011a99cff3497c44d30c1"); + let (request, use_sideband) = build_receive_pack_request( + &caps, + &[PushCommand { + old_oid: None, + new_oid: Some(new_oid), + refname: "refs/heads/main".to_string(), + }], + &[], + b"PACK", + false, + ) + .unwrap(); + let text = String::from_utf8_lossy(&request); + + assert!(use_sideband); + assert!(text.contains("refs/heads/main")); + assert!(text.contains("report-status side-band-64k agent=grit-test")); + assert!(request.ends_with(b"PACK")); + + let mut status_body = Vec::new(); + pkt_line::write_line_to_vec(&mut status_body, "unpack ok").unwrap(); + pkt_line::write_line_to_vec(&mut status_body, "ok refs/heads/main").unwrap(); + pkt_line::write_flush(&mut status_body).unwrap(); + let parsed = parse_receive_pack_response(&status_body, false).unwrap(); + + assert!(parsed.unpack_ok); + assert_eq!(parsed.statuses.len(), 1); + assert!(parsed.statuses[0].ok); + } + + #[test] + fn parses_service_wrapped_upload_pack_v2_fixture() { + let fixture = concat!( + "001e# service=git-upload-pack\n", + "0000", + "000eversion 2\n", + "0013agent=git/2.44\n", + "0017object-format=sha1\n", + "0019fetch=shallow filter\n", + "0000", + ) + .as_bytes(); + + let stripped = strip_http_service_advertisement_if_present(fixture).unwrap(); + let parsed = parse_upload_pack_advertisement(stripped).unwrap(); + + match parsed { + UploadPackAdvertisement::V2 { + caps, + object_format, + } => { + assert_eq!(object_format, "sha1"); + assert_eq!(caps[0], "version 2"); + assert!(caps.iter().any(|cap| cap == "fetch=shallow filter")); + } + UploadPackAdvertisement::V0V1 { .. } => panic!("expected protocol v2 fixture"), + } + } + + #[test] + fn parses_sidebanded_receive_pack_status_fixture() { + let mut primary = Vec::new(); + pkt_line::write_line_to_vec(&mut primary, "unpack ok").unwrap(); + pkt_line::write_line_to_vec(&mut primary, "ok refs/heads/main").unwrap(); + pkt_line::write_flush(&mut primary).unwrap(); + let mut sideband = Vec::new(); + pkt_line::write_sideband_channel1_64k(&mut sideband, &primary).unwrap(); + pkt_line::write_flush(&mut sideband).unwrap(); + + let parsed = parse_receive_pack_response(&sideband, true).unwrap(); + + assert!(parsed.unpack_ok); + assert_eq!(parsed.statuses.len(), 1); + assert_eq!(parsed.statuses[0].refname, "refs/heads/main"); + assert!(parsed.statuses[0].ok); + } +} diff --git a/grit-lib/src/storage.rs b/grit-lib/src/storage.rs new file mode 100644 index 000000000..ade717620 --- /dev/null +++ b/grit-lib/src/storage.rs @@ -0,0 +1,253 @@ +//! Storage traits for Git object access. +//! +//! These traits describe the object database operations needed by portable +//! library code without requiring a filesystem-backed [`crate::odb::Odb`]. + +use sha1::{Digest, Sha1}; + +use crate::error::{Error, Result}; +use crate::objects::{Object, ObjectId, ObjectKind}; + +/// Identifier for a stored pack. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PackId(String); + +impl PackId { + /// Construct a pack identifier from a display string. + #[must_use] + pub fn new(id: impl Into) -> Self { + Self(id.into()) + } + + /// Return the pack identifier as a string slice. + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// Summary metadata for a stored pack. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PackDescriptor { + /// Stable pack identifier inside this store. + pub id: PackId, + /// Raw pack byte length. + pub byte_len: usize, + /// Whether this pack came from a promisor remote. + pub promisor: bool, +} + +/// A stored Git reference target. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RefTarget { + /// Direct reference target, such as a branch pointing at a commit. + Direct(ObjectId), + /// Symbolic reference target, such as `HEAD` pointing at `refs/heads/main`. + Symbolic(String), +} + +/// Compute the Git object ID for an object kind and raw object body. +/// +/// The hash is over the canonical object storage bytes: +/// `" \0"`. +#[must_use] +pub fn object_id_for(kind: ObjectKind, data: &[u8]) -> ObjectId { + let header = format!("{} {}\0", kind.as_str(), data.len()); + let mut hasher = Sha1::new(); + hasher.update(header.as_bytes()); + hasher.update(data); + let digest = hasher.finalize(); + let bytes: [u8; 20] = digest.into(); + ObjectId::from_bytes(&bytes).unwrap_or_else(|_| ObjectId::zero()) +} + +/// Read-only object access for a Git object store. +pub trait ObjectReader { + /// Read the object named by `oid`. + /// + /// Returns an error when the object is missing or corrupt. + fn read_object(&self, oid: &ObjectId) -> Result; + + /// Return whether the object named by `oid` is available. + /// + /// Implementations may answer from indexes or metadata without reading the + /// full object body. + fn has_object(&self, oid: &ObjectId) -> Result { + match self.read_object(oid) { + Ok(_) => Ok(true), + Err(Error::ObjectNotFound(_)) => Ok(false), + Err(err) => Err(err), + } + } +} + +/// Mutable object access for a Git object store. +pub trait ObjectWriter: ObjectReader { + /// Write an object body under `kind` and return its object ID. + /// + /// Implementations must hash the canonical Git object storage bytes, not + /// just the raw body. + fn write_object(&mut self, kind: ObjectKind, data: &[u8]) -> Result; +} + +/// Full object store access. +pub trait ObjectStore: ObjectReader + ObjectWriter {} + +impl ObjectStore for T where T: ObjectReader + ObjectWriter {} + +/// Read-only access to a Git reference store. +pub trait RefReader { + /// Resolve `name` to an object ID. + /// + /// Symbolic refs are followed to their direct target. Missing refs return + /// `Ok(None)`. + fn resolve_ref(&self, name: &str) -> Result>; + + /// Read the symbolic target of `name`. + /// + /// Direct or missing refs return `Ok(None)`. + fn read_symbolic_ref(&self, name: &str) -> Result>; + + /// List direct object IDs for refs matching `prefix`. + /// + /// Symbolic refs may be included when they resolve to an object ID. + fn list_refs(&self, prefix: &str) -> Result>; +} + +/// Mutable access to a Git reference store. +pub trait RefWriter: RefReader { + /// Set `name` to the direct object ID `oid`. + fn set_ref(&mut self, name: &str, oid: ObjectId) -> Result<()>; + + /// Set `name` to the symbolic reference target `target`. + fn set_symbolic_ref(&mut self, name: &str, target: &str) -> Result<()>; + + /// Delete `name` from the reference store. + fn delete_ref(&mut self, name: &str) -> Result<()>; +} + +/// Full reference store access. +pub trait RefStore: RefReader + RefWriter {} + +impl RefStore for T where T: RefReader + RefWriter {} + +/// Read-only access to raw pack storage. +pub trait PackReader { + /// List packs known to this store. + fn list_packs(&self) -> Result>; + + /// Read raw pack bytes by `id`. + /// + /// Missing packs return `Ok(None)`. + fn read_pack(&self, id: &PackId) -> Result>>; +} + +/// Mutable access to raw pack storage. +pub trait PackWriter: PackReader { + /// Store raw pack bytes and return the assigned pack ID. + /// + /// The `promisor` flag records whether missing objects reachable from this + /// pack may be fetched later from the remote. + fn add_pack(&mut self, bytes: Vec, promisor: bool) -> Result; + + /// Delete a stored pack by `id`. + fn delete_pack(&mut self, id: &PackId) -> Result<()>; +} + +/// Full raw pack store access. +pub trait PackStore: PackReader + PackWriter {} + +impl PackStore for T where T: PackReader + PackWriter {} + +/// Compute a stable identifier for raw pack bytes. +/// +/// This is storage metadata, not a Git object ID. The identifier is the SHA-1 +/// of the complete pack byte stream. +#[must_use] +pub fn pack_id_for(bytes: &[u8]) -> PackId { + let mut hasher = Sha1::new(); + hasher.update(bytes); + PackId(hex::encode(hasher.finalize())) +} + +#[cfg(not(target_arch = "wasm32"))] +impl ObjectReader for crate::odb::Odb { + fn read_object(&self, oid: &ObjectId) -> Result { + self.read(oid) + } + + fn has_object(&self, oid: &ObjectId) -> Result { + Ok(self.exists(oid)) + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl ObjectWriter for crate::odb::Odb { + fn write_object(&mut self, kind: ObjectKind, data: &[u8]) -> Result { + self.write(kind, data) + } +} + +/// Native file-backed reference store adapter. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Clone, Debug)] +pub struct FileRefStore { + git_dir: std::path::PathBuf, +} + +#[cfg(not(target_arch = "wasm32"))] +impl FileRefStore { + /// Create a reference store rooted at `git_dir`. + /// + /// The path should point at the repository's `.git` directory, or at a bare + /// repository directory. + #[must_use] + pub fn new(git_dir: impl Into) -> Self { + Self { + git_dir: git_dir.into(), + } + } + + /// Return the git directory used by this store. + #[must_use] + pub fn git_dir(&self) -> &std::path::Path { + &self.git_dir + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl RefReader for FileRefStore { + fn resolve_ref(&self, name: &str) -> Result> { + match crate::refs::read_raw_ref(&self.git_dir, name)? { + crate::refs::RawRefLookup::NotFound | crate::refs::RawRefLookup::IsDirectory => { + Ok(None) + } + crate::refs::RawRefLookup::Exists => { + crate::refs::resolve_ref(&self.git_dir, name).map(Some) + } + } + } + + fn read_symbolic_ref(&self, name: &str) -> Result> { + crate::refs::read_symbolic_ref(&self.git_dir, name) + } + + fn list_refs(&self, prefix: &str) -> Result> { + crate::refs::list_refs(&self.git_dir, prefix) + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl RefWriter for FileRefStore { + fn set_ref(&mut self, name: &str, oid: ObjectId) -> Result<()> { + crate::refs::write_ref(&self.git_dir, name, &oid) + } + + fn set_symbolic_ref(&mut self, name: &str, target: &str) -> Result<()> { + crate::refs::write_symbolic_ref(&self.git_dir, name, target) + } + + fn delete_ref(&mut self, name: &str) -> Result<()> { + crate::refs::delete_ref(&self.git_dir, name) + } +} diff --git a/grit-lib/src/unpack_objects.rs b/grit-lib/src/unpack_objects.rs index 3b2fca239..a9fbb8c20 100644 --- a/grit-lib/src/unpack_objects.rs +++ b/grit-lib/src/unpack_objects.rs @@ -18,9 +18,14 @@ use flate2::{Decompress, FlushDecompress, Status}; use sha1::{Digest, Sha1}; use crate::error::{Error, Result}; +#[cfg(not(target_arch = "wasm32"))] use crate::gitmodules; -use crate::objects::{parse_commit, parse_tag, parse_tree, Object, ObjectId, ObjectKind}; +#[cfg(not(target_arch = "wasm32"))] +use crate::objects::Object; +use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind}; +#[cfg(not(target_arch = "wasm32"))] use crate::odb::Odb; +use crate::storage::{object_id_for, ObjectReader, ObjectWriter}; /// Options controlling `unpack-objects` behaviour. #[derive(Debug, Default)] @@ -65,7 +70,40 @@ struct PendingDelta { /// unresolvable delta chains. /// - [`Error::Io`] — I/O failure reading from `reader`. /// - [`Error::Zlib`] — decompression failure. +#[cfg(not(target_arch = "wasm32"))] pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> Result { + struct LocalOdbStore<'a>(&'a Odb); + + impl ObjectReader for LocalOdbStore<'_> { + fn read_object(&self, oid: &ObjectId) -> Result { + self.0.read(oid) + } + + fn has_object(&self, oid: &ObjectId) -> Result { + Ok(self.0.exists(oid)) + } + } + + impl ObjectWriter for LocalOdbStore<'_> { + fn write_object(&mut self, kind: ObjectKind, data: &[u8]) -> Result { + self.0.write_local(kind, data) + } + } + + let mut store = LocalOdbStore(odb); + unpack_objects_into_store(reader, &mut store, opts) +} + +/// Unpack a pack stream into a storage-agnostic object store. +/// +/// Reads the complete pack from `reader`, validates the trailing SHA-1 checksum, +/// resolves deltas, and writes each object through `store` unless +/// [`UnpackOptions::dry_run`] is set. +pub fn unpack_objects_into_store( + reader: &mut dyn Read, + store: &mut impl ObjectWriter, + opts: &UnpackOptions, +) -> Result { /// Blobs larger than this stay on disk only (after write) so huge packs do /// not retain every blob in RAM. Smaller objects are kept for delta bases /// and `--strict` graph walks without extra ODB reads. @@ -104,8 +142,8 @@ pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> 1..=4 => { let kind = type_code_to_kind(type_code)?; let data = rd.decompress(size)?; - let oid = write_or_hash(kind, &data, odb, opts.dry_run)?; - let entry = packed_entry_after_write(kind, data, oid, odb, opts, MAX_RETAIN_BYTES); + let oid = write_or_hash(kind, &data, store, opts.dry_run)?; + let entry = packed_entry_after_write(kind, data, oid, opts, MAX_RETAIN_BYTES); by_offset.insert(obj_offset, entry.clone()); by_oid.insert(oid, entry); count += 1; @@ -168,12 +206,13 @@ pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> if let Some(base_off) = delta.base_offset { by_offset .get(&base_off) - .map(|e| entry_object_bytes(e, odb).map(|d| (e.kind(), d))) + .map(|e| entry_object_bytes(e, store).map(|d| (e.kind(), d))) } else if let Some(ref base_id) = delta.base_oid { if let Some(e) = by_oid.get(base_id) { - Some(entry_object_bytes(e, odb).map(|d| (e.kind(), d))) + Some(entry_object_bytes(e, store).map(|d| (e.kind(), d))) } else if !opts.dry_run { - odb.read(base_id) + store + .read_object(base_id) .ok() .map(|obj| Ok((obj.kind, Cow::Owned(obj.data)))) } else { @@ -186,15 +225,9 @@ pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> match base_res { Some(Ok((base_kind, base_data))) => { let result = apply_delta(base_data.as_ref(), &delta.delta_data)?; - let oid = write_or_hash(base_kind, &result, odb, opts.dry_run)?; - let new_entry = packed_entry_after_write( - base_kind, - result, - oid, - odb, - opts, - MAX_RETAIN_BYTES, - ); + let oid = write_or_hash(base_kind, &result, store, opts.dry_run)?; + let new_entry = + packed_entry_after_write(base_kind, result, oid, opts, MAX_RETAIN_BYTES); by_offset.insert(delta.offset, new_entry.clone()); by_oid.insert(oid, new_entry); count += 1; @@ -220,12 +253,15 @@ pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> let kind = entry.kind(); let data = match entry { PackedObjectEntry::InMemory { data, .. } => data.clone(), - PackedObjectEntry::BlobOnDisk { oid: blob_oid } => odb.read(blob_oid)?.data, + PackedObjectEntry::BlobOnDisk { oid: blob_oid } => { + store.read_object(blob_oid)?.data + } }; dot_fsck_map.insert(*oid, (kind, data)); } + #[cfg(not(target_arch = "wasm32"))] gitmodules::verify_packed_dot_special(&dot_fsck_map)?; - strict_verify_packed_references_map(Some(odb), &by_oid)?; + strict_verify_packed_references_map(Some(&*store), &by_oid)?; } Ok(count) @@ -251,7 +287,6 @@ fn packed_entry_after_write( kind: ObjectKind, data: Vec, oid: ObjectId, - _odb: &Odb, opts: &UnpackOptions, max_retain: usize, ) -> PackedObjectEntry { @@ -262,15 +297,18 @@ fn packed_entry_after_write( } } -fn entry_object_bytes<'a>(entry: &'a PackedObjectEntry, odb: &Odb) -> Result> { +fn entry_object_bytes<'a>( + entry: &'a PackedObjectEntry, + store: &impl ObjectReader, +) -> Result> { match entry { PackedObjectEntry::InMemory { data, .. } => Ok(Cow::Borrowed(data.as_slice())), - PackedObjectEntry::BlobOnDisk { oid } => Ok(Cow::Owned(odb.read(oid)?.data)), + PackedObjectEntry::BlobOnDisk { oid } => Ok(Cow::Owned(store.read_object(oid)?.data)), } } -fn strict_verify_packed_references_map( - odb: Option<&Odb>, +fn strict_verify_packed_references_map( + store: Option<&S>, pack: &HashMap, ) -> Result<()> { for entry in pack.values() { @@ -279,7 +317,7 @@ fn strict_verify_packed_references_map( PackedObjectEntry::InMemory { kind, data } => match kind { ObjectKind::Tree => { for e in parse_tree(data)? { - if !strict_ref_resolves_map(&e.oid, pack, odb) { + if !strict_ref_resolves_map(&e.oid, pack, store) { return Err(Error::CorruptObject(format!( "strict: missing object {} referenced by tree", e.oid.to_hex() @@ -289,14 +327,14 @@ fn strict_verify_packed_references_map( } ObjectKind::Commit => { let c = parse_commit(data)?; - if !strict_ref_resolves_map(&c.tree, pack, odb) { + if !strict_ref_resolves_map(&c.tree, pack, store) { return Err(Error::CorruptObject(format!( "strict: missing tree {} referenced by commit", c.tree.to_hex() ))); } for p in &c.parents { - if !strict_ref_resolves_map(p, pack, odb) { + if !strict_ref_resolves_map(p, pack, store) { return Err(Error::CorruptObject(format!( "strict: missing parent {} referenced by commit", p.to_hex() @@ -306,7 +344,7 @@ fn strict_verify_packed_references_map( } ObjectKind::Tag => { let t = parse_tag(data)?; - if !strict_ref_resolves_map(&t.object, pack, odb) { + if !strict_ref_resolves_map(&t.object, pack, store) { return Err(Error::CorruptObject(format!( "strict: missing object {} referenced by tag", t.object.to_hex() @@ -320,14 +358,15 @@ fn strict_verify_packed_references_map( Ok(()) } -fn strict_ref_resolves_map( +fn strict_ref_resolves_map( oid: &ObjectId, pack: &HashMap, - odb: Option<&Odb>, + store: Option<&S>, ) -> bool { - pack.contains_key(oid) || odb.is_some_and(|o| o.exists(oid)) + pack.contains_key(oid) || store.is_some_and(|s| s.has_object(oid).unwrap_or(false)) } +#[cfg(not(target_arch = "wasm32"))] fn strict_ref_resolves( oid: &ObjectId, pack: &std::collections::HashMap)>, @@ -341,6 +380,7 @@ fn strict_ref_resolves( /// /// Use [`None`] for `odb` when indexing or unpacking in a context with no repository (Git allows /// `index-pack --strict` outside a work tree when the pack is self-contained). +#[cfg(not(target_arch = "wasm32"))] pub fn strict_verify_packed_references( odb: Option<&Odb>, pack: &HashMap)>, @@ -395,11 +435,13 @@ pub fn strict_verify_packed_references( /// applying a push to the permanent ODB. /// /// Thin-pack bases may be resolved from `odb` when they are not present in the pack. +#[cfg(not(target_arch = "wasm32"))] pub fn pack_bytes_to_object_map(data: &[u8], odb: &Odb) -> Result> { let rd = PackReader::new(data.to_vec()); build_pack_object_map(rd, odb) } +#[cfg(not(target_arch = "wasm32"))] fn build_pack_object_map(mut rd: PackReader, odb: &Odb) -> Result> { let sig = rd.read_exact(4)?; if sig != b"PACK" { @@ -531,13 +573,16 @@ fn build_pack_object_map(mut rd: PackReader, odb: &Odb) -> Result Result { +fn write_or_hash( + kind: ObjectKind, + data: &[u8], + store: &mut impl ObjectWriter, + dry_run: bool, +) -> Result { if dry_run { - Ok(Odb::hash_object_data(kind, data)) + Ok(object_id_for(kind, data)) } else { - // Always materialize into this ODB: objects reachable only via alternates must still be - // written locally (matches git unpack-objects; t5519-push-alternates). - odb.write_local(kind, data) + store.write_object(kind, data) } } @@ -555,11 +600,13 @@ fn type_code_to_kind(code: u8) -> Result { } /// Low-level cursor over a buffered pack byte stream (in-memory pack parsing). +#[cfg(not(target_arch = "wasm32"))] struct PackReader { data: Vec, pos: usize, } +#[cfg(not(target_arch = "wasm32"))] impl PackReader { fn new(data: Vec) -> Self { Self { data, pos: 0 } diff --git a/grit-lib/src/write_tree.rs b/grit-lib/src/write_tree.rs index d46a40da5..da32f58cc 100644 --- a/grit-lib/src/write_tree.rs +++ b/grit-lib/src/write_tree.rs @@ -8,14 +8,15 @@ use crate::index::{ }; use crate::objects::{parse_tree, serialize_tree, tree_entry_cmp, ObjectId, ObjectKind, TreeEntry}; use crate::odb::Odb; +use crate::storage::ObjectWriter; -fn ensure_empty_blob_for_intent_to_add(odb: &Odb, index: &Index) -> Result<()> { +fn ensure_empty_blob_for_intent_to_add(store: &mut impl ObjectWriter, index: &Index) -> Result<()> { if index .entries .iter() .any(|e| e.stage() == 0 && e.intent_to_add()) { - let _ = odb.write(ObjectKind::Blob, b"")?; + let _ = store.write_object(ObjectKind::Blob, b"")?; } Ok(()) } @@ -30,8 +31,19 @@ pub fn write_tree_from_index_subset( index: &Index, paths: &std::collections::HashSet>, ) -> Result { - ensure_empty_blob_for_intent_to_add(odb, index)?; + let mut store = odb.clone(); + write_tree_from_index_subset_in_store(&mut store, index, paths) +} +/// Build and write tree object(s) from a storage-agnostic object store. +/// +/// Only index entries whose path is listed in `paths` are included in the tree. +pub fn write_tree_from_index_subset_in_store( + store: &mut impl ObjectWriter, + index: &Index, + paths: &std::collections::HashSet>, +) -> Result { + ensure_empty_blob_for_intent_to_add(store, index)?; let mut entries: Vec<&IndexEntry> = index .entries .iter() @@ -43,13 +55,24 @@ pub fn write_tree_from_index_subset( }) .collect(); entries.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage()))); - build_tree(odb, &entries, b"") + build_tree(store, &entries, b"") } /// Build and write tree object(s) from index entries and return the tree OID. pub fn write_tree_from_index(odb: &Odb, index: &Index, prefix: &str) -> Result { - ensure_empty_blob_for_intent_to_add(odb, index)?; + let mut store = odb.clone(); + write_tree_from_index_in_store(&mut store, index, prefix) +} +/// Build and write tree object(s) from a storage-agnostic object store. +/// +/// The `prefix` argument optionally limits the write to a subtree path. +pub fn write_tree_from_index_in_store( + store: &mut impl ObjectWriter, + index: &Index, + prefix: &str, +) -> Result { + ensure_empty_blob_for_intent_to_add(store, index)?; let prefix_bytes = prefix.as_bytes(); let mut entries: Vec<&IndexEntry> = index .entries @@ -62,10 +85,14 @@ pub fn write_tree_from_index(odb: &Odb, index: &Index, prefix: &str) -> Result Result { +fn build_tree( + store: &mut impl ObjectWriter, + entries: &[&IndexEntry], + dir_prefix: &[u8], +) -> Result { let mut children: BTreeMap, ChildKind> = BTreeMap::new(); for entry in entries { @@ -107,7 +134,7 @@ fn build_tree(odb: &Odb, entries: &[&IndexEntry], dir_prefix: &[u8]) -> Result tree_entries.push(TreeEntry { mode, name, oid }), ChildKind::Tree(sub_prefix, sub_entries) => { - let sub_oid = build_tree(odb, &sub_entries, &sub_prefix)?; + let sub_oid = build_tree(store, &sub_entries, &sub_prefix)?; tree_entries.push(TreeEntry { mode: MODE_TREE, name, @@ -124,7 +151,7 @@ fn build_tree(odb: &Odb, entries: &[&IndexEntry], dir_prefix: &[u8]) -> Result>, ) -> Result { - let _ = odb.write(ObjectKind::Blob, b""); + let mut store = odb.clone(); + write_tree_partial_from_index_in_store(&mut store, index, base_tree_oid, paths_from_index) +} + +/// Build a partial commit tree from a storage-agnostic object store. +/// +/// Paths listed in `paths_from_index` are taken from `index`; every other path +/// is copied from `base_tree_oid`. +pub fn write_tree_partial_from_index_in_store( + store: &mut impl ObjectWriter, + index: &Index, + base_tree_oid: &ObjectId, + paths_from_index: &std::collections::HashSet>, +) -> Result { + let _ = store.write_object(ObjectKind::Blob, b""); fn full_path(prefix: &[u8], name: &[u8]) -> Vec { if prefix.is_empty() { @@ -158,14 +199,14 @@ pub fn write_tree_partial_from_index( .any(|p| p == dir || (p.starts_with(dir) && p.get(dir.len()) == Some(&b'/'))) } - fn merge_level( - odb: &Odb, + fn merge_level( + store: &mut S, index: &Index, base_tree_oid: &ObjectId, prefix: &[u8], paths_from_index: &std::collections::HashSet>, ) -> Result { - let base_obj = odb.read(base_tree_oid)?; + let base_obj = store.read_object(base_tree_oid)?; let base_entries = parse_tree(&base_obj.data)?; let mut by_name: BTreeMap, TreeEntry> = BTreeMap::new(); @@ -174,7 +215,7 @@ pub fn write_tree_partial_from_index( if !subtree_affected(paths_from_index, &fp) { by_name.insert(te.name.clone(), te); } else if te.mode == MODE_TREE { - let sub_oid = merge_level(odb, index, &te.oid, &fp, paths_from_index)?; + let sub_oid = merge_level(store, index, &te.oid, &fp, paths_from_index)?; by_name.insert( te.name.clone(), TreeEntry { @@ -225,8 +266,11 @@ pub fn write_tree_partial_from_index( continue; } let sub_prefix = full_path(prefix, &dir_name); - let sub_oid = - write_tree_from_index(odb, index, &String::from_utf8_lossy(&sub_prefix))?; + let sub_oid = write_tree_from_index_in_store( + store, + index, + &String::from_utf8_lossy(&sub_prefix), + )?; by_name.insert( dir_name.clone(), TreeEntry { @@ -257,10 +301,10 @@ pub fn write_tree_partial_from_index( tree_entry_cmp(&a.name, a_tree, &b.name, b_tree) }); let data = serialize_tree(&out); - odb.write(ObjectKind::Tree, &data) + store.write_object(ObjectKind::Tree, &data) } - merge_level(odb, index, base_tree_oid, b"", paths_from_index) + merge_level(store, index, base_tree_oid, b"", paths_from_index) } fn canonicalize_blob_mode(mode: u32) -> u32 { diff --git a/grit-wasm/Cargo.toml b/grit-wasm/Cargo.toml new file mode 100644 index 000000000..ad3e90f6e --- /dev/null +++ b/grit-wasm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "grit-wasm" +version.workspace = true +edition.workspace = true +license = "MIT" +description = "Browser WASM bindings for the grit Git implementation" +repository = "https://github.com/schacon/grit" + +[lib] +crate-type = ["cdylib", "rlib"] + +[lints] +workspace = true + +[dependencies] +base64.workspace = true +grit-lib = { version = "0.1.0", path = "../grit-lib" } +js-sys = "0.3.95" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +sha1 = { workspace = true } +url.workspace = true +wasm-bindgen = "0.2.118" +wasm-bindgen-futures = "0.4.68" +web-sys = { version = "0.3.95", features = ["DomException", "Event", "Headers", "IdbDatabase", "IdbFactory", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "Request", "RequestInit", "RequestMode", "Response", "Window"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3.68" diff --git a/grit-wasm/examples/github-log/README.md b/grit-wasm/examples/github-log/README.md new file mode 100644 index 000000000..d52ceb045 --- /dev/null +++ b/grit-wasm/examples/github-log/README.md @@ -0,0 +1,37 @@ +# Grit WASM Blobless Clone Demo + +This example loads the generated `grit-wasm` package, accepts a smart HTTP Git +URL, performs a blobless clone, and shows the latest five commit messages. + +## Build + +From the repository root: + +```bash +wasm-pack build grit-wasm --target web --out-dir examples/pkg +``` + +Then serve `grit-wasm/examples/github-log/` with any static file server. + +```bash +python3 -m http.server --directory grit-wasm/examples/github-log 8080 +``` + +Open . + +## CORS + +Browser smart HTTP requests require CORS. Public GitHub smart HTTP endpoints may +not allow direct browser fetches. For local testing, serve a repository with the +project test server: + +```bash +cargo build -p grit-rs --bin test-httpd +target/debug/test-httpd --root /tmp/git-http-root --cors --port 0 +``` + +Use a URL like: + +```text +http://127.0.0.1:/smart/repo.git +``` diff --git a/grit-wasm/examples/github-log/index.html b/grit-wasm/examples/github-log/index.html new file mode 100644 index 000000000..0eb512bad --- /dev/null +++ b/grit-wasm/examples/github-log/index.html @@ -0,0 +1,155 @@ + + + + + + Grit WASM Blobless Clone Demo + + + +

Grit WASM Blobless Clone Demo

+

+ Enter a smart HTTP Git URL. The page loads the Grit WASM package, performs + a blobless clone, and shows the latest five commit messages by walking + first parents from HEAD. +

+

+ Browser Git HTTP requests require CORS. GitHub repository URLs may be + blocked by the browser unless served through a CORS-enabled smart HTTP + endpoint or proxy. For local testing, use test-httpd --cors. +

+ +
+
+ + +
+
+ + +
+ +
+ +

Waiting for input.

+ +
    + + + + diff --git a/grit-wasm/src/bindings.rs b/grit-wasm/src/bindings.rs new file mode 100644 index 000000000..85f767ef9 --- /dev/null +++ b/grit-wasm/src/bindings.rs @@ -0,0 +1,292 @@ +//! `wasm-bindgen` exports for browser callers. + +use wasm_bindgen::prelude::*; + +use grit_lib::objects::parse_commit; +use grit_lib::storage::{ObjectReader, RefReader}; + +use crate::WasmRepository; + +/// Browser-facing in-memory Grit repository. +#[wasm_bindgen] +pub struct BrowserRepository { + inner: WasmRepository, +} + +#[wasm_bindgen] +impl BrowserRepository { + /// Create an empty in-memory repository. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + inner: WasmRepository::new(), + } + } + + /// Return the compiled package version. + #[wasm_bindgen] + pub fn version() -> String { + WasmRepository::version().to_string() + } + + /// Return the number of stored objects. + #[wasm_bindgen(js_name = objectCount)] + pub fn object_count(&self) -> usize { + self.inner.object_count() + } + + /// Return the number of staged file entries. + #[wasm_bindgen(js_name = stagedCount)] + pub fn staged_count(&self) -> usize { + self.inner.staged_count() + } + + /// Return the number of stored raw packs. + #[wasm_bindgen(js_name = packCount)] + pub fn pack_count(&self) -> usize { + self.inner.pack_count() + } + + /// Return whether an object exists. + #[wasm_bindgen(js_name = hasObject)] + pub fn has_object(&self, oid_hex: &str) -> bool { + self.inner.has_object_hex(oid_hex) + } + + /// Write a blob and return its hex object ID. + #[wasm_bindgen(js_name = writeBlob)] + pub fn write_blob(&mut self, data: &[u8]) -> String { + self.inner.write_blob(data) + } + + /// Read a blob by object ID. + #[wasm_bindgen(js_name = readBlob)] + pub fn read_blob(&self, oid_hex: &str) -> Result, JsValue> { + self.inner + .read_blob(oid_hex) + .ok_or_else(|| JsValue::from_str("blob not found")) + } + + /// Stage a file from bytes and return the blob object ID. + #[wasm_bindgen(js_name = stageFile)] + pub fn stage_file( + &mut self, + path: &str, + data: &[u8], + executable: bool, + ) -> Result { + self.inner + .stage_file(path, data, executable) + .map_err(|err| JsValue::from_str(&err.to_string())) + } + + /// Commit staged files and update the current branch. + #[wasm_bindgen(js_name = commitStaged)] + pub fn commit_staged( + &mut self, + message: &str, + author: &str, + committer: &str, + ) -> Result { + self.inner + .commit_staged(message, author, committer) + .map_err(|err| JsValue::from_str(&err.to_string())) + } + + /// Set symbolic `HEAD`. + #[wasm_bindgen(js_name = setHead)] + pub fn set_head(&mut self, refname: &str) { + self.inner.set_head(refname); + } + + /// Return symbolic `HEAD`, if set. + #[wasm_bindgen] + pub fn head(&self) -> Option { + self.inner.head() + } + + /// Return a ref's object ID, if it resolves. + #[wasm_bindgen(js_name = getRef)] + pub fn get_ref(&self, name: &str) -> Option { + self.inner.get_ref(name) + } + + /// Write a non-delta pack for an explicit object list. + #[wasm_bindgen(js_name = writePackForOids)] + pub fn write_pack_for_oids(&self, oid_hexes: Vec) -> Result, JsValue> { + self.inner + .write_pack_for_oids(oid_hexes) + .map_err(|err| JsValue::from_str(&err.to_string())) + } + + /// Blobless-clone a smart HTTP remote into this in-memory repository. + #[wasm_bindgen(js_name = cloneBlobless)] + pub async fn clone_blobless( + &mut self, + repo_url: String, + branch_or_ref: Option, + ) -> Result { + crate::remote::clone_blobless(&mut self.inner, &repo_url, branch_or_ref) + .await + .map(Into::into) + } + + /// Return recent commits reachable by following first parents from `HEAD`. + #[wasm_bindgen(js_name = recentCommits)] + pub fn recent_commits(&self, limit: usize) -> Result { + let head = self + .inner + .refs + .resolve_ref("HEAD") + .map_err(|err| JsValue::from_str(&err.to_string()))? + .ok_or_else(|| JsValue::from_str("HEAD is not set"))?; + let out = js_sys::Array::new(); + let mut current = Some(head); + for _ in 0..limit { + let Some(oid) = current else { + break; + }; + let object = self + .inner + .objects + .read_object(&oid) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let commit = + parse_commit(&object.data).map_err(|err| JsValue::from_str(&err.to_string()))?; + let entry = js_sys::Object::new(); + js_sys::Reflect::set( + &entry, + &JsValue::from_str("oid"), + &JsValue::from_str(&oid.to_hex()), + )?; + js_sys::Reflect::set( + &entry, + &JsValue::from_str("summary"), + &JsValue::from_str(commit_summary(&commit.message)), + )?; + js_sys::Reflect::set( + &entry, + &JsValue::from_str("message"), + &JsValue::from_str(&commit.message), + )?; + js_sys::Reflect::set( + &entry, + &JsValue::from_str("author"), + &JsValue::from_str(&commit.author), + )?; + js_sys::Reflect::set( + &entry, + &JsValue::from_str("committer"), + &JsValue::from_str(&commit.committer), + )?; + out.push(&entry); + current = commit.parents.first().copied(); + } + Ok(out) + } + + /// Export repository state as a JSON snapshot. + #[wasm_bindgen(js_name = exportJson)] + pub fn export_json(&self) -> Result { + crate::persistence::export_repository_json(&self.inner) + .map_err(|err| JsValue::from_str(&err.to_string())) + } + + /// Replace repository state from a JSON snapshot. + #[wasm_bindgen(js_name = importJson)] + pub fn import_json(&mut self, json: &str) -> Result<(), JsValue> { + self.inner = crate::persistence::import_repository_json(json) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + Ok(()) + } +} + +fn commit_summary(message: &str) -> &str { + message + .lines() + .find(|line| !line.trim().is_empty()) + .unwrap_or("") +} + +impl Default for BrowserRepository { + fn default() -> Self { + Self::new() + } +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod tests { + use wasm_bindgen_test::*; + + use super::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + fn exported_repo_writes_reads_and_commits() { + let mut repo = BrowserRepository::new(); + let blob = repo.write_blob(b"hello"); + + assert!(repo.has_object(&blob)); + assert_eq!(repo.read_blob(&blob).unwrap(), b"hello"); + + repo.set_head("refs/heads/main"); + repo.stage_file("README.md", b"readme", false).unwrap(); + let commit = repo + .commit_staged( + "initial\n", + "A U Thor 1 +0000", + "C O Mitter 1 +0000", + ) + .unwrap(); + + assert!(repo.has_object(&commit)); + assert_eq!(repo.get_ref("refs/heads/main"), Some(commit)); + assert_eq!(repo.staged_count(), 0); + } + + #[wasm_bindgen_test] + fn exported_repo_snapshot_round_trips() { + let mut repo = BrowserRepository::new(); + repo.set_head("refs/heads/main"); + repo.stage_file("README.md", b"readme", false).unwrap(); + + let json = repo.export_json().unwrap(); + let mut restored = BrowserRepository::new(); + restored.import_json(&json).unwrap(); + + assert_eq!(restored.head(), Some("refs/heads/main".to_string())); + assert_eq!(restored.staged_count(), 1); + } + + #[wasm_bindgen_test] + fn exported_repo_lists_recent_commits() { + let mut repo = BrowserRepository::new(); + repo.set_head("refs/heads/main"); + repo.stage_file("README.md", b"readme", false).unwrap(); + let commit = repo + .commit_staged( + "initial commit\n\nbody\n", + "A U Thor 1 +0000", + "C O Mitter 1 +0000", + ) + .unwrap(); + + let commits = repo.recent_commits(5).unwrap(); + + assert_eq!(commits.length(), 1); + let first = commits.get(0); + assert_eq!( + js_sys::Reflect::get(&first, &JsValue::from_str("oid")) + .unwrap() + .as_string(), + Some(commit) + ); + assert_eq!( + js_sys::Reflect::get(&first, &JsValue::from_str("summary")) + .unwrap() + .as_string(), + Some("initial commit".to_string()) + ); + } +} diff --git a/grit-wasm/src/browser_http.rs b/grit-wasm/src/browser_http.rs new file mode 100644 index 000000000..03d34ff72 --- /dev/null +++ b/grit-wasm/src/browser_http.rs @@ -0,0 +1,162 @@ +//! Browser HTTP transport helpers for smart Git requests. + +use base64::prelude::*; +use grit_lib::error::{Error, Result}; +use url::Url; + +/// Browser-ready request URL and headers. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BrowserRequestParts { + /// URL with userinfo stripped so browser `fetch` accepts it. + pub url: String, + /// Headers to send with the request. + pub headers: Vec<(String, String)>, +} + +/// Convert URL-embedded credentials into an `Authorization` header. +/// +/// Browsers reject `fetch("https://user:pass@example.com/...")`; this helper +/// strips userinfo from the URL and returns `Authorization: Basic ...` instead. +/// +/// # Errors +/// +/// Returns an error when `raw_url` is not a valid URL. +pub fn browser_request_parts( + raw_url: &str, + extra_headers: &[(String, String)], +) -> Result { + let mut url = Url::parse(raw_url).map_err(|err| Error::Message(err.to_string()))?; + let username = url.username().to_string(); + let password = url.password().map(ToOwned::to_owned); + let mut headers = extra_headers.to_vec(); + + if !username.is_empty() || password.is_some() { + let password = password.unwrap_or_default(); + let encoded = BASE64_STANDARD.encode(format!("{username}:{password}")); + headers.push(("Authorization".to_string(), format!("Basic {encoded}"))); + url.set_username("") + .map_err(|_| Error::Message("failed to strip username from URL".to_string()))?; + url.set_password(None) + .map_err(|_| Error::Message("failed to strip password from URL".to_string()))?; + } + + Ok(BrowserRequestParts { + url: url.to_string(), + headers, + }) +} + +/// Fetch bytes from the browser using `window.fetch`. +#[cfg(target_arch = "wasm32")] +pub async fn fetch_bytes( + method: &str, + raw_url: &str, + headers: &[(String, String)], + body: Option<&[u8]>, +) -> std::result::Result, wasm_bindgen::JsValue> { + use js_sys::Uint8Array; + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + use web_sys::{Headers, Request, RequestInit, RequestMode, Response}; + + let parts = browser_request_parts(raw_url, headers) + .map_err(|err| wasm_bindgen::JsValue::from_str(&err.to_string()))?; + let request_headers = Headers::new()?; + for (name, value) in &parts.headers { + request_headers.append(name, value)?; + } + + let init = RequestInit::new(); + init.set_method(method); + init.set_mode(RequestMode::Cors); + init.set_headers(&request_headers); + let body_value; + if let Some(body) = body { + body_value = Uint8Array::from(body).into(); + init.set_body(&body_value); + } + + let request = Request::new_with_str_and_init(&parts.url, &init)?; + let window = + web_sys::window().ok_or_else(|| wasm_bindgen::JsValue::from_str("missing window"))?; + let response_value = JsFuture::from(window.fetch_with_request(&request)).await?; + let response: Response = response_value.dyn_into()?; + if !response.ok() { + return Err(wasm_bindgen::JsValue::from_str(&format!( + "HTTP {} {}", + response.status(), + response.status_text() + ))); + } + let buffer = JsFuture::from(response.array_buffer()?).await?; + Ok(Uint8Array::new(&buffer).to_vec()) +} + +/// Fetch smart-Git bytes with standard content negotiation headers. +#[cfg(target_arch = "wasm32")] +pub async fn fetch_git_rpc( + raw_url: &str, + service: &str, + body: &[u8], + git_protocol: Option<&str>, +) -> std::result::Result, wasm_bindgen::JsValue> { + let mut headers = vec![ + ( + "Content-Type".to_string(), + format!("application/x-{service}-request"), + ), + ( + "Accept".to_string(), + format!("application/x-{service}-result"), + ), + ]; + if let Some(protocol) = git_protocol { + headers.push(("Git-Protocol".to_string(), protocol.to_string())); + } + fetch_bytes("POST", raw_url, &headers, Some(body)).await +} + +/// Fetch smart-Git discovery bytes with optional `Git-Protocol` negotiation. +#[cfg(target_arch = "wasm32")] +pub async fn fetch_git_discovery( + raw_url: &str, + git_protocol: Option<&str>, +) -> std::result::Result, wasm_bindgen::JsValue> { + let mut headers = Vec::new(); + if let Some(protocol) = git_protocol { + headers.push(("Git-Protocol".to_string(), protocol.to_string())); + } + fetch_bytes("GET", raw_url, &headers, None).await +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_userinfo_into_basic_auth_header() { + let parts = browser_request_parts( + "https://user:token@example.com/repo.git", + &[("Git-Protocol".to_string(), "version=2".to_string())], + ) + .unwrap(); + + assert_eq!(parts.url, "https://example.com/repo.git"); + assert!(parts + .headers + .iter() + .any(|(name, value)| name == "Authorization" && value == "Basic dXNlcjp0b2tlbg==")); + assert!(parts + .headers + .iter() + .any(|(name, value)| name == "Git-Protocol" && value == "version=2")); + } + + #[test] + fn leaves_credentialless_urls_unchanged() { + let parts = browser_request_parts("https://example.com/repo.git", &[]).unwrap(); + + assert_eq!(parts.url, "https://example.com/repo.git"); + assert!(parts.headers.is_empty()); + } +} diff --git a/grit-wasm/src/lib.rs b/grit-wasm/src/lib.rs new file mode 100644 index 000000000..1aa062b6b --- /dev/null +++ b/grit-wasm/src/lib.rs @@ -0,0 +1,838 @@ +//! Browser-oriented WASM entry points for Grit. +//! +//! This crate starts with a small in-memory repository core so the WASM build +//! can grow around portable Git data structures before browser HTTP and +//! persistent storage are wired in. + +#[cfg(target_arch = "wasm32")] +pub mod bindings; +pub mod browser_http; +pub mod persistence; +pub mod remote; + +use std::collections::BTreeMap; + +use grit_lib::commit::write_commit; +use grit_lib::error::{Error, Result}; +use grit_lib::objects::{ + serialize_tree, tree_entry_cmp, CommitData, Object, ObjectId, ObjectKind, TreeEntry, +}; +use grit_lib::pack_write::{objects_for_push_pack, write_pack, PackWriteOptions}; +use grit_lib::storage::{ + object_id_for, pack_id_for, ObjectReader, ObjectWriter, PackDescriptor, PackId, PackReader, + PackWriter, RefReader, RefWriter, +}; + +/// An in-memory object store used by the first WASM implementation milestone. +#[derive(Default)] +pub struct MemoryObjectStore { + objects: BTreeMap, +} + +impl MemoryObjectStore { + /// Create an empty in-memory object store. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Return the number of objects currently stored. + #[must_use] + pub fn len(&self) -> usize { + self.objects.len() + } + + /// Return whether the store currently contains no objects. + #[must_use] + pub fn is_empty(&self) -> bool { + self.objects.is_empty() + } + + /// Return true if `oid` is present in the store. + #[must_use] + pub fn contains(&self, oid: &ObjectId) -> bool { + self.has_object(oid).unwrap_or(false) + } + + /// Read an object from the store. + #[must_use] + pub fn read(&self, oid: &ObjectId) -> Option { + self.read_object(oid).ok() + } + + /// Write an object and return its Git object ID. + pub fn write(&mut self, kind: ObjectKind, data: &[u8]) -> ObjectId { + self.write_object(kind, data) + .unwrap_or_else(|_| object_id_for(kind, data)) + } +} + +impl ObjectReader for MemoryObjectStore { + fn read_object(&self, oid: &ObjectId) -> Result { + self.objects + .get(oid) + .cloned() + .ok_or_else(|| Error::ObjectNotFound(oid.to_hex())) + } + + fn has_object(&self, oid: &ObjectId) -> Result { + Ok(self.objects.contains_key(oid)) + } +} + +impl ObjectWriter for MemoryObjectStore { + fn write_object(&mut self, kind: ObjectKind, data: &[u8]) -> Result { + let oid = object_id_for(kind, data); + self.objects.insert( + oid, + Object { + kind, + data: data.to_vec(), + }, + ); + Ok(oid) + } +} + +/// An in-memory reference store used by the first WASM implementation milestone. +#[derive(Default)] +pub struct MemoryRefStore { + refs: BTreeMap, + symbolic_refs: BTreeMap, +} + +impl MemoryRefStore { + /// Create an empty in-memory reference store. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Return the number of direct references currently stored. + #[must_use] + pub fn len(&self) -> usize { + self.refs.len() + } + + /// Return whether the store currently contains no direct or symbolic refs. + #[must_use] + pub fn is_empty(&self) -> bool { + self.refs.is_empty() && self.symbolic_refs.is_empty() + } + + fn resolve_ref_inner(&self, name: &str, depth: usize) -> Result> { + if depth > 10 { + return Err(Error::InvalidRef(format!("ref symlink too deep: {name}"))); + } + if let Some(oid) = self.refs.get(name) { + return Ok(Some(*oid)); + } + let Some(target) = self.symbolic_refs.get(name) else { + return Ok(None); + }; + self.resolve_ref_inner(target, depth + 1) + } +} + +impl RefReader for MemoryRefStore { + fn resolve_ref(&self, name: &str) -> Result> { + self.resolve_ref_inner(name, 0) + } + + fn read_symbolic_ref(&self, name: &str) -> Result> { + Ok(self.symbolic_refs.get(name).cloned()) + } + + fn list_refs(&self, prefix: &str) -> Result> { + let mut refs = Vec::new(); + for name in self + .refs + .keys() + .chain(self.symbolic_refs.keys()) + .filter(|name| name.starts_with(prefix)) + { + if let Some(oid) = self.resolve_ref(name)? { + refs.push((name.clone(), oid)); + } + } + refs.sort_by(|a, b| a.0.cmp(&b.0)); + refs.dedup_by(|a, b| a.0 == b.0); + Ok(refs) + } +} + +impl RefWriter for MemoryRefStore { + fn set_ref(&mut self, name: &str, oid: ObjectId) -> Result<()> { + self.symbolic_refs.remove(name); + self.refs.insert(name.to_string(), oid); + Ok(()) + } + + fn set_symbolic_ref(&mut self, name: &str, target: &str) -> Result<()> { + self.refs.remove(name); + self.symbolic_refs + .insert(name.to_string(), target.to_string()); + Ok(()) + } + + fn delete_ref(&mut self, name: &str) -> Result<()> { + self.refs.remove(name); + self.symbolic_refs.remove(name); + Ok(()) + } +} + +#[derive(Clone)] +struct StoredPack { + bytes: Vec, + promisor: bool, +} + +/// An in-memory raw pack store used by early browser fetch work. +#[derive(Default)] +pub struct MemoryPackStore { + packs: BTreeMap, +} + +impl MemoryPackStore { + /// Create an empty in-memory pack store. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Return the number of packs currently stored. + #[must_use] + pub fn len(&self) -> usize { + self.packs.len() + } + + /// Return whether the store currently contains no packs. + #[must_use] + pub fn is_empty(&self) -> bool { + self.packs.is_empty() + } +} + +impl PackReader for MemoryPackStore { + fn list_packs(&self) -> Result> { + Ok(self + .packs + .iter() + .map(|(id, pack)| PackDescriptor { + id: id.clone(), + byte_len: pack.bytes.len(), + promisor: pack.promisor, + }) + .collect()) + } + + fn read_pack(&self, id: &PackId) -> Result>> { + Ok(self.packs.get(id).map(|pack| pack.bytes.clone())) + } +} + +impl PackWriter for MemoryPackStore { + fn add_pack(&mut self, bytes: Vec, promisor: bool) -> Result { + let id = pack_id_for(&bytes); + self.packs + .insert(id.clone(), StoredPack { bytes, promisor }); + Ok(id) + } + + fn delete_pack(&mut self, id: &PackId) -> Result<()> { + self.packs.remove(id); + Ok(()) + } +} + +/// Metadata for a promisor remote. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PromisorRemote { + /// Remote URL used for future promised-object hydration. + pub url: String, + /// Partial clone filter associated with this remote, for example `blob:none`. + pub filter: String, +} + +/// A staged file entry in the browser repository. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StagedFile { + /// Repository-relative path using `/` separators. + pub path: String, + /// Git tree mode for the staged file. + pub mode: u32, + /// Blob object ID for the staged contents. + pub oid: ObjectId, +} + +/// Browser-facing repository state for early WASM work. +#[derive(Default)] +pub struct WasmRepository { + objects: MemoryObjectStore, + refs: MemoryRefStore, + packs: MemoryPackStore, + promisor_remotes: BTreeMap, + staged: BTreeMap, +} + +impl WasmRepository { + /// Create an empty in-memory repository. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Return the package version compiled into this WASM crate. + #[must_use] + pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") + } + + /// Return the number of stored objects. + #[must_use] + pub fn object_count(&self) -> usize { + self.objects.len() + } + + /// Return the number of stored raw packs. + #[must_use] + pub fn pack_count(&self) -> usize { + self.packs.len() + } + + /// Return the number of configured promisor remotes. + #[must_use] + pub fn promisor_remote_count(&self) -> usize { + self.promisor_remotes.len() + } + + /// Return the number of staged file entries. + #[must_use] + pub fn staged_count(&self) -> usize { + self.staged.len() + } + + /// Store a blob object and return its hex object ID. + pub fn write_blob(&mut self, data: &[u8]) -> String { + self.objects.write(ObjectKind::Blob, data).to_hex() + } + + /// Store raw tree object data and return its hex object ID. + /// + /// This is a temporary low-level API for early WASM commit work. Higher + /// level browser index/tree builders will replace direct tree byte writes. + pub fn write_tree_raw(&mut self, data: &[u8]) -> String { + self.objects.write(ObjectKind::Tree, data).to_hex() + } + + /// Stage a regular file from browser-provided bytes. + /// + /// `path` must be repository-relative and use `/` separators. Set + /// `executable` to store mode `100755`; otherwise mode `100644` is used. + /// + /// # Errors + /// + /// Returns an error when the path is empty or contains `.` / `..` + /// components. + pub fn stage_file(&mut self, path: &str, data: &[u8], executable: bool) -> Result { + let path = normalize_repo_path(path)?; + let oid = self.objects.write(ObjectKind::Blob, data); + let mode = if executable { + MODE_EXECUTABLE + } else { + MODE_REGULAR + }; + self.staged + .insert(path.clone(), StagedFile { path, mode, oid }); + Ok(oid.to_hex()) + } + + /// Build tree objects from staged files and return the root tree ID. + /// + /// # Errors + /// + /// Returns an error when there are no staged files or the object store + /// cannot write a tree. + pub fn write_tree_from_staged(&mut self) -> Result { + if self.staged.is_empty() { + return Err(Error::IndexError("no staged files".to_string())); + } + let entries = self.staged.values().cloned().collect::>(); + let oid = build_tree_from_staged(&mut self.objects, &entries, "")?; + Ok(oid.to_hex()) + } + + /// Return true if an object with the provided hex ID exists. + #[must_use] + pub fn has_object_hex(&self, oid_hex: &str) -> bool { + ObjectId::from_hex(oid_hex) + .map(|oid| self.objects.contains(&oid)) + .unwrap_or(false) + } + + /// Read a blob object by hex ID. + #[must_use] + pub fn read_blob(&self, oid_hex: &str) -> Option> { + let oid = ObjectId::from_hex(oid_hex).ok()?; + let object = self.objects.read(&oid)?; + (object.kind == ObjectKind::Blob).then(|| object.data.clone()) + } + + /// Write a commit object from explicit caller-provided commit metadata. + /// + /// `author` and `committer` must be complete Git identity header payloads, + /// including timestamp and timezone, for example + /// `"A U Thor 1 +0000"`. + #[must_use] + pub fn write_commit( + &mut self, + tree_hex: &str, + parent_hexes: Vec, + author: &str, + committer: &str, + message: &str, + ) -> Option { + let tree = ObjectId::from_hex(tree_hex).ok()?; + let parents = parent_hexes + .iter() + .map(|parent| ObjectId::from_hex(parent)) + .collect::>>() + .ok()?; + let commit = CommitData { + tree, + parents, + author: author.to_string(), + committer: committer.to_string(), + author_raw: Vec::new(), + committer_raw: Vec::new(), + encoding: None, + message: message.to_string(), + raw_message: None, + }; + write_commit(&mut self.objects, &commit) + .ok() + .map(|oid| oid.to_hex()) + } + + /// Commit the staged files and update the current branch. + /// + /// `author` and `committer` must be complete Git identity header payloads, + /// including timestamp and timezone. The current `HEAD` commit, when set, + /// is used as the single parent. If `HEAD` is not set, this creates + /// `refs/heads/main` and points `HEAD` at it. + /// + /// # Errors + /// + /// Returns an error when tree or commit writing fails, or when `HEAD` + /// points at a non-commit object. + pub fn commit_staged( + &mut self, + message: &str, + author: &str, + committer: &str, + ) -> Result { + let tree_hex = self.write_tree_from_staged()?; + let tree = ObjectId::from_hex(&tree_hex)?; + let head_target = self.refs.read_symbolic_ref("HEAD")?; + let parent = self.refs.resolve_ref("HEAD")?; + if let Some(parent_oid) = parent { + let parent_object = self.objects.read_object(&parent_oid)?; + if parent_object.kind != ObjectKind::Commit { + return Err(Error::CorruptObject( + "HEAD does not point to a commit".to_string(), + )); + } + } + let commit = CommitData { + tree, + parents: parent.into_iter().collect(), + author: author.to_string(), + committer: committer.to_string(), + author_raw: Vec::new(), + committer_raw: Vec::new(), + encoding: None, + message: message.to_string(), + raw_message: None, + }; + let commit_oid = write_commit(&mut self.objects, &commit)?; + let target_ref = match head_target { + Some(target) => target, + None => { + let target = "refs/heads/main".to_string(); + self.refs.set_symbolic_ref("HEAD", &target)?; + target + } + }; + self.refs.set_ref(&target_ref, commit_oid)?; + self.staged.clear(); + Ok(commit_oid.to_hex()) + } + + /// Set a reference to the object named by `oid_hex`. + /// + /// Returns false when `oid_hex` is not a valid SHA-1 object ID. + pub fn set_ref(&mut self, name: &str, oid_hex: &str) -> bool { + let Ok(oid) = ObjectId::from_hex(oid_hex) else { + return false; + }; + self.refs.set_ref(name, oid).is_ok() + } + + /// Read a reference as a hex object ID. + #[must_use] + pub fn get_ref(&self, name: &str) -> Option { + self.refs + .resolve_ref(name) + .ok() + .flatten() + .map(|oid| oid.to_hex()) + } + + /// Set symbolic `HEAD` to a reference name. + pub fn set_head(&mut self, refname: &str) { + let _ = self.refs.set_symbolic_ref("HEAD", refname); + } + + /// Return the symbolic `HEAD` reference name, if one has been set. + #[must_use] + pub fn head(&self) -> Option { + self.refs.read_symbolic_ref("HEAD").ok().flatten() + } + + /// Store raw pack bytes and return the pack ID. + pub fn add_pack(&mut self, bytes: Vec, promisor: bool) -> String { + self.packs + .add_pack(bytes, promisor) + .map(|id| id.as_str().to_string()) + .unwrap_or_default() + } + + /// Return whether the pack identified by `id` is marked as promisor. + #[must_use] + pub fn pack_is_promisor(&self, id: &str) -> bool { + let id = PackId::new(id); + self.packs + .list_packs() + .unwrap_or_default() + .into_iter() + .any(|pack| pack.id == id && pack.promisor) + } + + /// Read raw pack bytes by pack ID. + #[must_use] + pub fn read_pack(&self, id: &str) -> Option> { + self.packs.read_pack(&PackId::new(id)).ok().flatten() + } + + /// Write a non-delta PACK v2 containing the explicit object IDs. + /// + /// The returned bytes can be used as receive-pack input once push command + /// construction is wired in. + pub fn write_pack_for_oids(&self, oid_hexes: Vec) -> Result> { + let oids = oid_hexes + .iter() + .map(|oid| ObjectId::from_hex(oid)) + .collect::>>()?; + let mut out = Vec::new(); + let _summary = write_pack(&self.objects, &oids, &mut out, PackWriteOptions::default())?; + Ok(out) + } + + /// Write a non-delta PACK v2 for pushing `tip_hex`. + /// + /// Objects reachable from `old_tip_hex`, when provided, are excluded from + /// the pack. + pub fn write_pack_for_push( + &self, + tip_hex: &str, + old_tip_hex: Option, + ) -> Result> { + let tip = ObjectId::from_hex(tip_hex)?; + let old_tips = old_tip_hex + .as_deref() + .map(ObjectId::from_hex) + .transpose()? + .into_iter() + .collect::>(); + let objects = objects_for_push_pack(&self.objects, &[tip], &old_tips)?; + let mut out = Vec::new(); + let _summary = write_pack( + &self.objects, + &objects, + &mut out, + PackWriteOptions::default(), + )?; + Ok(out) + } + + /// Record promisor metadata for `name`. + pub fn set_promisor_remote(&mut self, name: &str, url: &str, filter: &str) { + self.promisor_remotes.insert( + name.to_string(), + PromisorRemote { + url: url.to_string(), + filter: filter.to_string(), + }, + ); + } + + /// Return promisor remote metadata for `name`. + #[must_use] + pub fn promisor_remote(&self, name: &str) -> Option { + self.promisor_remotes.get(name).cloned() + } +} + +const MODE_REGULAR: u32 = 0o100644; +const MODE_EXECUTABLE: u32 = 0o100755; +const MODE_TREE: u32 = 0o040000; + +fn normalize_repo_path(path: &str) -> Result { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Err(Error::PathError("path is empty".to_string())); + } + if trimmed + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + { + return Err(Error::PathError(format!("invalid repository path: {path}"))); + } + Ok(trimmed.to_string()) +} + +fn build_tree_from_staged( + store: &mut MemoryObjectStore, + entries: &[StagedFile], + prefix: &str, +) -> Result { + let mut files = Vec::new(); + let mut dirs: BTreeMap = BTreeMap::new(); + for entry in entries { + let Some(rel) = relative_staged_path(&entry.path, prefix) else { + continue; + }; + if let Some((dir, _rest)) = rel.split_once('/') { + dirs.insert(dir.to_string(), ()); + } else { + files.push(TreeEntry { + mode: entry.mode, + name: rel.as_bytes().to_vec(), + oid: entry.oid, + }); + } + } + for dir in dirs.keys() { + let sub_prefix = if prefix.is_empty() { + dir.to_string() + } else { + format!("{prefix}/{dir}") + }; + let oid = build_tree_from_staged(store, entries, &sub_prefix)?; + files.push(TreeEntry { + mode: MODE_TREE, + name: dir.as_bytes().to_vec(), + oid, + }); + } + files + .sort_by(|a, b| tree_entry_cmp(&a.name, a.mode == MODE_TREE, &b.name, b.mode == MODE_TREE)); + let data = serialize_tree(&files); + store.write_object(ObjectKind::Tree, &data) +} + +fn relative_staged_path<'a>(path: &'a str, prefix: &str) -> Option<&'a str> { + if prefix.is_empty() { + return Some(path); + } + let rest = path.strip_prefix(prefix)?; + rest.strip_prefix('/') +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn writes_git_blob_object_ids() { + let mut repo = WasmRepository::new(); + let oid = repo.write_blob(b"hello\n"); + + assert_eq!(oid, "ce013625030ba8dba906f756967f9e9ca394464a"); + assert!(repo.has_object_hex(&oid)); + assert_eq!(repo.read_blob(&oid), Some(b"hello\n".to_vec())); + } + + #[test] + fn tracks_refs_and_head() { + let mut repo = WasmRepository::new(); + let oid = repo.write_blob(b"content"); + + assert!(repo.set_ref("refs/heads/main", &oid)); + repo.set_head("refs/heads/main"); + + assert_eq!(repo.get_ref("refs/heads/main"), Some(oid)); + assert_eq!(repo.head(), Some("refs/heads/main".to_string())); + } + + #[test] + fn resolves_symbolic_refs() { + let mut refs = MemoryRefStore::new(); + let oid = match ObjectId::from_hex("67bf698f3ab735e92fb011a99cff3497c44d30c1") { + Ok(oid) => oid, + Err(err) => panic!("valid test object ID failed to parse: {err}"), + }; + + assert!(refs.set_ref("refs/heads/main", oid).is_ok()); + assert!(refs.set_symbolic_ref("HEAD", "refs/heads/main").is_ok()); + + assert_eq!(refs.resolve_ref("HEAD").ok().flatten(), Some(oid)); + assert_eq!( + refs.list_refs("refs/heads/").unwrap_or_default(), + vec![("refs/heads/main".to_string(), oid)] + ); + } + + #[test] + fn stores_promisor_packs() { + let mut repo = WasmRepository::new(); + let id = repo.add_pack(b"PACK test bytes".to_vec(), true); + + assert_eq!(repo.pack_count(), 1); + assert!(repo.pack_is_promisor(&id)); + assert_eq!(repo.read_pack(&id), Some(b"PACK test bytes".to_vec())); + } + + #[test] + fn stores_promisor_remote_metadata() { + let mut repo = WasmRepository::new(); + repo.set_promisor_remote("origin", "https://example.com/repo.git", "blob:none"); + + let remote = repo.promisor_remote("origin"); + + assert_eq!(repo.promisor_remote_count(), 1); + assert_eq!( + remote, + Some(PromisorRemote { + url: "https://example.com/repo.git".to_string(), + filter: "blob:none".to_string(), + }) + ); + } + + #[test] + fn writes_commit_from_existing_tree() { + let mut repo = WasmRepository::new(); + let tree = repo.write_tree_raw(b""); + let commit = repo.write_commit( + &tree, + Vec::new(), + "A U Thor 1 +0000", + "C O Mitter 2 +0000", + "initial\n", + ); + + let commit = match commit { + Some(commit) => commit, + None => panic!("commit should be written"), + }; + assert!(repo.has_object_hex(&commit)); + } + + #[test] + fn builds_tree_from_staged_files() { + let mut repo = WasmRepository::new(); + let readme = repo.stage_file("README.md", b"readme", false).unwrap(); + let lib = repo.stage_file("src/lib.rs", b"lib", true).unwrap(); + + let tree = repo.write_tree_from_staged().unwrap(); + let tree_oid = ObjectId::from_hex(&tree).unwrap(); + let root = repo.objects.read_object(&tree_oid).unwrap(); + let entries = grit_lib::objects::parse_tree(&root.data).unwrap(); + + assert_eq!(repo.staged_count(), 2); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].name, b"README.md"); + assert_eq!(entries[0].oid.to_hex(), readme); + assert_eq!(entries[1].name, b"src"); + + let src = repo.objects.read_object(&entries[1].oid).unwrap(); + let src_entries = grit_lib::objects::parse_tree(&src.data).unwrap(); + assert_eq!(src_entries.len(), 1); + assert_eq!(src_entries[0].name, b"lib.rs"); + assert_eq!(src_entries[0].mode, MODE_EXECUTABLE); + assert_eq!(src_entries[0].oid.to_hex(), lib); + } + + #[test] + fn commit_staged_updates_current_branch() { + let mut repo = WasmRepository::new(); + repo.set_head("refs/heads/main"); + repo.stage_file("README.md", b"readme", false).unwrap(); + + let commit = repo + .commit_staged( + "initial\n", + "A U Thor 1 +0000", + "C O Mitter 2 +0000", + ) + .unwrap(); + + assert_eq!(repo.get_ref("refs/heads/main"), Some(commit.clone())); + assert_eq!(repo.head(), Some("refs/heads/main".to_string())); + assert_eq!(repo.staged_count(), 0); + let commit_oid = ObjectId::from_hex(&commit).unwrap(); + let object = repo.objects.read_object(&commit_oid).unwrap(); + assert_eq!(object.kind, ObjectKind::Commit); + } + + #[test] + fn writes_pack_for_explicit_oids() { + let mut repo = WasmRepository::new(); + let blob = repo.write_blob(b"hello"); + + let pack = repo.write_pack_for_oids(vec![blob]).unwrap(); + + assert_eq!(&pack[0..4], b"PACK"); + assert_eq!(u32::from_be_bytes(pack[4..8].try_into().unwrap()), 2); + assert_eq!(u32::from_be_bytes(pack[8..12].try_into().unwrap()), 1); + } + + #[test] + fn writes_pack_for_push_excluding_parent_history() { + let mut repo = WasmRepository::new(); + repo.set_head("refs/heads/main"); + repo.stage_file("old.txt", b"old", false).unwrap(); + let old_commit = repo + .commit_staged( + "old\n", + "A U Thor 1 +0000", + "C O Mitter 1 +0000", + ) + .unwrap(); + repo.stage_file("new.txt", b"new", false).unwrap(); + let new_commit = repo + .commit_staged( + "new\n", + "A U Thor 2 +0000", + "C O Mitter 2 +0000", + ) + .unwrap(); + + let pack = repo + .write_pack_for_push(&new_commit, Some(old_commit)) + .unwrap(); + + assert_eq!(&pack[0..4], b"PACK"); + assert_eq!(u32::from_be_bytes(pack[4..8].try_into().unwrap()), 2); + assert!(u32::from_be_bytes(pack[8..12].try_into().unwrap()) >= 3); + } + + #[test] + fn validates_staged_paths() { + assert_eq!(normalize_repo_path("/src/lib.rs").unwrap(), "src/lib.rs"); + assert!(normalize_repo_path("").is_err()); + assert!(normalize_repo_path("src/../secret").is_err()); + } +} diff --git a/grit-wasm/src/persistence.rs b/grit-wasm/src/persistence.rs new file mode 100644 index 000000000..09dffc806 --- /dev/null +++ b/grit-wasm/src/persistence.rs @@ -0,0 +1,469 @@ +//! Snapshot and browser persistence helpers. + +use std::collections::BTreeMap; + +use base64::prelude::*; +use grit_lib::error::{Error, Result}; +use grit_lib::objects::{Object, ObjectId, ObjectKind}; +use serde::{Deserialize, Serialize}; + +use grit_lib::storage::PackId; + +use crate::{ + MemoryObjectStore, MemoryPackStore, MemoryRefStore, PromisorRemote, StagedFile, StoredPack, + WasmRepository, +}; + +/// Serializable repository snapshot. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositorySnapshot { + objects: Vec, + refs: Vec, + symbolic_refs: Vec, + packs: Vec, + promisor_remotes: Vec, + staged: Vec, +} + +/// Browser storage quota estimate. +#[derive(Clone, Debug, PartialEq)] +pub struct StorageQuota { + /// Estimated bytes currently used by this origin. + pub usage: Option, + /// Estimated bytes available to this origin. + pub quota: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct ObjectSnapshot { + oid: String, + kind: String, + data_base64: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct RefSnapshot { + name: String, + oid: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct SymbolicRefSnapshot { + name: String, + target: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct PackSnapshot { + id: String, + bytes_base64: String, + promisor: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct PromisorRemoteSnapshot { + name: String, + url: String, + filter: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct StagedFileSnapshot { + path: String, + mode: u32, + oid: String, +} + +/// Export a repository snapshot to JSON. +/// +/// # Errors +/// +/// Returns an error when JSON serialization fails. +pub fn export_repository_json(repo: &WasmRepository) -> Result { + serde_json::to_string(&snapshot_repository(repo)).map_err(|err| Error::Message(err.to_string())) +} + +/// Import a repository snapshot from JSON. +/// +/// # Errors +/// +/// Returns an error when JSON parsing fails or the snapshot contains invalid +/// object IDs/object kinds. +pub fn import_repository_json(json: &str) -> Result { + let snapshot: RepositorySnapshot = + serde_json::from_str(json).map_err(|err| Error::Message(err.to_string()))?; + restore_repository(snapshot) +} + +fn snapshot_repository(repo: &WasmRepository) -> RepositorySnapshot { + RepositorySnapshot { + objects: repo + .objects + .objects + .iter() + .map(|(oid, object)| ObjectSnapshot { + oid: oid.to_hex(), + kind: object.kind.as_str().to_string(), + data_base64: BASE64_STANDARD.encode(&object.data), + }) + .collect(), + refs: repo + .refs + .refs + .iter() + .map(|(name, oid)| RefSnapshot { + name: name.clone(), + oid: oid.to_hex(), + }) + .collect(), + symbolic_refs: repo + .refs + .symbolic_refs + .iter() + .map(|(name, target)| SymbolicRefSnapshot { + name: name.clone(), + target: target.clone(), + }) + .collect(), + packs: repo + .packs + .packs + .iter() + .map(|(id, pack)| PackSnapshot { + id: id.as_str().to_string(), + bytes_base64: BASE64_STANDARD.encode(&pack.bytes), + promisor: pack.promisor, + }) + .collect(), + promisor_remotes: repo + .promisor_remotes + .iter() + .map(|(name, remote)| PromisorRemoteSnapshot { + name: name.clone(), + url: remote.url.clone(), + filter: remote.filter.clone(), + }) + .collect(), + staged: repo + .staged + .values() + .map(|entry| StagedFileSnapshot { + path: entry.path.clone(), + mode: entry.mode, + oid: entry.oid.to_hex(), + }) + .collect(), + } +} + +fn restore_repository(snapshot: RepositorySnapshot) -> Result { + let mut objects = BTreeMap::new(); + for object in snapshot.objects { + let oid = ObjectId::from_hex(&object.oid)?; + let kind = object_kind_from_str(&object.kind)?; + let data = BASE64_STANDARD + .decode(object.data_base64) + .map_err(|err| Error::Message(err.to_string()))?; + objects.insert(oid, Object::new(kind, data)); + } + + let mut refs = BTreeMap::new(); + for entry in snapshot.refs { + refs.insert(entry.name, ObjectId::from_hex(&entry.oid)?); + } + + let symbolic_refs = snapshot + .symbolic_refs + .into_iter() + .map(|entry| (entry.name, entry.target)) + .collect(); + + let mut packs = BTreeMap::new(); + for pack in snapshot.packs { + let bytes = BASE64_STANDARD + .decode(pack.bytes_base64) + .map_err(|err| Error::Message(err.to_string()))?; + packs.insert( + PackId::new(pack.id), + StoredPack { + bytes, + promisor: pack.promisor, + }, + ); + } + + let promisor_remotes = snapshot + .promisor_remotes + .into_iter() + .map(|entry| { + ( + entry.name, + PromisorRemote { + url: entry.url, + filter: entry.filter, + }, + ) + }) + .collect(); + + let mut staged = BTreeMap::new(); + for entry in snapshot.staged { + let staged_file = StagedFile { + path: entry.path.clone(), + mode: entry.mode, + oid: ObjectId::from_hex(&entry.oid)?, + }; + staged.insert(entry.path, staged_file); + } + + Ok(WasmRepository { + objects: MemoryObjectStore { objects }, + refs: MemoryRefStore { + refs, + symbolic_refs, + }, + packs: MemoryPackStore { packs }, + promisor_remotes, + staged, + }) +} + +fn object_kind_from_str(kind: &str) -> Result { + match kind { + "blob" => Ok(ObjectKind::Blob), + "tree" => Ok(ObjectKind::Tree), + "commit" => Ok(ObjectKind::Commit), + "tag" => Ok(ObjectKind::Tag), + other => Err(Error::UnknownObjectType(other.to_string())), + } +} + +/// Save a repository snapshot to IndexedDB. +#[cfg(target_arch = "wasm32")] +pub async fn save_repository( + db_name: &str, + repo: &WasmRepository, +) -> std::result::Result<(), wasm_bindgen::JsValue> { + let json = export_repository_json(repo) + .map_err(|err| wasm_bindgen::JsValue::from_str(&err.to_string()))?; + let db = open_database(db_name).await?; + let tx = + db.transaction_with_str_and_mode(STORE_NAME, web_sys::IdbTransactionMode::Readwrite)?; + let store = tx.object_store(STORE_NAME)?; + let request = store.put_with_key( + &wasm_bindgen::JsValue::from_str(&json), + &wasm_bindgen::JsValue::from_str(DEFAULT_KEY), + )?; + let _ = idb_request_result(&request).await?; + Ok(()) +} + +/// Load a repository snapshot from IndexedDB. +#[cfg(target_arch = "wasm32")] +pub async fn load_repository( + db_name: &str, +) -> std::result::Result, wasm_bindgen::JsValue> { + let db = open_database(db_name).await?; + let tx = db.transaction_with_str(STORE_NAME)?; + let store = tx.object_store(STORE_NAME)?; + let request = store.get(&wasm_bindgen::JsValue::from_str(DEFAULT_KEY))?; + let value = idb_request_result(&request).await?; + if value.is_undefined() { + return Ok(None); + } + let Some(json) = value.as_string() else { + return Err(wasm_bindgen::JsValue::from_str( + "stored repository snapshot is not a string", + )); + }; + import_repository_json(&json) + .map(Some) + .map_err(|err| wasm_bindgen::JsValue::from_str(&err.to_string())) +} + +/// Delete the saved repository snapshot from IndexedDB. +#[cfg(target_arch = "wasm32")] +pub async fn delete_repository_snapshot( + db_name: &str, +) -> std::result::Result<(), wasm_bindgen::JsValue> { + let db = open_database(db_name).await?; + let tx = + db.transaction_with_str_and_mode(STORE_NAME, web_sys::IdbTransactionMode::Readwrite)?; + let store = tx.object_store(STORE_NAME)?; + let request = store.delete(&wasm_bindgen::JsValue::from_str(DEFAULT_KEY))?; + let _ = idb_request_result(&request).await?; + Ok(()) +} + +/// Delete the entire IndexedDB database used for repository storage. +#[cfg(target_arch = "wasm32")] +pub async fn delete_repository_database( + db_name: &str, +) -> std::result::Result<(), wasm_bindgen::JsValue> { + let window = + web_sys::window().ok_or_else(|| wasm_bindgen::JsValue::from_str("missing window"))?; + let factory = window + .indexed_db()? + .ok_or_else(|| wasm_bindgen::JsValue::from_str("IndexedDB is unavailable"))?; + let request = factory.delete_database(db_name)?; + let _ = idb_open_request_result(&request).await?; + Ok(()) +} + +/// Estimate browser storage usage/quota for this origin. +#[cfg(target_arch = "wasm32")] +pub async fn estimate_storage_quota( +) -> std::result::Result, wasm_bindgen::JsValue> { + use js_sys::{Function, Promise, Reflect}; + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + + let window = + web_sys::window().ok_or_else(|| wasm_bindgen::JsValue::from_str("missing window"))?; + let navigator = Reflect::get(&window, &wasm_bindgen::JsValue::from_str("navigator"))?; + let storage = Reflect::get(&navigator, &wasm_bindgen::JsValue::from_str("storage"))?; + if storage.is_undefined() || storage.is_null() { + return Ok(None); + } + let estimate = Reflect::get(&storage, &wasm_bindgen::JsValue::from_str("estimate"))?; + if !estimate.is_function() { + return Ok(None); + } + let estimate_fn: Function = estimate.dyn_into()?; + let promise = estimate_fn.call0(&storage)?; + let value = JsFuture::from(Promise::from(promise)).await?; + let usage = Reflect::get(&value, &wasm_bindgen::JsValue::from_str("usage"))?.as_f64(); + let quota = Reflect::get(&value, &wasm_bindgen::JsValue::from_str("quota"))?.as_f64(); + Ok(Some(StorageQuota { usage, quota })) +} + +#[cfg(target_arch = "wasm32")] +const STORE_NAME: &str = "repositories"; +#[cfg(target_arch = "wasm32")] +const DEFAULT_KEY: &str = "default"; + +#[cfg(target_arch = "wasm32")] +async fn open_database( + db_name: &str, +) -> std::result::Result { + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; + use web_sys::IdbDatabase; + + let window = + web_sys::window().ok_or_else(|| wasm_bindgen::JsValue::from_str("missing window"))?; + let factory = window + .indexed_db()? + .ok_or_else(|| wasm_bindgen::JsValue::from_str("IndexedDB is unavailable"))?; + let request = factory.open_with_u32(db_name, 1)?; + let upgrade_request = request.clone(); + let on_upgrade = Closure::::new(move |_event: web_sys::Event| { + if let Ok(result) = upgrade_request.result() { + if let Ok(db) = result.dyn_into::() { + let _ = db.create_object_store(STORE_NAME); + } + } + }); + request.set_onupgradeneeded(Some(on_upgrade.as_ref().unchecked_ref())); + on_upgrade.forget(); + + let value = idb_open_request_result(&request).await?; + value.dyn_into::() +} + +#[cfg(target_arch = "wasm32")] +async fn idb_open_request_result( + request: &web_sys::IdbOpenDbRequest, +) -> std::result::Result { + use js_sys::Promise; + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + + let success_request = request.clone(); + let error_request = request.clone(); + let promise = Promise::new(&mut |resolve, reject| { + let success_request = success_request.clone(); + let error_request = error_request.clone(); + let on_success = Closure::::new(move |_event: web_sys::Event| { + let value = success_request + .result() + .unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + let _ = resolve.call1(&wasm_bindgen::JsValue::UNDEFINED, &value); + }); + let on_error = Closure::::new(move |_event: web_sys::Event| { + let value = error_request + .error() + .map(wasm_bindgen::JsValue::from) + .unwrap_or_else(|_| wasm_bindgen::JsValue::from_str("IndexedDB request failed")); + let _ = reject.call1(&wasm_bindgen::JsValue::UNDEFINED, &value); + }); + request.set_onsuccess(Some(on_success.as_ref().unchecked_ref())); + request.set_onerror(Some(on_error.as_ref().unchecked_ref())); + on_success.forget(); + on_error.forget(); + }); + JsFuture::from(promise).await +} + +#[cfg(target_arch = "wasm32")] +async fn idb_request_result( + request: &web_sys::IdbRequest, +) -> std::result::Result { + use js_sys::Promise; + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + + let success_request = request.clone(); + let error_request = request.clone(); + let promise = Promise::new(&mut |resolve, reject| { + let success_request = success_request.clone(); + let error_request = error_request.clone(); + let on_success = Closure::::new(move |_event: web_sys::Event| { + let value = success_request + .result() + .unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + let _ = resolve.call1(&wasm_bindgen::JsValue::UNDEFINED, &value); + }); + let on_error = Closure::::new(move |_event: web_sys::Event| { + let value = error_request + .error() + .map(wasm_bindgen::JsValue::from) + .unwrap_or_else(|_| wasm_bindgen::JsValue::from_str("IndexedDB request failed")); + let _ = reject.call1(&wasm_bindgen::JsValue::UNDEFINED, &value); + }); + request.set_onsuccess(Some(on_success.as_ref().unchecked_ref())); + request.set_onerror(Some(on_error.as_ref().unchecked_ref())); + on_success.forget(); + on_error.forget(); + }); + JsFuture::from(promise).await +} + +#[cfg(test)] +mod tests { + use grit_lib::storage::RefWriter; + + use super::*; + + #[test] + fn repository_snapshot_round_trips() { + let mut repo = WasmRepository::new(); + let blob = repo.stage_file("README.md", b"hello", false).unwrap(); + repo.refs + .set_symbolic_ref("HEAD", "refs/heads/main") + .unwrap(); + repo.set_promisor_remote("origin", "https://example.com/repo.git", "blob:none"); + let pack_id = repo.add_pack(b"PACK bytes".to_vec(), true); + + let json = export_repository_json(&repo).unwrap(); + let restored = import_repository_json(&json).unwrap(); + + assert_eq!(restored.staged_count(), 1); + assert_eq!(restored.promisor_remote_count(), 1); + assert!(restored.has_object_hex(&blob)); + assert!(restored.pack_is_promisor(&pack_id)); + assert_eq!(restored.head(), Some("refs/heads/main".to_string())); + } +} diff --git a/grit-wasm/src/remote.rs b/grit-wasm/src/remote.rs new file mode 100644 index 000000000..5d47f4ee3 --- /dev/null +++ b/grit-wasm/src/remote.rs @@ -0,0 +1,714 @@ +//! Browser smart-HTTP operations. + +use grit_lib::error::{Error, Result}; +#[cfg(any(target_arch = "wasm32", test))] +use grit_lib::objects::ObjectId; +#[cfg(any(target_arch = "wasm32", test))] +use grit_lib::smart_protocol::AdvertisedRef; +#[cfg(target_arch = "wasm32")] +use grit_lib::storage::{ObjectReader, PackWriter, RefReader, RefWriter}; + +/// Build the smart-HTTP discovery URL for `service`. +/// +/// `service` is usually `git-upload-pack` or `git-receive-pack`. +/// +/// # Errors +/// +/// Returns an error when the repository URL is empty. +pub fn info_refs_url(repo_url: &str, service: &str) -> Result { + let base = repo_url.trim_end_matches('/'); + if base.is_empty() { + return Err(Error::Message("remote URL is empty".to_string())); + } + let sep = if base.contains('?') { "&" } else { "?" }; + Ok(format!("{base}/info/refs{sep}service={service}")) +} + +/// Build the smart-HTTP RPC endpoint URL for `service`. +/// +/// `service` is usually `git-upload-pack` or `git-receive-pack`. +/// +/// # Errors +/// +/// Returns an error when the repository URL is empty. +pub fn service_url(repo_url: &str, service: &str) -> Result { + let base = repo_url.trim_end_matches('/'); + if base.is_empty() { + return Err(Error::Message("remote URL is empty".to_string())); + } + Ok(format!("{base}/{service}")) +} + +#[cfg(target_arch = "wasm32")] +async fn fetch_refs( + repo_url: &str, +) -> std::result::Result, wasm_bindgen::JsValue> { + use grit_lib::smart_protocol::{ + build_ls_refs_v2_request, capability_lines_for_client_request, parse_ls_refs_v2_response, + parse_upload_pack_advertisement, strip_http_service_advertisement_if_present, + UploadPackAdvertisement, + }; + use wasm_bindgen::JsValue; + + let discovery_url = info_refs_url(repo_url, "git-upload-pack") + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let discovery = + crate::browser_http::fetch_git_discovery(&discovery_url, Some("version=2")).await?; + let pkt_body = strip_http_service_advertisement_if_present(&discovery) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + match parse_upload_pack_advertisement(pkt_body) + .map_err(|err| JsValue::from_str(&err.to_string()))? + { + UploadPackAdvertisement::V2 { + caps, + object_format, + } => { + let request = build_ls_refs_v2_request( + &object_format, + &capability_lines_for_client_request(&caps), + true, + true, + ) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let rpc_url = service_url(repo_url, "git-upload-pack") + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let response = crate::browser_http::fetch_git_rpc( + &rpc_url, + "git-upload-pack", + &request, + Some("version=2"), + ) + .await?; + parse_ls_refs_v2_response(&response).map_err(|err| JsValue::from_str(&err.to_string())) + } + UploadPackAdvertisement::V0V1 { refs, .. } => Ok(refs), + } +} + +#[cfg(target_arch = "wasm32")] +async fn discover_receive_pack( + repo_url: &str, +) -> std::result::Result +{ + use grit_lib::smart_protocol::{ + parse_receive_pack_advertisement, strip_http_service_advertisement_if_present, + }; + use wasm_bindgen::JsValue; + + let discovery_url = info_refs_url(repo_url, "git-receive-pack") + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let discovery = crate::browser_http::fetch_git_discovery(&discovery_url, None).await?; + let pkt_body = strip_http_service_advertisement_if_present(&discovery) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + parse_receive_pack_advertisement(pkt_body).map_err(|err| JsValue::from_str(&err.to_string())) +} + +/// Fetch refs from a smart-HTTP remote in the browser. +/// +/// The return value is a JavaScript array of `{ name, oid }` objects. +#[cfg(target_arch = "wasm32")] +pub async fn ls_refs(repo_url: &str) -> std::result::Result { + use js_sys::{Array, Object, Reflect}; + use wasm_bindgen::JsValue; + + let refs = fetch_refs(repo_url).await?; + + let out = Array::new(); + for advertised in refs { + let obj = Object::new(); + Reflect::set( + &obj, + &JsValue::from_str("name"), + &JsValue::from_str(&advertised.name), + )?; + Reflect::set( + &obj, + &JsValue::from_str("oid"), + &JsValue::from_str(&advertised.oid.to_hex()), + )?; + if let Some(symref_target) = advertised.symref_target { + Reflect::set( + &obj, + &JsValue::from_str("symrefTarget"), + &JsValue::from_str(&symref_target), + )?; + } + out.push(&obj); + } + Ok(out) +} + +/// Fetch a blobless pack containing `want_oids` into an in-memory repository. +/// +/// The fetched pack is stored as a promisor pack and unpacked into the object +/// store. The returned string is the stored pack ID. +#[cfg(target_arch = "wasm32")] +pub async fn fetch_blobless( + repo: &mut crate::WasmRepository, + repo_url: &str, + want_oids: Vec, +) -> std::result::Result { + use wasm_bindgen::JsValue; + + let wants = parse_want_oids(&want_oids).map_err(|err| JsValue::from_str(&err.to_string()))?; + if wants.is_empty() { + return Err(JsValue::from_str( + "blobless fetch requires at least one want", + )); + } + fetch_upload_pack(repo, repo_url, wants, Some("blob:none".to_string()), true).await +} + +#[cfg(target_arch = "wasm32")] +fn parse_want_oids(want_oids: &[String]) -> Result> { + want_oids + .iter() + .map(|oid| ObjectId::from_hex(oid)) + .collect::>>() +} + +#[cfg(target_arch = "wasm32")] +async fn fetch_upload_pack( + repo: &mut crate::WasmRepository, + repo_url: &str, + wants: Vec, + filter_spec: Option, + require_filter: bool, +) -> std::result::Result { + use grit_lib::smart_protocol::{ + build_fetch_v2_request, capability_lines_for_client_request, + extract_packfile_from_fetch_response, fetch_features_from_caps, + parse_upload_pack_advertisement, strip_http_service_advertisement_if_present, + FetchRequestOptions, UploadPackAdvertisement, + }; + use grit_lib::unpack_objects::{unpack_objects_into_store, UnpackOptions}; + use std::io::Cursor; + use wasm_bindgen::JsValue; + + if wants.is_empty() { + return Err(JsValue::from_str( + "upload-pack fetch requires at least one want", + )); + } + + let discovery_url = info_refs_url(repo_url, "git-upload-pack") + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let discovery = + crate::browser_http::fetch_git_discovery(&discovery_url, Some("version=2")).await?; + let pkt_body = strip_http_service_advertisement_if_present(&discovery) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let (caps, object_format) = match parse_upload_pack_advertisement(pkt_body) + .map_err(|err| JsValue::from_str(&err.to_string()))? + { + UploadPackAdvertisement::V2 { + caps, + object_format, + } => (caps, object_format), + UploadPackAdvertisement::V0V1 { .. } => { + return Err(JsValue::from_str("blobless fetch requires protocol v2")); + } + }; + let fetch_features = fetch_features_from_caps(&caps); + if require_filter && !fetch_features.contains("filter") { + return Err(JsValue::from_str( + "remote upload-pack does not advertise filter support", + )); + } + let request = build_fetch_v2_request( + &fetch_features, + &FetchRequestOptions { + object_format, + capability_lines: capability_lines_for_client_request(&caps), + wants, + include_done: true, + filter_spec, + ..FetchRequestOptions::default() + }, + ) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let rpc_url = service_url(repo_url, "git-upload-pack") + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let response = crate::browser_http::fetch_git_rpc( + &rpc_url, + "git-upload-pack", + &request, + Some("version=2"), + ) + .await?; + let pack = extract_packfile_from_fetch_response(&response) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let pack_id = repo + .packs + .add_pack(pack.clone(), true) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let mut cursor = Cursor::new(pack); + unpack_objects_into_store( + &mut cursor, + &mut repo.objects, + &UnpackOptions { + quiet: true, + ..UnpackOptions::default() + }, + ) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + Ok(pack_id.as_str().to_string()) +} + +/// Fetch a promised object by ID from the recorded `origin` promisor remote. +/// +/// Returns `true` when the object is available after the fetch. +#[cfg(target_arch = "wasm32")] +pub async fn fetch_promised_object( + repo: &mut crate::WasmRepository, + oid_hex: &str, +) -> std::result::Result { + use wasm_bindgen::JsValue; + + if repo.has_object_hex(oid_hex) { + return Ok(true); + } + let oid = ObjectId::from_hex(oid_hex).map_err(|err| JsValue::from_str(&err.to_string()))?; + let remote = repo + .promisor_remote("origin") + .ok_or_else(|| JsValue::from_str("missing origin promisor remote"))?; + let _pack_id = fetch_upload_pack(repo, &remote.url, vec![oid], None, false).await?; + if repo.has_object_hex(oid_hex) { + Ok(true) + } else { + Err(JsValue::from_str( + "promisor fetch did not hydrate requested object", + )) + } +} + +/// Read a blob, fetching it from the promisor remote first if it is missing. +#[cfg(target_arch = "wasm32")] +pub async fn read_blob_promised( + repo: &mut crate::WasmRepository, + oid_hex: &str, +) -> std::result::Result, wasm_bindgen::JsValue> { + use wasm_bindgen::JsValue; + + if let Some(bytes) = repo.read_blob(oid_hex) { + return Ok(bytes); + } + fetch_promised_object(repo, oid_hex).await?; + repo.read_blob(oid_hex) + .ok_or_else(|| JsValue::from_str("hydrated object is not a blob")) +} + +#[cfg(target_arch = "wasm32")] +async fn read_object_promised( + repo: &mut crate::WasmRepository, + oid: ObjectId, +) -> std::result::Result { + use wasm_bindgen::JsValue; + + if !repo.has_object_hex(&oid.to_hex()) { + fetch_promised_object(repo, &oid.to_hex()).await?; + } + repo.objects + .read_object(&oid) + .map_err(|err| JsValue::from_str(&err.to_string())) +} + +/// Read a file at `path` from `HEAD`, lazily hydrating missing tree/blob objects. +/// +/// The path is repository-relative and uses `/` separators. The returned bytes +/// are the raw blob contents. +#[cfg(target_arch = "wasm32")] +pub async fn read_path_promised( + repo: &mut crate::WasmRepository, + path: &str, +) -> std::result::Result, wasm_bindgen::JsValue> { + use grit_lib::objects::{parse_commit, parse_tree, ObjectKind}; + use wasm_bindgen::JsValue; + + const TREE_MODE: u32 = 0o040000; + + let components = path_components(path).map_err(|err| JsValue::from_str(&err.to_string()))?; + let head_oid = repo + .refs + .resolve_ref("HEAD") + .map_err(|err| JsValue::from_str(&err.to_string()))? + .ok_or_else(|| JsValue::from_str("HEAD is not set"))?; + let head = read_object_promised(repo, head_oid).await?; + if head.kind != ObjectKind::Commit { + return Err(JsValue::from_str("HEAD does not point to a commit")); + } + let commit = parse_commit(&head.data).map_err(|err| JsValue::from_str(&err.to_string()))?; + let mut current_tree_oid = commit.tree; + + for (idx, component) in components.iter().enumerate() { + let tree = read_object_promised(repo, current_tree_oid).await?; + if tree.kind != ObjectKind::Tree { + return Err(JsValue::from_str("tree walk reached a non-tree object")); + } + let entries = parse_tree(&tree.data).map_err(|err| JsValue::from_str(&err.to_string()))?; + let entry = entries + .iter() + .find(|entry| entry.name.as_slice() == component.as_bytes()) + .ok_or_else(|| JsValue::from_str(&format!("path not found: {path}")))?; + let is_last = idx + 1 == components.len(); + if is_last { + if entry.mode == TREE_MODE { + return Err(JsValue::from_str("path points to a tree")); + } + let object = read_object_promised(repo, entry.oid).await?; + if object.kind != ObjectKind::Blob { + return Err(JsValue::from_str("path does not point to a blob")); + } + return Ok(object.data); + } + if entry.mode != TREE_MODE { + return Err(JsValue::from_str(&format!( + "path component is not a tree: {component}" + ))); + } + current_tree_oid = entry.oid; + } + + Err(JsValue::from_str("path not found")) +} + +#[cfg(any(target_arch = "wasm32", test))] +fn path_components(path: &str) -> Result> { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Err(Error::PathError("path is empty".to_string())); + } + if trimmed + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + { + return Err(Error::PathError(format!("invalid repository path: {path}"))); + } + Ok(trimmed.split('/').map(ToOwned::to_owned).collect()) +} + +/// Result metadata for a blobless clone. +#[cfg(any(target_arch = "wasm32", test))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CloneSelection { + /// Local branch name, without `refs/heads/`. + pub branch: String, + /// Local branch reference updated by the clone. + pub local_ref: String, + /// Remote-tracking reference updated by the clone. + pub remote_ref: String, + /// Commit OID fetched for the selected branch. + pub oid: ObjectId, +} + +#[cfg(any(target_arch = "wasm32", test))] +fn select_clone_ref(refs: &[AdvertisedRef], branch_or_ref: Option<&str>) -> Result { + let target_ref = if let Some(requested) = branch_or_ref.filter(|value| !value.trim().is_empty()) + { + if requested.starts_with("refs/") { + requested.to_string() + } else { + format!("refs/heads/{requested}") + } + } else if let Some(head_target) = refs + .iter() + .find(|entry| entry.name == "HEAD") + .and_then(|entry| entry.symref_target.clone()) + { + head_target + } else if refs.iter().any(|entry| entry.name == "refs/heads/main") { + "refs/heads/main".to_string() + } else if refs.iter().any(|entry| entry.name == "refs/heads/master") { + "refs/heads/master".to_string() + } else { + refs.iter() + .find(|entry| entry.name.starts_with("refs/heads/")) + .map(|entry| entry.name.clone()) + .ok_or_else(|| Error::Message("remote did not advertise any branches".to_string()))? + }; + + let advertised = refs + .iter() + .find(|entry| entry.name == target_ref) + .ok_or_else(|| Error::Message(format!("remote did not advertise {target_ref}")))?; + let branch = target_ref + .strip_prefix("refs/heads/") + .ok_or_else(|| Error::Message(format!("{target_ref} is not a branch ref")))? + .to_string(); + Ok(CloneSelection { + branch: branch.clone(), + local_ref: target_ref, + remote_ref: format!("refs/remotes/origin/{branch}"), + oid: advertised.oid, + }) +} + +/// Perform a blobless clone into an in-memory repository. +/// +/// This fetches the selected branch with `filter blob:none`, updates local and +/// remote-tracking refs, points `HEAD` at the local branch, and records `origin` +/// as a promisor remote with the `blob:none` filter. +#[cfg(target_arch = "wasm32")] +pub async fn clone_blobless( + repo: &mut crate::WasmRepository, + repo_url: &str, + branch_or_ref: Option, +) -> std::result::Result { + use js_sys::{Object, Reflect}; + use wasm_bindgen::JsValue; + + let refs = fetch_refs(repo_url).await?; + let selection = select_clone_ref(&refs, branch_or_ref.as_deref()) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let pack_id = fetch_blobless(repo, repo_url, vec![selection.oid.to_hex()]).await?; + + repo.refs + .set_ref(&selection.local_ref, selection.oid) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + repo.refs + .set_ref(&selection.remote_ref, selection.oid) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + repo.refs + .set_symbolic_ref("HEAD", &selection.local_ref) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + repo.refs + .set_symbolic_ref( + "refs/remotes/origin/HEAD", + &format!("refs/remotes/origin/{}", selection.branch), + ) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + repo.set_promisor_remote("origin", repo_url, "blob:none"); + + let out = Object::new(); + Reflect::set( + &out, + &JsValue::from_str("branch"), + &JsValue::from_str(&selection.branch), + )?; + Reflect::set( + &out, + &JsValue::from_str("head"), + &JsValue::from_str(&selection.local_ref), + )?; + Reflect::set( + &out, + &JsValue::from_str("oid"), + &JsValue::from_str(&selection.oid.to_hex()), + )?; + Reflect::set( + &out, + &JsValue::from_str("packId"), + &JsValue::from_str(&pack_id), + )?; + Ok(out) +} + +#[cfg(any(target_arch = "wasm32", test))] +fn remote_tracking_ref_for_push(remote_ref: &str) -> Result { + let branch = remote_ref.strip_prefix("refs/heads/").ok_or_else(|| { + Error::InvalidRef(format!("can only push branch refs for now: {remote_ref}")) + })?; + Ok(format!("refs/remotes/origin/{branch}")) +} + +/// Push a local branch to a smart-HTTP remote using a non-delta pack. +#[cfg(target_arch = "wasm32")] +pub async fn push( + repo: &mut crate::WasmRepository, + repo_url: &str, + local_ref: &str, + remote_ref: &str, +) -> std::result::Result { + use grit_lib::smart_protocol::{ + build_receive_pack_request, parse_receive_pack_response, PushCommand, + ReceivePackCapabilities, + }; + use js_sys::{Array, Object, Reflect}; + use wasm_bindgen::JsValue; + + let local_oid = repo + .refs + .resolve_ref(local_ref) + .map_err(|err| JsValue::from_str(&err.to_string()))? + .ok_or_else(|| JsValue::from_str(&format!("local ref not found: {local_ref}")))?; + let advertised = discover_receive_pack(repo_url).await?; + let old_oid = advertised.advertised_oid(remote_ref); + let pack = repo + .write_pack_for_push(&local_oid.to_hex(), old_oid.map(|oid| oid.to_hex())) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let capabilities = ReceivePackCapabilities { + advertised: advertised.capabilities, + agent: Some(format!("grit-wasm/{}", crate::WasmRepository::version())), + session_id: None, + }; + let command = PushCommand { + old_oid, + new_oid: Some(local_oid), + refname: remote_ref.to_string(), + }; + let (request, use_sideband) = + build_receive_pack_request(&capabilities, &[command], &[], &pack, false) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let rpc_url = service_url(repo_url, "git-receive-pack") + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let response = + crate::browser_http::fetch_git_rpc(&rpc_url, "git-receive-pack", &request, None).await?; + let status = parse_receive_pack_response(&response, use_sideband) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let ref_ok = status + .statuses + .iter() + .find(|entry| entry.refname == remote_ref) + .map(|entry| entry.ok) + .unwrap_or(status.statuses.is_empty() && status.unpack_ok); + if !status.unpack_ok || !ref_ok { + let message = status + .statuses + .iter() + .find(|entry| entry.refname == remote_ref) + .and_then(|entry| entry.message.clone()) + .unwrap_or(status.unpack_message.clone()); + return Err(JsValue::from_str(&format!("push rejected: {message}"))); + } + + let tracking_ref = remote_tracking_ref_for_push(remote_ref) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + repo.refs + .set_ref(&tracking_ref, local_oid) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + + let out = Object::new(); + Reflect::set( + &out, + &JsValue::from_str("remoteRef"), + &JsValue::from_str(remote_ref), + )?; + Reflect::set( + &out, + &JsValue::from_str("trackingRef"), + &JsValue::from_str(&tracking_ref), + )?; + Reflect::set( + &out, + &JsValue::from_str("oid"), + &JsValue::from_str(&local_oid.to_hex()), + )?; + Reflect::set( + &out, + &JsValue::from_str("unpackOk"), + &JsValue::from_bool(status.unpack_ok), + )?; + let status_array = Array::new(); + for entry in status.statuses { + let obj = Object::new(); + Reflect::set( + &obj, + &JsValue::from_str("refname"), + &JsValue::from_str(&entry.refname), + )?; + Reflect::set( + &obj, + &JsValue::from_str("ok"), + &JsValue::from_bool(entry.ok), + )?; + if let Some(message) = entry.message { + Reflect::set( + &obj, + &JsValue::from_str("message"), + &JsValue::from_str(&message), + )?; + } + status_array.push(&obj); + } + Reflect::set(&out, &JsValue::from_str("statuses"), &status_array)?; + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_discovery_url_without_existing_query() { + let url = info_refs_url("https://example.com/repo.git", "git-upload-pack").unwrap(); + + assert_eq!( + url, + "https://example.com/repo.git/info/refs?service=git-upload-pack" + ); + } + + #[test] + fn builds_discovery_url_with_existing_query() { + let url = info_refs_url("https://example.com/repo.git?x=1", "git-upload-pack").unwrap(); + + assert_eq!( + url, + "https://example.com/repo.git?x=1/info/refs&service=git-upload-pack" + ); + } + + #[test] + fn builds_service_url() { + let url = service_url("https://example.com/repo.git/", "git-upload-pack").unwrap(); + + assert_eq!(url, "https://example.com/repo.git/git-upload-pack"); + } + + #[test] + fn selects_head_symref_for_clone() { + let oid = ObjectId::from_hex("67bf698f3ab735e92fb011a99cff3497c44d30c1").unwrap(); + let refs = vec![ + AdvertisedRef { + name: "HEAD".to_string(), + oid, + symref_target: Some("refs/heads/main".to_string()), + }, + AdvertisedRef { + name: "refs/heads/main".to_string(), + oid, + symref_target: None, + }, + ]; + + let selected = select_clone_ref(&refs, None).unwrap(); + + assert_eq!(selected.branch, "main"); + assert_eq!(selected.local_ref, "refs/heads/main"); + assert_eq!(selected.remote_ref, "refs/remotes/origin/main"); + assert_eq!(selected.oid, oid); + } + + #[test] + fn selects_requested_branch_for_clone() { + let oid = ObjectId::from_hex("67bf698f3ab735e92fb011a99cff3497c44d30c1").unwrap(); + let refs = vec![AdvertisedRef { + name: "refs/heads/topic".to_string(), + oid, + symref_target: None, + }]; + + let selected = select_clone_ref(&refs, Some("topic")).unwrap(); + + assert_eq!(selected.branch, "topic"); + assert_eq!(selected.local_ref, "refs/heads/topic"); + } + + #[test] + fn maps_branch_push_to_origin_tracking_ref() { + assert_eq!( + remote_tracking_ref_for_push("refs/heads/main").unwrap(), + "refs/remotes/origin/main" + ); + assert!(remote_tracking_ref_for_push("refs/tags/v1").is_err()); + } + + #[test] + fn validates_path_components() { + assert_eq!( + path_components("/src/lib.rs").unwrap(), + vec!["src".to_string(), "lib.rs".to_string()] + ); + assert!(path_components("").is_err()); + assert!(path_components("src/../secret").is_err()); + } +} diff --git a/grit-wasm/tests/browser_network.rs b/grit-wasm/tests/browser_network.rs new file mode 100644 index 000000000..b2a6fb4ca --- /dev/null +++ b/grit-wasm/tests/browser_network.rs @@ -0,0 +1,33 @@ +#![cfg(target_arch = "wasm32")] + +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +/// Exercise smart-HTTP ref discovery against an opt-in CORS-enabled server. +/// +/// Compile/run with: +/// +/// ```text +/// GRIT_WASM_TEST_REMOTE_URL=http://127.0.0.1:/smart/repo.git \ +/// wasm-pack test --chrome --headless grit-wasm +/// ``` +/// +/// The server should be `test-httpd --cors` or equivalent and must support +/// protocol-v2 upload-pack discovery plus `ls-refs`. +#[wasm_bindgen_test(async)] +async fn ls_refs_against_cors_smart_http_remote() { + let Some(remote_url) = option_env!("GRIT_WASM_TEST_REMOTE_URL") else { + // Opt-in integration test: normal wasm test compiles should not require + // a live Git HTTP server. + return; + }; + let refs = grit_wasm::remote::ls_refs(remote_url) + .await + .expect("ls_refs against CORS smart HTTP remote"); + + assert!( + refs.length() > 0, + "remote should advertise at least one ref" + ); +} diff --git a/grit/src/bin/test_httpd.rs b/grit/src/bin/test_httpd.rs index 62033d81e..d181dada1 100644 --- a/grit/src/bin/test_httpd.rs +++ b/grit/src/bin/test_httpd.rs @@ -12,6 +12,7 @@ //! Usage: //! test-httpd --root /path/to/docroot [--auth user:pass] [--port 0] //! [--proxy] [--proxy-auth proxuser:proxpass] +//! [--cors] //! //! On startup, prints "READY " to stdout, then serves until killed. @@ -22,10 +23,14 @@ use std::io::{BufRead, BufReader, Read, Write}; use std::net::{TcpListener, TcpStream}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; + +static CORS_ENABLED: AtomicBool = AtomicBool::new(false); fn main() { let args: Vec = env::args().collect(); let config = parse_args(&args); + CORS_ENABLED.store(config.cors, Ordering::Relaxed); let listener = TcpListener::bind(format!("127.0.0.1:{}", config.port)).unwrap_or_else(|e| { eprintln!("Failed to bind: {e}"); @@ -86,6 +91,8 @@ struct Config { /// Path to git-http-backend (auto-detected if not specified) git_http_backend: PathBuf, access_log: PathBuf, + /// When set, emit permissive CORS headers and answer OPTIONS preflight requests. + cors: bool, } fn find_git_http_backend() -> PathBuf { @@ -121,6 +128,7 @@ fn parse_args(args: &[String]) -> Config { let mut proxy_auth_pass = None; let mut pid_file = None; let mut git_http_backend = find_git_http_backend(); + let mut cors = false; let mut i = 1; while i < args.len() { @@ -158,6 +166,9 @@ fn parse_args(args: &[String]) -> Config { i += 1; git_http_backend = PathBuf::from(&args[i]); } + "--cors" => { + cors = true; + } other => { eprintln!("Unknown argument: {other}"); std::process::exit(1); @@ -178,6 +189,7 @@ fn parse_args(args: &[String]) -> Config { pid_file, git_http_backend, access_log, + cors, } } @@ -410,6 +422,11 @@ fn handle_connection(mut stream: TcpStream, config: &Config) -> Result<(), Strin } ); + if req.method.eq_ignore_ascii_case("OPTIONS") && config.cors { + log_access(config, &req.method, &req.path, &req.query, 204); + return send_response(&mut stream, 204, "No Content", &[], b""); + } + let needs_auth = if req.path.starts_with("/auth-push/") { req.path.contains("git-receive-pack") || req.query.contains("service=git-receive-pack") } else if req.path.starts_with("/auth-fetch/") { @@ -1028,6 +1045,7 @@ fn parse_and_send_cgi_response(stream: &mut TcpStream, cgi_output: &[u8]) -> Res for (k, v) in &extra_headers { response.push_str(&format!("{k}: {v}\r\n")); } + append_cors_headers(&mut response); response.push_str(&format!("Content-Length: {}\r\n", body.len())); response.push_str("Connection: close\r\n"); response.push_str("\r\n"); @@ -1051,6 +1069,7 @@ fn send_response( for (k, v) in headers { response.push_str(&format!("{k}: {v}\r\n")); } + append_cors_headers(&mut response); response.push_str(&format!("Content-Length: {}\r\n", body.len())); response.push_str("Connection: close\r\n"); response.push_str("\r\n"); @@ -1062,3 +1081,17 @@ fn send_response( stream.flush().map_err(|e| e.to_string())?; Ok(()) } + +fn append_cors_headers(response: &mut String) { + if !CORS_ENABLED.load(Ordering::Relaxed) { + return; + } + response.push_str("Access-Control-Allow-Origin: *\r\n"); + response.push_str("Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"); + response.push_str( + "Access-Control-Allow-Headers: Authorization, Content-Type, Git-Protocol, Accept\r\n", + ); + response.push_str( + "Access-Control-Expose-Headers: Content-Type, Git-Protocol, WWW-Authenticate\r\n", + ); +} diff --git a/grit/src/http_bundle_uri.rs b/grit/src/http_bundle_uri.rs index 8dba4d443..2eba8a0bd 100644 --- a/grit/src/http_bundle_uri.rs +++ b/grit/src/http_bundle_uri.rs @@ -1,10 +1,9 @@ //! Smart HTTP client for protocol v2 `bundle-uri` (test-tool / harness). use anyhow::{bail, Context, Result}; +use grit_lib::pkt_line; use std::io::{Cursor, Read}; -use crate::pkt_line; - const SERVICE: &str = "git-upload-pack"; /// Skip the v0 smart-HTTP `# service=git-upload-pack` advertisement (pkt-lines until flush). diff --git a/grit/src/http_push_smart.rs b/grit/src/http_push_smart.rs index 170dfa15f..5207bcd10 100644 --- a/grit/src/http_push_smart.rs +++ b/grit/src/http_push_smart.rs @@ -4,13 +4,14 @@ //! `http://` and `https://` remotes. use std::collections::HashSet; -use std::io::{Cursor, Read, Write}; +use std::io::{Read, Write}; use anyhow::{bail, Context, Result}; use grit_lib::objects::ObjectId; +use grit_lib::pkt_line; +use grit_lib::smart_protocol; use crate::http_bundle_uri::strip_v0_service_advertisement_if_present; -use crate::pkt_line; const SERVICE: &str = "git-receive-pack"; @@ -52,74 +53,6 @@ impl ReceivePackAdvertisement { } } -fn read_v2_caps(body: &[u8]) -> Result> { - let mut cur = Cursor::new(body); - let first = match pkt_line::read_packet(&mut cur)? { - None => bail!("empty v2 capability block"), - Some(pkt_line::Packet::Data(s)) => s, - Some(other) => bail!("expected version line, got {other:?}"), - }; - if first != "version 2" { - bail!("expected 'version 2', got {first:?}"); - } - let mut caps = vec![first]; - loop { - match pkt_line::read_packet(&mut cur)? { - None => bail!("unexpected EOF in v2 capabilities"), - Some(pkt_line::Packet::Flush) => break, - Some(pkt_line::Packet::Data(s)) => caps.push(s), - Some(other) => bail!("unexpected packet in v2 capabilities: {other:?}"), - } - } - Ok(caps) -} - -fn parse_v0_v1_advertisement( - body: &[u8], -) -> Result<(Vec, HashSet)> { - let mut cur = Cursor::new(body); - let mut refs = Vec::new(); - let mut caps = HashSet::new(); - let mut first_ref_line = true; - loop { - match pkt_line::read_packet(&mut cur)? { - None => break, - Some(pkt_line::Packet::Flush) => break, - Some(pkt_line::Packet::Data(line)) => { - let line = line.trim_end_matches('\n'); - if line.starts_with("version ") { - continue; - } - let (payload, cap_part) = match line.split_once('\0') { - Some((p, c)) => (p.trim(), Some(c)), - None => (line.trim(), None), - }; - let (oid_hex, refname) = payload - .split_once('\t') - .or_else(|| payload.split_once(' ')) - .ok_or_else(|| anyhow::anyhow!("malformed v0/v1 advertisement: {line}"))?; - let oid = ObjectId::from_hex(oid_hex.trim()) - .with_context(|| format!("bad oid in receive-pack advertisement: {oid_hex}"))?; - let refname = refname.trim(); - if first_ref_line { - if let Some(raw_caps) = cap_part { - for cap in raw_caps.split_whitespace() { - caps.insert(cap.to_string()); - } - } - first_ref_line = false; - } - refs.push(ReceivePackAdvertisedRef { - name: refname.to_string(), - oid, - }); - } - Some(other) => bail!("unexpected packet in v0/v1 advertisement: {other:?}"), - } - } - Ok((refs, caps)) -} - /// Discover `git-receive-pack` refs/capabilities for an HTTP(S) remote URL. pub(crate) fn discover_receive_pack( repo_url: &str, @@ -133,42 +66,20 @@ pub(crate) fn discover_receive_pack( let body = client.get(&refs_url)?; let pkt_body = strip_v0_service_advertisement_if_present(&body)?; - let mut probe = Cursor::new(pkt_body); - let first = match pkt_line::read_packet(&mut probe)? { - None => bail!("empty smart-http receive-pack advertisement"), - Some(pkt_line::Packet::Data(s)) => s, - Some(other) => bail!("unexpected first receive-pack advertisement packet: {other:?}"), - }; - let service_url = format!("{base}/{SERVICE}"); - if first == "version 2" { - let caps = read_v2_caps(pkt_body)?; - let object_format = caps - .iter() - .find_map(|c| c.strip_prefix("object-format=")) - .unwrap_or("sha1") - .to_string(); - return Ok(ReceivePackAdvertisement { - protocol_version: 2, - refs: Vec::new(), - capabilities: caps.into_iter().collect(), - object_format, - service_url, - }); - } - - let (refs, caps) = parse_v0_v1_advertisement(pkt_body)?; - let protocol_version = if first == "version 1" { 1 } else { 0 }; - let object_format = caps - .iter() - .find_map(|c| c.strip_prefix("object-format=")) - .unwrap_or("sha1") - .to_string(); + let parsed = smart_protocol::parse_receive_pack_advertisement(pkt_body)?; Ok(ReceivePackAdvertisement { - protocol_version, - refs, - capabilities: caps, - object_format, + protocol_version: parsed.protocol_version, + refs: parsed + .refs + .into_iter() + .map(|entry| ReceivePackAdvertisedRef { + name: entry.name, + oid: entry.oid, + }) + .collect(), + capabilities: parsed.capabilities, + object_format: parsed.object_format, service_url, }) } @@ -303,139 +214,6 @@ pub(crate) struct PushStatusReport { pub(crate) sideband_stderr: Vec, } -fn format_push_old_new(oid: Option) -> String { - oid.map(|o| o.to_hex()).unwrap_or_else(|| "0".repeat(40)) -} - -fn client_push_capabilities( - advertised: &ReceivePackAdvertisement, - atomic: bool, - push_options: &[String], -) -> Result> { - let mut out = Vec::new(); - if advertised.supports("report-status-v2") { - out.push("report-status-v2".to_string()); - } else if advertised.supports("report-status") { - out.push("report-status".to_string()); - } else { - bail!("remote does not support report-status"); - } - if advertised.supports("ofs-delta") { - out.push("ofs-delta".to_string()); - } - if advertised.supports("side-band-64k") { - out.push("side-band-64k".to_string()); - } else if advertised.supports("side-band") { - out.push("side-band".to_string()); - } - if atomic { - if !advertised.supports("atomic") { - bail!("the receiving end does not support --atomic push"); - } - out.push("atomic".to_string()); - } - if !push_options.is_empty() { - if !advertised.supports("push-options") { - bail!("the receiving end does not support push options"); - } - out.push("push-options".to_string()); - } - if advertised.supports("agent") { - out.push(format!("agent={}", crate::http_smart::agent_header())); - } - if advertised.supports("object-format") { - out.push("object-format=sha1".to_string()); - } - if advertised.supports("session-id") { - out.push(format!( - "session-id={}", - crate::trace2_transfer::trace2_session_id_wire_once() - )); - } - Ok(out) -} - -fn decode_sideband_stream(body: &[u8]) -> Result<(Vec, Vec)> { - let mut i = 0usize; - let mut primary = Vec::new(); - let mut stderr = Vec::new(); - while i + 4 <= body.len() { - let len_str = std::str::from_utf8(&body[i..i + 4]) - .with_context(|| format!("invalid sideband length header at offset {i}"))?; - let pkt_len = usize::from_str_radix(len_str, 16) - .with_context(|| format!("invalid sideband length value '{len_str}'"))?; - i += 4; - if pkt_len == 0 { - break; - } - if pkt_len < 5 || i + (pkt_len - 4) > body.len() { - bail!("truncated sideband packet in push response"); - } - let payload = &body[i..i + (pkt_len - 4)]; - i += pkt_len - 4; - let (band, data) = (payload[0], &payload[1..]); - match band { - 1 => primary.extend_from_slice(data), - 2 | 3 => stderr.extend_from_slice(data), - _ => {} - } - } - Ok((primary, stderr)) -} - -fn parse_report_status_body(body: &[u8]) -> Result { - let mut cur = Cursor::new(body); - let unpack_line = match pkt_line::read_packet(&mut cur)? { - Some(pkt_line::Packet::Data(line)) => line, - Some(other) => bail!("unexpected first report-status packet: {other:?}"), - None => bail!("empty report-status response"), - }; - let unpack_line = unpack_line.trim_end_matches('\n').to_string(); - let unpack_ok = unpack_line == "unpack ok"; - let unpack_message = unpack_line - .strip_prefix("unpack ") - .unwrap_or(unpack_line.as_str()) - .to_string(); - - let mut statuses = Vec::new(); - loop { - match pkt_line::read_packet(&mut cur)? { - Some(pkt_line::Packet::Data(line)) => { - let line = line.trim_end_matches('\n'); - if let Some(rest) = line.strip_prefix("ok ") { - statuses.push(PushStatusEntry { - refname: rest.trim().to_string(), - ok: true, - message: None, - }); - continue; - } - if let Some(rest) = line.strip_prefix("ng ") { - let (refname, message) = rest - .split_once(' ') - .map(|(r, m)| (r.trim(), Some(m.trim().to_string()))) - .unwrap_or((rest.trim(), None)); - statuses.push(PushStatusEntry { - refname: refname.to_string(), - ok: false, - message, - }); - continue; - } - } - Some(pkt_line::Packet::Flush) | None => break, - Some(pkt_line::Packet::Delim | pkt_line::Packet::ResponseEnd) => {} - } - } - - Ok(PushStatusReport { - unpack_ok, - unpack_message, - statuses, - sideband_stderr: Vec::new(), - }) -} - /// Send a smart-HTTP `git-receive-pack` request and parse `report-status`. pub(crate) fn send_receive_pack( client: &crate::http_client::HttpClientContext, @@ -494,47 +272,42 @@ fn build_receive_pack_request( pack_data: &[u8], atomic: bool, ) -> Result<(Vec, bool)> { - let caps = client_push_capabilities(advertised, atomic, push_options)?; - let mut request = Vec::new(); - for (idx, cmd) in commands.iter().enumerate() { - let old_hex = format_push_old_new(cmd.old_oid); - let new_hex = format_push_old_new(cmd.new_oid); - let mut payload = format!("{old_hex} {new_hex} {}", cmd.refname); - if idx == 0 && !caps.is_empty() { - payload.push('\0'); - payload.push_str(&caps.join(" ")); - } - payload.push('\n'); - pkt_line::write_packet_raw(&mut request, payload.as_bytes())?; - } - pkt_line::write_flush(&mut request)?; - - if !push_options.is_empty() { - for opt in push_options { - pkt_line::write_line_to_vec(&mut request, opt)?; - } - pkt_line::write_flush(&mut request)?; - } - - let delete_only = commands.iter().all(|cmd| cmd.new_oid.is_none()); - if !delete_only { - request.extend_from_slice(pack_data); - } - - let use_sideband = caps + let capabilities = smart_protocol::ReceivePackCapabilities { + advertised: advertised.capabilities.clone(), + agent: Some(crate::http_smart::agent_header()), + session_id: Some(crate::trace2_transfer::trace2_session_id_wire_once()), + }; + let commands = commands .iter() - .any(|c| c == "side-band-64k" || c == "side-band"); - Ok((request, use_sideband)) + .map(|cmd| smart_protocol::PushCommand { + old_oid: cmd.old_oid, + new_oid: cmd.new_oid, + refname: cmd.refname.clone(), + }) + .collect::>(); + Ok(smart_protocol::build_receive_pack_request( + &capabilities, + &commands, + push_options, + pack_data, + atomic, + )?) } fn parse_receive_pack_response(response: Vec, use_sideband: bool) -> Result { - let (primary, sideband_stderr) = if use_sideband { - decode_sideband_stream(&response)? - } else { - (response, Vec::new()) - }; - - let mut status = parse_report_status_body(&primary)?; - status.sideband_stderr = sideband_stderr; - Ok(status) + let status = smart_protocol::parse_receive_pack_response(&response, use_sideband)?; + Ok(PushStatusReport { + unpack_ok: status.unpack_ok, + unpack_message: status.unpack_message, + statuses: status + .statuses + .into_iter() + .map(|entry| PushStatusEntry { + refname: entry.refname, + ok: entry.ok, + message: entry.message, + }) + .collect(), + sideband_stderr: status.sideband_stderr, + }) } diff --git a/grit/src/http_smart.rs b/grit/src/http_smart.rs index 6c2630132..e4786b42f 100644 --- a/grit/src/http_smart.rs +++ b/grit/src/http_smart.rs @@ -12,12 +12,13 @@ use anyhow::{bail, Context, Result}; use grit_lib::fetch_negotiator::SkippingNegotiator; use grit_lib::merge_base; use grit_lib::objects::ObjectId; +use grit_lib::pkt_line; use grit_lib::refs; use grit_lib::repo::Repository; use grit_lib::rev_parse::resolve_revision; +use grit_lib::smart_protocol; use crate::http_bundle_uri::strip_v0_service_advertisement_if_present; -use crate::pkt_line; const SERVICE: &str = "git-upload-pack"; @@ -100,78 +101,6 @@ fn http_post_discovery( client.post_with_git_protocol(url, content_type, accept, body, git_protocol_header) } -fn read_v2_caps(body: &[u8]) -> Result> { - let mut cur = Cursor::new(body); - let first = match pkt_line::read_packet(&mut cur)? { - None => bail!("empty v2 capability block"), - Some(pkt_line::Packet::Data(s)) => s, - Some(other) => bail!("expected version line, got {other:?}"), - }; - if first != "version 2" { - bail!("expected 'version 2', got {first:?}"); - } - let mut caps = vec![first]; - loop { - match pkt_line::read_packet(&mut cur)? { - None => bail!("unexpected EOF in v2 capabilities"), - Some(pkt_line::Packet::Flush) => break, - Some(pkt_line::Packet::Data(s)) => caps.push(s), - Some(other) => bail!("unexpected packet in v2 caps: {other:?}"), - } - } - Ok(caps) -} - -fn parse_v0_v1_advertisement( - body: &[u8], -) -> Result<(Vec, std::collections::HashSet)> { - let mut cur = Cursor::new(body); - let mut refs = Vec::new(); - let mut caps = std::collections::HashSet::new(); - let mut first_ref_line = true; - loop { - match pkt_line::read_packet(&mut cur)? { - None => break, - Some(pkt_line::Packet::Flush) => break, - Some(pkt_line::Packet::Data(line)) => { - let line = line.trim_end_matches('\n'); - if line.starts_with("version ") { - crate::trace_packet::trace_packet_git('<', line); - continue; - } - let (payload, cap_part) = match line.split_once('\0') { - Some((p, c)) => (p.trim(), Some(c)), - None => (line.trim(), None), - }; - let (oid_hex, refname) = payload - .split_once('\t') - .or_else(|| payload.split_once(' ')) - .ok_or_else(|| anyhow::anyhow!("malformed v0/v1 advertisement: {line}"))?; - let oid = ObjectId::from_hex(oid_hex.trim()) - .with_context(|| format!("bad oid in v0/v1 advertisement: {oid_hex}"))?; - let refname = refname.trim(); - if refname.is_empty() { - continue; - } - if first_ref_line { - if let Some(raw_caps) = cap_part { - for cap in raw_caps.split_whitespace() { - caps.insert(cap.to_string()); - } - } - first_ref_line = false; - } - refs.push(LsRefEntry { - name: refname.to_string(), - oid, - }); - } - Some(other) => bail!("unexpected packet in v0/v1 advertisement: {other:?}"), - } - } - Ok((refs, caps)) -} - enum HttpDiscovery { V2 { caps: Vec, @@ -184,26 +113,27 @@ enum HttpDiscovery { } fn discover_http_protocol(pkt_body: &[u8]) -> Result { - let mut cur = Cursor::new(pkt_body); - let first = match pkt_line::read_packet(&mut cur)? { - None => bail!("empty smart-http advertisement"), - Some(pkt_line::Packet::Data(s)) => s, - Some(other) => bail!("unexpected first advertisement packet: {other:?}"), - }; - if first == "version 2" { - let caps = read_v2_caps(pkt_body)?; - let object_format = caps - .iter() - .find_map(|c| c.strip_prefix("object-format=")) - .unwrap_or("sha1") - .to_string(); - return Ok(HttpDiscovery::V2 { + match smart_protocol::parse_upload_pack_advertisement(pkt_body)? { + smart_protocol::UploadPackAdvertisement::V2 { caps, object_format, - }); + } => Ok(HttpDiscovery::V2 { + caps, + object_format, + }), + smart_protocol::UploadPackAdvertisement::V0V1 { + refs, capabilities, .. + } => Ok(HttpDiscovery::V0V1 { + advertised: refs + .into_iter() + .map(|entry| LsRefEntry { + name: entry.name, + oid: entry.oid, + }) + .collect(), + caps: capabilities, + }), } - let (advertised, caps) = parse_v0_v1_advertisement(pkt_body)?; - Ok(HttpDiscovery::V0V1 { advertised, caps }) } fn trace_http_v0_v1_negotiated(client: &crate::http_client::HttpClientContext) { @@ -213,15 +143,16 @@ fn trace_http_v0_v1_negotiated(client: &crate::http_client::HttpClientContext) { } fn cap_lines_for_client_request(caps: &[String]) -> Vec { - let mut out = Vec::new(); - for line in caps { - if line.starts_with("agent=") { - out.push(line.clone()); - } else if let Some(fmt) = line.strip_prefix("object-format=") { - out.push(format!("object-format={fmt}")); - } - } - out + smart_protocol::capability_lines_for_client_request(caps) +} + +fn build_ls_refs_request(caps: &[String], object_format: &str) -> Result> { + Ok(smart_protocol::build_ls_refs_v2_request( + object_format, + &cap_lines_for_client_request(caps), + true, + true, + )?) } fn skip_to_flush(r: &mut Cursor<&[u8]>) -> Result<()> { @@ -362,16 +293,7 @@ pub fn http_ls_refs( HttpDiscovery::V0V1 { advertised, .. } => return Ok(advertised), }; - let mut req = Vec::new(); - pkt_line::write_line_to_vec(&mut req, "command=ls-refs")?; - pkt_line::write_line_to_vec(&mut req, &format!("object-format={object_format}"))?; - for line in cap_lines_for_client_request(&caps) { - pkt_line::write_line_to_vec(&mut req, &line)?; - } - pkt_line::write_delim(&mut req)?; - pkt_line::write_line_to_vec(&mut req, "peel")?; - pkt_line::write_line_to_vec(&mut req, "symrefs")?; - pkt_line::write_flush(&mut req)?; + let req = build_ls_refs_request(&caps, &object_format)?; let post_url = format!("{base}/{SERVICE}"); let resp = http_post( @@ -415,16 +337,7 @@ pub fn http_negotiate_only_common( bail!("server does not support wait-for-done"); } - let mut req = Vec::new(); - pkt_line::write_line_to_vec(&mut req, "command=ls-refs")?; - pkt_line::write_line_to_vec(&mut req, &format!("object-format={object_format}"))?; - for line in cap_lines_for_client_request(&caps) { - pkt_line::write_line_to_vec(&mut req, &line)?; - } - pkt_line::write_delim(&mut req)?; - pkt_line::write_line_to_vec(&mut req, "peel")?; - pkt_line::write_line_to_vec(&mut req, "symrefs")?; - pkt_line::write_flush(&mut req)?; + let req = build_ls_refs_request(&caps, &object_format)?; let post_url = format!("{base}/{SERVICE}"); let resp = http_post( @@ -464,26 +377,13 @@ pub fn http_negotiate_only_common( } fn parse_ls_refs_v2_response(data: &[u8]) -> Result> { - let mut cur = Cursor::new(data); - let mut out = Vec::new(); - loop { - let pkt = match pkt_line::read_packet(&mut cur)? { - None => break, - Some(pkt_line::Packet::Flush) => break, - Some(pkt_line::Packet::Data(line)) => line, - Some(other) => bail!("unexpected ls-refs packet: {other:?}"), - }; - let (oid_hex, rest) = pkt - .split_once(' ') - .ok_or_else(|| anyhow::anyhow!("bad ls-refs line: {pkt}"))?; - let oid = ObjectId::from_hex(oid_hex.trim())?; - let name = rest.split_whitespace().next().unwrap_or(rest).to_string(); - if name.is_empty() { - continue; - } - out.push(LsRefEntry { name, oid }); - } - Ok(out) + Ok(smart_protocol::parse_ls_refs_v2_response(data)? + .into_iter() + .map(|entry| LsRefEntry { + name: entry.name, + oid: entry.oid, + }) + .collect()) } fn collect_wants_from_advertised( @@ -979,16 +879,7 @@ pub fn http_fetch_pack( }; let advertised = { - let mut req = Vec::new(); - pkt_line::write_line_to_vec(&mut req, "command=ls-refs")?; - pkt_line::write_line_to_vec(&mut req, &format!("object-format={object_format}"))?; - for line in cap_lines_for_client_request(&caps) { - pkt_line::write_line_to_vec(&mut req, &line)?; - } - pkt_line::write_delim(&mut req)?; - pkt_line::write_line_to_vec(&mut req, "peel")?; - pkt_line::write_line_to_vec(&mut req, "symrefs")?; - pkt_line::write_flush(&mut req)?; + let req = build_ls_refs_request(&caps, &object_format)?; let post_url = format!("{base}/{SERVICE}"); let resp = http_post( diff --git a/logs/2026-04-27_17-28-wasm-build.md b/logs/2026-04-27_17-28-wasm-build.md new file mode 100644 index 000000000..49558c213 --- /dev/null +++ b/logs/2026-04-27_17-28-wasm-build.md @@ -0,0 +1,124 @@ +# WASM Build Plan and Bootstrap + +## Goal + +Plan and start a browser-targeted WASM build of Grit that can support blobless +HTTP fetch/clone, promisor blob hydration, object and pack handling, commit +creation, and smart-HTTP push. + +## Work Completed + +- Added `wasm-plan.md` with dependency-ordered implementation tasks. +- Added `grit-wasm` as a workspace member with `cdylib` and `rlib` outputs. +- Added an initial in-memory `WasmRepository` with object, ref, and symbolic + `HEAD` basics. +- Added native unit coverage for blob hashing/storage and ref/HEAD tracking. +- Added a narrow `wasm32` module surface in `grit-lib` so `grit-wasm` can reuse + portable object types without compiling native-only filesystem/date modules. +- Added `grit_lib::storage` traits for object, ref, and raw pack storage. +- Implemented the object storage traits for native `Odb`. +- Added a native `FileRefStore` adapter over the existing refs backend. +- Moved `grit-wasm` in-memory object, ref, and pack stores onto the shared + storage traits. +- Added storage-agnostic `write_tree` entry points while preserving the existing + `&Odb` API as compatibility wrappers. +- Added `grit_lib::commit::write_commit`, a storage-trait-based commit writer + that validates tree and parent object kinds before writing the commit. +- Added early `grit-wasm` APIs for raw tree object writes and commit object + creation from caller-provided tree, parents, identities, and message. +- Added storage-trait-based `unpack_objects_into_store` so pack streams can + unpack into browser-compatible stores while preserving native `unpack_objects` + local-ODB semantics. +- Extracted reusable pkt-line and sideband helpers into `grit_lib::pkt_line`. +- Switched smart HTTP fetch/push/bundle protocol code to use the library + pkt-line module. +- Added `grit_lib::smart_protocol` with transport-neutral builders for + protocol-v2 `ls-refs` and `fetch` requests, including partial-clone filters. +- Added transport-neutral receive-pack request building and report-status + parsing helpers. +- Added transport-neutral upload-pack and receive-pack advertisement parsers, + plus protocol-v2 `ls-refs` response parsing. +- Wired native HTTP discovery paths to use the shared advertisement parsers and + `ls-refs` parser. +- Added `grit_wasm::browser_http`, including browser URL/auth normalization, + `window.fetch` byte transport, Git RPC POST helpers, and discovery GET + helpers. +- Added shared smart-HTTP service-advertisement stripping in + `grit_lib::smart_protocol`. +- Added `grit_wasm::remote::ls_refs` for browser protocol-v2 ref discovery over + smart HTTP. +- Added `grit_lib::smart_protocol::extract_packfile_from_fetch_response`. +- Added low-level `grit_wasm::remote::fetch_blobless`, which builds a + protocol-v2 `filter blob:none` fetch request, downloads the pack through + browser `fetch`, stores it as a promisor pack, and unpacks it into the + in-memory object store. +- Preserved protocol-v2 `symref-target` metadata from `ls-refs`. +- Added promisor remote metadata storage to `WasmRepository`. +- Added `grit_wasm::remote::clone_blobless`, which selects the requested or + default branch, performs a blobless fetch, updates local and remote-tracking + refs, points `HEAD` at the local branch, records `origin` as promisor, and + returns clone metadata. +- Added promisor hydration APIs: `fetch_promised_object` fetches an explicit + object ID from the recorded `origin` promisor remote, and `read_blob_promised` + lazily hydrates a missing blob before returning its bytes. +- Added `read_path_promised`, which resolves `HEAD`, walks commit/tree objects + by repository-relative path, lazily hydrates missing promised tree/blob + objects, and returns the final blob bytes. +- Added an in-memory browser staging model: `stage_file` writes blobs from + browser-provided bytes and modes, `write_tree_from_staged` recursively builds + tree objects, and `commit_staged` creates a commit with explicit identities + and updates the current branch. +- Added `grit_lib::pack_write`, a correctness-first PACK v2 writer that emits + non-delta full-object packs with zlib-compressed entries and SHA-1 trailers. +- Added `WasmRepository::write_pack_for_oids` for producing pack bytes from + explicit object IDs. +- Added push-pack object selection: walk objects reachable from new tips and + exclude objects reachable from old remote tips. +- Added `WasmRepository::write_pack_for_push` for producing non-delta push packs + from a new tip and optional old tip. +- Added `grit_wasm::remote::push`, which discovers receive-pack refs and + capabilities, builds push commands from local refs, generates pack data, + sends the receive-pack request, parses report status, surfaces per-ref + failures, and updates the origin remote-tracking ref on success. +- Wired native `http_push_smart` receive-pack request construction and response + parsing to the shared `grit_lib::smart_protocol` helpers. +- Wired native upload-pack protocol-v2 `ls-refs` request construction to + `grit_lib::smart_protocol::build_ls_refs_v2_request`. +- Added `grit_wasm::persistence` with JSON snapshot export/import and + IndexedDB save/load wrappers for in-browser persistence of objects, refs, + symbolic refs, packs, promisor remotes, and staged entries. +- Added IndexedDB cleanup helpers for deleting a saved snapshot or the whole + repository database, plus browser storage quota estimation through + `navigator.storage.estimate()`. +- Added smart-HTTP fixture tests covering service-wrapped upload-pack v2 + advertisements and sidebanded receive-pack status responses. +- Added `wasm_bindgen` exports through `BrowserRepository` for in-memory object + writes/reads, staging, commits, refs, pack creation, and JSON snapshots. +- Added `wasm-bindgen-test` coverage for exported browser repository APIs. +- Added exported `BrowserRepository::cloneBlobless` and `recentCommits` APIs. +- Added `grit-wasm/examples/github-log/`, a static browser example that loads + the generated WASM package, accepts a smart HTTP/GitHub-style URL, performs a + blobless clone, and renders the latest five first-parent commit messages. +- Added opt-in `test-httpd --cors` support, including preflight handling and + CORS headers for static and smart-HTTP CGI responses, so browser integration + tests can target the local Git HTTP test server. +- Added opt-in `wasm-bindgen-test` browser network integration coverage in + `grit-wasm/tests/browser_network.rs`, gated by + `GRIT_WASM_TEST_REMOTE_URL`, for `ls_refs` against a CORS-enabled smart HTTP + remote. + +## Validation + +- `cargo fmt -p grit-lib -p grit-wasm` +- `cargo test -p grit-wasm` +- `cargo check -p grit-wasm --target wasm32-unknown-unknown` +- `cargo check -p grit-lib` +- `cargo test -p grit-lib write_tree` + +All completed successfully. + +## Next Task + +The initial WASM build plan is functionally covered. Remaining future work is +optimization-oriented: pack-index-backed storage, streaming ingestion, progress +callbacks, cancellation, and richer browser integration scenarios. diff --git a/test-results.md b/test-results.md index bc3346e8c..94e4a6e64 100644 --- a/test-results.md +++ b/test-results.md @@ -1,5 +1,105 @@ # Test results +**2026-04-27 (remote auth / credential protocol model)** + +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- `cargo test -p grit-lib --lib`: 197 passed +- `./scripts/run-tests.sh t0300-credentials.sh`: skipped by current `data/test-files.csv` scope; no credential harness tests executed +- Manual credential smoke checks: Basic helper fill, `authtype` capability filtering, encoded-newline URL rejection, `credential.protectProtocol` CR handling, sanitized askpass prompt, `grit credential capability`, URL-scoped `credential.username`, URL-scoped `credential.useHttpPath`, default HTTP path stripping, fatal credential error shape, and `credential-store` ephemeral skip passed + +**2026-04-27 (remote auth / credential-store parity)** + +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- `./scripts/run-tests.sh t0302-credential-store.sh`: skipped by current `data/test-files.csv` scope; no credential-store harness tests executed +- Manual credential-store smoke checks: home/XDG lookup precedence, XDG fallback, overwrite-on-store, erase across files, `--file` and `--file=`, path matching, CRLF path behavior, invalid-line handling, and Unix permissions passed + +**2026-04-27 (remote auth / credential-cache daemon)** + +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- Manual credential-cache smoke checks: default socket creation, custom socket creation, store/get output ordering, erase, timeout expiry, and exit cleanup passed + +**2026-04-27 (remote auth / SSH command precedence)** + +- `cargo build --release -p grit-rs`: passed +- `./scripts/run-tests.sh t5507-remote-environment.sh`: 5/5 passed +- `./scripts/run-tests.sh t5813-proto-disable-ssh.sh`: 63/81 passed (known remaining failures; no regression from SSH command precedence work) + +**2026-04-28 (remote auth / live SSH ls-remote)** + +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- `cargo test -p grit-lib --lib`: 197 passed +- `./scripts/run-tests.sh t5512-ls-remote.sh`: 16/40 passed (existing broader failures remain) +- `./scripts/run-tests.sh t5601-clone.sh`: 64/115 passed (existing broader failures remain) + +**2026-04-28 (remote auth / live SSH fetch)** + +- `cargo build --release -p grit-rs`: passed +- `./scripts/run-tests.sh t5510-fetch.sh`: 199/215 passed (existing broader failures remain) +- `./scripts/run-tests.sh t5700-protocol-v1.sh`: 0/0 warning from harness selection/status + +**2026-04-28 (remote auth / live SSH clone)** + +- `cargo build --release -p grit-rs`: passed +- `./scripts/run-tests.sh t5601-clone.sh`: 64/115 passed (existing broader failures remain) +- `./scripts/run-tests.sh t5603-clone-dirname.sh`: 25/47 passed (existing broader failures remain) + +**2026-04-28 (remote auth / SSH push hardening)** + +- `cargo build --release -p grit-rs`: passed +- `./scripts/run-tests.sh t5406-remote-rejects.sh`: 3/3 passed +- `./scripts/run-tests.sh t5545-push-options.sh`: 2/13 passed (existing broader failures remain) + +**2026-04-28 (remote auth / trace redaction audit)** + +- `cargo fmt`: passed +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- Manual HTTP trace/error smoke check with URL userinfo: passed; curl request line and connection error both scrubbed username/password. +- HTTP access errors, curl request-start traces (when `GIT_TRACE_REDACT` is not `0`), and trace2 `git-remote-https` child-start URLs now scrub URL username/password fields before display. +- Existing curl trace redaction already covers `Authorization`, `Proxy-Authorization`, cookie values, and auth-like `http.extraHeader` values by default. + +**2026-04-28 (remote auth / HTTP proxy and smart regression sweep)** + +- `cargo fmt`: passed +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- `cargo build -p grit-rs --bin test-httpd`: passed (local harness uses the debug helper when present) +- `./scripts/run-tests.sh --timeout 60 t5564-http-proxy.sh`: 7/8 passed; no timeout, proxy `407`, authenticated proxy clone, proxy askpass, and proxy redaction pass. Remaining failure: SOCKS Unix socket clone gets an empty v0 stateless upload-pack response. +- `./scripts/run-tests.sh --timeout 90 t5581-http-curl-verbose.sh t5555-http-smart-common.sh`: `t5581` 1/2, `t5555` 10/10. `t5581` currently selects system Git for HTTP clone and fails before curl output because the temporary `GIT_EXEC_PATH` lacks `git-remote-http`. +- `./scripts/run-tests.sh --timeout 90 t5549-fetch-push-http.sh t5541-http-push-smart.sh t5539-fetch-http-shallow.sh t5542-push-http-shallow.sh`: `t5549` 0/3, `t5539` 1/8, `t5542` 1/3; `t5541` skipped by current `data/test-files.csv` scope. +- Implemented upload-pack compatibility for smart-HTTP `--http-backend-info-refs` and `--stateless-rpc`, fixed stateless v2 responses so `ls-refs`/fetch responses are not prefixed by capability advertisements, avoided leaking v2 `Git-Protocol` into v0 HTTP POSTs, fixed Homebrew/system `git-http-backend` lookup in `test-httpd`, and cached proxy askpass results per process. + +**2026-04-28 (remote auth / SOCKS proxy completion)** + +- `cargo fmt`: passed +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- `cargo build -p grit-rs --bin test-httpd`: passed +- `./scripts/run-tests.sh --timeout 60 t5564-http-proxy.sh`: 8/8 passed +- `./scripts/run-tests.sh --timeout 90 t5555-http-smart-common.sh t5581-http-curl-verbose.sh`: `t5555` 10/10, `t5581` 1/2 +- Fixed SOCKS-over-Unix HTTP request construction so inserted headers are separate CRLF-delimited header lines instead of being appended to the previous header; this made `Git-Protocol` and `Content-Length` parse correctly for direct SOCKS GET/POST requests. + +**2026-04-27 (remote auth / HTTP challenge plumbing)** + +- `cargo check -p grit-rs`: passed +- `cargo build --release -p grit-rs`: passed +- `cargo test -p grit-lib --lib`: 197 passed +- HTTP client now captures response headers, extracts `WWW-Authenticate` challenges, passes `capability[]=authtype`, `capability[]=state`, and ordered `wwwauth[]` to `credential fill`, and passes `wwwauth[]` to reject paths while keeping Basic approve requests unchanged +- HTTP client now uses a typed auth credential representation and can build `Authorization: ` for helper-provided pre-encoded credentials while preserving Basic username/password auth +- HTTP client now performs one multistage `continue=1` follow-up with helper `state[]` and updated challenges, includes pre-encoded auth fields in approve/reject, and avoids storing ephemeral pre-encoded credentials through helper input +- HTTP client now parses `http.proactiveAuth` and proactively sends complete Basic or helper-selected pre-encoded credentials before the first request; `http.emptyAuth` is parsed and disables proactive auth for now +- HTTP client now applies global and URL-scoped `http.extraHeader` values to ureq, proxy, and SOCKS request paths, supports empty-value reset, and redacts auth-like extra headers in curl trace output +- HTTP client now parses Netscape and simplified `http.cookieFile` entries and matches cookies per request URL by domain, path, and secure flag +- HTTP client now honors `http.saveCookies` by appending received `Set-Cookie` headers to the configured cookie file in a format that is read back by `http.cookieFile` +- HTTP bundle URI downloads and protocol-v2 bundle-uri discovery now route through `HttpClientContext`, sharing auth, proxy, cookie, extra header, and curl trace behavior with normal HTTP remotes +- HTTP client now honors `http.sslVerify=false` and `GIT_SSL_NO_VERIFY` by disabling rustls certificate verification; CA file/path and client cert/key options remain unsupported with the current HTTP stack +- HTTP client now honors `http_proxy`, `https_proxy`, `all_proxy`, and `no_proxy` for requests without `http.proxy`, while keeping configured `http.proxy` precedence +- HTTP client now parses `http.proxyAuthMethod` and `GIT_HTTP_PROXY_AUTHMETHOD`; Basic/anyauth proxy credentials remain supported and unsupported methods are reported instead of silently downgraded +- HTTP fetch and push now honor `remote..proxy` as a per-remote override before falling back to `http.proxy` and environment proxy variables **2026-04-13 (t5322 / pack-objects sparse --revs)** - `cargo test -p grit-lib --lib`: passed (see merge) diff --git a/wasm-plan.md b/wasm-plan.md new file mode 100644 index 000000000..14cc6cd6e --- /dev/null +++ b/wasm-plan.md @@ -0,0 +1,161 @@ +# Grit WASM Build Plan + +Goal: build a browser-targeted WASM package that reuses the portable parts of +`grit-lib` and replaces native-only filesystem, process, environment, and +network assumptions. The target workflow is a blobless HTTP clone/fetch into a +browser, lazy promisor blob hydration, local commit creation, pack generation, +and unauthenticated smart-HTTP push where credentials are embedded in the input +URL and converted to browser-compatible request headers. + +## Dependency-Ordered Tasks + +### 1. Create the WASM Crate + +- [x] Add a `grit-wasm` workspace member with `cdylib` output. +- [~] Add a small public API surface for browser callers: repository creation, + object presence checks, object reads/writes, and a version/health check. +- [x] Keep the first crate build independent of browser HTTP so it can compile + before transport extraction is complete. + +### 2. Define Browser-Compatible Storage Boundaries + +- [x] Add object storage traits to `grit-lib` for reading, writing, and checking + objects without requiring a filesystem-backed `Odb`. +- [x] Add ref storage traits for `HEAD`, branch refs, remote-tracking refs, and + advertised remote refs. +- [x] Add pack storage traits for raw pack bytes, pack index metadata, and + promisor markers. +- [x] Implement adapters from the existing native `Odb`/refs code to the new + traits so native behavior is preserved. +- [x] Implement an in-memory store in `grit-wasm` for early tests. +- [x] Implement persistent browser storage after the in-memory flow works: + IndexedDB snapshots for refs/staging/promisor metadata plus raw pack and + object bytes. + +### 3. Make Core Object Operations Storage-Agnostic + +- [x] Refactor tree writing (`write_tree`) to accept object storage traits. +- [x] Refactor commit creation helpers to write through object storage traits. +- [x] Refactor pack unpacking so fetched packs can write to any object store. +- [x] Preserve the native `Odb` API as a compatibility adapter while migrated + call sites move to traits. + +### 4. Extract Git Wire Protocol Helpers + +- [x] Move pkt-line helpers from the binary crate into reusable library code. +- [x] Move sideband encode/decode helpers into reusable library code. +- [x] Extract upload-pack discovery and request/response builders from + `grit/src/http_smart.rs` without carrying native HTTP, tracing, or env logic. +- [x] Extract receive-pack advertisement, request building, sideband decoding, + and report-status parsing from `grit/src/http_push_smart.rs`. +- [x] Keep transport code out of these helpers: they should consume and produce + bytes, URLs, content types, and parsed protocol structs. + +### 5. Implement Browser HTTP Transport + +- [x] Add an async `fetch`-based HTTP backend in `grit-wasm`. +- [x] Parse URL userinfo and convert it to an `Authorization` header, then send + requests to the same URL without userinfo. +- [x] Send `Git-Protocol: version=2` when protocol v2 is requested. +- [~] Surface CORS and authentication failures with actionable errors. +- [~] Support smart HTTP endpoints: + `GET /info/refs?service=git-upload-pack` (implemented for `ls_refs`), + `POST /git-upload-pack` (implemented for `ls_refs`), + `GET /info/refs?service=git-receive-pack`, and + `POST /git-receive-pack`. + +### 6. Implement Blobless Fetch and Clone + +- [x] Discover refs with protocol v2 `ls-refs`. +- [x] Build upload-pack fetch requests with `filter blob:none`. +- [x] Parse sideband responses and extract pack bytes. +- [x] Store fetched packs as promisor packs. +- [x] Initially unpack fetched packs into the object store for simplicity. +- [x] Update `HEAD`, local branch refs, remote-tracking refs, and promisor + metadata. +- [x] Add an API for `clone_blobless(url, branch_or_ref)`. + +### 7. Implement Promisor Object Hydration + +- [x] Detect missing promised objects during blob reads. +- [x] Fetch explicit missing object IDs through upload-pack. +- [x] Store hydrated objects in the object store. +- [x] Add an API for reading a file by path that lazily fetches its blob. +- [x] Add an API for fetching arbitrary missing object IDs. + +### 8. Implement Browser Commit Creation + +- [x] Add a browser index model or a storage-backed equivalent for staged file + entries. +- [x] Write blobs from JS-provided file bytes and modes. +- [x] Build trees from the staged entries. +- [x] Serialize commits using caller-provided author, committer, message, + parent IDs, and timestamps. +- [x] Update the current branch ref after a successful commit. +- [x] Avoid implicit current time in core code; timestamps come from the caller. + +### 9. Implement Pack Writing for Push + +- [x] Add a pure Rust PACK v2 writer to `grit-lib`. +- [x] Start with non-delta full-object packs for correctness. +- [x] Walk objects reachable from the new commit. +- [x] Exclude objects reachable from the remote old tip. +- [x] Write pack headers, zlib-compressed object entries, and trailing SHA-1. +- [ ] Add optional thin-pack and delta support later using existing delta helpers. + +### 10. Implement HTTP Push + +- [x] Discover receive-pack refs and capabilities. +- [x] Build push commands from local ref updates. +- [x] Generate pack data using the new pack writer. +- [x] Send receive-pack requests with `report-status` and sideband capabilities. +- [x] Parse unpack/ref update status and surface per-ref failures. +- [x] Update remote-tracking refs after successful pushes. +- [x] Add an API for `push(url, local_ref, remote_ref)`. + +### 11. Add Tests and Browser Harnesses + +- [x] Add unit tests for storage traits, pkt-line, sideband, upload-pack request + bytes, receive-pack request bytes, and pack writer output. +- [x] Add `wasm-bindgen-test` coverage for the in-memory store and exported + browser APIs. +- [x] Add HTTP fixture tests using recorded smart-HTTP advertisements and pack + responses. +- [x] Add browser integration tests behind a local CORS-enabled Git HTTP test + server. +- [x] Add a static browser example that blobless-clones a remote and displays + recent commit messages. + +### 12. Optimize for Real Browser Repositories + +- [ ] Stop exploding every fetched pack into loose objects once pack indexes work + in browser storage. +- [ ] Add streaming pack ingestion where browser APIs make it practical. +- [ ] Add progress callbacks for fetch, lazy blob hydration, pack writing, and + push. +- [ ] Add cancellation support through `AbortController`. +- [x] Add storage quota reporting and cleanup APIs. + +## First MVP + +The first useful end-to-end MVP is intentionally simple: + +1. `grit-wasm` compiles and exposes an in-memory repository. +2. Protocol v2 upload-pack requests can be built for `filter blob:none`. +3. Browser `fetch` can download a pack and unpack it into memory. +4. Missing blobs can be fetched explicitly by object ID. +5. Commits can be created from JS-provided file contents. +6. A non-delta full-object pack can be generated. +7. Receive-pack push can send that pack and parse `report-status`. + +## Browser Integration Test + +The opt-in browser network test is `grit-wasm/tests/browser_network.rs`. To run +it, start a smart HTTP server with CORS enabled, then compile/run the wasm tests +with `GRIT_WASM_TEST_REMOTE_URL` set to a served repository URL: + +```bash +target/debug/test-httpd --root /tmp/git-http-root --cors --port 0 +GRIT_WASM_TEST_REMOTE_URL=http://127.0.0.1:/smart/repo.git \ + wasm-pack test --chrome --headless grit-wasm +```