@ours.network/notifications 0.1.1 → 0.3.1-nightly.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +122 -225
  2. package/dist/chat-target.d.ts +9 -0
  3. package/dist/chat-target.js +12 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +13 -445
  6. package/dist/presence.d.ts +12 -0
  7. package/dist/presence.js +55 -145
  8. package/dist/server.d.ts +26 -0
  9. package/dist/server.js +145 -0
  10. package/dist/store.d.ts +63 -0
  11. package/dist/store.js +181 -0
  12. package/dist/validation.d.ts +9 -0
  13. package/dist/validation.js +60 -0
  14. package/package.json +14 -24
  15. package/dist/config.js +0 -104
  16. package/dist/daemon.js +0 -353
  17. package/dist/http.js +0 -31
  18. package/dist/log-store.js +0 -48
  19. package/dist/provision.js +0 -29
  20. package/dist/push.js +0 -6
  21. package/dist/web-push-sender.js +0 -41
  22. package/mufl_code/20E39399689579D5AFA5132C59DEDDD328C3BCA1A74D546F2193B36585DDD283.muflo +0 -0
  23. package/mufl_code/config.mufl +0 -21
  24. package/mufl_code/core/.github/FUNDING.yml +0 -3
  25. package/mufl_code/core/.github/workflows/docs.yml +0 -39
  26. package/mufl_code/core/.github/workflows/tests.yml +0 -66
  27. package/mufl_code/core/COMMERCIAL-LICENCE.md +0 -25
  28. package/mufl_code/core/COMPATIBILITY.md +0 -221
  29. package/mufl_code/core/CONTRIBUTING.md +0 -37
  30. package/mufl_code/core/LICENSE +0 -98
  31. package/mufl_code/core/NOTIFICATIONS_CONTRACT.md +0 -290
  32. package/mufl_code/core/README.md +0 -91
  33. package/mufl_code/core/SECURITY.md +0 -40
  34. package/mufl_code/core/TRADEMARKS.md +0 -24
  35. package/mufl_code/core/a2a_capabilities.mm +0 -449
  36. package/mufl_code/core/a2a_cluster.mm +0 -601
  37. package/mufl_code/core/a2a_control.mm +0 -90
  38. package/mufl_code/core/a2a_messaging.mm +0 -4817
  39. package/mufl_code/core/a2a_monitoring.mm +0 -94
  40. package/mufl_code/core/a2a_notification_integration.mm +0 -144
  41. package/mufl_code/core/a2a_notifications.mm +0 -1127
  42. package/mufl_code/core/a2a_protocol.mm +0 -246
  43. package/mufl_code/core/a2a_versions.mm +0 -801
  44. package/mufl_code/core/config.mufl +0 -29
  45. package/mufl_code/core/docs/.vitepress/config.mts +0 -58
  46. package/mufl_code/core/docs/citations.json +0 -688
  47. package/mufl_code/core/docs/guide/01-vendor-the-core.md +0 -50
  48. package/mufl_code/core/docs/guide/02-configure-and-compile.md +0 -100
  49. package/mufl_code/core/docs/guide/03-wire-the-host.md +0 -154
  50. package/mufl_code/core/docs/guide/04-connect-and-message.md +0 -156
  51. package/mufl_code/core/docs/guide/05-test-your-app.md +0 -75
  52. package/mufl_code/core/docs/guide/index.md +0 -63
  53. package/mufl_code/core/docs/how-it-works/capabilities-and-control.md +0 -77
  54. package/mufl_code/core/docs/how-it-works/cluster.md +0 -65
  55. package/mufl_code/core/docs/how-it-works/identity.md +0 -56
  56. package/mufl_code/core/docs/how-it-works/invites-and-contacts.md +0 -76
  57. package/mufl_code/core/docs/how-it-works/messaging.md +0 -55
  58. package/mufl_code/core/docs/how-it-works/monitoring-and-config.md +0 -72
  59. package/mufl_code/core/docs/how-it-works/overview.md +0 -45
  60. package/mufl_code/core/docs/how-it-works/versioning.md +0 -116
  61. package/mufl_code/core/docs/index.md +0 -27
  62. package/mufl_code/core/docs/package-lock.json +0 -3467
  63. package/mufl_code/core/docs/package.json +0 -15
  64. package/mufl_code/core/docs/reference/contributing.md +0 -26
  65. package/mufl_code/core/docs/reference/glossary.md +0 -108
  66. package/mufl_code/core/docs/reference/implementations.md +0 -23
  67. package/mufl_code/core/docs/reference/modules.md +0 -19
  68. package/mufl_code/core/docs/scripts/check-citations.mjs +0 -36
  69. package/mufl_code/core/docs/scripts/check-denylist.mjs +0 -30
  70. package/mufl_code/core/docs/scripts/gen-llms.mjs +0 -32
  71. package/mufl_code/core/docs/workflows/cluster.md +0 -89
  72. package/mufl_code/core/docs/workflows/contact-exchange.md +0 -60
  73. package/mufl_code/core/docs/workflows/contact-restore.md +0 -60
  74. package/mufl_code/core/docs/workflows/control-verbs.md +0 -70
  75. package/mufl_code/core/docs/workflows/index.md +0 -57
  76. package/mufl_code/core/docs/workflows/introductions.md +0 -60
  77. package/mufl_code/core/docs/workflows/messaging.md +0 -75
  78. package/mufl_code/core/docs/workflows/monitoring.md +0 -84
  79. package/mufl_code/core/docs/workflows/notifications.md +0 -109
  80. package/mufl_code/core/release-notes/0.1.md +0 -60
  81. package/mufl_code/core/release-notes/0.3.md +0 -22
  82. package/mufl_code/core/release-notes/0.4.md +0 -97
  83. package/mufl_code/core/tests/README.md +0 -83
  84. package/mufl_code/core/tests/compat/README.md +0 -82
  85. package/mufl_code/core/tests/compat/compat.mjs +0 -245
  86. package/mufl_code/core/tests/compat/compat_peer.mjs +0 -146
  87. package/mufl_code/core/tests/compat/run_compat.sh +0 -96
  88. package/mufl_code/core/tests/corpus.mjs +0 -139
  89. package/mufl_code/core/tests/corpus_actor.mu +0 -242
  90. package/mufl_code/core/tests/fatcompile_actor.mu +0 -52
  91. package/mufl_code/core/tests/mig.mjs +0 -805
  92. package/mufl_code/core/tests/mig_actor.mu +0 -518
  93. package/mufl_code/core/tests/migapp.mjs +0 -431
  94. package/mufl_code/core/tests/migapp_actor.mu +0 -294
  95. package/mufl_code/core/tests/mufl_semantics/run.sh +0 -69
  96. package/mufl_code/core/tests/mufl_semantics/t11_three_version.mufl +0 -47
  97. package/mufl_code/core/tests/mufl_semantics/t2_extra_fields.mufl +0 -11
  98. package/mufl_code/core/tests/mufl_semantics/t7_runtime.mufl +0 -30
  99. package/mufl_code/core/tests/mufl_semantics/t9_passthrough.mufl +0 -19
  100. package/mufl_code/core/tests/mufl_semantics/t_str_order.mufl +0 -19
  101. package/mufl_code/core/tests/notif.mjs +0 -755
  102. package/mufl_code/core/tests/notif_actor.mu +0 -450
  103. package/mufl_code/core/tests/protocol_container.mm +0 -18
  104. package/mufl_code/core/tests/run.sh +0 -65
  105. package/mufl_code/core/tests/run_corpus.sh +0 -51
  106. package/mufl_code/core/tests/run_fatcompile.sh +0 -40
  107. package/mufl_code/core/tests/run_mig.sh +0 -51
  108. package/mufl_code/core/tests/run_migapp.sh +0 -51
  109. package/mufl_code/core/tests/run_notif.sh +0 -57
  110. package/mufl_code/core/tests/test.mjs +0 -750
  111. package/mufl_code/core/tests/test_actor.mu +0 -564
  112. package/mufl_code/core/tests/test_common.mjs +0 -93
  113. package/mufl_code/core/version.mm +0 -24
  114. package/mufl_code/packet.mu +0 -170
  115. package/mufl_code/protocol_container.mm +0 -18
  116. package/scripts/gen-vapid.mjs +0 -16
