@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.
- package/README.md +122 -225
- package/dist/chat-target.d.ts +9 -0
- package/dist/chat-target.js +12 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -445
- package/dist/presence.d.ts +12 -0
- package/dist/presence.js +55 -145
- package/dist/server.d.ts +26 -0
- package/dist/server.js +145 -0
- package/dist/store.d.ts +63 -0
- package/dist/store.js +181 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.js +60 -0
- package/package.json +14 -24
- package/dist/config.js +0 -104
- package/dist/daemon.js +0 -353
- package/dist/http.js +0 -31
- package/dist/log-store.js +0 -48
- package/dist/provision.js +0 -29
- package/dist/push.js +0 -6
- package/dist/web-push-sender.js +0 -41
- package/mufl_code/20E39399689579D5AFA5132C59DEDDD328C3BCA1A74D546F2193B36585DDD283.muflo +0 -0
- package/mufl_code/config.mufl +0 -21
- package/mufl_code/core/.github/FUNDING.yml +0 -3
- package/mufl_code/core/.github/workflows/docs.yml +0 -39
- package/mufl_code/core/.github/workflows/tests.yml +0 -66
- package/mufl_code/core/COMMERCIAL-LICENCE.md +0 -25
- package/mufl_code/core/COMPATIBILITY.md +0 -221
- package/mufl_code/core/CONTRIBUTING.md +0 -37
- package/mufl_code/core/LICENSE +0 -98
- package/mufl_code/core/NOTIFICATIONS_CONTRACT.md +0 -290
- package/mufl_code/core/README.md +0 -91
- package/mufl_code/core/SECURITY.md +0 -40
- package/mufl_code/core/TRADEMARKS.md +0 -24
- package/mufl_code/core/a2a_capabilities.mm +0 -449
- package/mufl_code/core/a2a_cluster.mm +0 -601
- package/mufl_code/core/a2a_control.mm +0 -90
- package/mufl_code/core/a2a_messaging.mm +0 -4817
- package/mufl_code/core/a2a_monitoring.mm +0 -94
- package/mufl_code/core/a2a_notification_integration.mm +0 -144
- package/mufl_code/core/a2a_notifications.mm +0 -1127
- package/mufl_code/core/a2a_protocol.mm +0 -246
- package/mufl_code/core/a2a_versions.mm +0 -801
- package/mufl_code/core/config.mufl +0 -29
- package/mufl_code/core/docs/.vitepress/config.mts +0 -58
- package/mufl_code/core/docs/citations.json +0 -688
- package/mufl_code/core/docs/guide/01-vendor-the-core.md +0 -50
- package/mufl_code/core/docs/guide/02-configure-and-compile.md +0 -100
- package/mufl_code/core/docs/guide/03-wire-the-host.md +0 -154
- package/mufl_code/core/docs/guide/04-connect-and-message.md +0 -156
- package/mufl_code/core/docs/guide/05-test-your-app.md +0 -75
- package/mufl_code/core/docs/guide/index.md +0 -63
- package/mufl_code/core/docs/how-it-works/capabilities-and-control.md +0 -77
- package/mufl_code/core/docs/how-it-works/cluster.md +0 -65
- package/mufl_code/core/docs/how-it-works/identity.md +0 -56
- package/mufl_code/core/docs/how-it-works/invites-and-contacts.md +0 -76
- package/mufl_code/core/docs/how-it-works/messaging.md +0 -55
- package/mufl_code/core/docs/how-it-works/monitoring-and-config.md +0 -72
- package/mufl_code/core/docs/how-it-works/overview.md +0 -45
- package/mufl_code/core/docs/how-it-works/versioning.md +0 -116
- package/mufl_code/core/docs/index.md +0 -27
- package/mufl_code/core/docs/package-lock.json +0 -3467
- package/mufl_code/core/docs/package.json +0 -15
- package/mufl_code/core/docs/reference/contributing.md +0 -26
- package/mufl_code/core/docs/reference/glossary.md +0 -108
- package/mufl_code/core/docs/reference/implementations.md +0 -23
- package/mufl_code/core/docs/reference/modules.md +0 -19
- package/mufl_code/core/docs/scripts/check-citations.mjs +0 -36
- package/mufl_code/core/docs/scripts/check-denylist.mjs +0 -30
- package/mufl_code/core/docs/scripts/gen-llms.mjs +0 -32
- package/mufl_code/core/docs/workflows/cluster.md +0 -89
- package/mufl_code/core/docs/workflows/contact-exchange.md +0 -60
- package/mufl_code/core/docs/workflows/contact-restore.md +0 -60
- package/mufl_code/core/docs/workflows/control-verbs.md +0 -70
- package/mufl_code/core/docs/workflows/index.md +0 -57
- package/mufl_code/core/docs/workflows/introductions.md +0 -60
- package/mufl_code/core/docs/workflows/messaging.md +0 -75
- package/mufl_code/core/docs/workflows/monitoring.md +0 -84
- package/mufl_code/core/docs/workflows/notifications.md +0 -109
- package/mufl_code/core/release-notes/0.1.md +0 -60
- package/mufl_code/core/release-notes/0.3.md +0 -22
- package/mufl_code/core/release-notes/0.4.md +0 -97
- package/mufl_code/core/tests/README.md +0 -83
- package/mufl_code/core/tests/compat/README.md +0 -82
- package/mufl_code/core/tests/compat/compat.mjs +0 -245
- package/mufl_code/core/tests/compat/compat_peer.mjs +0 -146
- package/mufl_code/core/tests/compat/run_compat.sh +0 -96
- package/mufl_code/core/tests/corpus.mjs +0 -139
- package/mufl_code/core/tests/corpus_actor.mu +0 -242
- package/mufl_code/core/tests/fatcompile_actor.mu +0 -52
- package/mufl_code/core/tests/mig.mjs +0 -805
- package/mufl_code/core/tests/mig_actor.mu +0 -518
- package/mufl_code/core/tests/migapp.mjs +0 -431
- package/mufl_code/core/tests/migapp_actor.mu +0 -294
- package/mufl_code/core/tests/mufl_semantics/run.sh +0 -69
- package/mufl_code/core/tests/mufl_semantics/t11_three_version.mufl +0 -47
- package/mufl_code/core/tests/mufl_semantics/t2_extra_fields.mufl +0 -11
- package/mufl_code/core/tests/mufl_semantics/t7_runtime.mufl +0 -30
- package/mufl_code/core/tests/mufl_semantics/t9_passthrough.mufl +0 -19
- package/mufl_code/core/tests/mufl_semantics/t_str_order.mufl +0 -19
- package/mufl_code/core/tests/notif.mjs +0 -755
- package/mufl_code/core/tests/notif_actor.mu +0 -450
- package/mufl_code/core/tests/protocol_container.mm +0 -18
- package/mufl_code/core/tests/run.sh +0 -65
- package/mufl_code/core/tests/run_corpus.sh +0 -51
- package/mufl_code/core/tests/run_fatcompile.sh +0 -40
- package/mufl_code/core/tests/run_mig.sh +0 -51
- package/mufl_code/core/tests/run_migapp.sh +0 -51
- package/mufl_code/core/tests/run_notif.sh +0 -57
- package/mufl_code/core/tests/test.mjs +0 -750
- package/mufl_code/core/tests/test_actor.mu +0 -564
- package/mufl_code/core/tests/test_common.mjs +0 -93
- package/mufl_code/core/version.mm +0 -24
- package/mufl_code/packet.mu +0 -170
- package/mufl_code/protocol_container.mm +0 -18
- package/scripts/gen-vapid.mjs +0 -16
package/dist/cli.js
CHANGED
|
@@ -1,446 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
};
|