agent-embassy 3.0.0 → 4.0.0

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/CONTRIBUTING.md +92 -200
  3. package/README.md +159 -138
  4. package/SECURITY.md +191 -335
  5. package/dist/src/gateway/broker-check.d.ts +26 -0
  6. package/dist/src/gateway/broker-check.js +89 -0
  7. package/dist/src/gateway/broker-check.js.map +1 -0
  8. package/dist/src/gateway/broker-control.d.ts +72 -0
  9. package/dist/src/gateway/broker-control.js +164 -0
  10. package/dist/src/gateway/broker-control.js.map +1 -0
  11. package/dist/src/gateway/broker.d.ts +117 -0
  12. package/dist/src/gateway/broker.js +186 -0
  13. package/dist/src/gateway/broker.js.map +1 -0
  14. package/dist/src/gateway/claude-peer.d.ts +1 -124
  15. package/dist/src/gateway/claude-peer.js +62 -1053
  16. package/dist/src/gateway/claude-peer.js.map +1 -1
  17. package/dist/src/gateway/codex-stateless-transport.d.ts +0 -12
  18. package/dist/src/gateway/codex-stateless-transport.js +8 -142
  19. package/dist/src/gateway/codex-stateless-transport.js.map +1 -1
  20. package/dist/src/gateway/config.d.ts +2 -6
  21. package/dist/src/gateway/config.js +17 -27
  22. package/dist/src/gateway/config.js.map +1 -1
  23. package/dist/src/gateway/coordinator.d.ts +57 -0
  24. package/dist/src/gateway/coordinator.js +217 -0
  25. package/dist/src/gateway/coordinator.js.map +1 -0
  26. package/dist/src/gateway/core-cli.d.ts +15 -0
  27. package/dist/src/gateway/core-cli.js +282 -0
  28. package/dist/src/gateway/core-cli.js.map +1 -0
  29. package/dist/src/gateway/core-service-command.d.ts +16 -0
  30. package/dist/src/gateway/core-service-command.js +71 -0
  31. package/dist/src/gateway/core-service-command.js.map +1 -0
  32. package/dist/src/gateway/endpoint-directory.d.ts +33 -0
  33. package/dist/src/gateway/endpoint-directory.js +223 -0
  34. package/dist/src/gateway/endpoint-directory.js.map +1 -0
  35. package/dist/src/gateway/federation.d.ts +89 -0
  36. package/dist/src/gateway/federation.js +632 -0
  37. package/dist/src/gateway/federation.js.map +1 -0
  38. package/dist/src/gateway/ledger-codec.d.ts +3 -0
  39. package/dist/src/gateway/ledger-codec.js +159 -0
  40. package/dist/src/gateway/ledger-codec.js.map +1 -0
  41. package/dist/src/gateway/ledger.d.ts +138 -0
  42. package/dist/src/gateway/ledger.js +319 -0
  43. package/dist/src/gateway/ledger.js.map +1 -0
  44. package/dist/src/gateway/local-control.d.ts +23 -0
  45. package/dist/src/gateway/local-control.js +489 -0
  46. package/dist/src/gateway/local-control.js.map +1 -0
  47. package/dist/src/gateway/native-destinations.d.ts +34 -0
  48. package/dist/src/gateway/native-destinations.js +272 -0
  49. package/dist/src/gateway/native-destinations.js.map +1 -0
  50. package/dist/src/gateway/owned-state.d.ts +57 -0
  51. package/dist/src/gateway/owned-state.js +398 -0
  52. package/dist/src/gateway/owned-state.js.map +1 -0
  53. package/dist/src/gateway/provenance-envelope.d.ts +3 -3
  54. package/dist/src/gateway/provenance-envelope.js +3 -23
  55. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  56. package/dist/src/gateway/runtime.d.ts +41 -0
  57. package/dist/src/gateway/runtime.js +175 -0
  58. package/dist/src/gateway/runtime.js.map +1 -0
  59. package/docs/CONFIGURATION.md +178 -266
  60. package/docs/DELIVERY.md +133 -82
  61. package/docs/GATEWAY-ARCHITECTURE.md +276 -943
  62. package/package.json +5 -5
  63. package/skills/embassy-peer/SKILL.md +38 -194
  64. package/skills/embassy-peer/agents/openai.yaml +2 -2
  65. package/dist/src/gateway/claude-helper-client.d.ts +0 -2
  66. package/dist/src/gateway/claude-helper-client.js +0 -2
  67. package/dist/src/gateway/claude-helper-client.js.map +0 -1
  68. package/dist/src/gateway/claude-helper-protocol.d.ts +0 -117
  69. package/dist/src/gateway/claude-helper-protocol.js +0 -122
  70. package/dist/src/gateway/claude-helper-protocol.js.map +0 -1
  71. package/dist/src/gateway/claude-helper-supervisor.d.ts +0 -87
  72. package/dist/src/gateway/claude-helper-supervisor.js +0 -347
  73. package/dist/src/gateway/claude-helper-supervisor.js.map +0 -1
  74. package/dist/src/gateway/claude-helper.d.ts +0 -1
  75. package/dist/src/gateway/claude-helper.js +0 -252
  76. package/dist/src/gateway/claude-helper.js.map +0 -1
  77. package/dist/src/gateway/cli.d.ts +0 -57
  78. package/dist/src/gateway/cli.js +0 -1503
  79. package/dist/src/gateway/cli.js.map +0 -1
  80. package/dist/src/gateway/codex-socket-holder.d.ts +0 -26
  81. package/dist/src/gateway/codex-socket-holder.js +0 -76
  82. package/dist/src/gateway/codex-socket-holder.js.map +0 -1
  83. package/dist/src/gateway/control.d.ts +0 -261
  84. package/dist/src/gateway/control.js +0 -903
  85. package/dist/src/gateway/control.js.map +0 -1
  86. package/dist/src/gateway/peer-client.d.ts +0 -58
  87. package/dist/src/gateway/peer-client.js +0 -217
  88. package/dist/src/gateway/peer-client.js.map +0 -1
  89. package/dist/src/gateway/peer-mailbox.d.ts +0 -78
  90. package/dist/src/gateway/peer-mailbox.js +0 -168
  91. package/dist/src/gateway/peer-mailbox.js.map +0 -1
  92. package/dist/src/gateway/peer-protocol.d.ts +0 -105
  93. package/dist/src/gateway/peer-protocol.js +0 -70
  94. package/dist/src/gateway/peer-protocol.js.map +0 -1
  95. package/dist/src/gateway/peer-stdio.d.ts +0 -23
  96. package/dist/src/gateway/peer-stdio.js +0 -100
  97. package/dist/src/gateway/peer-stdio.js.map +0 -1
  98. package/dist/src/gateway/providers.d.ts +0 -163
  99. package/dist/src/gateway/providers.js +0 -972
  100. package/dist/src/gateway/providers.js.map +0 -1
  101. package/dist/src/gateway/server.d.ts +0 -47
  102. package/dist/src/gateway/server.js +0 -219
  103. package/dist/src/gateway/server.js.map +0 -1
  104. package/dist/src/gateway/service.d.ts +0 -373
  105. package/dist/src/gateway/service.js +0 -2274
  106. package/dist/src/gateway/service.js.map +0 -1
  107. package/dist/src/gateway/status-view.d.ts +0 -167
  108. package/dist/src/gateway/status-view.js +0 -488
  109. package/dist/src/gateway/status-view.js.map +0 -1
  110. package/dist/src/gateway/store.d.ts +0 -185
  111. package/dist/src/gateway/store.js +0 -2359
  112. package/dist/src/gateway/store.js.map +0 -1
  113. package/dist/src/gateway/types.d.ts +0 -528
  114. package/dist/src/gateway/types.js +0 -341
  115. package/dist/src/gateway/types.js.map +0 -1
