@agimon-ai/po-session 0.0.0-alpha.37 → 0.0.0-alpha.38

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
@@ -34,6 +34,14 @@ A paired host is configured privately in `~/.po/session/peers.json`. The file mu
34
34
 
35
35
  Use `peer/<host-id>/<session-id>` as a qualified target. `allowedSessionIds` grants durable messages and acknowledgements. `allowedVoiceSessionIds` separately grants bounded Voice client-media relay operations. Neither grant exposes owner, `/host/*`, or `/hub/*` routes.
36
36
 
37
+ ## Host links
38
+
39
+ `po/session-peers` owns the live peer configuration and atomic private writes. Host identity is stable across tunnel URL changes. The existing version-1 format accepts optional URL generations and revocation tombstones; existing allowlist readers remain compatible.
40
+
41
+ The sealed API under `/api/plugins/session-peer` exposes `GET /links` and `POST /link/prepare`, `/link/accept`, `/link/unlink`, and `/link/hint`. Prepare, accept, and unlink require a fresh `peer.link` step-up. Quick-tunnel linking requires a native device-key session; named tunnels also accept passkeys. An invite expires after 60 seconds and is consumed once. The app relays public ECDH material, never the derived link secret.
42
+
43
+ Links begin with empty messaging and Voice allowlists. They do not chain through a third host. Peer delivery is sealed using an authenticated handshake; a URL hint cannot bypass it. Signed rebinds must name the sender itself and advance its URL generation. Unlinking stores a tombstone. Removing a paired device does not remove a host link.
44
+
37
45
  ## Development
38
46
 
