| index e00e5fd..d8ede7f 100644 |
| --- a/SKILL.md |
| +++ b/SKILL.md |
| @@ -22,8 +22,9 @@ service worker's lifecycle are the app's. |
| 3. `svc, err := aviso.New(aviso.Config{DB: writer, PrivateKey: key, |
| Contact: "mailto:ops@example", Origin: origin})`. One per process; |
| the send-concurrency bound lives on it. `Origin` is exactly |
| - `scheme://host[:port]`, no path, no trailing slash — CSRF compares |
| - the browser's Origin header to it byte for byte. |
| + `scheme://host[:port]`, no path, no trailing slash — when a browser |
| + sends no Sec-Fetch-Site, CSRF compares its Origin or Referer origin |
| + to this string byte for byte. |
| 4. Mount, behind your session middleware: |
| `GET /aviso/public-key → svc.PublicKey`, `POST /aviso/subscribe → |
| svc.Subscribe`, `POST /aviso/unsubscribe → svc.Unsubscribe`. |
| @@ -31,8 +32,11 @@ service worker's lifecycle are the app's. |
| 409 means the endpoint is another account's, or the browser |
| subscribed under a different server key. |
| 5. Serve `aviso.JS()` as `/static/aviso/push.mjs` and `aviso.WorkerJS()` |
| - as `/static/aviso/aviso-sw.js`; serve your own `sw.js` at the scope |
| - it should control with `Cache-Control: no-cache`. |
| + as `/static/aviso/aviso-sw.js`, both with |
| + `Content-Type: text/javascript` — they are bytes, not handlers, and |
| + Go's default `text/plain` makes a browser refuse both a module and |
| + a worker script. Serve your own `sw.js` the same way, at the scope |
| + it should control, with `Cache-Control: no-cache`. |
| |
| ## Send |
| |
| @@ -56,19 +60,23 @@ import { enable, reconcile, disable, capabilities } from "/static/aviso/push.mjs |
| const post = (path) => (b) => fetch(path, { method: "POST", credentials: "same-origin", |
| headers: { "Content-Type": "application/json" }, body: JSON.stringify(b) }); |
| const save = post("/aviso/subscribe"), remove = post("/aviso/unsubscribe"); |
| -const registration = await navigator.serviceWorker.register("/sw.js"); |
| +await navigator.serviceWorker.register("/sw.js"); |
| +const registration = await navigator.serviceWorker.ready; // active, not merely registered |
| const { publicKey } = await (await fetch("/aviso/public-key")).json(); |
| -await reconcile({ registration, publicKey, save }); // every load; never prompts |
| +await reconcile({ registration, publicKey, save }).catch(console.warn); // every load; never prompts |
| button.onclick = () => enable({ registration, publicKey, save }); // from the click; prompts |
| ``` |
| |
| -`enable` must be called synchronously from the click handler — it |
| -prompts before its first await, and a prompt after an await is denied. |
| -It resolves null when denied. `disable({registration, remove})` |
| -removes the server row first, then the browser subscription; call it |
| -before sign-out. `save`/`remove` may resolve to nothing; a rejection |
| -or an `{ok: false}` return counts as failure. `capabilities()` tells |
| -you whether to show the enable button and the Home Screen coaching. |
| +`ready` matters: `subscribe()` on a registration whose worker is still |
| +installing rejects with InvalidStateError. `enable` must be called |
| +synchronously from the click handler — it prompts before its first |
| +await, and a prompt after an await is denied. It resolves null when |
| +denied. `disable({registration, remove})` removes the server row |
| +first, then the browser subscription; call it before sign-out. |
| +`save`/`remove` may resolve to nothing; a rejection or an `{ok: false}` |
| +return counts as failure. `capabilities()` says whether to show the |
| +enable button (`push`) and, on iOS, the Home Screen coaching |
| +(`standalone` false). |
| |
| ## Worker (your sw.js) |
| |
| @@ -105,9 +113,10 @@ account deletion. Re-check entitlement before every `SendTo`. |
| |
| ## Installability |
| |
| -iOS delivers push only to a Home Screen app, and only after a tap in |
| -the installed copy. The manifest, head tags and coaching are yours: |
| -see `docs/installable.md`. |
| +Android and desktop browsers deliver push to an ordinary website. iOS |
| +and iPadOS deliver it only to a Home Screen app, and only after a tap |
| +in the installed copy. The manifest, head tags and iOS coaching are |
| +yours: see `docs/installable.md`. |
| |
| ## Rulings |
| |