@agimon-ai/po-session 0.0.0-alpha.37 → 0.0.0-alpha.39
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 +8 -0
- package/dist/extensions/server.cjs +377 -121
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +372 -116
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/index.cjs +34 -25
- package/dist/index.d.mts +122 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{sessionDelivery-BAXrVTum.cjs → sessionGroupBridge-DL3QTIaH.cjs} +750 -11
- package/dist/sessionGroupBridge-DL3QTIaH.cjs.map +1 -0
- package/dist/{sessionDelivery-CN7GF38B.mjs → sessionGroupBridge-DQeabG52.mjs} +677 -16
- package/dist/sessionGroupBridge-DQeabG52.mjs.map +1 -0
- package/package.json +7 -5
- package/dist/sessionDelivery-BAXrVTum.cjs.map +0 -1
- package/dist/sessionDelivery-CN7GF38B.mjs.map +0 -1
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
|
|
5
|
+
const require_sessionGroupBridge = require("../sessionGroupBridge-DL3QTIaH.cjs");
|
|
6
6
|
let node_path = require("node:path");
|
|
7
|
-
node_path =
|
|
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: [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
147
|
-
const file = node_path.default.join(node_path.default.dirname(
|
|
148
|
-
await
|
|
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
|
|
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
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 }) =>
|
|
194
|
-
|
|
195
|
-
|
|
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)({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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,
|
|
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
|
}
|