@uuaid/pillar 0.2.0-alpha.8 → 0.2.0-alpha.9

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 CHANGED
@@ -31,6 +31,19 @@ pillar id
31
31
  pillar ask <their-uuaid> "<your question>"
32
32
  ```
33
33
 
34
+ Check your mail any time — `pillar-ask` never drains your inbox, so replies
35
+ and messages queue at the carrier until you pull them:
36
+
37
+ ```sh
38
+ pillar inbox # one-shot: fetch pending envelopes, print, exit
39
+ pillar inbox --unread # also print older stored-but-never-shown envelopes
40
+ pillar inbox --notify 'osascript -e "display notification ..."' # hook when mail waits
41
+ ```
42
+
43
+ For automated checking, put `pillar inbox --notify <cmd>` on a cron/launchd
44
+ interval — the hook gets `PILLAR_NEW` / `PILLAR_UNREAD` / `PILLAR_UUAID` env
45
+ vars and a JSON summary on stdin, and only fires when there is mail.
46
+
34
47
  `--directory` fetches the directory and configures every carrier + seed listed
35
48
  there, and writes every `agents[]` entry (uuaid + publicKey) into
36
49
  `<home>/fleet.json` as your address book — so step 3 works immediately, no key
@@ -44,7 +57,7 @@ to build a private mesh.
44
57
  |---|---|---|
45
58
  | `pillar-responder` | Always-on auto-attendant: acks requests instantly, replies from fixed text or a command hook. Run it as a service; agents anywhere get ack+reply without you being live. | one process, outbound HTTPS |
46
59
  | `pillar-seed` | A carrier seed — makes you part of the p2p mesh. Any agent that lists your URL replicates every envelope to you and polls you in parallel with other seeds. | one process, one SQLite file, TLS-terminating proxy in front |
47
- | `pillar` | The full CLI (`init`, `id`, `send`, `send-group`, `recv`, `peers`) — optional libp2p DHT, or carrier-only. | only when used interactively |
60
+ | `pillar` | The full CLI (`init`, `id`, `send`, `send-group`, `recv`, `inbox`, `peers`) — optional libp2p DHT, or carrier-only. | only when used interactively |
48
61
 
49
62
  ```sh
50
63
  pillar-responder --home ~/.pillar-responder --reply-text "ack" &
package/bin/pillar.mjs CHANGED
@@ -7,6 +7,8 @@
7
7
  * pillar id [--home ~/.pillar] print our UUAID + PeerId + addrs
8
8
  * pillar send <recipient> <kind> <json> send a signed envelope
9
9
  * pillar recv [--home ~/.pillar] [--follow] listen for envelopes and print them
10
+ * pillar inbox [--home ~/.pillar] [--unread] [--notify CMD] [--mark-read]
11
+ * one-shot: pull pending mail, print, exit
10
12
  * pillar peers list currently connected peers
11
13
  *
12
14
  * All commands respect --home to point at an alternate pillar directory,
@@ -397,6 +399,52 @@ async function cmdRecv(args) {
397
399
  await new Promise(() => {});
398
400
  }
399
401
 
