@dxos/edge-client 0.10.0 → 0.11.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 (57) hide show
  1. package/dist/lib/chunk-edge-ws-muxer.mjs +322 -0
  2. package/dist/lib/chunk-edge-ws-muxer.mjs.map +1 -0
  3. package/dist/lib/cors-proxy.mjs +29 -0
  4. package/dist/lib/cors-proxy.mjs.map +1 -0
  5. package/dist/lib/edge-ws-muxer.mjs +2 -0
  6. package/dist/lib/index.mjs +1769 -0
  7. package/dist/lib/index.mjs.map +1 -0
  8. package/dist/lib/service.mjs +138 -0
  9. package/dist/lib/service.mjs.map +1 -0
  10. package/dist/lib/testing.mjs +160 -0
  11. package/dist/lib/testing.mjs.map +1 -0
  12. package/dist/types/src/base-http-client.d.ts +18 -0
  13. package/dist/types/src/base-http-client.d.ts.map +1 -1
  14. package/dist/types/src/browser-rendering.d.ts +0 -7
  15. package/dist/types/src/browser-rendering.d.ts.map +1 -1
  16. package/dist/types/src/edge-client.d.ts +22 -2
  17. package/dist/types/src/edge-client.d.ts.map +1 -1
  18. package/dist/types/src/edge-http-client.d.ts +86 -2
  19. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  20. package/dist/types/src/edge-ws-connection.d.ts +19 -0
  21. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  22. package/dist/types/src/edge-ws-connection.test.d.ts +2 -0
  23. package/dist/types/src/edge-ws-connection.test.d.ts.map +1 -0
  24. package/dist/types/src/index.d.ts +1 -0
  25. package/dist/types/src/index.d.ts.map +1 -1
  26. package/dist/types/src/protocol.d.ts +2 -1
  27. package/dist/types/src/protocol.d.ts.map +1 -1
  28. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  29. package/dist/types/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +20 -20
  31. package/src/base-http-client.ts +71 -3
  32. package/src/browser-rendering.ts +0 -9
  33. package/src/edge-client.ts +21 -3
  34. package/src/edge-http-client.test.ts +121 -0
  35. package/src/edge-http-client.ts +156 -2
  36. package/src/edge-ws-connection.test.ts +219 -0
  37. package/src/edge-ws-connection.ts +97 -28
  38. package/src/index.ts +1 -0
  39. package/src/protocol.ts +11 -2
  40. package/src/testing/test-utils.ts +5 -1
  41. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  42. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  43. package/dist/lib/neutral/chunk-L5ZHLJ4B.mjs +0 -310
  44. package/dist/lib/neutral/chunk-L5ZHLJ4B.mjs.map +0 -7
  45. package/dist/lib/neutral/chunk-WQKMEZJR.mjs +0 -30
  46. package/dist/lib/neutral/chunk-WQKMEZJR.mjs.map +0 -7
  47. package/dist/lib/neutral/cors-proxy.mjs +0 -8
  48. package/dist/lib/neutral/cors-proxy.mjs.map +0 -7
  49. package/dist/lib/neutral/edge-ws-muxer.mjs +0 -12
  50. package/dist/lib/neutral/edge-ws-muxer.mjs.map +0 -7
  51. package/dist/lib/neutral/index.mjs +0 -1524
  52. package/dist/lib/neutral/index.mjs.map +0 -7
  53. package/dist/lib/neutral/meta.json +0 -1
  54. package/dist/lib/neutral/service/index.mjs +0 -134
  55. package/dist/lib/neutral/service/index.mjs.map +0 -7
  56. package/dist/lib/neutral/testing/index.mjs +0 -161
  57. package/dist/lib/neutral/testing/index.mjs.map +0 -7
