clone

push a directory of html from the terminal and get a four-character url back. a deno cli on cloudflare

doc: raise JSR score and tidy prose for 0.1.1

Dami committed 9a4054c
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>
11 files changed, +190 -37
MREADME.md68+++++++++++++++---------------
Acli/README.md52+++++++++++++++++++++++
Mcli/deno.json3+-
Mcli/main.ts16+++++++
Ashared/README.md40++++++++++++++++++
Mshared/address.ts5++
Mshared/deno.json3+-
Mshared/manifest.tsbin
Mshared/mediatypes.ts1+
Mshared/mod.ts22++++++++++
Mshared/types.ts17+++++++-
M README.md +34 -34
@@ -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 [![JSR](https://jsr.io/badges/@nzip/cli)](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>
A cli/README.md +52 -0
@@ -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.
M cli/deno.json +2 -1
M cli/main.ts +16 -0
A shared/README.md +40 -0
M shared/address.ts +5 -0
M shared/deno.json +2 -1
M shared/manifest.ts +0 -0

Binary file.

M shared/mediatypes.ts +1 -0
M shared/mod.ts +22 -0
M shared/types.ts +16 -1
@@ -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 }