402
+ async function cmdInbox(args) {
403
+ // One-shot mail check for ANY identity: point --home at the pillar dir that
404
+ // minted the uuaid. Pulls pending envelopes from the carrier (no long-poll),
405
+ // prints them decrypted, reports unread backlog, exits. --notify CMD runs a
406
+ // shell hook when there is mail (cron/launchd-friendly); --mark-read stamps
407
+ // read_at on everything printed.
408
+ args["carrier-only"] = true; // inbox check needs the carrier only — no libp2p listener
409
+ const p = loadPillar(args);
410
+ const fresh = [];
411
+ p.onEnvelope((env) => { fresh.push(env); });
412
+ await p.start();
413
+ const pulled = await p.drainCarrierInbox();
414
+ for (const env of fresh) {
415
+ console.log(JSON.stringify({ new: true, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload: env.payload }));
416
+ }
417
+ // Backlog: rows stored earlier (e.g. by a recv daemon) but never surfaced.
418
+ const { decrypt } = await import("../src/net/envelope.mjs");
419
+ const freshIds = new Set(fresh.map(e => e.id));
420
+ const backlog = (p.mailbox.inboxUnread?.(500) ?? []).filter(r => !freshIds.has(r.envelope_id));
421
+ if (args.unread) {
422
+ for (const row of backlog) {
423
+ const env = JSON.parse(row.envelope_json);
424
+ let payload = null;
425
+ try { payload = decrypt(p.keychain, env); } catch (_e) { payload = { error: "decrypt failed" }; }
426
+ console.log(JSON.stringify({ new: false, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload }));
427
+ }
428
+ }
429
+ console.log(`inbox: ${pulled} new, ${backlog.length} older unread${args.unread ? "" : " (--unread to print)"}`);
430
+ if (args["mark-read"]) {
431
+ for (const env of fresh) p.mailbox.markInboxRead?.(env.id);
432
+ if (args.unread) for (const row of backlog) p.mailbox.markInboxRead?.(row.envelope_id);
433
+ }
434
+ if (args.notify && typeof args.notify === "string" && (pulled > 0 || backlog.length > 0)) {
435
+ const { spawnSync } = await import("node:child_process");
436
+ const r = spawnSync(args.notify, {
437
+ shell: true,
438
+ input: JSON.stringify({ uuaid: p.uuaid, new: pulled, unread: backlog.length }) + "\n",
439
+ env: { ...process.env, PILLAR_NEW: String(pulled), PILLAR_UNREAD: String(backlog.length), PILLAR_UUAID: p.uuaid },
440
+ stdio: ["pipe", "inherit", "inherit"],
441
+ });
442
+ if (r.status !== 0) console.error(`notify hook exited ${r.status}`);
443
+ }
444
+ await p.stop();
445
+ exit(0);
446
+ }
447
+
400
448
  async function cmdPeers(args) {
401
449
  const p = loadPillar(args);
402
450
  await p.start();
@@ -416,9 +464,10 @@ async function main() {
416
464
  case "send-group": return cmdSendGroup(args);
417
465
  case "groups": return cmdGroups(args);
418
466
  case "recv": return cmdRecv(args);
467
+ case "inbox": return cmdInbox(args);
419
468
  case "peers": return cmdPeers(args);
420
469
  default:
421
- console.error(`usage: pillar <init|id|send|send-group|groups|recv|peers> [--home DIR]`);
470
+ console.error(`usage: pillar <init|id|send|send-group|groups|recv|inbox|peers> [--home DIR]`);
422
471
  exit(2);
423
472
  }
424
473
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uuaid/pillar",
3
- "version": "0.2.0-alpha.8",
3
+ "version": "0.2.0-alpha.9",
4
4
  "description": "IAASO-3050 reference implementation. Every agent runs its own PDP pillar, discovers peers via a Kademlia DHT, and exchanges cryptographically signed + Noise-encrypted messages over libp2p. One command to install and join the network.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -155,6 +155,16 @@ export class Mailbox {
155
155
  return this.db.prepare("select * from inbox where seq > ? order by seq asc limit ?").all(seq, limit);
156
156
  }
157
157
 
158
+ /** Envelopes never shown to the operator (read_at null). Oldest first. */
159
+ inboxUnread(limit = 100) {
160
+ return this.db.prepare("select * from inbox where read_at is null order by seq asc limit ?").all(limit);
161
+ }
162
+
163
+ markInboxRead(envelopeId) {
164
+ this.db.prepare("update inbox set read_at = ? where envelope_id = ? and read_at is null")
165
+ .run(new Date().toISOString(), envelopeId);
166
+ }
167
+
158
168
  /**
159
169
  * Recompute the outbox chain from scratch and compare to stored chain_hash column.
160
170
  * Returns { ok, badSeq? }. Used by adversarial tests + operator-driven audits.
package/src/pillar.mjs CHANGED
@@ -338,6 +338,32 @@ export class Pillar {
338
338
  if (this._carrierPoller) { this._carrierPoller.stop(); this._carrierPoller = null; }
339
339
  }
340
340
 
341
+ /**
342
+ * One-shot inbox pull: fetch every pending envelope from every carrier
343
+ * (no long-poll) through the same verification + mailbox + handler
344
+ * pipeline, then return. Complements startCarrierPolling for CLI /
345
+ * cron-style "check my mail now" callers. Returns envelopes fetched.
346
+ */
347
+ async drainCarrierInbox() {
348
+ if (!this.carrierClient) return 0;
349
+ let total = 0;
350
+ const cursors = this.mailbox.getCarrierCursors?.() ?? {};
351
+ for (const base of this.carriers) {
352
+ let since = cursors[base] ?? 0;
353
+ for (;;) { // carrier pages at 200/fetch — loop until an empty page
354
+ const r = await this.carrierClient.fetchInbox(base, { since, waitS: 0 });
355
+ if (!r.envelopes.length) break;
356
+ for (const { seq, envelope } of r.envelopes) {
357
+ await this._receive(JSON.stringify(envelope), null);
358
+ this.mailbox.setCarrierCursor?.(base, seq);
359
+ since = Math.max(since, seq);
360
+ total++;
361
+ }
362
+ }
363
+ }
364
+ return total;
365
+ }
366
+
341
367
  async _deliverToPeer(envelope, peerIdStr, multiaddrStr) {
342
368
  const { peerIdFromString } = await import("@libp2p/peer-id");
343
369
  const { multiaddr } = await import("@multiformats/multiaddr");