@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.
- package/dist/lib/chunk-edge-ws-muxer.mjs +322 -0
- package/dist/lib/chunk-edge-ws-muxer.mjs.map +1 -0
- package/dist/lib/cors-proxy.mjs +29 -0
- package/dist/lib/cors-proxy.mjs.map +1 -0
- package/dist/lib/edge-ws-muxer.mjs +2 -0
- package/dist/lib/index.mjs +1769 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/service.mjs +138 -0
- package/dist/lib/service.mjs.map +1 -0
- package/dist/lib/testing.mjs +160 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/base-http-client.d.ts +18 -0
- package/dist/types/src/base-http-client.d.ts.map +1 -1
- package/dist/types/src/browser-rendering.d.ts +0 -7
- package/dist/types/src/browser-rendering.d.ts.map +1 -1
- package/dist/types/src/edge-client.d.ts +22 -2
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +86 -2
- package/dist/types/src/edge-http-client.d.ts.map +1 -1
- package/dist/types/src/edge-ws-connection.d.ts +19 -0
- package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
- package/dist/types/src/edge-ws-connection.test.d.ts +2 -0
- package/dist/types/src/edge-ws-connection.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/protocol.d.ts +2 -1
- package/dist/types/src/protocol.d.ts.map +1 -1
- package/dist/types/src/testing/test-utils.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -20
- package/src/base-http-client.ts +71 -3
- package/src/browser-rendering.ts +0 -9
- package/src/edge-client.ts +21 -3
- package/src/edge-http-client.test.ts +121 -0
- package/src/edge-http-client.ts +156 -2
- package/src/edge-ws-connection.test.ts +219 -0
- package/src/edge-ws-connection.ts +97 -28
- package/src/index.ts +1 -0
- package/src/protocol.ts +11 -2
- package/src/testing/test-utils.ts +5 -1
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-L5ZHLJ4B.mjs +0 -310
- package/dist/lib/neutral/chunk-L5ZHLJ4B.mjs.map +0 -7
- package/dist/lib/neutral/chunk-WQKMEZJR.mjs +0 -30
- package/dist/lib/neutral/chunk-WQKMEZJR.mjs.map +0 -7
- package/dist/lib/neutral/cors-proxy.mjs +0 -8
- package/dist/lib/neutral/cors-proxy.mjs.map +0 -7
- package/dist/lib/neutral/edge-ws-muxer.mjs +0 -12
- package/dist/lib/neutral/edge-ws-muxer.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -1524
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/service/index.mjs +0 -134
- package/dist/lib/neutral/service/index.mjs.map +0 -7
- package/dist/lib/neutral/testing/index.mjs +0 -161
- 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
|