package/README.md CHANGED
@@ -1,231 +1,128 @@
1
- # ours-notifications
2
-
3
- **Reach a person — or their agent — even when their app, tab, or daemon is
4
- closed.** Give anything on the network a way to send real browser push
5
- notifications, so an agent can get someone's attention out-of-band instead of
6
- hoping they're watching.
7
-
8
- The ours.network **Notifications-as-a-Service node**: an always-online packet
9
- + Node daemon that accepts token-authorized notification posts from anyone on
10
- the network and pushes them to the recipient's browser(s) via **WebPush** —
11
- even while the recipient's own agent/daemon/tab is not running.
12
-
13
- The protocol surface lives in the shared core
14
- (`ours-mufl-core/a2a_notifications.mm`, contract:
15
- `ours-mufl-core/NOTIFICATIONS_CONTRACT.md`). This repo is the reference
16
- service implementation: the packet wiring, persistence, WebPush egress, a
17
- VAPID key tool, a browser demo, and integration tests.
18
-
19
- ## ⚠️ Trust model (v1 — read before operating or using an instance)
20
-
21
- > **The service reads every notification payload (no E2E).** Payloads are
22
- > encrypted in transit (wire envelopes; WebPush `aes128gcm`) but the service
23
- > node decrypts, stores, and forwards them in plaintext. Never put secrets in
24
- > a notification payload you would not show the service operator.
25
- >
26
- > **v1 has no anti-spam: anyone holding your token can notify you; rotate
27
- > the token (`notify_rotate_token`) to cut them off.** Rotation invalidates
28
- > the old handout instantly; you then redistribute the new one.
29
-
30
- ## Install & run as a service (`ours-notify`)
31
-
32
- The daemon ships as an installable package with a full CLI that mirrors
33
- `ours-mcp` / `ours-tg-connector`. It self-provisions its identity (`identity.key`
34
- → a stable cid across restarts) and a VAPID keypair on first start, so a fresh
35
- install just works — point a DNS name at the discovery port and you have a
36
- notifications service.
37
-
38
- ```sh
39
- npm i -g @ours.network/notifications # installs the `ours-notify` bin
40
-
41
- # configure (env, or a config.json — see below). BROKER_URL is the only required field.
42
- export BROKER_URL=ws://broker.example.net:9000 # your ADAPT broker
43
- export STATE_DIR=~/.ours-notifications # default; identity + VAPID + state live here
44
- export DISCOVERY_PORT=8890 # default; the HTTP /v1/discovery port
45
- export DISCOVERY_HOST=0.0.0.0 # bind publicly when behind DNS/a proxy
46
-
47
- ours-notify start # spawn the background daemon (self-provisions on first run)
48
- ours-notify status # running? port, cid, broker, state dir, version
49
- ours-notify stop # graceful stop
50
- ours-notify restart
51
-
52
- # boot-persistent service (systemd user unit + linger on Linux; launchd on macOS):
53
- ours-notify install-service # bakes the resolved config into the unit, enables + starts it
54
- ours-notify uninstall-service
55
- ```
56
-
57
- Point DNS at the host and expose `DISCOVERY_PORT`: clients fetch the service cid
58
- + VAPID public key from `https://<your-domain>/v1/discovery` at runtime, so a
59
- client (e.g. the messenger panel) needs no rebuild when you deploy or rotate.
60
-
61
- **Config precedence (per field): env > config.json > default.** `config.json`
62
- lives at `$OURS_NOTIFY_CONFIG`, else `<HOME>/.ours-notifications/config.json`:
63
-
64
- ```json
65
- { "brokerUrl": "ws://broker.example.net:9000", "discoveryPort": 8890, "discoveryHost": "0.0.0.0" }
66
- ```
1
+ # Ours Notifications
67
2
 