39
47
  ```bash
@@ -2,13 +2,76 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_sessionDelivery = require("../sessionDelivery-BAXrVTum.cjs");
5
+ const require_sessionGroupBridge = require("../sessionGroupBridge-DL3QTIaH.cjs");
6
6
  let node_path = require("node:path");
7
- node_path = require_sessionDelivery.__toESM(node_path, 1);
7
+ node_path = require_sessionGroupBridge.__toESM(node_path, 1);
8
8
  let _agimon_ai_po_core_platform = require("@agimon-ai/po-core/platform");
9
9
  let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
10
- let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
11
10
  let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
11
+ let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
12
+ let _agimon_ai_po_core_hubChannel = require("@agimon-ai/po-core/hubChannel");
13
+ let _agimon_ai_po_core_remoteOrigin = require("@agimon-ai/po-core/remoteOrigin");
14
+ let _agimon_ai_po_core_packageApi = require("@agimon-ai/po-core/packageApi");
15
+ //#region src/extensions/(backend)/root.server.ts
16
+ var root_server_default$1 = (0, _agimon_ai_po_core_extensionFile.defineRoot)((plugin) => ({
17
+ value: void 0,
18
+ services: [async (context) => {
19
+ await context.inject([
20
+ _agimon_ai_po_core_platform.PO_FS_SERVICE,
21
+ _agimon_ai_po_core_platform.PO_SQLITE_SERVICE,
22
+ _agimon_ai_po_core_serverPlugin.PO_SERVER_SCOPE_SERVICE
23
+ ], async (scoped) => {
24
+ const homeDirectory = plugin.host.context.homeDirectory;
25
+ if (!homeDirectory) throw new Error("Host links require an explicit host home directory.");
26
+ const peers = await require_sessionGroupBridge.createSessionPeers({
27
+ fs: (0, _agimon_ai_po_core_platform.requirePoFs)(scoped),
28
+ homeDirectory
29
+ });
30
+ await peers.identity();
31
+ const directory = node_path.default.join((0, _agimon_ai_po_core_piSettings.poHomeDirectory)(homeDirectory), "session");
32
+ await require_sessionGroupBridge.ensurePrivateDirectory((0, _agimon_ai_po_core_platform.requirePoFs)(scoped), directory, _agimon_ai_po_core_platform.BACKGROUND_CONTEXT);
33
+ const database = await (0, _agimon_ai_po_core_platform.requirePoSqlite)(scoped).open(node_path.default.join(directory, "groups.sqlite"), _agimon_ai_po_core_platform.BACKGROUND_CONTEXT);
34
+ const groups = await require_sessionGroupBridge.createSessionGroupStore({ database });
35
+ await database.run("DELETE FROM session_group_members WHERE group_id IN (SELECT group_id FROM session_groups WHERE owner_authority = 'session-group-bridge')");
36
+ await database.run("DELETE FROM session_groups WHERE owner_authority = 'session-group-bridge'");
37
+ const bridge = require_sessionGroupBridge.createSessionGroupBridge({
38
+ groups,
39
+ peers,
40
+ onError: (error) => plugin.host.context.onNotice(`Group bridge failed: ${String(error)}`),
41
+ send: async (hostId, advert) => {
42
+ const config = peers.current();
43
+ const peer = config?.peers.find((entry) => entry.hostId === hostId);
44
+ if (!config || !peer) return;
45
+ await require_sessionGroupBridge.authenticatedPeerPost({
46
+ config,
47
+ peer,
48
+ path: "/api/plugins/session-peer/control",
49
+ value: {
50
+ type: "groups",
51
+ groups: advert
52
+ }
53
+ });
54
+ }
55
+ });
56
+ const scope = (0, _agimon_ai_po_core_serverPlugin.requirePoServerScope)(scoped);
57
+ const releasePeers = plugin.host.scope === "global" ? await scope.provide(require_sessionGroupBridge.PO_SESSION_PEERS_CONTRACT, peers) : scoped.provide(require_sessionGroupBridge.PO_SESSION_PEERS_SERVICE, peers);
58
+ let releaseBridge;
59
+ try {
60
+ releaseBridge = plugin.host.scope === "global" ? await scope.provide(require_sessionGroupBridge.PO_SESSION_GROUP_BRIDGE_CONTRACT, bridge) : scoped.provide(require_sessionGroupBridge.PO_SESSION_GROUP_BRIDGE_SERVICE, bridge);
61
+ } catch (error) {
62
+ await Promise.resolve(releasePeers());
63
+ await bridge.close();
64
+ throw error;
65
+ }
66
+ return async () => {
67
+ await releaseBridge();
68
+ await Promise.resolve(releasePeers());
69
+ await bridge.close();
70
+ };
71
+ });
72
+ }]
73
+ }));
74
+ //#endregion
12
75
  //#region src/services/peerInbox/index.ts
13
76
  const inboxes = /* @__PURE__ */ new Map();
14
77
  /**
@@ -30,98 +93,145 @@ async function deliverSessionPeerEnvelope(targetSessionId, sourceKey, type, payl
30
93
  //#region src/extensions/workspaces/sessions/(backend)/root.server.ts
31
94
  var root_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoot)((pluginContext) => ({
32
95
  value: void 0,
33
- services: [require_sessionDelivery.provideBackgroundWorkService, async (context) => {
34
- const agent = pluginContext.agent;
35
- const session = agent?.context.session;
36
- const communication = pluginContext.host.context.sessionCommunication;
37
- const homeDirectory = pluginContext.host.context.homeDirectory;
38
- const sessionService = pluginContext.host.context.sessionService;
39
- if (!agent || !session?.admitPrompt || !communication || !homeDirectory || !sessionService?.canCommunicate) throw new Error("Session delivery requires the headless agent, authenticated session communication, host home directory, and communication policy.");
40
- const fs = (0, _agimon_ai_po_core_platform.requirePoFs)(pluginContext.context);
41
- const sqlite = (0, _agimon_ai_po_core_platform.requirePoSqlite)(pluginContext.context);
42
- const io = _agimon_ai_po_core_platform.BACKGROUND_CONTEXT;
43
- const sessionDirectory = node_path.default.join((0, _agimon_ai_po_core_piSettings.poHomeDirectory)(homeDirectory), "session");
44
- const deliveryDirectory = node_path.default.join(sessionDirectory, "delivery");
45
- await require_sessionDelivery.ensurePrivateDirectory(fs, deliveryDirectory, io);
46
- const peerConfig = await require_sessionDelivery.readSessionPeerConfigAsync(fs, homeDirectory, io);
47
- const peerCommunication = require_sessionDelivery.createPeerCommunication({
48
- local: communication,
49
- config: peerConfig
50
- });
51
- const remotePeerIsGranted = (peerKey) => {
52
- const peer = require_sessionDelivery.parseRemoteSessionReference(peerKey);
53
- return peer !== void 0 && peerConfig?.peers.some((candidate) => candidate.hostId === peer.hostId && candidate.allowedSessionIds.includes(agent.context.sessionId)) === true;
54
- };
55
- const fallbackAuthorization = (peerKey) => sessionService.canCommunicate?.(agent.context.sessionId, peerKey) === true || remotePeerIsGranted(peerKey);
56
- const groups = peerConfig ? await require_sessionDelivery.createSessionGroupStore({ database: await sqlite.open(node_path.default.join(sessionDirectory, "groups.sqlite"), io) }) : void 0;
57
- const localHostId = peerConfig?.hostId;
58
- const selfReference = localHostId ? require_sessionDelivery.peerSessionReference(localHostId, agent.context.sessionId) : void 0;
59
- const groupAuthorization = groups && selfReference && localHostId ? require_sessionDelivery.createSessionGroupDeliveryAuthorizer({
60
- selfReference,
61
- groups,
62
- peerReference: (peerKey) => require_sessionDelivery.parseRemoteSessionReference(peerKey) === void 0 ? require_sessionDelivery.peerSessionReference(localHostId, peerKey) : peerKey,
63
- fallback: fallbackAuthorization
64
- }) : void 0;
65
- let delivery;
66
- try {
67
- delivery = await require_sessionDelivery.provideSessionDeliveryService(context, {
68
- database: await sqlite.open(node_path.default.join(deliveryDirectory, `${agent.context.sessionId}.sqlite`), io),
69
- recipientKey: agent.context.sessionId,
70
- communication: peerCommunication,
71
- authorizePeer: (peerKey, metadata) => groupAuthorization ? groupAuthorization(peerKey, metadata) : fallbackAuthorization(peerKey),
72
- admitPrompt: (prompt, admission, requestId, metadata) => {
73
- if (!metadata.customType) return session.admitPrompt(prompt, admission, void 0, requestId);
74
- if (!session.admitMessage) throw new Error("The session cannot admit a custom delivery.");
75
- return session.admitMessage({
76
- type: metadata.customType,
77
- text: prompt,
78
- display: true,
79
- details: metadata.customDetails === void 0 ? void 0 : JSON.parse(metadata.customDetails)
80
- }, admission === "steer" ? "steer" : "followUp", requestId);
81
- }
82
- });
83
- } catch (error) {
84
- await groups?.close();
85
- throw error;
86
- }
87
- const { service, close: closeDelivery } = delivery;
88
- const unprovide = [];
89
- if (groups && selfReference && peerConfig) {
90
- const directory = require_sessionDelivery.createSessionDirectory({ authorizeDiscovery: async (caller, subject) => {
91
- const reference = require_sessionDelivery.peerSessionReference(subject.hostId, subject.sessionId);
92
- return (await groups.groupsFor(caller)).some((group) => group.members.includes(reference)) ? { capabilities: ["message"] } : void 0;
93
- } });
94
- for (const peer of peerConfig.peers) for (const sessionId of peer.allowedSessionIds) directory.observe({
95
- hostId: peer.hostId,
96
- sessionId,
97
- hostIncarnation: "unknown",
98
- sessionIncarnation: "unknown",
99
- sequence: 0,
100
- observedAt: 0,
101
- staleAfterMs: 0,
102
- deliveryTarget: require_sessionDelivery.peerSessionReference(peer.hostId, sessionId),
103
- reachability: "unknown",
104
- runtime: "unknown",
105
- activity: "unknown",
106
- voice: {
107
- eligible: "unknown",
108
- readiness: "unknown"
109
- }
96
+ services: [require_sessionGroupBridge.provideBackgroundWorkService, (owner) => {
97
+ owner.inject([
98
+ require_sessionGroupBridge.PO_SESSION_PEERS_SERVICE,
99
+ require_sessionGroupBridge.PO_SESSION_GROUP_BRIDGE_SERVICE,
100
+ _agimon_ai_po_core_platform.PO_FS_SERVICE,
101
+ _agimon_ai_po_core_platform.PO_SQLITE_SERVICE
102
+ ], async (context) => {
103
+ const agent = pluginContext.agent;
104
+ const session = agent?.context.session;
105
+ const communication = pluginContext.host.context.sessionCommunication;
106
+ const homeDirectory = pluginContext.host.context.homeDirectory;
107
+ const sessionService = pluginContext.host.context.sessionService;
108
+ if (!agent || !session?.admitPrompt || !communication || !homeDirectory || !sessionService?.canCommunicate) throw new Error("Session delivery requires the headless agent, authenticated session communication, host home directory, and communication policy.");
109
+ const fs = (0, _agimon_ai_po_core_platform.requirePoFs)(context);
110
+ const sqlite = (0, _agimon_ai_po_core_platform.requirePoSqlite)(context);
111
+ const io = _agimon_ai_po_core_platform.BACKGROUND_CONTEXT;
112
+ const sessionDirectory = node_path.default.join((0, _agimon_ai_po_core_piSettings.poHomeDirectory)(homeDirectory), "session");
113
+ const deliveryDirectory = node_path.default.join(sessionDirectory, "delivery");
114
+ await require_sessionGroupBridge.ensurePrivateDirectory(fs, deliveryDirectory, io);
115
+ const peers = require_sessionGroupBridge.requireSessionPeers(context);
116
+ const peerConfig = peers.current();
117
+ const peerCommunication = require_sessionGroupBridge.createPeerCommunication({
118
+ local: communication,
119
+ config: () => peers.current(),
120
+ canPublish: (target, type, payload) => {
121
+ if (type === "po/session-delivery/ack") return true;
122
+ const metadata = payload?.metadata;
123
+ return bridge?.peersFor(agent.context.sessionId).some((peer) => peer.sessionId === target && peer.sharedGroups.some((name) => require_sessionGroupBridge.namedSessionGroupId(name) === metadata?.groupId)) === true;
124
+ },
125
+ onError: (error) => pluginContext.host.context.onNotice(`Peer delivery failed: ${String(error)}`)
110
126
  });
111
- unprovide.push(context.provide(require_sessionDelivery.PO_SESSION_INTERCOM_SERVICE, require_sessionDelivery.createSessionIntercom({
127
+ const remotePeerIsGranted = (peerKey) => {
128
+ const peer = require_sessionGroupBridge.parseRemoteSessionReference(peerKey);
129
+ return peer !== void 0 && peers.current()?.peers.some((candidate) => candidate.hostId === peer.hostId && candidate.allowedSessionIds.includes(agent.context.sessionId)) === true;
130
+ };
131
+ const fallbackAuthorization = (peerKey) => sessionService.canCommunicate?.(agent.context.sessionId, peerKey) === true || remotePeerIsGranted(peerKey);
132
+ const bridge = require_sessionGroupBridge.readSessionGroupBridge(context);
133
+ const groups = bridge?.groups;
134
+ await bridge?.updateLocal(agent.context.sessionId, communication.sessionName ?? agent.context.sessionId, communication.groups ?? []);
135
+ const localHostId = peerConfig?.hostId;
136
+ const selfReference = localHostId ? require_sessionGroupBridge.peerSessionReference(localHostId, agent.context.sessionId) : void 0;
137
+ const groupAuthorization = groups && selfReference && localHostId ? require_sessionGroupBridge.createSessionGroupDeliveryAuthorizer({
112
138
  selfReference,
113
- directory,
114
139
  groups,
115
- delivery: service
116
- })));
117
- }
118
- const unregister = registerSessionPeerInbox(agent.context.sessionId, (sourceKey, kind, payload) => service.receive?.(sourceKey, kind, payload));
119
- return async () => {
120
- unregister();
121
- for (const release of unprovide.reverse()) release();
122
- await closeDelivery();
123
- await groups?.close();
124
- };
140
+ peerReference: (peerKey) => require_sessionGroupBridge.parseRemoteSessionReference(peerKey) === void 0 ? require_sessionGroupBridge.peerSessionReference(localHostId, peerKey) : peerKey,
141
+ fallback: fallbackAuthorization
142
+ }) : void 0;
143
+ let delivery;
144
+ try {
145
+ delivery = await require_sessionGroupBridge.provideSessionDeliveryService(context, {
146
+ database: await sqlite.open(node_path.default.join(deliveryDirectory, `${agent.context.sessionId}.sqlite`), io),
147
+ recipientKey: agent.context.sessionId,
148
+ communication: peerCommunication,
149
+ authorizePeer: (peerKey, metadata) => {
150
+ const remote = require_sessionGroupBridge.parseRemoteSessionReference(peerKey);
151
+ if (remote && !peers.current()?.peers.some((peer) => peer.hostId === remote.hostId)) return false;
152
+ if (remote) {
153
+ if (!bridge?.metadataFor(agent.context.sessionId, peerKey) || !metadata?.groupId || !bridge?.peersFor(agent.context.sessionId).find((peer) => peer.sessionId === peerKey)?.sharedGroups.some((name) => require_sessionGroupBridge.namedSessionGroupId(name) === metadata.groupId)) return false;
154
+ return groupAuthorization ? groupAuthorization(peerKey, metadata) : false;
155
+ }
156
+ return groupAuthorization ? groupAuthorization(peerKey, metadata) : fallbackAuthorization(peerKey);
157
+ },
158
+ admitPrompt: async (prompt, admission, requestId, metadata) => {
159
+ if (!metadata.customType) {
160
+ await session.admitPrompt(prompt, admission, void 0, requestId);
161
+ return;
162
+ }
163
+ if (!session.admitMessage) throw new Error("The session cannot admit a custom delivery.");
164
+ await session.admitMessage({
165
+ type: metadata.customType,
166
+ text: prompt,
167
+ display: true,
168
+ details: metadata.customDetails === void 0 ? void 0 : JSON.parse(metadata.customDetails)
169
+ }, admission === "steer" ? "steer" : "followUp", requestId);
170
+ }
171
+ });
172
+ } catch (error) {
173
+ await bridge?.removeLocal(agent.context.sessionId);
174
+ throw error;
175
+ }
176
+ const { service, close: closeDelivery } = delivery;
177
+ const unprovide = [];
178
+ if (groups && selfReference && peerConfig) {
179
+ const directory = require_sessionGroupBridge.createSessionDirectory({ authorizeDiscovery: (_caller, subject) => {
180
+ const reference = require_sessionGroupBridge.peerSessionReference(subject.hostId, subject.sessionId);
181
+ return bridge?.peersFor(agent.context.sessionId).some((peer) => peer.sessionId === reference) ? { capabilities: ["message"] } : void 0;
182
+ } });
183
+ const observedHosts = /* @__PURE__ */ new Set();
184
+ let sequence = 0;
185
+ const observeRemote = () => {
186
+ const remotePeers = bridge?.peersFor(agent.context.sessionId) ?? [];
187
+ for (const peer of remotePeers) observedHosts.add(require_sessionGroupBridge.parseRemoteSessionReference(peer.sessionId).hostId);
188
+ sequence += 1;
189
+ for (const hostId of observedHosts) directory.replaceHostSnapshot(hostId, "linked", remotePeers.flatMap((peer) => {
190
+ const remote = require_sessionGroupBridge.parseRemoteSessionReference(peer.sessionId);
191
+ return remote.hostId !== hostId ? [] : [{
192
+ hostId,
193
+ sessionId: remote.sessionId,
194
+ label: peer.name,
195
+ hostIncarnation: "linked",
196
+ sessionIncarnation: "live",
197
+ sequence,
198
+ observedAt: Date.now(),
199
+ staleAfterMs: 12e4,
200
+ deliveryTarget: peer.sessionId,
201
+ reachability: "unknown",
202
+ runtime: "unknown",
203
+ activity: "unknown",
204
+ voice: {
205
+ eligible: "unknown",
206
+ readiness: "unknown"
207
+ }
208
+ }];
209
+ }));
210
+ };
211
+ observeRemote();
212
+ unprovide.push(bridge.subscribe(() => {
213
+ observeRemote();
214
+ service.reauthorize?.().catch((error) => pluginContext.host.context.onNotice(`Delivery reauthorization failed: ${String(error)}`));
215
+ }));
216
+ unprovide.push(context.provide(require_sessionGroupBridge.PO_SESSION_INTERCOM_SERVICE, require_sessionGroupBridge.createSessionIntercom({
217
+ selfReference,
218
+ directory,
219
+ groups,
220
+ delivery: service
221
+ })));
222
+ }
223
+ const releaseGroups = pluginContext.host.context.directEvents?.subscribe(_agimon_ai_po_core_hubChannel.PO_SESSION_ATTENTION_EVENT, agent.context.sessionId, () => {
224
+ bridge?.updateLocal(agent.context.sessionId, communication.sessionName ?? agent.context.sessionId, communication.groups ?? []).catch((error) => pluginContext.host.context.onNotice(`Group update failed: ${String(error)}`));
225
+ });
226
+ const unregister = registerSessionPeerInbox(agent.context.sessionId, (sourceKey, kind, payload) => service.receive?.(sourceKey, kind, payload));
227
+ return async () => {
228
+ unregister();
229
+ releaseGroups?.();
230
+ await bridge?.removeLocal(agent.context.sessionId);
231
+ for (const release of unprovide.reverse()) release();
232
+ await closeDelivery();
233
+ };
234
+ });
125
235
  }]
126
236
  }));
127
237
  //#endregion
@@ -138,32 +248,108 @@ function createPeerInboxApi(platform) {
138
248
  start(context) {
139
249
  const homeDirectory = context.homeDirectory;
140
250
  let opened;
251
+ const channels = require_sessionGroupBridge.createSealedPeerChannels();
252
+ let link;
253
+ let announcing = false;
254
+ const announce = () => {
255
+ if (!link || announcing) return;
256
+ announcing = true;
257
+ link.announce().catch((error) => context.onNotice(`Peer URL announcement failed: ${String(error)}`)).finally(() => {
258
+ announcing = false;
259
+ });
260
+ };
261
+ const timer = platform.peers && platform.origin ? setInterval(() => {
262
+ open().then(announce).catch((error) => context.onNotice(`Host links could not start: ${String(error)}`));
263
+ }, 1e3) : void 0;
264
+ timer?.unref?.();
141
265
  const open = () => opened ??= (async () => {
142
266
  if (homeDirectory === void 0) return {
143
267
  config: void 0,
144
268
  replay: void 0
145
269
  };
146
- const config = await require_sessionDelivery.readSessionPeerConfigAsync(platform.fs, homeDirectory, _agimon_ai_po_core_platform.BACKGROUND_CONTEXT);
147
- const file = node_path.default.join(node_path.default.dirname(require_sessionDelivery.sessionPeerConfigPath(homeDirectory)), require_sessionDelivery.PEER_REPLAY_DATABASE_FILE);
148
- await require_sessionDelivery.ensurePrivateDirectory(platform.fs, node_path.default.dirname(file), _agimon_ai_po_core_platform.BACKGROUND_CONTEXT);
270
+ const config = platform.peers ? platform.peers.current() : await require_sessionGroupBridge.readSessionPeerConfigAsync(platform.fs, homeDirectory, _agimon_ai_po_core_platform.BACKGROUND_CONTEXT);
271
+ const file = node_path.default.join(node_path.default.dirname(require_sessionGroupBridge.sessionPeerConfigPath(homeDirectory)), require_sessionGroupBridge.PEER_REPLAY_DATABASE_FILE);
272
+ await require_sessionGroupBridge.ensurePrivateDirectory(platform.fs, node_path.default.dirname(file), _agimon_ai_po_core_platform.BACKGROUND_CONTEXT);
273
+ const replay = await require_sessionGroupBridge.createPeerReplayGuardAsync(await platform.sqlite.open(file, _agimon_ai_po_core_platform.BACKGROUND_CONTEXT));
274
+ if (platform.peers) link = require_sessionGroupBridge.createPeerLink({
275
+ peers: platform.peers,
276
+ groups: platform.groups,
277
+ replay,
278
+ channels,
279
+ origin: platform.origin ?? (() => void 0),
280
+ fetch: platform.fetch
281
+ });
282
+ announce();
149
283
  return {
150
284
  config,
151
- replay: await require_sessionDelivery.createPeerReplayGuardAsync(await platform.sqlite.open(file, _agimon_ai_po_core_platform.BACKGROUND_CONTEXT))
285
+ replay
152
286
  };
153
287
  })().catch((error) => {
154
288
  opened = void 0;
155
289
  throw error;
156
290
  });
291
+ if (platform.peers) open().catch((error) => context.onNotice(`Host links could not start: ${String(error)}`));
157
292
  return {
158
293
  async fetch(request) {
159
- if (request.method !== "POST" || new URL(request.url).pathname !== "/inbox") return response(404, "Not found.");
160
- const { config, replay } = await open();
161
- if (!config) return response(503, "Paired Session delivery is not configured.");
294
+ const pathname = new URL(request.url).pathname;
295
+ const openedState = await open();
296
+ const config = platform.peers ? platform.peers.current() : openedState.config;
297
+ const replay = openedState.replay;
298
+ if (request.method === "GET" && pathname === "/links" && platform.peers) {
299
+ if (!(0, _agimon_ai_po_core_packageApi.poApiCallerFrom)(request.headers)) return response(403, "An authenticated caller is required.");
300
+ return Response.json({
301
+ hostId: config?.hostId,
302
+ peers: config?.peers.map(({ hostId, url, urlGeneration }) => ({
303
+ hostId,
304
+ url,
305
+ urlGeneration
306
+ })) ?? []
307
+ });
308
+ }
309
+ if (request.method !== "POST" || ![
310
+ "/inbox",
311
+ "/control",
312
+ "/link/prepare",
313
+ "/link/accept",
314
+ "/link/unlink",
315
+ "/link/hint"
316
+ ].includes(pathname)) return response(404, "Not found.");
162
317
  const length = request.headers.get("content-length");
163
318
  if (length !== null && (!/^\d+$/u.test(length) || Number(length) > MAX_BODY_BYTES)) return response(413, "Request body is too large.");
164
319
  const body = await request.text();
165
320
  if (Buffer.byteLength(body) > MAX_BODY_BYTES) return response(413, "Request body is too large.");
166
- const peer = require_sessionDelivery.verifyPeerRequest(config, request, body);
321
+ if (pathname.startsWith("/link/")) {
322
+ const caller = (0, _agimon_ai_po_core_packageApi.poApiCallerFrom)(request.headers);
323
+ if (!caller || caller.locality === "remote" && pathname !== "/link/hint" && caller.stepUp !== "verified") return response(403, "Linking hosts needs a fresh peer.link step-up.");
324
+ if (!link) return response(503, "Host links are unavailable.");
325
+ try {
326
+ const input = body === "" ? {} : JSON.parse(body);
327
+ if (pathname === "/link/prepare") return Response.json(await link.prepare());
328
+ if (pathname === "/link/accept") {
329
+ await link.accept(input.invite);
330
+ return Response.json({ linked: true });
331
+ }
332
+ if (typeof input.hostId !== "string") return response(400, "A host identity is required.");
333
+ if (pathname === "/link/unlink") {
334
+ await link.unlink(input.hostId);
335
+ return Response.json({ unlinked: true });
336
+ }
337
+ if (typeof input.url !== "string") return response(400, "A URL hint is required.");
338
+ return Response.json({ rebound: await link.hint(input.hostId, input.url) });
339
+ } catch (error) {
340
+ return response(400, error instanceof Error ? error.message : "Host link failed.");
341
+ }
342
+ }
343
+ if (pathname === "/control") {
344
+ if (!link) return response(503, "Host links are unavailable.");
345
+ try {
346
+ return await link.control(request, body);
347
+ } catch (error) {
348
+ return response(400, error instanceof Error ? error.message : "Invalid peer control.");
349
+ }
350
+ }
351
+ if (!config) return response(503, "Paired Session delivery is not configured.");
352
+ const peer = require_sessionGroupBridge.verifyPeerRequest(config, request, body);
167
353
  if (!peer) return response(401, "Paired Session request is unauthorized.");
168
354
  if (!await replay?.admit(request)) return response(409, "Paired Session request was already received.");
169
355
  let parsed;
@@ -172,17 +358,41 @@ function createPeerInboxApi(platform) {
172
358
  } catch {
173
359
  return response(400, "Paired Session request is malformed.");
174
360
  }
175
- const envelope = require_sessionDelivery.parseSessionPeerEnvelope(parsed);
361
+ let peerChannel;
362
+ if (peer.linkGeneration) {
363
+ const sealed = parsed;
364
+ peerChannel = typeof sealed?.epochId === "string" ? channels.take(peer, sealed.epochId) : void 0;
365
+ const opened = peerChannel?.open(sealed?.envelope);
366
+ if (!opened?.ok) return response(401, "A fresh sealed peer channel is required.");
367
+ try {
368
+ parsed = JSON.parse(Buffer.from(opened.plaintext).toString("utf8"));
369
+ } catch {
370
+ return response(400, "Invalid sealed peer envelope.");
371
+ }
372
+ }
373
+ const envelope = require_sessionGroupBridge.parseSessionPeerEnvelope(parsed);
176
374
  if (!envelope || !DELIVERY_TYPES.has(envelope.type)) return response(400, "Paired Session envelope is invalid.");
177
- if (!peer.allowedSessionIds.includes(envelope.targetSessionId)) return response(403, "Peer is not granted this session.");
178
- const state = await deliverSessionPeerEnvelope(envelope.targetSessionId, require_sessionDelivery.peerSessionReference(peer.hostId, envelope.sourceSessionId), envelope.type, envelope.payload);
375
+ if (!peer.linkGeneration && !peer.allowedSessionIds.includes(envelope.targetSessionId)) return response(403, "Peer is not granted this session.");
376
+ const state = await deliverSessionPeerEnvelope(envelope.targetSessionId, require_sessionGroupBridge.peerSessionReference(peer.hostId, envelope.sourceSessionId), envelope.type, envelope.payload);
377
+ if (state !== void 0 && peerChannel) {
378
+ const sealed = peerChannel.seal(Buffer.from(JSON.stringify({
379
+ accepted: true,
380
+ state
381
+ })));
382
+ if (!sealed.ok) return response(503, "Peer acknowledgement could not be sealed.");
383
+ const text = JSON.stringify(sealed.envelope);
384
+ return new Response(text, { headers: require_sessionGroupBridge.peerRequestHeaders(config.hostId, peer, "RESPONSE", "/inbox", text) });
385
+ }
179
386
  return state === void 0 ? response(404, "Target Session is unavailable.") : Response.json({
180
387
  accepted: true,
181
388
  state
182
389
  }, { status: 202 });
183
390
  },
184
391
  async close() {
185
- await (await opened?.catch(() => void 0))?.replay?.close();
392
+ if (timer) clearInterval(timer);
393
+ const state = await opened?.catch(() => void 0);
394
+ link?.close();
395
+ await state?.replay?.close();
186
396
  }
187
397
  };
188
398
  }
@@ -190,33 +400,79 @@ function createPeerInboxApi(platform) {
190
400
  }
191
401
  //#endregion
192
402
  //#region src/extensions/(backend)/api/session-peer/route.server.ts
193
- var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)(({ context }) => createPeerInboxApi({
194
- fs: (0, _agimon_ai_po_core_platform.requirePoFs)(context),
195
- sqlite: (0, _agimon_ai_po_core_platform.requirePoSqlite)(context)
196
- }));
403
+ var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)(({ context }) => {
404
+ let origin;
405
+ context.inject([_agimon_ai_po_core_remoteOrigin.PO_REMOTE_ORIGIN_SERVICE], (scoped) => {
406
+ origin = scoped.get(_agimon_ai_po_core_remoteOrigin.PO_REMOTE_ORIGIN_SERVICE);
407
+ return () => {
408
+ origin = void 0;
409
+ };
410
+ });
411
+ const hasOrigin = origin !== void 0;
412
+ return createPeerInboxApi({
413
+ fs: (0, _agimon_ai_po_core_platform.requirePoFs)(context),
414
+ sqlite: (0, _agimon_ai_po_core_platform.requirePoSqlite)(context),
415
+ peers: require_sessionGroupBridge.requireSessionPeers(context),
416
+ groups: () => require_sessionGroupBridge.readSessionGroupBridge(context),
417
+ ...hasOrigin ? { origin: () => origin?.current() } : {}
418
+ });
419
+ });
197
420
  //#endregion
198
421
  //#region generated/server.ts
199
422
  const at = (value, context) => typeof value === "function" ? value(context) : value;
423
+ /* v8 ignore next -- generated normalization branch */
424
+ const defined = (values) => values.filter((value) => value !== void 0);
200
425
  const serverPlugin = (0, _agimon_ai_po_core_serverPlugin.defineServerPlugin)({
201
426
  name: "@agimon-ai/po-session",
202
- providerOverrides: (0, _agimon_ai_po_core_serverPlugin.composeServerProviderOverrides)(root_server_default),
203
- injects: (0, _agimon_ai_po_core_extensionFile.composeRootInjects)({ session: [root_server_default] }),
204
- global: (context) => ({ get api() {
205
- return [at(route_server_default, context)];
206
- } }),
207
- workspace: (context) => ({ get api() {
208
- return [at(route_server_default, context)];
209
- } }),
427
+ providerOverrides: (0, _agimon_ai_po_core_serverPlugin.composeServerProviderOverrides)(root_server_default$1, root_server_default),
428
+ injects: (0, _agimon_ai_po_core_extensionFile.composeRootInjects)({
429
+ global: [root_server_default$1],
430
+ workspace: [root_server_default$1],
431
+ session: [root_server_default$1, root_server_default]
432
+ }),
433
+ global: async (context) => {
434
+ const scopeGlobal = await context.scopeRoot("global", root_server_default$1, context);
435
+ const contextGlobal = {
436
+ ...context,
437
+ root: scopeGlobal.value
438
+ };
439
+ return {
440
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeGlobal),
441
+ get api() {
442
+ return [at(route_server_default, contextGlobal)];
443
+ },
444
+ services: [...scopeGlobal.services ?? []]
445
+ };
446
+ },
447
+ workspace: async (context) => {
448
+ const scopeGlobal = await context.scopeRoot("global", root_server_default$1, context);
449
+ const contextGlobal = {
450
+ ...context,
451
+ root: scopeGlobal.value
452
+ };
453
+ return {
454
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeGlobal),
455
+ get api() {
456
+ return defined([context.hosted("global") ? void 0 : at(route_server_default, contextGlobal)]);
457
+ },
458
+ services: [...scopeGlobal.services ?? []]
459
+ };
460
+ },
210
461
  session: async (context) => {
211
- const scopeSession = await context.scopeRoot("session", root_server_default, context);
462
+ const scopeGlobal = await context.scopeRoot("global", root_server_default$1, context);
463
+ const contextGlobal = {
464
+ ...context,
465
+ root: scopeGlobal.value
466
+ };
467
+ const scopeSession = await context.scopeRoot("session", root_server_default, contextGlobal);
212
468
  return {
213
- ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeSession),
469
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeGlobal, scopeSession),
214
470
  get api() {
215
- return [at(route_server_default, context)];
471
+ return defined([context.hosted("global") ? void 0 : at(route_server_default, contextGlobal)]);
216
472
  },
217
- services: [...scopeSession.services ?? []],
473
+ services: [...scopeGlobal.services ?? [], ...scopeSession.services ?? []],
218
474
  get activities() {
219
- return [...scopeSession.activities ?? []];
475
+ return [...scopeGlobal.activities ?? [], ...scopeSession.activities ?? []];
220
476
  }
221
477
  };
222
478
  }