Bumps @nzip/cli and @nzip/shared to 0.1.1: - add cli/README.md and shared/README.md with usage examples - add description fields to both deno.json - add @module docs to the cli and shared entrypoints - document every exported symbol in shared - reduce em-dashes across all READMEs, rewording for readability
- commit
- 9a4054c90f4c333d0b5a71461206941905b1ec3a
- parent
- ef43ade433bda5e57faa07a20a03209d840296e1
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 68 | +++++++++++++++--------------- |
| A | cli/ | 52 | +++++++++++++++++++++++ |
| M | cli/ | 3 | +- |
| M | cli/ | 16 | +++++++ |
| A | shared/ | 40 | ++++++++++++++++++ |
| M | shared/ | 5 | ++ |
| M | shared/ | 3 | +- |
| M | shared/ | bin | |
| M | shared/ | 1 | + |
| M | shared/ | 22 | ++++++++++ |
| M | shared/ | 17 | +++++++- |
@@ -4,7 +4,7 @@
44 55 **Push a directory of HTML from the terminal. Get a four-character URL back.**66 7-_A personal share tool — Deno CLI → Cloudflare Worker → R2 + D1. No daemon, no build step, no7+_A personal share tool. Deno CLI → Cloudflare Worker → R2 + D1. No daemon, no build step, no88 redeploys._99 1010 [](https://jsr.io/@nzip/cli)@@ -23,8 +23,8 @@ $ nzip push ./demo work:demo --ttl 30d
2323 ## How addresses work2424 2525 Every share lives at four hex characters. The first digit selects one of **16 registered vaults**,26-the rest a site within it — 4,096 slots per vault, allocated randomly so URLs don't leak how many27-sites exist.26+the rest a site within it. Each vault holds 4,096 slots, allocated randomly so URLs don't leak how27+many sites exist.2828 2929 ```text3030 https://share.example.com/2a3f@@ -43,24 +43,24 @@ three target forms:
4343 4444 ## Features4545 46-- **Instant** — a push is one manifest exchange plus the blobs the server hasn't seen. Nothing46+- **Instant.** A push is one manifest exchange plus the blobs the server hasn't seen. Nothing4747 rebuilds, nothing redeploys.48-- **Content-addressed** — per-file sha256 blobs + a canonical manifest per push. Identical files are49- stored once across every site; re-pushing an unchanged directory uploads `0 new blobs`.50-- **Ephemeral by default** — 14-day TTL, `--ttl 30d` or `--ttl forever` to override. Expired shares51- answer `410 Gone`; a daily cron sweeps them and garbage-collects unreferenced content.52-- **Revertible** — the last 10 pushes per site are kept. `nzip revert` repoints the address at any53- of them; the revert itself is recorded as a push.54-- **Password-protectable** — `nzip share work:demo --password …` gates the site behind an unlock55- form (PBKDF2 hashing, signed per-site cookie, 7 days).56-- **Single-user by design** — one bearer token, stored as a Worker secret and in48+- **Content-addressed.** Per-file sha256 blobs plus a canonical manifest per push. Identical files49+ are stored once across every site; re-pushing an unchanged directory uploads `0 new blobs`.50+- **Ephemeral by default.** 14-day TTL, with `--ttl 30d` or `--ttl forever` to override. Expired51+ shares answer `410 Gone`; a daily cron sweeps them and garbage-collects unreferenced content.52+- **Revertible.** The last 10 pushes per site are kept. `nzip revert` repoints the address at any of53+ them, and the revert itself is recorded as a push.54+- **Password-protectable.** `nzip share work:demo --password …` gates the site behind an unlock form55+ (PBKDF2 hashing, signed per-site cookie, 7 days).56+- **Single-user by design.** One bearer token, stored as a Worker secret and in5757 `~/.config/nzip/config.json` (mode 0600).58-- **Locates itself** — `nzip where <target>` prints the local directory this machine pushed a site59- from. A breadcrumb registry (`~/.config/nzip/paths.json`) records the source path on every push;60- it self-cleans — expired entries drop on write, `rm` forgets its entry, and `ls` reconciles61- against the live set. Use it as `cd "$(nzip where personal:plan)"`.62-- **Vault guardrail** — an optional `"allowVaults": ["home"]` in `config.json` restricts which vaults63- this install may target by name. Pushes or aliases outside the list are refused before any upload —58+- **Locates itself.** `nzip where <target>` prints the local directory this machine pushed a site59+ from. A breadcrumb registry (`~/.config/nzip/paths.json`) records the source path on every push and60+ self-cleans: expired entries drop on write, `rm` forgets its entry, and `ls` reconciles against the61+ live set. Use it as `cd "$(nzip where personal:plan)"`.62+- **Vault guardrail.** An optional `"allowVaults": ["home"]` in `config.json` restricts which vaults63+ this install may target by name. Pushes or aliases outside the list are refused before any upload,6464 so a home-project agent can't drop a doc into a vault that sits adjacent to what you share6565 professionally. Absent = unrestricted; raw hex addresses bypass it (they name no vault).6666 @@ -94,13 +94,13 @@ flowchart LR
9494 V(["visitor"]) -- "GET /2a3f" --> W9595 ```9696 97-A push is a stateless three-step protocol — the manifest itself is the state:97+A push is a stateless three-step protocol, and the manifest itself is the state:9898 99-1. `POST /api/push/prepare` — send the manifest, get back which blob hashes the server is missing100-2. `PUT /api/blob/{sha256}` — upload only those, one per request, 6 at a time; the server re-hashes99+1. `POST /api/push/prepare`: send the manifest, get back which blob hashes the server is missing100+2. `PUT /api/blob/{sha256}`: upload only those, one per request, 6 at a time; the server re-hashes101101 and rejects mismatches102-3. `POST /api/push/commit` — server re-verifies every blob exists, then commits atomically:103- resolve/allocate the address, repoint the site, append history102+3. `POST /api/push/commit`: the server re-verifies every blob exists, then commits atomically,103+ resolving or allocating the address, repointing the site, and appending history104104 105105 Serving is one D1 read (address → manifest, expiry, password) and two R2 reads, with `ETag`106106 revalidation and a 60-second cache. Metadata lives in three tables:@@ -109,17 +109,17 @@ revalidation and a 60-second cache. Metadata lives in three tables:
109109 | -------- | ------------------------------------------------------------------------ |110110 | `vaults` | slot (0–15), name |111111 | `sites` | address (0–65535), vault, alias, current manifest, expiry, password hash |112-| `pushes` | per-site history (seq, manifest, note) — capped at 10, powers `revert` |112+| `pushes` | per-site history (seq, manifest, note), capped at 10, powers `revert` |113113 114114 **GC safety rule:** an R2 object is deleted only if no live site or retained history entry115-references it _and_ it's older than 24 hours — so an in-flight push can never lose objects to a115+references it _and_ it's older than 24 hours, so an in-flight push can never lose objects to a116116 concurrent sweep.117117 118118 ## Repo layout119119 120120 ```text121-shared/ manifest canonicalization, hashing, addressing — imported by both sides122-cli/ the nzip command (Deno, no framework) — published as jsr:@nzip/cli121+shared/ manifest canonicalization, hashing, addressing (imported by both sides)122+cli/ the nzip command (Deno, no framework), published as jsr:@nzip/cli123123 worker/ Cloudflare Worker: serving + API + GC cron (wrangler)124124 ```125125 @@ -130,7 +130,7 @@ manifest hash. It sticks to Web-standard APIs only, so the same code runs under
130130 ## Install131131 132132 The CLI ships on [JSR](https://jsr.io/@nzip/cli). With [Deno](https://docs.deno.com/runtime/) on133-your `PATH`, install it in one line — then point it at your server and push:133+your `PATH`, install it in one line, then point it at your server and push:134134 135135 ```sh136136 deno install -g -A -f -n nzip jsr:@nzip/cli # 1. install the `nzip` command@@ -144,13 +144,13 @@ nzip push ./site work:demo # 3. get a URL back
144144 so every later command just works. Upgrade any time by re-running the install line; try it without145145 installing via `deno run -A jsr:@nzip/cli --help`.146146 147-Adding a **second machine**? Only the CLI is per-machine — the Worker, R2, D1, vaults, and token are148-already provisioned. Install from JSR and run `nzip auth` with the same server and token; every vault149-and site is instantly there. (The `nzip where` breadcrumb registry is local, so it only knows sites150-pushed from this machine.)147+Adding a **second machine**? Only the CLI is per-machine; the Worker, R2, D1, vaults, and token are148+already provisioned. Install from JSR and run `nzip auth` with the same server and token, and every149+vault and site is instantly there. (The `nzip where` breadcrumb registry is local, so it only knows150+sites pushed from this machine.)151151 152152 `nzip` is self-hosted. The server URL is supplied by the operator; there is no bundled public153-service — use the same URL you set as `vars.PUBLIC_BASE` in your Wrangler config. Standing up that153+service. Use the same URL you set as `vars.PUBLIC_BASE` in your Wrangler config. Standing up that154154 server is the one-time setup below.155155 156156 <details>@@ -0,0 +1,52 @@
1+# `@nzip/cli`2+3+**Push a directory of HTML from the terminal. Get a four-character URL back.**4+5+The command-line client for [nzip](https://github.com/FelineStateMachine/nzip), a personal,6+self-hosted share tool. The CLI bundles a directory, uploads only the blobs your server hasn't seen,7+and prints a tiny URL. The backend is your own Cloudflare Worker (R2 + D1); there is no public8+service.9+10+## Install11+12+Requires [Deno](https://docs.deno.com/runtime/) on your `PATH`.13+14+```sh15+deno install -g -A -f -n nzip jsr:@nzip/cli16+```17+18+Try it without installing:19+20+```sh21+deno run -A jsr:@nzip/cli --help22+```23+24+## Use25+26+```sh27+nzip auth --server https://share.example.com # authenticate (prompts for the token)28+nzip vault add work # register a named vault29+nzip push ./site work:demo --ttl 30d # → https://share.example.com/12d830+```31+32+Config is saved to `~/.config/nzip/config.json` (mode 0600), so later commands just work. On a second33+machine, install from JSR and re-run `nzip auth` with the same server and token; every vault and site34+is already there.35+36+## Commands37+38+```text39+nzip auth [--server URL] [--token T] authenticate and save config40+nzip vault add <name> [--slot N] register a vault (16 slots, 0x0–0xf)41+nzip vault ls | default <name> list vaults / set the default42+nzip push <dir|file> [target] [--ttl 14d|30d|forever]43+nzip share <target> [--ttl …] [--password PW | --no-password]44+nzip ls [vault] list sites45+nzip where <target> print the local dir this machine pushed from46+nzip rm <target> [--yes] delete a site47+nzip status server + vault overview48+nzip revert <target> [--to N] [--list] repoint to a previous push49+```50+51+See the [project README](https://github.com/FelineStateMachine/nzip) for how addresses work,52+self-hosting setup, and architecture. MIT licensed.@@ -1,7 +1,8 @@
11 {22 "name": "@nzip/cli",3- "version": "0.1.0",3+ "version": "0.1.1",44 "license": "MIT",5+ "description": "Push a directory of HTML from the terminal and get a four-character URL back — the Deno CLI for nzip, a self-hosted Cloudflare-backed share tool.",56 "exports": "./main.ts",67 "imports": {78 "@std/cli": "jsr:@std/cli@^1.0.0",@@ -1,4 +1,20 @@
11 2+/**3+ * `nzip` — push a directory of HTML from the terminal and get a four-character4+ * URL back. This is the CLI entrypoint; it parses argv and dispatches to the5+ * command handlers (`auth`, `vault`, `push`, `share`, `ls`, `where`, `rm`,6+ * `status`, `revert`).7+ *8+ * Install and run it as a command rather than importing it:9+ *10+ * ```sh11+ * deno install -g -A -f -n nzip jsr:@nzip/cli12+ * nzip auth --server https://share.example.com13+ * nzip push ./site work:demo14+ * ```15+ *16+ * @module17+ */218 // nzip — push HTML and get a tiny URL back.319 420 import { parseArgs } from "@std/cli/parse-args";@@ -0,0 +1,40 @@
1+# `@nzip/shared`2+3+**The runtime-agnostic contract between the [nzip](https://github.com/FelineStateMachine/nzip) CLI4+and Worker.**5+6+Manifest canonicalization, content hashing, address/target parsing, and the wire types exchanged7+over the API. Canonical JSON serialization lives in exactly one place8+([`manifest.ts`](./manifest.ts)) so the CLI and Worker can never disagree about a manifest hash.9+10+This package uses only Web-standard APIs (`crypto.subtle`, `TextEncoder`/`TextDecoder`, `JSON`), so11+the same code runs under Deno, Cloudflare Workers, Node.js, Bun, and browsers.12+13+## Install14+15+```sh16+deno add jsr:@nzip/shared17+```18+19+## Use20+21+```ts22+import { canonicalManifestBytes, manifestHash, parseTarget, type Manifest } from "@nzip/shared";23+24+const manifest: Manifest = {25+ v: 1,26+ files: {27+ "index.html": { h: "…64-hex-sha256…", s: 128, ct: "text/html; charset=utf-8" },28+ },29+};30+31+// Byte-identical on every runtime, which is what makes hashes agree across CLI and Worker.32+const hash = await manifestHash(manifest);33+const bytes = canonicalManifestBytes(manifest);34+35+parseTarget("2a3f"); // { kind: "address", address: 10815 }36+parseTarget("work:demo"); // { kind: "vaultAlias", vault: "work", alias: "demo" }37+parseTarget("demo"); // { kind: "alias", alias: "demo" }38+```39+40+MIT licensed.@@ -1,12 +1,15 @@
11 // 4-hex site addresses and vault:alias target parsing.22 // address (int 0..65535) = vaultSlot << 12 | siteId33 4+/** Number of named vault slots — the high nibble of an address (0x0–0xf). */45 export const VAULT_SLOTS = 16;6+/** Sites addressable within a single vault — the low three nibbles (0x000–0xfff). */57 export const SITES_PER_VAULT = 0x1000;68 79 const ADDR_RE = /^[0-9a-f]{4}$/;810 const NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;911 12+/** True if `s` is a well-formed 4-hex address like `"2a3f"`. */1013 export function isAddressString(s: string): boolean {1114 return ADDR_RE.test(s);1215 }@@ -25,6 +28,7 @@ export function formatAddress(addr: number): string {
2528 return addr.toString(16).padStart(4, "0");2629 }2730 31+/** Extract the vault slot (0–15) from an address integer. */2832 export function vaultSlotOf(addr: number): number {2933 return addr >> 12;3034 }@@ -34,6 +38,7 @@ export function isValidName(name: string): boolean {
3438 return NAME_RE.test(name) && !ADDR_RE.test(name);3539 }3640 41+/** A parsed target: a raw address, a `vault:alias` pair, or a bare alias in the default vault. */3742 export type ParsedTarget =3843 | { kind: "address"; address: number }3944 | { kind: "vaultAlias"; vault: string; alias: string }Binary file.
@@ -35,6 +35,7 @@ const TYPES: Record<string, string> = {
3535 webmanifest: "application/manifest+json",3636 };3737 38+/** Resolve a content-type from a path's file extension, defaulting to `application/octet-stream`. */3839 export function contentTypeFor(path: string): string {3940 const dot = path.lastIndexOf(".");4041 const ext = dot === -1 ? "" : path.slice(dot + 1).toLowerCase();@@ -1,3 +1,25 @@
1+/**2+ * Shared contract between the nzip CLI and Worker: manifest canonicalization,3+ * content hashing, address/target parsing, media-type resolution, and the wire4+ * types exchanged over the API.5+ *6+ * This module is deliberately runtime-agnostic — it uses only Web-standard APIs7+ * (no `Deno.*` or Workers globals), so the exact same code runs under Deno and8+ * workerd and both sides always agree on a manifest hash.9+ *10+ * ```ts11+ * import { manifestHash, parseTarget } from "@nzip/shared";12+ *13+ * const t = parseTarget("work:demo"); // { kind: "vaultAlias", vault: "work", alias: "demo" }14+ * const hash = await manifestHash({15+ * v: 1,16+ * files: { "index.html": { h: "…", s: 12, ct: "text/html" } },17+ * });18+ * ```19+ *20+ * @module21+ */22+123 export * from "./types.ts";224 export * from "./hash.ts";325 export * from "./manifest.ts";@@ -1,6 +1,7 @@
11 // Wire types shared by the CLI and the Worker.22 // This package must stay runtime-agnostic: Web APIs only, no Deno.* or Workers globals.33 4+/** A single file entry in a manifest. Keys are kept short because they're hashed verbatim. */45 export interface ManifestFile {56 /** sha256 hex of the file bytes */67 h: string;@@ -10,27 +11,32 @@ export interface ManifestFile {
1011 ct: string;1112 }1213 14+/** A push's complete file set — the content-addressed description of a site version. */1315 export interface Manifest {16+ /** manifest format version */1417 v: 1;1518 /** path (relative, forward slashes, no leading ./) → file entry */1619 files: Record<string, ManifestFile>;1720 }1821 19-/** "2a3f" | "work:demo" | "demo" (alias in default vault, resolved client-side to vault:alias) */22+/** A push destination: a raw address, or a vault (optionally with an alias). */2023 export type Target =2124 | { address: number }2225 | { vault: string; alias?: string | null };2326 27+/** Body of `POST /api/push/prepare`: the manifest the client wants to push. */2428 export interface PrepareRequest {2529 manifest: Manifest;2630 }2731 32+/** Reply to `prepare`: the manifest's hash and which blobs the server still needs. */2833 export interface PrepareResponse {2934 manifestHash: string;3035 /** blob hashes the server does not have yet */3136 missing: string[];3237 }3338 39+/** Body of `POST /api/push/commit`: finalize a push at `target` with an optional TTL. */3440 export interface CommitRequest {3541 manifest: Manifest;3642 target: Target;@@ -38,6 +44,7 @@ export interface CommitRequest {
3844 ttl?: number | "forever";3945 }4046 47+/** Reply to `commit`: the allocated address and the live URL for the new push. */4148 export interface CommitResponse {4249 /** 4-hex address, e.g. "2a3f" */4350 address: string;@@ -50,6 +57,7 @@ export interface CommitResponse {
5057 seq: number;5158 }5259 60+/** Summary of a single site, as returned by list/status endpoints. */5361 export interface SiteInfo {5462 address: string;5563 vault: string;@@ -62,6 +70,7 @@ export interface SiteInfo {
6270 protected: boolean;6371 }6472 73+/** One entry in a site's push history. */6574 export interface PushInfo {6675 seq: number;6776 manifestHash: string;@@ -69,10 +78,12 @@ export interface PushInfo {
6978 note: string | null;7079 }7180 81+/** A site plus its retained push history (powers `nzip revert`). */7282 export interface SiteDetail extends SiteInfo {7383 history: PushInfo[];7484 }7585 86+/** A registered vault: its slot, name, and current site count. */7687 export interface VaultInfo {7788 slot: number;7889 name: string;@@ -80,6 +91,7 @@ export interface VaultInfo {
8091 siteCount: number;8192 }8293 94+/** Reply to `GET /api/status`: server version and a snapshot of vaults and sites. */8395 export interface StatusResponse {8496 ok: true;8597 version: string;@@ -89,16 +101,19 @@ export interface StatusResponse {
89101 expiringSoon: number;90102 }91103 104+/** Body of a revert request: the push sequence number to repoint to (defaults to previous). */92105 export interface RevertRequest {93106 toSeq?: number;94107 }95108 109+/** Body of a site PATCH: change TTL and/or password. Omitted fields are left unchanged. */96110 export interface PatchSiteRequest {97111 ttl?: number | "forever";98112 /** set a password (string), clear it (null), or leave unchanged (undefined) */99113 password?: string | null;100114 }101115 116+/** Standard error envelope returned by the API on failure. */102117 export interface ApiError {103118 error: string;104119 }