@@ -0,0 +1,1769 @@
1
+ import { proxyFetchLegacy } from "./cors-proxy.mjs";
2
+ import { a as Protocol, i as protocol, n as CLOUDFLARE_RPC_MAX_BYTES, o as getTypename, r as WebSocketMuxer, s as toUint8Array, t as CLOUDFLARE_MESSAGE_MAX_BYTES } from "./chunk-edge-ws-muxer.mjs";
3
+ import { Event, Mutex, PersistentLifecycle, Trigger, TriggerState, scheduleMicroTask, scheduleTask, scheduleTaskInterval, sleep } from "@dxos/async";
4
+ import { log, logInfo } from "@dxos/log";
5
+ import { buf } from "@dxos/protocols/buf";
6
+ import { MessageSchema } from "@dxos/protocols/buf/dxos/edge/messenger_pb";
7
+ import { invariant } from "@dxos/invariant";
8
+ import { createUrl } from "@dxos/util";
9
+ import { createCredential, createDidFromIdentityKey, signPresentation } from "@dxos/credentials";
10
+ import { Keyring } from "@dxos/keyring";
11
+ import { IdentityDid, PublicKey } from "@dxos/keys";
12
+ import * as EffectContext from "effect/Context";
13
+ import { Context, Resource, TRACE_SPAN_ATTRIBUTE } from "@dxos/context";
14
+ import { EdgeStatus } from "@dxos/protocols/proto/dxos/client/services";
15
+ import { schema } from "@dxos/protocols/proto";
16
+ import WebSocket from "isomorphic-ws";
17
+ import { BYOK_HEADER, EDGE_CLIENT_TAG_HEADER, EdgeAuthChallengeError, EdgeCallFailedError, EdgeWebsocketProtocol } from "@dxos/protocols";
18
+ import * as Duration from "effect/Duration";
19
+ import * as Effect from "effect/Effect";
20
+ import * as Layer from "effect/Layer";
21
+ import * as Schedule from "effect/Schedule";
22
+ import * as Headers$1 from "@effect/platform/Headers";
23
+ import * as HttpClient from "@effect/platform/HttpClient";
24
+ import * as HttpClientError from "@effect/platform/HttpClientError";
25
+ import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
26
+ import * as FiberRef from "effect/FiberRef";
27
+ import * as Stream from "effect/Stream";
28
+ import { BaseError } from "@dxos/errors";
29
+ import * as FetchHttpClient from "@effect/platform/FetchHttpClient";
30
+ import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
31
+ import * as Function from "effect/Function";
32
+ import { EffectEx } from "@dxos/effect";
33
+ import * as Schema from "effect/Schema";
34
+ export * from "@dxos/protocols/buf/dxos/edge/messenger_pb";
35
+ //#region src/auth.ts
36
+ var __dxlog_file$7 = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/auth.ts";
37
+ /**
38
+ * Edge identity backed by a device key without a credential chain.
39
+ */
40
+ var createDeviceEdgeIdentity = async (signer, key) => {
41
+ return {
42
+ identityDid: await createDidFromIdentityKey(key),
43
+ peerKey: key.toHex(),
44
+ presentCredentials: async ({ challenge }) => {
45
+ return signPresentation({
46
+ presentation: { credentials: [await createCredential({
47
+ assertion: { "@type": "dxos.halo.credentials.Auth" },
48
+ issuer: key,
49
+ subject: key,
50
+ signer
51
+ })] },
52
+ signer,
53
+ signerKey: key,
54
+ nonce: challenge
55
+ });
56
+ }
57
+ };
58
+ };
59
+ /**
60
+ * Edge identity backed by a chain of credentials.
61
+ */
62
+ var createChainEdgeIdentity = async (signer, identityKey, peerKey, chain, credentials) => {
63
+ const credentialsToSign = credentials.length > 0 ? credentials : [await createCredential({
64
+ assertion: { "@type": "dxos.halo.credentials.Auth" },
65
+ issuer: identityKey,
66
+ subject: identityKey,
67
+ signer,
68
+ chain,
69
+ signingKey: peerKey
70
+ })];
71
+ return {
72
+ identityDid: await createDidFromIdentityKey(identityKey),
73
+ peerKey: peerKey.toHex(),
74
+ presentCredentials: async ({ challenge }) => {
75
+ invariant(chain, void 0, {
76
+ "~LogMeta": "~LogMeta",
77
+ F: __dxlog_file$7,
78
+ L: 75,
79
+ S: void 0,
80
+ A: ["chain", ""]
81
+ });
82
+ return signPresentation({
83
+ presentation: { credentials: credentialsToSign },
84
+ signer,
85
+ nonce: challenge,
86
+ signerKey: peerKey,
87
+ chain
88
+ });
89
+ }
90
+ };
91
+ };
92
+ /**
93
+ * Edge identity backed by a random ephemeral key without HALO.
94
+ */
95
+ var createEphemeralEdgeIdentity = async () => {
96
+ const keyring = new Keyring();
97
+ return createDeviceEdgeIdentity(keyring, await keyring.createKey());
98
+ };
99
+ /**
100
+ * Creates a HALO chain of credentials to act as an edge identity.
101
+ */
102
+ var createTestHaloEdgeIdentity = async (signer, identityKey, deviceKey) => {
103
+ return createChainEdgeIdentity(signer, identityKey, deviceKey, { credential: await createCredential({
104
+ assertion: {
105
+ "@type": "dxos.halo.credentials.AuthorizedDevice",
106
+ deviceKey,
107
+ identityKey
108
+ },
109
+ issuer: identityKey,
110
+ subject: deviceKey,
111
+ signer
112
+ }) }, [await createCredential({
113
+ assertion: { "@type": "dxos.halo.credentials.Auth" },
114
+ issuer: identityKey,
115
+ subject: identityKey,
116
+ signer
117
+ })]);
118
+ };
119
+ var createStubEdgeIdentity = () => {
120
+ const deviceKey = PublicKey.random();
121
+ return {
122
+ identityDid: IdentityDid.random(),
123
+ peerKey: deviceKey.toHex(),
124
+ presentCredentials: async () => {
125
+ throw new Error("Stub identity does not support authentication.");
126
+ }
127
+ };
128
+ };
129
+ //#endregion
130
+ //#region src/edge-identity.ts
131
+ var __dxlog_file$6 = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/edge-identity.ts";
132
+ var handleAuthChallenge = async (failedResponse, identity) => {
133
+ invariant(failedResponse.status === 401, void 0, {
134
+ "~LogMeta": "~LogMeta",
135
+ F: __dxlog_file$6,
136
+ L: 25,
137
+ S: void 0,
138
+ A: ["failedResponse.status === 401", ""]
139
+ });
140
+ const headerValue = failedResponse.headers.get("Www-Authenticate");
141
+ invariant(headerValue?.startsWith("VerifiablePresentation challenge="), void 0, {
142
+ "~LogMeta": "~LogMeta",
143
+ F: __dxlog_file$6,
144
+ L: 28,
145
+ S: void 0,
146
+ A: ["headerValue?.startsWith('VerifiablePresentation challenge=')", ""]
147
+ });
148
+ const challenge = headerValue?.slice(33);
149
+ invariant(challenge, void 0, {
150
+ "~LogMeta": "~LogMeta",
151
+ F: __dxlog_file$6,
152
+ L: 31,
153
+ S: void 0,
154
+ A: ["challenge", ""]
155
+ });
156
+ const presentation = await identity.presentCredentials({ challenge: Buffer.from(challenge, "base64") });
157
+ return schema.getCodecForType("dxos.halo.credentials.Presentation").encode(presentation);
158
+ };
159
+ //#endregion
160
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
161
+ function __decorate(decorators, target, key, desc) {
162
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
163
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
164
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
165
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
166
+ }
167
+ //#endregion
168
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/usingCtx.js
169
+ function _usingCtx() {
170
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
171
+ var n = Error();
172
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
173
+ }, e = {}, n = [];
174
+ function using(r, e) {
175
+ if (null != e) {
176
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
177
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
178
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
179
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
180
+ t && (o = function o() {
181
+ try {
182
+ t.call(e);
183
+ } catch (r) {
184
+ return Promise.reject(r);
185
+ }
186
+ }), n.push({
187
+ v: e,
188
+ d: o,
189
+ a: r
190
+ });
191
+ } else r && n.push({
192
+ d: e,
193
+ a: r
194
+ });
195
+ return e;
196
+ }
197
+ return {
198
+ e,
199
+ u: using.bind(null, !1),
200
+ a: using.bind(null, !0),
201
+ d: function d() {
202
+ var o, t = this.e, s = 0;
203
+ function next() {
204
+ for (; o = n.pop();) try {
205
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
206
+ if (o.d) {
207
+ var r = o.d.call(o.v);
208
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
209
+ } else s |= 1;
210
+ } catch (r) {
211
+ return err(r);
212
+ }
213
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
214
+ if (t !== e) throw t;
215
+ }
216
+ function err(n) {
217
+ return t = t !== e ? new r(n, t) : n, next();
218
+ }
219
+ return next();
220
+ }
221
+ };
222
+ }
223
+ //#endregion
224
+ //#region src/edge-ws-connection.ts
225
+ var __dxlog_file$5 = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/edge-ws-connection.ts";
226
+ var SIGNAL_KEEPALIVE_INTERVAL = 4e3;
227
+ var SIGNAL_KEEPALIVE_TIMEOUT = 12e3;
228
+ /**
229
+ * Watchdog self-check: if the inactivity timer fires later than its schedule by more than this,
230
+ * the local event loop was starved (heavy WASM sync compute pins it for seconds at a time) —
231
+ * our pings were not being sent and inbound pongs were not being processed, so the silence says
232
+ * nothing about the connection. Probe and re-arm instead of restarting.
233
+ */
234
+ var KEEPALIVE_WATCHDOG_LATE_TOLERANCE = 3e3;
235
+ var EdgeWsConnection = class extends Resource {
236
+ _identity;
237
+ _connectionInfo;
238
+ _callbacks;
239
+ _inactivityTimeoutCtx;
240
+ _ws;
241
+ _wsMuxer;
242
+ _lastReceivedMessageTimestamp = Date.now();
243
+ _openTimestamp;
244
+ _pingTimestamp;
245
+ _lastPingSentTimestamp = 0;
246
+ _rtt = 0;
247
+ _uploadRate = 0;
248
+ _downloadRate = 0;
249
+ _rateWindow = 1e4;
250
+ _rateUpdateInterval = 1e3;
251
+ _bytesSamples = [];
252
+ _messagesSent = 0;
253
+ _messagesReceived = 0;
254
+ /**
255
+ * WebSocket frames arrive in order, but converting frame data to bytes is async
256
+ * (the `Blob` fallback path awaits `blob.arrayBuffer()`), and concurrent conversions
257
+ * are not guaranteed to complete in arrival order. Segmented-message reassembly in
258
+ * `WebSocketMuxer` requires chunks to reach `receiveData` in arrival order, so message
259
+ * processing is serialized through this lock.
260
+ */
261
+ _receiveMutex = new Mutex();
262
+ constructor(_identity, _connectionInfo, _callbacks) {
263
+ super();
264
+ this._identity = _identity;
265
+ this._connectionInfo = _connectionInfo;
266
+ this._callbacks = _callbacks;
267
+ }
268
+ get info() {
269
+ return {
270
+ open: this.isOpen,
271
+ identity: this._identity.identityDid,
272
+ device: this._identity.peerKey
273
+ };
274
+ }
275
+ get rtt() {
276
+ return this._rtt;
277
+ }
278
+ get uptime() {
279
+ return this._openTimestamp ? (Date.now() - this._openTimestamp) / 1e3 : 0;
280
+ }
281
+ get uploadRate() {
282
+ return this._uploadRate;
283
+ }
284
+ get downloadRate() {
285
+ return this._downloadRate;
286
+ }
287
+ get messagesSent() {
288
+ return this._messagesSent;
289
+ }
290
+ get messagesReceived() {
291
+ return this._messagesReceived;
292
+ }
293
+ send(message) {
294
+ invariant(this._ws, void 0, {
295
+ "~LogMeta": "~LogMeta",
296
+ F: __dxlog_file$5,
297
+ L: 110,
298
+ S: this,
299
+ A: ["this._ws", ""]
300
+ });
301
+ invariant(this._wsMuxer, void 0, {
302
+ "~LogMeta": "~LogMeta",
303
+ F: __dxlog_file$5,
304
+ L: 111,
305
+ S: this,
306
+ A: ["this._wsMuxer", ""]
307
+ });
308
+ log("sending...", {
309
+ peerKey: this._identity.peerKey,
310
+ payload: protocol.getPayloadType(message)
311
+ }, {
312
+ "~LogMeta": "~LogMeta",
313
+ F: __dxlog_file$5,
314
+ L: 112,
315
+ S: this
316
+ });
317
+ this._messagesSent++;
318
+ if (this._ws?.protocol.includes(EdgeWebsocketProtocol.V0)) {
319
+ const binary = buf.toBinary(MessageSchema, message);
320
+ if (binary.length > 1e6) {
321
+ log.error("Message dropped because it was too large (>1MB).", {
322
+ byteLength: binary.byteLength,
323
+ serviceId: message.serviceId,
324
+ payload: protocol.getPayloadType(message)
325
+ }, {
326
+ "~LogMeta": "~LogMeta",
327
+ F: __dxlog_file$5,
328
+ L: 117,
329
+ S: this
330
+ });
331
+ return;
332
+ }
333
+ this._recordBytes(binary.byteLength, 0);
334
+ this._ws.send(binary);
335
+ } else {
336
+ const binary = buf.toBinary(MessageSchema, message);
337
+ this._recordBytes(binary.byteLength, 0);
338
+ this._wsMuxer.send(message).catch((e) => log.catch(e, void 0, {
339
+ "~LogMeta": "~LogMeta",
340
+ F: __dxlog_file$5,
341
+ L: 130,
342
+ S: this
343
+ }));
344
+ }
345
+ }
346
+ async _open() {
347
+ const baseProtocols = [...Object.values(EdgeWebsocketProtocol)];
348
+ this._ws = new WebSocket(this._connectionInfo.url.toString(), this._connectionInfo.protocolHeader ? [...baseProtocols, this._connectionInfo.protocolHeader] : [...baseProtocols], this._connectionInfo.headers ? { headers: this._connectionInfo.headers } : void 0);
349
+ this._ws.binaryType = "arraybuffer";
350
+ const muxer = new WebSocketMuxer(this._ws);
351
+ this._wsMuxer = muxer;
352
+ this._ws.onopen = () => {
353
+ if (this.isOpen) {
354
+ log("connected", void 0, {
355
+ "~LogMeta": "~LogMeta",
356
+ F: __dxlog_file$5,
357
+ L: 152,
358
+ S: this
359
+ });
360
+ this._openTimestamp = Date.now();
361
+ this._callbacks.onConnected();
362
+ this._scheduleHeartbeats();
363
+ this._scheduleRateCalculation();
364
+ } else log.verbose("connected after becoming inactive", { currentIdentity: this._identity }, {
365
+ "~LogMeta": "~LogMeta",
366
+ F: __dxlog_file$5,
367
+ L: 158,
368
+ S: this
369
+ });
370
+ };
371
+ this._ws.onclose = (event) => {
372
+ if (this.isOpen) {
373
+ log.warn("server disconnected", {
374
+ code: event.code,
375
+ reason: event.reason
376
+ }, {
377
+ "~LogMeta": "~LogMeta",
378
+ F: __dxlog_file$5,
379
+ L: 163,
380
+ S: this
381
+ });
382
+ this._callbacks.onRestartRequired();
383
+ muxer.destroy();
384
+ }
385
+ };
386
+ this._ws.onerror = (event) => {
387
+ if (this.isOpen) {
388
+ log.warn("edge connection socket error", {
389
+ error: event.error,
390
+ info: event.message
391
+ }, {
392
+ "~LogMeta": "~LogMeta",
393
+ F: __dxlog_file$5,
394
+ L: 170,
395
+ S: this
396
+ });
397
+ this._callbacks.onRestartRequired();
398
+ } else log.verbose("error ignored on closed connection", { error: event.error }, {
399
+ "~LogMeta": "~LogMeta",
400
+ F: __dxlog_file$5,
401
+ L: 173,
402
+ S: this
403
+ });
404
+ };
405
+ /**
406
+ * https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
407
+ */
408
+ this._ws.onmessage = (event) => {
409
+ if (!this.isOpen) {
410
+ log.verbose("message ignored on closed connection", { event: event.type }, {
411
+ "~LogMeta": "~LogMeta",
412
+ F: __dxlog_file$5,
413
+ L: 181,
414
+ S: this
415
+ });
416
+ return;
417
+ }
418
+ this._lastReceivedMessageTimestamp = Date.now();
419
+ if (event.data === "__pong__") {
420
+ if (this._pingTimestamp) {
421
+ this._rtt = Date.now() - this._pingTimestamp;
422
+ this._pingTimestamp = void 0;
423
+ }
424
+ this._rescheduleHeartbeatTimeout();
425
+ return;
426
+ }
427
+ this._receiveMessage(event.data, muxer).catch((err) => log.catch(err, void 0, {
428
+ "~LogMeta": "~LogMeta",
429
+ F: __dxlog_file$5,
430
+ L: 197,
431
+ S: this
432
+ }));
433
+ };
434
+ }
435
+ async _receiveMessage(data, muxer) {
436
+ try {
437
+ var _usingCtx$1 = _usingCtx();
438
+ _usingCtx$1.u(await this._receiveMutex.acquire());
439
+ const bytes = await toUint8Array(data);
440
+ this._recordBytes(0, bytes.byteLength);
441
+ if (!this.isOpen) return;
442
+ this._messagesReceived++;
443
+ const message = this._ws?.protocol?.includes(EdgeWebsocketProtocol.V0) ? buf.fromBinary(MessageSchema, bytes) : muxer.receiveData(bytes);
444
+ if (message) {
445
+ log("received", {
446
+ from: message.source,
447
+ payload: protocol.getPayloadType(message)
448
+ }, {
449
+ "~LogMeta": "~LogMeta",
450
+ F: __dxlog_file$5,
451
+ L: 219,
452
+ S: this
453
+ });
454
+ this._callbacks.onMessage(message);
455
+ }
456
+ } catch (_) {
457
+ _usingCtx$1.e = _;
458
+ } finally {
459
+ _usingCtx$1.d();
460
+ }
461
+ }
462
+ async _close() {
463
+ this._inactivityTimeoutCtx?.dispose().catch(() => {});
464
+ try {
465
+ this._ws?.close();
466
+ this._ws = void 0;
467
+ this._wsMuxer?.destroy();
468
+ this._wsMuxer = void 0;
469
+ } catch (err) {
470
+ if (err instanceof Error && err.message.includes("WebSocket is closed before the connection is established.")) return;
471
+ log.warn("error closing websocket", { err }, {
472
+ "~LogMeta": "~LogMeta",
473
+ F: __dxlog_file$5,
474
+ L: 236,
475
+ S: this
476
+ });
477
+ }
478
+ }
479
+ _scheduleHeartbeats() {
480
+ invariant(this._ws, void 0, {
481
+ "~LogMeta": "~LogMeta",
482
+ F: __dxlog_file$5,
483
+ L: 241,
484
+ S: this,
485
+ A: ["this._ws", ""]
486
+ });
487
+ scheduleTaskInterval(this._ctx, async () => {
488
+ this._sendPing();
489
+ }, SIGNAL_KEEPALIVE_INTERVAL);
490
+ this._sendPing();
491
+ this._rescheduleHeartbeatTimeout();
492
+ }
493
+ _sendPing() {
494
+ if (!this._ws) return;
495
+ this._pingTimestamp = Date.now();
496
+ this._lastPingSentTimestamp = Date.now();
497
+ this._ws.send("__ping__");
498
+ }
499
+ /**
500
+ * Inactivity watchdog. Restarts the connection only after a fair trial: pings were actually
501
+ * flowing (a recent send), the timer fired on schedule (the local event loop was alive to
502
+ * process an answer), and still nothing was received for the full window. Wall-clock silence
503
+ * alone is not evidence — sync compute can pin the event loop for seconds, during which the
504
+ * ping sender does not run and arrived pongs are not processed; restarting a healthy
505
+ * connection on that basis costs a re-handshake and fails in-flight sync rounds.
506
+ */
507
+ _rescheduleHeartbeatTimeout() {
508
+ if (!this.isOpen) return;
509
+ this._inactivityTimeoutCtx?.dispose();
510
+ this._inactivityTimeoutCtx = new Context(void 0, {
511
+ "~LogMeta": "~LogMeta",
512
+ F: __dxlog_file$5,
513
+ L: 277
514
+ });
515
+ const armedAt = Date.now();
516
+ scheduleTask(this._inactivityTimeoutCtx, () => {
517
+ if (!this.isOpen) return;
518
+ const now = Date.now();
519
+ const silenceMs = now - this._lastReceivedMessageTimestamp;
520
+ if (silenceMs <= SIGNAL_KEEPALIVE_TIMEOUT) {
521
+ this._rescheduleHeartbeatTimeout();
522
+ return;
523
+ }
524
+ const pingAgeMs = this._lastPingSentTimestamp ? now - this._lastPingSentTimestamp : Number.POSITIVE_INFINITY;
525
+ const firedLateByMs = now - armedAt - SIGNAL_KEEPALIVE_TIMEOUT;
526
+ if (pingAgeMs <= SIGNAL_KEEPALIVE_INTERVAL * 2 && firedLateByMs < KEEPALIVE_WATCHDOG_LATE_TOLERANCE) {
527
+ log.warn("restart due to inactivity timeout", {
528
+ silenceMs,
529
+ pingAgeMs,
530
+ lastReceivedMessageTimestamp: this._lastReceivedMessageTimestamp
531
+ }, {
532
+ "~LogMeta": "~LogMeta",
533
+ F: __dxlog_file$5,
534
+ L: 296,
535
+ S: this
536
+ });
537
+ this._callbacks.onRestartRequired();
538
+ return;
539
+ }
540
+ log.verbose("keepalive starved by event loop; probing instead of restarting", {
541
+ silenceMs,
542
+ pingAgeMs,
543
+ firedLateByMs
544
+ }, {
545
+ "~LogMeta": "~LogMeta",
546
+ F: __dxlog_file$5,
547
+ L: 306,
548
+ S: this
549
+ });
550
+ this._sendPing();
551
+ this._rescheduleHeartbeatTimeout();
552
+ }, SIGNAL_KEEPALIVE_TIMEOUT);
553
+ }
554
+ _recordBytes(sent, received) {
555
+ const now = Date.now();
556
+ const currentSecond = Math.floor(now / 1e3) * 1e3;
557
+ const existingSample = this._bytesSamples.find((s) => Math.floor(s.timestamp / 1e3) * 1e3 === currentSecond);
558
+ if (existingSample) {
559
+ existingSample.sent += sent;
560
+ existingSample.received += received;
561
+ } else this._bytesSamples.push({
562
+ timestamp: now,
563
+ sent,
564
+ received
565
+ });
566
+ }
567
+ _scheduleRateCalculation() {
568
+ scheduleTaskInterval(this._ctx, async () => {
569
+ this._calculateRates();
570
+ }, this._rateUpdateInterval);
571
+ this._calculateRates();
572
+ }
573
+ _calculateRates() {
574
+ const now = Date.now();
575
+ const cutoff = now - this._rateWindow;
576
+ this._bytesSamples = this._bytesSamples.filter((s) => s.timestamp > cutoff);
577
+ if (this._bytesSamples.length === 0) {
578
+ this._uploadRate = 0;
579
+ this._downloadRate = 0;
580
+ return;
581
+ }
582
+ let totalSent = 0;
583
+ let totalReceived = 0;
584
+ const timeSpan = (now - Math.min(...this._bytesSamples.map((s) => s.timestamp))) / 1e3;
585
+ for (const sample of this._bytesSamples) {
586
+ totalSent += sample.sent;
587
+ totalReceived += sample.received;
588
+ }
589
+ this._uploadRate = timeSpan > 0 ? Math.round(totalSent / timeSpan) : 0;
590
+ this._downloadRate = timeSpan > 0 ? Math.round(totalReceived / timeSpan) : 0;
591
+ }
592
+ };
593
+ __decorate([logInfo], EdgeWsConnection.prototype, "info", null);
594
+ //#endregion
595
+ //#region src/errors.ts
596
+ var EdgeConnectionClosedError = class extends Error {
597
+ constructor() {
598
+ super("Edge connection closed.");
599
+ }
600
+ };
601
+ var EdgeIdentityChangedError = class extends Error {
602
+ constructor() {
603
+ super("Edge identity changed.");
604
+ }
605
+ };
606
+ //#endregion
607
+ //#region src/utils.ts
608
+ var getEdgeUrlWithProtocol = (baseUrl, protocol) => {
609
+ const isSecure = baseUrl.startsWith("https") || baseUrl.startsWith("wss");
610
+ const url = new URL(baseUrl);
611
+ url.protocol = protocol + (isSecure ? "s" : "");
612
+ return url.toString();
613
+ };
614
+ //#endregion
615
+ //#region src/edge-client.ts
616
+ var __dxlog_file$4 = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/edge-client.ts";
617
+ var DEFAULT_TIMEOUT = 1e4;
618
+ var STATUS_REFRESH_INTERVAL = 1e3;
619
+ /**
620
+ * Effect service tag for {@link EdgeConnection}.
621
+ */
622
+ var EdgeConnectionService = class extends EffectContext.Tag("@dxos/edge-client/EdgeConnection")() {};
623
+ /**
624
+ * Messenger client for EDGE:
625
+ * - While open, uses PersistentLifecycle to keep an open EdgeWsConnection, reconnecting on failures.
626
+ * - Manages identity and re-create EdgeWsConnection when identity changes.
627
+ * - Dispatches connection state and message notifications.
628
+ */
629
+ var EdgeClient = class extends Resource {
630
+ _identity;
631
+ _config;
632
+ statusChanged = new Event();
633
+ _persistentLifecycle = new PersistentLifecycle({
634
+ start: async () => this._connect(),
635
+ stop: async (state) => this._disconnect(state)
636
+ });
637
+ _messageListeners = /* @__PURE__ */ new Set();
638
+ _reconnectListeners = /* @__PURE__ */ new Set();
639
+ _baseWsUrl;
640
+ _baseHttpUrl;
641
+ _currentConnection = void 0;
642
+ _ready = new Trigger();
643
+ constructor(_identity, _config) {
644
+ super();
645
+ this._identity = _identity;
646
+ this._config = _config;
647
+ this._baseWsUrl = getEdgeUrlWithProtocol(_config.socketEndpoint, "ws");
648
+ this._baseHttpUrl = getEdgeUrlWithProtocol(_config.socketEndpoint, "http");
649
+ }
650
+ get info() {
651
+ return {
652
+ open: this.isOpen,
653
+ status: this.status,
654
+ identity: this._identity.identityDid,
655
+ device: this._identity.peerKey
656
+ };
657
+ }
658
+ get status() {
659
+ return {
660
+ state: Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED ? EdgeStatus.ConnectionState.CONNECTED : EdgeStatus.ConnectionState.NOT_CONNECTED,
661
+ uptime: this._currentConnection?.uptime ?? 0,
662
+ rtt: this._currentConnection?.rtt ?? 0,
663
+ rateBytesUp: this._currentConnection?.uploadRate ?? 0,
664
+ rateBytesDown: this._currentConnection?.downloadRate ?? 0,
665
+ messagesSent: this._currentConnection?.messagesSent ?? 0,
666
+ messagesReceived: this._currentConnection?.messagesReceived ?? 0
667
+ };
668
+ }
669
+ get identityDid() {
670
+ return this._identity.identityDid;
671
+ }
672
+ get peerKey() {
673
+ return this._identity.peerKey;
674
+ }
675
+ setIdentity(identity) {
676
+ if (identity.identityDid !== this._identity.identityDid || identity.peerKey !== this._identity.peerKey) {
677
+ log("Edge identity changed", {
678
+ identity,
679
+ oldIdentity: this._identity
680
+ }, {
681
+ "~LogMeta": "~LogMeta",
682
+ F: __dxlog_file$4,
683
+ L: 140,
684
+ S: this
685
+ });
686
+ this._identity = identity;
687
+ this._closeCurrentConnection(new EdgeIdentityChangedError());
688
+ this._persistentLifecycle.scheduleRestart();
689
+ }
690
+ }
691
+ /**
692
+ * Send message.
693
+ * NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
694
+ */
695
+ async send(ctx, message) {
696
+ if (this._ready.state !== TriggerState.RESOLVED) {
697
+ log("waiting for websocket", void 0, {
698
+ "~LogMeta": "~LogMeta",
699
+ F: __dxlog_file$4,
700
+ L: 153,
701
+ S: this
702
+ });
703
+ await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
704
+ }
705
+ if (!this._currentConnection) throw new EdgeConnectionClosedError();
706
+ if (message.source && (message.source.peerKey !== this._identity.peerKey || message.source.identityDid !== this.identityDid)) throw new EdgeIdentityChangedError();
707
+ const traceCtx = ctx.getAttribute(TRACE_SPAN_ATTRIBUTE);
708
+ if (traceCtx) message.traceContext = {
709
+ $typeName: "dxos.edge.messenger.TraceContext",
710
+ traceparent: traceCtx.traceparent,
711
+ tracestate: traceCtx.tracestate
712
+ };
713
+ this._currentConnection.send(message);
714
+ }
715
+ onMessage(listener) {
716
+ this._messageListeners.add(listener);
717
+ return () => this._messageListeners.delete(listener);
718
+ }
719
+ onReconnected(listener, opts) {
720
+ this._reconnectListeners.add(listener);
721
+ if ((opts?.emitCurrentState ?? true) && this._ready.state === TriggerState.RESOLVED) scheduleMicroTask(this._ctx, () => {
722
+ if (this._reconnectListeners.has(listener)) try {
723
+ listener();
724
+ } catch (error) {
725
+ log.catch(error, void 0, {
726
+ "~LogMeta": "~LogMeta",
727
+ F: __dxlog_file$4,
728
+ L: 196,
729
+ S: this
730
+ });
731
+ }
732
+ });
733
+ return () => this._reconnectListeners.delete(listener);
734
+ }
735
+ /**
736
+ * Open connection to messaging service.
737
+ */
738
+ async _open() {
739
+ log("opening...", { info: this.info }, {
740
+ "~LogMeta": "~LogMeta",
741
+ F: __dxlog_file$4,
742
+ L: 209,
743
+ S: this
744
+ });
745
+ this._persistentLifecycle.open().catch((err) => {
746
+ log.warn("Error while opening connection", { err }, {
747
+ "~LogMeta": "~LogMeta",
748
+ F: __dxlog_file$4,
749
+ L: 211,
750
+ S: this
751
+ });
752
+ });
753
+ scheduleTaskInterval(this._ctx, async () => {
754
+ if (!this._currentConnection) return;
755
+ this.statusChanged.emit(this.status);
756
+ }, STATUS_REFRESH_INTERVAL);
757
+ }
758
+ /**
759
+ * Close connection and free resources.
760
+ */
761
+ async _close() {
762
+ log("closing...", { peerKey: this._identity.peerKey }, {
763
+ "~LogMeta": "~LogMeta",
764
+ F: __dxlog_file$4,
765
+ L: 231,
766
+ S: this
767
+ });
768
+ this._closeCurrentConnection();
769
+ await this._persistentLifecycle.close();
770
+ }
771
+ async _connect() {
772
+ if (this._ctx.disposed) return;
773
+ const identity = this._identity;
774
+ const path = `/ws/${identity.identityDid}/${identity.peerKey}`;
775
+ const protocolHeader = this._config.disableAuth ? void 0 : await this._createAuthHeader(path);
776
+ if (this._identity !== identity) {
777
+ log("identity changed during auth header request", void 0, {
778
+ "~LogMeta": "~LogMeta",
779
+ F: __dxlog_file$4,
780
+ L: 245,
781
+ S: this
782
+ });
783
+ return;
784
+ }
785
+ const restartRequired = new Trigger();
786
+ const url = new URL(path, this._baseWsUrl);
787
+ log("Opening websocket", {
788
+ url: url.toString(),
789
+ protocolHeader
790
+ }, {
791
+ "~LogMeta": "~LogMeta",
792
+ F: __dxlog_file$4,
793
+ L: 251,
794
+ S: this
795
+ });
796
+ const connection = new EdgeWsConnection(identity, {
797
+ url,
798
+ protocolHeader,
799
+ headers: this._config.clientTag ? { "X-DXOS-Client-Tag": this._config.clientTag } : void 0
800
+ }, {
801
+ onConnected: () => {
802
+ if (this._isActive(connection)) {
803
+ this._ready.wake();
804
+ this._notifyReconnected();
805
+ } else log.verbose("connected callback ignored, because connection is not active", void 0, {
806
+ "~LogMeta": "~LogMeta",
807
+ F: __dxlog_file$4,
808
+ L: 265,
809
+ S: this
810
+ });
811
+ },
812
+ onRestartRequired: () => {
813
+ if (this._isActive(connection)) {
814
+ this._closeCurrentConnection();
815
+ this._persistentLifecycle.scheduleRestart();
816
+ } else log.verbose("restart requested by inactive connection", void 0, {
817
+ "~LogMeta": "~LogMeta",
818
+ F: __dxlog_file$4,
819
+ L: 273,
820
+ S: this
821
+ });
822
+ restartRequired.wake();
823
+ },
824
+ onMessage: (message) => {
825
+ if (this._isActive(connection)) this._notifyMessageReceived(message);
826
+ else log.verbose("ignored a message on inactive connection", {
827
+ from: message.source,
828
+ type: message.payload?.typeUrl
829
+ }, {
830
+ "~LogMeta": "~LogMeta",
831
+ F: __dxlog_file$4,
832
+ L: 281,
833
+ S: this
834
+ });
835
+ }
836
+ });
837
+ this._currentConnection = connection;
838
+ await connection.open();
839
+ if (!await Promise.race([this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT }).then(() => true, () => false), restartRequired.wait().then(() => false)])) throw new EdgeConnectionClosedError();
840
+ return connection;
841
+ }
842
+ async _disconnect(state) {
843
+ await state.close();
844
+ this.statusChanged.emit(this.status);
845
+ }
846
+ _closeCurrentConnection(error = new EdgeConnectionClosedError()) {
847
+ this._currentConnection = void 0;
848
+ this._ready.throw(error);
849
+ this._ready.reset();
850
+ this.statusChanged.emit(this.status);
851
+ }
852
+ _notifyReconnected() {
853
+ this.statusChanged.emit(this.status);
854
+ for (const listener of this._reconnectListeners) try {
855
+ listener();
856
+ } catch (err) {
857
+ log.error("ws reconnect listener failed", { err }, {
858
+ "~LogMeta": "~LogMeta",
859
+ F: __dxlog_file$4,
860
+ L: 330,
861
+ S: this
862
+ });
863
+ }
864
+ }
865
+ _notifyMessageReceived(message) {
866
+ for (const listener of this._messageListeners) try {
867
+ listener(message);
868
+ } catch (err) {
869
+ log.error("ws incoming message processing failed", {
870
+ err,
871
+ payload: protocol.getPayloadType(message)
872
+ }, {
873
+ "~LogMeta": "~LogMeta",
874
+ F: __dxlog_file$4,
875
+ L: 340,
876
+ S: this
877
+ });
878
+ }
879
+ }
880
+ async _createAuthHeader(path) {
881
+ const httpUrl = new URL(path, this._baseHttpUrl);
882
+ httpUrl.protocol = getEdgeUrlWithProtocol(this._baseWsUrl.toString(), "http");
883
+ const response = await fetch(httpUrl, { method: "GET" });
884
+ if (response.status === 401) return encodePresentationWsAuthHeader(await handleAuthChallenge(response, this._identity));
885
+ else {
886
+ log.warn("no auth challenge from edge", {
887
+ status: response.status,
888
+ statusText: response.statusText
889
+ }, {
890
+ "~LogMeta": "~LogMeta",
891
+ F: __dxlog_file$4,
892
+ L: 352,
893
+ S: this
894
+ });
895
+ return;
896
+ }
897
+ }
898
+ _isActive = (connection) => connection === this._currentConnection;
899
+ };
900
+ __decorate([logInfo], EdgeClient.prototype, "info", null);
901
+ var encodePresentationWsAuthHeader = (encodedPresentation) => {
902
+ return `base64url.bearer.authorization.dxos.org.${Buffer.from(encodedPresentation).toString("base64").replace(/=*$/, "").replaceAll("/", "|")}`;
903
+ };
904
+ //#endregion
905
+ //#region src/http-client.ts
906
+ var __dxlog_file$3 = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/http-client.ts";
907
+ var HttpConfig = class HttpConfig extends EffectContext.Tag("HttpConfig")() {
908
+ static default = Layer.succeed(HttpConfig, {
909
+ timeout: Duration.millis(1e3),
910
+ retryTimes: 3,
911
+ retryBaseDelay: Duration.millis(1e3)
912
+ });
913
+ };
914
+ var withRetry = (effect, { timeout = Duration.millis(1e3), retryBaseDelay = Duration.millis(1e3), retryTimes = 3 } = {}) => {
915
+ return effect.pipe(Effect.flatMap((res) => res.status === 500 ? Effect.fail(new Error(res.status.toString())) : res.json), Effect.timeout(timeout), Effect.retry({
916
+ schedule: Schedule.exponential(retryBaseDelay).pipe(Schedule.jittered),
917
+ times: retryTimes
918
+ }));
919
+ };
920
+ var withRetryConfig = (effect) => Effect.gen(function* () {
921
+ return yield* withRetry(effect, yield* HttpConfig);
922
+ });
923
+ var withLogging = (effect) => effect.pipe(Effect.tap((res) => {
924
+ log.info("response", { status: res.status }, {
925
+ "~LogMeta": "~LogMeta",
926
+ F: __dxlog_file$3,
927
+ L: 66,
928
+ S: void 0
929
+ });
930
+ }));
931
+ /**
932
+ *
933
+ */
934
+ var encodeAuthHeader = (challenge) => {
935
+ return `VerifiablePresentation pb;base64,${Buffer.from(challenge).toString("base64")}`;
936
+ };
937
+ //#endregion
938
+ //#region src/base-http-client.ts
939
+ var __dxlog_file$2 = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/base-http-client.ts";
940
+ var DEFAULT_RETRY_TIMEOUT = 1500;
941
+ var DEFAULT_RETRY_JITTER = 500;
942
+ var DEFAULT_MAX_RETRIES_COUNT = 3;
943
+ var WARNING_BODY_SIZE = 10 * 1024 * 1024;
944
+ var BaseHttpClient = class {
945
+ _baseUrl;
946
+ _clientTag;
947
+ _edgeIdentity;
948
+ /** Auth header cached until next 401. */
949
+ _authHeader;
950
+ constructor(baseUrl, options) {
951
+ this._baseUrl = getEdgeUrlWithProtocol(baseUrl, "http");
952
+ this._clientTag = options?.clientTag;
953
+ log("created", { url: this._baseUrl }, {
954
+ "~LogMeta": "~LogMeta",
955
+ F: __dxlog_file$2,
956
+ L: 74,
957
+ S: this
958
+ });
959
+ }
960
+ get baseUrl() {
961
+ return this._baseUrl;
962
+ }
963
+ setIdentity(identity) {
964
+ if (this._edgeIdentity?.identityDid !== identity.identityDid || this._edgeIdentity?.peerKey !== identity.peerKey) {
965
+ this._edgeIdentity = identity;
966
+ this._authHeader = void 0;
967
+ }
968
+ }
969
+ async _call(ctx, url, args) {
970
+ const shouldRetry = createRetryHandler(args);
971
+ log("fetch", {
972
+ url,
973
+ hasBody: args.body !== void 0,
974
+ bodySize: typeof args.body === "string" ? args.body.length : void 0
975
+ }, {
976
+ "~LogMeta": "~LogMeta",
977
+ F: __dxlog_file$2,
978
+ L: 91,
979
+ S: this
980
+ });
981
+ const traceHeaders = getTraceHeaders(ctx);
982
+ let handledAuth = false;
983
+ const tryCount = 1;
984
+ while (true) {
985
+ let processingError = void 0;
986
+ try {
987
+ if (!this._authHeader && args.auth) {
988
+ const response = await fetch(new URL("/auth", this._baseUrl));
989
+ if (response.status === 401) this._authHeader = await this._handleUnauthorized(response);
990
+ }
991
+ const request = createRequest(args, this._authHeader, traceHeaders, this._clientTag);
992
+ log("call", {
993
+ url,
994
+ tryCount,
995
+ authHeader: !!this._authHeader
996
+ }, {
997
+ "~LogMeta": "~LogMeta",
998
+ F: __dxlog_file$2,
999
+ L: 112,
1000
+ S: this
1001
+ });
1002
+ const response = await fetch(url, request);
1003
+ if (response.ok) {
1004
+ const contentType = response.headers.get("Content-Type") ?? "";
1005
+ if (response.status === 204 || response.headers.get("Content-Length") === "0" || !contentType.includes("application/json")) return;
1006
+ const body = await response.clone().json();
1007
+ if (typeof body !== "object" || body === null) return body;
1008
+ if (!("success" in body)) return body;
1009
+ if (body.success) return body.data;
1010
+ } else if (response.status === 401 && response.headers.get("WWW-Authenticate") !== null && !handledAuth) {
1011
+ this._authHeader = await this._handleUnauthorized(response);
1012
+ handledAuth = true;
1013
+ continue;
1014
+ }
1015
+ const body = (response.headers.get("Content-Type") ?? "").startsWith("application/json") ? await response.clone().json() : void 0;
1016
+ invariant(!body?.success, "Expected body to not be a failure response or undefined.", {
1017
+ "~LogMeta": "~LogMeta",
1018
+ F: __dxlog_file$2,
1019
+ L: 148,
1020
+ S: this,
1021
+ A: ["!body?.success", "'Expected body to not be a failure response or undefined.'"]
1022
+ });
1023
+ if (body?.data?.type === "auth_challenge" && typeof body?.data?.challenge === "string") processingError = new EdgeAuthChallengeError(body.data.challenge, body.data);
1024
+ else if (body?.success === false) processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
1025
+ else {
1026
+ invariant(!response.ok, "Expected response to not be ok.", {
1027
+ "~LogMeta": "~LogMeta",
1028
+ F: __dxlog_file$2,
1029
+ L: 155,
1030
+ S: this,
1031
+ A: ["!response.ok", "'Expected response to not be ok.'"]
1032
+ });
1033
+ processingError = await EdgeCallFailedError.fromHttpFailure(response);
1034
+ }
1035
+ } catch (error) {
1036
+ processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
1037
+ }
1038
+ if (processingError?.isRetryable && await shouldRetry(ctx, processingError.retryAfterMs)) log.verbose("retrying request", {
1039
+ url,
1040
+ processingError
1041
+ }, {
1042
+ "~LogMeta": "~LogMeta",
1043
+ F: __dxlog_file$2,
1044
+ L: 163,
1045
+ S: this
1046
+ });
1047
+ else throw processingError;
1048
+ }
1049
+ }
1050
+ /**
1051
+ * Like {@link _call} but returns the raw `Response` instead of parsing a JSON envelope — for
1052
+ * endpoints with binary or absent response bodies (e.g. blob storage). A 404 is returned to the
1053
+ * caller rather than thrown, since "not found" is an expected outcome for lookups; all other
1054
+ * non-ok, non-retryable statuses throw `EdgeCallFailedError`, mirroring `_call`.
1055
+ *
1056
+ * NOTE: Duplicates `_call`'s auth/retry loop rather than sharing it, to avoid touching `_call`'s
1057
+ * broadly-depended-on JSON-envelope behavior. `EdgeHttpClient.anthropicAiRequest`'s separate
1058
+ * duplicate loop is a follow-up candidate for consolidating onto this method.
1059
+ */
1060
+ async _callRaw(ctx, url, args) {
1061
+ const shouldRetry = createRetryHandler(args);
1062
+ log("fetch", {
1063
+ url,
1064
+ hasBody: args.body !== void 0
1065
+ }, {
1066
+ "~LogMeta": "~LogMeta",
1067
+ F: __dxlog_file$2,
1068
+ L: 182,
1069
+ S: this
1070
+ });
1071
+ const traceHeaders = getTraceHeaders(ctx);
1072
+ let handledAuth = false;
1073
+ while (true) {
1074
+ let processingError;
1075
+ try {
1076
+ if (!this._authHeader && args.auth) {
1077
+ const response = await fetch(new URL("/auth", this._baseUrl));
1078
+ if (response.status === 401) this._authHeader = await this._handleUnauthorized(response);
1079
+ }
1080
+ const headers = { ...args.headers };
1081
+ if (this._authHeader) headers["Authorization"] = this._authHeader;
1082
+ if (traceHeaders) Object.assign(headers, traceHeaders);
1083
+ if (this._clientTag) headers[EDGE_CLIENT_TAG_HEADER] = this._clientTag;
1084
+ const response = await fetch(url, {
1085
+ method: args.method,
1086
+ body: args.body,
1087
+ headers
1088
+ });
1089
+ if (response.ok || response.status === 404) return response;
1090
+ if (response.status === 401 && response.headers.get("WWW-Authenticate") !== null && !handledAuth) {
1091
+ this._authHeader = await this._handleUnauthorized(response);
1092
+ handledAuth = true;
1093
+ continue;
1094
+ }
1095
+ processingError = await EdgeCallFailedError.fromHttpFailure(response);
1096
+ } catch (error) {
1097
+ processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
1098
+ }
1099
+ if (processingError?.isRetryable && await shouldRetry(ctx, processingError.retryAfterMs)) log.verbose("retrying raw request", {
1100
+ url,
1101
+ processingError
1102
+ }, {
1103
+ "~LogMeta": "~LogMeta",
1104
+ F: __dxlog_file$2,
1105
+ L: 226,
1106
+ S: this
1107
+ });
1108
+ else throw processingError;
1109
+ }
1110
+ }
1111
+ async _handleUnauthorized(response) {
1112
+ if (!this._edgeIdentity) {
1113
+ log.warn("unauthorized response received before identity was set", void 0, {
1114
+ "~LogMeta": "~LogMeta",
1115
+ F: __dxlog_file$2,
1116
+ L: 235,
1117
+ S: this
1118
+ });
1119
+ throw await EdgeCallFailedError.fromHttpFailure(response);
1120
+ }
1121
+ return encodeAuthHeader(await handleAuthChallenge(response, this._edgeIdentity));
1122
+ }
1123
+ };
1124
+ var createRequest = ({ method, body, json = true }, authHeader, traceHeaders, clientTag) => {
1125
+ let requestBody;
1126
+ const headers = {};
1127
+ if (json) {
1128
+ requestBody = body === void 0 ? void 0 : JSON.stringify(body);
1129
+ headers["Content-Type"] = "application/json";
1130
+ } else requestBody = body;
1131
+ if (typeof requestBody === "string" && requestBody.length > WARNING_BODY_SIZE) log.warn("Request with large body", { bodySize: requestBody.length }, {
1132
+ "~LogMeta": "~LogMeta",
1133
+ F: __dxlog_file$2,
1134
+ L: 260,
1135
+ S: void 0
1136
+ });
1137
+ if (authHeader) headers["Authorization"] = authHeader;
1138
+ if (traceHeaders) Object.assign(headers, traceHeaders);
1139
+ if (clientTag) headers[EDGE_CLIENT_TAG_HEADER] = clientTag;
1140
+ return {
1141
+ method,
1142
+ body: requestBody,
1143
+ headers
1144
+ };
1145
+ };
1146
+ var getTraceHeaders = (ctx) => {
1147
+ const traceCtx = ctx.getAttribute(TRACE_SPAN_ATTRIBUTE);
1148
+ if (!traceCtx) return;
1149
+ const headers = { traceparent: traceCtx.traceparent };
1150
+ if (traceCtx.tracestate) headers.tracestate = traceCtx.tracestate;
1151
+ return headers;
1152
+ };
1153
+ /** @deprecated */
1154
+ var createRetryHandler = ({ retry }) => {
1155
+ if (!retry || retry.count < 1) return async () => false;
1156
+ let retries = 0;
1157
+ const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
1158
+ const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
1159
+ const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
1160
+ return async (ctx, retryAfter) => {
1161
+ if (++retries > maxRetries || ctx.disposed) return false;
1162
+ if (retryAfter) await sleep(retryAfter);
1163
+ else await sleep(baseTimeout + Math.random() * jitter);
1164
+ return true;
1165
+ };
1166
+ };
1167
+ //#endregion
1168
+ //#region src/edge-ai-http-client.ts
1169
+ var __dxlog_file$1 = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/edge-ai-http-client.ts";
1170
+ /**
1171
+ * Thrown by {@link EdgeAiHttpClient} when an AI request carrying {@link BYOK_HEADER} is rejected
1172
+ * with 401/403 by the upstream provider — i.e. the user-supplied API key is invalid. Wrapped as
1173
+ * the `cause` of an `HttpClientError.ResponseError` so it flows through `@effect/ai`'s error
1174
+ * mapping; callers walk the cause chain (via {@link ByokError.is}) to render a useful message.
1175
+ */
1176
+ var ByokError = class extends BaseError.extend("ByokError", "BYOK authentication failed") {
1177
+ constructor(options) {
1178
+ super({
1179
+ context: {
1180
+ status: options.status,
1181
+ provider: options.provider
1182
+ },
1183
+ ...options
1184
+ });
1185
+ }
1186
+ };
1187
+ /**
1188
+ * Thrown by {@link EdgeAiHttpClient} when EDGE rejects an AI request with 429 because the
1189
+ * authenticated profile exceeded a metering limit. Wrapped as the `cause` of an
1190
+ * {@link HttpClientError.ResponseError} so it survives `@effect/ai`'s error mapping.
1191
+ */
1192
+ var UsageQuotaExceededError = class extends BaseError.extend("UsageQuotaExceededError", "Usage quota exceeded") {};
1193
+ /**
1194
+ * Copy pasted from https://github.com/Effect-TS/effect/blob/main/packages/platform/src/internal/fetchHttpClient.ts
1195
+ */
1196
+ var requestInitTagKey = "@effect/platform/FetchHttpClient/FetchOptions";
1197
+ var isUserDefinedAnthropicTool = (tool) => tool.input_schema != null && typeof tool.input_schema === "object";
1198
+ /**
1199
+ * Enables Anthropic fine-grained tool input streaming for client-defined tools.
1200
+ * Provider tools (bash, web_search, etc.) are left unchanged.
1201
+ */
1202
+ var patchAnthropicMessagesRequestBody = (body) => {
1203
+ if (body == null) return body;
1204
+ const decodeBody = () => {
1205
+ if (typeof body === "string") return body;
1206
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
1207
+ };
1208
+ const text = decodeBody();
1209
+ if (text == null) return body;
1210
+ try {
1211
+ const payload = JSON.parse(text);
1212
+ if (!Array.isArray(payload.tools)) return body;
1213
+ payload.tools = payload.tools.map((tool) => isUserDefinedAnthropicTool(tool) ? {
1214
+ ...tool,
1215
+ eager_input_streaming: true
1216
+ } : tool);
1217
+ const patched = JSON.stringify(payload);
1218
+ return typeof body === "string" ? patched : new TextEncoder().encode(patched);
1219
+ } catch {
1220
+ return body;
1221
+ }
1222
+ };
1223
+ var readStreamBody = (stream) => Effect.promise(async () => {
1224
+ return await new Response(stream).text();
1225
+ });
1226
+ /**
1227
+ * An `@effect/platform` {@link HttpClient.HttpClient} that routes requests through the
1228
+ * authenticated EDGE AI endpoint via {@link EdgeHttpClient.anthropicAiRequest}, instead of
1229
+ * fetching the AI service directly.
1230
+ *
1231
+ * Provide this layer in place of `FetchHttpClient.layer` when constructing an Anthropic client,
1232
+ * e.g. `AnthropicClient.layer({ apiUrl: 'http://edge' }).pipe(Layer.provide(EdgeAiHttpClient.layer(() => edgeClient)))`.
1233
+ * The `apiUrl` host is a sentinel; only the request path is forwarded (see `anthropicAiRequest`).
1234
+ *
1235
+ * Modeled on `FunctionsAiHttpClient` in `@dxos/functions`.
1236
+ */
1237
+ var EdgeAiHttpClient = class EdgeAiHttpClient {
1238
+ static make = (getClient) => HttpClient.make((request, url, signal, fiber) => {
1239
+ const edgeClient = getClient();
1240
+ const options = fiber.getFiberRef(FiberRef.currentContext).unsafeMap.get("@effect/platform/FetchHttpClient/FetchOptions") ?? {};
1241
+ const headers = options.headers ? Headers$1.merge(Headers$1.fromInput(options.headers), request.headers) : request.headers;
1242
+ const carriedByok = !!headers[BYOK_HEADER.toLowerCase()];
1243
+ const send = (body) => Effect.tryPromise({
1244
+ try: () => edgeClient.anthropicAiRequest(new Request(url, {
1245
+ ...options,
1246
+ method: request.method,
1247
+ headers,
1248
+ body: patchAnthropicMessagesRequestBody(body),
1249
+ signal
1250
+ })),
1251
+ catch: (cause) => {
1252
+ log.error("Failed to fetch", { cause }, {
1253
+ "~LogMeta": "~LogMeta",
1254
+ F: __dxlog_file$1,
1255
+ L: 136,
1256
+ S: this
1257
+ });
1258
+ return new HttpClientError.RequestError({
1259
+ request,
1260
+ reason: "Transport",
1261
+ cause
1262
+ });
1263
+ }
1264
+ }).pipe(Effect.flatMap((response) => {
1265
+ const httpResponse = HttpClientResponse.fromWeb(request, response);
1266
+ if (carriedByok && (response.status === 401 || response.status === 403)) return Effect.tryPromise({
1267
+ try: () => response.clone().json(),
1268
+ catch: () => void 0
1269
+ }).pipe(Effect.orElseSucceed(() => void 0), Effect.flatMap((body) => Effect.fail(new HttpClientError.ResponseError({
1270
+ request,
1271
+ response: httpResponse,
1272
+ reason: "StatusCode",
1273
+ cause: new ByokError({
1274
+ status: response.status,
1275
+ provider: "anthropic.com",
1276
+ message: body?.error?.message ?? "Authentication failed"
1277
+ })
1278
+ }))));
1279
+ if (!carriedByok && response.status === 429) return Effect.tryPromise({
1280
+ try: () => response.clone().json(),
1281
+ catch: () => void 0
1282
+ }).pipe(Effect.orElseSucceed(() => void 0), Effect.flatMap((body) => Effect.fail(new HttpClientError.ResponseError({
1283
+ request,
1284
+ response: httpResponse,
1285
+ reason: "StatusCode",
1286
+ cause: new UsageQuotaExceededError({ message: body?.error?.message })
1287
+ }))));
1288
+ return Effect.succeed(httpResponse);
1289
+ }));
1290
+ switch (request.body._tag) {
1291
+ case "Raw":
1292
+ case "Uint8Array": return send(request.body.body);
1293
+ case "FormData": return send(request.body.formData);
1294
+ case "Stream": return Stream.toReadableStreamEffect(request.body.stream).pipe(Effect.flatMap((readable) => readStreamBody(readable)), Effect.flatMap((text) => send(text)));
1295
+ }
1296
+ return send(void 0);
1297
+ });
1298
+ static layer = (getClient) => Layer.succeed(HttpClient.HttpClient, EdgeAiHttpClient.make(getClient));
1299
+ };
1300
+ //#endregion
1301
+ //#region src/edge-http-client.ts
1302
+ var __dxlog_file = "/__w/dxos/dxos/packages/core/mesh/edge-client/src/edge-http-client.ts";
1303
+ var EdgeHttpClientService = class extends EffectContext.Tag("@dxos/edge-client/EdgeHttpClient")() {};
1304
+ /**
1305
+ * HTTP client for the edge worker API (spaces, queues, functions, agents, etc.).
1306
+ *
1307
+ * Hub-service API (accounts, invitations) lives in {@link HubHttpClient} — the two
1308
+ * services run at different URLs and are never both available from the same base URL.
1309
+ */
1310
+ var EdgeHttpClient = class extends BaseHttpClient {
1311
+ constructor(baseUrl, options) {
1312
+ super(baseUrl, options);
1313
+ log("created", { url: this.baseUrl }, {
1314
+ "~LogMeta": "~LogMeta",
1315
+ F: __dxlog_file,
1316
+ L: 143,
1317
+ S: this
1318
+ });
1319
+ }
1320
+ async getStatus(ctx, args) {
1321
+ return this._call(ctx, new URL("/status", this.baseUrl), {
1322
+ ...args,
1323
+ method: "GET",
1324
+ auth: true
1325
+ });
1326
+ }
1327
+ createAgent(ctx, body, args) {
1328
+ return this._call(ctx, new URL("/agents/create", this.baseUrl), {
1329
+ ...args,
1330
+ method: "POST",
1331
+ body
1332
+ });
1333
+ }
1334
+ getAgentStatus(ctx, request, args) {
1335
+ return this._call(ctx, new URL(`/users/${request.ownerIdentityDid}/agent/status`, this.baseUrl), {
1336
+ ...args,
1337
+ method: "GET"
1338
+ });
1339
+ }
1340
+ getCredentialsForNotarization(ctx, spaceId, args) {
1341
+ return this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), {
1342
+ ...args,
1343
+ method: "GET"
1344
+ });
1345
+ }
1346
+ async notarizeCredentials(ctx, spaceId, body, args) {
1347
+ await this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), {
1348
+ ...args,
1349
+ body,
1350
+ method: "POST"
1351
+ });
1352
+ }
1353
+ async recoverIdentity(ctx, body, args) {
1354
+ return this._call(ctx, new URL("/identity/recover", this.baseUrl), {
1355
+ ...args,
1356
+ body,
1357
+ method: "POST"
1358
+ });
1359
+ }
1360
+ async joinSpaceByInvitation(ctx, spaceId, body, args) {
1361
+ return this._call(ctx, new URL(`/spaces/${spaceId}/join`, this.baseUrl), {
1362
+ ...args,
1363
+ body,
1364
+ method: "POST"
1365
+ });
1366
+ }
1367
+ async initiateOAuthFlow(ctx, body, args) {
1368
+ return this._call(ctx, new URL("/oauth/initiate", this.baseUrl), {
1369
+ ...args,
1370
+ body,
1371
+ method: "POST"
1372
+ });
1373
+ }
1374
+ async completeOAuthRegistration(ctx, body, args) {
1375
+ return this._call(ctx, new URL("/oauth/registration/complete", this.baseUrl), {
1376
+ ...args,
1377
+ body,
1378
+ method: "POST"
1379
+ });
1380
+ }
1381
+ /**
1382
+ * Resolves the live access token behind a `MANAGED_ACCESS_TOKEN` placeholder. Authorized by the
1383
+ * caller's presentation: EDGE serves it only to members of the owning space.
1384
+ */
1385
+ async getAccessToken(ctx, body, args) {
1386
+ return this._call(ctx, new URL("/oauth/token", this.baseUrl), {
1387
+ ...args,
1388
+ body,
1389
+ method: "POST"
1390
+ });
1391
+ }
1392
+ async createSpace(ctx, body, args) {
1393
+ return this._call(ctx, new URL("/spaces/create", this.baseUrl), {
1394
+ ...args,
1395
+ body,
1396
+ method: "POST"
1397
+ });
1398
+ }
1399
+ async queryQueue(ctx, subspaceTag, spaceId, query, args) {
1400
+ const queueId = query.feedIds?.[0];
1401
+ invariant(queueId, "queueId required", {
1402
+ "~LogMeta": "~LogMeta",
1403
+ F: __dxlog_file,
1404
+ L: 275,
1405
+ S: this,
1406
+ A: ["queueId", "'queueId required'"]
1407
+ });
1408
+ return this._call(ctx, createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
1409
+ after: query.after,
1410
+ before: query.before,
1411
+ limit: query.limit,
1412
+ reverse: query.reverse,
1413
+ objectIds: query.objectIds?.join(",")
1414
+ }), {
1415
+ ...args,
1416
+ method: "GET"
1417
+ });
1418
+ }
1419
+ async insertIntoQueue(ctx, subspaceTag, spaceId, queueId, objects, args) {
1420
+ return this._call(ctx, new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
1421
+ ...args,
1422
+ body: { objects },
1423
+ method: "POST"
1424
+ });
1425
+ }
1426
+ async deleteFromQueue(ctx, subspaceTag, spaceId, queueId, objectIds, args) {
1427
+ return this._call(ctx, createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), { ids: objectIds.join(",") }), {
1428
+ ...args,
1429
+ method: "DELETE"
1430
+ });
1431
+ }
1432
+ /**
1433
+ * Builds the URL for the blob stored under `key`. `key` is URL-encoded for defense in depth —
1434
+ * callers pass a lowercase hex SHA-256 digest (extracted from an `ni:` URI by the edge backend).
1435
+ */
1436
+ getBlobUrl(key) {
1437
+ return new URL(`/api/file/${encodeURIComponent(key)}`, this.baseUrl);
1438
+ }
1439
+ /**
1440
+ * Uploads bytes to the edge blob service, keyed by content hash. Pre-fetches `/auth` (`auth:
1441
+ * true`) so large bodies aren't sent twice on an auth challenge.
1442
+ */
1443
+ async putBlob(ctx, key, data, args) {
1444
+ const headers = {};
1445
+ if (args?.contentType) headers["Content-Type"] = args.contentType;
1446
+ await this._callRaw(ctx, this.getBlobUrl(key), {
1447
+ retry: args?.retry,
1448
+ auth: args?.auth ?? true,
1449
+ method: "POST",
1450
+ body: data,
1451
+ headers
1452
+ });
1453
+ }
1454
+ /**
1455
+ * Downloads bytes previously stored with {@link putBlob}. Returns `undefined` if `key` is not
1456
+ * found.
1457
+ */
1458
+ async getBlob(ctx, key, args) {
1459
+ const response = await this._callRaw(ctx, this.getBlobUrl(key), {
1460
+ ...args,
1461
+ method: "GET"
1462
+ });
1463
+ if (response.status === 404) return;
1464
+ return new Uint8Array(await response.arrayBuffer());
1465
+ }
1466
+ /**
1467
+ * Checks whether bytes are stored under `key`, without downloading them.
1468
+ */
1469
+ async hasBlob(ctx, key, args) {
1470
+ return (await this._callRaw(ctx, this.getBlobUrl(key), {
1471
+ ...args,
1472
+ method: "HEAD"
1473
+ })).status !== 404;
1474
+ }
1475
+ /**
1476
+ * Deletes bytes stored under `key`. Not called by any core `Blob.remove` path in v1 (deletion is
1477
+ * deferred), provided for completeness.
1478
+ */
1479
+ async deleteBlob(ctx, key, args) {
1480
+ await this._callRaw(ctx, this.getBlobUrl(key), {
1481
+ ...args,
1482
+ method: "DELETE"
1483
+ });
1484
+ }
1485
+ async uploadFunction(ctx, pathParts, body, args) {
1486
+ const formData = new FormData();
1487
+ formData.append("name", body.name ?? "");
1488
+ formData.append("version", body.version);
1489
+ const ownerUri = this._edgeIdentity?.identityDid ?? body.ownerUri;
1490
+ formData.append("ownerUri", ownerUri);
1491
+ formData.append("entryPoint", body.entryPoint);
1492
+ body.runtime && formData.append("runtime", body.runtime);
1493
+ for (const [filename, content] of Object.entries(body.assets)) formData.append("assets", new Blob([content], { type: getFileMimeType(filename) }), filename);
1494
+ const path = ["functions", ...pathParts.functionId ? [pathParts.functionId] : []].join("/");
1495
+ return this._call(ctx, new URL(path, this.baseUrl), {
1496
+ ...args,
1497
+ body: formData,
1498
+ method: "PUT",
1499
+ json: false
1500
+ });
1501
+ }
1502
+ async listFunctions(ctx, args) {
1503
+ return this._call(ctx, new URL("/functions", this.baseUrl), {
1504
+ ...args,
1505
+ method: "GET"
1506
+ });
1507
+ }
1508
+ async invokeFunction(ctx, params, input, args) {
1509
+ const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
1510
+ if (params.version) url.searchParams.set("version", params.version);
1511
+ if (params.spaceId) url.searchParams.set("spaceId", params.spaceId.toString());
1512
+ if (params.cpuTimeLimit) url.searchParams.set("cpuTimeLimit", params.cpuTimeLimit.toString());
1513
+ if (params.subrequestsLimit) url.searchParams.set("subrequestsLimit", params.subrequestsLimit.toString());
1514
+ return this._call(ctx, url, {
1515
+ ...args,
1516
+ body: input,
1517
+ method: "POST"
1518
+ });
1519
+ }
1520
+ async executeWorkflow(ctx, spaceId, graphId, input, args) {
1521
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
1522
+ ...args,
1523
+ body: input,
1524
+ method: "POST"
1525
+ });
1526
+ }
1527
+ async getCronTriggers(ctx, spaceId) {
1528
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons`, this.baseUrl), { method: "GET" });
1529
+ }
1530
+ async getTriggersDispatcherStatus(ctx, spaceId, args) {
1531
+ return this._call(ctx, new URL(`/triggers/${spaceId}/status`, this.baseUrl), {
1532
+ ...args,
1533
+ method: "GET",
1534
+ auth: true
1535
+ });
1536
+ }
1537
+ async forceRunCronTrigger(ctx, spaceId, triggerId) {
1538
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons/${triggerId}/run`, this.baseUrl), { method: "POST" });
1539
+ }
1540
+ /**
1541
+ * Cancels the current run of a cron trigger on the EDGE dispatcher — its in-flight execution and
1542
+ * `runAgain` continuation chain. The trigger stays enabled, so its schedule keeps firing.
1543
+ */
1544
+ async cancelTriggerRun(ctx, spaceId, triggerId) {
1545
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons/${triggerId}/cancel`, this.baseUrl), { method: "POST" });
1546
+ }
1547
+ /**
1548
+ * Returns the full list of triggers registered on a space's EDGE dispatcher, with per-trigger
1549
+ * runtime status. Polled by the remote trigger monitor to surface edge trigger state.
1550
+ *
1551
+ * TODO(edge): Proposed endpoint; not yet implemented server-side.
1552
+ */
1553
+ async getSpaceTriggers(ctx, spaceId, args) {
1554
+ return this._call(ctx, new URL(`/triggers/${spaceId}`, this.baseUrl), {
1555
+ ...args,
1556
+ method: "GET",
1557
+ auth: true
1558
+ });
1559
+ }
1560
+ async execQuery(ctx, spaceId, body, args) {
1561
+ return this._call(ctx, new URL(`/spaces/${spaceId}/exec-query`, this.baseUrl), {
1562
+ ...args,
1563
+ body,
1564
+ method: "POST"
1565
+ });
1566
+ }
1567
+ async getRegistryPlugins(ctx, args) {
1568
+ return this._call(ctx, new URL("/registry/plugins", this.baseUrl), {
1569
+ ...args,
1570
+ method: "GET"
1571
+ });
1572
+ }
1573
+ /**
1574
+ * Uploads a built plugin bundle to the registry's R2-backed hosting. Authenticated
1575
+ * with the caller's hub identity (verifiable presentation) — `setIdentity` must
1576
+ * have been called. Returns the canonical `moduleUrl` (the hosted `manifest.json`).
1577
+ */
1578
+ async uploadPluginBundle(ctx, request, args) {
1579
+ return this._call(ctx, new URL("/registry/upload", this.baseUrl), {
1580
+ body: request,
1581
+ method: "POST",
1582
+ auth: true,
1583
+ ...args
1584
+ });
1585
+ }
1586
+ async importBundle(ctx, spaceId, body, args) {
1587
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), {
1588
+ ...args,
1589
+ body,
1590
+ method: "PUT"
1591
+ });
1592
+ }
1593
+ async exportBundle(ctx, spaceId, body, args) {
1594
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
1595
+ ...args,
1596
+ body,
1597
+ method: "POST"
1598
+ });
1599
+ }
1600
+ /**
1601
+ * Fetch through the edge proxy for third-party REST APIs.
1602
+ * TEMPORARY: currently routes through legacy open proxy. See https://github.com/dxos/edge/pull/576.
1603
+ */
1604
+ async proxyFetch(target, init = {}) {
1605
+ return proxyFetchLegacy(target, init, this._clientTag);
1606
+ }
1607
+ /**
1608
+ * Issue an authenticated request to the EDGE AI route (`/ai/generate/anthropic/*`), which
1609
+ * proxies to the AI service. Used as the backend HTTP client for the Anthropic AI provider
1610
+ * (see {@link EdgeAiHttpClient}).
1611
+ *
1612
+ * Returns the raw `Response` so streaming bodies are forwarded unchanged to `@effect/ai`.
1613
+ * Requires an identity to have been set via {@link setIdentity}.
1614
+ */
1615
+ async anthropicAiRequest(request) {
1616
+ const incoming = new URL(request.url);
1617
+ const base = this.baseUrl.replace(/\/$/, "");
1618
+ const target = new URL(`${base}/ai/generate/anthropic${incoming.pathname}${incoming.search}`);
1619
+ const method = request.method;
1620
+ const body = method === "GET" || method === "HEAD" ? void 0 : await request.arrayBuffer();
1621
+ let handledAuth = false;
1622
+ while (true) {
1623
+ if (!this._authHeader) {
1624
+ const authResponse = await fetch(new URL("/auth", this.baseUrl));
1625
+ if (authResponse.status === 401) this._authHeader = await this._handleUnauthorized(authResponse);
1626
+ }
1627
+ const headers = new Headers(request.headers);
1628
+ if (this._authHeader) headers.set("Authorization", this._authHeader);
1629
+ if (this._clientTag) headers.set(EDGE_CLIENT_TAG_HEADER, this._clientTag);
1630
+ const response = await fetch(target, {
1631
+ method,
1632
+ headers,
1633
+ body,
1634
+ signal: request.signal
1635
+ });
1636
+ if (response.status === 401 && response.headers.get("WWW-Authenticate") !== null && !handledAuth) {
1637
+ this._authHeader = await this._handleUnauthorized(response);
1638
+ handledAuth = true;
1639
+ continue;
1640
+ }
1641
+ return response;
1642
+ }
1643
+ }
1644
+ async _fetch(url, _args) {
1645
+ return Function.pipe(HttpClient.execute(HttpClientRequest.make(_args.method)(url.toString())), withLogging, withRetryConfig, Effect.provide(FetchHttpClient.layer), Effect.provide(HttpConfig.default), Effect.withSpan("EdgeHttpClient"), EffectEx.runAndForwardErrors);
1646
+ }
1647
+ };
1648
+ var getFileMimeType = (filename) => [".js", ".mjs"].some((ext) => filename.endsWith(ext)) ? "application/javascript+module" : filename.endsWith(".wasm") ? "application/wasm" : "application/octet-stream";
1649
+ //#endregion
1650
+ //#region src/hub-http-client.ts
1651
+ /**
1652
+ * HTTP client for the hub-service API (accounts, invitations, email verification).
1653
+ *
1654
+ * Hub-service and the edge worker are separate Cloudflare Workers deployed at different
1655
+ * URLs (`DX_HUB_URL` vs `DX_EDGE_URL`). This client is never used to talk to the edge
1656
+ * worker, and vice versa — keep them separate.
1657
+ *
1658
+ * NOTE: Do NOT set `auth: true` on any call here. Hub-service has no `/auth` VP-challenge
1659
+ * endpoint (it has an admin login page that 302s and is not CORS-enabled). Auth is handled
1660
+ * via the regular request → 401 → WWW-Authenticate challenge → retry path.
1661
+ */
1662
+ var HubHttpClient = class extends BaseHttpClient {
1663
+ constructor(hubUrl, options) {
1664
+ super(hubUrl, options);
1665
+ }
1666
+ async checkEmailExists(ctx, body, args) {
1667
+ return this._call(ctx, new URL("/account/email/exists", this.baseUrl), {
1668
+ ...args,
1669
+ body,
1670
+ method: "POST"
1671
+ });
1672
+ }
1673
+ async validateInvitationCode(ctx, body, args) {
1674
+ return this._call(ctx, new URL("/account/invitation-code/validate", this.baseUrl), {
1675
+ ...args,
1676
+ body,
1677
+ method: "POST"
1678
+ });
1679
+ }
1680
+ async redeemInvitationCode(ctx, body, args) {
1681
+ return this._call(ctx, new URL("/account/invitation-code/redeem", this.baseUrl), {
1682
+ ...args,
1683
+ body,
1684
+ method: "POST"
1685
+ });
1686
+ }
1687
+ /**
1688
+ * Existing-account email login. Server inlines `token` for test emails; regular
1689
+ * emails are delivered out-of-band. Response is identical for unknown emails
1690
+ * (enumeration-safe).
1691
+ */
1692
+ async login(ctx, body, args) {
1693
+ return this._call(ctx, new URL("/account/login", this.baseUrl), {
1694
+ ...args,
1695
+ body,
1696
+ method: "POST"
1697
+ });
1698
+ }
1699
+ async requestAccess(ctx, body, args) {
1700
+ return this._call(ctx, new URL("/account/request-access", this.baseUrl), {
1701
+ ...args,
1702
+ body,
1703
+ method: "POST"
1704
+ });
1705
+ }
1706
+ async getAccount(ctx, args) {
1707
+ return this._call(ctx, new URL("/account/me", this.baseUrl), {
1708
+ ...args,
1709
+ method: "GET"
1710
+ });
1711
+ }
1712
+ async deleteAccount(ctx, args) {
1713
+ return this._call(ctx, new URL("/account/me", this.baseUrl), {
1714
+ ...args,
1715
+ method: "DELETE"
1716
+ });
1717
+ }
1718
+ async listAccountInvitations(ctx, args) {
1719
+ return this._call(ctx, new URL("/account/invitation", this.baseUrl), {
1720
+ ...args,
1721
+ method: "GET"
1722
+ });
1723
+ }
1724
+ async issueAccountInvitation(ctx, args) {
1725
+ return this._call(ctx, new URL("/account/invitation/issue", this.baseUrl), {
1726
+ ...args,
1727
+ method: "POST"
1728
+ });
1729
+ }
1730
+ async resendVerificationEmail(ctx, args) {
1731
+ return this._call(ctx, new URL("/account/email/resend-verification", this.baseUrl), {
1732
+ ...args,
1733
+ method: "POST"
1734
+ });
1735
+ }
1736
+ /**
1737
+ * Rolling-window usage and effective limits for the authenticated identity.
1738
+ * Served from the per-user metering DO; optional `windowSeconds` defaults to the largest limit window.
1739
+ */
1740
+ async getProfileUsage(ctx, query, args) {
1741
+ return this._call(ctx, createUrl(new URL("/api/metering/profile/usage", this.baseUrl), { windowSeconds: query?.windowSeconds }), {
1742
+ ...args,
1743
+ method: "GET"
1744
+ });
1745
+ }
1746
+ };
1747
+ //#endregion
1748
+ //#region src/browser-rendering.ts
1749
+ /** Request body for EDGE `/ai/browser-rendering/markdown` (ai-service Browser Run markdown quick action). */
1750
+ var MarkdownRequest = Schema.Struct({
1751
+ url: Schema.optional(Schema.String),
1752
+ html: Schema.optional(Schema.String),
1753
+ gotoOptions: Schema.optional(Schema.Struct({
1754
+ waitUntil: Schema.optional(Schema.Literal("load", "domcontentloaded", "networkidle0", "networkidle2")),
1755
+ timeout: Schema.optional(Schema.Number)
1756
+ })),
1757
+ rejectRequestPattern: Schema.optional(Schema.Array(Schema.String)),
1758
+ userAgent: Schema.optional(Schema.String),
1759
+ waitForSelector: Schema.optional(Schema.String)
1760
+ });
1761
+ /** JSON body returned by Cloudflare Browser Run markdown quick action. */
1762
+ var MarkdownResponse = Schema.Struct({
1763
+ success: Schema.Boolean,
1764
+ result: Schema.String
1765
+ });
1766
+ //#endregion
1767
+ export { BaseHttpClient, ByokError, CLOUDFLARE_MESSAGE_MAX_BYTES, CLOUDFLARE_RPC_MAX_BYTES, EdgeAiHttpClient, EdgeClient, EdgeConnectionClosedError, EdgeConnectionService, EdgeHttpClient, EdgeHttpClientService, EdgeIdentityChangedError, HttpConfig, HubHttpClient, MarkdownRequest, MarkdownResponse, Protocol, UsageQuotaExceededError, WebSocketMuxer, createChainEdgeIdentity, createDeviceEdgeIdentity, createEphemeralEdgeIdentity, createStubEdgeIdentity, createTestHaloEdgeIdentity, encodeAuthHeader, getTypename, handleAuthChallenge, patchAnthropicMessagesRequestBody, protocol, proxyFetchLegacy, requestInitTagKey, toUint8Array, withLogging, withRetry, withRetryConfig };
1768
+
1769
+ //# sourceMappingURL=index.mjs.map