@agimon-ai/po-session 0.0.0-alpha.2 → 0.0.0-alpha.21

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.
@@ -23,19 +23,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  let _agimon_ai_po_core_backgroundWork = require("@agimon-ai/po-core/backgroundWork");
24
24
  let typebox_value = require("typebox/value");
25
25
  let node_crypto = require("node:crypto");
26
- let node_fs = require("node:fs");
27
- node_fs = __toESM(node_fs, 1);
28
26
  let node_path = require("node:path");
29
27
  node_path = __toESM(node_path, 1);
30
- let node_sqlite = require("node:sqlite");
28
+ let _agimon_ai_po_core_platform = require("@agimon-ai/po-core/platform");
31
29
  let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
30
+ let node_fs = require("node:fs");
31
+ node_fs = __toESM(node_fs, 1);
32
+ let node_sqlite = require("node:sqlite");
33
+ let node_timers = require("node:timers");
32
34
  //#region src/services/backgroundWorkService/index.ts
33
35
  /** Creates the generation-bound, pull-based coordinator shared by session extensions. */
34
36
  function createBackgroundWorkService(ctx) {
35
37
  const registrations = /* @__PURE__ */ new Map();
36
- const generation = `doom-background-work:${crypto.randomUUID()}`;
38
+ const generation = `po-background-work:${crypto.randomUUID()}`;
37
39
  const changed = (provider, providerGeneration, kind) => {
38
- ctx.emit(_agimon_ai_po_core_backgroundWork.DOOM_BACKGROUND_WORK_CHANGED_EVENT, {
40
+ ctx.emit(_agimon_ai_po_core_backgroundWork.PO_BACKGROUND_WORK_CHANGED_EVENT, {
39
41
  provider,
40
42
  generation: providerGeneration,
41
43
  kind
@@ -99,11 +101,8 @@ function createBackgroundWorkService(ctx) {
99
101
  }
100
102
  /** Installs one coordinator in the current session service context. */
101
103
  function provideBackgroundWorkService(context) {
102
- context.provide(_agimon_ai_po_core_backgroundWork.DOOM_BACKGROUND_WORK_SERVICE, createBackgroundWorkService(context));
104
+ context.provide(_agimon_ai_po_core_backgroundWork.PO_BACKGROUND_WORK_SERVICE, createBackgroundWorkService(context));
103
105
  }
104
- //#endregion
105
- //#region src/services/peerTransport/index.ts
106
- const PEER_CONFIG_VERSION = 1;
107
106
  const PEER_CONFIG_FILE = "peers.json";
108
107
  const PEER_ROUTE = "/api/plugins/session-peer/inbox";
109
108
  const PEER_HEADER = "x-po-session-peer";
@@ -111,7 +110,8 @@ const TIMESTAMP_HEADER = "x-po-session-timestamp";
111
110
  const SIGNATURE_HEADER = "x-po-session-signature";
112
111
  const MAX_CLOCK_SKEW_MS = 3e5;
113
112
  const MAX_REPLAY_ENTRIES = 1e4;
114
- const REPLAY_DATABASE_FILE = "peer-replay.sqlite";
113
+ /** File the replay guard database lives in, beside the peer config. */
114
+ const PEER_REPLAY_DATABASE_FILE = "peer-replay.sqlite";
115
115
  function validId(value) {
116
116
  return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
117
117
  }
@@ -140,29 +140,144 @@ function peerOf(value) {
140
140
  function sessionPeerConfigPath(homeDirectory) {
141
141
  return node_path.default.join((0, _agimon_ai_po_core_piSettings.poHomeDirectory)(homeDirectory), "session", PEER_CONFIG_FILE);
142
142
  }
143
- /** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */
144
- function readSessionPeerConfig(homeDirectory) {
143
+ /** Group or other permission bits on the peer config make it untrusted. */
144
+ function peerConfigModeIsPrivate(mode) {
145
+ return (mode & 63) === 0;
146
+ }
147
+ /** Validates peer config text. Invalid config fails closed. */
148
+ function parseSessionPeerConfig(text) {
145
149
  let parsed;
146
150
  try {
147
- const file = sessionPeerConfigPath(homeDirectory);
148
- if ((node_fs.default.statSync(file).mode & 63) !== 0) return void 0;
149
- parsed = JSON.parse(node_fs.default.readFileSync(file, "utf8"));
151
+ parsed = JSON.parse(text);
150
152
  } catch {
151
153
  return;
152
154
  }
153
155
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
154
156
  const record = parsed;
155
- if (record.version !== PEER_CONFIG_VERSION || !validId(record.hostId) || !Array.isArray(record.peers)) return void 0;
157
+ if (record.version !== 1 || !validId(record.hostId) || !Array.isArray(record.peers)) return void 0;
156
158
  const peers = record.peers.map(peerOf);
157
159
  if (peers.some((peer) => peer === void 0)) return void 0;
158
160
  const resolved = peers;
159
161
  if (new Set(resolved.map((peer) => peer.hostId)).size !== resolved.length || resolved.some((peer) => peer.hostId === record.hostId)) return void 0;
160
162
  return {
161
- version: PEER_CONFIG_VERSION,
163
+ version: 1,
162
164
  hostId: record.hostId,
163
165
  peers: resolved
164
166
  };
165
167
  }
168
+ /** Replay guard SQL shared by the async guard and the Node-only sync shim. */
169
+ const PEER_REPLAY_SQL = Object.freeze({
170
+ schema: `
171
+ CREATE TABLE IF NOT EXISTS peer_request_replays (
172
+ signature TEXT PRIMARY KEY,
173
+ expires_at INTEGER NOT NULL
174
+ );
175
+ `,
176
+ removeExpired: "DELETE FROM peer_request_replays WHERE expires_at < ?",
177
+ insert: "INSERT OR IGNORE INTO peer_request_replays (signature, expires_at) VALUES (?, ?)",
178
+ insertReturning: "INSERT OR IGNORE INTO peer_request_replays (signature, expires_at) VALUES (?, ?) RETURNING signature",
179
+ count: "SELECT COUNT(*) AS count FROM peer_request_replays",
180
+ trim: `
181
+ DELETE FROM peer_request_replays
182
+ WHERE signature IN (
183
+ SELECT signature FROM peer_request_replays ORDER BY expires_at ASC, signature ASC LIMIT ?
184
+ )
185
+ `
186
+ });
187
+ function peerRequestSignature(request) {
188
+ return request.headers.get(SIGNATURE_HEADER);
189
+ }
190
+ function peerReplayExpiry(now) {
191
+ return now + MAX_CLOCK_SKEW_MS;
192
+ }
193
+ function peerReplayOverflow(total) {
194
+ return total > MAX_REPLAY_ENTRIES ? total - MAX_REPLAY_ENTRIES : 0;
195
+ }
196
+ //#endregion
197
+ //#region src/services/peerTransport/sync.ts
198
+ /**
199
+ * Reads operator-provisioned peers. Invalid or unreadable config fails closed.
200
+ *
201
+ * @deprecated Node-only sync shim; use readSessionPeerConfigAsync. Removed in M2.
202
+ */
203
+ function readSessionPeerConfig(homeDirectory) {
204
+ let text;
205
+ try {
206
+ const file = sessionPeerConfigPath(homeDirectory);
207
+ if (!peerConfigModeIsPrivate(node_fs.default.statSync(file).mode)) return void 0;
208
+ text = node_fs.default.readFileSync(file, "utf8");
209
+ } catch {
210
+ return;
211
+ }
212
+ return parseSessionPeerConfig(text);
213
+ }
214
+ /**
215
+ * Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window.
216
+ *
217
+ * @deprecated Node-only sync shim; use createPeerReplayGuardAsync. Removed in M2.
218
+ */
219
+ function createPeerReplayGuard(homeDirectory) {
220
+ const directory = node_path.default.dirname(sessionPeerConfigPath(homeDirectory));
221
+ node_fs.default.mkdirSync(directory, {
222
+ recursive: true,
223
+ mode: 448
224
+ });
225
+ const database = new node_sqlite.DatabaseSync(node_path.default.join(directory, PEER_REPLAY_DATABASE_FILE));
226
+ database.exec(`PRAGMA journal_mode = WAL;${PEER_REPLAY_SQL.schema}`);
227
+ const removeExpired = database.prepare(PEER_REPLAY_SQL.removeExpired);
228
+ const insert = database.prepare(PEER_REPLAY_SQL.insert);
229
+ const count = database.prepare(PEER_REPLAY_SQL.count);
230
+ const trim = database.prepare(PEER_REPLAY_SQL.trim);
231
+ return {
232
+ admit(request, now = Date.now()) {
233
+ const supplied = peerRequestSignature(request);
234
+ if (!supplied) return false;
235
+ database.exec("BEGIN IMMEDIATE");
236
+ try {
237
+ removeExpired.run(now);
238
+ const accepted = insert.run(supplied, peerReplayExpiry(now)).changes === 1;
239
+ const overflow = peerReplayOverflow(Number(count.get().count));
240
+ if (overflow > 0) trim.run(overflow);
241
+ database.exec("COMMIT");
242
+ return accepted;
243
+ } catch (cause) {
244
+ database.exec("ROLLBACK");
245
+ throw cause;
246
+ }
247
+ },
248
+ close: () => database.close()
249
+ };
250
+ }
251
+ //#endregion
252
+ //#region src/services/peerTransport/index.ts
253
+ /** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */
254
+ async function readSessionPeerConfigAsync(fs, homeDirectory, context) {
255
+ try {
256
+ const file = sessionPeerConfigPath(homeDirectory);
257
+ const mode = await _agimon_ai_po_core_platform.nodeHost.fileMode(file);
258
+ if (mode !== void 0 && !peerConfigModeIsPrivate(mode)) return void 0;
259
+ const text = await (0, _agimon_ai_po_core_platform.readTextIfExists)(fs, file, context);
260
+ return text === void 0 ? void 0 : parseSessionPeerConfig(text);
261
+ } catch {
262
+ return;
263
+ }
264
+ }
265
+ /**
266
+ * Creates a directory tree, giving each directory it creates owner-only access, the way
267
+ * `mkdirSync(directory, { recursive: true, mode: 0o700 })` does. Existing directories keep their mode.
268
+ */
269
+ async function ensurePrivateDirectory(fs, directory, context) {
270
+ const missing = [];
271
+ for (let current = node_path.default.resolve(directory); !(0, _agimon_ai_po_core_platform.getOrThrow)(await fs.exists(current, context));) {
272
+ missing.unshift(current);
273
+ const parent = node_path.default.dirname(current);
274
+ if (parent === current) break;
275
+ current = parent;
276
+ }
277
+ if (missing.length === 0) return;
278
+ (0, _agimon_ai_po_core_platform.getOrThrow)(await fs.createDir(directory, { recursive: true }, context));
279
+ for (const created of missing) await _agimon_ai_po_core_platform.nodeHost.setMode(created, 448);
280
+ }
166
281
  /** `peer/<host-id>/<session-id>` is the only remote target spelling. */
167
282
  function parseRemoteSessionReference(value) {
168
283
  const match = /^peer\/([^/]+)\/([^/]+)$/u.exec(value);
@@ -189,46 +304,24 @@ function peerRequestHeaders(sourceHostId, peer, method, pathname, body, now = Da
189
304
  [SIGNATURE_HEADER]: signature(peer.secret, method, pathname, timestamp, body)
190
305
  });
191
306
  }
192
- /** Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window. */
193
- function createPeerReplayGuard(homeDirectory) {
194
- const directory = node_path.default.dirname(sessionPeerConfigPath(homeDirectory));
195
- node_fs.default.mkdirSync(directory, {
196
- recursive: true,
197
- mode: 448
198
- });
199
- const database = new node_sqlite.DatabaseSync(node_path.default.join(directory, REPLAY_DATABASE_FILE));
200
- database.exec(`
201
- PRAGMA journal_mode = WAL;
202
- CREATE TABLE IF NOT EXISTS peer_request_replays (
203
- signature TEXT PRIMARY KEY,
204
- expires_at INTEGER NOT NULL
205
- );
206
- `);
207
- const removeExpired = database.prepare("DELETE FROM peer_request_replays WHERE expires_at < ?");
208
- const insert = database.prepare("INSERT OR IGNORE INTO peer_request_replays (signature, expires_at) VALUES (?, ?)");
209
- const count = database.prepare("SELECT COUNT(*) AS count FROM peer_request_replays");
210
- const trim = database.prepare(`
211
- DELETE FROM peer_request_replays
212
- WHERE signature IN (
213
- SELECT signature FROM peer_request_replays ORDER BY expires_at ASC, signature ASC LIMIT ?
214
- )
215
- `);
307
+ /**
308
+ * Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window.
309
+ * Takes over the database and closes it in `close()`.
310
+ */
311
+ async function createPeerReplayGuardAsync(database) {
312
+ await database.exec(PEER_REPLAY_SQL.schema);
216
313
  return {
217
- admit(request, now = Date.now()) {
218
- const supplied = request.headers.get(SIGNATURE_HEADER);
314
+ async admit(request, now = Date.now()) {
315
+ const supplied = peerRequestSignature(request);
219
316
  if (!supplied) return false;
220
- database.exec("BEGIN IMMEDIATE");
221
- try {
222
- removeExpired.run(now);
223
- const accepted = insert.run(supplied, now + MAX_CLOCK_SKEW_MS).changes === 1;
224
- const total = Number(count.get().count);
225
- if (total > MAX_REPLAY_ENTRIES) trim.run(total - MAX_REPLAY_ENTRIES);
226
- database.exec("COMMIT");
317
+ return database.transaction(async (transaction) => {
318
+ await transaction.run(PEER_REPLAY_SQL.removeExpired, now);
319
+ const accepted = await transaction.get(PEER_REPLAY_SQL.insertReturning, supplied, peerReplayExpiry(now)) !== void 0;
320
+ const row = await transaction.get(PEER_REPLAY_SQL.count);
321
+ const overflow = peerReplayOverflow(Number(row?.count ?? 0));
322
+ if (overflow > 0) await transaction.run(PEER_REPLAY_SQL.trim, overflow);
227
323
  return accepted;
228
- } catch (cause) {
229
- database.exec("ROLLBACK");
230
- throw cause;
231
- }
324
+ });
232
325
  },
233
326
  close: () => database.close()
234
327
  };
@@ -239,7 +332,7 @@ function verifyPeerRequest(config, request, body, now = Date.now()) {
239
332
  const supplied = request.headers.get(SIGNATURE_HEADER);
240
333
  if (!hostId || !timestamp || !supplied || !/^\d{1,16}$/u.test(timestamp)) return void 0;
241
334
  const sentAt = Number(timestamp);
242
- if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > MAX_CLOCK_SKEW_MS) return void 0;
335
+ if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > 3e5) return void 0;
243
336
  const peer = config.peers.find((candidate) => candidate.hostId === hostId);
244
337
  if (!peer) return void 0;
245
338
  const expected = Buffer.from(signature(peer.secret, request.method, new URL(request.url).pathname, timestamp, body));
@@ -261,7 +354,7 @@ function parseSessionPeerEnvelope(value) {
261
354
  }
262
355
  /** Adds asynchronous tunnel publication to a host-bound local communication endpoint. */
263
356
  function createPeerCommunication(options) {
264
- const config = readSessionPeerConfig(options.homeDirectory);
357
+ const config = options.config;
265
358
  const send = options.fetch ?? fetch;
266
359
  return {
267
360
  sessionId: options.local.sessionId,
@@ -329,19 +422,26 @@ function createSessionDirectory(options) {
329
422
  const observations = /* @__PURE__ */ new Map();
330
423
  const snapshotRequired = /* @__PURE__ */ new Set();
331
424
  const subscribers = /* @__PURE__ */ new Set();
332
- const discover = (callerReference, allowedReferences) => {
425
+ const discover = async (callerReference, allowedReferences) => {
333
426
  const result = [];
334
- for (const observation of observations.values()) {
427
+ for (const observation of Array.from(observations.values())) {
335
428
  const reference = keyOf(observation);
336
429
  if (allowedReferences && !allowedReferences.has(reference)) continue;
337
- const grant = options.authorizeDiscovery(callerReference, observation);
430
+ const grant = await options.authorizeDiscovery(callerReference, observation);
338
431
  if (!grant) continue;
339
432
  result.push(publicEntry(observation, now(), grant.label ?? observation.label, grant.capabilities));
340
433
  }
341
434
  return result.sort((left, right) => left.reference.localeCompare(right.reference));
342
435
  };
436
+ const push = (subscriber) => {
437
+ subscriber.queue = subscriber.queue.then(async () => {
438
+ if (!subscribers.has(subscriber)) return;
439
+ const sessions = await discover(subscriber.callerReference);
440
+ if (subscribers.has(subscriber)) subscriber.listener(sessions);
441
+ }).catch(() => void 0);
442
+ };
343
443
  const notify = () => {
344
- for (const subscriber of subscribers) subscriber.listener(discover(subscriber.callerReference));
444
+ for (const subscriber of subscribers) push(subscriber);
345
445
  };
346
446
  return {
347
447
  observe(observation) {
@@ -390,17 +490,18 @@ function createSessionDirectory(options) {
390
490
  notify();
391
491
  },
392
492
  discover,
393
- resolveDeliveryTarget(callerReference, reference) {
493
+ async resolveDeliveryTarget(callerReference, reference) {
394
494
  const observation = observations.get(reference);
395
- return observation && options.authorizeDiscovery(callerReference, observation) ? observation.deliveryTarget : void 0;
495
+ return observation && await options.authorizeDiscovery(callerReference, observation) ? observation.deliveryTarget : void 0;
396
496
  },
397
497
  subscribe(callerReference, listener) {
398
498
  const subscriber = {
399
499
  callerReference,
400
- listener
500
+ listener,
501
+ queue: Promise.resolve()
401
502
  };
402
503
  subscribers.add(subscriber);
403
- listener(discover(callerReference));
504
+ push(subscriber);
404
505
  return () => subscribers.delete(subscriber);
405
506
  }
406
507
  };
@@ -423,100 +524,96 @@ function validateGroupId(value) {
423
524
  function validateReference(value) {
424
525
  if (!REFERENCE_PATTERN.test(value)) throw new TypeError(`Session group member '${value}' is not a qualified reference.`);
425
526
  }
527
+ const SCHEMA$1 = `
528
+ CREATE TABLE IF NOT EXISTS session_groups (
529
+ group_id TEXT PRIMARY KEY,
530
+ owner_authority TEXT NOT NULL,
531
+ revision INTEGER NOT NULL
532
+ );
533
+ CREATE TABLE IF NOT EXISTS session_group_members (
534
+ group_id TEXT NOT NULL REFERENCES session_groups(group_id) ON DELETE CASCADE,
535
+ member_reference TEXT NOT NULL,
536
+ PRIMARY KEY (group_id, member_reference)
537
+ );
538
+ `;
539
+ async function readGroup(database, groupId) {
540
+ const row = await database.get("SELECT * FROM session_groups WHERE group_id = ?", groupId);
541
+ if (!row) return void 0;
542
+ const members = await database.all("SELECT member_reference FROM session_group_members WHERE group_id = ? ORDER BY member_reference", groupId);
543
+ return Object.freeze({
544
+ id: row.group_id,
545
+ ownerAuthority: row.owner_authority,
546
+ revision: Number(row.revision),
547
+ members: Object.freeze(members.map((member) => member.member_reference))
548
+ });
549
+ }
426
550
  /** Opens a revisioned SQLite registry for communication-only group membership. */
427
- function createSessionGroupStore(options) {
428
- const database = new node_sqlite.DatabaseSync(options.databasePath);
429
- database.exec(`
430
- PRAGMA journal_mode = WAL;
431
- PRAGMA foreign_keys = ON;
432
- CREATE TABLE IF NOT EXISTS session_groups (
433
- group_id TEXT PRIMARY KEY,
434
- owner_authority TEXT NOT NULL,
435
- revision INTEGER NOT NULL
436
- );
437
- CREATE TABLE IF NOT EXISTS session_group_members (
438
- group_id TEXT NOT NULL REFERENCES session_groups(group_id) ON DELETE CASCADE,
439
- member_reference TEXT NOT NULL,
440
- PRIMARY KEY (group_id, member_reference)
441
- );
442
- `);
443
- const read = (groupId) => {
444
- const row = database.prepare("SELECT * FROM session_groups WHERE group_id = ?").get(groupId);
445
- if (!row) return void 0;
446
- const members = database.prepare("SELECT member_reference FROM session_group_members WHERE group_id = ? ORDER BY member_reference").all(groupId);
447
- return Object.freeze({
448
- id: row.group_id,
449
- ownerAuthority: row.owner_authority,
450
- revision: row.revision,
451
- members: Object.freeze(members.map((member) => member.member_reference))
452
- });
453
- };
454
- const enroll = (groupId, members) => {
455
- const insert = database.prepare("INSERT OR IGNORE INTO session_group_members (group_id, member_reference) VALUES (?, ?)");
551
+ async function createSessionGroupStore(options) {
552
+ const { database } = options;
553
+ try {
554
+ await database.exec("PRAGMA foreign_keys = ON");
555
+ } catch {}
556
+ await database.exec(SCHEMA$1);
557
+ const enroll = async (transaction, groupId, members) => {
456
558
  for (const member of new Set(members)) {
457
559
  validateReference(member);
458
560
  if (options.validateEnrollment?.(groupId, member) === false) throw new Error(`Session group enrollment policy rejected '${member}'.`);
459
- insert.run(groupId, member);
561
+ await transaction.run("INSERT OR IGNORE INTO session_group_members (group_id, member_reference) VALUES (?, ?)", groupId, member);
460
562
  }
461
563
  };
462
564
  return {
463
- create(groupId, ownerAuthority, members) {
565
+ async create(groupId, ownerAuthority, members) {
464
566
  validateGroupId(groupId);
465
567
  if (!ownerAuthority.trim()) throw new TypeError("Session group owner authority is required.");
466
- database.exec("BEGIN IMMEDIATE");
467
- try {
468
- database.prepare("INSERT INTO session_groups (group_id, owner_authority, revision) VALUES (?, ?, 1)").run(groupId, ownerAuthority);
469
- enroll(groupId, members);
470
- database.exec("COMMIT");
471
- } catch (cause) {
472
- database.exec("ROLLBACK");
473
- throw cause;
474
- }
475
- return read(groupId);
568
+ return database.transaction(async (transaction) => {
569
+ await transaction.run("INSERT INTO session_groups (group_id, owner_authority, revision) VALUES (?, ?, 1)", groupId, ownerAuthority);
570
+ await enroll(transaction, groupId, members);
571
+ return await readGroup(transaction, groupId);
572
+ });
476
573
  },
477
- mutate(mutation) {
478
- database.exec("BEGIN IMMEDIATE");
479
- try {
480
- const current = read(mutation.groupId);
574
+ async mutate(mutation) {
575
+ return database.transaction(async (transaction) => {
576
+ const current = await readGroup(transaction, mutation.groupId);
481
577
  if (!current) throw new Error(`Session group '${mutation.groupId}' does not exist.`);
482
578
  if (mutation.authority !== current.ownerAuthority && options.authorizeManagement?.(mutation.authority, current) !== true) throw new Error(`Authority '${mutation.authority}' cannot manage Session group '${mutation.groupId}'.`);
483
579
  if (mutation.expectedRevision !== current.revision) throw new SessionGroupRevisionError(mutation.groupId, mutation.expectedRevision, current.revision);
484
- enroll(mutation.groupId, mutation.add ?? []);
485
- const remove = database.prepare("DELETE FROM session_group_members WHERE group_id = ? AND member_reference = ?");
580
+ await enroll(transaction, mutation.groupId, mutation.add ?? []);
486
581
  for (const member of new Set(mutation.remove ?? [])) {
487
582
  validateReference(member);
488
- remove.run(mutation.groupId, member);
583
+ await transaction.run("DELETE FROM session_group_members WHERE group_id = ? AND member_reference = ?", mutation.groupId, member);
489
584
  }
490
- database.prepare("UPDATE session_groups SET revision = revision + 1 WHERE group_id = ? AND revision = ?").run(mutation.groupId, current.revision);
491
- database.exec("COMMIT");
492
- } catch (cause) {
493
- database.exec("ROLLBACK");
494
- throw cause;
495
- }
496
- return read(mutation.groupId);
585
+ await transaction.run("UPDATE session_groups SET revision = revision + 1 WHERE group_id = ? AND revision = ?", mutation.groupId, current.revision);
586
+ return await readGroup(transaction, mutation.groupId);
587
+ });
497
588
  },
498
- get: read,
499
- groupsFor(memberReference) {
589
+ get: (groupId) => readGroup(database, groupId),
590
+ async groupsFor(memberReference) {
500
591
  validateReference(memberReference);
501
- return database.prepare(`SELECT g.group_id FROM session_groups g
502
- INNER JOIN session_group_members m ON m.group_id = g.group_id
503
- WHERE m.member_reference = ? ORDER BY g.group_id`).all(memberReference).map((row) => read(row.group_id));
592
+ const rows = await database.all(`SELECT g.group_id FROM session_groups g
593
+ INNER JOIN session_group_members m ON m.group_id = g.group_id
594
+ WHERE m.member_reference = ? ORDER BY g.group_id`, memberReference);
595
+ const groups = [];
596
+ for (const row of rows) {
597
+ const group = await readGroup(database, row.group_id);
598
+ if (group) groups.push(group);
599
+ }
600
+ return groups;
504
601
  },
505
- canMessage(groupId, sourceReference, targetReference) {
602
+ async canMessage(groupId, sourceReference, targetReference) {
506
603
  validateReference(sourceReference);
507
604
  validateReference(targetReference);
508
- const row = database.prepare(`SELECT COUNT(*) AS count FROM session_group_members
509
- WHERE group_id = ? AND member_reference IN (?, ?)`).get(groupId, sourceReference, targetReference);
510
- return Number(row.count) === (sourceReference === targetReference ? 1 : 2);
605
+ const row = await database.get(`SELECT COUNT(*) AS count FROM session_group_members
606
+ WHERE group_id = ? AND member_reference IN (?, ?)`, groupId, sourceReference, targetReference);
607
+ return Number(row?.count ?? 0) === (sourceReference === targetReference ? 1 : 2);
511
608
  },
512
609
  close: () => database.close()
513
610
  };
514
611
  }
515
612
  //#endregion
516
613
  //#region src/services/sessionIntercom/type.ts
517
- const DOOM_SESSION_INTERCOM_SERVICE = "doom/session-intercom";
614
+ const PO_SESSION_INTERCOM_SERVICE = "po/session-intercom";
518
615
  function readSessionIntercom(context) {
519
- return context.get(DOOM_SESSION_INTERCOM_SERVICE);
616
+ return context.get(PO_SESSION_INTERCOM_SERVICE);
520
617
  }
521
618
  function requireSessionIntercom(context) {
522
619
  const service = readSessionIntercom(context);
@@ -527,9 +624,9 @@ function requireSessionIntercom(context) {
527
624
  //#region src/services/sessionIntercom/index.ts
528
625
  const MESSAGE_KIND = "session-message";
529
626
  const REPORT_KIND = "session-report";
530
- function metadataFor(options, request, reportStatus) {
531
- const group = options.groups.get(request.groupId);
532
- if (!group || !options.groups.canMessage(request.groupId, options.selfReference, request.targetReference)) throw new Error(`Session group '${request.groupId}' does not authorize this message.`);
627
+ async function metadataFor(options, request, reportStatus) {
628
+ const group = await options.groups.get(request.groupId);
629
+ if (!group || !await options.groups.canMessage(request.groupId, options.selfReference, request.targetReference)) throw new Error(`Session group '${request.groupId}' does not authorize this message.`);
533
630
  return {
534
631
  sourceReference: options.selfReference,
535
632
  targetReference: request.targetReference,
@@ -539,12 +636,21 @@ function metadataFor(options, request, reportStatus) {
539
636
  ...reportStatus === void 0 ? {} : { reportStatus }
540
637
  };
541
638
  }
639
+ /** The group IDs, among those given, whose current membership includes this session. */
640
+ async function groupMembership(options, groupIds) {
641
+ const memberOf = /* @__PURE__ */ new Set();
642
+ for (const groupId of new Set(groupIds)) {
643
+ if (groupId === void 0) continue;
644
+ if ((await options.groups.get(groupId))?.members.includes(options.selfReference) === true) memberOf.add(groupId);
645
+ }
646
+ return memberOf;
647
+ }
542
648
  /** Agent-facing intercom operations. A composition may acquire this service without gaining session controls. */
543
649
  function createSessionIntercom(options) {
544
650
  const deliver = async (request, kind, status) => {
545
- const recipientKey = options.directory.resolveDeliveryTarget(options.selfReference, request.targetReference);
651
+ const recipientKey = await options.directory.resolveDeliveryTarget(options.selfReference, request.targetReference);
546
652
  if (!recipientKey) throw new Error(`Session '${request.targetReference}' is not discoverable.`);
547
- const metadata = metadataFor(options, request, status);
653
+ const metadata = await metadataFor(options, request, status);
548
654
  return options.delivery.deliver({
549
655
  ...request.deliveryId === void 0 ? {} : { deliveryId: request.deliveryId },
550
656
  recipientKey,
@@ -554,16 +660,18 @@ function createSessionIntercom(options) {
554
660
  });
555
661
  };
556
662
  return Object.freeze({
557
- discover() {
558
- const members = new Set(options.groups.groupsFor(options.selfReference).flatMap((group) => group.members).filter((reference) => reference !== options.selfReference));
663
+ async discover() {
664
+ const members = new Set((await options.groups.groupsFor(options.selfReference)).flatMap((group) => group.members).filter((reference) => reference !== options.selfReference));
559
665
  return options.directory.discover(options.selfReference, members);
560
666
  },
561
667
  send: (request) => deliver(request, MESSAGE_KIND),
562
668
  report: (request) => deliver(request, REPORT_KIND, request.status),
563
- messages() {
564
- return options.delivery.inbox().filter((entry) => entry.kind === MESSAGE_KIND || entry.kind === REPORT_KIND).filter((entry) => {
669
+ async messages() {
670
+ const entries = (await options.delivery.inbox()).filter((entry) => entry.kind === MESSAGE_KIND || entry.kind === REPORT_KIND);
671
+ const memberOf = await groupMembership(options, entries.map((entry) => entry.metadata.groupId));
672
+ return entries.filter((entry) => {
565
673
  const groupId = entry.metadata.groupId;
566
- return groupId !== void 0 && options.groups.get(groupId)?.members.includes(options.selfReference) === true;
674
+ return groupId !== void 0 && memberOf.has(groupId);
567
675
  }).flatMap((entry) => {
568
676
  const sourceReference = entry.metadata.sourceReference;
569
677
  const targetReference = entry.metadata.targetReference;
@@ -583,38 +691,38 @@ function createSessionIntercom(options) {
583
691
  }];
584
692
  });
585
693
  },
586
- delivery(deliveryId) {
587
- const entry = options.delivery.outbox(deliveryId);
694
+ async delivery(deliveryId) {
695
+ const entry = await options.delivery.outbox(deliveryId);
588
696
  const groupId = entry?.metadata.groupId;
589
- return entry && groupId !== void 0 && options.groups.get(groupId)?.members.includes(options.selfReference) === true ? entry : void 0;
697
+ return entry && groupId !== void 0 && (await groupMembership(options, [groupId])).has(groupId) ? entry : void 0;
590
698
  }
591
699
  });
592
700
  }
593
701
  /** Builds the dynamic delivery check used before every send, retry, and recipient acceptance. */
594
702
  function createSessionGroupDeliveryAuthorizer(options) {
595
- return (peerKey, metadata) => {
703
+ return async (peerKey, metadata) => {
596
704
  const sourceReference = metadata?.sourceReference;
597
705
  const targetReference = metadata?.targetReference;
598
706
  const groupId = metadata?.groupId;
599
707
  if (sourceReference && targetReference && groupId) {
600
708
  const peerReference = options.peerReference(peerKey);
601
709
  if (!peerReference) return false;
602
- return (sourceReference === options.selfReference && targetReference === peerReference || targetReference === options.selfReference && sourceReference === peerReference) && options.groups.canMessage(groupId, sourceReference, targetReference);
710
+ return (sourceReference === options.selfReference && targetReference === peerReference || targetReference === options.selfReference && sourceReference === peerReference) && await options.groups.canMessage(groupId, sourceReference, targetReference);
603
711
  }
604
- return options.fallback?.(peerKey, metadata) === true;
712
+ return await options.fallback?.(peerKey, metadata) === true;
605
713
  };
606
714
  }
607
715
  //#endregion
608
716
  //#region src/services/sessionDelivery/type.ts
609
- const DOOM_SESSION_DELIVERY_SERVICE = "doom/session-delivery";
717
+ const PO_SESSION_DELIVERY_SERVICE = "po/session-delivery";
610
718
  /** Reads the currently active provider each time, without retaining a stale binding. */
611
- function readDoomSessionDelivery(context) {
612
- return context.get(DOOM_SESSION_DELIVERY_SERVICE);
719
+ function readPoSessionDelivery(context) {
720
+ return context.get(PO_SESSION_DELIVERY_SERVICE);
613
721
  }
614
722
  //#endregion
615
723
  //#region src/services/sessionDelivery/index.ts
616
- const DELIVERY_EVENT = "doom/session-delivery/envelope";
617
- const ACK_EVENT = "doom/session-delivery/ack";
724
+ const DELIVERY_EVENT = "po/session-delivery/envelope";
725
+ const ACK_EVENT = "po/session-delivery/ack";
618
726
  const DEFAULT_DELIVERY = "prompt";
619
727
  const INBOX_STATES = /* @__PURE__ */ new Set([
620
728
  "accepted",
@@ -665,112 +773,127 @@ function matchesMetadata(entry, query) {
665
773
  function sameMetadata(left, right) {
666
774
  return matchesMetadata(left, right) && matchesMetadata(right, left);
667
775
  }
776
+ const SCHEMA = `
777
+ CREATE TABLE IF NOT EXISTS session_delivery_outbox (
778
+ delivery_id TEXT PRIMARY KEY,
779
+ recipient_key TEXT NOT NULL,
780
+ kind TEXT NOT NULL,
781
+ prompt TEXT NOT NULL,
782
+ delivery_mode TEXT NOT NULL,
783
+ metadata_json TEXT NOT NULL,
784
+ state TEXT NOT NULL,
785
+ recipient_state TEXT,
786
+ created_at INTEGER NOT NULL,
787
+ acknowledged_at INTEGER
788
+ );
789
+ CREATE TABLE IF NOT EXISTS session_delivery_inbox (
790
+ delivery_id TEXT PRIMARY KEY,
791
+ sender_key TEXT NOT NULL,
792
+ recipient_key TEXT NOT NULL,
793
+ kind TEXT NOT NULL,
794
+ prompt TEXT NOT NULL,
795
+ delivery_mode TEXT NOT NULL,
796
+ metadata_json TEXT NOT NULL,
797
+ state TEXT NOT NULL,
798
+ created_at INTEGER NOT NULL,
799
+ updated_at INTEGER NOT NULL,
800
+ consumed_at INTEGER
801
+ );
802
+ `;
803
+ const SET_INBOX_STATE = "UPDATE session_delivery_inbox SET state = ?, updated_at = ? WHERE delivery_id = ?";
804
+ const UNSETTLED_OUTBOX = "(recipient_state IS NULL OR recipient_state NOT IN ('admitted', 'recovery_required'))";
668
805
  /** Opens and starts one durable delivery endpoint. */
669
- function createSessionDeliveryService(options) {
806
+ async function createSessionDeliveryService(options) {
670
807
  if (!options.recipientKey.trim() || options.recipientKey !== options.recipientKey.trim()) throw new TypeError("Session delivery requires a trimmed, non-empty recipient key.");
671
- const database = new node_sqlite.DatabaseSync(options.databasePath);
672
- database.exec(`
673
- PRAGMA journal_mode = WAL;
674
- CREATE TABLE IF NOT EXISTS session_delivery_outbox (
675
- delivery_id TEXT PRIMARY KEY,
676
- recipient_key TEXT NOT NULL,
677
- kind TEXT NOT NULL,
678
- prompt TEXT NOT NULL,
679
- delivery_mode TEXT NOT NULL,
680
- metadata_json TEXT NOT NULL,
681
- state TEXT NOT NULL,
682
- recipient_state TEXT,
683
- created_at INTEGER NOT NULL,
684
- acknowledged_at INTEGER
685
- );
686
- CREATE TABLE IF NOT EXISTS session_delivery_inbox (
687
- delivery_id TEXT PRIMARY KEY,
688
- sender_key TEXT NOT NULL,
689
- recipient_key TEXT NOT NULL,
690
- kind TEXT NOT NULL,
691
- prompt TEXT NOT NULL,
692
- delivery_mode TEXT NOT NULL,
693
- metadata_json TEXT NOT NULL,
694
- state TEXT NOT NULL,
695
- created_at INTEGER NOT NULL,
696
- updated_at INTEGER NOT NULL,
697
- consumed_at INTEGER
698
- );
699
- `);
700
- database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE state = 'admitting'").run(Date.now());
808
+ const database = options.database;
809
+ try {
810
+ await database.exec(SCHEMA);
811
+ await database.run("UPDATE session_delivery_inbox SET state = 'accepted', updated_at = ? WHERE state = 'admitting'", Date.now());
812
+ } catch (error) {
813
+ await database.close();
814
+ throw error;
815
+ }
701
816
  let closed = false;
702
817
  const scheduled = /* @__PURE__ */ new Set();
703
818
  let admissionQueue = Promise.resolve();
819
+ const pending = /* @__PURE__ */ new Set();
820
+ /** Tracks fire-and-forget work so close() can wait for it before the database closes. */
821
+ const track = (work) => {
822
+ const settled = work.catch(() => void 0);
823
+ pending.add(settled);
824
+ settled.finally(() => pending.delete(settled));
825
+ };
826
+ const authorized = async (peerKey, metadata) => await options.authorizePeer(peerKey, metadata) === true;
704
827
  const publishAck = (deliveryId, senderKey, state) => {
705
828
  options.communication.publish(senderKey, ACK_EVENT, {
706
829
  deliveryId,
707
830
  state
708
831
  });
709
832
  };
710
- const readInboxRow = (deliveryId) => database.prepare("SELECT * FROM session_delivery_inbox WHERE delivery_id = ?").get(deliveryId);
833
+ const readInboxRow = (deliveryId) => database.get("SELECT * FROM session_delivery_inbox WHERE delivery_id = ?", deliveryId);
711
834
  const scheduleAdmission = (deliveryId) => {
712
835
  if (closed || scheduled.has(deliveryId)) return;
713
836
  scheduled.add(deliveryId);
714
837
  admissionQueue = admissionQueue.then(async () => {
715
838
  try {
716
839
  if (closed) return;
717
- const row = readInboxRow(deliveryId);
718
- if (!row || row.state !== "accepted") return;
719
- database.prepare("UPDATE session_delivery_inbox SET state = 'admitting', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
840
+ const row = await database.get("UPDATE session_delivery_inbox SET state = 'admitting', updated_at = ? WHERE delivery_id = ? AND state = 'accepted' RETURNING *", Date.now(), deliveryId);
841
+ if (!row) return;
720
842
  publishAck(deliveryId, row.sender_key, "admitting");
721
843
  try {
722
- await options.admitPrompt(row.prompt, row.delivery_mode);
844
+ await options.admitPrompt(row.prompt, row.delivery_mode, deliveryId);
723
845
  } catch {
724
- database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
846
+ await database.run(SET_INBOX_STATE, "recovery_required", Date.now(), deliveryId);
725
847
  publishAck(deliveryId, row.sender_key, "recovery_required");
726
848
  return;
727
849
  }
728
- database.prepare("UPDATE session_delivery_inbox SET state = 'admitted', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
850
+ await database.run(SET_INBOX_STATE, "admitted", Date.now(), deliveryId);
729
851
  publishAck(deliveryId, row.sender_key, "admitted");
730
852
  } finally {
731
853
  scheduled.delete(deliveryId);
732
854
  }
733
- }).catch(() => {
855
+ }).catch(async () => {
734
856
  scheduled.delete(deliveryId);
735
857
  try {
736
- const row = readInboxRow(deliveryId);
858
+ const row = await readInboxRow(deliveryId);
737
859
  if (row?.state !== "admitting") return;
738
- database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
860
+ await database.run(SET_INBOX_STATE, "recovery_required", Date.now(), deliveryId);
739
861
  try {
740
862
  publishAck(deliveryId, row.sender_key, "recovery_required");
741
863
  } catch {}
742
864
  } catch {}
743
865
  });
744
866
  };
745
- const receiveEnvelope = (senderKey, value) => {
867
+ const receiveEnvelope = async (senderKey, value) => {
746
868
  if (closed) return void 0;
747
869
  const envelope = envelopeOf(value);
748
- if (!envelope || !options.authorizePeer(senderKey, envelope.metadata)) return void 0;
749
- database.prepare(`INSERT OR IGNORE INTO session_delivery_inbox
750
- (delivery_id, sender_key, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at, updated_at)
751
- VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?)`).run(envelope.deliveryId, senderKey, options.recipientKey, envelope.kind, envelope.prompt, envelope.delivery, JSON.stringify(envelope.metadata), Date.now(), Date.now());
752
- const current = readInboxRow(envelope.deliveryId);
870
+ if (!envelope || !await authorized(senderKey, envelope.metadata) || closed) return void 0;
871
+ await database.run(`INSERT OR IGNORE INTO session_delivery_inbox
872
+ (delivery_id, sender_key, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at, updated_at)
873
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?)`, envelope.deliveryId, senderKey, options.recipientKey, envelope.kind, envelope.prompt, envelope.delivery, JSON.stringify(envelope.metadata), Date.now(), Date.now());
874
+ const current = await readInboxRow(envelope.deliveryId);
753
875
  if (!current) return void 0;
754
876
  if (!(current.sender_key === senderKey && current.recipient_key === options.recipientKey && current.kind === envelope.kind && current.prompt === envelope.prompt && current.delivery_mode === envelope.delivery && sameMetadata(JSON.parse(current.metadata_json), envelope.metadata))) return void 0;
755
877
  publishAck(envelope.deliveryId, current.sender_key, current.state);
756
878
  if (current.state === "accepted") scheduleAdmission(envelope.deliveryId);
757
879
  return current.state;
758
880
  };
759
- const receiveAck = (recipientKey, value) => {
881
+ const receiveAck = async (recipientKey, value) => {
760
882
  const ack = ackOf(value);
761
883
  if (closed || !ack) return;
762
- database.prepare(`UPDATE session_delivery_outbox
763
- SET state = 'acknowledged', recipient_state = ?, acknowledged_at = ?
764
- WHERE delivery_id = ? AND recipient_key = ?
765
- AND (recipient_state IS NULL
766
- OR (recipient_state = 'accepted' AND ? != 'accepted')
767
- OR (recipient_state = 'admitting' AND ? IN ('admitted', 'recovery_required')))`).run(ack.state, Date.now(), ack.deliveryId, recipientKey, ack.state, ack.state);
884
+ await database.run(`UPDATE session_delivery_outbox
885
+ SET state = 'acknowledged', recipient_state = ?, acknowledged_at = ?
886
+ WHERE delivery_id = ? AND recipient_key = ?
887
+ AND (recipient_state IS NULL
888
+ OR (recipient_state = 'accepted' AND ? != 'accepted')
889
+ OR (recipient_state = 'admitting' AND ? IN ('admitted', 'recovery_required')))`, ack.state, Date.now(), ack.deliveryId, recipientKey, ack.state, ack.state);
768
890
  };
769
- const publishOutbox = (deliveryId) => {
770
- const row = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ? AND (recipient_state IS NULL OR recipient_state NOT IN ('admitted', 'recovery_required'))").get(deliveryId);
891
+ const publishOutbox = async (deliveryId) => {
892
+ if (closed) return;
893
+ const row = await database.get(`SELECT * FROM session_delivery_outbox WHERE delivery_id = ? AND ${UNSETTLED_OUTBOX}`, deliveryId);
771
894
  if (!row) return;
772
895
  const metadata = JSON.parse(row.metadata_json);
773
- if (!options.authorizePeer(row.recipient_key, metadata)) return;
896
+ if (!await authorized(row.recipient_key, metadata) || closed) return;
774
897
  options.communication.publish(row.recipient_key, DELIVERY_EVENT, {
775
898
  deliveryId: row.delivery_id,
776
899
  kind: row.kind,
@@ -779,20 +902,47 @@ function createSessionDeliveryService(options) {
779
902
  metadata
780
903
  });
781
904
  };
782
- const publishQueued = (recipientKey) => {
783
- for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE recipient_key = ? AND (recipient_state IS NULL OR recipient_state NOT IN ('admitted', 'recovery_required'))").all(recipientKey)) publishOutbox(row.delivery_id);
905
+ const publishQueued = async (recipientKey) => {
906
+ if (closed) return;
907
+ for (const row of await database.all(`SELECT delivery_id FROM session_delivery_outbox WHERE recipient_key = ? AND ${UNSETTLED_OUTBOX}`, recipientKey)) await publishOutbox(row.delivery_id);
908
+ };
909
+ let retryTimer;
910
+ const startRetry = () => {
911
+ if (closed || retryTimer) return;
912
+ retryTimer = (0, node_timers.setInterval)(() => track(publishAllQueued()), 1e3);
913
+ retryTimer.unref?.();
914
+ };
915
+ const stopRetry = () => {
916
+ if (retryTimer) (0, node_timers.clearInterval)(retryTimer);
917
+ retryTimer = void 0;
784
918
  };
919
+ let retrying;
785
920
  const publishAllQueued = () => {
786
- for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE recipient_state IS NULL OR recipient_state NOT IN ('admitted', 'recovery_required')").all()) publishOutbox(row.delivery_id);
921
+ retrying ??= (async () => {
922
+ try {
923
+ if (closed) return;
924
+ const rows = await database.all(`SELECT delivery_id FROM session_delivery_outbox WHERE ${UNSETTLED_OUTBOX}`);
925
+ if (rows.length === 0) stopRetry();
926
+ else startRetry();
927
+ for (const row of rows) await publishOutbox(row.delivery_id);
928
+ } finally {
929
+ retrying = void 0;
930
+ }
931
+ })();
932
+ return retrying;
787
933
  };
788
- const releaseEnvelope = options.communication.subscribe(DELIVERY_EVENT, receiveEnvelope);
789
- const releaseAck = options.communication.subscribe(ACK_EVENT, receiveAck);
790
- const releaseReady = options.communication.onPeerReady(publishQueued);
791
- for (const row of database.prepare("SELECT delivery_id, sender_key FROM session_delivery_inbox WHERE state = 'recovery_required'").all()) publishAck(row.delivery_id, row.sender_key, "recovery_required");
792
- for (const row of database.prepare("SELECT delivery_id FROM session_delivery_inbox WHERE state = 'accepted'").all()) scheduleAdmission(row.delivery_id);
793
- publishAllQueued();
794
- const retryTimer = setInterval(publishAllQueued, 1e3);
795
- retryTimer.unref();
934
+ const releaseEnvelope = options.communication.subscribe(DELIVERY_EVENT, (senderKey, payload) => {
935
+ track(receiveEnvelope(senderKey, payload));
936
+ });
937
+ const releaseAck = options.communication.subscribe(ACK_EVENT, (recipientKey, payload) => {
938
+ track(receiveAck(recipientKey, payload));
939
+ });
940
+ const releaseReady = options.communication.onPeerReady((recipientKey) => {
941
+ track(publishQueued(recipientKey));
942
+ });
943
+ for (const row of await database.all("SELECT delivery_id, sender_key FROM session_delivery_inbox WHERE state = 'recovery_required'")) publishAck(row.delivery_id, row.sender_key, "recovery_required");
944
+ for (const row of await database.all("SELECT delivery_id FROM session_delivery_inbox WHERE state = 'accepted'")) scheduleAdmission(row.delivery_id);
945
+ track(publishAllQueued());
796
946
  return Object.freeze({
797
947
  recipientKey: options.recipientKey,
798
948
  async deliver(request) {
@@ -800,36 +950,38 @@ function createSessionDeliveryService(options) {
800
950
  const deliveryId = request.deliveryId ?? crypto.randomUUID();
801
951
  const mode = request.delivery ?? DEFAULT_DELIVERY;
802
952
  if (!deliveryId.trim() || !request.recipientKey.trim() || !request.kind.trim() || !isMode(mode)) throw new TypeError("Delivery id, recipient key, kind, and mode must be valid.");
803
- if (!options.authorizePeer(request.recipientKey, request.metadata)) throw new Error(`Session '${request.recipientKey}' is not an authorized communication peer.`);
953
+ if (!await authorized(request.recipientKey, request.metadata)) throw new Error(`Session '${request.recipientKey}' is not an authorized communication peer.`);
954
+ if (closed) throw new Error("Session delivery service is closed.");
804
955
  const metadata = normalizedMetadata(request.metadata);
805
- database.prepare(`INSERT OR IGNORE INTO session_delivery_outbox
806
- (delivery_id, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at)
807
- VALUES (?, ?, ?, ?, ?, ?, 'queued', ?)`).run(deliveryId, request.recipientKey, request.kind, request.prompt, mode, JSON.stringify(metadata), Date.now());
808
- const current = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
956
+ await database.run(`INSERT OR IGNORE INTO session_delivery_outbox
957
+ (delivery_id, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at)
958
+ VALUES (?, ?, ?, ?, ?, ?, 'queued', ?)`, deliveryId, request.recipientKey, request.kind, request.prompt, mode, JSON.stringify(metadata), Date.now());
959
+ const current = await database.get("SELECT * FROM session_delivery_outbox WHERE delivery_id = ?", deliveryId);
809
960
  if (!current || current.recipient_key !== request.recipientKey || current.kind !== request.kind || current.prompt !== request.prompt || current.delivery_mode !== mode || !sameMetadata(JSON.parse(current.metadata_json), metadata)) throw new TypeError(`Delivery id '${deliveryId}' is already bound to another envelope.`);
810
- publishOutbox(deliveryId);
961
+ startRetry();
962
+ await publishOutbox(deliveryId);
811
963
  return { deliveryId };
812
964
  },
813
965
  async waitForAdmission(deliveryId, timeoutMs = 5e3) {
814
966
  const deadline = Date.now() + Math.max(0, timeoutMs);
815
967
  let state;
816
968
  do {
817
- state = database.prepare("SELECT recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId)?.recipient_state ?? void 0;
969
+ state = (await database.get("SELECT recipient_state FROM session_delivery_outbox WHERE delivery_id = ?", deliveryId))?.recipient_state ?? void 0;
818
970
  if (state === "admitted" || state === "recovery_required") return state;
819
971
  if (Date.now() >= deadline) return state;
820
- await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));
972
+ await new Promise((resolve) => (0, node_timers.setTimeout)(resolve, Math.min(10, deadline - Date.now())));
821
973
  } while (!closed);
822
974
  return state;
823
975
  },
824
- receive(senderKey, kind, payload) {
976
+ async receive(senderKey, kind, payload) {
825
977
  if (kind === DELIVERY_EVENT) return receiveEnvelope(senderKey, payload);
826
978
  if (kind === ACK_EVENT) {
827
- receiveAck(senderKey, payload);
979
+ await receiveAck(senderKey, payload);
828
980
  return "accepted";
829
981
  }
830
982
  },
831
- inbox(query = {}) {
832
- return database.prepare("SELECT * FROM session_delivery_inbox ORDER BY created_at, delivery_id").all().map((row) => {
983
+ async inbox(query = {}) {
984
+ return (await database.all("SELECT * FROM session_delivery_inbox ORDER BY created_at, delivery_id")).map((row) => {
833
985
  const metadata = JSON.parse(row.metadata_json);
834
986
  return {
835
987
  deliveryId: row.delivery_id,
@@ -844,11 +996,11 @@ function createSessionDeliveryService(options) {
844
996
  };
845
997
  }).filter((entry) => (query.kind === void 0 || entry.kind === query.kind) && (query.state === void 0 || entry.state === query.state) && (query.consumed === void 0 || entry.consumed === query.consumed) && matchesMetadata(entry.metadata, query.metadata));
846
998
  },
847
- consume(deliveryId) {
848
- return database.prepare("UPDATE session_delivery_inbox SET consumed_at = ? WHERE delivery_id = ? AND state IN ('admitted', 'recovery_required') AND consumed_at IS NULL").run(Date.now(), deliveryId).changes === 1;
999
+ async consume(deliveryId) {
1000
+ return await database.get("UPDATE session_delivery_inbox SET consumed_at = ? WHERE delivery_id = ? AND state IN ('admitted', 'recovery_required') AND consumed_at IS NULL RETURNING delivery_id", Date.now(), deliveryId) !== void 0;
849
1001
  },
850
- outbox(deliveryId) {
851
- const row = database.prepare("SELECT delivery_id, recipient_key, kind, metadata_json, state, recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
1002
+ async outbox(deliveryId) {
1003
+ const row = await database.get("SELECT delivery_id, recipient_key, kind, metadata_json, state, recipient_state FROM session_delivery_outbox WHERE delivery_id = ?", deliveryId);
852
1004
  if (!row) return void 0;
853
1005
  return {
854
1006
  deliveryId: row.delivery_id,
@@ -862,14 +1014,15 @@ function createSessionDeliveryService(options) {
862
1014
  async close() {
863
1015
  if (closed) return;
864
1016
  closed = true;
865
- clearInterval(retryTimer);
1017
+ stopRetry();
866
1018
  releaseEnvelope();
867
1019
  releaseAck();
868
1020
  releaseReady();
869
1021
  try {
870
1022
  await admissionQueue;
1023
+ await Promise.all(pending);
871
1024
  } finally {
872
- database.close();
1025
+ await database.close();
873
1026
  }
874
1027
  }
875
1028
  });
@@ -881,9 +1034,9 @@ function createSessionDeliveryService(options) {
881
1034
  * `ctx.get` is strict and hides an implementation while its providing fiber is
882
1035
  * LOADING, which is the whole of that fiber's own callback.
883
1036
  */
884
- function provideSessionDeliveryService(context, options) {
885
- const service = createSessionDeliveryService(options);
886
- const unprovide = context.provide(DOOM_SESSION_DELIVERY_SERVICE, service);
1037
+ async function provideSessionDeliveryService(context, options) {
1038
+ const service = await createSessionDeliveryService(options);
1039
+ const unprovide = context.provide(PO_SESSION_DELIVERY_SERVICE, service);
887
1040
  context.effect(() => () => service.close(), "session delivery database");
888
1041
  return {
889
1042
  service,
@@ -894,16 +1047,22 @@ function provideSessionDeliveryService(context, options) {
894
1047
  };
895
1048
  }
896
1049
  //#endregion
897
- Object.defineProperty(exports, "DOOM_SESSION_DELIVERY_SERVICE", {
1050
+ Object.defineProperty(exports, "PEER_REPLAY_DATABASE_FILE", {
1051
+ enumerable: true,
1052
+ get: function() {
1053
+ return PEER_REPLAY_DATABASE_FILE;
1054
+ }
1055
+ });
1056
+ Object.defineProperty(exports, "PO_SESSION_DELIVERY_SERVICE", {
898
1057
  enumerable: true,
899
1058
  get: function() {
900
- return DOOM_SESSION_DELIVERY_SERVICE;
1059
+ return PO_SESSION_DELIVERY_SERVICE;
901
1060
  }
902
1061
  });
903
- Object.defineProperty(exports, "DOOM_SESSION_INTERCOM_SERVICE", {
1062
+ Object.defineProperty(exports, "PO_SESSION_INTERCOM_SERVICE", {
904
1063
  enumerable: true,
905
1064
  get: function() {
906
- return DOOM_SESSION_INTERCOM_SERVICE;
1065
+ return PO_SESSION_INTERCOM_SERVICE;
907
1066
  }
908
1067
  });
909
1068
  Object.defineProperty(exports, "SessionGroupRevisionError", {
@@ -936,6 +1095,12 @@ Object.defineProperty(exports, "createPeerReplayGuard", {
936
1095
  return createPeerReplayGuard;
937
1096
  }
938
1097
  });
1098
+ Object.defineProperty(exports, "createPeerReplayGuardAsync", {
1099
+ enumerable: true,
1100
+ get: function() {
1101
+ return createPeerReplayGuardAsync;
1102
+ }
1103
+ });
939
1104
  Object.defineProperty(exports, "createSessionDeliveryService", {
940
1105
  enumerable: true,
941
1106
  get: function() {
@@ -966,6 +1131,12 @@ Object.defineProperty(exports, "createSessionIntercom", {
966
1131
  return createSessionIntercom;
967
1132
  }
968
1133
  });
1134
+ Object.defineProperty(exports, "ensurePrivateDirectory", {
1135
+ enumerable: true,
1136
+ get: function() {
1137
+ return ensurePrivateDirectory;
1138
+ }
1139
+ });
969
1140
  Object.defineProperty(exports, "parseRemoteSessionReference", {
970
1141
  enumerable: true,
971
1142
  get: function() {
@@ -1002,10 +1173,10 @@ Object.defineProperty(exports, "provideSessionDeliveryService", {
1002
1173
  return provideSessionDeliveryService;
1003
1174
  }
1004
1175
  });
1005
- Object.defineProperty(exports, "readDoomSessionDelivery", {
1176
+ Object.defineProperty(exports, "readPoSessionDelivery", {
1006
1177
  enumerable: true,
1007
1178
  get: function() {
1008
- return readDoomSessionDelivery;
1179
+ return readPoSessionDelivery;
1009
1180
  }
1010
1181
  });
1011
1182
  Object.defineProperty(exports, "readSessionIntercom", {
@@ -1020,6 +1191,12 @@ Object.defineProperty(exports, "readSessionPeerConfig", {
1020
1191
  return readSessionPeerConfig;
1021
1192
  }
1022
1193
  });
1194
+ Object.defineProperty(exports, "readSessionPeerConfigAsync", {
1195
+ enumerable: true,
1196
+ get: function() {
1197
+ return readSessionPeerConfigAsync;
1198
+ }
1199
+ });
1023
1200
  Object.defineProperty(exports, "requireSessionIntercom", {
1024
1201
  enumerable: true,
1025
1202
  get: function() {
@@ -1039,4 +1216,4 @@ Object.defineProperty(exports, "verifyPeerRequest", {
1039
1216
  }
1040
1217
  });
1041
1218
 
1042
- //# sourceMappingURL=sessionDelivery-CkuX6jlw.cjs.map
1219
+ //# sourceMappingURL=sessionDelivery-CGWuQyLV.cjs.map