wake-bridge 0.9.0-preview.8

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/LICENSE +201 -0
  2. package/README.md +222 -0
  3. package/dist/src/adapters/botlingknows.d.ts +42 -0
  4. package/dist/src/adapters/botlingknows.js +141 -0
  5. package/dist/src/adapters/botlingknows.js.map +1 -0
  6. package/dist/src/adapters/group-chat.d.ts +43 -0
  7. package/dist/src/adapters/group-chat.js +120 -0
  8. package/dist/src/adapters/group-chat.js.map +1 -0
  9. package/dist/src/cli.d.ts +2 -0
  10. package/dist/src/cli.js +637 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/connector-catalog.d.ts +17 -0
  13. package/dist/src/connector-catalog.js +43 -0
  14. package/dist/src/connector-catalog.js.map +1 -0
  15. package/dist/src/connectors/botlingknows-mcp.d.ts +15 -0
  16. package/dist/src/connectors/botlingknows-mcp.js +130 -0
  17. package/dist/src/connectors/botlingknows-mcp.js.map +1 -0
  18. package/dist/src/connectors/group-chat-http.d.ts +17 -0
  19. package/dist/src/connectors/group-chat-http.js +226 -0
  20. package/dist/src/connectors/group-chat-http.js.map +1 -0
  21. package/dist/src/connectors/mcp-http-client.d.ts +11 -0
  22. package/dist/src/connectors/mcp-http-client.js +108 -0
  23. package/dist/src/connectors/mcp-http-client.js.map +1 -0
  24. package/dist/src/core.d.ts +170 -0
  25. package/dist/src/core.js +1720 -0
  26. package/dist/src/core.js.map +1 -0
  27. package/dist/src/daemon.d.ts +31 -0
  28. package/dist/src/daemon.js +605 -0
  29. package/dist/src/daemon.js.map +1 -0
  30. package/dist/src/db.d.ts +31 -0
  31. package/dist/src/db.js +513 -0
  32. package/dist/src/db.js.map +1 -0
  33. package/dist/src/event-sdk.d.ts +43 -0
  34. package/dist/src/event-sdk.js +152 -0
  35. package/dist/src/event-sdk.js.map +1 -0
  36. package/dist/src/host-adapter-registry.d.ts +21 -0
  37. package/dist/src/host-adapter-registry.js +59 -0
  38. package/dist/src/host-adapter-registry.js.map +1 -0
  39. package/dist/src/index.d.ts +5 -0
  40. package/dist/src/index.js +6 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/instance-config.d.ts +46 -0
  43. package/dist/src/instance-config.js +176 -0
  44. package/dist/src/instance-config.js.map +1 -0
  45. package/dist/src/mcp.d.ts +51 -0
  46. package/dist/src/mcp.js +728 -0
  47. package/dist/src/mcp.js.map +1 -0
  48. package/dist/src/operator-control.d.ts +82 -0
  49. package/dist/src/operator-control.js +220 -0
  50. package/dist/src/operator-control.js.map +1 -0
  51. package/dist/src/out-of-process-host.d.ts +37 -0
  52. package/dist/src/out-of-process-host.js +196 -0
  53. package/dist/src/out-of-process-host.js.map +1 -0
  54. package/dist/src/policy-control.d.ts +3 -0
  55. package/dist/src/policy-control.js +120 -0
  56. package/dist/src/policy-control.js.map +1 -0
  57. package/dist/src/release-lifecycle.d.ts +98 -0
  58. package/dist/src/release-lifecycle.js +378 -0
  59. package/dist/src/release-lifecycle.js.map +1 -0
  60. package/dist/src/sdk-error.d.ts +6 -0
  61. package/dist/src/sdk-error.js +12 -0
  62. package/dist/src/sdk-error.js.map +1 -0
  63. package/dist/src/source-adapter.d.ts +32 -0
  64. package/dist/src/source-adapter.js +164 -0
  65. package/dist/src/source-adapter.js.map +1 -0
  66. package/dist/src/source-connector.d.ts +50 -0
  67. package/dist/src/source-connector.js +358 -0
  68. package/dist/src/source-connector.js.map +1 -0
  69. package/dist/src/source-control-client.d.ts +38 -0
  70. package/dist/src/source-control-client.js +61 -0
  71. package/dist/src/source-control-client.js.map +1 -0
  72. package/dist/src/source-ingress.d.ts +16 -0
  73. package/dist/src/source-ingress.js +47 -0
  74. package/dist/src/source-ingress.js.map +1 -0
  75. package/dist/src/source-sdk.d.ts +6 -0
  76. package/dist/src/source-sdk.js +53 -0
  77. package/dist/src/source-sdk.js.map +1 -0
  78. package/dist/src/transport-sdk.d.ts +102 -0
  79. package/dist/src/transport-sdk.js +222 -0
  80. package/dist/src/transport-sdk.js.map +1 -0
  81. package/dist/src/transport.d.ts +23 -0
  82. package/dist/src/transport.js +71 -0
  83. package/dist/src/transport.js.map +1 -0
  84. package/dist/src/types.d.ts +522 -0
  85. package/dist/src/types.js +2 -0
  86. package/dist/src/types.js.map +1 -0
  87. package/dist/src/util.d.ts +26 -0
  88. package/dist/src/util.js +136 -0
  89. package/dist/src/util.js.map +1 -0
  90. package/dist/src/version.d.ts +1 -0
  91. package/dist/src/version.js +2 -0
  92. package/dist/src/version.js.map +1 -0
  93. package/docs/adapters/botlingknows.md +40 -0
  94. package/docs/adapters/gmail-planned.md +16 -0
  95. package/docs/adapters/group-chat.md +126 -0
  96. package/docs/operations/connector-catalog.md +40 -0
  97. package/docs/operations/operator-control.md +83 -0
  98. package/docs/operations/out-of-process-host-adapter.md +150 -0
  99. package/docs/operations/policies.md +106 -0
  100. package/docs/operations/public-sdk.md +87 -0
  101. package/docs/operations/release-lifecycle.md +154 -0
  102. package/docs/operations/source-connectors.md +163 -0
  103. package/docs/operations/source-ingress.md +108 -0
  104. package/docs/operations/templates/botlingknows-connector.env.example +5 -0
  105. package/docs/operations/templates/sources.json.example +15 -0
  106. package/docs/quickstart.md +143 -0
  107. package/docs/releases/0.9.0-preview.8.md +44 -0
  108. package/docs/verification.md +19 -0
  109. package/examples/policies/botlingknows-conservative.json +44 -0
  110. package/examples/reference-host-adapter/README.md +41 -0
  111. package/examples/reference-host-adapter/adapter.mjs +173 -0
  112. package/examples/reference-host-adapter/host-adapters.json.example +26 -0
  113. package/package.json +94 -0
  114. package/schemas/event-v1.schema.json +26 -0
  115. package/schemas/policy-v1.schema.json +66 -0