68
- ## Self-hosting
3
+ Dedicated web-push delivery and a persistent notification inbox. Fleet and
4
+ Messenger submit events with producer credentials. Browsers register devices
5
+ and share unread/read state through their authenticated Fleet gateway. Native
6
+ Android/iOS transports are outside this package.
69
7
 
70
- You can run this service yourself and point the ours control panel at it — the
71
- panel's default stays the hosted `notify.ours.network`, self-hosting is the
72
- opt-out. Full walkthrough (VPS → TLS → verify → panel bind):
73
- **[docs/SELF-HOSTING.md](docs/SELF-HOSTING.md)**.
8
+ ## Run
74
9
 
75
- ## Architecture
10
+ Requires Node 22 or later. Build with `npm ci && npm run build`, then run
11
+ `node dist/cli.js /absolute/path/private-config.json`. The config must have mode
12
+ 0600. The state directory must be private and backed up together with the VAPID
13
+ keys. Generate keys once with `web-push.generateVAPIDKeys()`; rotating them
14
+ requires each browser to subscribe again.
76
15
 
16
+ ```json
17
+ {
18
+ "host": "127.0.0.1",
19
+ "port": 49677,
20
+ "stateFile": "/private/notifications/state.json",
21
+ "users": [{ "userId": "stable-owner-account", "token": "REPLACE_WITH_RANDOM_USER_TOKEN_AT_LEAST_32_CHARS" }],
22
+ "producers": [
23
+ { "userId": "stable-owner-account", "source": "fleet", "token": "REPLACE_WITH_RANDOM_FLEET_TOKEN_AT_LEAST_32_CHARS" },
24
+ { "userId": "stable-owner-account", "source": "messenger", "token": "REPLACE_WITH_RANDOM_MESSENGER_TOKEN_AT_LEAST_32_CHARS" }
25
+ ],
26
+ "vapid": { "subject": "mailto:operator@example.com", "publicKey": "GENERATED_PUBLIC_KEY", "privateKey": "GENERATED_PRIVATE_KEY" }
27
+ }
77
28
  ```