@@ -1,903 +0,0 @@
1
- /** Private, bounded JSONL control transport over one same-user Unix socket. */
2
- import { randomBytes } from "node:crypto";
3
- import { chmod, lstat, mkdtemp, realpath, rename, rmdir, unlink } from "node:fs/promises";
4
- import net from "node:net";
5
- import path from "node:path";
6
- import { TextDecoder } from "node:util";
7
- import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, isGatewayProvider, isMessageDirection, isPublicRegistryObservationSnapshot, } from "./types.js";
8
- import { decodePeerParams, decodePeerResult } from "./peer-protocol.js";
9
- import { isPeerMailboxAwaitResult } from "./peer-mailbox.js";
10
- export const GATEWAY_CONTROL_PROTOCOL_VERSION = 3;
11
- export const GATEWAY_CONTROL_MAX_FRAME_BYTES = 32 * 1024;
12
- export const GATEWAY_CONTROL_MAX_RESPONSE_BYTES = 256 * 1024;
13
- export const GATEWAY_CONTROL_MAX_MESSAGE_BYTES = 16 * 1024;
14
- export const GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS = 3_000;
15
- export const GATEWAY_CONTROL_MAX_CONNECTIONS = 32;
16
- const MAX_SOCKET_PATH_BYTES = 100;
17
- const MAX_REPLY_ADDRESS_BYTES = 256;
18
- const MAX_REVISION = Number.MAX_SAFE_INTEGER;
19
- const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
20
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
21
- const CONVERSATION_ID_PATTERN = /^conv_[A-Za-z0-9_-]{16,64}$/;
22
- const DELIVERY_TOKEN_PATTERN = /^dlv_[A-Za-z0-9_-]{24}$/;
23
- const PEER_TOKEN_PATTERN = /^peer_[A-Za-z0-9_-]{32}$/;
24
- const PEER_RECEIPT_PATTERN = /^prc_[A-Za-z0-9_-]{24}$/;
25
- const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
26
- const SAFE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
27
- const PROTOCOL_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
28
- const PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:+-]{0,63}$/;
29
- const MESSAGE_SUFFIX_PATTERN = /^[0-9a-f]{8}$/;
30
- const CONVERSATION_SUFFIX_PATTERN = /^[A-Za-z0-9_-]{8}$/;
31
- const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
32
- export const gatewayControlMethods = [
33
- "health", "register_codex", "unregister_codex", "list_snapshot",
34
- "observe_snapshot", "delivery_status", "send",
35
- "refresh_discovery", "peer_catalog", "peer_handoff",
36
- "register_peer", "unregister_peer", "await_peer", "peer_receipt",
37
- ];
38
- /** Bounds on an ephemeral peer registration's lifetime, in milliseconds. */
39
- export const GATEWAY_EPHEMERAL_PEER_TTL_DEFAULT_MS = 300_000;
40
- export const GATEWAY_EPHEMERAL_PEER_TTL_MIN_MS = 1_000;
41
- export const GATEWAY_EPHEMERAL_PEER_TTL_MAX_MS = 3_600_000;
42
- const WIRE_ERROR_MESSAGES = {
43
- INVALID_JSON: "The control frame is not valid JSON.", FRAME_TOO_LARGE: "The control frame exceeds the size limit.",
44
- INVALID_REQUEST: "The control request is invalid.", UNSUPPORTED_VERSION: "The control protocol version is unsupported.",
45
- UNKNOWN_METHOD: "The control method is unsupported.", MULTIPLE_FRAMES: "Only one control request is allowed per connection.",
46
- SERVER_BUSY: "The gateway control server is at its connection limit.", REQUEST_TIMEOUT: "The control request timed out.",
47
- HANDLER_FAILURE: "The gateway could not complete the control request.", INVALID_HANDLER_RESPONSE: "The gateway produced an invalid control response.",
48
- RESPONSE_TOO_LARGE: "The control response exceeds the size limit.",
49
- };
50
- const TRANSPORT_MESSAGES = {
51
- UNSUPPORTED_PLATFORM: "Unix-domain gateway control sockets are unavailable.",
52
- INVALID_STATE_DIR: "The gateway state directory is invalid.",
53
- INSECURE_STATE_DIR: "The gateway state directory is not private to this user.",
54
- INVALID_SOCKET_PATH: "The gateway control socket path is invalid.",
55
- UNSAFE_SOCKET_TARGET: "The gateway control socket target is unsafe.",
56
- SOCKET_IN_USE: "The gateway control socket is already served by a live process.",
57
- SOCKET_PROBE_FAILED: "The gateway control socket could not be checked safely.",
58
- SOCKET_BIND_FAILED: "The gateway control socket could not be bound.",
59
- SOCKET_PERMISSION_FAILED: "The gateway control socket permissions could not be secured.",
60
- SOCKET_CLEANUP_CONFLICT: "The gateway control socket path changed during cleanup.",
61
- CONTROL_CONNECT_FAILED: "The gateway control socket could not be reached.",
62
- CONTROL_CONNECT_DENIED: "The gateway control socket connection was denied by local policy.",
63
- CONTROL_SOCKET_MISSING: "The gateway control socket does not exist at the configured path.",
64
- CONTROL_LISTENER_UNAVAILABLE: "Nothing is listening on the gateway control socket.",
65
- CONTROL_TIMEOUT: "The gateway control request timed out.",
66
- CONTROL_RESPONSE_TOO_LARGE: "The gateway control response exceeds the client limit.",
67
- CONTROL_INVALID_RESPONSE: "The gateway returned an invalid control response. Restart the broker, then retry.",
68
- CONTROL_VERSION_MISMATCH: "The gateway control protocol version does not match this client.",
69
- CONTROL_CONNECTION_CLOSED: "The gateway closed before returning a control response.",
70
- CONTROL_OUTCOME_AMBIGUOUS: "The gateway may have applied the control mutation before the response was lost; do not retry automatically.",
71
- };
72
- export class GatewayControlTransportError extends Error {
73
- code;
74
- ambiguous;
75
- recoverable;
76
- constructor(code, message, ambiguous = false) {
77
- super(message);
78
- this.name = "GatewayControlTransportError";
79
- this.code = code;
80
- this.ambiguous = ambiguous;
81
- this.recoverable = !ambiguous;
82
- }
83
- }
84
- class ProtocolFault extends Error {
85
- code;
86
- constructor(code) {
87
- super(WIRE_ERROR_MESSAGES[code]);
88
- this.name = "ProtocolFault";
89
- this.code = code;
90
- }
91
- }
92
- function controlTransportError(code, ambiguous = false) {
93
- return new GatewayControlTransportError(code, TRANSPORT_MESSAGES[code] ?? "The gateway control transport failed.", ambiguous);
94
- }
95
- function invalid() { throw new ProtocolFault("INVALID_REQUEST"); }
96
- export function createGatewayConversationId() {
97
- return `conv_${randomBytes(18).toString("base64url")}`;
98
- }
99
- export function isGatewayAlias(value) { return ALIAS_PATTERN.test(value); }
100
- export function isClaudeSessionSelector(value) {
101
- return isGatewayAlias(value) || UUID_PATTERN.test(value);
102
- }
103
- export function isGatewayHostId(value) { return HOST_PATTERN.test(value); }
104
- export function isGatewayConversationId(value) { return CONVERSATION_ID_PATTERN.test(value); }
105
- export function isGatewayDeliveryToken(value) { return DELIVERY_TOKEN_PATTERN.test(value); }
106
- export function isGatewayReplyAddress(value) {
107
- if (value.includes("\0") || !value.startsWith("uds:") ||
108
- Buffer.byteLength(value, "utf8") > MAX_REPLY_ADDRESS_BYTES)
109
- return false;
110
- const socketPath = value.slice(4);
111
- return path.isAbsolute(socketPath) && path.resolve(socketPath) === socketPath &&
112
- Buffer.byteLength(socketPath, "utf8") <= MAX_SOCKET_PATH_BYTES;
113
- }
114
- function isRecord(value) {
115
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
116
- return false;
117
- }
118
- const prototype = Object.getPrototypeOf(value);
119
- return prototype === Object.prototype || prototype === null;
120
- }
121
- function hasExactKeys(value, required, optional = []) {
122
- const ownKeys = Reflect.ownKeys(value);
123
- if (ownKeys.some((key) => typeof key !== "string"))
124
- return false;
125
- const allowed = new Set([...required, ...optional]);
126
- return (required.every((key) => Object.hasOwn(value, key)) &&
127
- ownKeys.every((key) => typeof key === "string" && allowed.has(key)));
128
- }
129
- function isNonNegativeInteger(value) {
130
- return (typeof value === "number" &&
131
- Number.isSafeInteger(value) &&
132
- value >= 0 &&
133
- value <= MAX_REVISION);
134
- }
135
- function isIsoTimestamp(value) {
136
- if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value)) {
137
- return false;
138
- }
139
- const parsed = Date.parse(value);
140
- return Number.isFinite(parsed);
141
- }
142
- function isUuid(value) {
143
- return typeof value === "string" && UUID_PATTERN.test(value);
144
- }
145
- function isAlias(value) {
146
- return typeof value === "string" && isGatewayAlias(value);
147
- }
148
- function isHostId(value) {
149
- return typeof value === "string" && isGatewayHostId(value);
150
- }
151
- function isMessageText(value) {
152
- return (typeof value === "string" &&
153
- value.trim().length > 0 &&
154
- !value.includes("\0") &&
155
- Buffer.byteLength(value, "utf8") <= GATEWAY_CONTROL_MAX_MESSAGE_BYTES);
156
- }
157
- const exact = hasExactKeys;
158
- const nonNegative = isNonNegativeInteger;
159
- const iso = isIsoTimestamp;
160
- const uuid = isUuid;
161
- const alias = isAlias;
162
- const host = isHostId;
163
- function shape(value, required, optional = {}) {
164
- if (!isRecord(value) || !exact(value, Object.keys(required), Object.keys(optional)))
165
- return false;
166
- return Object.entries(required).every(([key, check]) => check(value[key])) &&
167
- Object.entries(optional).every(([key, check]) => value[key] === undefined || check(value[key]));
168
- }
169
- function oneOf(...values) { return (value) => values.includes(value); }
170
- function arrayOf(value, maximum, check) {
171
- return Array.isArray(value) && value.length <= maximum && value.every(check);
172
- }
173
- function positive(value) { return nonNegative(value) && value > 0; }
174
- function safeCode(value) { return typeof value === "string" && SAFE_CODE_PATTERN.test(value); }
175
- const messageText = isMessageText;
176
- const peerToken = (value) => typeof value === "string" && PEER_TOKEN_PATTERN.test(value);
177
- const peerAlias = (value) => alias(value) && value.startsWith("peer-");
178
- function emptyParams(value) {
179
- if (!isRecord(value) || !exact(value, []))
180
- invalid();
181
- return {};
182
- }
183
- function decodePeerCatalog(value) {
184
- if (!shape(value, { peerHost: host }))
185
- invalid();
186
- return { peerHost: value.peerHost };
187
- }
188
- function decodePeerHandoff(value) {
189
- if (!isRecord(value) || !exact(value, ["peerHost", "handoff"]) || !host(value.peerHost))
190
- invalid();
191
- try {
192
- return { peerHost: value.peerHost, handoff: decodePeerParams("handoff", value.handoff) };
193
- }
194
- catch {
195
- return invalid();
196
- }
197
- }
198
- function decodeRegisterPeer(value) {
199
- // `ephemeral` belongs only to a first registration: re-presenting a token
200
- // proves an existing route, and an existing route's lifetime is already
201
- // fixed. `ttlMs` without `ephemeral` would name a lifetime nothing honours.
202
- if (!shape(value, { alias: peerAlias }, { token: peerToken, ephemeral: oneOf(true),
203
- ttlMs: (item) => Number.isSafeInteger(item) &&
204
- Number(item) >= GATEWAY_EPHEMERAL_PEER_TTL_MIN_MS &&
205
- Number(item) <= GATEWAY_EPHEMERAL_PEER_TTL_MAX_MS }) ||
206
- (value.ephemeral !== undefined && value.token !== undefined) ||
207
- (value.ttlMs !== undefined && value.ephemeral === undefined))
208
- invalid();
209
- return { alias: value.alias,
210
- ...(value.token === undefined ? {} : { token: value.token }),
211
- ...(value.ephemeral === undefined ? {} : { ephemeral: true }),
212
- ...(value.ttlMs === undefined ? {} : { ttlMs: value.ttlMs }) };
213
- }
214
- function decodePeerPrincipal(value) {
215
- if (!shape(value, { alias: peerAlias, token: peerToken }))
216
- invalid();
217
- return { alias: value.alias, token: value.token };
218
- }
219
- function decodePeerReceipt(value) {
220
- if (!shape(value, { alias: peerAlias, token: peerToken,
221
- receipt: (item) => typeof item === "string" && PEER_RECEIPT_PATTERN.test(item) }))
222
- invalid();
223
- return { alias: value.alias, token: value.token, receipt: value.receipt };
224
- }
225
- const isPeerCatalog = (value) => {
226
- try {
227
- decodePeerResult("catalog/get", value);
228
- return true;
229
- }
230
- catch {
231
- return false;
232
- }
233
- };
234
- const isPeerHandoffResult = (value) => {
235
- try {
236
- decodePeerResult("handoff", value);
237
- return true;
238
- }
239
- catch {
240
- return false;
241
- }
242
- };
243
- function decodeRegister(value) {
244
- if (!isRecord(value) || !exact(value, ["alias", "threadId", "hostId", "busyPolicy"], ["succeedsAlias"]) ||
245
- !alias(value.alias) || !value.alias.startsWith("codex-") || !uuid(value.threadId) ||
246
- !host(value.hostId) || value.busyPolicy !== "queue" ||
247
- (value.succeedsAlias !== undefined && (!alias(value.succeedsAlias) || !value.succeedsAlias.startsWith("codex-"))))
248
- invalid();
249
- if (!value.alias.endsWith(`@${value.hostId}`) ||
250
- (value.succeedsAlias !== undefined &&
251
- (value.succeedsAlias === value.alias || !value.succeedsAlias.endsWith(`@${value.hostId}`))))
252
- invalid();
253
- return { alias: value.alias, threadId: value.threadId.toLowerCase(), hostId: value.hostId,
254
- busyPolicy: value.busyPolicy, ...(value.succeedsAlias === undefined ? {} : { succeedsAlias: value.succeedsAlias }) };
255
- }
256
- function decodeUnregister(value) {
257
- if (!shape(value, { alias, threadId: uuid }) || !value.alias.startsWith("codex-"))
258
- invalid();
259
- return { alias: value.alias, threadId: value.threadId.toLowerCase() };
260
- }
261
- function decodeSend(value) {
262
- if (!isRecord(value) || !exact(value, ["fromAlias", "text"], ["toAlias", "conversationId", "threadId", "replyAddress", "peerToken", "expectsReply"]) ||
263
- !alias(value.fromAlias) || !messageText(value.text) ||
264
- [value.toAlias, value.conversationId].filter((item) => item !== undefined).length !== 1 ||
265
- (value.toAlias !== undefined && (typeof value.toAlias !== "string" || !isClaudeSessionSelector(value.toAlias))) ||
266
- (value.conversationId !== undefined && (typeof value.conversationId !== "string" || !isGatewayConversationId(value.conversationId))) ||
267
- [value.threadId, value.replyAddress, value.peerToken].filter((item) => item !== undefined).length !== 1 ||
268
- (value.threadId !== undefined && !uuid(value.threadId)) ||
269
- (value.replyAddress !== undefined && (typeof value.replyAddress !== "string" || !isGatewayReplyAddress(value.replyAddress))) ||
270
- (value.peerToken !== undefined && !peerToken(value.peerToken)) ||
271
- (value.expectsReply !== undefined &&
272
- (typeof value.expectsReply !== "boolean" || value.conversationId !== undefined)))
273
- invalid();
274
- const authority = value.threadId !== undefined ? { threadId: value.threadId.toLowerCase() }
275
- : value.replyAddress !== undefined ? { replyAddress: value.replyAddress }
276
- : { peerToken: value.peerToken };
277
- const target = value.conversationId !== undefined
278
- ? { conversationId: value.conversationId }
279
- : { toAlias: UUID_PATTERN.test(value.toAlias) ? value.toAlias.toLowerCase() : value.toAlias,
280
- expectsReply: value.expectsReply === true };
281
- return { fromAlias: value.fromAlias, text: value.text, ...target, ...authority };
282
- }
283
- function decodeDeliveryStatus(value) {
284
- if (!shape(value, { token: (item) => typeof item === "string" && DELIVERY_TOKEN_PATTERN.test(item) }))
285
- invalid();
286
- return { token: value.token };
287
- }
288
- function isDecision(value) {
289
- return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "route_mismatch", "busy", "unavailable", "rejected") }) && (value.accepted === true ? value.code === "ok" : value.code !== "ok");
290
- }
291
- function isRegisterPeerResult(value) {
292
- return isDecision(value) || shape(value, { accepted: oneOf(true), code: oneOf("ok"), token: peerToken });
293
- }
294
- function isHealthResult(value) {
295
- return shape(value, { status: oneOf("ok", "degraded"), revision: nonNegative });
296
- }
297
- function isSendResult(value) {
298
- if (!isRecord(value) || value.accepted !== true) {
299
- return shape(value, { accepted: oneOf(false), code: oneOf("not_found", "conflict", "route_mismatch", "busy", "unavailable", "rejected") }, { reason: safeCode });
300
- }
301
- return shape(value, { accepted: oneOf(true), code: oneOf("ok"),
302
- conversationId: (item) => typeof item === "string" && CONVERSATION_ID_PATTERN.test(item),
303
- deliveryToken: (item) => typeof item === "string" && DELIVERY_TOKEN_PATTERN.test(item) });
304
- }
305
- function isRefreshResult(value) {
306
- return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "route_mismatch", "busy", "unavailable", "rejected"), revision: nonNegative }) && isDecision({ accepted: value.accepted, code: value.code });
307
- }
308
- const TERMINAL = new Set(["delivered", "unconfirmed", "expired", "failed", "ambiguous", "cancelled"]);
309
- function isDeliveryStatusResult(value) {
310
- if (!isRecord(value) || typeof value.found !== "boolean")
311
- return false;
312
- if (!value.found)
313
- return exact(value, ["found"]);
314
- return shape(value, { found: oneOf(true), state: oneOf("queued", "stalled", "delivered", "unconfirmed", "expired", "failed", "ambiguous", "cancelled"), terminal: (item) => typeof item === "boolean", updatedAt: iso, deadlineAt: iso }, { pendingForMs: nonNegative, safeErrorCode: safeCode }) && value.terminal === TERMINAL.has(value.state);
315
- }
316
- function isRouteCounters(value) {
317
- const keys = ["accepted", "delivered", "unconfirmed", "failed", "ambiguous", "expired",
318
- "cancelled", "abandoned", "rejected", "bytesAccepted"];
319
- return isRecord(value) && exact(value, keys) && keys.every((key) => nonNegative(value[key]));
320
- }
321
- const isConnectorHealth = oneOf("offline", "connecting", "healthy", "degraded");
322
- function isConnectorSnapshot(value) {
323
- if (!shape(value, { provider: isGatewayProvider, host, health: isConnectorHealth,
324
- protocol: (item) => typeof item === "string" && PROTOCOL_PATTERN.test(item),
325
- protocolVersion: (item) => typeof item === "string" && PROTOCOL_VERSION_PATTERN.test(item) }, { lastSeenAt: iso, observationAgeMs: nonNegative, safeErrorCode: safeCode,
326
- registry: isPublicRegistryObservationSnapshot }))
327
- return false;
328
- return value.registry === undefined || value.provider === "claude";
329
- }
330
- function isRouteSnapshot(value) {
331
- if (!shape(value, { alias, provider: isGatewayProvider, host, enabled: (item) => typeof item === "boolean",
332
- state: oneOf("stale", "idle", "busy", "awaiting_approval", "offline", "disabled"),
333
- busyPolicy: oneOf("queue"), queueDepth: nonNegative, counters: isRouteCounters }, { lastSeenAt: iso, oldestQueuedAt: iso, safeErrorCode: safeCode, mutable: (item) => typeof item === "boolean" }))
334
- return false;
335
- const row = value;
336
- return row.alias.endsWith(`@${row.host}`) &&
337
- (row.oldestQueuedAt === undefined ? row.queueDepth === 0 : row.queueDepth > 0);
338
- }
339
- function isAvailablePeerSnapshot(value) {
340
- return shape(value, { alias, provider: oneOf("claude"), host,
341
- state: oneOf("idle", "busy", "awaiting_approval", "offline"),
342
- validated: (item) => typeof item === "boolean", routed: (item) => typeof item === "boolean" }, { lastSeenAt: iso, safeErrorCode: safeCode }) &&
343
- value.alias.endsWith(`@${String(value.host)}`);
344
- }
345
- function isNormalizedMessageEvent(value) {
346
- if (!shape(value, { sequence: nonNegative, timestamp: iso,
347
- messageIdSuffix: (item) => typeof item === "string" && MESSAGE_SUFFIX_PATTERN.test(item),
348
- direction: isMessageDirection, sourceAlias: alias, targetAlias: alias,
349
- state: oneOf("queued", "duplicate", "dispatching", "transport_written", "held", "delivered", "unconfirmed", "failed", "ambiguous", "expired", "cancelled", "abandoned", "rejected"),
350
- bytes: nonNegative }, { conversationIdSuffix: (item) => typeof item === "string" && CONVERSATION_SUFFIX_PATTERN.test(item),
351
- body: (item) => typeof item === "string" && item.length > 0 && !item.includes("\0"),
352
- latencyMs: nonNegative, safeErrorCode: safeCode, steer: oneOf(true) }))
353
- return false;
354
- return value.body === undefined || Buffer.byteLength(value.body, "utf8") === value.bytes;
355
- }
356
- function isAccounting(value) {
357
- const keys = ["accepted", "duplicates", "delivered", "unconfirmed", "failed", "ambiguous",
358
- "expired", "cancelled", "abandoned", "rejected", "bytesAccepted", "queuedBytes"];
359
- return isRecord(value) && exact(value, keys) && keys.every((key) => nonNegative(value[key]));
360
- }
361
- function isSafeAlert(value) {
362
- return shape(value, { code: safeCode, severity: oneOf("info", "warning", "error"), timestamp: iso }, { provider: isGatewayProvider, host, alias });
363
- }
364
- function isGatewayActivityEvent(value) {
365
- if (!shape(value, { sequence: positive, timestamp: iso,
366
- kind: oneOf("discovery", "registration"),
367
- action: oneOf("discovery_refreshed", "codex_registered", "codex_succeeded", "codex_unregistered", "claude_route_installed", "claude_route_retired"),
368
- outcome: oneOf("accepted", "rejected"), aliases: (rows) => arrayOf(rows, 2, alias),
369
- operatorAction: oneOf(true) }, { safeErrorCode: safeCode }))
370
- return false;
371
- const allowed = {
372
- discovery: ["discovery_refreshed"],
373
- registration: ["codex_registered", "codex_succeeded", "codex_unregistered",
374
- "claude_route_installed", "claude_route_retired"],
375
- };
376
- return new Set(value.aliases).size === value.aliases.length &&
377
- (allowed[value.kind]?.includes(value.action) ?? false);
378
- }
379
- function isDeadlinePressure(value) {
380
- const names = ["under_1m", "1m_to_5m", "5m_to_15m", "15m_to_60m", "over_60m"];
381
- if (!shape(value, { configuredDeadlineMs: positive, terminalEvents: nonNegative,
382
- expiredEvents: nonNegative, buckets: (rows) => Array.isArray(rows) && rows.length === names.length }, { retainedSince: iso }) || Number(value.expiredEvents) > Number(value.terminalEvents))
383
- return false;
384
- const rows = value.buckets;
385
- if (!rows.every((row, index) => shape(row, { bucket: oneOf(names[index]), settled: nonNegative, expired: nonNegative }) &&
386
- row.expired <= row.settled))
387
- return false;
388
- return rows.reduce((sum, row) => sum + Number(row.settled), 0) === value.terminalEvents &&
389
- rows.reduce((sum, row) => sum + Number(row.expired), 0) === value.expiredEvents;
390
- }
391
- function isSnapshotTruncation(value) {
392
- const required = ["connectors", "availablePeers", "routes", "messages", "alerts"];
393
- const optional = ["activityEvents"];
394
- return isRecord(value) && exact(value, required, optional) &&
395
- [...required, ...optional].every((key) => value[key] === undefined || nonNegative(value[key]));
396
- }
397
- export function isGatewaySnapshot(value) {
398
- if (!shape(value, { schemaVersion: oneOf(2), generatedAt: iso,
399
- health: isConnectorHealth,
400
- connectors: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.connectors, isConnectorSnapshot),
401
- availablePeers: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.availablePeers, isAvailablePeerSnapshot),
402
- routes: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.routes, isRouteSnapshot),
403
- messages: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.messages, isNormalizedMessageEvent),
404
- accounting: isAccounting, alerts: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.alerts, isSafeAlert),
405
- truncation: isSnapshotTruncation }, { activityEvents: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.activityEvents, isGatewayActivityEvent),
406
- deadlinePressure: isDeadlinePressure }))
407
- return false;
408
- const snapshot = value;
409
- const unique = (rows) => new Set(rows).size === rows.length;
410
- const generated = Date.parse(snapshot.generatedAt);
411
- const connectorKeys = snapshot.connectors.map((row) => `${row.provider}@${row.host}`);
412
- const aliases = snapshot.routes.map((row) => row.alias);
413
- const peers = snapshot.availablePeers.map((row) => row.alias);
414
- const honest = snapshot.connectors.every((row) => {
415
- const seen = row.lastSeenAt === undefined ? undefined : Date.parse(row.lastSeenAt);
416
- const age = seen === undefined ? undefined : Math.min(MAX_REVISION, Math.max(0, generated - seen));
417
- return (row.observationAgeMs === undefined || row.observationAgeMs === age) &&
418
- (row.health !== "healthy" || (age !== undefined && age <= CONNECTOR_OBSERVATION_STALE_AFTER_MS));
419
- });
420
- return honest && unique(connectorKeys) && unique(aliases) && unique(peers) &&
421
- Buffer.byteLength(JSON.stringify(snapshot), "utf8") <= GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET;
422
- }
423
- function isSnapshotObservation(value) {
424
- return shape(value, { snapshotRevision: nonNegative, snapshot: isGatewaySnapshot });
425
- }
426
- const descriptors = {
427
- health: { handler: "health", decode: emptyParams, result: isHealthResult, mutation: false }, register_codex: { handler: "registerCodex", decode: decodeRegister, result: isDecision, mutation: true },
428
- unregister_codex: { handler: "unregisterCodex", decode: decodeUnregister, result: isDecision, mutation: true },
429
- list_snapshot: { handler: "listSnapshot", decode: emptyParams, result: isGatewaySnapshot, mutation: false }, observe_snapshot: { handler: "observeSnapshot", decode: emptyParams, result: isSnapshotObservation, mutation: false },
430
- delivery_status: { handler: "deliveryStatus", decode: decodeDeliveryStatus, result: isDeliveryStatusResult, mutation: false },
431
- send: { handler: "send", decode: decodeSend, result: isSendResult, mutation: true },
432
- refresh_discovery: { handler: "refreshDiscovery", decode: emptyParams, result: isRefreshResult, mutation: false },
433
- peer_catalog: { handler: "peerCatalog", decode: decodePeerCatalog, result: isPeerCatalog, mutation: false },
434
- peer_handoff: { handler: "peerHandoff", decode: decodePeerHandoff, result: isPeerHandoffResult, mutation: true },
435
- register_peer: { handler: "registerPeer", decode: decodeRegisterPeer, result: isRegisterPeerResult, mutation: true },
436
- unregister_peer: { handler: "unregisterPeer", decode: decodePeerPrincipal, result: isDecision, mutation: true },
437
- await_peer: { handler: "awaitPeer", decode: decodePeerPrincipal, result: isPeerMailboxAwaitResult, mutation: false },
438
- peer_receipt: { handler: "peerReceipt", decode: decodePeerReceipt, result: isDecision, mutation: true },
439
- };
440
- function parseRequestObject(value) {
441
- if (!isRecord(value) || !exact(value, ["protocolVersion", "method", "params"]))
442
- invalid();
443
- if (value.protocolVersion !== GATEWAY_CONTROL_PROTOCOL_VERSION)
444
- throw new ProtocolFault("UNSUPPORTED_VERSION");
445
- if (typeof value.method !== "string" || !Object.hasOwn(descriptors, value.method))
446
- throw new ProtocolFault("UNKNOWN_METHOD");
447
- const method = value.method;
448
- return { protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION, method, params: descriptors[method].decode(value.params) };
449
- }
450
- function decodeJson(frame, invalidCode) {
451
- try {
452
- return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(frame));
453
- }
454
- catch {
455
- throw new ProtocolFault(invalidCode);
456
- }
457
- }
458
- function errorResponse(code) {
459
- return { protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION, ok: false, error: { code, message: WIRE_ERROR_MESSAGES[code] } };
460
- }
461
- async function dispatch(request, handlers) {
462
- const descriptor = descriptors[request.method];
463
- const handler = handlers[descriptor.handler];
464
- try {
465
- const result = await (Object.keys(request.params).length === 0 ? handler() : handler(request.params));
466
- return { protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION, ok: true, result };
467
- }
468
- catch {
469
- return errorResponse("HANDLER_FAILURE");
470
- }
471
- }
472
- function serializeResponse(response) {
473
- try {
474
- const encoded = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
475
- return encoded.length <= GATEWAY_CONTROL_MAX_RESPONSE_BYTES ? encoded : Buffer.from(`${JSON.stringify(errorResponse("RESPONSE_TOO_LARGE"))}\n`);
476
- }
477
- catch {
478
- return Buffer.from(`${JSON.stringify(errorResponse("INVALID_HANDLER_RESPONSE"))}\n`);
479
- }
480
- }
481
- class FrameFault extends Error {
482
- kind;
483
- systemCode;
484
- constructor(kind, systemCode) {
485
- super(kind);
486
- this.kind = kind;
487
- this.systemCode = systemCode;
488
- }
489
- }
490
- function readOneFrame(socket, maximum) {
491
- return new Promise((resolve, reject) => {
492
- let buffered = Buffer.alloc(0);
493
- let settled = false;
494
- const finish = (value) => {
495
- if (settled)
496
- return;
497
- settled = true;
498
- socket.pause();
499
- socket.off("data", onData);
500
- socket.off("end", onEnd);
501
- socket.off("timeout", onTimeout);
502
- value instanceof FrameFault ? reject(value) : resolve(value);
503
- };
504
- const onData = (chunk) => {
505
- if (buffered.length + chunk.length > maximum)
506
- return finish(new FrameFault("too_large"));
507
- buffered = Buffer.concat([buffered, chunk]);
508
- const newline = buffered.indexOf(0x0a);
509
- if (newline < 0)
510
- return;
511
- if (buffered.subarray(newline + 1).toString("utf8").trim().length > 0)
512
- return finish(new FrameFault("multiple"));
513
- finish(buffered.subarray(0, newline));
514
- };
515
- const onEnd = () => finish(new FrameFault("closed"));
516
- const onTimeout = () => finish(new FrameFault("timeout"));
517
- socket.on("data", onData);
518
- socket.once("end", onEnd);
519
- socket.once("timeout", onTimeout);
520
- socket.once("error", (error) => finish(new FrameFault("error", error.code)));
521
- });
522
- }
523
- function respond(socket, response) {
524
- if (!socket.destroyed)
525
- socket.end(serializeResponse(response));
526
- }
527
- function handleConnection(socket, handlers, timeoutMs) {
528
- socket.setTimeout(timeoutMs);
529
- void readOneFrame(socket, GATEWAY_CONTROL_MAX_FRAME_BYTES).then(async (frame) => {
530
- socket.setTimeout(0);
531
- let request;
532
- try {
533
- request = parseRequestObject(decodeJson(frame, "INVALID_JSON"));
534
- }
535
- catch (error) {
536
- respond(socket, errorResponse(error instanceof ProtocolFault ? error.code : "INVALID_REQUEST"));
537
- return;
538
- }
539
- respond(socket, await dispatch(request, handlers));
540
- }, (error) => {
541
- const kind = error instanceof FrameFault ? error.kind : "error";
542
- if (kind === "too_large")
543
- respond(socket, errorResponse("FRAME_TOO_LARGE"));
544
- else if (kind === "multiple")
545
- respond(socket, errorResponse("MULTIPLE_FRAMES"));
546
- else if (kind === "timeout")
547
- respond(socket, errorResponse("REQUEST_TIMEOUT"));
548
- else
549
- socket.destroy();
550
- });
551
- }
552
- async function optionalLstat(target) {
553
- try {
554
- return await lstat(target);
555
- }
556
- catch (error) {
557
- if (error.code === "ENOENT")
558
- return undefined;
559
- throw error;
560
- }
561
- }
562
- function assertSocketPath(socketPath) {
563
- if (!path.isAbsolute(socketPath) ||
564
- path.resolve(socketPath) !== socketPath ||
565
- Buffer.byteLength(socketPath, "utf8") > MAX_SOCKET_PATH_BYTES) {
566
- throw controlTransportError("INVALID_SOCKET_PATH");
567
- }
568
- }
569
- async function validateSocketLocation(stateDir, socketPath) {
570
- if (process.platform === "win32") {
571
- throw controlTransportError("UNSUPPORTED_PLATFORM");
572
- }
573
- assertSocketPath(socketPath);
574
- if (!path.isAbsolute(stateDir) || path.resolve(stateDir) !== stateDir) {
575
- throw controlTransportError("INVALID_SOCKET_PATH");
576
- }
577
- let stateInfo;
578
- try {
579
- stateInfo = await lstat(stateDir);
580
- }
581
- catch {
582
- throw controlTransportError("INVALID_STATE_DIR");
583
- }
584
- if (stateInfo.isSymbolicLink() || !stateInfo.isDirectory()) {
585
- throw controlTransportError("INVALID_STATE_DIR");
586
- }
587
- const getuid = process.getuid;
588
- if ((getuid !== undefined && stateInfo.uid !== getuid.call(process)) ||
589
- (stateInfo.mode & 0o077) !== 0) {
590
- throw controlTransportError("INSECURE_STATE_DIR");
591
- }
592
- let stateReal;
593
- let parentReal;
594
- try {
595
- [stateReal, parentReal] = await Promise.all([
596
- realpath(stateDir),
597
- realpath(path.dirname(socketPath)),
598
- ]);
599
- }
600
- catch {
601
- throw controlTransportError("INVALID_SOCKET_PATH");
602
- }
603
- if (socketPath === stateDir ||
604
- (parentReal !== stateReal &&
605
- !parentReal.startsWith(`${stateReal}${path.sep}`))) {
606
- throw controlTransportError("INVALID_SOCKET_PATH");
607
- }
608
- }
609
- async function socketIsLive(socketPath) {
610
- return await new Promise((resolve, reject) => {
611
- const socket = net.createConnection(socketPath);
612
- let settled = false;
613
- const finish = (action) => {
614
- if (settled)
615
- return;
616
- settled = true;
617
- socket.destroy();
618
- action();
619
- };
620
- socket.setTimeout(250);
621
- socket.once("connect", () => finish(() => resolve(true)));
622
- socket.once("timeout", () => finish(() => reject(controlTransportError("SOCKET_PROBE_FAILED"))));
623
- socket.once("error", (error) => {
624
- if (error.code === "ECONNREFUSED" || error.code === "ENOENT") {
625
- finish(() => resolve(false));
626
- }
627
- else {
628
- finish(() => reject(controlTransportError("SOCKET_PROBE_FAILED")));
629
- }
630
- });
631
- });
632
- }
633
- function sameIdentity(info, identity) {
634
- return info.dev === identity.dev && info.ino === identity.ino;
635
- }
636
- async function prepareSocketTarget(socketPath) {
637
- let existing;
638
- try {
639
- existing = await optionalLstat(socketPath);
640
- }
641
- catch {
642
- throw controlTransportError("UNSAFE_SOCKET_TARGET");
643
- }
644
- if (!existing)
645
- return;
646
- if (existing.isSymbolicLink() || !existing.isSocket()) {
647
- throw controlTransportError("UNSAFE_SOCKET_TARGET");
648
- }
649
- const identity = { dev: existing.dev, ino: existing.ino };
650
- if (await socketIsLive(socketPath)) {
651
- throw controlTransportError("SOCKET_IN_USE");
652
- }
653
- let current;
654
- try {
655
- current = await lstat(socketPath);
656
- }
657
- catch {
658
- throw controlTransportError("UNSAFE_SOCKET_TARGET");
659
- }
660
- if (!current.isSocket() || !sameIdentity(current, identity)) {
661
- throw controlTransportError("UNSAFE_SOCKET_TARGET");
662
- }
663
- try {
664
- await unlink(socketPath);
665
- }
666
- catch {
667
- throw controlTransportError("UNSAFE_SOCKET_TARGET");
668
- }
669
- }
670
- async function closeNativeServer(server) {
671
- await new Promise((resolve) => {
672
- try {
673
- server.close(() => resolve());
674
- }
675
- catch {
676
- resolve();
677
- }
678
- });
679
- }
680
- async function removeOwnedSocket(socketPath, identity) {
681
- let current;
682
- try {
683
- current = await optionalLstat(socketPath);
684
- }
685
- catch {
686
- return;
687
- }
688
- if (!current)
689
- return;
690
- if (!current.isSocket() || !sameIdentity(current, identity))
691
- return;
692
- try {
693
- await unlink(socketPath);
694
- }
695
- catch {
696
- // Never broaden cleanup to a replacement path.
697
- }
698
- }
699
- async function protectSocketReplacement(socketPath, identity) {
700
- let current;
701
- try {
702
- current = await optionalLstat(socketPath);
703
- }
704
- catch {
705
- throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
706
- }
707
- if (!current || sameIdentity(current, identity))
708
- return undefined;
709
- let directory;
710
- try {
711
- directory = await mkdtemp(path.join(path.dirname(socketPath), ".gateway-control-close-"));
712
- }
713
- catch {
714
- throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
715
- }
716
- const backupPath = path.join(directory, "replacement");
717
- try {
718
- await rename(socketPath, backupPath);
719
- }
720
- catch (error) {
721
- await rmdir(directory).catch(() => undefined);
722
- if (error.code === "ENOENT")
723
- return undefined;
724
- throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
725
- }
726
- return { directory, backupPath };
727
- }
728
- async function restoreSocketReplacement(socketPath, replacement) {
729
- if (!replacement)
730
- return;
731
- let current;
732
- try {
733
- current = await optionalLstat(socketPath);
734
- }
735
- catch {
736
- throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
737
- }
738
- if (current) {
739
- throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
740
- }
741
- try {
742
- await rename(replacement.backupPath, socketPath);
743
- await rmdir(replacement.directory);
744
- }
745
- catch {
746
- throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
747
- }
748
- }
749
- export async function startGatewayControlServer(options) {
750
- await validateSocketLocation(options.stateDir, options.socketPath);
751
- await prepareSocketTarget(options.socketPath);
752
- const requestTimeoutMs = options.requestTimeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
753
- if (!Number.isInteger(requestTimeoutMs) ||
754
- requestTimeoutMs < 50 ||
755
- requestTimeoutMs > 30_000) {
756
- throw controlTransportError("INVALID_SOCKET_PATH");
757
- }
758
- const connections = new Set();
759
- const server = net.createServer({ allowHalfOpen: true }, (socket) => {
760
- if (connections.size >= GATEWAY_CONTROL_MAX_CONNECTIONS) {
761
- socket.once("error", () => socket.destroy());
762
- socket.end(serializeResponse(errorResponse("SERVER_BUSY")), () => socket.destroy());
763
- return;
764
- }
765
- connections.add(socket);
766
- socket.once("close", () => connections.delete(socket));
767
- handleConnection(socket, options.handlers, requestTimeoutMs);
768
- });
769
- try {
770
- await new Promise((resolve, reject) => {
771
- const onError = () => reject(controlTransportError("SOCKET_BIND_FAILED"));
772
- server.once("error", onError);
773
- server.listen(options.socketPath, () => {
774
- server.off("error", onError);
775
- resolve();
776
- });
777
- });
778
- }
779
- catch (error) {
780
- await closeNativeServer(server);
781
- throw error;
782
- }
783
- let identity;
784
- try {
785
- const info = await lstat(options.socketPath);
786
- if (!info.isSocket())
787
- throw new Error("not a socket");
788
- identity = { dev: info.dev, ino: info.ino };
789
- await chmod(options.socketPath, 0o600);
790
- const secured = await lstat(options.socketPath);
791
- if (!secured.isSocket() ||
792
- !sameIdentity(secured, identity) ||
793
- (secured.mode & 0o777) !== 0o600) {
794
- throw new Error("socket identity changed");
795
- }
796
- }
797
- catch {
798
- for (const socket of connections)
799
- socket.destroy();
800
- await closeNativeServer(server);
801
- if (identity)
802
- await removeOwnedSocket(options.socketPath, identity);
803
- throw controlTransportError("SOCKET_PERMISSION_FAILED");
804
- }
805
- const ownedIdentity = identity;
806
- let closed = false;
807
- let closePromise;
808
- const close = () => {
809
- closePromise ??= (async () => {
810
- const replacement = await protectSocketReplacement(options.socketPath, ownedIdentity);
811
- for (const socket of connections)
812
- socket.destroy();
813
- await closeNativeServer(server);
814
- await removeOwnedSocket(options.socketPath, ownedIdentity);
815
- await restoreSocketReplacement(options.socketPath, replacement);
816
- closed = true;
817
- })();
818
- return closePromise;
819
- };
820
- return {
821
- socketPath: options.socketPath,
822
- get closed() {
823
- return closed;
824
- },
825
- close,
826
- };
827
- }
828
- function decodeResponse(method, params, value) {
829
- if (!isRecord(value) || !Number.isSafeInteger(value.protocolVersion))
830
- throw controlTransportError("CONTROL_INVALID_RESPONSE");
831
- if (value.protocolVersion !== GATEWAY_CONTROL_PROTOCOL_VERSION)
832
- throw controlTransportError("CONTROL_VERSION_MISMATCH");
833
- if (value.ok === false) {
834
- if (!shape(value, { protocolVersion: oneOf(GATEWAY_CONTROL_PROTOCOL_VERSION), ok: oneOf(false), error: (item) => shape(item, { code: (code) => typeof code === "string" && Object.hasOwn(WIRE_ERROR_MESSAGES, code),
835
- message: (message) => typeof message === "string" }) }) ||
836
- !isRecord(value.error) || value.error.message !== WIRE_ERROR_MESSAGES[value.error.code])
837
- throw controlTransportError("CONTROL_INVALID_RESPONSE");
838
- return value;
839
- }
840
- if (!shape(value, { protocolVersion: oneOf(GATEWAY_CONTROL_PROTOCOL_VERSION), ok: oneOf(true), result: descriptors[method].result }))
841
- throw controlTransportError("CONTROL_INVALID_RESPONSE");
842
- if (method === "register_peer" && isRecord(value.result) && value.result.accepted === true &&
843
- Object.hasOwn(params, "token") === Object.hasOwn(value.result, "token"))
844
- throw controlTransportError("CONTROL_INVALID_RESPONSE");
845
- if (method === "await_peer" && isRecord(value.result) && value.result.state === "message" &&
846
- JSON.parse(value.result.frame).result.toAlias !==
847
- params.alias)
848
- throw controlTransportError("CONTROL_INVALID_RESPONSE");
849
- return value;
850
- }
851
- export async function sendGatewayControlRequest(options) {
852
- assertSocketPath(options.socketPath);
853
- const timeout = options.timeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
854
- const maximum = options.maxResponseBytes ?? GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
855
- if (!Number.isInteger(timeout) || timeout < 50 || timeout > 90_000 ||
856
- !Number.isInteger(maximum) || maximum < 256 || maximum > GATEWAY_CONTROL_MAX_RESPONSE_BYTES)
857
- throw controlTransportError("CONTROL_INVALID_RESPONSE");
858
- const request = parseRequestObject(options.request);
859
- const frame = Buffer.from(`${JSON.stringify(request)}\n`, "utf8");
860
- if (frame.length > GATEWAY_CONTROL_MAX_FRAME_BYTES)
861
- throw new ProtocolFault("FRAME_TOO_LARGE");
862
- return await new Promise((resolve, reject) => {
863
- const socket = net.createConnection(options.socketPath);
864
- let settled = false;
865
- let writeStarted = false;
866
- const mutation = descriptors[request.method].mutation;
867
- const fail = (error) => {
868
- if (settled)
869
- return;
870
- settled = true;
871
- socket.destroy();
872
- reject(mutation && writeStarted ? controlTransportError("CONTROL_OUTCOME_AMBIGUOUS", true) : error);
873
- };
874
- socket.setTimeout(timeout);
875
- socket.once("connect", () => { writeStarted = true; socket.write(frame); });
876
- void readOneFrame(socket, maximum).then((responseFrame) => {
877
- try {
878
- const response = decodeResponse(options.request.method, request.params, decodeJson(responseFrame, "INVALID_JSON"));
879
- if (!settled) {
880
- settled = true;
881
- socket.destroy();
882
- resolve(response);
883
- }
884
- }
885
- catch (error) {
886
- fail(error instanceof GatewayControlTransportError
887
- ? error : controlTransportError("CONTROL_INVALID_RESPONSE"));
888
- }
889
- }, (error) => {
890
- const kind = error instanceof FrameFault ? error.kind : "error";
891
- const systemCode = !writeStarted && error instanceof FrameFault ? error.systemCode : undefined;
892
- const code = kind === "timeout" ? "CONTROL_TIMEOUT" :
893
- kind === "too_large" ? "CONTROL_RESPONSE_TOO_LARGE" :
894
- kind === "closed" ? "CONTROL_CONNECTION_CLOSED" :
895
- systemCode === "EPERM" || systemCode === "EACCES"
896
- ? "CONTROL_CONNECT_DENIED" : systemCode === "ENOENT"
897
- ? "CONTROL_SOCKET_MISSING" : systemCode === "ECONNREFUSED"
898
- ? "CONTROL_LISTENER_UNAVAILABLE" : "CONTROL_CONNECT_FAILED";
899
- fail(controlTransportError(code));
900
- });
901
- });
902
- }
903
- //# sourceMappingURL=control.js.map