clone

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

docs: document notifications and stabilize mobile surfaces

Dami committed 8a3c1cf
commit
8a3c1cf481ec0f1a695f8e117d226554a1048784
parent
84ef7ee176bb57920afffd0d8df7609c0bae2bb6
author
Dami <dami.etoile@gmail.com>
8 files changed, +88 -2
MREADME.md32++++++++++++++++++++++++++++++
Mcli/README.md23++++++++++++++++++++++
Mcli/main.ts3+++
Mskills/nzip/SKILL.md15++++++++++++++
Mworker/src/notify_ui.ts2+-
Mworker/src/serve.ts2+-
Mworker/test/index.runtime.test.ts1+
Mworker/test/serve.test.mjs12+++++++++++
M README.md +32 -0
@@ -59,6 +59,9 @@ Commands accept any of three target forms:
5959 change protection later.6060 - **Single-user by design.** One bearer token, stored as a Worker secret and in6161 `~/.config/nzip/config.json` (mode 0600).62+- **Owner notifications.** Pair explicitly approved phones from the quiet root63+ PWA, then send Web Push notifications from the authenticated CLI. Clicks may64+ open `/` or a manifest-pinned nzip site; arbitrary external URLs are refused.6265 - **Locates itself.** `nzip where <target>` prints the local directory this6366 machine pushed a site from. A breadcrumb registry6467 (`~/.config/nzip/paths.json`) records the source path on every push and
@@ -86,6 +89,11 @@ nzip ls [vault] list sites
8689 nzip where <target> print the local dir this machine pushed from8790 nzip rm <target> [--yes] delete a site8891 nzip status server + vault overview92+nzip notify <body> [--title TEXT] [--open TARGET] [--tag TEXT]93+nzip notify test send a diagnostic notification94+nzip notify approve <code> --name NAME [--yes]95+nzip notify devices list paired notification devices96+nzip notify revoke <device-id> [--yes] revoke a notification device8997 nzip revert <target> [--to N] [--list] repoint to a previous push9098 ```9199
@@ -112,6 +120,27 @@ files—not dotfiles, `.nzipignore`, or other local project metadata excluded fr
112120 a push. The former `nzip download` command remains available as a compatibility113121 alias.114122 123+### Owner notifications124+125+Open the deployment root on a phone, tap `pair`, and approve the displayed code126+from an authenticated terminal:127+128+```sh129+nzip notify approve ABCD-1234 --name "Personal phone"130+```131+132+Wait for `paired` before installing the PWA. In the installed app, tap133+`notifications off` to request permission and attach the subscription. Send a134+message with `nzip notify "Build finished"`; add `--open work:report` to open an135+existing same-origin site when the notification is tapped. Use136+`nzip notify devices` to inspect delivery health and `nzip notify revoke` to137+remove a device.138+139+Notification titles and bodies may be visible on a lock screen. Never put140+passwords, tokens, private URLs, or sensitive personal data in them. If an141+installed app loses its pairing cookie, remove it, pair again in the browser,142+wait for `paired`, and reinstall it.143+115144 ## Architecture116145 117146 ### Overview
@@ -213,6 +242,9 @@ Security alert state is deliberately separate:
213242 | `security_signals` | rate-limit confirmations, deduplicated by scanner and window |214243 | `security_incidents` | open/closed state, severity, suppression timestamps, and incident totals |215244 | `security_notifications` | durable email payloads, delivery attempts, and retry state |245+| `notification_devices` | approved claims and active Web Push subscriptions |246+| `notification_events` | bounded payloads and manifest-pinned click targets |247+| `notification_deliveries`| leased per-device attempts, retries, and terminal outcomes |216248 217249 **GC safety rule:** an R2 object is deleted only if no live site or retained218250 history entry references it _and_ it's older than 24 hours, so an in-flight push
M cli/README.md +23 -0
@@ -50,6 +50,11 @@ nzip ls [vault] list sites
5050 nzip where <target> print the local dir this machine pushed from5151 nzip rm <target> [--yes] delete a site5252 nzip status server + vault overview53+nzip notify <body> [--title TEXT] [--open TARGET] [--tag TEXT]54+nzip notify test send a diagnostic notification55+nzip notify approve <code> --name NAME [--yes]56+nzip notify devices list paired notification devices57+nzip notify revoke <device-id> [--yes] revoke a notification device5358 nzip revert <target> [--to N] [--list] repoint to a previous push5459 ```5560
@@ -66,5 +71,23 @@ It uses the configured bearer token, verifies file hashes, and never exposes sou
6671 page URL. Only uploaded files can be restored; ignored dotfiles and local metadata were never6772 stored. The former `nzip download` command remains available as a compatibility alias.6873 74+## Notifications75+76+Pairing starts from the deployment root. After the phone shows a code, approve it from the77+authenticated CLI:78+79+```sh80+nzip notify approve ABCD-1234 --name "Personal phone"81+nzip notify devices82+nzip notify "Report ready" --title "nzip" --open work:report83+```84+85+`--open` accepts an existing nzip target and applies the same local vault guardrails as other86+target-aware commands. It never accepts an arbitrary URL. `nzip notify test` uses the ordinary87+notification delivery path. Use `nzip notify revoke <device-id> --yes` to remove a device.88+89+Notification content may appear on a lock screen. Never include passwords, tokens, private URLs, or90+sensitive personal data in a title or body.91+6992 See the [project README](https://github.com/FelineStateMachine/nzip) for how addresses work,7093 self-hosting setup, and architecture. MIT licensed.
M cli/main.ts +3 -0
M skills/nzip/SKILL.md +15 -0
@@ -101,6 +101,13 @@ Prefer `--json` when consuming command output programmatically.
101101 - Review history: `nzip revert <target> --list --json`102102 - Restore history: `nzip revert <target> --to <push-number>`103103 - Delete only on explicit request: `nzip rm <target> --yes`104+- List notification devices: `nzip notify devices --json`105+- Approve a user-supplied pairing code:106+ `nzip notify approve <code> --name <name> [--yes]`107+- Send only on explicit request:108+ `nzip notify <body> [--title TEXT] [--open TARGET] [--tag TEXT]`109+- Exercise the ordinary delivery path: `nzip notify test`110+- Revoke only on explicit request: `nzip notify revoke <device-id> --yes`104111 105112 Use password protection when the content or user requires it; `nzip push` accepts the same106113 `--password <pw> | --no-password` flags to set it at publish time. Vault descriptions appear in
@@ -110,6 +117,14 @@ records, but other clients keep the old name; renaming to a name outside `allowV
110117 Treat `revert`, vault renames, password changes, TTL changes, and deletion as state changes; do not111118 perform them while merely diagnosing. Never infer deletion confirmation.112119 120+Notifications are external, lock-screen-visible side effects. Do not send,121+test, approve, or revoke unless the user explicitly requests that action. Never122+put credentials, private URLs, sensitive personal data, or other secrets in a123+notification title or body. Prefer `--open <target>` for click-throughs because124+the CLI resolves an existing nzip site, applies vault guardrails, and sends only125+a same-origin path. A pairing code is short-lived but is not sufficient on its126+own; approval still uses the configured owner bearer token.127+113128 ## Sane defaults114129 115130 - Keep generated HTML in `.nzip-<purpose>/`, not a system temp directory or a tracked source
M worker/src/notify_ui.ts +1 -1
@@ -289,7 +289,7 @@ function escapeHtml(value: string): string {
289289 /** Claim-independent shell. Safe to cache publicly; all state is fetched client-side. */290290 export function notifyLandingPage(env: NotifyEnv): string {291291 const host = escapeHtml(new URL(env.PUBLIC_BASE).host);292- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#1e1810"><title>${host}</title>292+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"><meta name="theme-color" content="#1e1810"><title>${host}</title>293293 <style>*{box-sizing:border-box}body{background:#1e1810;color:#e7dbc5;font-family:ui-monospace,monospace;margin:0;min-height:100dvh;display:flex;flex-direction:column}main{flex:1;display:grid;place-items:center;padding:24px;text-align:center}.landing{display:grid;gap:24px;justify-items:center}.wordmark{color:#f2e8d4;font-size:clamp(40px,10vw,72px);font-weight:700;letter-spacing:-.18em;line-height:.8}.wordmark .z{color:#ff6b4a}#notify-status{color:#8a8172;font-size:12px;line-height:1.6;white-space:pre-line;min-height:3.2em;letter-spacing:.02em}footer{text-align:center;padding:14px 16px calc(14px + env(safe-area-inset-bottom));color:#6b6355;font-size:12px}a,button{color:#6b6355;font:inherit;text-decoration:underline}button{appearance:none;background:none;border:0;padding:0;cursor:pointer}a:hover,button:hover{color:#d99a5b}button:focus-visible,a:focus-visible{outline:2px solid #ffb347;outline-offset:4px}button:disabled{cursor:default;text-decoration:none}</style></head>294294 <body><main><div class="landing"><div class="wordmark" aria-label="nzip">n<span class="z">z</span>ip</div><div id="notify-status" aria-live="polite"></div></div></main><footer><a href="https://args.io/cat/nzip">args</a> · <button id="pair" type="button">pair</button></footer><script>${APP_SCRIPT}</script></body></html>`;295295 }
M worker/src/serve.ts +1 -1
@@ -53,7 +53,7 @@ function publicSiteRedirect(url: string, address: string): Response {
5353 }5454 5555 function unlockForm(address: string, error?: string): string {56- return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="robots" content="noindex"><title>unlock — nzip</title>56+ return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"><meta name="robots" content="noindex"><title>unlock — nzip</title>5757 <style>*{box-sizing:border-box}body{background:#121110;color:#d6cfc2;font-family:ui-monospace,monospace;display:grid;place-items:center;min-height:100vh;min-height:100dvh;margin:0;padding:max(24px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(24px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left))}form{width:min(100%,420px);text-align:center}b{color:#ffb347;font-size:24px;display:block;margin-bottom:20px}.field{display:flex;width:100%}input{background:#1a1815;border:1px solid #2e2a24;color:#d6cfc2;min-width:0;flex:1;padding:12px 14px;font:inherit;font-size:16px;line-height:20px;outline:none;border-radius:0}input:focus{border-color:#ffb347}button{background:#ffb347;border:0;color:#121110;min-height:46px;padding:12px 18px;font:inherit;font-weight:700;cursor:pointer;border-radius:0}.e{color:#f7768e;margin-top:12px;font-size:13px}@media(max-width:360px){.field{display:grid;gap:10px}button{width:100%}}</style>5858 <form method="post" action="/${address}/__unlock"><b>&#128274; ${address}</b><div class="field"><input type="password" name="password" aria-label="password" placeholder="password" maxlength="256" autofocus autocomplete="current-password"><button>unlock</button></div>${5959 error ? `<div class="e">${error}</div>` : ""
M worker/test/index.runtime.test.ts +1 -0
M worker/test/serve.test.mjs +12 -0