78
- sender S ──bare signed send (token authz)──▶ ┌────────────────────────────┐
79
- │ service packet (MUFL) │
80
- recipient R ◀─encrypted channel (contacts)─▶ │ a2a_notifications service │
81
- register / bindings / rotate / mark_read │ half: validate + resolve │
82
- └───────────┬────────────────┘
83
- │ $notify_agent events
84
- ▼
85
- ┌────────────────────────────┐
86
- │ daemon (Node, this repo) │
87
- │ notifications.json log │
88
- │ web-push VAPID egress ────┼──▶ browser push services
89
- └────────────────────────────┘
90
- ```
91
-
92
- - The packet validates registrations/tokens and hands
93
- `(notification, bindings)` to the daemon; storage and HTTP stay app-side.
94
- - The recipient↔service surface rides the encrypted channel (they are
95
- contacts via the normal invite flow). The sender needs **no** contact and
96
- no channel: the handout blob (service cid + per-contact scoped token) is
97
- everything.
98
- - Identity + state survive restarts: `identity.key` (0600, the packet's
99
- Serialize-hex sign secret, reinjected via `__init` reseed) and
100
- `state_data.bin` (written on every `$save_state`).
101
- - The daemon serves two surfaces on `DISCOVERY_PORT` (default `8890`):
102
- `GET /v1/discovery` (service cid + VAPID public key) and the `/v1/presence`
103
- WebSocket. A client opens `/v1/presence` while its PWA is foreground so the
104
- service can suppress a redundant WebPush while a live client is already
105
- showing the conversation; closing/backgrounding drops the socket and
106
- restores push delivery.
107
-
108
- > **Must sit behind TLS.** `/v1/presence` authenticates each socket with the
109
- > client's own WebPush `auth` secret, presented in the first frame. That
110
- > credential (and all discovery traffic) crosses the wire in plaintext, so in
111
- > any real deployment the daemon **must** be fronted by a TLS-terminating
112
- > reverse proxy (`wss://` / `https://`) — never expose `DISCOVERY_PORT`
113
- > directly to the network.
114
-
115
- ## Environment
116
-
117
- | var | meaning |
118
- |---|---|
119
- | `BROKER_URL` | ADAPT broker websocket, e.g. `ws://127.0.0.1:9000` (**required**; env or config.json) |
120
- | `STATE_DIR` | daemon state: `identity.key`, `state_data.bin`, `notifications.json`, `vapid.*` (default `<HOME>/.ours-notifications`) |
121
- | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | inline VAPID keypair (or `*_FILE` variants). **Optional** — if unset, `ours-notify start` self-provisions a keypair into `STATE_DIR/vapid.{public,private}`. The private key never enters packet state. |
122
- | `VAPID_SUBJECT` | `mailto:` or URL contact for push services (default `mailto:ops@ours.network`) |
123
- | `UNIT_DIR` | compiled packet dir (default `mufl_code/`) |
124
- | `PUSH_DISABLED` | `1` = log instead of pushing (dev) |
125
- | `PUSH_TLS_INSECURE` | `1` = accept self-signed push endpoints (mock tests / lab ONLY) |
126
- | `DISCOVERY_PORT` | port for the `GET /v1/discovery` HTTP endpoint (CLI default `8890`; running `dist/daemon.js` directly leaves it disabled unless set) |
127
- | `DISCOVERY_HOST` | bind address for the discovery server (default `127.0.0.1`; use `0.0.0.0` behind DNS/a proxy) |
128
- | `OURS_NOTIFY_CONFIG` | path to `config.json` (default `<HOME>/.ours-notifications/config.json`) |
129
-
130
- ## Run (local or any VPS — identical steps; WebPush needs no vendor accounts)
131
-
132
- ```sh
133
- npm install
134
- git submodule update --init
135
- ADAPT_TOOLKIT=/path/to/adapt ./scripts/compile-mufl.sh
136
- node scripts/gen-vapid.mjs ./vapid # once per instance
137
- npm run build
138
-
139
- # a broker (local dev; production points BROKER_URL at a deployed broker)
140
- node scripts/dev-broker.mjs --host 127.0.0.1 --port 9000 --test_mode &
141
-
142
- BROKER_URL=ws://127.0.0.1:9000 STATE_DIR=./state \
143
- VAPID_PUBLIC_KEY=$(cat vapid/vapid.public) \
144
- VAPID_PRIVATE_KEY=$(cat vapid/vapid.private) \
145
- node dist/daemon.js
146
- ```
147
-
148
- The daemon speaks JSON-lines on stdin/stdout:
149
- `{"cmd":"invite","name":"alice"}` → invite blob (hex) for a recipient;
150
- `{"cmd":"contacts"}`, `{"cmd":"notifications"}`, `{"cmd":"status"}`,
151
- `{"cmd":"export_vis"}`.
152
-
153
- ## Browser demo (the human-visible proof)
154
-
155
- 1. Run broker + daemon (above), then `node demo/serve.mjs 8080`.
156
- 2. Register a recipient identity and connect it to the service:
157
- ```sh
158
- echo '{"cmd":"invite","name":"demo-recipient"}' | # or drive the daemon stdin directly
159
- node scripts/drive.mjs register --broker ws://127.0.0.1:9000 --state ./demo-recipient \
160
- --invite <invite-hex-file>
161
- ```
162
- 3. Open `http://localhost:8080/`, paste `vapid/vapid.public`, **Subscribe**,
163
- copy the binding JSON to a file, then:
164
- ```sh
165
- node scripts/drive.mjs update-bindings --broker ws://127.0.0.1:9000 \
166
- --state ./demo-recipient --binding binding.json
167
- node scripts/drive.mjs export-address --broker ws://127.0.0.1:9000 \
168
- --state ./demo-recipient --out handout.hex
169
- ```
170
- 4. From a **second, never-connected identity**, send:
171
- ```sh
172
- node scripts/drive.mjs send --broker ws://127.0.0.1:9000 --state ./demo-sender \
173
- --address handout.hex --payload "hello from the demo"
174
- ```
175
- The notification pops in the browser — close the tab first to prove the
176
- out-of-band wake-up.
177
-
178
- ## Tests
179
-
180
- ```sh
181
- npm test # daemon boot/persistence/VAPID hygiene + mock-push e2e (Flow A→B→C + rejections)
182
- ```
183
-
184
- The e2e test runs the full flow against a dev broker and a local HTTPS mock
185
- push endpoint: register → browser-style subscription keys → non-contact
186
- sender post → ONE `aes128gcm` WebPush POST with VAPID headers →
187
- `notifications.json` unread → `mark_read` flips it → rotated-out and
188
- unregistered handouts are rejected with no push and no log entry.
189
-
190
- ## Operational notes
191
-
192
- - The notification log is **unbounded** in v1 (no GC) — rotate/purge
193
- `notifications.json` operationally if needed.
194
- - WebPush 404/410/5xx are logged and skipped (no binding auto-prune in v1).
195
- - Duplicate posts are delivered twice (no dedup in v1).
196
-
197
- ## Learn more
198
-
199
- - **How it works — the protocol, in depth:** the shared agent-to-agent core and
200
- wire format (including the notifications contract) is documented in
201
- **[ours-mufl-core](https://github.com/adapt-toolkit/ours-mufl-core)**.
202
- - **The whole project:** [ours.network](https://ours.network) ·
203
- [umbrella repo](https://github.com/adapt-toolkit/ours-network)
204
-
205
- ## Support ours.network
206
-
207
- ours.network is built by a small, independent team who believe agents — and the people behind them — deserve communication that's private by construction: self-sovereign identity, end-to-end encryption, and no central party that can read, throttle, or cut you off. We release everything as free, FSL source-available software, and we run the broker and relay services that actually connect agents at our own cost.
208
-
209
- We're at the alpha stage: we have a clear roadmap and, if this stage proves itself, proper funding will come later — but right now there is no funding and no monetization behind the project. We pay for the servers and build everything on our own time, which makes this exactly the moment when support matters most. Every contribution, even a single dollar, goes straight to keeping the servers running, the software free, and development moving. If ours.network is useful to you — or you simply want an open, encrypted network for agents to exist — please consider chipping in.
210
-
211
- **Like it? Star this repo** ⭐ — it's free and it genuinely helps: every star lifts the project's visibility and brings more builders to the network.
212
-
213
- **→ https://github.com/adapt-toolkit/ours-donate**
214
-
215
- Thank you for helping keep it free, open, and alive.
216
-
217
- ## Licence, status & warranty
218
-
219
- > **Alpha software.** ours-notifications is part of **ours.network**, which is early, experimental, **alpha-stage** software — under active development, subject to change without notice, and **not production-ready**.
220
-
221
- > **No warranty / not security-audited.** ours.network has **not** been independently security-audited. It is provided **"as is", without warranty of any kind**, and you use it **at your own risk**. See [`LICENSE`](./LICENSE) and [`SECURITY.md`](./SECURITY.md).
222
-
223
- **ours.network** is owned and licensed by **Adapt Framework Solutions Ltd**. It is released under the **Functional Source License, Version 1.1 ([FSL-1.1-Apache-2.0](./LICENSE))** — **source-available, not open source** during the FSL period. Each release **converts to Apache 2.0 two years after it is published**.
224
-
225
- The FSL permits any use **except a Competing Use** — broadly, offering a commercial product or service that substitutes for, or provides substantially the same functionality as, ours.network. Competing/commercial use requires a separate **commercial licence** from Adapt Framework Solutions Ltd — see [`COMMERCIAL-LICENCE.md`](./COMMERCIAL-LICENCE.md) (contact: **license@adaptframework.solutions**).
226
-
227
- **Built on Adapt.** ours.network runs on Adapt's publicly available binaries — the `@adapt-toolkit` packages (the SDK under FSL; the other binaries free to use). Adapt's own source code (its C++ core) is not open yet — it will be opened later.
228
-
229
- Third-party notices: [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md).
230
29
 
231
- Copyright 2026 Adapt Framework Solutions Ltd.
30
+ Each credential fixes its user principal; each producer also fixes its source.
31
+ User IDs and sources in producer request bodies are rejected. Keep separate
32
+ producer and user credentials, never deliver any credential to browser code.
33
+ Fleet's password console currently represents one owner account: all trusted
34
+ browser sessions use the same configured stable user token. Separate user
35
+ accounts require separate trusted gateway/account mappings; arbitrary browser
36
+ cookies or claimed IDs cannot create one.
37
+
38
+ Configure Fleet's generic prefix gateway with `/notifications`, service origin,
39
+ `headers: { "authorization": "Bearer USER_TOKEN" }`, and
40
+ `stripBrowserContext: true`. Fleet checks the browser session, exact origin and
41
+ mutation CSRF before stripping cookies/browser headers and injecting its user
42
+ credential. This user token cannot call the producer API. Expose only the
43
+ gateway publicly; the notification listener rejects direct browser context.
44
+
45
+ ## API
46
+
47
+ All operations require a credential in `Authorization: Bearer TOKEN`. JSON
48
+ mutations require `Content-Type: application/json`, with a 16 KiB body limit.
49
+ Responses are never cached. User and producer credentials have separate scopes.
50
+
51
+ | Method/path | Credential | Request/result |
52
+ | --- | --- | --- |
53
+ | GET `/api/v1/push/config` | User | Public VAPID key |
54
+ | POST `/api/v1/devices` | User | `{ "subscription": PushSubscriptionJSON }` → device `id` |
55
+ | POST `/api/v1/devices/delete` | User | `{ "id": "device-id" }`, only that user's device |
56
+ | GET `/api/v1/devices/status?id=...` | User | `{ "active": true }` |
57
+ | GET `/api/v1/inbox?limit=50&before=...` | User | Items, shared unread count, revision, next cursor |
58
+ | GET `/api/v1/summary` | User | Shared unread count, revision, and per-chat source/identity/count |
59
+ | POST `/api/v1/read-target` | User | `{ "url": "/fleet/chats?chat=agent", "revision": OBSERVED_REVISION }`, reads only this chat up to the revision captured before its history load |
60
+ | POST `/api/v1/read` | User | `{ "ids": ["notification-id"] }`, idempotent shared read |
61
+ | POST `/api/v1/send` | Producer | `{ "eventId": "stable-event-id", "title": "Agent completed", "body": "Open the chat", "url": "/fleet/chats?chat=agent&detail=1" }` |
62
+
63
+ Inbox admission and queued delivery are one fsynced atomic snapshot. The service
64
+ deduplicates by user/source/event ID; replay must retain identical title/body/URL
65
+ or it receives 409. Inbox entries survive failed pushes and service restart.
66
+ Reading stops remaining queued deliveries. Device endpoints cannot transfer
67
+ between users, and only supported HTTPS push-provider hosts and valid P-256
68
+ subscription keys are accepted. Navigation stays within `/fleet`.
69
+
70
+ The service holds an exclusive state-file lock. Run one writer per state file.
71
+ After a crash, inspect the lock PID and prove no service is using this state
72
+ before manually removing its `.lock`; no automatic stale-lock deletion occurs.
73
+ Malformed state fails closed. Restore a known backup rather than resetting it.
74
+ Provider 404/410 removes the expired binding; other failures retry with capped
75
+ exponential backoff (eight attempts). The inbox remains after delivery exhaustion.
76
+ Limits are 32 devices/user, 10,000 inbox entries/user, 20,000 queued deliveries,
77
+ 180 user API requests/minute and 600 producer requests/minute. Capacity failure
78
+ returns 503 so durable producers can retain/retry events. This first version
79
+ requires operator archival/reset after inbox capacity is reached.
80
+
81
+ ## Producers and PWA
82
+
83
+ Fleet web and Messenger use `OURS_NOTIFICATIONS_ORIGIN` and their distinct
84
+ `OURS_NOTIFICATIONS_PRODUCER_TOKEN`. Fleet web observes durable supervisor
85
+ ledgers incrementally without restarting agents or attaching a controller.
86
+ Owner browser/channel prompt completions notify; monitor/startup turns do not.
87
+ Existing history is skipped at first install. Persist Fleet web state to retain
88
+ offsets and pending prompt correlation. Messenger uses its canonical incoming
89
+ message/file watcher and persists payloads before sending. Both outboxes retry
90
+ without requiring a locally registered push binding.
91
+
92
+ In ours-app, open Settings → Notifications → Enable notifications. Permission
93
+ is requested from that click. The service worker subscribes this device, repairs
94
+ its registration on login/focus when enabled, and opens in-app targets. Disable
95
+ or sign out revokes this device only. The Notifications launcher opens the
96
+ backend inbox; marking a record read synchronizes with other sessions on their
97
+ next visible refresh (five seconds). Inbox polling is paginated and does not
98
+ load chat transcripts.
99
+
100
+ On iPhone/iPad, install the HTTPS app on the Home Screen and open the installed
101
+ app before enabling push; WebKit documents this requirement and the explicit
102
+ interaction for permission in its [Web Push announcement](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/).
103
+ Automated browser permission/subscription fixtures cannot establish physical
104
+ phone push delivery; test a real installed device separately.
105
+
106
+ ## Release channels
107
+
108
+ Repository history is preserved. The legacy MUFL implementation is replaced by
109
+ this service, with community/legal files from `ours-shared`. Version 0.2.0 marks
110
+ the new service API; the previous package API is incompatible.
111
+
112
+ CI gates typecheck, build, tests and pack on PRs to `main`/`prerelease`. Only a
113
+ push to `main` may bump a stable version using conventional commit subjects and
114
+ publish `latest`. Only a push to `prerelease` may calculate an ephemeral
115
+ patch-base `-nightly.N` version and publish `nightly`; the nightly version is
116
+ never committed. Guard scripts refuse branch/version/tag mismatches. Publication
117
+ needs the same NPM/version-bump app secrets as sibling repositories. Opening a
118
+ feature PR performs no publication or merge. Run `npm test` for local gates.
119
+
120
+ Foreground presence uses the authenticated gateway WebSocket at
121
+ `/api/v1/presence`. Its first frame is `{type:"hello",id,endpoint,auth}`;
122
+ `id` must be an active device owned by the credential's user, with matching
123
+ subscription endpoint and auth key. Visible clients send `{type:"heartbeat"}`
124
+ every 15 seconds. Transport ping/pong and a 45-second application freshness
125
+ limit remove frozen/disconnected clients. Only that foreground device's push
126
+ is suppressed; its inbox item remains unread until explicitly marked read.
127
+ Other devices still receive push. `inbox.changed` invalidates account-wide inbox
128
+ views after producer admission or reads. Presence is ephemeral across restart.
@@ -0,0 +1,9 @@
1
+ /** Stable conversation identity, independent of list filters and mobile detail state. */
2
+ export declare function chatTarget(path: string): {
3
+ source: 'work' | 'messenger';
4
+ chat: string;
5
+ } | null;
6
+ export declare function chatKey(value: {
7
+ source: string;
8
+ chat: string;
9
+ }): string;
@@ -0,0 +1,12 @@
1
+ /** Stable conversation identity, independent of list filters and mobile detail state. */
2
+ export function chatTarget(path) {
3
+ const url = new URL(path, 'https://ours.invalid');
4
+ if (url.pathname === '/fleet/chats') {
5
+ const chat = url.searchParams.get('chat');
6
+ if (!chat || chat.length > 512)
7
+ return null;
8
+ return { source: url.searchParams.get('source') === 'messenger' ? 'messenger' : 'work', chat };
9
+ }
10
+ return null;
11
+ }
12
+ export function chatKey(value) { return JSON.stringify([value.source, value.chat]); }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};