@ours.network/notifications 0.1.1 → 0.3.0

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/dist/cli.js CHANGED
@@ -1,446 +1,14 @@
1
1
  #!/usr/bin/env node
2
- // ours-notify — CLI + lifecycle for the ours.network notifications daemon.
3
- // Mirrors ours-mcp / ours-tg-connector: pidfile-based start/stop/restart/status,
4
- // a foreground `serve`, and install-service (systemd user unit + linger on Linux,
5
- // launchd on macOS) that bakes the resolved config into the unit. The daemon's
6
- // protocol logic (WebPush egress, scoped-token validation, registrations,
7
- // discovery) is unchanged — this only wraps it in a proper service skeleton.
8
- //
9
- // ours-notify start | stop | restart | status | serve
10
- // ours-notify install-service | uninstall-service | version | help
11
- import * as fs from 'node:fs';
12
- import { spawn, spawnSync } from 'node:child_process';
13
- import { connect } from 'node:net';
14
- import { createInterface } from 'node:readline';
15
- import { join, dirname } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
- import { homedir, userInfo } from 'node:os';
18
- import { resolveConfig, loadConfig } from './config.js';
19
- import { provision } from './provision.js';
20
- // NOTE: ./daemon.js (which loads the native ADAPT SDK) is imported lazily inside
21
- // cmdServe so help/version/status/stop stay light and don't spin up the SDK.
22
- const DEFAULT_DISCOVERY_PORT = 8890;
23
- const SELF = fileURLToPath(import.meta.url);
24
- const out = (...p) => process.stdout.write(`${p.join(' ')}\n`);
25
- const err = (...p) => process.stderr.write(`${p.join(' ')}\n`);
26
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
27
- function cliVersion() {
28
- try {
29
- const pkg = JSON.parse(fs.readFileSync(join(dirname(SELF), '..', 'package.json'), 'utf8'));
30
- return String(pkg.version ?? '0.0.0-dev');
31
- }
32
- catch {
33
- return '0.0.0-dev';
34
- }
35
- }
36
- // Resolved once — the port the CLI operates on (config port, else product default).
37
- const R0 = resolveConfig();
38
- const STATE_DIR = R0.stateDir;
39
- const PORT = R0.discoveryPort ?? DEFAULT_DISCOVERY_PORT;
40
- const PID_PATH = join(STATE_DIR, 'daemon.pid');
41
- const LOG_PATH = join(STATE_DIR, 'daemon.log');
42
- function readPid() {
43
- try {
44
- const n = parseInt(fs.readFileSync(PID_PATH, 'utf8').trim(), 10);
45
- return Number.isFinite(n) ? n : null;
46
- }
47
- catch {
48
- return null;
49
- }
50
- }
51
- function isAlive(pid) {
52
- try {
53
- process.kill(pid, 0);
54
- return true;
55
- }
56
- catch {
57
- return false;
58
- }
59
- }
60
- function runningPid() {
61
- const pid = readPid();
62
- if (pid && isAlive(pid))
63
- return pid;
64
- if (pid) {
65
- try {
66
- fs.rmSync(PID_PATH, { force: true });
67
- }
68
- catch { /* ignore */ }
69
- }
70
- return null;
71
- }
72
- function portOpen(port, timeoutMs = 1000) {
73
- return new Promise((res) => {
74
- const sock = connect({ host: '127.0.0.1', port });
75
- const done = (ok) => { sock.destroy(); res(ok); };
76
- sock.setTimeout(timeoutMs);
77
- sock.once('connect', () => done(true));
78
- sock.once('timeout', () => done(false));
79
- sock.once('error', () => done(false));
80
- });
81
- }
82
- async function waitForPort(port, totalMs = 30_000) {
83
- const deadline = Date.now() + totalMs;
84
- while (Date.now() < deadline) {
85
- if (await portOpen(port))
86
- return true;
87
- await sleep(400);
88
- }
89
- return false;
90
- }
91
- async function fetchDiscovery() {
92
- try {
93
- const ctrl = new AbortController();
94
- const t = setTimeout(() => ctrl.abort(), 1500);
95
- const resp = await fetch(`http://127.0.0.1:${PORT}/v1/discovery`, { signal: ctrl.signal });
96
- clearTimeout(t);
97
- if (resp.ok)
98
- return (await resp.json());
99
- }
100
- catch { /* unreachable */ }
101
- return null;
102
- }
103
- // ---- serve (foreground) -----------------------------------------------------
104
- async function cmdServe() {
105
- const r = resolveConfig();
106
- if (!r.brokerUrl) {
107
- err('BROKER_URL is required (env BROKER_URL or config.json "brokerUrl").');
108
- process.exit(1);
109
- }
110
- provision(r); // mkdir state dir + generate VAPID if absent (identity.key self-provisions in startDaemon)
111
- if (r.discoveryPort === undefined && !process.env.DISCOVERY_PORT) {
112
- process.env.DISCOVERY_PORT = String(DEFAULT_DISCOVERY_PORT); // product default
113
- }
114
- const cfg = loadConfig();
115
- const { startDaemon } = await import('./daemon.js');
116
- const daemon = await startDaemon(cfg);
117
- const emit = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
118
- emit({ event: 'ready', cid: daemon.cid, version: daemon.version, port: cfg.discoveryPort, stateDir: cfg.stateDir });
119
- daemon.onEvent((e) => emit(e));
120
- // Optional JSON-lines control on stdin (handy for `serve` in the foreground).
121
- const rl = createInterface({ input: process.stdin });
122
- rl.on('line', (line) => {
123
- void (async () => {
124
- let msg;
125
- try {
126
- msg = JSON.parse(line);
127
- }
128
- catch {
129
- return;
130
- }
131
- try {
132
- if (msg.cmd === 'invite')
133
- emit({ reply: await daemon.invite(msg.name ?? '') });
134
- else if (msg.cmd === 'contacts')
135
- emit({ reply: daemon.contacts() });
136
- else if (msg.cmd === 'status')
137
- emit({ reply: { cid: daemon.cid, version: daemon.version } });
138
- else if (msg.cmd === 'notifications')
139
- emit({ reply: daemon.logStore.list() });
140
- else
141
- emit({ reply: { error: `unknown cmd ${msg.cmd}` } });
142
- }
143
- catch (e) {
144
- emit({ reply: { error: String(e) } });
145
- }
146
- })();
147
- });
148
- const shutdown = () => { void daemon.stop().then(() => process.exit(0)); };
149
- process.on('SIGTERM', shutdown);
150
- process.on('SIGINT', shutdown);
151
- }
152
- // ---- start / stop / restart / status ---------------------------------------
153
- async function cmdStart() {
154
- const existing = runningPid();
155
- if (existing) {
156
- out(`ours-notify is already running (pid ${existing}, port ${PORT}).`);
157
- return;
158
- }
159
- const r = resolveConfig();
160
- if (!r.brokerUrl) {
161
- err('BROKER_URL is required (env BROKER_URL or config.json "brokerUrl"), e.g. ws://127.0.0.1:9000');
162
- process.exit(1);
163
- }
164
- fs.mkdirSync(STATE_DIR, { recursive: true });
165
- const logFd = fs.openSync(LOG_PATH, 'a');
166
- const child = spawn(process.execPath, [SELF, 'serve'], {
167
- detached: true,
168
- stdio: ['ignore', logFd, logFd],
169
- env: {
170
- ...process.env,
171
- BROKER_URL: r.brokerUrl,
172
- STATE_DIR,
173
- DISCOVERY_PORT: String(PORT),
174
- DISCOVERY_HOST: r.discoveryHost,
175
- },
176
- });
177
- child.unref();
178
- if (!child.pid) {
179
- err('failed to spawn the daemon.');
180
- process.exit(1);
181
- }
182
- fs.writeFileSync(PID_PATH, String(child.pid));
183
- out(`starting ours-notify (pid ${child.pid})…`);
184
- if (await waitForPort(PORT)) {
185
- const d = await fetchDiscovery();
186
- out(`ours-notify is up on http://${r.discoveryHost}:${PORT}/v1/discovery`);
187
- if (d?.service_cid)
188
- out(` cid: ${d.service_cid}`);
189
- out(` broker: ${r.brokerUrl}`);
190
- out(` state: ${STATE_DIR}`);
191
- out(` logs: ${LOG_PATH}`);
192
- }
193
- else {
194
- err(`daemon started (pid ${child.pid}) but port ${PORT} did not open within 30s — check ${LOG_PATH}.`);
195
- process.exit(1);
196
- }
197
- }
198
- async function cmdStop() {
199
- const pid = runningPid();
200
- if (!pid) {
201
- out('ours-notify is not running.');
202
- return;
203
- }
204
- out(`stopping ours-notify (pid ${pid})…`);
205
- try {
206
- process.kill(pid, 'SIGTERM');
207
- }
208
- catch (e) {
209
- err(`failed to signal pid ${pid}: ${String(e)}`);
210
- }
211
- for (let i = 0; i < 25 && isAlive(pid); i++)
212
- await sleep(200);
213
- if (isAlive(pid)) {
214
- err(`pid ${pid} did not exit; sending SIGKILL.`);
215
- try {
216
- process.kill(pid, 'SIGKILL');
217
- }
218
- catch { /* ignore */ }
219
- }
220
- try {
221
- fs.rmSync(PID_PATH, { force: true });
222
- }
223
- catch { /* ignore */ }
224
- out('stopped.');
225
- }
226
- async function cmdStatus() {
227
- const pid = runningPid();
228
- const reachable = await portOpen(PORT);
229
- if (pid || reachable) {
230
- out(pid ? `ours-notify: running (pid ${pid})` : 'ours-notify: running (no pidfile — external launcher or stale process)');
231
- out(` url: http://127.0.0.1:${PORT}/v1/discovery${reachable ? ' (reachable)' : ' (not reachable yet)'}`);
232
- const d = reachable ? await fetchDiscovery() : null;
233
- if (d?.service_cid)
234
- out(` cid: ${d.service_cid}`);
235
- if (d?.broker_url)
236
- out(` broker: ${d.broker_url}`);
237
- if (d)
238
- out(` vapid: ${d.vapid_public_key ? 'advertised' : 'absent'}`);
239
- out(` state: ${STATE_DIR}`);
240
- out(` cli: v${cliVersion()}`);
241
- return;
242
- }
243
- out('ours-notify: stopped');
244
- out(` state: ${STATE_DIR}`);
245
- out(` cli: v${cliVersion()}`);
246
- }
247
- // ---- install-service --------------------------------------------------------
248
- const SYSTEMD_UNIT = process.env.OURS_NOTIFY_SERVICE_NAME ?? 'ours-notify.service';
249
- const LAUNCHD_LABEL = 'solutions.adaptframework.ours-notify';
250
- function systemdUnitPath() { return join(homedir(), '.config', 'systemd', 'user', SYSTEMD_UNIT); }
251
- function launchdPlistPath() { return join(homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`); }
252
- function runCmd(cmd, args) { return spawnSync(cmd, args, { stdio: 'inherit' }).status === 0; }
253
- // Exported so install-service can be validated (rendered + systemd-analyze verify)
254
- // without enabling a boot unit that competes with the running fleet.
255
- export function renderSystemdUnit() {
256
- const r = resolveConfig();
257
- const port = r.discoveryPort ?? DEFAULT_DISCOVERY_PORT;
258
- return `[Unit]
259
- Description=ours.network notifications daemon (WebPush + a2a_notifications over ADAPT)
260
- After=network-online.target
261
- Wants=network-online.target
262
-
263
- [Service]
264
- Type=simple
265
- ExecStart=${process.execPath} ${SELF} serve
266
- Environment=STATE_DIR=${STATE_DIR}
267
- Environment=BROKER_URL=${r.brokerUrl ?? ''}
268
- Environment=DISCOVERY_PORT=${port}
269
- Environment=DISCOVERY_HOST=${r.discoveryHost}
270
- Environment=VAPID_SUBJECT=${r.vapidSubject}
271
- Restart=on-failure
272
- RestartSec=2
273
-
274
- [Install]
275
- WantedBy=default.target
276
- `;
277
- }
278
- export function renderLaunchdPlist() {
279
- const r = resolveConfig();
280
- const port = r.discoveryPort ?? DEFAULT_DISCOVERY_PORT;
281
- return `<?xml version="1.0" encoding="UTF-8"?>
282
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
283
- <plist version="1.0">
284
- <dict>
285
- <key>Label</key><string>${LAUNCHD_LABEL}</string>
286
- <key>ProgramArguments</key>
287
- <array>
288
- <string>${process.execPath}</string>
289
- <string>${SELF}</string>
290
- <string>serve</string>
291
- </array>
292
- <key>EnvironmentVariables</key>
293
- <dict>
294
- <key>STATE_DIR</key><string>${STATE_DIR}</string>
295
- <key>BROKER_URL</key><string>${r.brokerUrl ?? ''}</string>
296
- <key>DISCOVERY_PORT</key><string>${port}</string>
297
- <key>DISCOVERY_HOST</key><string>${r.discoveryHost}</string>
298
- <key>VAPID_SUBJECT</key><string>${r.vapidSubject}</string>
299
- </dict>
300
- <key>RunAtLoad</key><true/>
301
- <key>KeepAlive</key><true/>
302
- <key>StandardOutPath</key><string>${LOG_PATH}</string>
303
- <key>StandardErrorPath</key><string>${LOG_PATH}</string>
304
- </dict>
305
- </plist>
306
- `;
307
- }
308
- function installSystemd() {
309
- const r = resolveConfig();
310
- if (!r.brokerUrl) {
311
- err('BROKER_URL is required to install the service (it is baked into the unit).');
312
- process.exit(1);
313
- }
314
- const unitPath = systemdUnitPath();
315
- fs.mkdirSync(dirname(unitPath), { recursive: true });
316
- fs.writeFileSync(unitPath, renderSystemdUnit());
317
- out(`wrote ${unitPath}`);
318
- runCmd('systemctl', ['--user', 'daemon-reload']);
319
- if (!runCmd('systemctl', ['--user', 'enable', '--now', SYSTEMD_UNIT])) {
320
- err('failed to enable/start the service via systemctl --user.');
321
- process.exit(1);
322
- }
323
- if (!runCmd('loginctl', ['enable-linger', userInfo().username])) {
324
- err(`warning: could not enable linger — run manually: loginctl enable-linger ${userInfo().username}`);
325
- }
326
- out('');
327
- out('ours-notify installed as a systemd user service and started.');
328
- out(` status: systemctl --user status ${SYSTEMD_UNIT}`);
329
- out(` logs: journalctl --user -u ${SYSTEMD_UNIT} -f`);
330
- out(' remove: ours-notify uninstall-service');
331
- }
332
- function uninstallSystemd() {
333
- runCmd('systemctl', ['--user', 'disable', '--now', SYSTEMD_UNIT]);
334
- const unitPath = systemdUnitPath();
335
- try {
336
- fs.rmSync(unitPath, { force: true });
337
- out(`removed ${unitPath}`);
338
- }
339
- catch (e) {
340
- err(`failed to remove ${unitPath}: ${String(e)}`);
341
- }
342
- runCmd('systemctl', ['--user', 'daemon-reload']);
343
- out('ours-notify service uninstalled.');
344
- }
345
- function installLaunchd() {
346
- const plistPath = launchdPlistPath();
347
- fs.mkdirSync(dirname(plistPath), { recursive: true });
348
- fs.writeFileSync(plistPath, renderLaunchdPlist());
349
- out(`wrote ${plistPath}`);
350
- const domain = `gui/${process.getuid()}`;
351
- spawnSync('launchctl', ['bootout', domain, plistPath], { stdio: 'ignore' });
352
- if (!runCmd('launchctl', ['bootstrap', domain, plistPath])) {
353
- err('failed to load the launchd agent.');
354
- process.exit(1);
355
- }
356
- out('');
357
- out('ours-notify installed as a launchd agent and started.');
358
- out(' remove: ours-notify uninstall-service');
359
- }
360
- function uninstallLaunchd() {
361
- const plistPath = launchdPlistPath();
362
- const domain = `gui/${process.getuid()}`;
363
- spawnSync('launchctl', ['bootout', domain, plistPath], { stdio: 'ignore' });
364
- try {
365
- fs.rmSync(plistPath, { force: true });
366
- out(`removed ${plistPath}`);
367
- }
368
- catch (e) {
369
- err(`failed to remove ${plistPath}: ${String(e)}`);
370
- }
371
- out('ours-notify service uninstalled.');
372
- }
373
- async function cmdInstallService() {
374
- await cmdStop();
375
- if (process.platform === 'linux')
376
- return installSystemd();
377
- if (process.platform === 'darwin')
378
- return installLaunchd();
379
- err(`install-service: unsupported platform "${process.platform}" (only linux/systemd and macOS/launchd).`);
380
- process.exit(1);
381
- }
382
- function cmdUninstallService() {
383
- if (process.platform === 'linux')
384
- return uninstallSystemd();
385
- if (process.platform === 'darwin')
386
- return uninstallLaunchd();
387
- err(`uninstall-service: unsupported platform "${process.platform}".`);
388
- process.exit(1);
389
- }
390
- function usage() {
391
- out('ours-notify — ours.network notifications daemon');
392
- out('');
393
- out('Usage: ours-notify <command>');
394
- out(' start start the daemon in the background');
395
- out(' stop stop the running daemon');
396
- out(' restart stop then start');
397
- out(' status show running state, port, cid, state dir, version');
398
- out(' serve run in the foreground (used by start; handy for debugging)');
399
- out(' install-service install + start a boot-persistent service (systemd/launchd)');
400
- out(' uninstall-service stop + remove that service');
401
- out(' version print the CLI version');
402
- out('');
403
- out('Config precedence (per field): env > config.json > default.');
404
- out(' STATE_DIR (~/.ours-notifications) · BROKER_URL (required) · DISCOVERY_PORT (8890)');
405
- out(' DISCOVERY_HOST (127.0.0.1) · VAPID_SUBJECT · config.json at $OURS_NOTIFY_CONFIG');
406
- out('VAPID keys + identity self-provision into the state dir on first start.');
407
- }
408
- async function main() {
409
- const cmd = process.argv[2];
410
- switch (cmd) {
411
- case 'serve': return cmdServe();
412
- case 'start': return cmdStart();
413
- case 'stop': return cmdStop();
414
- case 'restart':
415
- await cmdStop();
416
- return cmdStart();
417
- case 'status': return cmdStatus();
418
- case 'install-service': return cmdInstallService();
419
- case 'uninstall-service': return cmdUninstallService();
420
- case 'version':
421
- out(`ours-notify v${cliVersion()}`);
422
- return;
423
- case undefined:
424
- case 'help':
425
- case '--help':
426
- case '-h': return usage();
427
- default:
428
- err(`unknown command: ${cmd}`);
429
- usage();
430
- process.exit(1);
431
- }
432
- }
433
- // Run only as a CLI entrypoint (not when imported by tests). Compare via realpath
434
- // so the npm-installed `ours-notify` bin SYMLINK (argv[1]) still matches this
435
- // module's resolved path (import.meta.url) — a plain path compare would not.
436
- function invokedAsMain() {
437
- try {
438
- return !!process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(SELF);
439
- }
440
- catch {
441
- return false;
442
- }
443
- }
444
- if (invokedAsMain()) {
445
- main().catch((e) => { err(String(e?.stack ?? e)); process.exit(1); });
446
- }
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { createNotificationService } from './server.js';
4
+ const file = process.argv[2];
5
+ if (!file)
6
+ throw new Error('Usage: ours-notifications <private-config.json>');
7
+ if ((statSync(file).mode & 0o077) !== 0)
8
+ throw new Error('notification config must be private (chmod 600)');
9
+ const config = JSON.parse(readFileSync(file, 'utf8'));
10
+ const service = createNotificationService(config);
11
+ await new Promise((resolve, reject) => { service.server.once('error', reject); service.server.listen(config.port, config.host ?? '127.0.0.1', resolve); });
12
+ console.log('Notification service ready');
13
+ for (const signal of ['SIGTERM', 'SIGINT'])
14
+ process.once(signal, async () => { await service.close(); process.exit(0); });
@@ -0,0 +1,12 @@
1
+ import type { Server } from 'node:http';
2
+ import type { Store } from './store.js';
3
+ import type { Credential } from './server.js';
4
+ /** Foreground is a property of one verified device, never the whole account. */
5
+ export declare function attachPresence(server: Server, store: Store, users: Credential[], options?: {
6
+ intervalMs: number;
7
+ freshnessMs: number;
8
+ }): {
9
+ online: (user: string, id: string) => boolean;
10
+ notify: (user: string) => void;
11
+ close(): void;
12
+ };