Push a directory of HTML from the terminal, get a four-character URL back. Deno CLI + Cloudflare Worker (R2 + D1). cli and shared are published to JSR.
- commit
- e5f5a02a59fb0dd79248e249b16cea933a1a997d
- author
- Dami <dami.etoile@gmail.com>
| A | .gitignore | 6 | + |
| A | README.md | 195 | ++++ |
| A | cli/ | 58 | + |
| A | cli/ | 98 | ++ |
| A | cli/ | 164 | +++ |
| A | cli/ | 70 | + |
| A | cli/ | 60 | + |
| A | cli/ | 11 | + |
| A | cli/ | 125 | ++ |
| A | cli/ | 90 | ++ |
| A | cli/ | 56 | + |
| A | cli/ | 117 | ++ |
| A | cli/ | 142 | +++ |
| A | cli/ | 82 | ++ |
| A | deno.json | 9 | + |
| A | deno.lock | 38 | + |
| A | package-lock.json | 1502 | ++++++++++++++++++++++++++++++ |
| A | package.json | 5 | + |
| A | shared/ | 56 | + |
| A | shared/ | 6 | + |
| A | shared/ | 5 | + |
| A | shared/ | bin | |
| A | shared/ | 42 | + |
| A | shared/ | 5 | + |
| A | shared/ | 104 | ++ |
| A | worker/ | 1526 | ++++++++++++++++++++++++++++++ |
| A | worker/ | 16 | + |
| A | worker/ | 30 | + |
| A | worker/ | 74 | + |
| A | worker/ | 303 | ++++++ |
| A | worker/ | 19 | + |
| A | worker/ | 187 | ++++ |
| A | worker/ | 21 | + |
| A | worker/ | 57 | + |
| A | worker/ | 31 | + |
| A | worker/ | 100 | ++ |
| A | worker/ | 135 | +++ |
| A | worker/ | 14 | + |
| A | worker/ | 28 | + |
| A | worker/ | 29 | + |
@@ -0,0 +1,195 @@
1+<div align="center">2+3+# `nzip`4+5+**Push a directory of HTML from the terminal. Get a four-character URL back.**6+7+_A personal share tool — Deno CLI → Cloudflare Worker → R2 + D1. No daemon, no build step, no8+redeploys._9+10+</div>11+12+```console13+$ nzip push ./demo work:demo --ttl 30d14+ bundling ./demo … 3 files, 197 B15+ manifest 4a91d75d — 3 new blobs (197 B), 0 deduped16+✓ pushed work:demo -> https://share.example.com/12d8 (expires in 30d, push #1)17+```18+19+---20+21+## How addresses work22+23+Every share lives at four hex characters. The first digit selects one of **16 registered vaults**,24+the rest a site within it — 4,096 slots per vault, allocated randomly so URLs don't leak how many25+sites exist.26+27+```text28+https://share.example.com/2a3f29+ |+- site 0xa3f (0x000-0xFFF, random within the vault)30+ +-- vault 0x2 ("work" - 16 slots, registered by name)31+```32+33+Aliases like `work:demo` are resolved by the API; the URL never exposes them. Commands accept any of34+three target forms:35+36+| form | example | meaning |37+| ----------- | ----------- | -------------------------- |38+| address | `2a3f` | direct hex address |39+| vault:alias | `work:demo` | alias within a named vault |40+| bare alias | `demo` | alias in the default vault |41+42+## Features43+44+- **Instant** — a push is one manifest exchange plus the blobs the server hasn't seen. Nothing45+ rebuilds, nothing redeploys.46+- **Content-addressed** — per-file sha256 blobs + a canonical manifest per push. Identical files are47+ stored once across every site; re-pushing an unchanged directory uploads `0 new blobs`.48+- **Ephemeral by default** — 14-day TTL, `--ttl 30d` or `--ttl forever` to override. Expired shares49+ answer `410 Gone`; a daily cron sweeps them and garbage-collects unreferenced content.50+- **Revertible** — the last 10 pushes per site are kept. `nzip revert` repoints the address at any51+ of them; the revert itself is recorded as a push.52+- **Password-protectable** — `nzip share work:demo --password …` gates the site behind an unlock53+ form (PBKDF2 hashing, signed per-site cookie, 7 days).54+- **Single-user by design** — one bearer token, stored as a Worker secret and in55+ `~/.config/nzip/config.json` (mode 0600).56+- **Locates itself** — `nzip where <target>` prints the local directory this machine pushed a site57+ from. A breadcrumb registry (`~/.config/nzip/paths.json`) records the source path on every push;58+ it self-cleans — expired entries drop on write, `rm` forgets its entry, and `ls` reconciles59+ against the live set. Use it as `cd "$(nzip where personal:plan)"`.60+- **Vault guardrail** — an optional `"allowVaults": ["home"]` in `config.json` restricts which vaults61+ this install may target by name. Pushes or aliases outside the list are refused before any upload —62+ so a home-project agent can't drop a doc into a vault that sits adjacent to what you share63+ professionally. Absent = unrestricted; raw hex addresses bypass it (they name no vault).64+65+## Commands66+67+```text68+nzip auth [--server URL] [--token T] authenticate and save config69+nzip vault add <name> [--slot N] register a vault (16 slots, 0x0–0xf)70+nzip vault ls | default <name> list vaults / set the default71+nzip push <dir|file> [target] [--ttl 14d|30d|forever]72+nzip share <target> [--ttl …] [--password PW | --no-password]73+nzip ls [vault] list sites74+nzip where <target> print the local dir this machine pushed from75+nzip rm <target> [--yes] delete a site76+nzip status server + vault overview77+nzip revert <target> [--to N] [--list] repoint to a previous push78+```79+80+Pushing a single `page.html` stores it as the site's `index.html`. Directory pushes skip dotfiles81+and `node_modules`, and honor a `.nzipignore` (one glob per line). Single-file sites serve directly82+at the bare address (`/2a3f`); multi-file bundles redirect to `/2a3f/` so relative asset URLs83+resolve.84+85+## Architecture86+87+```mermaid88+flowchart LR89+ CLI["nzip<br/><i>Deno CLI</i>"] -- "https + bearer" --> W["Worker<br/><i>self-hosted</i>"]90+ W --> R2[("R2<br/>blob/{sha256}<br/>manifest/{sha256}")]91+ W --> D1[("D1<br/>vaults · sites · pushes")]92+ V(["visitor"]) -- "GET /2a3f" --> W93+```94+95+A push is a stateless three-step protocol — the manifest itself is the state:96+97+1. `POST /api/push/prepare` — send the manifest, get back which blob hashes the server is missing98+2. `PUT /api/blob/{sha256}` — upload only those, one per request, 6 at a time; the server re-hashes99+ and rejects mismatches100+3. `POST /api/push/commit` — server re-verifies every blob exists, then commits atomically:101+ resolve/allocate the address, repoint the site, append history102+103+Serving is one D1 read (address → manifest, expiry, password) and two R2 reads, with `ETag`104+revalidation and a 60-second cache. Metadata lives in three tables:105+106+| table | keys |107+| -------- | ------------------------------------------------------------------------ |108+| `vaults` | slot (0–15), name |109+| `sites` | address (0–65535), vault, alias, current manifest, expiry, password hash |110+| `pushes` | per-site history (seq, manifest, note) — capped at 10, powers `revert` |111+112+**GC safety rule:** an R2 object is deleted only if no live site or retained history entry113+references it _and_ it's older than 24 hours — so an in-flight push can never lose objects to a114+concurrent sweep.115+116+The full design doc is a pushable page of its own: [`docs/plan.html`](docs/plan.html).117+118+## Repo layout119+120+```text121+shared/ manifest canonicalization, hashing, addressing — imported by both sides122+cli/ the nzip command (Deno, no framework)123+worker/ Cloudflare Worker: serving + API + GC cron (wrangler)124+docs/ the architecture plan as a self-contained HTML page125+```126+127+`shared/` is the contract: canonical JSON serialization lives in exactly one file128+([`shared/manifest.ts`](shared/manifest.ts)) so the CLI and Worker can never disagree about a129+manifest hash. It sticks to Web-standard APIs only, so the same code runs under Deno and workerd.130+131+## Install132+133+```sh134+deno install -g -f -n nzip \135+ --allow-net --allow-read --allow-write --allow-env \136+ cli/main.ts137+138+nzip auth --server https://share.example.com --token <owner-token>139+```140+141+`nzip` is self-hosted. The server URL is supplied by the operator; there is no bundled public142+service. Use the same URL you set as `vars.PUBLIC_BASE` in your Wrangler config.143+144+<details>145+<summary><b>Self-hosting: one-time Cloudflare setup</b></summary>146+147+See [`worker/setup.md`](worker/setup.md) for the full checklist. In short:148+149+```sh150+cd worker151+npx wrangler login152+cp wrangler.local.example.jsonc wrangler.local.jsonc153+# edit wrangler.local.jsonc:154+# - routes[0].pattern: your hostname, such as share.example.com155+# - vars.PUBLIC_BASE: https://<that hostname>156+# - d1_databases[0].database_id: filled in after D1 creation157+npx wrangler r2 bucket create nzip-content158+npx wrangler d1 create nzip # paste id into wrangler.local.jsonc159+npx wrangler d1 execute nzip --remote --file schema.sql160+openssl rand -hex 32 | npx wrangler secret put NZIP_TOKEN --config wrangler.local.jsonc161+npx wrangler deploy --config wrangler.local.jsonc162+nzip auth --server https://share.example.com # use your actual PUBLIC_BASE163+```164+165+`routes[0].pattern` and `vars.PUBLIC_BASE` are required for a real deployment. `PUBLIC_BASE`166+controls the URLs returned by `nzip push`, and the CLI authenticates against that same origin.167+168+Two gotchas learned the hard way:169+170+- **Cron triggers require a workers.dev subdomain** on the account (API error `10063`), even if the171+ Worker only serves a custom domain. Register one in the dashboard (or172+ `PUT /accounts/{id}/workers/subdomain`) before deploying with `triggers`.173+- The required custom-domain route174+ (`"routes": [{ "pattern": "share.example.com",175+ "custom_domain": true }]`) needs the zone on your176+ account; it provisions DNS and the cert automatically on deploy.177+178+</details>179+180+<details>181+<summary><b>Development</b></summary>182+183+```sh184+deno task check # typecheck CLI + shared185+cd worker && npx tsc --noEmit # typecheck Worker186+187+npx wrangler d1 execute nzip --local --file schema.sql # once per fresh state188+npx wrangler dev # local R2/D1; token in .dev.vars189+nzip auth --server http://localhost:8787 --token dev-token-local-only190+```191+192+Test the GC cron: `npx wrangler dev --test-scheduled`, then193+`curl "http://localhost:8787/__scheduled?cron=0+4+*+*+*"`.194+195+</details>@@ -0,0 +1,58 @@
1+import { ApiClient } from "../lib/api.ts";2+import { configPath, loadConfig, saveConfig } from "../lib/config.ts";3+import { emit, fail, green } from "../lib/fmt.ts";4+5+function prompt2(question: string, fallback?: string): string {6+ const answer = prompt(fallback ? `${question} [${fallback}]:` : `${question}:`);7+ const value = (answer ?? "").trim() || fallback || "";8+ if (!value) fail(`${question} is required`);9+ return value;10+}11+12+function envDefaultServer(): string | undefined {13+ try {14+ const server = Deno.env.get("NZIP_SERVER")?.trim();15+ if (server) return server;16+ const domain = Deno.env.get("NZIP_DOMAIN")?.trim().replace(/^https?:\/\//, "").replace(17+ /\/$/,18+ "",19+ );20+ return domain ? `https://${domain}` : undefined;21+ } catch {22+ return undefined;23+ }24+}25+26+export async function cmdAuth(27+ serverFlag?: string,28+ tokenFlag?: string,29+): Promise<void> {30+ const existing = await loadConfig();31+ const server = (serverFlag ?? prompt2("server", existing?.server ?? envDefaultServer()))32+ .replace(/\/$/, "");33+ const token = tokenFlag ?? prompt2("token", existing?.token);34+ const config = { server, token, defaultVault: existing?.defaultVault };35+36+ const api = new ApiClient(config);37+ let status;38+ try {39+ status = await api.status();40+ } catch (e) {41+ fail(`could not verify token against ${server}/api/status — ${(e as Error).message}`);42+ }43+44+ if (!config.defaultVault && status.vaults.length > 0) {45+ config.defaultVault = status.vaults[0].name;46+ }47+ await saveConfig(config);48+ emit(() => {49+ console.log(`${green("✓")} authenticated against ${server} — saved to ${configPath()}`);50+ if (config.defaultVault) console.log(` default vault: ${config.defaultVault}`);51+ }, {52+ ok: true,53+ server,54+ configPath: configPath(),55+ defaultVault: config.defaultVault ?? null,56+ vaults: status.vaults,57+ });58+}@@ -0,0 +1,98 @@
1+import { ApiClient, commitTargetFor } from "../lib/api.ts";2+import { buildBundle, formatBytes } from "../lib/bundle.ts";3+import type { Config } from "../lib/config.ts";4+import { recordPush } from "../lib/paths.ts";5+import { amber, bold, cyan, dim, emit, fail, green, parseTtl, ttlLeft } from "../lib/fmt.ts";6+7+const UPLOAD_CONCURRENCY = 6;8+9+export async function cmdPush(10+ config: Config,11+ path: string | undefined,12+ targetRaw: string | undefined,13+ ttlRaw: string | undefined,14+): Promise<void> {15+ if (!path) fail("usage: nzip push <dir|file> [target] [--ttl 14d|forever]");16+ const api = new ApiClient(config);17+18+ // Resolve (and vault-guard) the target up front — before any bundling or19+ // upload — so a disallowed vault is refused without touching the network.20+ const target = (() => {21+ try {22+ return commitTargetFor(targetRaw, config);23+ } catch (e) {24+ return fail((e as Error).message);25+ }26+ })();27+28+ const bundle = await buildBundle(path).catch((e) => fail((e as Error).message));29+ const fileCount = Object.keys(bundle.manifest.files).length;30+ emit(() => {31+ console.log(dim(` bundling ${path} … ${fileCount} files, ${formatBytes(bundle.totalBytes)}`));32+ for (const w of bundle.warnings) console.log(` ${amber("!")} ${w}`);33+ }, undefined);34+35+ const prep = await api.prepare(bundle.manifest);36+ const dedupCount = bundle.blobs.size - prep.missing.length;37+ const missingBytes = prep.missing.reduce((n, h) => n + (bundle.blobs.get(h)?.length ?? 0), 0);38+ emit(() =>39+ console.log(40+ dim(41+ ` manifest ${prep.manifestHash.slice(0, 8)} — ${prep.missing.length} new blobs (${42+ formatBytes(missingBytes)43+ }), ${dedupCount} deduped`,44+ ),45+ ), undefined);46+47+ // Upload missing blobs with bounded concurrency.48+ let done = 0;49+ const queue = [...prep.missing];50+ const workers = Array.from(51+ { length: Math.min(UPLOAD_CONCURRENCY, queue.length) },52+ async () => {53+ for (let h = queue.shift(); h !== undefined; h = queue.shift()) {54+ const bytes = bundle.blobs.get(h);55+ if (!bytes) fail(`internal: missing blob bytes for ${h}`);56+ await api.uploadBlob(h, bytes);57+ done++;58+ }59+ },60+ );61+ await Promise.all(workers);62+ if (prep.missing.length > 0) {63+ emit(() => console.log(dim(` uploaded ${done}/${prep.missing.length}`)));64+ }65+66+ const ttl = ttlRaw === undefined ? undefined : parseTtl(ttlRaw);67+ const res = await api.commit({ manifest: bundle.manifest, target, ttl });68+69+ // Breadcrumb: remember which directory this machine pushed from so70+ // `nzip where` can find it later. Best-effort — never fail a push over it.71+ await recordPush({72+ address: res.address,73+ vault: "vault" in target ? target.vault : undefined,74+ alias: res.alias,75+ path,76+ url: res.url,77+ expiresAt: res.expiresAt,78+ }).catch(() => {});79+80+ const label = res.alias ? `${"vault" in target ? target.vault : "?"}:${res.alias}` : res.address;81+ const expiry = res.expiresAt === null ? "forever" : `expires in ${ttlLeft(res.expiresAt)}`;82+ emit(83+ () =>84+ console.log(85+ `${green("✓")} pushed ${bold(label)} → ${cyan(res.url)} ${86+ dim(`(${expiry}, push #${res.seq})`)87+ }`,88+ ),89+ {90+ ok: true,91+ ...res,92+ files: fileCount,93+ newBlobs: prep.missing.length,94+ dedupedBlobs: dedupCount,95+ warnings: bundle.warnings,96+ },97+ );98+}@@ -0,0 +1,164 @@
1+// share, ls, rm, status, revert — the site-management commands.2+3+import { ApiClient, resolveCliTarget } from "../lib/api.ts";4+import type { Config } from "../lib/config.ts";5+import { forget, reconcile } from "../lib/paths.ts";6+import {7+ ago,8+ amber,9+ bold,10+ cyan,11+ dim,12+ emit,13+ fail,14+ green,15+ parseTtl,16+ table,17+ ttlLeft,18+} from "../lib/fmt.ts";19+20+function targetOrFail(raw: string | undefined, config: Config, usage: string): string {21+ if (!raw) fail(usage);22+ try {23+ return resolveCliTarget(raw, config);24+ } catch (e) {25+ return fail((e as Error).message);26+ }27+}28+29+export async function cmdShare(30+ config: Config,31+ raw: string | undefined,32+ ttlRaw: string | undefined,33+ password: string | undefined,34+ noPassword: boolean,35+): Promise<void> {36+ const target = targetOrFail(37+ raw,38+ config,39+ "usage: nzip share <target> [--ttl 14d|forever] [--password PW | --no-password]",40+ );41+ const api = new ApiClient(config);42+ const patch: { ttl?: number | "forever"; password?: string | null } = {};43+ if (ttlRaw !== undefined) patch.ttl = parseTtl(ttlRaw);44+ if (noPassword) patch.password = null;45+ else if (password !== undefined) patch.password = password;46+ const site = Object.keys(patch).length === 047+ ? await api.siteDetail(target)48+ : await api.patchSite(target, patch);49+ const name = site.alias ? `${site.vault}:${site.alias}` : site.address;50+ emit(51+ () =>52+ console.log(53+ `${bold(name)} ${cyan(site.url)} ${site.manifestHash.slice(0, 8)} ${54+ site.expiresAt === null ? green("forever") : `expires in ${ttlLeft(site.expiresAt)}`55+ }${site.protected ? ` ${amber("🔒")}` : ""}`,56+ ),57+ { ok: true, ...site },58+ );59+}60+61+export async function cmdLs(config: Config, vault: string | undefined): Promise<void> {62+ const api = new ApiClient(config);63+ const sites = await api.listSites(vault);64+ // The server is authoritative for what still exists — drop breadcrumbs for65+ // sites it no longer lists (expired, deleted here or on another machine).66+ await reconcile(new Set(sites.map((s) => s.address)), vault).catch(() => {});67+ emit(() => {68+ if (sites.length === 0) {69+ console.log(dim(vault ? `no sites in ${vault}` : "no sites"));70+ return;71+ }72+ console.log(table(73+ ["ADDR", "VAULT", "ALIAS", "PUSHED", "EXPIRES", "MANIFEST"],74+ sites.map((s) => [75+ cyan(s.address),76+ s.vault,77+ (s.alias ?? dim("—")) + (s.protected ? " 🔒" : ""),78+ ago(s.updatedAt),79+ s.expiresAt === null ? green("forever") : ttlLeft(s.expiresAt),80+ dim(s.manifestHash.slice(0, 8)),81+ ]),82+ ));83+ }, { ok: true, sites });84+}85+86+export async function cmdRm(config: Config, raw: string | undefined, yes: boolean): Promise<void> {87+ const target = targetOrFail(raw, config, "usage: nzip rm <target> [--yes]");88+ const api = new ApiClient(config);89+ const site = await api.siteDetail(target);90+ const name = site.alias ? `${site.vault}:${site.alias}` : site.address;91+ if (!yes) {92+ const answer = prompt(`delete ${name} (${site.url})? [y/N]`);93+ if (answer?.toLowerCase() !== "y") {94+ emit(() => console.log(dim("aborted")), {95+ ok: false,96+ error: "aborted (pass --yes to skip the prompt)",97+ });98+ return;99+ }100+ }101+ await api.deleteSite(target);102+ await forget({ address: site.address, vault: site.vault, alias: site.alias ?? undefined }).catch(103+ () => {},104+ );105+ emit(106+ () => console.log(`${green("✓")} removed ${bold(name)} — content becomes eligible for GC`),107+ { ok: true, removed: site.address, alias: site.alias, vault: site.vault },108+ );109+}110+111+export async function cmdStatus(config: Config): Promise<void> {112+ const api = new ApiClient(config);113+ const status = await api.status();114+ emit(() => {115+ console.log(`${green("●")} ${config.server} ${dim(`v${status.version}`)}`);116+ console.log(117+ ` sites: ${status.siteCount}${118+ status.expiringSoon > 0 ? ` ${amber(`(${status.expiringSoon} expiring <48h)`)}` : ""119+ }`,120+ );121+ if (status.vaults.length > 0) {122+ console.log(table(123+ ["SLOT", "VAULT", "SITES"],124+ status.vaults.map((v) => [`0x${v.slot.toString(16)}`, v.name, String(v.siteCount)]),125+ ));126+ }127+ }, { ...status, server: config.server });128+}129+130+export async function cmdRevert(131+ config: Config,132+ raw: string | undefined,133+ toSeq: number | undefined,134+ list: boolean,135+): Promise<void> {136+ const target = targetOrFail(raw, config, "usage: nzip revert <target> [--to N] [--list]");137+ const api = new ApiClient(config);138+139+ if (list) {140+ const site = await api.siteDetail(target);141+ emit(() =>142+ console.log(table(143+ ["PUSH", "MANIFEST", "WHEN", "NOTE"],144+ site.history.map((h) => [145+ h.manifestHash === site.manifestHash ? bold(`#${h.seq} ←`) : `#${h.seq}`,146+ dim(h.manifestHash.slice(0, 8)),147+ ago(h.pushedAt),148+ h.note ?? "",149+ ]),150+ )), { ok: true, current: site.manifestHash, history: site.history });151+ return;152+ }153+154+ const res = await api.revert(target, toSeq);155+ emit(156+ () =>157+ console.log(158+ `${green("✓")} ${bold(raw!)} reverted to push #${res.revertedTo} (${159+ dim(res.manifestHash.slice(0, 8))160+ }) — ${cyan(res.url)}`,161+ ),162+ { ok: true, ...res },163+ );164+}@@ -0,0 +1,70 @@
1+import { ApiClient } from "../lib/api.ts";2+import type { Config } from "../lib/config.ts";3+import { assertVaultAllowed, saveConfig } from "../lib/config.ts";4+import { bold, dim, emit, fail, green, table } from "../lib/fmt.ts";5+6+export async function cmdVault(7+ config: Config,8+ sub: string | undefined,9+ name: string | undefined,10+ slot: number | undefined,11+): Promise<void> {12+ const api = new ApiClient(config);13+14+ if (sub === "ls" || sub === undefined) {15+ const vaults = await api.listVaults();16+ emit(() => {17+ if (vaults.length === 0) {18+ console.log(dim("no vaults registered — run `nzip vault add <name>`"));19+ return;20+ }21+ console.log(table(22+ ["SLOT", "VAULT", "SITES"],23+ vaults.map((v) => [24+ `0x${v.slot.toString(16)}`,25+ v.name === config.defaultVault ? bold(`${v.name} *`) : v.name,26+ String(v.siteCount),27+ ]),28+ ));29+ }, { ok: true, defaultVault: config.defaultVault ?? null, vaults });30+ return;31+ }32+33+ if (sub === "add") {34+ if (!name) fail("usage: nzip vault add <name> [--slot N]");35+ try {36+ assertVaultAllowed(name, config);37+ } catch (e) {38+ return fail((e as Error).message);39+ }40+ const v = await api.createVault(name, slot);41+ const madeDefault = !config.defaultVault;42+ if (madeDefault) await saveConfig({ ...config, defaultVault: v.name });43+ emit(() => {44+ console.log(45+ `${green("✓")} vault ${bold(v.name)} registered at slot 0x${v.slot.toString(16)}`,46+ );47+ if (madeDefault) console.log(dim(` set as default vault`));48+ }, { ok: true, ...v, default: madeDefault || config.defaultVault === v.name });49+ return;50+ }51+52+ if (sub === "default") {53+ if (!name) fail("usage: nzip vault default <name>");54+ try {55+ assertVaultAllowed(name, config);56+ } catch (e) {57+ return fail((e as Error).message);58+ }59+ const vaults = await api.listVaults();60+ if (!vaults.some((v) => v.name === name)) fail(`unknown vault: ${name}`);61+ await saveConfig({ ...config, defaultVault: name });62+ emit(63+ () => console.log(`${green("✓")} default vault set to ${bold(name)}`),64+ { ok: true, defaultVault: name },65+ );66+ return;67+ }68+69+ fail(`unknown vault subcommand: ${sub} (use ls | add | default)`);70+}@@ -0,0 +1,60 @@
1+// `nzip where <target>` — print the local directory this machine pushed a site2+// from. Purely local: reads the breadcrumb registry, never touches the server,3+// so it stays fast enough for `cd "$(nzip where personal:plan)"`.4+5+import { resolveCliTarget } from "../lib/api.ts";6+import type { Config } from "../lib/config.ts";7+import { lookup } from "../lib/paths.ts";8+import { amber, dim, emit, fail } from "../lib/fmt.ts";9+10+export async function cmdWhere(config: Config, raw: string | undefined): Promise<void> {11+ if (!raw) fail("usage: nzip where <target>");12+13+ const q = /^[0-9a-f]{4}$/.test(raw) ? { address: raw } : (() => {14+ let resolved: string;15+ try {16+ resolved = resolveCliTarget(raw, config);17+ } catch (e) {18+ return fail((e as Error).message);19+ }20+ const [vault, alias] = resolved.split(":");21+ return { vault, alias };22+ })();23+24+ const entry = await lookup(q);25+ if (!entry) {26+ fail(27+ `no local path tracked for "${raw}" on this machine`,28+ "it was pushed elsewhere (or before tracking existed) — push it again from its directory to record the path",29+ );30+ }31+32+ let exists = false;33+ try {34+ await Deno.stat(entry.path); // dir, or a file for single-file pushes35+ exists = true;36+ } catch {37+ exists = false;38+ }39+40+ emit(41+ () => {42+ console.log(entry.path); // bare path on stdout for `cd "$(nzip where …)"`43+ if (!exists) {44+ console.error(dim(` ${amber("↳")} directory no longer exists (moved or deleted)`));45+ }46+ },47+ {48+ ok: true,49+ target: raw,50+ address: entry.address,51+ vault: entry.vault ?? null,52+ alias: entry.alias ?? null,53+ path: entry.path,54+ exists,55+ url: entry.url,56+ expiresAt: entry.expiresAt,57+ pushedAt: entry.pushedAt,58+ },59+ );60+}@@ -0,0 +1,125 @@
1+// Typed fetch client for the Worker API.2+3+import type {4+ ApiError,5+ CommitRequest,6+ CommitResponse,7+ Manifest,8+ PrepareResponse,9+ SiteDetail,10+ SiteInfo,11+ StatusResponse,12+ Target,13+ VaultInfo,14+} from "@nzip/shared";15+import { assertVaultAllowed, type Config } from "./config.ts";16+17+export class ApiClient {18+ constructor(private config: Config) {}19+20+ private async request<T>(method: string, path: string, body?: BodyInit): Promise<T> {21+ const res = await fetch(`${this.config.server}${path}`, {22+ method,23+ headers: {24+ authorization: `Bearer ${this.config.token}`,25+ ...(typeof body === "string" ? { "content-type": "application/json" } : {}),26+ },27+ body,28+ });29+ const text = await res.text();30+ if (!res.ok) {31+ let message = text;32+ try {33+ message = (JSON.parse(text) as ApiError).error;34+ } catch { /* not json */ }35+ throw new Error(`${res.status}: ${message}`);36+ }37+ return JSON.parse(text) as T;38+ }39+40+ status(): Promise<StatusResponse> {41+ return this.request("GET", "/api/status");42+ }43+44+ prepare(manifest: Manifest): Promise<PrepareResponse> {45+ return this.request("POST", "/api/push/prepare", JSON.stringify({ manifest }));46+ }47+48+ uploadBlob(hash: string, bytes: Uint8Array): Promise<{ ok: true }> {49+ return this.request("PUT", `/api/blob/${hash}`, bytes as unknown as BodyInit);50+ }51+52+ commit(req: CommitRequest): Promise<CommitResponse> {53+ return this.request("POST", "/api/push/commit", JSON.stringify(req));54+ }55+56+ listSites(vault?: string): Promise<SiteInfo[]> {57+ const q = vault ? `?vault=${encodeURIComponent(vault)}` : "";58+ return this.request("GET", `/api/sites${q}`);59+ }60+61+ siteDetail(target: string): Promise<SiteDetail> {62+ return this.request("GET", `/api/sites/${encodeURIComponent(target)}`);63+ }64+65+ patchSite(66+ target: string,67+ patch: { ttl?: number | "forever"; password?: string | null },68+ ): Promise<SiteDetail> {69+ return this.request("PATCH", `/api/sites/${encodeURIComponent(target)}`, JSON.stringify(patch));70+ }71+72+ deleteSite(target: string): Promise<{ ok: true; address: string }> {73+ return this.request("DELETE", `/api/sites/${encodeURIComponent(target)}`);74+ }75+76+ revert(target: string, toSeq?: number): Promise<CommitResponse & { revertedTo: number }> {77+ return this.request(78+ "POST",79+ `/api/sites/${encodeURIComponent(target)}/revert`,80+ JSON.stringify(toSeq === undefined ? {} : { toSeq }),81+ );82+ }83+84+ listVaults(): Promise<VaultInfo[]> {85+ return this.request("GET", "/api/vaults");86+ }87+88+ createVault(name: string, slot?: number): Promise<VaultInfo> {89+ return this.request("POST", "/api/vaults", JSON.stringify({ name, slot }));90+ }91+}92+93+/**94+ * Resolve a CLI target string to (API path target, commit body target).95+ * A bare alias picks up the default vault from config.96+ */97+export function resolveCliTarget(raw: string, config: Config): string {98+ if (/^[0-9a-f]{4}$/.test(raw)) return raw; // raw address names no vault99+ if (raw.includes(":")) {100+ const [vault] = raw.split(":");101+ assertVaultAllowed(vault, config);102+ return raw;103+ }104+ const vault = config.defaultVault;105+ if (!vault) {106+ throw new Error(107+ `"${raw}" is a bare alias but no defaultVault is set — use vault:alias or set defaultVault in config`,108+ );109+ }110+ assertVaultAllowed(vault, config);111+ return `${vault}:${raw}`;112+}113+114+export function commitTargetFor(raw: string | undefined, config: Config): Target {115+ if (raw === undefined) {116+ const vault = config.defaultVault;117+ if (!vault) throw new Error("no target given and no defaultVault set");118+ assertVaultAllowed(vault, config);119+ return { vault };120+ }121+ if (/^[0-9a-f]{4}$/.test(raw)) return { address: parseInt(raw, 16) };122+ const resolved = resolveCliTarget(raw, config);123+ const [vault, alias] = resolved.split(":");124+ return { vault, alias };125+}@@ -0,0 +1,90 @@
1+// Directory walking, hashing, and manifest building for `nzip push`.2+3+import { globToRegExp, join, relative } from "@std/path";4+import { contentTypeFor, sha256hex, validatePath } from "@nzip/shared";5+import type { Manifest } from "@nzip/shared";6+7+const SKIP_DIRS = new Set(["node_modules", ".git"]);8+9+export interface Bundle {10+ manifest: Manifest;11+ /** blob hash → bytes (deduped) */12+ blobs: Map<string, Uint8Array>;13+ totalBytes: number;14+ warnings: string[];15+}16+17+async function loadIgnore(dir: string): Promise<RegExp[]> {18+ try {19+ const text = await Deno.readTextFile(join(dir, ".nzipignore"));20+ return text.split("\n")21+ .map((l) => l.trim())22+ .filter((l) => l.length > 0 && !l.startsWith("#"))23+ .map((glob) => globToRegExp(glob, { extended: true, globstar: true }));24+ } catch {25+ return [];26+ }27+}28+29+async function* walk(root: string, dir: string, ignore: RegExp[]): AsyncGenerator<string> {30+ for await (const entry of Deno.readDir(dir)) {31+ if (entry.name.startsWith(".")) continue;32+ const full = join(dir, entry.name);33+ const rel = relative(root, full);34+ if (ignore.some((re) => re.test(rel))) continue;35+ if (entry.isDirectory) {36+ if (SKIP_DIRS.has(entry.name)) continue;37+ yield* walk(root, full, ignore);38+ } else if (entry.isFile) {39+ yield rel;40+ }41+ }42+}43+44+export async function buildBundle(path: string): Promise<Bundle> {45+ const stat = await Deno.stat(path);46+ const warnings: string[] = [];47+ const manifest: Manifest = { v: 1, files: {} };48+ const blobs = new Map<string, Uint8Array>();49+ let totalBytes = 0;50+51+ const addFile = async (storedPath: string, filePath: string) => {52+ const bytes = await Deno.readFile(filePath);53+ const hash = await sha256hex(bytes);54+ manifest.files[storedPath] = { h: hash, s: bytes.length, ct: contentTypeFor(storedPath) };55+ if (!blobs.has(hash)) blobs.set(hash, bytes);56+ totalBytes += bytes.length;57+ };58+59+ if (stat.isFile) {60+ // Single-file push: an HTML file becomes the site's index.html.61+ const name = path.split("/").pop()!;62+ const stored = /\.html?$/i.test(name) ? "index.html" : name;63+ await addFile(stored, path);64+ return { manifest, blobs, totalBytes, warnings };65+ }66+67+ const ignore = await loadIgnore(path);68+ for await (const rel of walk(path, path, ignore)) {69+ const stored = rel.replaceAll("\\", "/");70+ if (!validatePath(stored)) {71+ warnings.push(`skipped (unsupported path): ${stored}`);72+ continue;73+ }74+ await addFile(stored, join(path, rel));75+ }76+77+ if (Object.keys(manifest.files).length === 0) {78+ throw new Error(`no files to push in ${path}`);79+ }80+ if (!manifest.files["index.html"]) {81+ warnings.push("no root index.html — the site URL will 404 until one exists");82+ }83+ return { manifest, blobs, totalBytes, warnings };84+}85+86+export function formatBytes(n: number): string {87+ if (n < 1024) return `${n} B`;88+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;89+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;90+}@@ -0,0 +1,56 @@
1+import { join } from "@std/path";2+import { fail } from "./fmt.ts";3+4+export interface Config {5+ server: string;6+ token: string;7+ defaultVault?: string;8+ /**9+ * Optional allow-list of vault names this config may target by name. When10+ * present, any push/share/rm/revert/default that names a vault outside the11+ * list is refused — so a restricted agent can't drop content into a vault12+ * that sits adjacent to things you share professionally. Absent = no limit;13+ * empty array = no named vault is permitted.14+ */15+ allowVaults?: string[];16+}17+18+/** Throw if `config.allowVaults` is set and does not include `vault`. */19+export function assertVaultAllowed(vault: string, config: Config): void {20+ if (!config.allowVaults) return; // unset → unrestricted21+ if (!config.allowVaults.includes(vault)) {22+ const allowed = config.allowVaults.length ? config.allowVaults.join(", ") : "none";23+ throw new Error(`vault "${vault}" is not allowed by this config (allowed: ${allowed})`);24+ }25+}26+27+export function configDir(): string {28+ const xdg = Deno.env.get("XDG_CONFIG_HOME");29+ const home = Deno.env.get("HOME") ?? ".";30+ return join(xdg ?? join(home, ".config"), "nzip");31+}32+33+export function configPath(): string {34+ return join(configDir(), "config.json");35+}36+37+export async function loadConfig(): Promise<Config | null> {38+ try {39+ return JSON.parse(await Deno.readTextFile(configPath())) as Config;40+ } catch {41+ return null;42+ }43+}44+45+export async function saveConfig(config: Config): Promise<void> {46+ await Deno.mkdir(configDir(), { recursive: true });47+ const path = configPath();48+ await Deno.writeTextFile(path, JSON.stringify(config, null, 2) + "\n");49+ await Deno.chmod(path, 0o600);50+}51+52+export async function requireConfig(): Promise<Config> {53+ const config = await loadConfig();54+ if (!config) fail("not authenticated");55+ return config;56+}@@ -0,0 +1,117 @@
1+// Small output helpers.2+3+export const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;4+export const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;5+export const green = (s: string) => `\x1b[32m${s}\x1b[0m`;6+export const amber = (s: string) => `\x1b[33m${s}\x1b[0m`;7+export const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;8+export const red = (s: string) => `\x1b[31m${s}\x1b[0m`;9+10+export function ago(unixSeconds: number): string {11+ const s = Math.max(0, Math.floor(Date.now() / 1000) - unixSeconds);12+ if (s < 60) return `${s}s ago`;13+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;14+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;15+ return `${Math.floor(s / 86400)}d ago`;16+}17+18+export function ttlLeft(expiresAt: number | null): string {19+ if (expiresAt === null) return "forever";20+ const s = expiresAt - Math.floor(Date.now() / 1000);21+ if (s <= 0) return "expired";22+ if (s < 3600) return `${Math.ceil(s / 60)}m`;23+ if (s < 86400) return `${Math.ceil(s / 3600)}h`;24+ return `${Math.ceil(s / 86400)}d`;25+}26+27+export function table(headers: string[], rows: string[][]): string {28+ const widths = headers.map((h, i) =>29+ Math.max(h.length, ...rows.map((r) => stripAnsi(r[i]).length))30+ );31+ const line = (cells: string[], pad: (s: string, w: number) => string) =>32+ cells.map((c, i) => pad(c, widths[i])).join(" ");33+ const padAnsi = (s: string, w: number) => s + " ".repeat(Math.max(0, w - stripAnsi(s).length));34+ return [35+ dim(line(headers, (s, w) => s.padEnd(w))),36+ ...rows.map((r) => line(r, padAnsi)),37+ ].join("\n");38+}39+40+function stripAnsi(s: string): string {41+ // deno-lint-ignore no-control-regex42+ return s.replace(/\x1b\[[0-9;]*m/g, "");43+}44+45+let jsonMode = false;46+47+export function setJsonMode(on: boolean): void {48+ jsonMode = on;49+}50+51+/**52+ * Print `data` as one JSON line in --json mode, otherwise run the human printer.53+ * With `data` undefined the line is progress-only and suppressed in --json mode.54+ */55+export function emit(human: () => void, data?: unknown): void {56+ if (jsonMode) {57+ if (data !== undefined) console.log(JSON.stringify(data));58+ } else {59+ human();60+ }61+}62+63+/** Actionable next step for common failures — agents read this from the `hint` field. */64+export function hintFor(message: string): string | undefined {65+ if (/not authenticated/.test(message)) {66+ return "run: nzip auth --server <url> --token <token>";67+ }68+ if (/401|unauthorized/i.test(message)) {69+ return "token rejected — re-run: nzip auth --server <url> --token <token>";70+ }71+ if (/not allowed by this config/.test(message)) {72+ return "this config restricts which vaults it may target — target an allowed vault, or ask to edit allowVaults in ~/.config/nzip/config.json";73+ }74+ if (/unknown vault/.test(message)) {75+ return "list vaults with: nzip vault ls — register one with: nzip vault add <name>";76+ }77+ if (/site not found/.test(message)) {78+ return "list sites with: nzip ls --json (targets: 2a3f | vault:alias | alias)";79+ }80+ if (/bare alias.*defaultVault|no defaultVault/.test(message)) {81+ return "use vault:alias explicitly, or set a default with: nzip vault default <name>";82+ }83+ if (/blobs missing/.test(message)) {84+ return "an earlier upload was interrupted — re-run the same nzip push";85+ }86+ if (/invalid --ttl|ttl must be/.test(message)) {87+ return "use --ttl 14d, --ttl 30d, or --ttl forever";88+ }89+ if (/invalid target/.test(message)) {90+ return "targets are 4 hex chars (2a3f), vault:alias (work:demo), or a bare alias";91+ }92+ if (/error sending request|Connection refused|dns error/i.test(message)) {93+ return "server unreachable — check the server URL with: nzip status, or re-run nzip auth";94+ }95+ if (/vault is full|slots are taken/.test(message)) {96+ return "check usage with: nzip vault ls — remove sites with: nzip rm <target>";97+ }98+ return undefined;99+}100+101+export function fail(message: string, hint = hintFor(message)): never {102+ if (jsonMode) {103+ console.error(JSON.stringify({ ok: false, error: message, ...(hint ? { hint } : {}) }));104+ } else {105+ console.error(`${red("✗")} ${message}`);106+ if (hint) console.error(` ${dim(`↳ ${hint}`)}`);107+ }108+ Deno.exit(1);109+}110+111+/** Parse "14d" | "30" | "forever" → ttl value for the API. */112+export function parseTtl(raw: string): number | "forever" {113+ if (raw === "forever" || raw === "0") return "forever";114+ const m = raw.match(/^(\d+)d?$/);115+ if (!m) throw new Error(`invalid --ttl: ${raw} (use e.g. 14d, 30d, forever)`);116+ return parseInt(m[1], 10);117+}@@ -0,0 +1,142 @@
1+// Local breadcrumb registry: which directory on *this* machine produced each2+// pushed site. The server never learns the source path, so we keep a small3+// map here so `nzip where <target>` can answer "where does that live on disk".4+//5+// Kept clean by three forces: expired entries are dropped on every write,6+// `nzip rm` deletes its entry, and `nzip ls` reconciles against the live set.7+8+import { join, resolve } from "@std/path";9+import { configDir } from "./config.ts";10+11+export interface PathEntry {12+ address: string; // 4-hex, canonical identity of the site13+ vault?: string;14+ alias?: string;15+ path: string; // absolute source dir/file this machine pushed from16+ url: string;17+ expiresAt: number | null; // unix seconds, null = permanent18+ pushedAt: number; // unix seconds19+}20+21+/** address → entry */22+type Registry = Record<string, PathEntry>;23+24+function pathsFile(): string {25+ return join(configDir(), "paths.json");26+}27+28+function nowSeconds(): number {29+ return Math.floor(Date.now() / 1000);30+}31+32+async function load(): Promise<Registry> {33+ try {34+ const parsed = JSON.parse(await Deno.readTextFile(pathsFile())) as Registry;35+ return parsed && typeof parsed === "object" ? parsed : {};36+ } catch {37+ return {};38+ }39+}40+41+async function save(reg: Registry): Promise<void> {42+ await Deno.mkdir(configDir(), { recursive: true });43+ const file = pathsFile();44+ await Deno.writeTextFile(file, JSON.stringify(reg, null, 2) + "\n");45+ await Deno.chmod(file, 0o600);46+}47+48+/** Drop entries whose TTL has already elapsed — the server has GC'd them. */49+function pruneExpired(reg: Registry, now = nowSeconds()): Registry {50+ for (const [addr, e] of Object.entries(reg)) {51+ if (e.expiresAt !== null && e.expiresAt <= now) delete reg[addr];52+ }53+ return reg;54+}55+56+/** Record (or refresh) the source path for a site just pushed from `path`. */57+export async function recordPush(entry: {58+ address: string;59+ vault?: string;60+ alias?: string | null;61+ path: string;62+ url: string;63+ expiresAt: number | null;64+}): Promise<void> {65+ const reg = pruneExpired(await load());66+ // An alias points at exactly one address at a time; clear any stale67+ // entry that still claims this vault:alias under a different address.68+ if (entry.vault && entry.alias) {69+ for (const [addr, e] of Object.entries(reg)) {70+ if (addr !== entry.address && e.vault === entry.vault && e.alias === entry.alias) {71+ delete reg[addr];72+ }73+ }74+ }75+ let abs: string;76+ try {77+ abs = Deno.realPathSync(entry.path); // canonical absolute path; file exists now78+ } catch {79+ abs = resolve(entry.path);80+ }81+ reg[entry.address] = {82+ address: entry.address,83+ vault: entry.vault,84+ alias: entry.alias ?? undefined,85+ path: abs,86+ url: entry.url,87+ expiresAt: entry.expiresAt,88+ pushedAt: nowSeconds(),89+ };90+ await save(reg);91+}92+93+/** Look up a tracked source path by address or by vault:alias. */94+export async function lookup(95+ q: { address?: string; vault?: string; alias?: string },96+): Promise<PathEntry | null> {97+ const reg = pruneExpired(await load());98+ if (q.address && reg[q.address]) return reg[q.address];99+ if (q.vault && q.alias) {100+ for (const e of Object.values(reg)) {101+ if (e.vault === q.vault && e.alias === q.alias) return e;102+ }103+ }104+ return null;105+}106+107+/** Forget a single site (called from `nzip rm`). */108+export async function forget(109+ q: { address?: string; vault?: string; alias?: string },110+): Promise<void> {111+ const reg = await load();112+ let changed = false;113+ for (const [addr, e] of Object.entries(reg)) {114+ const hit = (q.address && addr === q.address) ||115+ (q.vault && q.alias && e.vault === q.vault && e.alias === q.alias);116+ if (hit) {117+ delete reg[addr];118+ changed = true;119+ }120+ }121+ if (changed) await save(reg);122+}123+124+/**125+ * Drop registry entries the server no longer lists. `live` is the set of126+ * addresses returned by the API; when `scopeVault` is set only entries in127+ * that vault are eligible for removal (the listing was vault-scoped).128+ */129+export async function reconcile(live: Set<string>, scopeVault?: string): Promise<void> {130+ const reg = pruneExpired(await load());131+ let changed = false;132+ for (const [addr, e] of Object.entries(reg)) {133+ if (scopeVault && e.vault !== scopeVault) continue;134+ if (!live.has(addr)) {135+ delete reg[addr];136+ changed = true;137+ }138+ }139+ // pruneExpired above may also have removed entries — persist either way.140+ await save(reg);141+ void changed;142+}@@ -0,0 +1,82 @@
1+2+// nzip — push HTML and get a tiny URL back.3+4+import { parseArgs } from "@std/cli/parse-args";5+import { cmdAuth } from "./commands/auth.ts";6+import { cmdPush } from "./commands/push.ts";7+import { cmdLs, cmdRevert, cmdRm, cmdShare, cmdStatus } from "./commands/sites.ts";8+import { cmdVault } from "./commands/vault.ts";9+import { cmdWhere } from "./commands/where.ts";10+import { requireConfig } from "./lib/config.ts";11+import { fail, setJsonMode } from "./lib/fmt.ts";12+13+const HELP = `nzip — html share tool14+15+usage:16+ nzip auth [--server URL] [--token T] authenticate against the server17+ nzip vault add <name> [--slot N] register a vault (16 slots, 0x0-0xf)18+ nzip vault ls list vaults19+ nzip vault default <name> set default vault20+ nzip push <dir|file> [target] [--ttl 14d|30d|forever]21+ nzip share <target> [--ttl ...] [--password PW | --no-password]22+ resolve + print url; set ttl/password23+ nzip ls [vault] list sites24+ nzip where <target> print the local dir this machine pushed from25+ nzip rm <target> [--yes] delete a site26+ nzip status server + vault overview27+ nzip revert <target> [--to N] [--list] repoint to a previous push28+29+targets: 2a3f | work:demo | demo (alias in default vault)30+31+agent mode: add --json to any command for one-line JSON on stdout;32+errors go to stderr as {"ok":false,"error":…,"hint":…} with a suggested next step.33+34+vault guard: set "allowVaults": ["home"] in config.json to restrict this install35+to named vaults — pushes/aliases outside the list are refused (agent guardrail).36+`;37+38+async function main(): Promise<void> {39+ const args = parseArgs(Deno.args, {40+ string: ["ttl", "to", "slot", "server", "token", "password"],41+ boolean: ["yes", "list", "help", "no-password", "json"],42+ alias: { h: "help", y: "yes" },43+ });44+ setJsonMode(args.json);45+ const [command, ...rest] = args._.map(String);46+47+ if (args.help || !command) {48+ console.log(HELP);49+ return;50+ }51+52+ if (command === "auth") return await cmdAuth(args.server, args.token);53+54+ const config = await requireConfig();55+ const slot = args.slot === undefined ? undefined : parseInt(args.slot, 10);56+ const toSeq = args.to === undefined ? undefined : parseInt(args.to, 10);57+58+ switch (command) {59+ case "push":60+ return await cmdPush(config, rest[0], rest[1], args.ttl);61+ case "share":62+ return await cmdShare(config, rest[0], args.ttl, args.password, args["no-password"]);63+ case "ls":64+ return await cmdLs(config, rest[0]);65+ case "where":66+ return await cmdWhere(config, rest[0]);67+ case "rm":68+ return await cmdRm(config, rest[0], args.yes);69+ case "status":70+ return await cmdStatus(config);71+ case "revert":72+ return await cmdRevert(config, rest[0], toSeq, args.list);73+ case "vault":74+ return await cmdVault(config, rest[0], rest[1], slot);75+ default:76+ fail(`unknown command: ${command}\n\n${HELP}`);77+ }78+}79+80+if (import.meta.main) {81+ main().catch((e) => fail((e as Error).message));82+}@@ -0,0 +1,38 @@
1+{2+ "version": "5",3+ "specifiers": {4+ "jsr:@std/cli@1": "1.0.31",5+ "jsr:@std/internal@^1.0.14": "1.0.14",6+ "jsr:@std/path@1": "1.1.6"7+ },8+ "jsr": {9+ "@std/cli@1.0.31": {10+ "integrity": "190bb61f809378267c06be3bcd689fcc540f0cfaa5892171144cae787d02e2fd"11+ },12+ "@std/internal@1.0.14": {13+ "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"14+ },15+ "@std/path@1.1.6": {16+ "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe",17+ "dependencies": [18+ "jsr:@std/internal"19+ ]20+ }21+ },22+ "workspace": {23+ "packageJson": {24+ "dependencies": [25+ "npm:wrangler@^4.109.0"26+ ]27+ },28+ "members": {29+ "cli": {30+ "dependencies": [31+ "jsr:@std/cli@1",32+ "jsr:@std/fmt@1",33+ "jsr:@std/path@1"34+ ]35+ }36+ }37+ }38+}@@ -0,0 +1,1502 @@
1+{2+ "name": "nzip",3+ "lockfileVersion": 3,4+ "requires": true,5+ "packages": {6+ "": {7+ "devDependencies": {8+ "wrangler": "^4.109.0"9+ }10+ },11+ "node_modules/@cloudflare/kv-asset-handler": {12+ "version": "0.5.0",13+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",14+ "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",15+ "dev": true,16+ "license": "MIT OR Apache-2.0",17+ "engines": {18+ "node": ">=22.0.0"19+ }20+ },21+ "node_modules/@cloudflare/unenv-preset": {22+ "version": "2.16.1",23+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",24+ "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",25+ "dev": true,26+ "license": "MIT OR Apache-2.0",27+ "peerDependencies": {28+ "unenv": "2.0.0-rc.24",29+ "workerd": ">1.20260305.0 <2.0.0-0"30+ },31+ "peerDependenciesMeta": {32+ "workerd": {33+ "optional": true34+ }35+ }36+ },37+ "node_modules/@cloudflare/workerd-darwin-64": {38+ "version": "1.20260708.1",39+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260708.1.tgz",40+ "integrity": "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==",41+ "cpu": [42+ "x64"43+ ],44+ "dev": true,45+ "license": "Apache-2.0",46+ "optional": true,47+ "os": [48+ "darwin"49+ ],50+ "engines": {51+ "node": ">=16"52+ }53+ },54+ "node_modules/@cloudflare/workerd-darwin-arm64": {55+ "version": "1.20260708.1",56+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260708.1.tgz",57+ "integrity": "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==",58+ "cpu": [59+ "arm64"60+ ],61+ "dev": true,62+ "license": "Apache-2.0",63+ "optional": true,64+ "os": [65+ "darwin"66+ ],67+ "engines": {68+ "node": ">=16"69+ }70+ },71+ "node_modules/@cloudflare/workerd-linux-64": {72+ "version": "1.20260708.1",73+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260708.1.tgz",74+ "integrity": "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==",75+ "cpu": [76+ "x64"77+ ],78+ "dev": true,79+ "license": "Apache-2.0",80+ "optional": true,81+ "os": [82+ "linux"83+ ],84+ "engines": {85+ "node": ">=16"86+ }87+ },88+ "node_modules/@cloudflare/workerd-linux-arm64": {89+ "version": "1.20260708.1",90+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260708.1.tgz",91+ "integrity": "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==",92+ "cpu": [93+ "arm64"94+ ],95+ "dev": true,96+ "license": "Apache-2.0",97+ "optional": true,98+ "os": [99+ "linux"100+ ],101+ "engines": {102+ "node": ">=16"103+ }104+ },105+ "node_modules/@cloudflare/workerd-windows-64": {106+ "version": "1.20260708.1",107+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260708.1.tgz",108+ "integrity": "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==",109+ "cpu": [110+ "x64"111+ ],112+ "dev": true,113+ "license": "Apache-2.0",114+ "optional": true,115+ "os": [116+ "win32"117+ ],118+ "engines": {119+ "node": ">=16"120+ }121+ },122+ "node_modules/@cspotcode/source-map-support": {123+ "version": "0.8.1",124+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",125+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",126+ "dev": true,127+ "license": "MIT",128+ "dependencies": {129+ "@jridgewell/trace-mapping": "0.3.9"130+ },131+ "engines": {132+ "node": ">=12"133+ }134+ },135+ "node_modules/@emnapi/runtime": {136+ "version": "1.11.2",137+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",138+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",139+ "dev": true,140+ "license": "MIT",141+ "optional": true,142+ "dependencies": {143+ "tslib": "^2.4.0"144+ }145+ },146+ "node_modules/@esbuild/aix-ppc64": {147+ "version": "0.28.1",148+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",149+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",150+ "cpu": [151+ "ppc64"152+ ],153+ "dev": true,154+ "license": "MIT",155+ "optional": true,156+ "os": [157+ "aix"158+ ],159+ "engines": {160+ "node": ">=18"161+ }162+ },163+ "node_modules/@esbuild/android-arm": {164+ "version": "0.28.1",165+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",166+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",167+ "cpu": [168+ "arm"169+ ],170+ "dev": true,171+ "license": "MIT",172+ "optional": true,173+ "os": [174+ "android"175+ ],176+ "engines": {177+ "node": ">=18"178+ }179+ },180+ "node_modules/@esbuild/android-arm64": {181+ "version": "0.28.1",182+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",183+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",184+ "cpu": [185+ "arm64"186+ ],187+ "dev": true,188+ "license": "MIT",189+ "optional": true,190+ "os": [191+ "android"192+ ],193+ "engines": {194+ "node": ">=18"195+ }196+ },197+ "node_modules/@esbuild/android-x64": {198+ "version": "0.28.1",199+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",200+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",201+ "cpu": [202+ "x64"203+ ],204+ "dev": true,205+ "license": "MIT",206+ "optional": true,207+ "os": [208+ "android"209+ ],210+ "engines": {211+ "node": ">=18"212+ }213+ },214+ "node_modules/@esbuild/darwin-arm64": {215+ "version": "0.28.1",216+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",217+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",218+ "cpu": [219+ "arm64"220+ ],221+ "dev": true,222+ "license": "MIT",223+ "optional": true,224+ "os": [225+ "darwin"226+ ],227+ "engines": {228+ "node": ">=18"229+ }230+ },231+ "node_modules/@esbuild/darwin-x64": {232+ "version": "0.28.1",233+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",234+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",235+ "cpu": [236+ "x64"237+ ],238+ "dev": true,239+ "license": "MIT",240+ "optional": true,241+ "os": [242+ "darwin"243+ ],244+ "engines": {245+ "node": ">=18"246+ }247+ },248+ "node_modules/@esbuild/freebsd-arm64": {249+ "version": "0.28.1",250+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",251+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",252+ "cpu": [253+ "arm64"254+ ],255+ "dev": true,256+ "license": "MIT",257+ "optional": true,258+ "os": [259+ "freebsd"260+ ],261+ "engines": {262+ "node": ">=18"263+ }264+ },265+ "node_modules/@esbuild/freebsd-x64": {266+ "version": "0.28.1",267+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",268+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",269+ "cpu": [270+ "x64"271+ ],272+ "dev": true,273+ "license": "MIT",274+ "optional": true,275+ "os": [276+ "freebsd"277+ ],278+ "engines": {279+ "node": ">=18"280+ }281+ },282+ "node_modules/@esbuild/linux-arm": {283+ "version": "0.28.1",284+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",285+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",286+ "cpu": [287+ "arm"288+ ],289+ "dev": true,290+ "license": "MIT",291+ "optional": true,292+ "os": [293+ "linux"294+ ],295+ "engines": {296+ "node": ">=18"297+ }298+ },299+ "node_modules/@esbuild/linux-arm64": {300+ "version": "0.28.1",301+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",302+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",303+ "cpu": [304+ "arm64"305+ ],306+ "dev": true,307+ "license": "MIT",308+ "optional": true,309+ "os": [310+ "linux"311+ ],312+ "engines": {313+ "node": ">=18"314+ }315+ },316+ "node_modules/@esbuild/linux-ia32": {317+ "version": "0.28.1",318+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",319+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",320+ "cpu": [321+ "ia32"322+ ],323+ "dev": true,324+ "license": "MIT",325+ "optional": true,326+ "os": [327+ "linux"328+ ],329+ "engines": {330+ "node": ">=18"331+ }332+ },333+ "node_modules/@esbuild/linux-loong64": {334+ "version": "0.28.1",335+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",336+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",337+ "cpu": [338+ "loong64"339+ ],340+ "dev": true,341+ "license": "MIT",342+ "optional": true,343+ "os": [344+ "linux"345+ ],346+ "engines": {347+ "node": ">=18"348+ }349+ },350+ "node_modules/@esbuild/linux-mips64el": {351+ "version": "0.28.1",352+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",353+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",354+ "cpu": [355+ "mips64el"356+ ],357+ "dev": true,358+ "license": "MIT",359+ "optional": true,360+ "os": [361+ "linux"362+ ],363+ "engines": {364+ "node": ">=18"365+ }366+ },367+ "node_modules/@esbuild/linux-ppc64": {368+ "version": "0.28.1",369+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",370+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",371+ "cpu": [372+ "ppc64"373+ ],374+ "dev": true,375+ "license": "MIT",376+ "optional": true,377+ "os": [378+ "linux"379+ ],380+ "engines": {381+ "node": ">=18"382+ }383+ },384+ "node_modules/@esbuild/linux-riscv64": {385+ "version": "0.28.1",386+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",387+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",388+ "cpu": [389+ "riscv64"390+ ],391+ "dev": true,392+ "license": "MIT",393+ "optional": true,394+ "os": [395+ "linux"396+ ],397+ "engines": {398+ "node": ">=18"399+ }400+ },401+ "node_modules/@esbuild/linux-s390x": {402+ "version": "0.28.1",403+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",404+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",405+ "cpu": [406+ "s390x"407+ ],408+ "dev": true,409+ "license": "MIT",410+ "optional": true,411+ "os": [412+ "linux"413+ ],414+ "engines": {415+ "node": ">=18"416+ }417+ },418+ "node_modules/@esbuild/linux-x64": {419+ "version": "0.28.1",420+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",421+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",422+ "cpu": [423+ "x64"424+ ],425+ "dev": true,426+ "license": "MIT",427+ "optional": true,428+ "os": [429+ "linux"430+ ],431+ "engines": {432+ "node": ">=18"433+ }434+ },435+ "node_modules/@esbuild/netbsd-arm64": {436+ "version": "0.28.1",437+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",438+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",439+ "cpu": [440+ "arm64"441+ ],442+ "dev": true,443+ "license": "MIT",444+ "optional": true,445+ "os": [446+ "netbsd"447+ ],448+ "engines": {449+ "node": ">=18"450+ }451+ },452+ "node_modules/@esbuild/netbsd-x64": {453+ "version": "0.28.1",454+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",455+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",456+ "cpu": [457+ "x64"458+ ],459+ "dev": true,460+ "license": "MIT",461+ "optional": true,462+ "os": [463+ "netbsd"464+ ],465+ "engines": {466+ "node": ">=18"467+ }468+ },469+ "node_modules/@esbuild/openbsd-arm64": {470+ "version": "0.28.1",471+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",472+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",473+ "cpu": [474+ "arm64"475+ ],476+ "dev": true,477+ "license": "MIT",478+ "optional": true,479+ "os": [480+ "openbsd"481+ ],482+ "engines": {483+ "node": ">=18"484+ }485+ },486+ "node_modules/@esbuild/openbsd-x64": {487+ "version": "0.28.1",488+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",489+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",490+ "cpu": [491+ "x64"492+ ],493+ "dev": true,494+ "license": "MIT",495+ "optional": true,496+ "os": [497+ "openbsd"498+ ],499+ "engines": {500+ "node": ">=18"501+ }502+ },503+ "node_modules/@esbuild/openharmony-arm64": {504+ "version": "0.28.1",505+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",506+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",507+ "cpu": [508+ "arm64"509+ ],510+ "dev": true,511+ "license": "MIT",512+ "optional": true,513+ "os": [514+ "openharmony"515+ ],516+ "engines": {517+ "node": ">=18"518+ }519+ },520+ "node_modules/@esbuild/sunos-x64": {521+ "version": "0.28.1",522+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",523+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",524+ "cpu": [525+ "x64"526+ ],527+ "dev": true,528+ "license": "MIT",529+ "optional": true,530+ "os": [531+ "sunos"532+ ],533+ "engines": {534+ "node": ">=18"535+ }536+ },537+ "node_modules/@esbuild/win32-arm64": {538+ "version": "0.28.1",539+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",540+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",541+ "cpu": [542+ "arm64"543+ ],544+ "dev": true,545+ "license": "MIT",546+ "optional": true,547+ "os": [548+ "win32"549+ ],550+ "engines": {551+ "node": ">=18"552+ }553+ },554+ "node_modules/@esbuild/win32-ia32": {555+ "version": "0.28.1",556+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",557+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",558+ "cpu": [559+ "ia32"560+ ],561+ "dev": true,562+ "license": "MIT",563+ "optional": true,564+ "os": [565+ "win32"566+ ],567+ "engines": {568+ "node": ">=18"569+ }570+ },571+ "node_modules/@esbuild/win32-x64": {572+ "version": "0.28.1",573+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",574+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",575+ "cpu": [576+ "x64"577+ ],578+ "dev": true,579+ "license": "MIT",580+ "optional": true,581+ "os": [582+ "win32"583+ ],584+ "engines": {585+ "node": ">=18"586+ }587+ },588+ "node_modules/@img/colour": {589+ "version": "1.1.0",590+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",591+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",592+ "dev": true,593+ "license": "MIT",594+ "engines": {595+ "node": ">=18"596+ }597+ },598+ "node_modules/@img/sharp-darwin-arm64": {599+ "version": "0.34.5",600+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",601+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",602+ "cpu": [603+ "arm64"604+ ],605+ "dev": true,606+ "license": "Apache-2.0",607+ "optional": true,608+ "os": [609+ "darwin"610+ ],611+ "engines": {612+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"613+ },614+ "funding": {615+ "url": "https://opencollective.com/libvips"616+ },617+ "optionalDependencies": {618+ "@img/sharp-libvips-darwin-arm64": "1.2.4"619+ }620+ },621+ "node_modules/@img/sharp-darwin-x64": {622+ "version": "0.34.5",623+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",624+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",625+ "cpu": [626+ "x64"627+ ],628+ "dev": true,629+ "license": "Apache-2.0",630+ "optional": true,631+ "os": [632+ "darwin"633+ ],634+ "engines": {635+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"636+ },637+ "funding": {638+ "url": "https://opencollective.com/libvips"639+ },640+ "optionalDependencies": {641+ "@img/sharp-libvips-darwin-x64": "1.2.4"642+ }643+ },644+ "node_modules/@img/sharp-libvips-darwin-arm64": {645+ "version": "1.2.4",646+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",647+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",648+ "cpu": [649+ "arm64"650+ ],651+ "dev": true,652+ "license": "LGPL-3.0-or-later",653+ "optional": true,654+ "os": [655+ "darwin"656+ ],657+ "funding": {658+ "url": "https://opencollective.com/libvips"659+ }660+ },661+ "node_modules/@img/sharp-libvips-darwin-x64": {662+ "version": "1.2.4",663+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",664+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",665+ "cpu": [666+ "x64"667+ ],668+ "dev": true,669+ "license": "LGPL-3.0-or-later",670+ "optional": true,671+ "os": [672+ "darwin"673+ ],674+ "funding": {675+ "url": "https://opencollective.com/libvips"676+ }677+ },678+ "node_modules/@img/sharp-libvips-linux-arm": {679+ "version": "1.2.4",680+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",681+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",682+ "cpu": [683+ "arm"684+ ],685+ "dev": true,686+ "license": "LGPL-3.0-or-later",687+ "optional": true,688+ "os": [689+ "linux"690+ ],691+ "funding": {692+ "url": "https://opencollective.com/libvips"693+ }694+ },695+ "node_modules/@img/sharp-libvips-linux-arm64": {696+ "version": "1.2.4",697+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",698+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",699+ "cpu": [700+ "arm64"701+ ],702+ "dev": true,703+ "license": "LGPL-3.0-or-later",704+ "optional": true,705+ "os": [706+ "linux"707+ ],708+ "funding": {709+ "url": "https://opencollective.com/libvips"710+ }711+ },712+ "node_modules/@img/sharp-libvips-linux-ppc64": {713+ "version": "1.2.4",714+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",715+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",716+ "cpu": [717+ "ppc64"718+ ],719+ "dev": true,720+ "license": "LGPL-3.0-or-later",721+ "optional": true,722+ "os": [723+ "linux"724+ ],725+ "funding": {726+ "url": "https://opencollective.com/libvips"727+ }728+ },729+ "node_modules/@img/sharp-libvips-linux-riscv64": {730+ "version": "1.2.4",731+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",732+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",733+ "cpu": [734+ "riscv64"735+ ],736+ "dev": true,737+ "license": "LGPL-3.0-or-later",738+ "optional": true,739+ "os": [740+ "linux"741+ ],742+ "funding": {743+ "url": "https://opencollective.com/libvips"744+ }745+ },746+ "node_modules/@img/sharp-libvips-linux-s390x": {747+ "version": "1.2.4",748+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",749+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",750+ "cpu": [751+ "s390x"752+ ],753+ "dev": true,754+ "license": "LGPL-3.0-or-later",755+ "optional": true,756+ "os": [757+ "linux"758+ ],759+ "funding": {760+ "url": "https://opencollective.com/libvips"761+ }762+ },763+ "node_modules/@img/sharp-libvips-linux-x64": {764+ "version": "1.2.4",765+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",766+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",767+ "cpu": [768+ "x64"769+ ],770+ "dev": true,771+ "license": "LGPL-3.0-or-later",772+ "optional": true,773+ "os": [774+ "linux"775+ ],776+ "funding": {777+ "url": "https://opencollective.com/libvips"778+ }779+ },780+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {781+ "version": "1.2.4",782+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",783+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",784+ "cpu": [785+ "arm64"786+ ],787+ "dev": true,788+ "license": "LGPL-3.0-or-later",789+ "optional": true,790+ "os": [791+ "linux"792+ ],793+ "funding": {794+ "url": "https://opencollective.com/libvips"795+ }796+ },797+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {798+ "version": "1.2.4",799+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",800+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",801+ "cpu": [802+ "x64"803+ ],804+ "dev": true,805+ "license": "LGPL-3.0-or-later",806+ "optional": true,807+ "os": [808+ "linux"809+ ],810+ "funding": {811+ "url": "https://opencollective.com/libvips"812+ }813+ },814+ "node_modules/@img/sharp-linux-arm": {815+ "version": "0.34.5",816+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",817+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",818+ "cpu": [819+ "arm"820+ ],821+ "dev": true,822+ "license": "Apache-2.0",823+ "optional": true,824+ "os": [825+ "linux"826+ ],827+ "engines": {828+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"829+ },830+ "funding": {831+ "url": "https://opencollective.com/libvips"832+ },833+ "optionalDependencies": {834+ "@img/sharp-libvips-linux-arm": "1.2.4"835+ }836+ },837+ "node_modules/@img/sharp-linux-arm64": {838+ "version": "0.34.5",839+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",840+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",841+ "cpu": [842+ "arm64"843+ ],844+ "dev": true,845+ "license": "Apache-2.0",846+ "optional": true,847+ "os": [848+ "linux"849+ ],850+ "engines": {851+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"852+ },853+ "funding": {854+ "url": "https://opencollective.com/libvips"855+ },856+ "optionalDependencies": {857+ "@img/sharp-libvips-linux-arm64": "1.2.4"858+ }859+ },860+ "node_modules/@img/sharp-linux-ppc64": {861+ "version": "0.34.5",862+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",863+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",864+ "cpu": [865+ "ppc64"866+ ],867+ "dev": true,868+ "license": "Apache-2.0",869+ "optional": true,870+ "os": [871+ "linux"872+ ],873+ "engines": {874+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"875+ },876+ "funding": {877+ "url": "https://opencollective.com/libvips"878+ },879+ "optionalDependencies": {880+ "@img/sharp-libvips-linux-ppc64": "1.2.4"881+ }882+ },883+ "node_modules/@img/sharp-linux-riscv64": {884+ "version": "0.34.5",885+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",886+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",887+ "cpu": [888+ "riscv64"889+ ],890+ "dev": true,891+ "license": "Apache-2.0",892+ "optional": true,893+ "os": [894+ "linux"895+ ],896+ "engines": {897+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"898+ },899+ "funding": {900+ "url": "https://opencollective.com/libvips"901+ },902+ "optionalDependencies": {903+ "@img/sharp-libvips-linux-riscv64": "1.2.4"904+ }905+ },906+ "node_modules/@img/sharp-linux-s390x": {907+ "version": "0.34.5",908+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",909+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",910+ "cpu": [911+ "s390x"912+ ],913+ "dev": true,914+ "license": "Apache-2.0",915+ "optional": true,916+ "os": [917+ "linux"918+ ],919+ "engines": {920+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"921+ },922+ "funding": {923+ "url": "https://opencollective.com/libvips"924+ },925+ "optionalDependencies": {926+ "@img/sharp-libvips-linux-s390x": "1.2.4"927+ }928+ },929+ "node_modules/@img/sharp-linux-x64": {930+ "version": "0.34.5",931+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",932+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",933+ "cpu": [934+ "x64"935+ ],936+ "dev": true,937+ "license": "Apache-2.0",938+ "optional": true,939+ "os": [940+ "linux"941+ ],942+ "engines": {943+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"944+ },945+ "funding": {946+ "url": "https://opencollective.com/libvips"947+ },948+ "optionalDependencies": {949+ "@img/sharp-libvips-linux-x64": "1.2.4"950+ }951+ },952+ "node_modules/@img/sharp-linuxmusl-arm64": {953+ "version": "0.34.5",954+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",955+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",956+ "cpu": [957+ "arm64"958+ ],959+ "dev": true,960+ "license": "Apache-2.0",961+ "optional": true,962+ "os": [963+ "linux"964+ ],965+ "engines": {966+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"967+ },968+ "funding": {969+ "url": "https://opencollective.com/libvips"970+ },971+ "optionalDependencies": {972+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"973+ }974+ },975+ "node_modules/@img/sharp-linuxmusl-x64": {976+ "version": "0.34.5",977+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",978+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",979+ "cpu": [980+ "x64"981+ ],982+ "dev": true,983+ "license": "Apache-2.0",984+ "optional": true,985+ "os": [986+ "linux"987+ ],988+ "engines": {989+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"990+ },991+ "funding": {992+ "url": "https://opencollective.com/libvips"993+ },994+ "optionalDependencies": {995+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"996+ }997+ },998+ "node_modules/@img/sharp-wasm32": {999+ "version": "0.34.5",1000+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",1001+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",1002+ "cpu": [1003+ "wasm32"1004+ ],1005+ "dev": true,1006+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",1007+ "optional": true,1008+ "dependencies": {1009+ "@emnapi/runtime": "^1.7.0"1010+ },1011+ "engines": {1012+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1013+ },1014+ "funding": {1015+ "url": "https://opencollective.com/libvips"1016+ }1017+ },1018+ "node_modules/@img/sharp-win32-arm64": {1019+ "version": "0.34.5",1020+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",1021+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",1022+ "cpu": [1023+ "arm64"1024+ ],1025+ "dev": true,1026+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1027+ "optional": true,1028+ "os": [1029+ "win32"1030+ ],1031+ "engines": {1032+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1033+ },1034+ "funding": {1035+ "url": "https://opencollective.com/libvips"1036+ }1037+ },1038+ "node_modules/@img/sharp-win32-ia32": {1039+ "version": "0.34.5",1040+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",1041+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",1042+ "cpu": [1043+ "ia32"1044+ ],1045+ "dev": true,1046+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1047+ "optional": true,1048+ "os": [1049+ "win32"1050+ ],1051+ "engines": {1052+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1053+ },1054+ "funding": {1055+ "url": "https://opencollective.com/libvips"1056+ }1057+ },1058+ "node_modules/@img/sharp-win32-x64": {1059+ "version": "0.34.5",1060+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",1061+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",1062+ "cpu": [1063+ "x64"1064+ ],1065+ "dev": true,1066+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1067+ "optional": true,1068+ "os": [1069+ "win32"1070+ ],1071+ "engines": {1072+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1073+ },1074+ "funding": {1075+ "url": "https://opencollective.com/libvips"1076+ }1077+ },1078+ "node_modules/@jridgewell/resolve-uri": {1079+ "version": "3.1.2",1080+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",1081+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",1082+ "dev": true,1083+ "license": "MIT",1084+ "engines": {1085+ "node": ">=6.0.0"1086+ }1087+ },1088+ "node_modules/@jridgewell/sourcemap-codec": {1089+ "version": "1.5.5",1090+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",1091+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",1092+ "dev": true,1093+ "license": "MIT"1094+ },1095+ "node_modules/@jridgewell/trace-mapping": {1096+ "version": "0.3.9",1097+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",1098+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",1099+ "dev": true,1100+ "license": "MIT",1101+ "dependencies": {1102+ "@jridgewell/resolve-uri": "^3.0.3",1103+ "@jridgewell/sourcemap-codec": "^1.4.10"1104+ }1105+ },1106+ "node_modules/@poppinss/colors": {1107+ "version": "4.1.6",1108+ "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",1109+ "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",1110+ "dev": true,1111+ "license": "MIT",1112+ "dependencies": {1113+ "kleur": "^4.1.5"1114+ }1115+ },1116+ "node_modules/@poppinss/dumper": {1117+ "version": "0.6.5",1118+ "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",1119+ "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",1120+ "dev": true,1121+ "license": "MIT",1122+ "dependencies": {1123+ "@poppinss/colors": "^4.1.5",1124+ "@sindresorhus/is": "^7.0.2",1125+ "supports-color": "^10.0.0"1126+ }1127+ },1128+ "node_modules/@poppinss/exception": {1129+ "version": "1.2.3",1130+ "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",1131+ "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",1132+ "dev": true,1133+ "license": "MIT"1134+ },1135+ "node_modules/@sindresorhus/is": {1136+ "version": "7.2.0",1137+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",1138+ "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",1139+ "dev": true,1140+ "license": "MIT",1141+ "engines": {1142+ "node": ">=18"1143+ },1144+ "funding": {1145+ "url": "https://github.com/sindresorhus/is?sponsor=1"1146+ }1147+ },1148+ "node_modules/@speed-highlight/core": {1149+ "version": "1.2.17",1150+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz",1151+ "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==",1152+ "dev": true,1153+ "license": "CC0-1.0"1154+ },1155+ "node_modules/blake3-wasm": {1156+ "version": "2.1.5",1157+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",1158+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",1159+ "dev": true,1160+ "license": "MIT"1161+ },1162+ "node_modules/cookie": {1163+ "version": "1.1.1",1164+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",1165+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",1166+ "dev": true,1167+ "license": "MIT",1168+ "engines": {1169+ "node": ">=18"1170+ },1171+ "funding": {1172+ "type": "opencollective",1173+ "url": "https://opencollective.com/express"1174+ }1175+ },1176+ "node_modules/detect-libc": {1177+ "version": "2.1.2",1178+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",1179+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",1180+ "dev": true,1181+ "license": "Apache-2.0",1182+ "engines": {1183+ "node": ">=8"1184+ }1185+ },1186+ "node_modules/error-stack-parser-es": {1187+ "version": "1.0.5",1188+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",1189+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",1190+ "dev": true,1191+ "license": "MIT",1192+ "funding": {1193+ "url": "https://github.com/sponsors/antfu"1194+ }1195+ },1196+ "node_modules/esbuild": {1197+ "version": "0.28.1",1198+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",1199+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",1200+ "dev": true,1201+ "hasInstallScript": true,1202+ "license": "MIT",1203+ "bin": {1204+ "esbuild": "bin/esbuild"1205+ },1206+ "engines": {1207+ "node": ">=18"1208+ },1209+ "optionalDependencies": {1210+ "@esbuild/aix-ppc64": "0.28.1",1211+ "@esbuild/android-arm": "0.28.1",1212+ "@esbuild/android-arm64": "0.28.1",1213+ "@esbuild/android-x64": "0.28.1",1214+ "@esbuild/darwin-arm64": "0.28.1",1215+ "@esbuild/darwin-x64": "0.28.1",1216+ "@esbuild/freebsd-arm64": "0.28.1",1217+ "@esbuild/freebsd-x64": "0.28.1",1218+ "@esbuild/linux-arm": "0.28.1",1219+ "@esbuild/linux-arm64": "0.28.1",1220+ "@esbuild/linux-ia32": "0.28.1",1221+ "@esbuild/linux-loong64": "0.28.1",1222+ "@esbuild/linux-mips64el": "0.28.1",1223+ "@esbuild/linux-ppc64": "0.28.1",1224+ "@esbuild/linux-riscv64": "0.28.1",1225+ "@esbuild/linux-s390x": "0.28.1",1226+ "@esbuild/linux-x64": "0.28.1",1227+ "@esbuild/netbsd-arm64": "0.28.1",1228+ "@esbuild/netbsd-x64": "0.28.1",1229+ "@esbuild/openbsd-arm64": "0.28.1",1230+ "@esbuild/openbsd-x64": "0.28.1",1231+ "@esbuild/openharmony-arm64": "0.28.1",1232+ "@esbuild/sunos-x64": "0.28.1",1233+ "@esbuild/win32-arm64": "0.28.1",1234+ "@esbuild/win32-ia32": "0.28.1",1235+ "@esbuild/win32-x64": "0.28.1"1236+ }1237+ },1238+ "node_modules/fsevents": {1239+ "version": "2.3.3",1240+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",1241+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",1242+ "dev": true,1243+ "hasInstallScript": true,1244+ "license": "MIT",1245+ "optional": true,1246+ "os": [1247+ "darwin"1248+ ],1249+ "engines": {1250+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"1251+ }1252+ },1253+ "node_modules/kleur": {1254+ "version": "4.1.5",1255+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",1256+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",1257+ "dev": true,1258+ "license": "MIT",1259+ "engines": {1260+ "node": ">=6"1261+ }1262+ },1263+ "node_modules/miniflare": {1264+ "version": "4.20260708.0",1265+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.0.tgz",1266+ "integrity": "sha512-ut32kKKTyZIoXhbtteDAMOwTVx9aCeehMeKb1xmJs4RLdBrF5flSZD1MOpWqmJycIV7YBHjcaZcVHLMr/g5RZA==",1267+ "dev": true,1268+ "license": "MIT",1269+ "dependencies": {1270+ "@cspotcode/source-map-support": "0.8.1",1271+ "sharp": "0.34.5",1272+ "undici": "7.28.0",1273+ "workerd": "1.20260708.1",1274+ "ws": "8.21.0",1275+ "youch": "4.1.0-beta.10"1276+ },1277+ "bin": {1278+ "miniflare": "bootstrap.js"1279+ },1280+ "engines": {1281+ "node": ">=22.0.0"1282+ }1283+ },1284+ "node_modules/path-to-regexp": {1285+ "version": "6.3.0",1286+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",1287+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",1288+ "dev": true,1289+ "license": "MIT"1290+ },1291+ "node_modules/pathe": {1292+ "version": "2.0.3",1293+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",1294+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",1295+ "dev": true,1296+ "license": "MIT"1297+ },1298+ "node_modules/semver": {1299+ "version": "7.8.5",1300+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",1301+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",1302+ "dev": true,1303+ "license": "ISC",1304+ "bin": {1305+ "semver": "bin/semver.js"1306+ },1307+ "engines": {1308+ "node": ">=10"1309+ }1310+ },1311+ "node_modules/sharp": {1312+ "version": "0.34.5",1313+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",1314+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",1315+ "dev": true,1316+ "hasInstallScript": true,1317+ "license": "Apache-2.0",1318+ "dependencies": {1319+ "@img/colour": "^1.0.0",1320+ "detect-libc": "^2.1.2",1321+ "semver": "^7.7.3"1322+ },1323+ "engines": {1324+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1325+ },1326+ "funding": {1327+ "url": "https://opencollective.com/libvips"1328+ },1329+ "optionalDependencies": {1330+ "@img/sharp-darwin-arm64": "0.34.5",1331+ "@img/sharp-darwin-x64": "0.34.5",1332+ "@img/sharp-libvips-darwin-arm64": "1.2.4",1333+ "@img/sharp-libvips-darwin-x64": "1.2.4",1334+ "@img/sharp-libvips-linux-arm": "1.2.4",1335+ "@img/sharp-libvips-linux-arm64": "1.2.4",1336+ "@img/sharp-libvips-linux-ppc64": "1.2.4",1337+ "@img/sharp-libvips-linux-riscv64": "1.2.4",1338+ "@img/sharp-libvips-linux-s390x": "1.2.4",1339+ "@img/sharp-libvips-linux-x64": "1.2.4",1340+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",1341+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",1342+ "@img/sharp-linux-arm": "0.34.5",1343+ "@img/sharp-linux-arm64": "0.34.5",1344+ "@img/sharp-linux-ppc64": "0.34.5",1345+ "@img/sharp-linux-riscv64": "0.34.5",1346+ "@img/sharp-linux-s390x": "0.34.5",1347+ "@img/sharp-linux-x64": "0.34.5",1348+ "@img/sharp-linuxmusl-arm64": "0.34.5",1349+ "@img/sharp-linuxmusl-x64": "0.34.5",1350+ "@img/sharp-wasm32": "0.34.5",1351+ "@img/sharp-win32-arm64": "0.34.5",1352+ "@img/sharp-win32-ia32": "0.34.5",1353+ "@img/sharp-win32-x64": "0.34.5"1354+ }1355+ },1356+ "node_modules/supports-color": {1357+ "version": "10.2.2",1358+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",1359+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",1360+ "dev": true,1361+ "license": "MIT",1362+ "engines": {1363+ "node": ">=18"1364+ },1365+ "funding": {1366+ "url": "https://github.com/chalk/supports-color?sponsor=1"1367+ }1368+ },1369+ "node_modules/tslib": {1370+ "version": "2.8.1",1371+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",1372+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",1373+ "dev": true,1374+ "license": "0BSD",1375+ "optional": true1376+ },1377+ "node_modules/undici": {1378+ "version": "7.28.0",1379+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",1380+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",1381+ "dev": true,1382+ "license": "MIT",1383+ "engines": {1384+ "node": ">=20.18.1"1385+ }1386+ },1387+ "node_modules/unenv": {1388+ "version": "2.0.0-rc.24",1389+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",1390+ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",1391+ "dev": true,1392+ "license": "MIT",1393+ "dependencies": {1394+ "pathe": "^2.0.3"1395+ }1396+ },1397+ "node_modules/workerd": {1398+ "version": "1.20260708.1",1399+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260708.1.tgz",1400+ "integrity": "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==",1401+ "dev": true,1402+ "hasInstallScript": true,1403+ "license": "Apache-2.0",1404+ "bin": {1405+ "workerd": "bin/workerd"1406+ },1407+ "engines": {1408+ "node": ">=16"1409+ },1410+ "optionalDependencies": {1411+ "@cloudflare/workerd-darwin-64": "1.20260708.1",1412+ "@cloudflare/workerd-darwin-arm64": "1.20260708.1",1413+ "@cloudflare/workerd-linux-64": "1.20260708.1",1414+ "@cloudflare/workerd-linux-arm64": "1.20260708.1",1415+ "@cloudflare/workerd-windows-64": "1.20260708.1"1416+ }1417+ },1418+ "node_modules/wrangler": {1419+ "version": "4.109.0",1420+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.109.0.tgz",1421+ "integrity": "sha512-Ev6iIBNnRwvYeJv8B7377YsnY+PxEznHmz3r33cWgP+6A5HNqqqJhChBaVUWxxs0uQPKuYBy8tqrfyd7o8WmBg==",1422+ "dev": true,1423+ "license": "MIT OR Apache-2.0",1424+ "dependencies": {1425+ "@cloudflare/kv-asset-handler": "0.5.0",1426+ "@cloudflare/unenv-preset": "2.16.1",1427+ "blake3-wasm": "2.1.5",1428+ "esbuild": "0.28.1",1429+ "miniflare": "4.20260708.0",1430+ "path-to-regexp": "6.3.0",1431+ "unenv": "2.0.0-rc.24",1432+ "workerd": "1.20260708.1"1433+ },1434+ "bin": {1435+ "cf-wrangler": "bin/cf-wrangler.js",1436+ "wrangler": "bin/wrangler.js",1437+ "wrangler2": "bin/wrangler.js"1438+ },1439+ "engines": {1440+ "node": ">=22.0.0"1441+ },1442+ "optionalDependencies": {1443+ "fsevents": "2.3.3"1444+ },1445+ "peerDependencies": {1446+ "@cloudflare/workers-types": "^5.20260708.1"1447+ },1448+ "peerDependenciesMeta": {1449+ "@cloudflare/workers-types": {1450+ "optional": true1451+ }1452+ }1453+ },1454+ "node_modules/ws": {1455+ "version": "8.21.0",1456+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",1457+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",1458+ "dev": true,1459+ "license": "MIT",1460+ "engines": {1461+ "node": ">=10.0.0"1462+ },1463+ "peerDependencies": {1464+ "bufferutil": "^4.0.1",1465+ "utf-8-validate": ">=5.0.2"1466+ },1467+ "peerDependenciesMeta": {1468+ "bufferutil": {1469+ "optional": true1470+ },1471+ "utf-8-validate": {1472+ "optional": true1473+ }1474+ }1475+ },1476+ "node_modules/youch": {1477+ "version": "4.1.0-beta.10",1478+ "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",1479+ "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",1480+ "dev": true,1481+ "license": "MIT",1482+ "dependencies": {1483+ "@poppinss/colors": "^4.1.5",1484+ "@poppinss/dumper": "^0.6.4",1485+ "@speed-highlight/core": "^1.2.7",1486+ "cookie": "^1.0.2",1487+ "youch-core": "^0.3.3"1488+ }1489+ },1490+ "node_modules/youch-core": {1491+ "version": "0.3.3",1492+ "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",1493+ "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",1494+ "dev": true,1495+ "license": "MIT",1496+ "dependencies": {1497+ "@poppinss/exception": "^1.2.2",1498+ "error-stack-parser-es": "^1.0.5"1499+ }1500+ }1501+ }1502+}@@ -0,0 +1,56 @@
1+// 4-hex site addresses and vault:alias target parsing.2+// address (int 0..65535) = vaultSlot << 12 | siteId3+4+export const VAULT_SLOTS = 16;5+export const SITES_PER_VAULT = 0x1000;6+7+const ADDR_RE = /^[0-9a-f]{4}$/;8+const NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;9+10+export function isAddressString(s: string): boolean {11+ return ADDR_RE.test(s);12+}13+14+/** "2a3f" → 0x2a3f. Throws on malformed input. */15+export function parseAddress(s: string): number {16+ if (!ADDR_RE.test(s)) throw new Error(`invalid address: ${JSON.stringify(s)}`);17+ return parseInt(s, 16);18+}19+20+/** 0x2a3f → "2a3f" */21+export function formatAddress(addr: number): string {22+ if (!Number.isInteger(addr) || addr < 0 || addr >= VAULT_SLOTS * SITES_PER_VAULT) {23+ throw new Error(`address out of range: ${addr}`);24+ }25+ return addr.toString(16).padStart(4, "0");26+}27+28+export function vaultSlotOf(addr: number): number {29+ return addr >> 12;30+}31+32+/** Valid vault or alias name: lowercase alnum + hyphen, 1-32 chars, must not look like an address. */33+export function isValidName(name: string): boolean {34+ return NAME_RE.test(name) && !ADDR_RE.test(name);35+}36+37+export type ParsedTarget =38+ | { kind: "address"; address: number }39+ | { kind: "vaultAlias"; vault: string; alias: string }40+ | { kind: "alias"; alias: string };41+42+/** Parse a CLI/API target: "2a3f" | "work:demo" | "demo". */43+export function parseTarget(s: string): ParsedTarget {44+ if (ADDR_RE.test(s)) return { kind: "address", address: parseInt(s, 16) };45+ const colon = s.indexOf(":");46+ if (colon !== -1) {47+ const vault = s.slice(0, colon);48+ const alias = s.slice(colon + 1);49+ if (!isValidName(vault) || !isValidName(alias)) {50+ throw new Error(`invalid target: ${JSON.stringify(s)}`);51+ }52+ return { kind: "vaultAlias", vault, alias };53+ }54+ if (!isValidName(s)) throw new Error(`invalid target: ${JSON.stringify(s)}`);55+ return { kind: "alias", alias: s };56+}@@ -0,0 +1,5 @@
1+/** sha256 of bytes as lowercase hex. Web Crypto only — works in Deno and Workers. */2+export async function sha256hex(bytes: Uint8Array): Promise<string> {3+ const digest = await crypto.subtle.digest("SHA-256", bytes as BufferSource);4+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");5+}Binary file.
@@ -0,0 +1,42 @@
1+// Extension → content-type. Resolved at push time by the CLI and stored in the2+// manifest; the Worker never guesses.3+4+const TYPES: Record<string, string> = {5+ html: "text/html; charset=utf-8",6+ htm: "text/html; charset=utf-8",7+ css: "text/css; charset=utf-8",8+ js: "text/javascript; charset=utf-8",9+ mjs: "text/javascript; charset=utf-8",10+ json: "application/json; charset=utf-8",11+ map: "application/json; charset=utf-8",12+ txt: "text/plain; charset=utf-8",13+ md: "text/markdown; charset=utf-8",14+ xml: "application/xml; charset=utf-8",15+ svg: "image/svg+xml",16+ png: "image/png",17+ jpg: "image/jpeg",18+ jpeg: "image/jpeg",19+ gif: "image/gif",20+ webp: "image/webp",21+ avif: "image/avif",22+ ico: "image/x-icon",23+ woff: "font/woff",24+ woff2: "font/woff2",25+ ttf: "font/ttf",26+ otf: "font/otf",27+ mp3: "audio/mpeg",28+ wav: "audio/wav",29+ ogg: "audio/ogg",30+ mp4: "video/mp4",31+ webm: "video/webm",32+ pdf: "application/pdf",33+ wasm: "application/wasm",34+ csv: "text/csv; charset=utf-8",35+ webmanifest: "application/manifest+json",36+};37+38+export function contentTypeFor(path: string): string {39+ const dot = path.lastIndexOf(".");40+ const ext = dot === -1 ? "" : path.slice(dot + 1).toLowerCase();41+ return TYPES[ext] ?? "application/octet-stream";42+}@@ -0,0 +1,104 @@
1+// Wire types shared by the CLI and the Worker.2+// This package must stay runtime-agnostic: Web APIs only, no Deno.* or Workers globals.3+4+export interface ManifestFile {5+ /** sha256 hex of the file bytes */6+ h: string;7+ /** size in bytes */8+ s: number;9+ /** content-type, resolved at push time by the CLI */10+ ct: string;11+}12+13+export interface Manifest {14+ v: 1;15+ /** path (relative, forward slashes, no leading ./) → file entry */16+ files: Record<string, ManifestFile>;17+}18+19+/** "2a3f" | "work:demo" | "demo" (alias in default vault, resolved client-side to vault:alias) */20+export type Target =21+ | { address: number }22+ | { vault: string; alias?: string | null };23+24+export interface PrepareRequest {25+ manifest: Manifest;26+}27+28+export interface PrepareResponse {29+ manifestHash: string;30+ /** blob hashes the server does not have yet */31+ missing: string[];32+}33+34+export interface CommitRequest {35+ manifest: Manifest;36+ target: Target;37+ /** days until expiry; "forever" = no expiry; omitted = server default (14) */38+ ttl?: number | "forever";39+}40+41+export interface CommitResponse {42+ /** 4-hex address, e.g. "2a3f" */43+ address: string;44+ url: string;45+ alias: string | null;46+ manifestHash: string;47+ /** unix seconds, null = permanent */48+ expiresAt: number | null;49+ /** push sequence number for this site */50+ seq: number;51+}52+53+export interface SiteInfo {54+ address: string;55+ vault: string;56+ alias: string | null;57+ manifestHash: string;58+ createdAt: number;59+ updatedAt: number;60+ expiresAt: number | null;61+ url: string;62+ protected: boolean;63+}64+65+export interface PushInfo {66+ seq: number;67+ manifestHash: string;68+ pushedAt: number;69+ note: string | null;70+}71+72+export interface SiteDetail extends SiteInfo {73+ history: PushInfo[];74+}75+76+export interface VaultInfo {77+ slot: number;78+ name: string;79+ createdAt: number;80+ siteCount: number;81+}82+83+export interface StatusResponse {84+ ok: true;85+ version: string;86+ vaults: VaultInfo[];87+ siteCount: number;88+ /** sites expiring within 48h */89+ expiringSoon: number;90+}91+92+export interface RevertRequest {93+ toSeq?: number;94+}95+96+export interface PatchSiteRequest {97+ ttl?: number | "forever";98+ /** set a password (string), clear it (null), or leave unchanged (undefined) */99+ password?: string | null;100+}101+102+export interface ApiError {103+ error: string;104+}@@ -0,0 +1,1526 @@
1+{2+ "name": "nzip-worker",3+ "lockfileVersion": 3,4+ "requires": true,5+ "packages": {6+ "": {7+ "name": "nzip-worker",8+ "devDependencies": {9+ "@cloudflare/workers-types": "^5.20260708.1",10+ "typescript": "^5.8.0",11+ "wrangler": "^4.0.0"12+ }13+ },14+ "node_modules/@cloudflare/kv-asset-handler": {15+ "version": "0.5.0",16+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",17+ "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",18+ "dev": true,19+ "license": "MIT OR Apache-2.0",20+ "engines": {21+ "node": ">=22.0.0"22+ }23+ },24+ "node_modules/@cloudflare/unenv-preset": {25+ "version": "2.16.1",26+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",27+ "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",28+ "dev": true,29+ "license": "MIT OR Apache-2.0",30+ "peerDependencies": {31+ "unenv": "2.0.0-rc.24",32+ "workerd": ">1.20260305.0 <2.0.0-0"33+ },34+ "peerDependenciesMeta": {35+ "workerd": {36+ "optional": true37+ }38+ }39+ },40+ "node_modules/@cloudflare/workerd-darwin-64": {41+ "version": "1.20260708.1",42+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260708.1.tgz",43+ "integrity": "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==",44+ "cpu": [45+ "x64"46+ ],47+ "dev": true,48+ "license": "Apache-2.0",49+ "optional": true,50+ "os": [51+ "darwin"52+ ],53+ "engines": {54+ "node": ">=16"55+ }56+ },57+ "node_modules/@cloudflare/workerd-darwin-arm64": {58+ "version": "1.20260708.1",59+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260708.1.tgz",60+ "integrity": "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==",61+ "cpu": [62+ "arm64"63+ ],64+ "dev": true,65+ "license": "Apache-2.0",66+ "optional": true,67+ "os": [68+ "darwin"69+ ],70+ "engines": {71+ "node": ">=16"72+ }73+ },74+ "node_modules/@cloudflare/workerd-linux-64": {75+ "version": "1.20260708.1",76+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260708.1.tgz",77+ "integrity": "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==",78+ "cpu": [79+ "x64"80+ ],81+ "dev": true,82+ "license": "Apache-2.0",83+ "optional": true,84+ "os": [85+ "linux"86+ ],87+ "engines": {88+ "node": ">=16"89+ }90+ },91+ "node_modules/@cloudflare/workerd-linux-arm64": {92+ "version": "1.20260708.1",93+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260708.1.tgz",94+ "integrity": "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==",95+ "cpu": [96+ "arm64"97+ ],98+ "dev": true,99+ "license": "Apache-2.0",100+ "optional": true,101+ "os": [102+ "linux"103+ ],104+ "engines": {105+ "node": ">=16"106+ }107+ },108+ "node_modules/@cloudflare/workerd-windows-64": {109+ "version": "1.20260708.1",110+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260708.1.tgz",111+ "integrity": "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==",112+ "cpu": [113+ "x64"114+ ],115+ "dev": true,116+ "license": "Apache-2.0",117+ "optional": true,118+ "os": [119+ "win32"120+ ],121+ "engines": {122+ "node": ">=16"123+ }124+ },125+ "node_modules/@cloudflare/workers-types": {126+ "version": "5.20260708.1",127+ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260708.1.tgz",128+ "integrity": "sha512-FSRyCsxALKmmNnk/2HMkTiX9Iz9yqTiTWX/BJZdffGznMSunXmQgb3mKy9wGBX2BCTLOuRYWL36uh7/3Gm05Eg==",129+ "dev": true,130+ "license": "MIT OR Apache-2.0"131+ },132+ "node_modules/@cspotcode/source-map-support": {133+ "version": "0.8.1",134+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",135+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",136+ "dev": true,137+ "license": "MIT",138+ "dependencies": {139+ "@jridgewell/trace-mapping": "0.3.9"140+ },141+ "engines": {142+ "node": ">=12"143+ }144+ },145+ "node_modules/@emnapi/runtime": {146+ "version": "1.11.2",147+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",148+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",149+ "dev": true,150+ "license": "MIT",151+ "optional": true,152+ "dependencies": {153+ "tslib": "^2.4.0"154+ }155+ },156+ "node_modules/@esbuild/aix-ppc64": {157+ "version": "0.28.1",158+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",159+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",160+ "cpu": [161+ "ppc64"162+ ],163+ "dev": true,164+ "license": "MIT",165+ "optional": true,166+ "os": [167+ "aix"168+ ],169+ "engines": {170+ "node": ">=18"171+ }172+ },173+ "node_modules/@esbuild/android-arm": {174+ "version": "0.28.1",175+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",176+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",177+ "cpu": [178+ "arm"179+ ],180+ "dev": true,181+ "license": "MIT",182+ "optional": true,183+ "os": [184+ "android"185+ ],186+ "engines": {187+ "node": ">=18"188+ }189+ },190+ "node_modules/@esbuild/android-arm64": {191+ "version": "0.28.1",192+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",193+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",194+ "cpu": [195+ "arm64"196+ ],197+ "dev": true,198+ "license": "MIT",199+ "optional": true,200+ "os": [201+ "android"202+ ],203+ "engines": {204+ "node": ">=18"205+ }206+ },207+ "node_modules/@esbuild/android-x64": {208+ "version": "0.28.1",209+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",210+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",211+ "cpu": [212+ "x64"213+ ],214+ "dev": true,215+ "license": "MIT",216+ "optional": true,217+ "os": [218+ "android"219+ ],220+ "engines": {221+ "node": ">=18"222+ }223+ },224+ "node_modules/@esbuild/darwin-arm64": {225+ "version": "0.28.1",226+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",227+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",228+ "cpu": [229+ "arm64"230+ ],231+ "dev": true,232+ "license": "MIT",233+ "optional": true,234+ "os": [235+ "darwin"236+ ],237+ "engines": {238+ "node": ">=18"239+ }240+ },241+ "node_modules/@esbuild/darwin-x64": {242+ "version": "0.28.1",243+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",244+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",245+ "cpu": [246+ "x64"247+ ],248+ "dev": true,249+ "license": "MIT",250+ "optional": true,251+ "os": [252+ "darwin"253+ ],254+ "engines": {255+ "node": ">=18"256+ }257+ },258+ "node_modules/@esbuild/freebsd-arm64": {259+ "version": "0.28.1",260+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",261+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",262+ "cpu": [263+ "arm64"264+ ],265+ "dev": true,266+ "license": "MIT",267+ "optional": true,268+ "os": [269+ "freebsd"270+ ],271+ "engines": {272+ "node": ">=18"273+ }274+ },275+ "node_modules/@esbuild/freebsd-x64": {276+ "version": "0.28.1",277+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",278+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",279+ "cpu": [280+ "x64"281+ ],282+ "dev": true,283+ "license": "MIT",284+ "optional": true,285+ "os": [286+ "freebsd"287+ ],288+ "engines": {289+ "node": ">=18"290+ }291+ },292+ "node_modules/@esbuild/linux-arm": {293+ "version": "0.28.1",294+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",295+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",296+ "cpu": [297+ "arm"298+ ],299+ "dev": true,300+ "license": "MIT",301+ "optional": true,302+ "os": [303+ "linux"304+ ],305+ "engines": {306+ "node": ">=18"307+ }308+ },309+ "node_modules/@esbuild/linux-arm64": {310+ "version": "0.28.1",311+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",312+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",313+ "cpu": [314+ "arm64"315+ ],316+ "dev": true,317+ "license": "MIT",318+ "optional": true,319+ "os": [320+ "linux"321+ ],322+ "engines": {323+ "node": ">=18"324+ }325+ },326+ "node_modules/@esbuild/linux-ia32": {327+ "version": "0.28.1",328+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",329+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",330+ "cpu": [331+ "ia32"332+ ],333+ "dev": true,334+ "license": "MIT",335+ "optional": true,336+ "os": [337+ "linux"338+ ],339+ "engines": {340+ "node": ">=18"341+ }342+ },343+ "node_modules/@esbuild/linux-loong64": {344+ "version": "0.28.1",345+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",346+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",347+ "cpu": [348+ "loong64"349+ ],350+ "dev": true,351+ "license": "MIT",352+ "optional": true,353+ "os": [354+ "linux"355+ ],356+ "engines": {357+ "node": ">=18"358+ }359+ },360+ "node_modules/@esbuild/linux-mips64el": {361+ "version": "0.28.1",362+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",363+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",364+ "cpu": [365+ "mips64el"366+ ],367+ "dev": true,368+ "license": "MIT",369+ "optional": true,370+ "os": [371+ "linux"372+ ],373+ "engines": {374+ "node": ">=18"375+ }376+ },377+ "node_modules/@esbuild/linux-ppc64": {378+ "version": "0.28.1",379+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",380+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",381+ "cpu": [382+ "ppc64"383+ ],384+ "dev": true,385+ "license": "MIT",386+ "optional": true,387+ "os": [388+ "linux"389+ ],390+ "engines": {391+ "node": ">=18"392+ }393+ },394+ "node_modules/@esbuild/linux-riscv64": {395+ "version": "0.28.1",396+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",397+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",398+ "cpu": [399+ "riscv64"400+ ],401+ "dev": true,402+ "license": "MIT",403+ "optional": true,404+ "os": [405+ "linux"406+ ],407+ "engines": {408+ "node": ">=18"409+ }410+ },411+ "node_modules/@esbuild/linux-s390x": {412+ "version": "0.28.1",413+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",414+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",415+ "cpu": [416+ "s390x"417+ ],418+ "dev": true,419+ "license": "MIT",420+ "optional": true,421+ "os": [422+ "linux"423+ ],424+ "engines": {425+ "node": ">=18"426+ }427+ },428+ "node_modules/@esbuild/linux-x64": {429+ "version": "0.28.1",430+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",431+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",432+ "cpu": [433+ "x64"434+ ],435+ "dev": true,436+ "license": "MIT",437+ "optional": true,438+ "os": [439+ "linux"440+ ],441+ "engines": {442+ "node": ">=18"443+ }444+ },445+ "node_modules/@esbuild/netbsd-arm64": {446+ "version": "0.28.1",447+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",448+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",449+ "cpu": [450+ "arm64"451+ ],452+ "dev": true,453+ "license": "MIT",454+ "optional": true,455+ "os": [456+ "netbsd"457+ ],458+ "engines": {459+ "node": ">=18"460+ }461+ },462+ "node_modules/@esbuild/netbsd-x64": {463+ "version": "0.28.1",464+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",465+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",466+ "cpu": [467+ "x64"468+ ],469+ "dev": true,470+ "license": "MIT",471+ "optional": true,472+ "os": [473+ "netbsd"474+ ],475+ "engines": {476+ "node": ">=18"477+ }478+ },479+ "node_modules/@esbuild/openbsd-arm64": {480+ "version": "0.28.1",481+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",482+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",483+ "cpu": [484+ "arm64"485+ ],486+ "dev": true,487+ "license": "MIT",488+ "optional": true,489+ "os": [490+ "openbsd"491+ ],492+ "engines": {493+ "node": ">=18"494+ }495+ },496+ "node_modules/@esbuild/openbsd-x64": {497+ "version": "0.28.1",498+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",499+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",500+ "cpu": [501+ "x64"502+ ],503+ "dev": true,504+ "license": "MIT",505+ "optional": true,506+ "os": [507+ "openbsd"508+ ],509+ "engines": {510+ "node": ">=18"511+ }512+ },513+ "node_modules/@esbuild/openharmony-arm64": {514+ "version": "0.28.1",515+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",516+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",517+ "cpu": [518+ "arm64"519+ ],520+ "dev": true,521+ "license": "MIT",522+ "optional": true,523+ "os": [524+ "openharmony"525+ ],526+ "engines": {527+ "node": ">=18"528+ }529+ },530+ "node_modules/@esbuild/sunos-x64": {531+ "version": "0.28.1",532+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",533+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",534+ "cpu": [535+ "x64"536+ ],537+ "dev": true,538+ "license": "MIT",539+ "optional": true,540+ "os": [541+ "sunos"542+ ],543+ "engines": {544+ "node": ">=18"545+ }546+ },547+ "node_modules/@esbuild/win32-arm64": {548+ "version": "0.28.1",549+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",550+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",551+ "cpu": [552+ "arm64"553+ ],554+ "dev": true,555+ "license": "MIT",556+ "optional": true,557+ "os": [558+ "win32"559+ ],560+ "engines": {561+ "node": ">=18"562+ }563+ },564+ "node_modules/@esbuild/win32-ia32": {565+ "version": "0.28.1",566+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",567+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",568+ "cpu": [569+ "ia32"570+ ],571+ "dev": true,572+ "license": "MIT",573+ "optional": true,574+ "os": [575+ "win32"576+ ],577+ "engines": {578+ "node": ">=18"579+ }580+ },581+ "node_modules/@esbuild/win32-x64": {582+ "version": "0.28.1",583+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",584+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",585+ "cpu": [586+ "x64"587+ ],588+ "dev": true,589+ "license": "MIT",590+ "optional": true,591+ "os": [592+ "win32"593+ ],594+ "engines": {595+ "node": ">=18"596+ }597+ },598+ "node_modules/@img/colour": {599+ "version": "1.1.0",600+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",601+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",602+ "dev": true,603+ "license": "MIT",604+ "engines": {605+ "node": ">=18"606+ }607+ },608+ "node_modules/@img/sharp-darwin-arm64": {609+ "version": "0.34.5",610+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",611+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",612+ "cpu": [613+ "arm64"614+ ],615+ "dev": true,616+ "license": "Apache-2.0",617+ "optional": true,618+ "os": [619+ "darwin"620+ ],621+ "engines": {622+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"623+ },624+ "funding": {625+ "url": "https://opencollective.com/libvips"626+ },627+ "optionalDependencies": {628+ "@img/sharp-libvips-darwin-arm64": "1.2.4"629+ }630+ },631+ "node_modules/@img/sharp-darwin-x64": {632+ "version": "0.34.5",633+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",634+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",635+ "cpu": [636+ "x64"637+ ],638+ "dev": true,639+ "license": "Apache-2.0",640+ "optional": true,641+ "os": [642+ "darwin"643+ ],644+ "engines": {645+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"646+ },647+ "funding": {648+ "url": "https://opencollective.com/libvips"649+ },650+ "optionalDependencies": {651+ "@img/sharp-libvips-darwin-x64": "1.2.4"652+ }653+ },654+ "node_modules/@img/sharp-libvips-darwin-arm64": {655+ "version": "1.2.4",656+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",657+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",658+ "cpu": [659+ "arm64"660+ ],661+ "dev": true,662+ "license": "LGPL-3.0-or-later",663+ "optional": true,664+ "os": [665+ "darwin"666+ ],667+ "funding": {668+ "url": "https://opencollective.com/libvips"669+ }670+ },671+ "node_modules/@img/sharp-libvips-darwin-x64": {672+ "version": "1.2.4",673+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",674+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",675+ "cpu": [676+ "x64"677+ ],678+ "dev": true,679+ "license": "LGPL-3.0-or-later",680+ "optional": true,681+ "os": [682+ "darwin"683+ ],684+ "funding": {685+ "url": "https://opencollective.com/libvips"686+ }687+ },688+ "node_modules/@img/sharp-libvips-linux-arm": {689+ "version": "1.2.4",690+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",691+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",692+ "cpu": [693+ "arm"694+ ],695+ "dev": true,696+ "license": "LGPL-3.0-or-later",697+ "optional": true,698+ "os": [699+ "linux"700+ ],701+ "funding": {702+ "url": "https://opencollective.com/libvips"703+ }704+ },705+ "node_modules/@img/sharp-libvips-linux-arm64": {706+ "version": "1.2.4",707+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",708+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",709+ "cpu": [710+ "arm64"711+ ],712+ "dev": true,713+ "license": "LGPL-3.0-or-later",714+ "optional": true,715+ "os": [716+ "linux"717+ ],718+ "funding": {719+ "url": "https://opencollective.com/libvips"720+ }721+ },722+ "node_modules/@img/sharp-libvips-linux-ppc64": {723+ "version": "1.2.4",724+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",725+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",726+ "cpu": [727+ "ppc64"728+ ],729+ "dev": true,730+ "license": "LGPL-3.0-or-later",731+ "optional": true,732+ "os": [733+ "linux"734+ ],735+ "funding": {736+ "url": "https://opencollective.com/libvips"737+ }738+ },739+ "node_modules/@img/sharp-libvips-linux-riscv64": {740+ "version": "1.2.4",741+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",742+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",743+ "cpu": [744+ "riscv64"745+ ],746+ "dev": true,747+ "license": "LGPL-3.0-or-later",748+ "optional": true,749+ "os": [750+ "linux"751+ ],752+ "funding": {753+ "url": "https://opencollective.com/libvips"754+ }755+ },756+ "node_modules/@img/sharp-libvips-linux-s390x": {757+ "version": "1.2.4",758+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",759+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",760+ "cpu": [761+ "s390x"762+ ],763+ "dev": true,764+ "license": "LGPL-3.0-or-later",765+ "optional": true,766+ "os": [767+ "linux"768+ ],769+ "funding": {770+ "url": "https://opencollective.com/libvips"771+ }772+ },773+ "node_modules/@img/sharp-libvips-linux-x64": {774+ "version": "1.2.4",775+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",776+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",777+ "cpu": [778+ "x64"779+ ],780+ "dev": true,781+ "license": "LGPL-3.0-or-later",782+ "optional": true,783+ "os": [784+ "linux"785+ ],786+ "funding": {787+ "url": "https://opencollective.com/libvips"788+ }789+ },790+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {791+ "version": "1.2.4",792+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",793+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",794+ "cpu": [795+ "arm64"796+ ],797+ "dev": true,798+ "license": "LGPL-3.0-or-later",799+ "optional": true,800+ "os": [801+ "linux"802+ ],803+ "funding": {804+ "url": "https://opencollective.com/libvips"805+ }806+ },807+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {808+ "version": "1.2.4",809+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",810+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",811+ "cpu": [812+ "x64"813+ ],814+ "dev": true,815+ "license": "LGPL-3.0-or-later",816+ "optional": true,817+ "os": [818+ "linux"819+ ],820+ "funding": {821+ "url": "https://opencollective.com/libvips"822+ }823+ },824+ "node_modules/@img/sharp-linux-arm": {825+ "version": "0.34.5",826+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",827+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",828+ "cpu": [829+ "arm"830+ ],831+ "dev": true,832+ "license": "Apache-2.0",833+ "optional": true,834+ "os": [835+ "linux"836+ ],837+ "engines": {838+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"839+ },840+ "funding": {841+ "url": "https://opencollective.com/libvips"842+ },843+ "optionalDependencies": {844+ "@img/sharp-libvips-linux-arm": "1.2.4"845+ }846+ },847+ "node_modules/@img/sharp-linux-arm64": {848+ "version": "0.34.5",849+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",850+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",851+ "cpu": [852+ "arm64"853+ ],854+ "dev": true,855+ "license": "Apache-2.0",856+ "optional": true,857+ "os": [858+ "linux"859+ ],860+ "engines": {861+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"862+ },863+ "funding": {864+ "url": "https://opencollective.com/libvips"865+ },866+ "optionalDependencies": {867+ "@img/sharp-libvips-linux-arm64": "1.2.4"868+ }869+ },870+ "node_modules/@img/sharp-linux-ppc64": {871+ "version": "0.34.5",872+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",873+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",874+ "cpu": [875+ "ppc64"876+ ],877+ "dev": true,878+ "license": "Apache-2.0",879+ "optional": true,880+ "os": [881+ "linux"882+ ],883+ "engines": {884+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"885+ },886+ "funding": {887+ "url": "https://opencollective.com/libvips"888+ },889+ "optionalDependencies": {890+ "@img/sharp-libvips-linux-ppc64": "1.2.4"891+ }892+ },893+ "node_modules/@img/sharp-linux-riscv64": {894+ "version": "0.34.5",895+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",896+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",897+ "cpu": [898+ "riscv64"899+ ],900+ "dev": true,901+ "license": "Apache-2.0",902+ "optional": true,903+ "os": [904+ "linux"905+ ],906+ "engines": {907+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"908+ },909+ "funding": {910+ "url": "https://opencollective.com/libvips"911+ },912+ "optionalDependencies": {913+ "@img/sharp-libvips-linux-riscv64": "1.2.4"914+ }915+ },916+ "node_modules/@img/sharp-linux-s390x": {917+ "version": "0.34.5",918+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",919+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",920+ "cpu": [921+ "s390x"922+ ],923+ "dev": true,924+ "license": "Apache-2.0",925+ "optional": true,926+ "os": [927+ "linux"928+ ],929+ "engines": {930+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"931+ },932+ "funding": {933+ "url": "https://opencollective.com/libvips"934+ },935+ "optionalDependencies": {936+ "@img/sharp-libvips-linux-s390x": "1.2.4"937+ }938+ },939+ "node_modules/@img/sharp-linux-x64": {940+ "version": "0.34.5",941+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",942+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",943+ "cpu": [944+ "x64"945+ ],946+ "dev": true,947+ "license": "Apache-2.0",948+ "optional": true,949+ "os": [950+ "linux"951+ ],952+ "engines": {953+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"954+ },955+ "funding": {956+ "url": "https://opencollective.com/libvips"957+ },958+ "optionalDependencies": {959+ "@img/sharp-libvips-linux-x64": "1.2.4"960+ }961+ },962+ "node_modules/@img/sharp-linuxmusl-arm64": {963+ "version": "0.34.5",964+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",965+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",966+ "cpu": [967+ "arm64"968+ ],969+ "dev": true,970+ "license": "Apache-2.0",971+ "optional": true,972+ "os": [973+ "linux"974+ ],975+ "engines": {976+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"977+ },978+ "funding": {979+ "url": "https://opencollective.com/libvips"980+ },981+ "optionalDependencies": {982+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"983+ }984+ },985+ "node_modules/@img/sharp-linuxmusl-x64": {986+ "version": "0.34.5",987+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",988+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",989+ "cpu": [990+ "x64"991+ ],992+ "dev": true,993+ "license": "Apache-2.0",994+ "optional": true,995+ "os": [996+ "linux"997+ ],998+ "engines": {999+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1000+ },1001+ "funding": {1002+ "url": "https://opencollective.com/libvips"1003+ },1004+ "optionalDependencies": {1005+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"1006+ }1007+ },1008+ "node_modules/@img/sharp-wasm32": {1009+ "version": "0.34.5",1010+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",1011+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",1012+ "cpu": [1013+ "wasm32"1014+ ],1015+ "dev": true,1016+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",1017+ "optional": true,1018+ "dependencies": {1019+ "@emnapi/runtime": "^1.7.0"1020+ },1021+ "engines": {1022+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1023+ },1024+ "funding": {1025+ "url": "https://opencollective.com/libvips"1026+ }1027+ },1028+ "node_modules/@img/sharp-win32-arm64": {1029+ "version": "0.34.5",1030+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",1031+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",1032+ "cpu": [1033+ "arm64"1034+ ],1035+ "dev": true,1036+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1037+ "optional": true,1038+ "os": [1039+ "win32"1040+ ],1041+ "engines": {1042+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1043+ },1044+ "funding": {1045+ "url": "https://opencollective.com/libvips"1046+ }1047+ },1048+ "node_modules/@img/sharp-win32-ia32": {1049+ "version": "0.34.5",1050+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",1051+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",1052+ "cpu": [1053+ "ia32"1054+ ],1055+ "dev": true,1056+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1057+ "optional": true,1058+ "os": [1059+ "win32"1060+ ],1061+ "engines": {1062+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1063+ },1064+ "funding": {1065+ "url": "https://opencollective.com/libvips"1066+ }1067+ },1068+ "node_modules/@img/sharp-win32-x64": {1069+ "version": "0.34.5",1070+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",1071+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",1072+ "cpu": [1073+ "x64"1074+ ],1075+ "dev": true,1076+ "license": "Apache-2.0 AND LGPL-3.0-or-later",1077+ "optional": true,1078+ "os": [1079+ "win32"1080+ ],1081+ "engines": {1082+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1083+ },1084+ "funding": {1085+ "url": "https://opencollective.com/libvips"1086+ }1087+ },1088+ "node_modules/@jridgewell/resolve-uri": {1089+ "version": "3.1.2",1090+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",1091+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",1092+ "dev": true,1093+ "license": "MIT",1094+ "engines": {1095+ "node": ">=6.0.0"1096+ }1097+ },1098+ "node_modules/@jridgewell/sourcemap-codec": {1099+ "version": "1.5.5",1100+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",1101+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",1102+ "dev": true,1103+ "license": "MIT"1104+ },1105+ "node_modules/@jridgewell/trace-mapping": {1106+ "version": "0.3.9",1107+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",1108+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",1109+ "dev": true,1110+ "license": "MIT",1111+ "dependencies": {1112+ "@jridgewell/resolve-uri": "^3.0.3",1113+ "@jridgewell/sourcemap-codec": "^1.4.10"1114+ }1115+ },1116+ "node_modules/@poppinss/colors": {1117+ "version": "4.1.6",1118+ "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",1119+ "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",1120+ "dev": true,1121+ "license": "MIT",1122+ "dependencies": {1123+ "kleur": "^4.1.5"1124+ }1125+ },1126+ "node_modules/@poppinss/dumper": {1127+ "version": "0.6.5",1128+ "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",1129+ "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",1130+ "dev": true,1131+ "license": "MIT",1132+ "dependencies": {1133+ "@poppinss/colors": "^4.1.5",1134+ "@sindresorhus/is": "^7.0.2",1135+ "supports-color": "^10.0.0"1136+ }1137+ },1138+ "node_modules/@poppinss/exception": {1139+ "version": "1.2.3",1140+ "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",1141+ "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",1142+ "dev": true,1143+ "license": "MIT"1144+ },1145+ "node_modules/@sindresorhus/is": {1146+ "version": "7.2.0",1147+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",1148+ "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",1149+ "dev": true,1150+ "license": "MIT",1151+ "engines": {1152+ "node": ">=18"1153+ },1154+ "funding": {1155+ "url": "https://github.com/sindresorhus/is?sponsor=1"1156+ }1157+ },1158+ "node_modules/@speed-highlight/core": {1159+ "version": "1.2.17",1160+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz",1161+ "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==",1162+ "dev": true,1163+ "license": "CC0-1.0"1164+ },1165+ "node_modules/blake3-wasm": {1166+ "version": "2.1.5",1167+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",1168+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",1169+ "dev": true,1170+ "license": "MIT"1171+ },1172+ "node_modules/cookie": {1173+ "version": "1.1.1",1174+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",1175+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",1176+ "dev": true,1177+ "license": "MIT",1178+ "engines": {1179+ "node": ">=18"1180+ },1181+ "funding": {1182+ "type": "opencollective",1183+ "url": "https://opencollective.com/express"1184+ }1185+ },1186+ "node_modules/detect-libc": {1187+ "version": "2.1.2",1188+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",1189+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",1190+ "dev": true,1191+ "license": "Apache-2.0",1192+ "engines": {1193+ "node": ">=8"1194+ }1195+ },1196+ "node_modules/error-stack-parser-es": {1197+ "version": "1.0.5",1198+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",1199+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",1200+ "dev": true,1201+ "license": "MIT",1202+ "funding": {1203+ "url": "https://github.com/sponsors/antfu"1204+ }1205+ },1206+ "node_modules/esbuild": {1207+ "version": "0.28.1",1208+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",1209+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",1210+ "dev": true,1211+ "hasInstallScript": true,1212+ "license": "MIT",1213+ "bin": {1214+ "esbuild": "bin/esbuild"1215+ },1216+ "engines": {1217+ "node": ">=18"1218+ },1219+ "optionalDependencies": {1220+ "@esbuild/aix-ppc64": "0.28.1",1221+ "@esbuild/android-arm": "0.28.1",1222+ "@esbuild/android-arm64": "0.28.1",1223+ "@esbuild/android-x64": "0.28.1",1224+ "@esbuild/darwin-arm64": "0.28.1",1225+ "@esbuild/darwin-x64": "0.28.1",1226+ "@esbuild/freebsd-arm64": "0.28.1",1227+ "@esbuild/freebsd-x64": "0.28.1",1228+ "@esbuild/linux-arm": "0.28.1",1229+ "@esbuild/linux-arm64": "0.28.1",1230+ "@esbuild/linux-ia32": "0.28.1",1231+ "@esbuild/linux-loong64": "0.28.1",1232+ "@esbuild/linux-mips64el": "0.28.1",1233+ "@esbuild/linux-ppc64": "0.28.1",1234+ "@esbuild/linux-riscv64": "0.28.1",1235+ "@esbuild/linux-s390x": "0.28.1",1236+ "@esbuild/linux-x64": "0.28.1",1237+ "@esbuild/netbsd-arm64": "0.28.1",1238+ "@esbuild/netbsd-x64": "0.28.1",1239+ "@esbuild/openbsd-arm64": "0.28.1",1240+ "@esbuild/openbsd-x64": "0.28.1",1241+ "@esbuild/openharmony-arm64": "0.28.1",1242+ "@esbuild/sunos-x64": "0.28.1",1243+ "@esbuild/win32-arm64": "0.28.1",1244+ "@esbuild/win32-ia32": "0.28.1",1245+ "@esbuild/win32-x64": "0.28.1"1246+ }1247+ },1248+ "node_modules/fsevents": {1249+ "version": "2.3.3",1250+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",1251+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",1252+ "dev": true,1253+ "hasInstallScript": true,1254+ "license": "MIT",1255+ "optional": true,1256+ "os": [1257+ "darwin"1258+ ],1259+ "engines": {1260+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"1261+ }1262+ },1263+ "node_modules/kleur": {1264+ "version": "4.1.5",1265+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",1266+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",1267+ "dev": true,1268+ "license": "MIT",1269+ "engines": {1270+ "node": ">=6"1271+ }1272+ },1273+ "node_modules/miniflare": {1274+ "version": "4.20260708.0",1275+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.0.tgz",1276+ "integrity": "sha512-ut32kKKTyZIoXhbtteDAMOwTVx9aCeehMeKb1xmJs4RLdBrF5flSZD1MOpWqmJycIV7YBHjcaZcVHLMr/g5RZA==",1277+ "dev": true,1278+ "license": "MIT",1279+ "dependencies": {1280+ "@cspotcode/source-map-support": "0.8.1",1281+ "sharp": "0.34.5",1282+ "undici": "7.28.0",1283+ "workerd": "1.20260708.1",1284+ "ws": "8.21.0",1285+ "youch": "4.1.0-beta.10"1286+ },1287+ "bin": {1288+ "miniflare": "bootstrap.js"1289+ },1290+ "engines": {1291+ "node": ">=22.0.0"1292+ }1293+ },1294+ "node_modules/path-to-regexp": {1295+ "version": "6.3.0",1296+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",1297+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",1298+ "dev": true,1299+ "license": "MIT"1300+ },1301+ "node_modules/pathe": {1302+ "version": "2.0.3",1303+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",1304+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",1305+ "dev": true,1306+ "license": "MIT"1307+ },1308+ "node_modules/semver": {1309+ "version": "7.8.5",1310+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",1311+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",1312+ "dev": true,1313+ "license": "ISC",1314+ "bin": {1315+ "semver": "bin/semver.js"1316+ },1317+ "engines": {1318+ "node": ">=10"1319+ }1320+ },1321+ "node_modules/sharp": {1322+ "version": "0.34.5",1323+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",1324+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",1325+ "dev": true,1326+ "hasInstallScript": true,1327+ "license": "Apache-2.0",1328+ "dependencies": {1329+ "@img/colour": "^1.0.0",1330+ "detect-libc": "^2.1.2",1331+ "semver": "^7.7.3"1332+ },1333+ "engines": {1334+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"1335+ },1336+ "funding": {1337+ "url": "https://opencollective.com/libvips"1338+ },1339+ "optionalDependencies": {1340+ "@img/sharp-darwin-arm64": "0.34.5",1341+ "@img/sharp-darwin-x64": "0.34.5",1342+ "@img/sharp-libvips-darwin-arm64": "1.2.4",1343+ "@img/sharp-libvips-darwin-x64": "1.2.4",1344+ "@img/sharp-libvips-linux-arm": "1.2.4",1345+ "@img/sharp-libvips-linux-arm64": "1.2.4",1346+ "@img/sharp-libvips-linux-ppc64": "1.2.4",1347+ "@img/sharp-libvips-linux-riscv64": "1.2.4",1348+ "@img/sharp-libvips-linux-s390x": "1.2.4",1349+ "@img/sharp-libvips-linux-x64": "1.2.4",1350+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",1351+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",1352+ "@img/sharp-linux-arm": "0.34.5",1353+ "@img/sharp-linux-arm64": "0.34.5",1354+ "@img/sharp-linux-ppc64": "0.34.5",1355+ "@img/sharp-linux-riscv64": "0.34.5",1356+ "@img/sharp-linux-s390x": "0.34.5",1357+ "@img/sharp-linux-x64": "0.34.5",1358+ "@img/sharp-linuxmusl-arm64": "0.34.5",1359+ "@img/sharp-linuxmusl-x64": "0.34.5",1360+ "@img/sharp-wasm32": "0.34.5",1361+ "@img/sharp-win32-arm64": "0.34.5",1362+ "@img/sharp-win32-ia32": "0.34.5",1363+ "@img/sharp-win32-x64": "0.34.5"1364+ }1365+ },1366+ "node_modules/supports-color": {1367+ "version": "10.2.2",1368+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",1369+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",1370+ "dev": true,1371+ "license": "MIT",1372+ "engines": {1373+ "node": ">=18"1374+ },1375+ "funding": {1376+ "url": "https://github.com/chalk/supports-color?sponsor=1"1377+ }1378+ },1379+ "node_modules/tslib": {1380+ "version": "2.8.1",1381+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",1382+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",1383+ "dev": true,1384+ "license": "0BSD",1385+ "optional": true1386+ },1387+ "node_modules/typescript": {1388+ "version": "5.9.3",1389+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",1390+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",1391+ "dev": true,1392+ "license": "Apache-2.0",1393+ "bin": {1394+ "tsc": "bin/tsc",1395+ "tsserver": "bin/tsserver"1396+ },1397+ "engines": {1398+ "node": ">=14.17"1399+ }1400+ },1401+ "node_modules/undici": {1402+ "version": "7.28.0",1403+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",1404+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",1405+ "dev": true,1406+ "license": "MIT",1407+ "engines": {1408+ "node": ">=20.18.1"1409+ }1410+ },1411+ "node_modules/unenv": {1412+ "version": "2.0.0-rc.24",1413+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",1414+ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",1415+ "dev": true,1416+ "license": "MIT",1417+ "dependencies": {1418+ "pathe": "^2.0.3"1419+ }1420+ },1421+ "node_modules/workerd": {1422+ "version": "1.20260708.1",1423+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260708.1.tgz",1424+ "integrity": "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==",1425+ "dev": true,1426+ "hasInstallScript": true,1427+ "license": "Apache-2.0",1428+ "bin": {1429+ "workerd": "bin/workerd"1430+ },1431+ "engines": {1432+ "node": ">=16"1433+ },1434+ "optionalDependencies": {1435+ "@cloudflare/workerd-darwin-64": "1.20260708.1",1436+ "@cloudflare/workerd-darwin-arm64": "1.20260708.1",1437+ "@cloudflare/workerd-linux-64": "1.20260708.1",1438+ "@cloudflare/workerd-linux-arm64": "1.20260708.1",1439+ "@cloudflare/workerd-windows-64": "1.20260708.1"1440+ }1441+ },1442+ "node_modules/wrangler": {1443+ "version": "4.109.0",1444+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.109.0.tgz",1445+ "integrity": "sha512-Ev6iIBNnRwvYeJv8B7377YsnY+PxEznHmz3r33cWgP+6A5HNqqqJhChBaVUWxxs0uQPKuYBy8tqrfyd7o8WmBg==",1446+ "dev": true,1447+ "license": "MIT OR Apache-2.0",1448+ "dependencies": {1449+ "@cloudflare/kv-asset-handler": "0.5.0",1450+ "@cloudflare/unenv-preset": "2.16.1",1451+ "blake3-wasm": "2.1.5",1452+ "esbuild": "0.28.1",1453+ "miniflare": "4.20260708.0",1454+ "path-to-regexp": "6.3.0",1455+ "unenv": "2.0.0-rc.24",1456+ "workerd": "1.20260708.1"1457+ },1458+ "bin": {1459+ "cf-wrangler": "bin/cf-wrangler.js",1460+ "wrangler": "bin/wrangler.js",1461+ "wrangler2": "bin/wrangler.js"1462+ },1463+ "engines": {1464+ "node": ">=22.0.0"1465+ },1466+ "optionalDependencies": {1467+ "fsevents": "2.3.3"1468+ },1469+ "peerDependencies": {1470+ "@cloudflare/workers-types": "^5.20260708.1"1471+ },1472+ "peerDependenciesMeta": {1473+ "@cloudflare/workers-types": {1474+ "optional": true1475+ }1476+ }1477+ },1478+ "node_modules/ws": {1479+ "version": "8.21.0",1480+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",1481+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",1482+ "dev": true,1483+ "license": "MIT",1484+ "engines": {1485+ "node": ">=10.0.0"1486+ },1487+ "peerDependencies": {1488+ "bufferutil": "^4.0.1",1489+ "utf-8-validate": ">=5.0.2"1490+ },1491+ "peerDependenciesMeta": {1492+ "bufferutil": {1493+ "optional": true1494+ },1495+ "utf-8-validate": {1496+ "optional": true1497+ }1498+ }1499+ },1500+ "node_modules/youch": {1501+ "version": "4.1.0-beta.10",1502+ "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",1503+ "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",1504+ "dev": true,1505+ "license": "MIT",1506+ "dependencies": {1507+ "@poppinss/colors": "^4.1.5",1508+ "@poppinss/dumper": "^0.6.4",1509+ "@speed-highlight/core": "^1.2.7",1510+ "cookie": "^1.0.2",1511+ "youch-core": "^0.3.3"1512+ }1513+ },1514+ "node_modules/youch-core": {1515+ "version": "0.3.3",1516+ "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",1517+ "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",1518+ "dev": true,1519+ "license": "MIT",1520+ "dependencies": {1521+ "@poppinss/exception": "^1.2.2",1522+ "error-stack-parser-es": "^1.0.5"1523+ }1524+ }1525+ }1526+}@@ -0,0 +1,16 @@
1+{2+ "name": "nzip-worker",3+ "private": true,4+ "scripts": {5+ "dev": "wrangler dev",6+ "dev:local": "wrangler dev --config wrangler.local.jsonc",7+ "deploy": "wrangler deploy",8+ "deploy:local": "wrangler deploy --config wrangler.local.jsonc",9+ "check": "tsc --noEmit"10+ },11+ "devDependencies": {12+ "@cloudflare/workers-types": "^5.20260708.1",13+ "typescript": "^5.8.0",14+ "wrangler": "^4.0.0"15+ }16+}@@ -0,0 +1,30 @@
1+-- nzip D1 schema. Idempotent; apply with:2+-- wrangler d1 execute nzip --remote --file schema.sql3+4+CREATE TABLE IF NOT EXISTS vaults (5+ slot INTEGER PRIMARY KEY CHECK (slot BETWEEN 0 AND 15),6+ name TEXT NOT NULL UNIQUE,7+ created_at INTEGER NOT NULL8+);9+10+CREATE TABLE IF NOT EXISTS sites (11+ address INTEGER PRIMARY KEY CHECK (address BETWEEN 0 AND 65535),12+ vault_slot INTEGER NOT NULL REFERENCES vaults(slot),13+ alias TEXT,14+ current_manifest TEXT NOT NULL,15+ created_at INTEGER NOT NULL,16+ updated_at INTEGER NOT NULL,17+ expires_at INTEGER, -- unix seconds; NULL = permanent18+ password_hash TEXT, -- reserved for password shares; NULL = public19+ UNIQUE (vault_slot, alias)20+);21+CREATE INDEX IF NOT EXISTS idx_sites_expiry ON sites(expires_at) WHERE expires_at IS NOT NULL;22+23+CREATE TABLE IF NOT EXISTS pushes (24+ address INTEGER NOT NULL REFERENCES sites(address) ON DELETE CASCADE,25+ seq INTEGER NOT NULL,26+ manifest_hash TEXT NOT NULL,27+ pushed_at INTEGER NOT NULL,28+ note TEXT,29+ PRIMARY KEY (address, seq)30+);@@ -0,0 +1,74 @@
1+# One-time Cloudflare setup2+3+Everything here happens once. Day-to-day content pushes never touch wrangler.4+5+```sh6+cd worker7+8+# 1. auth9+npx wrangler login10+11+# 2. private Wrangler config12+cp wrangler.local.example.jsonc wrangler.local.jsonc13+# Edit wrangler.local.jsonc now:14+# - routes[0].pattern: your hostname, such as share.example.com15+# - vars.PUBLIC_BASE: https://<that hostname>16+# - d1_databases[0].database_id: filled in after D1 creation17+18+# 3. storage19+npx wrangler r2 bucket create nzip-content20+npx wrangler d1 create nzip21+# -> paste the printed database_id into wrangler.local.jsonc22+23+# 4. schema24+npx wrangler d1 execute nzip --remote --file schema.sql25+26+# 5. owner token27+openssl rand -hex 32 # keep this — it's your CLI token28+npx wrangler secret put NZIP_TOKEN --config wrangler.local.jsonc29+30+# 6. first deploy31+npx wrangler deploy --config wrangler.local.jsonc32+```33+34+`routes[0].pattern` and `vars.PUBLIC_BASE` are required user-provided values. `PUBLIC_BASE` is the35+origin printed in share URLs and the server URL passed to `nzip auth`.36+37+> **Cron gotcha:** deploying the `triggers` block fails with a 403 (API error `10063`) until the38+> account has a workers.dev subdomain registered — even if the Worker only serves a custom domain.39+> Open the Workers dashboard once to auto-create it, or `PUT /accounts/{id}/workers/subdomain` with40+> `{"subdomain":"<name>"}`.41+42+## Required custom domain43+44+1. Add your zone to your Cloudflare account (registrar -> Cloudflare nameservers).45+2. In `wrangler.local.jsonc`, set `routes[0].pattern`, `vars.PUBLIC_BASE`, and your D146+ `database_id`.47+3. `npx wrangler deploy --config wrangler.local.jsonc` - the custom-domain route provisions DNS +48+ cert automatically.49+50+## CLI51+52+```sh53+# from the repo root54+deno install -g -f -n nzip \55+ --allow-net --allow-read --allow-write --allow-env \56+ cli/main.ts57+58+nzip auth --server https://share.example.com --token <token-from-step-5>59+nzip vault add personal # slot 0x060+nzip vault add work # slot 0x161+nzip push ./docs personal:plan --ttl forever62+```63+64+## Local development65+66+```sh67+cd worker68+npx wrangler d1 execute nzip --local --file schema.sql # once69+npx wrangler dev # local R2/D1, token from .dev.vars70+nzip auth --server http://localhost:8787 --token dev-token-local-only71+```72+73+Test the GC cron locally: run `wrangler dev --test-scheduled`, then74+`curl "http://localhost:8787/__scheduled?cron=0+4+*+*+*"`.@@ -0,0 +1,303 @@
1+import {2+ canonicalManifestBytes,3+ formatAddress,4+ isValidName,5+ manifestHash,6+ parseTarget,7+ sha256hex,8+ VAULT_SLOTS,9+ vaultSlotOf,10+} from "../../shared/mod.ts";11+import type {12+ CommitRequest,13+ CommitResponse,14+ Manifest,15+ PatchSiteRequest,16+ PrepareRequest,17+ PrepareResponse,18+ RevertRequest,19+ StatusResponse,20+ Target,21+} from "../../shared/mod.ts";22+import {23+ allocateAddress,24+ commitSite,25+ createVault,26+ getSiteByAddress,27+ getVaultByName,28+ getVaultBySlot,29+ listSites,30+ listVaults,31+ resolveTarget,32+ siteHistory,33+ siteRowToDetail,34+ siteRowToInfo,35+ type SiteRow,36+} from "./db.ts";37+import { type Env, err, json, siteUrl } from "./env.ts";38+import { hashPassword } from "./password.ts";39+40+const HEX64 = /^[0-9a-f]{64}$/;41+const DEFAULT_TTL_DAYS = 14;42+const MAX_BLOB_BYTES = 50 * 1024 * 1024;43+const HEAD_CHUNK = 40; // stay under Worker subrequest limits on big bundles44+45+function ttlToExpiry(ttl: number | "forever" | undefined): number | null {46+ if (ttl === "forever") return null;47+ const days = ttl ?? DEFAULT_TTL_DAYS;48+ if (!Number.isFinite(days) || days <= 0 || days > 3650) {49+ throw new Error("ttl must be 1-3650 days or \"forever\"");50+ }51+ return Math.floor(Date.now() / 1000) + Math.round(days * 86400);52+}53+54+/** Which blob hashes referenced by the manifest are absent from R2? */55+async function missingBlobs(env: Env, manifest: Manifest): Promise<string[]> {56+ const hashes = [...new Set(Object.values(manifest.files).map((f) => f.h))];57+ const missing: string[] = [];58+ for (let i = 0; i < hashes.length; i += HEAD_CHUNK) {59+ const chunk = hashes.slice(i, i + HEAD_CHUNK);60+ const heads = await Promise.all(chunk.map((h) => env.CONTENT.head(`blob/${h}`)));61+ heads.forEach((head, j) => {62+ if (!head) missing.push(chunk[j]);63+ });64+ }65+ return missing;66+}67+68+async function handlePrepare(req: Request, env: Env): Promise<Response> {69+ const body = await req.json<PrepareRequest>();70+ const hash = await manifestHash(body.manifest); // validates too71+ return json<PrepareResponse>({ manifestHash: hash, missing: await missingBlobs(env, body.manifest) });72+}73+74+async function handleBlobPut(req: Request, env: Env, hash: string): Promise<Response> {75+ if (!HEX64.test(hash)) return err("invalid blob hash", 400);76+ const bytes = new Uint8Array(await req.arrayBuffer());77+ if (bytes.length > MAX_BLOB_BYTES) return err("blob too large", 413);78+ const actual = await sha256hex(bytes);79+ if (actual !== hash) return err(`hash mismatch: body is ${actual}`, 400);80+ await env.CONTENT.put(`blob/${hash}`, bytes);81+ return json({ ok: true, hash });82+}83+84+async function handleCommit(req: Request, env: Env): Promise<Response> {85+ const body = await req.json<CommitRequest>();86+ const bytes = canonicalManifestBytes(body.manifest);87+ const hash = await sha256hex(bytes);88+89+ // Guard against crashed uploads: every referenced blob must exist before commit.90+ const missing = await missingBlobs(env, body.manifest);91+ if (missing.length > 0) {92+ return err(`cannot commit: ${missing.length} blobs missing (run prepare again)`, 409);93+ }94+95+ let expiresAt: number | null;96+ try {97+ expiresAt = ttlToExpiry(body.ttl);98+ } catch (e) {99+ return err((e as Error).message, 400);100+ }101+102+ // Resolve target → (address, vaultSlot, alias, isNew)103+ const t = body.target as Target;104+ let address: number;105+ let alias: string | null = null;106+ let isNew: boolean;107+ if ("address" in t) {108+ const existing = await getSiteByAddress(env, t.address);109+ const slot = vaultSlotOf(t.address);110+ if (!(await getVaultBySlot(env, slot))) return err(`vault slot 0x${slot.toString(16)} not registered`, 404);111+ address = t.address;112+ alias = existing?.alias ?? null;113+ isNew = !existing;114+ } else {115+ const vault = await getVaultByName(env, t.vault);116+ if (!vault) return err(`unknown vault: ${t.vault}`, 404);117+ alias = t.alias ?? null;118+ if (alias !== null && !isValidName(alias)) return err(`invalid alias: ${alias}`, 400);119+ const existing = alias === null ? null : await resolveTarget(env, { vault: t.vault, alias });120+ if (existing) {121+ address = existing.address;122+ isNew = false;123+ } else {124+ address = await allocateAddress(env, vault.slot);125+ isNew = true;126+ }127+ }128+129+ if (!(await env.CONTENT.head(`manifest/${hash}`))) {130+ await env.CONTENT.put(`manifest/${hash}`, bytes);131+ }132+ const seq = await commitSite(env, {133+ address,134+ vaultSlot: vaultSlotOf(address),135+ alias,136+ manifestHash: hash,137+ expiresAt,138+ isNew,139+ });140+141+ const addressStr = formatAddress(address);142+ return json<CommitResponse>({143+ address: addressStr,144+ url: siteUrl(env, addressStr),145+ alias,146+ manifestHash: hash,147+ expiresAt,148+ seq,149+ });150+}151+152+/** Resolve a URL path target segment ("2a3f" | "work:demo") to a site row. */153+async function resolvePathTarget(env: Env, segment: string): Promise<SiteRow | null> {154+ const parsed = parseTarget(decodeURIComponent(segment));155+ if (parsed.kind === "address") return await resolveTarget(env, { address: parsed.address });156+ if (parsed.kind === "vaultAlias") {157+ return await resolveTarget(env, { vault: parsed.vault, alias: parsed.alias });158+ }159+ return null; // bare alias needs a default vault — the CLI resolves that client-side160+}161+162+async function handleStatus(env: Env): Promise<Response> {163+ const vaults = await listVaults(env);164+ const sites = await listSites(env);165+ const soon = Math.floor(Date.now() / 1000) + 48 * 3600;166+ return json<StatusResponse>({167+ ok: true,168+ version: "0.1.0",169+ vaults,170+ siteCount: sites.length,171+ expiringSoon: sites.filter((s) => s.expires_at !== null && s.expires_at < soon).length,172+ });173+}174+175+export async function api(req: Request, env: Env, url: URL): Promise<Response> {176+ const parts = url.pathname.split("/").filter(Boolean); // ["api", ...]177+ const method = req.method;178+179+ try {180+ // push protocol181+ if (parts[1] === "push" && parts[2] === "prepare" && method === "POST") {182+ return await handlePrepare(req, env);183+ }184+ if (parts[1] === "push" && parts[2] === "commit" && method === "POST") {185+ return await handleCommit(req, env);186+ }187+ if (parts[1] === "blob" && parts.length === 3 && method === "PUT") {188+ return await handleBlobPut(req, env, parts[2]);189+ }190+191+ // status192+ if (parts[1] === "status" && method === "GET") return await handleStatus(env);193+194+ // vaults195+ if (parts[1] === "vaults" && parts.length === 2) {196+ if (method === "GET") return json(await listVaults(env));197+ if (method === "POST") {198+ const body = await req.json<{ name: string; slot?: number }>();199+ if (!isValidName(body.name)) return err("invalid vault name", 400);200+ if (body.slot !== undefined && (body.slot < 0 || body.slot >= VAULT_SLOTS)) {201+ return err("slot must be 0-15", 400);202+ }203+ try {204+ const v = await createVault(env, body.name, body.slot);205+ return json({ slot: v.slot, name: v.name, createdAt: v.created_at }, 201);206+ } catch (e) {207+ return err((e as Error).message, 409);208+ }209+ }210+ }211+212+ // sites collection213+ if (parts[1] === "sites" && parts.length === 2 && method === "GET") {214+ const vaultName = url.searchParams.get("vault");215+ let slot: number | undefined;216+ if (vaultName) {217+ const vault = await getVaultByName(env, vaultName);218+ if (!vault) return err(`unknown vault: ${vaultName}`, 404);219+ slot = vault.slot;220+ }221+ const rows = await listSites(env, slot);222+ return json(rows.map((r) => siteRowToInfo(env, r, r.vault_name)));223+ }224+225+ // sites/{target}[/revert]226+ if (parts[1] === "sites" && parts.length >= 3) {227+ const site = await resolvePathTarget(env, parts[2]);228+ if (!site) return err("site not found", 404);229+230+ if (parts.length === 3 && method === "GET") {231+ return json(await siteRowToDetail(env, site));232+ }233+ if (parts.length === 3 && method === "DELETE") {234+ await env.DB.prepare("DELETE FROM sites WHERE address = ?").bind(site.address).run();235+ return json({ ok: true, address: formatAddress(site.address) });236+ }237+ if (parts.length === 3 && method === "PATCH") {238+ const body = await req.json<PatchSiteRequest>();239+ const now = Math.floor(Date.now() / 1000);240+ if (body.ttl !== undefined) {241+ const expiresAt = ttlToExpiry(body.ttl);242+ await env.DB.prepare("UPDATE sites SET expires_at = ?, updated_at = ? WHERE address = ?")243+ .bind(expiresAt, now, site.address).run();244+ }245+ if (body.password !== undefined) {246+ if (body.password !== null && body.password.length < 4) {247+ return err("password must be at least 4 characters", 400);248+ }249+ const hash = body.password === null ? null : await hashPassword(body.password);250+ await env.DB.prepare("UPDATE sites SET password_hash = ?, updated_at = ? WHERE address = ?")251+ .bind(hash, now, site.address).run();252+ }253+ const updated = await getSiteByAddress(env, site.address);254+ return json(await siteRowToDetail(env, updated!));255+ }256+ if (parts.length === 4 && parts[3] === "revert" && method === "POST") {257+ return await handleRevert(req, env, site);258+ }259+ }260+261+ return err("not found", 404);262+ } catch (e) {263+ const msg = (e as Error).message ?? "internal error";264+ // Validation errors from shared/ read as client mistakes.265+ const status = /invalid|unsupported|too many|no files|must be/.test(msg) ? 400 : 500;266+ return err(msg, status);267+ }268+}269+270+async function handleRevert(req: Request, env: Env, site: SiteRow): Promise<Response> {271+ const body = await req.json<RevertRequest>().catch(() => ({} as RevertRequest));272+ const history = await siteHistory(env, site.address); // newest first273+274+ let target;275+ if (body.toSeq !== undefined) {276+ target = history.find((h) => h.seq === body.toSeq);277+ if (!target) return err(`no push #${body.toSeq} in history`, 404);278+ } else {279+ // Default: most recent push whose manifest differs from what's live now.280+ target = history.find((h) => h.manifestHash !== site.current_manifest);281+ if (!target) return err("nothing to revert to", 409);282+ }283+284+ const seq = await commitSite(env, {285+ address: site.address,286+ vaultSlot: site.vault_slot,287+ alias: site.alias,288+ manifestHash: target.manifestHash,289+ expiresAt: site.expires_at,290+ isNew: false,291+ note: `revert to #${target.seq}`,292+ });293+294+ const addressStr = formatAddress(site.address);295+ return json({296+ address: addressStr,297+ url: siteUrl(env, addressStr),298+ alias: site.alias,299+ manifestHash: target.manifestHash,300+ revertedTo: target.seq,301+ seq,302+ });303+}@@ -0,0 +1,19 @@
1+import type { Env } from "./env.ts";2+3+/** Constant-time string comparison over UTF-8 bytes. */4+function timingSafeEqual(a: string, b: string): boolean {5+ const enc = new TextEncoder();6+ const ab = enc.encode(a);7+ const bb = enc.encode(b);8+ // Length leak is fine: token length is not secret.9+ if (ab.length !== bb.length) return false;10+ let diff = 0;11+ for (let i = 0; i < ab.length; i++) diff |= ab[i] ^ bb[i];12+ return diff === 0;13+}14+15+export function checkBearer(req: Request, env: Env): boolean {16+ const header = req.headers.get("authorization") ?? "";17+ if (!header.startsWith("Bearer ")) return false;18+ return timingSafeEqual(header.slice(7).trim(), env.NZIP_TOKEN);19+}@@ -0,0 +1,187 @@
1+import {2+ formatAddress,3+ SITES_PER_VAULT,4+ type Target,5+ VAULT_SLOTS,6+ vaultSlotOf,7+} from "../../shared/mod.ts";8+import type { PushInfo, SiteDetail, SiteInfo, VaultInfo } from "../../shared/mod.ts";9+import { type Env, siteUrl } from "./env.ts";10+11+export const HISTORY_CAP = 10;12+13+export interface SiteRow {14+ address: number;15+ vault_slot: number;16+ alias: string | null;17+ current_manifest: string;18+ created_at: number;19+ updated_at: number;20+ expires_at: number | null;21+ password_hash: string | null;22+}23+24+export interface VaultRow {25+ slot: number;26+ name: string;27+ created_at: number;28+}29+30+export async function getSiteByAddress(env: Env, address: number): Promise<SiteRow | null> {31+ return await env.DB.prepare("SELECT * FROM sites WHERE address = ?")32+ .bind(address).first<SiteRow>();33+}34+35+export async function getVaultByName(env: Env, name: string): Promise<VaultRow | null> {36+ return await env.DB.prepare("SELECT * FROM vaults WHERE name = ?").bind(name).first<VaultRow>();37+}38+39+export async function getVaultBySlot(env: Env, slot: number): Promise<VaultRow | null> {40+ return await env.DB.prepare("SELECT * FROM vaults WHERE slot = ?").bind(slot).first<VaultRow>();41+}42+43+/** Resolve an API target to an existing site row, or null if it doesn't exist. */44+export async function resolveTarget(env: Env, target: Target): Promise<SiteRow | null> {45+ if ("address" in target) return await getSiteByAddress(env, target.address);46+ const vault = await getVaultByName(env, target.vault);47+ if (!vault) return null;48+ return await env.DB.prepare("SELECT * FROM sites WHERE vault_slot = ? AND alias = ?")49+ .bind(vault.slot, target.alias).first<SiteRow>();50+}51+52+/**53+ * Pick a random free address within a vault. Random (not sequential) so URLs54+ * don't leak site count or enumerate in order.55+ */56+export async function allocateAddress(env: Env, slot: number): Promise<number> {57+ const taken = await env.DB.prepare("SELECT address FROM sites WHERE vault_slot = ?")58+ .bind(slot).all<{ address: number }>();59+ const used = new Set(taken.results.map((r) => r.address));60+ if (used.size >= SITES_PER_VAULT) throw new Error("vault is full");61+ for (let i = 0; i < 64; i++) {62+ const candidate = (slot << 12) | (crypto.getRandomValues(new Uint32Array(1))[0] & 0xfff);63+ if (!used.has(candidate)) return candidate;64+ }65+ // Vault nearly full and randomness unlucky: fall back to first free slot.66+ for (let id = 0; id < SITES_PER_VAULT; id++) {67+ const candidate = (slot << 12) | id;68+ if (!used.has(candidate)) return candidate;69+ }70+ throw new Error("vault is full");71+}72+73+export interface CommitParams {74+ address: number;75+ vaultSlot: number;76+ alias: string | null;77+ manifestHash: string;78+ expiresAt: number | null;79+ isNew: boolean;80+ note?: string;81+}82+83+/** Atomic commit: upsert site, append history, prune beyond HISTORY_CAP. Returns new seq. */84+export async function commitSite(env: Env, p: CommitParams): Promise<number> {85+ const now = Math.floor(Date.now() / 1000);86+ const seqRow = await env.DB.prepare("SELECT COALESCE(MAX(seq), 0) AS s FROM pushes WHERE address = ?")87+ .bind(p.address).first<{ s: number }>();88+ const seq = (seqRow?.s ?? 0) + 1;89+90+ const statements = [91+ p.isNew92+ ? env.DB.prepare(93+ `INSERT INTO sites (address, vault_slot, alias, current_manifest, created_at, updated_at, expires_at)94+ VALUES (?, ?, ?, ?, ?, ?, ?)`,95+ ).bind(p.address, p.vaultSlot, p.alias, p.manifestHash, now, now, p.expiresAt)96+ : env.DB.prepare(97+ `UPDATE sites SET current_manifest = ?, updated_at = ?, expires_at = ?, alias = COALESCE(?, alias)98+ WHERE address = ?`,99+ ).bind(p.manifestHash, now, p.expiresAt, p.alias, p.address),100+ env.DB.prepare(101+ "INSERT INTO pushes (address, seq, manifest_hash, pushed_at, note) VALUES (?, ?, ?, ?, ?)",102+ ).bind(p.address, seq, p.manifestHash, now, p.note ?? null),103+ env.DB.prepare(104+ "DELETE FROM pushes WHERE address = ? AND seq <= ?",105+ ).bind(p.address, seq - HISTORY_CAP),106+ ];107+ await env.DB.batch(statements);108+ return seq;109+}110+111+export async function listSites(env: Env, vaultSlot?: number): Promise<(SiteRow & { vault_name: string })[]> {112+ const base =113+ `SELECT s.*, v.name AS vault_name FROM sites s JOIN vaults v ON v.slot = s.vault_slot`;114+ const stmt = vaultSlot === undefined115+ ? env.DB.prepare(`${base} ORDER BY s.updated_at DESC`)116+ : env.DB.prepare(`${base} WHERE s.vault_slot = ? ORDER BY s.updated_at DESC`).bind(vaultSlot);117+ const res = await stmt.all<SiteRow & { vault_name: string }>();118+ return res.results;119+}120+121+export async function siteHistory(env: Env, address: number): Promise<PushInfo[]> {122+ const res = await env.DB.prepare(123+ "SELECT seq, manifest_hash, pushed_at, note FROM pushes WHERE address = ? ORDER BY seq DESC",124+ ).bind(address).all<{ seq: number; manifest_hash: string; pushed_at: number; note: string | null }>();125+ return res.results.map((r) => ({126+ seq: r.seq,127+ manifestHash: r.manifest_hash,128+ pushedAt: r.pushed_at,129+ note: r.note,130+ }));131+}132+133+export async function listVaults(env: Env): Promise<VaultInfo[]> {134+ const res = await env.DB.prepare(135+ `SELECT v.slot, v.name, v.created_at, COUNT(s.address) AS site_count136+ FROM vaults v LEFT JOIN sites s ON s.vault_slot = v.slot137+ GROUP BY v.slot ORDER BY v.slot`,138+ ).all<{ slot: number; name: string; created_at: number; site_count: number }>();139+ return res.results.map((r) => ({140+ slot: r.slot,141+ name: r.name,142+ createdAt: r.created_at,143+ siteCount: r.site_count,144+ }));145+}146+147+export async function createVault(env: Env, name: string, slot?: number): Promise<VaultRow> {148+ const now = Math.floor(Date.now() / 1000);149+ let chosen = slot;150+ if (chosen === undefined) {151+ const takenRes = await env.DB.prepare("SELECT slot FROM vaults").all<{ slot: number }>();152+ const taken = new Set(takenRes.results.map((r) => r.slot));153+ for (let s = 0; s < VAULT_SLOTS; s++) {154+ if (!taken.has(s)) {155+ chosen = s;156+ break;157+ }158+ }159+ if (chosen === undefined) throw new Error("all 16 vault slots are taken");160+ }161+ await env.DB.prepare("INSERT INTO vaults (slot, name, created_at) VALUES (?, ?, ?)")162+ .bind(chosen, name, now).run();163+ return { slot: chosen, name, created_at: now };164+}165+166+export function siteRowToInfo(env: Env, row: SiteRow, vaultName: string): SiteInfo {167+ const address = formatAddress(row.address);168+ return {169+ address,170+ vault: vaultName,171+ alias: row.alias,172+ manifestHash: row.current_manifest,173+ createdAt: row.created_at,174+ updatedAt: row.updated_at,175+ expiresAt: row.expires_at,176+ url: siteUrl(env, address),177+ protected: row.password_hash !== null,178+ };179+}180+181+export async function siteRowToDetail(env: Env, row: SiteRow): Promise<SiteDetail> {182+ const vault = await getVaultBySlot(env, vaultSlotOf(row.address));183+ return {184+ ...siteRowToInfo(env, row, vault?.name ?? `slot-${row.vault_slot}`),185+ history: await siteHistory(env, row.address),186+ };187+}@@ -0,0 +1,21 @@
1+export interface Env {2+ CONTENT: R2Bucket;3+ DB: D1Database;4+ NZIP_TOKEN: string;5+ PUBLIC_BASE: string;6+}7+8+export function json<T = unknown>(body: T, status = 200, headers: HeadersInit = {}): Response {9+ return new Response(JSON.stringify(body), {10+ status,11+ headers: { "content-type": "application/json; charset=utf-8", ...headers },12+ });13+}14+15+export function err(message: string, status: number): Response {16+ return json({ error: message }, status);17+}18+19+export function siteUrl(env: Env, address: string): string {20+ return `${env.PUBLIC_BASE}/${address}`;21+}@@ -0,0 +1,57 @@
1+import { parseManifest } from "../../shared/mod.ts";2+import type { Env } from "./env.ts";3+4+// An R2 object is live iff it is referenced by the current manifest or any5+// retained history entry of any non-deleted site. The 24h age guard means an6+// in-flight push (blobs uploaded, commit pending) can never lose objects to a7+// concurrent sweep.8+const MIN_AGE_MS = 24 * 3600 * 1000;9+10+export async function runGc(env: Env): Promise<{ expiredSites: number; deletedObjects: number }> {11+ const now = Math.floor(Date.now() / 1000);12+13+ // 1. Sweep expired sites (cascades to pushes history).14+ const expired = await env.DB.prepare(15+ "DELETE FROM sites WHERE expires_at IS NOT NULL AND expires_at < ? RETURNING address",16+ ).bind(now).all<{ address: number }>();17+18+ // 2. Mark: every manifest referenced by any remaining site or history row.19+ const liveManifests = new Set<string>();20+ const current = await env.DB.prepare("SELECT current_manifest FROM sites")21+ .all<{ current_manifest: string }>();22+ for (const r of current.results) liveManifests.add(r.current_manifest);23+ const historic = await env.DB.prepare("SELECT DISTINCT manifest_hash FROM pushes")24+ .all<{ manifest_hash: string }>();25+ for (const r of historic.results) liveManifests.add(r.manifest_hash);26+27+ // Live blobs = union of files across live manifests.28+ const liveBlobs = new Set<string>();29+ for (const hash of liveManifests) {30+ const obj = await env.CONTENT.get(`manifest/${hash}`);31+ if (!obj) continue;32+ const manifest = parseManifest(new Uint8Array(await obj.arrayBuffer()));33+ for (const f of Object.values(manifest.files)) liveBlobs.add(f.h);34+ }35+36+ // 3. Sweep R2: delete unreferenced objects older than the age guard.37+ let deleted = 0;38+ const cutoff = Date.now() - MIN_AGE_MS;39+ for (const prefix of ["manifest/", "blob/"] as const) {40+ let cursor: string | undefined;41+ do {42+ const page: R2Objects = await env.CONTENT.list({ prefix, cursor, limit: 1000 });43+ const doomed = page.objects.filter((o) => {44+ if (o.uploaded.getTime() > cutoff) return false;45+ const hash = o.key.slice(prefix.length);46+ return prefix === "manifest/" ? !liveManifests.has(hash) : !liveBlobs.has(hash);47+ }).map((o) => o.key);48+ if (doomed.length > 0) {49+ await env.CONTENT.delete(doomed);50+ deleted += doomed.length;51+ }52+ cursor = page.truncated ? page.cursor : undefined;53+ } while (cursor);54+ }55+56+ return { expiredSites: expired.results.length, deletedObjects: deleted };57+}@@ -0,0 +1,31 @@
1+import { api } from "./api.ts";2+import { checkBearer } from "./auth.ts";3+import type { Env } from "./env.ts";4+import { err } from "./env.ts";5+import { runGc } from "./gc.ts";6+import { serve } from "./serve.ts";7+8+export default {9+ async fetch(req: Request, env: Env): Promise<Response> {10+ const url = new URL(req.url);11+12+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {13+ if (!checkBearer(req, env)) return err("unauthorized", 401);14+ return await api(req, env, url);15+ }16+17+ const isUnlock = req.method === "POST" && /^\/[0-9a-f]{4}\/__unlock$/.test(url.pathname);18+ if (req.method !== "GET" && req.method !== "HEAD" && !isUnlock) {19+ return err("method not allowed", 405);20+ }21+ return await serve(req, env, url);22+ },23+24+ async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {25+ ctx.waitUntil(26+ runGc(env).then((r) =>27+ console.log(`gc: ${r.expiredSites} expired sites, ${r.deletedObjects} objects deleted`)28+ ),29+ );30+ },31+} satisfies ExportedHandler<Env>;@@ -0,0 +1,100 @@
1+// Password hashing + unlock-cookie signing for protected shares.2+3+import type { Env } from "./env.ts";4+5+const PBKDF2_ITERATIONS = 100_000;6+const COOKIE_TTL_S = 7 * 24 * 3600;7+8+const hex = (bytes: ArrayBuffer | Uint8Array) =>9+ [...new Uint8Array(bytes as ArrayBuffer)].map((b) => b.toString(16).padStart(2, "0")).join("");10+11+function hexToBytes(s: string): Uint8Array {12+ const out = new Uint8Array(s.length / 2);13+ for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);14+ return out;15+}16+17+async function pbkdf2(password: string, salt: Uint8Array, iterations: number): Promise<string> {18+ const key = await crypto.subtle.importKey(19+ "raw",20+ new TextEncoder().encode(password),21+ "PBKDF2",22+ false,23+ ["deriveBits"],24+ );25+ const bits = await crypto.subtle.deriveBits(26+ { name: "PBKDF2", hash: "SHA-256", salt: salt as BufferSource, iterations },27+ key,28+ 256,29+ );30+ return hex(bits);31+}32+33+/** → "pbkdf2$<iterations>$<saltHex>$<hashHex>" */34+export async function hashPassword(password: string): Promise<string> {35+ const salt = crypto.getRandomValues(new Uint8Array(16));36+ return `pbkdf2$${PBKDF2_ITERATIONS}$${hex(salt)}$${await pbkdf2(password, salt, PBKDF2_ITERATIONS)}`;37+}38+39+export async function verifyPassword(password: string, stored: string): Promise<boolean> {40+ const [scheme, iterStr, saltHex, hashHex] = stored.split("$");41+ if (scheme !== "pbkdf2") return false;42+ const actual = await pbkdf2(password, hexToBytes(saltHex), parseInt(iterStr, 10));43+ // Both sides are fixed-length hex of a fresh derivation; timing is not a concern here,44+ // but compare in constant time anyway.45+ if (actual.length !== hashHex.length) return false;46+ let diff = 0;47+ for (let i = 0; i < actual.length; i++) diff |= actual.charCodeAt(i) ^ hashHex.charCodeAt(i);48+ return diff === 0;49+}50+51+async function hmacKey(env: Env): Promise<CryptoKey> {52+ return await crypto.subtle.importKey(53+ "raw",54+ new TextEncoder().encode(`nzip-unlock:${env.NZIP_TOKEN}`),55+ { name: "HMAC", hash: "SHA-256" },56+ false,57+ ["sign", "verify"],58+ );59+}60+61+function cookieName(address: string): string {62+ return `nzip_a${address}`;63+}64+65+/** Set-Cookie value granting access to one site for COOKIE_TTL_S. */66+export async function makeUnlockCookie(env: Env, address: string): Promise<string> {67+ const exp = Math.floor(Date.now() / 1000) + COOKIE_TTL_S;68+ const sig = hex(69+ await crypto.subtle.sign(70+ "HMAC",71+ await hmacKey(env),72+ new TextEncoder().encode(`${address}.${exp}`),73+ ),74+ );75+ // Path without trailing slash so the cookie also matches the bare /{address} URL.76+ return `${cookieName(address)}=${exp}.${sig}; Max-Age=${COOKIE_TTL_S}; Path=/${address}; HttpOnly; Secure; SameSite=Lax`;77+}78+79+export async function hasValidUnlockCookie(80+ req: Request,81+ env: Env,82+ address: string,83+): Promise<boolean> {84+ const cookies = req.headers.get("cookie") ?? "";85+ const match = cookies.match(new RegExp(`(?:^|;\\s*)${cookieName(address)}=([^;]+)`));86+ if (!match) return false;87+ const [expStr, sig] = match[1].split(".");88+ const exp = parseInt(expStr, 10);89+ if (!Number.isFinite(exp) || exp < Math.floor(Date.now() / 1000)) return false;90+ try {91+ return await crypto.subtle.verify(92+ "HMAC",93+ await hmacKey(env),94+ hexToBytes(sig) as BufferSource,95+ new TextEncoder().encode(`${address}.${exp}`),96+ );97+ } catch {98+ return false;99+ }100+}@@ -0,0 +1,135 @@
1+import { parseAddress, parseManifest } from "../../shared/mod.ts";2+import { getSiteByAddress, type SiteRow } from "./db.ts";3+import type { Env } from "./env.ts";4+import { hasValidUnlockCookie, makeUnlockCookie, verifyPassword } from "./password.ts";5+6+const GONE_PAGE = `<!doctype html><meta charset="utf-8"><title>expired — nzip</title>7+<style>body{background:#121110;color:#8a8172;font-family:ui-monospace,monospace;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}b{color:#ffb347;font-size:32px;display:block;margin-bottom:8px}</style>8+<div><b>410</b>this share expired</div>`;9+10+const NOT_FOUND_PAGE = `<!doctype html><meta charset="utf-8"><title>not found — nzip</title>11+<style>body{background:#121110;color:#8a8172;font-family:ui-monospace,monospace;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}b{color:#ffb347;font-size:32px;display:block;margin-bottom:8px}</style>12+<div><b>404</b>nothing here</div>`;13+14+const escapeHtml = (s: string): string =>15+ s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]!));16+17+// The landing shows the deployment's own hostname, derived from PUBLIC_BASE, so18+// the committed source carries no operator-specific branding. The first label19+// is plain; the rest (from the first dot) is accented.20+function landingPage(env: Env): string {21+ const host = new URL(env.PUBLIC_BASE).host;22+ const dot = host.indexOf(".");23+ const head = escapeHtml(dot === -1 ? host : host.slice(0, dot));24+ const tail = dot === -1 ? "" : escapeHtml(host.slice(dot));25+ return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeHtml(host)}</title>26+<style>body{background:#1e1810;color:#e7dbc5;font-family:ui-monospace,monospace;display:grid;place-items:center;min-height:100dvh;margin:0}b{color:#d99a5b}</style>27+<div>${head}${tail ? `<b>${tail}</b>` : ""}</div>`;28+}29+30+function htmlResponse(body: string, status: number, headers: HeadersInit = {}): Response {31+ return new Response(body, {32+ status,33+ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", ...headers },34+ });35+}36+37+function unlockForm(address: string, error?: string): string {38+ return `<!doctype html><meta charset="utf-8"><meta name="robots" content="noindex"><title>unlock — nzip</title>39+<style>body{background:#121110;color:#d6cfc2;font-family:ui-monospace,monospace;display:grid;place-items:center;height:100vh;margin:0}form{text-align:center}b{color:#ffb347;font-size:24px;display:block;margin-bottom:16px}input{background:#1a1815;border:1px solid #2e2a24;color:#d6cfc2;padding:10px 14px;font:inherit;outline:none}input:focus{border-color:#ffb347}button{background:#ffb347;border:0;color:#121110;padding:10px 18px;font:inherit;font-weight:700;cursor:pointer}.e{color:#f7768e;margin-top:12px;font-size:13px}</style>40+<form method="post" action="/${address}/__unlock"><b>🔒 ${address}</b><input type="password" name="password" placeholder="password" autofocus autocomplete="current-password"><button>unlock</button>${41+ error ? `<div class="e">${error}</div>` : ""42+ }</form>`;43+}44+45+async function handleUnlock(req: Request, env: Env, site: SiteRow, address: string): Promise<Response> {46+ const form = await req.formData().catch(() => null);47+ const password = form?.get("password");48+ if (typeof password !== "string" || !site.password_hash ||49+ !(await verifyPassword(password, site.password_hash))) {50+ return htmlResponse(unlockForm(address, "wrong password"), 401);51+ }52+ return new Response(null, {53+ status: 303,54+ headers: {55+ // Bare address: single-file sites serve there directly, multi-file ones56+ // bounce through the trailing-slash redirect.57+ location: `/${address}`,58+ "set-cookie": await makeUnlockCookie(env, address),59+ },60+ });61+}62+63+export async function serve(req: Request, env: Env, url: URL): Promise<Response> {64+ const path = url.pathname;65+66+ if (path === "/") return htmlResponse(landingPage(env), 200);67+ if (path === "/favicon.ico") return new Response(null, { status: 204 });68+ if (path === "/robots.txt") {69+ return new Response("User-agent: *\nAllow: /\n", {70+ headers: { "content-type": "text/plain" },71+ });72+ }73+74+ // /{4hex} or /{4hex}/{asset-path}75+ const m = path.match(/^\/([0-9a-f]{4})(\/.*)?$/);76+ if (!m) return htmlResponse(NOT_FOUND_PAGE, 404);77+ const addressStr = m[1];78+79+ const site = await getSiteByAddress(env, parseAddress(addressStr));80+ if (!site) return htmlResponse(NOT_FOUND_PAGE, 404);81+ const now = Math.floor(Date.now() / 1000);82+ if (site.expires_at !== null && site.expires_at < now) return htmlResponse(GONE_PAGE, 410);83+84+ // Password gate.85+ if (m[2] === "/__unlock" && req.method === "POST") {86+ return await handleUnlock(req, env, site, addressStr);87+ }88+ if (site.password_hash && !(await hasValidUnlockCookie(req, env, addressStr))) {89+ return htmlResponse(unlockForm(addressStr), 401);90+ }91+92+ const manifestObj = await env.CONTENT.get(`manifest/${site.current_manifest}`);93+ if (!manifestObj) return htmlResponse(NOT_FOUND_PAGE, 404);94+ const manifest = parseManifest(new Uint8Array(await manifestObj.arrayBuffer()));95+ const filePaths = Object.keys(manifest.files);96+97+ // Bare address: single-file sites serve directly (URL stays /{4hex} — nothing98+ // relative to break); multi-file bundles redirect to the trailing slash so99+ // relative asset URLs resolve.100+ let assetPath: string;101+ if (m[2] === undefined) {102+ if (filePaths.length !== 1) {103+ return Response.redirect(new URL(`/${addressStr}/`, url).toString(), 302);104+ }105+ assetPath = filePaths[0];106+ } else {107+ assetPath = decodeURIComponent(m[2].slice(1)); // strip leading /108+ if (assetPath === "" || assetPath.endsWith("/")) assetPath += "index.html";109+ }110+111+ let entry = manifest.files[assetPath];112+ if (!entry && manifest.files[`${assetPath}/index.html`]) {113+ // Directory hit without trailing slash: redirect so relative URLs resolve.114+ return Response.redirect(new URL(`${path}/`, url).toString(), 302);115+ }116+ if (!entry) return htmlResponse(NOT_FOUND_PAGE, 404);117+118+ const etag = `"${entry.h}"`;119+ if (req.headers.get("if-none-match") === etag) {120+ return new Response(null, { status: 304, headers: { etag } });121+ }122+123+ const blob = await env.CONTENT.get(`blob/${entry.h}`);124+ if (!blob) return htmlResponse(NOT_FOUND_PAGE, 404);125+126+ return new Response(blob.body, {127+ headers: {128+ "content-type": entry.ct,129+ "content-length": String(entry.s),130+ etag,131+ // Short max-age: addresses are mutable on re-push. ETag makes revalidation free.132+ "cache-control": "public, max-age=60",133+ },134+ });135+}@@ -0,0 +1,14 @@
1+{2+ "compilerOptions": {3+ "target": "ES2023",4+ "module": "ESNext",5+ "moduleResolution": "bundler",6+ "lib": ["ES2023"],7+ "types": ["@cloudflare/workers-types"],8+ "strict": true,9+ "noEmit": true,10+ "allowImportingTsExtensions": true,11+ "skipLibCheck": true12+ },13+ "include": ["src/**/*.ts", "../shared/**/*.ts"]14+}@@ -0,0 +1,28 @@
1+{2+ "$schema": "node_modules/wrangler/config-schema.json",3+ "name": "nzip",4+ "main": "src/index.ts",5+ "compatibility_date": "2026-06-01",6+ "r2_buckets": [7+ {8+ "binding": "CONTENT",9+ "bucket_name": "nzip-content"10+ }11+ ],12+ "d1_databases": [13+ {14+ "binding": "DB",15+ "database_name": "nzip",16+ "database_id": "00000000-0000-0000-0000-000000000000"17+ }18+ ],19+ "triggers": {20+ "crons": [21+ "0 4 * * *"22+ ]23+ },24+ "vars": {25+ "PUBLIC_BASE": "http://localhost:8787"26+ }27+ // Secrets (wrangler secret put): NZIP_TOKEN28+}@@ -0,0 +1,29 @@
1+{2+ "$schema": "node_modules/wrangler/config-schema.json",3+ "name": "nzip",4+ "main": "src/index.ts",5+ "compatibility_date": "2026-06-01",6+ "routes": [{ "pattern": "share.example.com", "custom_domain": true }],7+ "r2_buckets": [8+ {9+ "binding": "CONTENT",10+ "bucket_name": "nzip-content"11+ }12+ ],13+ "d1_databases": [14+ {15+ "binding": "DB",16+ "database_name": "nzip",17+ "database_id": "00000000-0000-0000-0000-000000000000"18+ }19+ ],20+ "triggers": {21+ "crons": [22+ "0 4 * * *"23+ ]24+ },25+ "vars": {26+ "PUBLIC_BASE": "https://share.example.com"27+ }28+ // Secrets (wrangler secret put --config wrangler.local.jsonc): NZIP_TOKEN29+}