@@ -0,0 +1,605 @@
1
+ import { createServer } from "node:http";
2
+ import { createHash, timingSafeEqual } from "node:crypto";
3
+ import { WakeBridge, BridgeError } from "./core.js";
4
+ import { validateEndpointForAdapter, } from "./transport-sdk.js";
5
+ import { HostAdapterRegistry } from "./host-adapter-registry.js";
6
+ import { WakeBridgeSdkError } from "./sdk-error.js";
7
+ import { SourceSupervisor } from "./source-connector.js";
8
+ import { validateSourceIngestCredential } from "./source-ingress.js";
9
+ import { validatePolicyFile } from "./policy-control.js";
10
+ import { operatorStatus, retryDeadLetter } from "./operator-control.js";
11
+ function jsonResponse(response, status, value) {
12
+ const body = JSON.stringify(value);
13
+ response.statusCode = status;
14
+ response.setHeader("content-type", "application/json; charset=utf-8");
15
+ response.setHeader("content-length", Buffer.byteLength(body));
16
+ response.end(body);
17
+ }
18
+ async function readJson(request) {
19
+ const chunks = [];
20
+ let bytes = 0;
21
+ for await (const chunk of request) {
22
+ const buffer = Buffer.from(chunk);
23
+ bytes += buffer.length;
24
+ if (bytes > 1_048_576)
25
+ throw new BridgeError("request body is too large", "oversized_request", 413);
26
+ chunks.push(buffer);
27
+ }
28
+ if (!chunks.length)
29
+ return {};
30
+ try {
31
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
32
+ if (!value || typeof value !== "object" || Array.isArray(value))
33
+ throw new Error("JSON object required");
34
+ return value;
35
+ }
36
+ catch (error) {
37
+ throw new BridgeError(`invalid JSON body: ${String(error)}`, "invalid_json", 400);
38
+ }
39
+ }
40
+ function authorized(request, bridge) {
41
+ const header = request.headers.authorization || request.headers["x-wakebridge-token"];
42
+ const value = Array.isArray(header) ? header[0] : header;
43
+ if (!value || !bridge.config.admin_token)
44
+ return false;
45
+ const token = value.startsWith("Bearer ") ? value.slice(7) : value;
46
+ return secretEqual(token, bridge.config.admin_token);
47
+ }
48
+ function bearerValue(request) {
49
+ const header = request.headers.authorization;
50
+ const value = Array.isArray(header) ? header[0] : header;
51
+ return typeof value === "string" && value.startsWith("Bearer ") ? value.slice(7) : "";
52
+ }
53
+ function secretEqual(actual, expected) {
54
+ const left = Buffer.from(actual, "utf8");
55
+ const right = Buffer.from(expected, "utf8");
56
+ return left.length > 0 && left.length === right.length && timingSafeEqual(left, right);
57
+ }
58
+ function hostCredential(request, credentials, adapterKind) {
59
+ const token = bearerValue(request);
60
+ return credentials.find((credential) => credential.adapter_kind === adapterKind && secretEqual(token, credential.token)) ?? null;
61
+ }
62
+ function sourceCredential(request, credentials, source) {
63
+ const token = bearerValue(request);
64
+ return credentials.find((credential) => credential.source === source && secretEqual(token, credential.token)) ?? null;
65
+ }
66
+ function loopbackHost(host) {
67
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
68
+ }
69
+ function validateCredentialSeparation(adminToken, hosts, sources) {
70
+ const credentials = [
71
+ ...(adminToken ? [{ kind: "owner", id: "admin", token: adminToken }] : []),
72
+ ...hosts.map((credential) => ({ kind: "host", id: credential.id, token: credential.token })),
73
+ ...sources.map((credential) => ({ kind: "source", id: credential.id, token: credential.token })),
74
+ ];
75
+ for (let left = 0; left < credentials.length; left += 1) {
76
+ for (let right = left + 1; right < credentials.length; right += 1) {
77
+ if (secretEqual(credentials[left].token, credentials[right].token)) {
78
+ throw new BridgeError(`credential token is reused across credential scopes: ${credentials[left].kind}/${credentials[left].id} and ${credentials[right].kind}/${credentials[right].id}`, "credential_scope_collision", 400);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ function endpointId(instanceId, hostKind, sessionRef) {
84
+ return `ep_${createHash("sha256").update(`${instanceId}\0${hostKind}\0${sessionRef}`).digest("hex").slice(0, 32)}`;
85
+ }
86
+ function hostSessionEndpoint(endpoint) {
87
+ return {
88
+ ...endpoint,
89
+ routes: endpoint.routes.map((route) => ({
90
+ ...route,
91
+ address: Object.fromEntries(Object.entries(route.address).map(([key, value]) => [
92
+ key,
93
+ /token|secret|authorization|bearer/i.test(key) ? "[redacted]" : value,
94
+ ])),
95
+ })),
96
+ };
97
+ }
98
+ function pathParts(url) {
99
+ return url.split("?")[0].split("/").filter(Boolean).map((part) => decodeURIComponent(part));
100
+ }
101
+ /** Start the local management API. It binds loopback by default. */
102
+ export async function startDaemon(config, options = {}) {
103
+ const host = options.host ?? "127.0.0.1";
104
+ if (options.unsafe_no_auth && !loopbackHost(host)) {
105
+ throw new BridgeError("unsafe_no_auth is restricted to an explicit loopback bind", "unsafe_daemon_bind", 400);
106
+ }
107
+ if (!config.admin_token && !options.unsafe_no_auth) {
108
+ throw new BridgeError("admin_token is required; use unsafe_no_auth only for an explicit loopback development daemon", "admin_token_required", 400);
109
+ }
110
+ if (config.admin_token && config.admin_token.length < 32) {
111
+ throw new BridgeError("admin_token must contain at least 32 characters", "invalid_admin_token", 400);
112
+ }
113
+ const adapterRegistry = new HostAdapterRegistry(options.host_adapters ?? []);
114
+ const hostCredentials = options.host_credentials ?? [];
115
+ const sourceCredentials = options.source_credentials ?? [];
116
+ if (hostCredentials.length && !config.admin_token) {
117
+ throw new BridgeError("admin_token is required when host bootstrap credentials are enabled", "invalid_host_credential", 400);
118
+ }
119
+ const hostCredentialIds = new Set();
120
+ for (const credential of hostCredentials) {
121
+ if (!credential.id || credential.token.length < 32 || !credential.adapter_kind || !credential.host_kind
122
+ || !Array.isArray(credential.attention_channels) || !credential.attention_channels.length) {
123
+ throw new BridgeError("host bootstrap credential is invalid", "invalid_host_credential", 400);
124
+ }
125
+ if (hostCredentialIds.has(credential.id)) {
126
+ throw new BridgeError(`duplicate host credential id: ${credential.id}`, "invalid_host_credential", 400);
127
+ }
128
+ hostCredentialIds.add(credential.id);
129
+ if (!adapterRegistry.get(credential.adapter_kind)) {
130
+ throw new BridgeError(`host credential references unknown adapter: ${credential.adapter_kind}`, "invalid_host_credential", 400);
131
+ }
132
+ }
133
+ const sourceCredentialIds = new Set();
134
+ for (const credential of sourceCredentials) {
135
+ validateSourceIngestCredential(credential);
136
+ if (sourceCredentialIds.has(credential.id)) {
137
+ throw new BridgeError(`duplicate source credential id: ${credential.id}`, "invalid_source_credential", 400);
138
+ }
139
+ sourceCredentialIds.add(credential.id);
140
+ }
141
+ validateCredentialSeparation(config.admin_token, hostCredentials, sourceCredentials);
142
+ const bridge = options.bridge ?? new WakeBridge(config);
143
+ if (bridge.config.instance_id !== config.instance_id || bridge.config.owner_id !== config.owner_id
144
+ || bridge.config.db_path !== config.db_path || bridge.config.admin_token !== config.admin_token) {
145
+ throw new BridgeError("embedded bridge identity or owner credential does not match daemon config", "daemon_bridge_mismatch", 400);
146
+ }
147
+ const sourceSupervisor = new SourceSupervisor(bridge, options.source_connectors ?? [], options.source_environment, options.source_fetch);
148
+ const server = createServer(async (request, response) => {
149
+ try {
150
+ const method = request.method || "GET";
151
+ const parts = pathParts(request.url || "/");
152
+ const hostSessionRoute = parts[0] === "v1" && parts[1] === "host-sessions";
153
+ if (method === "POST" && hostSessionRoute && parts[2] === "open") {
154
+ const body = await readJson(request);
155
+ const adapterKind = typeof body.adapter_kind === "string" ? body.adapter_kind : "";
156
+ const credential = hostCredential(request, hostCredentials, adapterKind);
157
+ if (!credential) {
158
+ jsonResponse(response, 401, { error: "unauthorized" });
159
+ return;
160
+ }
161
+ const adapter = adapterRegistry.get(adapterKind);
162
+ if (!adapter || !adapter.manifest.host_kinds.includes(credential.host_kind)) {
163
+ throw new BridgeError("host credential does not match adapter manifest", "invalid_host_credential", 400);
164
+ }
165
+ const sessionRef = typeof body.session_ref === "string" ? body.session_ref.trim() : "";
166
+ const attentionChannel = typeof body.attention_channel === "string" ? body.attention_channel.trim() : "";
167
+ const routeAddress = body.route_address;
168
+ if (!sessionRef || sessionRef.length > 256 || !attentionChannel || !credential.attention_channels.includes(attentionChannel)
169
+ || !routeAddress || typeof routeAddress !== "object" || Array.isArray(routeAddress)) {
170
+ throw new BridgeError("host session registration is invalid or outside credential scope", "invalid_host_session", 400);
171
+ }
172
+ const routeToken = typeof routeAddress.token === "string" ? routeAddress.token : "";
173
+ const reservedTokens = [bridge.config.admin_token, ...hostCredentials.map((item) => item.token), ...sourceCredentials.map((item) => item.token)]
174
+ .filter((token) => Boolean(token));
175
+ if (routeToken && reservedTokens.some((token) => secretEqual(routeToken, token))) {
176
+ throw new BridgeError("host delivery route credential must be distinct from owner, host, and source credentials", "credential_scope_collision", 400);
177
+ }
178
+ const registration = {
179
+ id: endpointId(bridge.config.instance_id, credential.host_kind, sessionRef),
180
+ host_kind: credential.host_kind,
181
+ session_ref: sessionRef,
182
+ capabilities: adapter.manifest.capabilities,
183
+ routes: [{ kind: adapter.kind, priority: 100, address: routeAddress }],
184
+ };
185
+ validateEndpointForAdapter(registration, adapter.manifest);
186
+ try {
187
+ adapter.validateEndpoint?.(registration);
188
+ }
189
+ catch (error) {
190
+ throw new BridgeError(`host session route is invalid: ${String(error)}`, "invalid_host_session", 400);
191
+ }
192
+ const currentGeneration = bridge.getBinding(attentionChannel)?.generation ?? 0;
193
+ const endpoint = bridge.registerEndpoint(registration);
194
+ const binding = bridge.takeover(attentionChannel, endpoint.id, currentGeneration);
195
+ jsonResponse(response, 201, { endpoint: hostSessionEndpoint(endpoint), binding });
196
+ return;
197
+ }
198
+ if (method === "POST" && hostSessionRoute && parts[2] === "close" && parts.length === 3) {
199
+ const body = await readJson(request);
200
+ const adapterKind = typeof body.adapter_kind === "string" ? body.adapter_kind : "";
201
+ const credential = hostCredential(request, hostCredentials, adapterKind);
202
+ if (!credential) {
203
+ jsonResponse(response, 401, { error: "unauthorized" });
204
+ return;
205
+ }
206
+ const sessionRef = typeof body.session_ref === "string" ? body.session_ref.trim() : "";
207
+ const attentionChannel = typeof body.attention_channel === "string" ? body.attention_channel.trim() : "";
208
+ const expectedGeneration = Number(body.expected_generation);
209
+ if (!sessionRef || sessionRef.length > 256 || !credential.attention_channels.includes(attentionChannel)
210
+ || !Number.isSafeInteger(expectedGeneration) || expectedGeneration < 1) {
211
+ throw new BridgeError("host session close is invalid or outside credential scope", "invalid_host_session", 400);
212
+ }
213
+ const id = endpointId(bridge.config.instance_id, credential.host_kind, sessionRef);
214
+ const endpoint = bridge.getEndpoint(id);
215
+ if (!endpoint || endpoint.host_kind !== credential.host_kind || endpoint.session_ref !== sessionRef) {
216
+ throw new BridgeError("host session endpoint is unknown", "endpoint_not_found", 404);
217
+ }
218
+ const revoked = bridge.revokeBoundEndpoint(id, attentionChannel, expectedGeneration);
219
+ jsonResponse(response, 200, { endpoint: hostSessionEndpoint(revoked) });
220
+ return;
221
+ }
222
+ if (method === "POST" && hostSessionRoute && parts[2] === "renew" && parts.length === 3) {
223
+ const body = await readJson(request);
224
+ const adapterKind = typeof body.adapter_kind === "string" ? body.adapter_kind : "";
225
+ const credential = hostCredential(request, hostCredentials, adapterKind);
226
+ if (!credential) {
227
+ jsonResponse(response, 401, { error: "unauthorized" });
228
+ return;
229
+ }
230
+ const sessionRef = typeof body.session_ref === "string" ? body.session_ref.trim() : "";
231
+ const attentionChannel = typeof body.attention_channel === "string" ? body.attention_channel.trim() : "";
232
+ const expectedGeneration = Number(body.expected_generation);
233
+ if (!sessionRef || sessionRef.length > 256 || !credential.attention_channels.includes(attentionChannel)
234
+ || !Number.isSafeInteger(expectedGeneration) || expectedGeneration < 1) {
235
+ throw new BridgeError("host session renewal is invalid or outside credential scope", "invalid_host_session", 400);
236
+ }
237
+ const id = endpointId(bridge.config.instance_id, credential.host_kind, sessionRef);
238
+ const endpoint = bridge.getEndpoint(id);
239
+ if (!endpoint || endpoint.host_kind !== credential.host_kind || endpoint.session_ref !== sessionRef) {
240
+ throw new BridgeError("host session endpoint is unknown", "endpoint_not_found", 404);
241
+ }
242
+ const renewed = bridge.renewBoundEndpoint(id, attentionChannel, expectedGeneration);
243
+ jsonResponse(response, 200, { endpoint: hostSessionEndpoint(renewed), binding: bridge.getBinding(attentionChannel) });
244
+ return;
245
+ }
246
+ if (method === "POST" && hostSessionRoute && parts.length === 4 && ["renew", "close", "presence", "activity"].includes(parts[3])) {
247
+ const body = await readJson(request);
248
+ const leaseToken = bearerValue(request);
249
+ if (!leaseToken) {
250
+ jsonResponse(response, 401, { error: "unauthorized" });
251
+ return;
252
+ }
253
+ if (parts[3] === "renew") {
254
+ jsonResponse(response, 200, { endpoint: hostSessionEndpoint(bridge.renewEndpoint(parts[2], leaseToken)) });
255
+ return;
256
+ }
257
+ if (parts[3] === "close") {
258
+ jsonResponse(response, 200, { endpoint: hostSessionEndpoint(bridge.revokeEndpoint(parts[2], leaseToken)) });
259
+ return;
260
+ }
261
+ if (parts[3] === "presence") {
262
+ jsonResponse(response, 200, { presence: bridge.renewPresence({
263
+ attention_channel: String(body.attention_channel || ""),
264
+ endpoint_id: parts[2],
265
+ generation: Number(body.generation),
266
+ lease_token: leaseToken,
267
+ ttl_ms: body.ttl_ms,
268
+ observed_by: String(body.observed_by || ""),
269
+ observation: String(body.observation || "user_message_accepted"),
270
+ }) });
271
+ return;
272
+ }
273
+ const common = {
274
+ attention_channel: String(body.attention_channel || ""),
275
+ endpoint_id: parts[2],
276
+ generation: Number(body.generation),
277
+ lease_token: leaseToken,
278
+ observation_id: String(body.observation_id || ""),
279
+ observed_at: body.observed_at,
280
+ ttl_ms: body.ttl_ms,
281
+ };
282
+ const activity = body.classification === "wake_echo"
283
+ ? bridge.consumeWakeEcho({ ...common, delivery_nonce: String(body.delivery_nonce || "") })
284
+ : bridge.observeActivity({ ...common, kind: body.kind });
285
+ jsonResponse(response, 200, { activity });
286
+ return;
287
+ }
288
+ const scopedSourceIngress = method === "POST" && parts[0] === "v1" && parts[1] === "ingress" && parts[3] === "events" && parts.length === 4;
289
+ if (scopedSourceIngress) {
290
+ const source = parts[2];
291
+ if (!sourceCredential(request, sourceCredentials, source)) {
292
+ jsonResponse(response, 401, { error: "unauthorized" });
293
+ return;
294
+ }
295
+ const body = await readJson(request);
296
+ const event = (body.event && typeof body.event === "object" && !Array.isArray(body.event) ? body.event : body);
297
+ if (Object.prototype.hasOwnProperty.call(body, "source") || Object.prototype.hasOwnProperty.call(event, "source")) {
298
+ throw new BridgeError("source identity is fixed by the ingest credential and path", "source_identity_override", 400);
299
+ }
300
+ jsonResponse(response, 201, bridge.emitEvent(source, event));
301
+ return;
302
+ }
303
+ if (!options.unsafe_no_auth && !authorized(request, bridge)) {
304
+ jsonResponse(response, 401, { error: "unauthorized" });
305
+ return;
306
+ }
307
+ const body = method === "GET" || method === "HEAD" ? {} : await readJson(request);
308
+ if (method === "GET" && parts.join("/") === "health") {
309
+ jsonResponse(response, 200, {
310
+ ok: true,
311
+ instance_id: bridge.config.instance_id,
312
+ owner_id: bridge.config.owner_id,
313
+ now: bridge.nowIso(),
314
+ host_adapters: adapterRegistry.list(),
315
+ });
316
+ return;
317
+ }
318
+ if (method === "GET" && (parts.join("/") === "v1/inspect" || parts.join("/") === "inspect")) {
319
+ jsonResponse(response, 200, bridge.inspect());
320
+ return;
321
+ }
322
+ if (method === "GET" && parts.join("/") === "v1/status") {
323
+ jsonResponse(response, 200, operatorStatus(bridge, {
324
+ sources: sourceSupervisor.list(),
325
+ host_adapters: adapterRegistry.list(),
326
+ }));
327
+ return;
328
+ }
329
+ if (method === "GET" && parts.join("/") === "v1/sources") {
330
+ jsonResponse(response, 200, { sources: sourceSupervisor.list() });
331
+ return;
332
+ }
333
+ if (method === "GET" && parts.join("/") === "v1/policies") {
334
+ jsonResponse(response, 200, { policies: bridge.listPolicies(), active: bridge.listActivePolicies(), status: bridge.listPolicyStatuses() });
335
+ return;
336
+ }
337
+ if (method === "POST" && parts.join("/") === "v1/policies/install") {
338
+ const file = validatePolicyFile(body);
339
+ bridge.installPolicies(file.policies);
340
+ jsonResponse(response, 200, { ok: true, installed: file.policies.map((policy) => ({ id: policy.id, version: policy.version })), active: bridge.listActivePolicies(), status: bridge.listPolicyStatuses() });
341
+ return;
342
+ }
343
+ if (method === "POST" && (parts.join("/") === "v1/policies/test" || parts.join("/") === "v1/policies/preview")) {
344
+ const source = typeof body.source === "string" ? body.source : "";
345
+ const event = body.event;
346
+ if (!source || !event || typeof event !== "object" || Array.isArray(event)) {
347
+ throw new BridgeError("policy evaluation requires source and an event object", "invalid_arguments", 400);
348
+ }
349
+ const candidates = body.policy_file == null ? undefined : validatePolicyFile(body.policy_file).policies;
350
+ jsonResponse(response, 200, parts[2] === "test"
351
+ ? bridge.testPolicy(source, event, candidates)
352
+ : bridge.previewPolicy(source, event, candidates));
353
+ return;
354
+ }
355
+ if (method === "POST" && parts[0] === "v1" && parts[1] === "sources" && parts[3] === "verify") {
356
+ jsonResponse(response, 200, { verification: await sourceSupervisor.verify(parts[2]) });
357
+ return;
358
+ }
359
+ if (method === "POST" && parts[0] === "v1" && parts[1] === "sources" && parts[3] === "bootstrap") {
360
+ if (body.mode !== "from-now")
361
+ throw new BridgeError("source bootstrap mode must be from-now", "invalid_arguments", 400);
362
+ jsonResponse(response, 200, { verification: await sourceSupervisor.bootstrap(parts[2], {
363
+ subject_ref: String(body.expected_subject_ref || ""),
364
+ binding_fingerprint: String(body.expected_binding_fingerprint || ""),
365
+ }) });
366
+ return;
367
+ }
368
+ if (method === "POST" && parts[0] === "v1" && parts[1] === "sources" && parts[3] === "rebind") {
369
+ const revision = Number(body.expected_checkpoint_revision);
370
+ if (!Number.isSafeInteger(revision) || revision < 1)
371
+ throw new BridgeError("expected_checkpoint_revision is invalid", "invalid_arguments", 400);
372
+ const reason = typeof body.reason === "string" ? body.reason : "";
373
+ jsonResponse(response, 200, { verification: await sourceSupervisor.rebind(parts[2], revision, {
374
+ subject_ref: String(body.expected_subject_ref || ""),
375
+ binding_fingerprint: String(body.expected_binding_fingerprint || ""),
376
+ }, reason) });
377
+ return;
378
+ }
379
+ if (method === "POST" && parts[0] === "v1" && parts[1] === "sources" && parts[3] === "enable") {
380
+ jsonResponse(response, 200, { source: await sourceSupervisor.enable(parts[2]) });
381
+ return;
382
+ }
383
+ if (method === "POST" && parts[0] === "v1" && parts[1] === "sources" && parts[3] === "disable") {
384
+ jsonResponse(response, 200, { source: await sourceSupervisor.disable(parts[2]) });
385
+ return;
386
+ }
387
+ if (method === "POST" && parts[0] === "v1" && parts[1] === "sources" && parts[3] === "poll") {
388
+ jsonResponse(response, 200, { source: await sourceSupervisor.runOnce(parts[2]) });
389
+ return;
390
+ }
391
+ const collection = parts[0] === "v1" ? parts[1] : parts[0];
392
+ if (method === "GET" && collection === "events") {
393
+ jsonResponse(response, 200, { events: bridge.listEvents({ limit: Number(new URL(request.url || "/", "http://localhost").searchParams.get("limit") || 100) }) });
394
+ return;
395
+ }
396
+ if (method === "GET" && collection === "claims") {
397
+ jsonResponse(response, 200, { claims: bridge.listClaims({ state: new URL(request.url || "/", "http://localhost").searchParams.get("state") || undefined, channel: new URL(request.url || "/", "http://localhost").searchParams.get("channel") || undefined }) });
398
+ return;
399
+ }
400
+ if (method === "GET" && collection === "batches") {
401
+ jsonResponse(response, 200, { batches: bridge.listBatches({ channel: new URL(request.url || "/", "http://localhost").searchParams.get("channel") || undefined }) });
402
+ return;
403
+ }
404
+ if (method === "GET" && collection === "receipts") {
405
+ jsonResponse(response, 200, { receipts: bridge.listReceipts(parts[2]) });
406
+ return;
407
+ }
408
+ if (method === "GET" && collection === "bindings") {
409
+ jsonResponse(response, 200, { bindings: bridge.listBindings() });
410
+ return;
411
+ }
412
+ if (method === "GET" && collection === "endpoints") {
413
+ jsonResponse(response, 200, { endpoints: bridge.listEndpoints() });
414
+ return;
415
+ }
416
+ if (method === "GET" && collection === "presence") {
417
+ jsonResponse(response, 200, { presence: bridge.listPresence() });
418
+ return;
419
+ }
420
+ if (method === "GET" && collection === "activity-watches") {
421
+ jsonResponse(response, 200, { activity_watches: bridge.listActivityWatches() });
422
+ return;
423
+ }
424
+ if (method === "GET" && collection === "delivery-correlations") {
425
+ jsonResponse(response, 200, { delivery_correlations: bridge.listDeliveryCorrelations() });
426
+ return;
427
+ }
428
+ if (method === "POST" && collection === "events") {
429
+ const source = String(body.source || "manual");
430
+ const event = (body.event && typeof body.event === "object" ? body.event : body);
431
+ delete event.source;
432
+ jsonResponse(response, 201, bridge.emitEvent(source, event));
433
+ return;
434
+ }
435
+ if (method === "POST" && collection === "tick") {
436
+ jsonResponse(response, 200, bridge.tick());
437
+ return;
438
+ }
439
+ if (method === "POST" && collection === "dispatch") {
440
+ jsonResponse(response, 200, { results: await bridge.dispatchDue() });
441
+ return;
442
+ }
443
+ if (method === "POST" && collection === "batches" && parts[3] === "retry") {
444
+ const expectedAttempt = Number(body.expected_attempt);
445
+ if (!Number.isSafeInteger(expectedAttempt) || expectedAttempt < 1) {
446
+ throw new BridgeError("expected_attempt is invalid", "invalid_arguments", 400);
447
+ }
448
+ jsonResponse(response, 200, retryDeadLetter(bridge, {
449
+ batch_id: parts[2],
450
+ expected_attempt: expectedAttempt,
451
+ reason: typeof body.reason === "string" ? body.reason : "",
452
+ }));
453
+ return;
454
+ }
455
+ if (method === "POST" && collection === "claims" && parts[2] === "schedule") {
456
+ jsonResponse(response, 201, bridge.scheduleClaim(body));
457
+ return;
458
+ }
459
+ if (method === "POST" && collection === "claims" && parts[3] === "snooze") {
460
+ jsonResponse(response, 200, { claim: bridge.snoozeClaim(parts[2], String(body.until)) });
461
+ return;
462
+ }
463
+ if (method === "POST" && collection === "claims" && parts[3] === "dismiss") {
464
+ jsonResponse(response, 200, { claim: bridge.dismissClaim(parts[2], String(body.reason || "dismissed")) });
465
+ return;
466
+ }
467
+ if (method === "POST" && collection === "claims" && parts[3] === "consume") {
468
+ jsonResponse(response, 200, { claim: bridge.consumeClaim(parts[2], body.result) });
469
+ return;
470
+ }
471
+ if (method === "POST" && collection === "endpoints" && parts[2] === "register") {
472
+ jsonResponse(response, 201, { endpoint: hostSessionEndpoint(bridge.registerEndpoint(body)) });
473
+ return;
474
+ }
475
+ if (method === "POST" && collection === "endpoints" && parts[3] === "renew") {
476
+ jsonResponse(response, 200, { endpoint: hostSessionEndpoint(bridge.renewEndpoint(parts[2], String(body.lease_token), body.lease_ms)) });
477
+ return;
478
+ }
479
+ if (method === "POST" && collection === "takeover") {
480
+ jsonResponse(response, 200, { binding: bridge.takeover(String(body.attention_channel), String(body.endpoint_id), body.expected_generation == null ? undefined : Number(body.expected_generation)) });
481
+ return;
482
+ }
483
+ if (method === "POST" && collection === "presence" && parts[2] === "renew") {
484
+ jsonResponse(response, 200, { presence: bridge.renewPresence(body) });
485
+ return;
486
+ }
487
+ if (method === "POST" && collection === "activity" && parts[2] === "observe") {
488
+ jsonResponse(response, 200, { activity: bridge.observeActivity(body) });
489
+ return;
490
+ }
491
+ if (method === "POST" && collection === "activity-watches" && parts[2] === "configure") {
492
+ jsonResponse(response, 200, { activity_watch: bridge.configureActivityWatch(body) });
493
+ return;
494
+ }
495
+ if (method === "POST" && collection === "receipts" && parts[2] === "record") {
496
+ jsonResponse(response, 201, { receipt: bridge.recordReceipt(String(body.batch_id), body.stage, body.details, body.endpoint_id, body.generation == null ? undefined : Number(body.generation)) });
497
+ return;
498
+ }
499
+ jsonResponse(response, 404, { error: "not_found" });
500
+ }
501
+ catch (error) {
502
+ const contractError = error instanceof WakeBridgeSdkError;
503
+ const status = contractError ? error.status : 500;
504
+ jsonResponse(response, status, { error: error instanceof Error ? error.message : String(error), code: contractError ? error.code : "internal_error" });
505
+ }
506
+ });
507
+ const port = options.port ?? 4311;
508
+ await new Promise((resolve, reject) => {
509
+ const onError = (error) => { server.off("listening", onListening); reject(error); };
510
+ const onListening = () => { server.off("error", onError); resolve(); };
511
+ server.once("error", onError);
512
+ server.once("listening", onListening);
513
+ server.listen(port, host);
514
+ });
515
+ const address = server.address();
516
+ const daemonOrigin = typeof address === "object" && address
517
+ ? `http://${host.includes(":") ? `[${host}]` : host}:${address.port}`
518
+ : null;
519
+ try {
520
+ await adapterRegistry.start(bridge, { instance_id: bridge.config.instance_id, daemon_origin: daemonOrigin });
521
+ }
522
+ catch (error) {
523
+ try {
524
+ await adapterRegistry.stop();
525
+ }
526
+ catch (stopError) {
527
+ process.emitWarning(`wakebridge host adapter cleanup failed: ${String(stopError)}`);
528
+ }
529
+ await new Promise((resolve) => server.close(() => resolve()));
530
+ throw error;
531
+ }
532
+ sourceSupervisor.start();
533
+ const schedulerInterval = options.scheduler_interval_ms ?? 1_000;
534
+ let schedulerTimer;
535
+ let schedulerPromise;
536
+ const runScheduler = async () => {
537
+ bridge.tick();
538
+ await bridge.dispatchDue();
539
+ };
540
+ const triggerScheduler = () => {
541
+ // Keep the one real pass promise authoritative. An interval tick while
542
+ // dispatch is awaiting a transport must not replace it with an already
543
+ // resolved "skipped" promise, or close() could return while that worker
544
+ // still owns a durable lease.
545
+ if (schedulerPromise)
546
+ return;
547
+ const pass = runScheduler().catch((error) => {
548
+ // Keep the daemon alive; the durable event/claim/outbox state remains
549
+ // inspectable and the next interval retries the scheduler pass.
550
+ process.emitWarning(`wakebridge scheduler pass failed: ${String(error)}`);
551
+ });
552
+ schedulerPromise = pass;
553
+ void pass.finally(() => {
554
+ if (schedulerPromise === pass)
555
+ schedulerPromise = undefined;
556
+ });
557
+ };
558
+ if (schedulerInterval > 0) {
559
+ schedulerTimer = setInterval(triggerScheduler, schedulerInterval);
560
+ // The HTTP server itself owns daemon lifetime; this timer must not keep an
561
+ // embedded caller alive after it closes the server.
562
+ schedulerTimer.unref();
563
+ triggerScheduler();
564
+ }
565
+ return {
566
+ bridge,
567
+ server,
568
+ address,
569
+ sources: () => sourceSupervisor.list(),
570
+ adapters: () => adapterRegistry.list(),
571
+ close: async () => {
572
+ let firstError;
573
+ try {
574
+ await adapterRegistry.stop();
575
+ }
576
+ catch (error) {
577
+ firstError = error;
578
+ }
579
+ try {
580
+ await sourceSupervisor.stop();
581
+ }
582
+ catch (error) {
583
+ firstError ??= error;
584
+ }
585
+ if (schedulerTimer)
586
+ clearInterval(schedulerTimer);
587
+ schedulerTimer = undefined;
588
+ try {
589
+ await schedulerPromise;
590
+ }
591
+ catch (error) {
592
+ firstError ??= error;
593
+ }
594
+ try {
595
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
596
+ }
597
+ catch (error) {
598
+ firstError ??= error;
599
+ }
600
+ if (firstError)
601
+ throw firstError;
602
+ },
603
+ };
604
+ }
605
+ //# sourceMappingURL=daemon.js.map