@dxos/edge-client 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced

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/LICENSE +102 -5
  2. package/dist/lib/{browser/chunk-LMP5TVOP.mjs → neutral/chunk-ZIQ5T3A7.mjs} +13 -43
  3. package/dist/lib/{browser/chunk-LMP5TVOP.mjs.map → neutral/chunk-ZIQ5T3A7.mjs.map} +3 -3
  4. package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
  5. package/dist/lib/neutral/index.mjs +1238 -0
  6. package/dist/lib/neutral/index.mjs.map +7 -0
  7. package/dist/lib/neutral/meta.json +1 -0
  8. package/dist/lib/{browser → neutral}/testing/index.mjs +6 -31
  9. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  10. package/dist/types/src/auth.d.ts.map +1 -1
  11. package/dist/types/src/edge-client.d.ts +18 -15
  12. package/dist/types/src/edge-client.d.ts.map +1 -1
  13. package/dist/types/src/edge-http-client.d.ts +114 -21
  14. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  15. package/dist/types/src/edge-identity.d.ts.map +1 -1
  16. package/dist/types/src/edge-ws-connection.d.ts +21 -0
  17. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  18. package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
  19. package/dist/types/src/errors.d.ts.map +1 -1
  20. package/dist/types/src/http-client.d.ts +10 -7
  21. package/dist/types/src/http-client.d.ts.map +1 -1
  22. package/dist/types/src/index.d.ts +4 -3
  23. package/dist/types/src/index.d.ts.map +1 -1
  24. package/dist/types/src/protocol.d.ts +1 -1
  25. package/dist/types/src/protocol.d.ts.map +1 -1
  26. package/dist/types/src/testing/test-server.d.ts.map +1 -1
  27. package/dist/types/src/testing/test-utils.d.ts +3 -3
  28. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  29. package/dist/types/src/utils.d.ts +1 -1
  30. package/dist/types/src/utils.d.ts.map +1 -1
  31. package/dist/types/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +32 -32
  33. package/src/edge-client.test.ts +20 -15
  34. package/src/edge-client.ts +90 -43
  35. package/src/edge-http-client.test.ts +3 -2
  36. package/src/edge-http-client.ts +462 -70
  37. package/src/edge-ws-connection.ts +131 -9
  38. package/src/edge-ws-muxer.ts +1 -1
  39. package/src/http-client.test.ts +11 -8
  40. package/src/http-client.ts +18 -8
  41. package/src/index.ts +4 -3
  42. package/src/testing/test-utils.ts +8 -8
  43. package/src/websocket.test.ts +1 -1
  44. package/dist/lib/browser/index.mjs +0 -1089
  45. package/dist/lib/browser/index.mjs.map +0 -7
  46. package/dist/lib/browser/meta.json +0 -1
  47. package/dist/lib/browser/testing/index.mjs.map +0 -7
  48. package/dist/lib/node-esm/chunk-X7J46ISZ.mjs +0 -299
  49. package/dist/lib/node-esm/chunk-X7J46ISZ.mjs.map +0 -7
  50. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  51. package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
  52. package/dist/lib/node-esm/index.mjs +0 -1090
  53. package/dist/lib/node-esm/index.mjs.map +0 -7
  54. package/dist/lib/node-esm/meta.json +0 -1
  55. package/dist/lib/node-esm/testing/index.mjs +0 -186
  56. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  57. /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -2,47 +2,78 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FetchHttpClient, HttpClient } from '@effect/platform';
6
- import { Effect, pipe } from 'effect';
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import * as HttpClient from '@effect/platform/HttpClient';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Function from 'effect/Function';
7
9
 
8
10
  import { sleep } from '@dxos/async';
9
- import { Context } from '@dxos/context';
11
+ import { Context, TRACE_SPAN_ATTRIBUTE, type TraceContextData } from '@dxos/context';
12
+ import { runAndForwardErrors } from '@dxos/effect';
13
+ import { invariant } from '@dxos/invariant';
10
14
  import { type PublicKey, type SpaceId } from '@dxos/keys';
11
15
  import { log } from '@dxos/log';
12
16
  import {
13
- type CreateAgentResponseBody,
14
17
  type CreateAgentRequestBody,
18
+ type CreateAgentResponseBody,
15
19
  type CreateSpaceRequest,
16
20
  type CreateSpaceResponseBody,
21
+ EDGE_CLIENT_TAG_HEADER,
17
22
  EdgeAuthChallengeError,
18
23
  EdgeCallFailedError,
19
- type EdgeHttpResponse,
24
+ type EdgeFailure,
25
+ type EdgeStatus,
20
26
  type ExecuteWorkflowResponseBody,
27
+ type ExportBundleRequest,
28
+ type ExportBundleResponse,
29
+ type FeedProtocol,
21
30
  type GetAgentStatusResponseBody,
31
+ type GetPluginVersionsResponseBody,
32
+ type GetPluginsResponseBody,
22
33
  type GetNotarizationResponseBody,
34
+ type ImportBundleRequest,
23
35
  type InitiateOAuthFlowRequest,
24
36
  type InitiateOAuthFlowResponse,
25
37
  type JoinSpaceRequest,
26
38
  type JoinSpaceResponseBody,
27
- type RecoverIdentityRequest,
28
- type RecoverIdentityResponseBody,
29
39
  type ObjectId,
30
40
  type PostNotarizationRequestBody,
31
- type QueueQuery,
32
- type QueryResult,
41
+ type RecoverIdentityRequest,
42
+ type RecoverIdentityResponseBody,
33
43
  type UploadFunctionRequest,
34
44
  type UploadFunctionResponseBody,
35
- type EdgeStatus,
36
45
  } from '@dxos/protocols';
46
+ import {
47
+ type QueryRequest as QueryRequestProto,
48
+ type QueryResponse as QueryResponseProto,
49
+ } from '@dxos/protocols/proto/dxos/echo/query';
37
50
  import { createUrl } from '@dxos/util';
38
51
 
39
52
  import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
40
- import { encodeAuthHeader, HttpConfig, withLogging, withRetryConfig } from './http-client';
53
+ import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
41
54
  import { getEdgeUrlWithProtocol } from './utils';
42
55
 
56
+ /**
57
+ * HTTP wire shape returned by `/queue/.../query`. Unlike `FeedProtocol.QueryResult`
58
+ * (the RPC proto type, which transports object payloads as JSON strings), the edge
59
+ * HTTP endpoint embeds each object directly in the response JSON.
60
+ */
61
+ export type EdgeQueryQueueResponse = {
62
+ objects?: unknown[];
63
+ nextCursor?: string;
64
+ prevCursor?: string;
65
+ };
66
+
43
67
  const DEFAULT_RETRY_TIMEOUT = 1500;
44
68
  const DEFAULT_RETRY_JITTER = 500;
45
69
  const DEFAULT_MAX_RETRIES_COUNT = 3;
70
+ const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
71
+
72
+ // TEMPORARY: legacy standalone CORS proxy used by `proxyFetch` until the
73
+ // authenticated `/proxy/*` route on the main edge worker ships
74
+ // (https://github.com/dxos/edge/pull/576). Delete this constant when the
75
+ // commented-out authenticated branch in `proxyFetch` is restored.
76
+ const LEGACY_CORS_PROXY_URL = 'https://cors-proxy.dxos.workers.dev';
46
77
 
47
78
  export type RetryConfig = {
48
79
  /**
@@ -61,16 +92,38 @@ export type RetryConfig = {
61
92
 
62
93
  type EdgeHttpRequestArgs = {
63
94
  method: string;
64
- context?: Context;
65
95
  retry?: RetryConfig;
66
96
  body?: any;
97
+ /**
98
+ * @default true
99
+ */
100
+ json?: boolean;
101
+
102
+ /**
103
+ * Force authentication.
104
+ * This should be used for requests with large bodies to avoid sending the body twice.
105
+ * The client will call /auth endpoint to generate the auth header.
106
+ */
107
+ auth?: boolean;
67
108
  };
68
109
 
69
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
70
- export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body'>;
110
+ export type EdgeHttpCallArgs = Pick<EdgeHttpRequestArgs, 'retry' | 'auth'>;
111
+
112
+ export type GetCronTriggersResponse = {
113
+ cronIds: string[];
114
+ };
115
+
116
+ export type EdgeHttpClientOptions = {
117
+ /**
118
+ * Tag included in the {@link EDGE_CLIENT_TAG_HEADER} header on every request.
119
+ * Used on Edge to classify traffic for metering (e.g. `ci-e2e`).
120
+ */
121
+ clientTag?: string;
122
+ };
71
123
 
72
124
  export class EdgeHttpClient {
73
125
  private readonly _baseUrl: string;
126
+ private readonly _clientTag: string | undefined;
74
127
 
75
128
  private _edgeIdentity: EdgeIdentity | undefined;
76
129
 
@@ -79,8 +132,9 @@ export class EdgeHttpClient {
79
132
  */
80
133
  private _authHeader: string | undefined;
81
134
 
82
- constructor(baseUrl: string) {
135
+ constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
83
136
  this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
137
+ this._clientTag = options?.clientTag;
84
138
  log('created', { url: this._baseUrl });
85
139
  }
86
140
 
@@ -99,23 +153,28 @@ export class EdgeHttpClient {
99
153
  // Status
100
154
  //
101
155
 
102
- public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
103
- return this._call(new URL('/status', this.baseUrl), { ...args, method: 'GET' });
156
+ public async getStatus(ctx: Context, args?: EdgeHttpCallArgs): Promise<EdgeStatus> {
157
+ return this._call(ctx, new URL('/status', this.baseUrl), { ...args, method: 'GET', auth: true });
104
158
  }
105
159
 
106
160
  //
107
161
  // Agents
108
162
  //
109
163
 
110
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
111
- return this._call(new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
164
+ public createAgent(
165
+ ctx: Context,
166
+ body: CreateAgentRequestBody,
167
+ args?: EdgeHttpCallArgs,
168
+ ): Promise<CreateAgentResponseBody> {
169
+ return this._call(ctx, new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
112
170
  }
113
171
 
114
172
  public getAgentStatus(
173
+ ctx: Context,
115
174
  request: { ownerIdentityKey: PublicKey },
116
- args?: EdgeHttpGetArgs,
175
+ args?: EdgeHttpCallArgs,
117
176
  ): Promise<GetAgentStatusResponseBody> {
118
- return this._call(new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
177
+ return this._call(ctx, new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
119
178
  ...args,
120
179
  method: 'GET',
121
180
  });
@@ -125,16 +184,21 @@ export class EdgeHttpClient {
125
184
  // Credentials
126
185
  //
127
186
 
128
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
129
- return this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
187
+ public getCredentialsForNotarization(
188
+ ctx: Context,
189
+ spaceId: SpaceId,
190
+ args?: EdgeHttpCallArgs,
191
+ ): Promise<GetNotarizationResponseBody> {
192
+ return this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
130
193
  }
131
194
 
132
195
  public async notarizeCredentials(
196
+ ctx: Context,
133
197
  spaceId: SpaceId,
134
198
  body: PostNotarizationRequestBody,
135
- args?: EdgeHttpGetArgs,
199
+ args?: EdgeHttpCallArgs,
136
200
  ): Promise<void> {
137
- await this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
201
+ await this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
138
202
  }
139
203
 
140
204
  //
@@ -142,10 +206,11 @@ export class EdgeHttpClient {
142
206
  //
143
207
 
144
208
  public async recoverIdentity(
209
+ ctx: Context,
145
210
  body: RecoverIdentityRequest,
146
- args?: EdgeHttpGetArgs,
211
+ args?: EdgeHttpCallArgs,
147
212
  ): Promise<RecoverIdentityResponseBody> {
148
- return this._call(new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
213
+ return this._call(ctx, new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
149
214
  }
150
215
 
151
216
  //
@@ -153,11 +218,12 @@ export class EdgeHttpClient {
153
218
  //
154
219
 
155
220
  public async joinSpaceByInvitation(
221
+ ctx: Context,
156
222
  spaceId: SpaceId,
157
223
  body: JoinSpaceRequest,
158
- args?: EdgeHttpGetArgs,
224
+ args?: EdgeHttpCallArgs,
159
225
  ): Promise<JoinSpaceResponseBody> {
160
- return this._call(new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
226
+ return this._call(ctx, new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
161
227
  }
162
228
 
163
229
  //
@@ -165,18 +231,19 @@ export class EdgeHttpClient {
165
231
  //
166
232
 
167
233
  public async initiateOAuthFlow(
234
+ ctx: Context,
168
235
  body: InitiateOAuthFlowRequest,
169
- args?: EdgeHttpGetArgs,
236
+ args?: EdgeHttpCallArgs,
170
237
  ): Promise<InitiateOAuthFlowResponse> {
171
- return this._call(new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
238
+ return this._call(ctx, new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
172
239
  }
173
240
 
174
241
  //
175
242
  // Spaces
176
243
  //
177
244
 
178
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
179
- return this._call(new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
245
+ async createSpace(ctx: Context, body: CreateSpaceRequest, args?: EdgeHttpCallArgs): Promise<CreateSpaceResponseBody> {
246
+ return this._call(ctx, new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
180
247
  }
181
248
 
182
249
  //
@@ -184,13 +251,16 @@ export class EdgeHttpClient {
184
251
  //
185
252
 
186
253
  public async queryQueue(
254
+ ctx: Context,
187
255
  subspaceTag: string,
188
256
  spaceId: SpaceId,
189
- query: QueueQuery,
190
- args?: EdgeHttpGetArgs,
191
- ): Promise<QueryResult> {
192
- const { queueId } = query;
257
+ query: FeedProtocol.QueueQuery,
258
+ args?: EdgeHttpCallArgs,
259
+ ): Promise<EdgeQueryQueueResponse> {
260
+ const queueId = query.queueIds?.[0];
261
+ invariant(queueId, 'queueId required');
193
262
  return this._call(
263
+ ctx,
194
264
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
195
265
  after: query.after,
196
266
  before: query.before,
@@ -206,13 +276,14 @@ export class EdgeHttpClient {
206
276
  }
207
277
 
208
278
  public async insertIntoQueue(
279
+ ctx: Context,
209
280
  subspaceTag: string,
210
281
  spaceId: SpaceId,
211
282
  queueId: ObjectId,
212
283
  objects: unknown[],
213
- args?: EdgeHttpGetArgs,
284
+ args?: EdgeHttpCallArgs,
214
285
  ): Promise<void> {
215
- return this._call(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
286
+ return this._call(ctx, new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
216
287
  ...args,
217
288
  body: { objects },
218
289
  method: 'POST',
@@ -220,13 +291,15 @@ export class EdgeHttpClient {
220
291
  }
221
292
 
222
293
  public async deleteFromQueue(
294
+ ctx: Context,
223
295
  subspaceTag: string,
224
296
  spaceId: SpaceId,
225
297
  queueId: ObjectId,
226
298
  objectIds: ObjectId[],
227
- args?: EdgeHttpGetArgs,
299
+ args?: EdgeHttpCallArgs,
228
300
  ): Promise<void> {
229
301
  return this._call(
302
+ ctx,
230
303
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
231
304
  ids: objectIds.join(','),
232
305
  }),
@@ -242,12 +315,69 @@ export class EdgeHttpClient {
242
315
  //
243
316
 
244
317
  public async uploadFunction(
318
+ ctx: Context,
245
319
  pathParts: { functionId?: string },
246
320
  body: UploadFunctionRequest,
247
- args?: EdgeHttpGetArgs,
321
+ args?: EdgeHttpCallArgs,
248
322
  ): Promise<UploadFunctionResponseBody> {
323
+ const formData = new FormData();
324
+ formData.append('name', body.name ?? '');
325
+ formData.append('version', body.version);
326
+ formData.append('ownerPublicKey', body.ownerPublicKey);
327
+ formData.append('entryPoint', body.entryPoint);
328
+ body.runtime && formData.append('runtime', body.runtime);
329
+ for (const [filename, content] of Object.entries(body.assets)) {
330
+ formData.append(
331
+ 'assets',
332
+ new Blob([content as Uint8Array<ArrayBuffer>], { type: getFileMimeType(filename) }),
333
+ filename,
334
+ );
335
+ }
336
+
249
337
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
250
- return this._call(new URL(path, this.baseUrl), { ...args, body, method: 'PUT' });
338
+ return this._call(ctx, new URL(path, this.baseUrl), {
339
+ ...args,
340
+ body: formData,
341
+ method: 'PUT',
342
+ json: false,
343
+ });
344
+ }
345
+
346
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
347
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
348
+ }
349
+
350
+ public async invokeFunction(
351
+ ctx: Context,
352
+ params: {
353
+ functionId: string;
354
+ version?: string;
355
+ spaceId?: SpaceId;
356
+ cpuTimeLimit?: number;
357
+ subrequestsLimit?: number;
358
+ },
359
+ input: unknown,
360
+ args?: EdgeHttpCallArgs,
361
+ ): Promise<any> {
362
+ const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
363
+ if (params.version) {
364
+ url.searchParams.set('version', params.version);
365
+ }
366
+ if (params.spaceId) {
367
+ url.searchParams.set('spaceId', params.spaceId.toString());
368
+ }
369
+ if (params.cpuTimeLimit) {
370
+ url.searchParams.set('cpuTimeLimit', params.cpuTimeLimit.toString());
371
+ }
372
+ if (params.subrequestsLimit) {
373
+ url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
374
+ }
375
+
376
+ return this._call(ctx, url, {
377
+ ...args,
378
+ body: input,
379
+ method: 'POST',
380
+ });
251
381
  }
252
382
 
253
383
  //
@@ -255,75 +385,241 @@ export class EdgeHttpClient {
255
385
  //
256
386
 
257
387
  public async executeWorkflow(
388
+ ctx: Context,
258
389
  spaceId: SpaceId,
259
390
  graphId: ObjectId,
260
391
  input: any,
261
- args?: EdgeHttpGetArgs,
392
+ args?: EdgeHttpCallArgs,
262
393
  ): Promise<ExecuteWorkflowResponseBody> {
263
- return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
394
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
264
395
  ...args,
265
396
  body: input,
266
397
  method: 'POST',
267
398
  });
268
399
  }
269
400
 
401
+ //
402
+ // Triggers
403
+ //
404
+
405
+ public async getCronTriggers(ctx: Context, spaceId: SpaceId): Promise<GetCronTriggersResponse> {
406
+ return this._call<GetCronTriggersResponse>(ctx, new URL(`/functions/${spaceId}/triggers/crons`, this.baseUrl), {
407
+ method: 'GET',
408
+ });
409
+ }
410
+
411
+ public async forceRunCronTrigger(ctx: Context, spaceId: SpaceId, triggerId: ObjectId) {
412
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons/${triggerId}/run`, this.baseUrl), {
413
+ method: 'POST',
414
+ });
415
+ }
416
+
417
+ //
418
+ // Query
419
+ //
420
+
421
+ /**
422
+ * Execute a QueryAST query against a space.
423
+ */
424
+ public async execQuery(
425
+ ctx: Context,
426
+ spaceId: SpaceId,
427
+ body: QueryRequestProto,
428
+ args?: EdgeHttpCallArgs,
429
+ ): Promise<QueryResponseProto> {
430
+ return this._call(ctx, new URL(`/spaces/${spaceId}/exec-query`, this.baseUrl), { ...args, body, method: 'POST' });
431
+ }
432
+
433
+ //
434
+ // Registry
435
+ //
436
+
437
+ /**
438
+ * Fetches the hydrated plugin directory from the Edge registry service.
439
+ * Unauthenticated; safe to call without an identity.
440
+ */
441
+ public async getRegistryPlugins(ctx: Context, args?: EdgeHttpCallArgs): Promise<GetPluginsResponseBody> {
442
+ return this._call(ctx, new URL('/registry/plugins', this.baseUrl), { ...args, method: 'GET' });
443
+ }
444
+
445
+ /**
446
+ * Fetches the available release versions for a given plugin repo. `repo` is the
447
+ * GitHub `owner/name` form; this method takes care of URL-encoding before issuing
448
+ * the request. Unauthenticated; same surface area as {@link getRegistryPlugins}.
449
+ *
450
+ * Versions are returned newest first, suitable for direct rendering in a picker.
451
+ */
452
+ public async getRegistryPluginVersions(
453
+ ctx: Context,
454
+ repo: string,
455
+ args?: EdgeHttpCallArgs,
456
+ ): Promise<GetPluginVersionsResponseBody> {
457
+ return this._call(ctx, new URL(`/registry/plugins/${encodeURIComponent(repo)}/versions`, this.baseUrl), {
458
+ ...args,
459
+ method: 'GET',
460
+ });
461
+ }
462
+
463
+ //
464
+ // Import/Export space.
465
+ //
466
+
467
+ public async importBundle(
468
+ ctx: Context,
469
+ spaceId: SpaceId,
470
+ body: ImportBundleRequest,
471
+ args?: EdgeHttpCallArgs,
472
+ ): Promise<void> {
473
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
474
+ }
475
+
476
+ public async exportBundle(
477
+ ctx: Context,
478
+ spaceId: SpaceId,
479
+ body: ExportBundleRequest,
480
+ args?: EdgeHttpCallArgs,
481
+ ): Promise<ExportBundleResponse> {
482
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
483
+ ...args,
484
+ body,
485
+ method: 'POST',
486
+ });
487
+ }
488
+
489
+ //
490
+ // Integration proxy.
491
+ //
492
+
493
+ /**
494
+ * Fetch through the edge proxy, used by integration plugins (Discord, ...)
495
+ * to call third-party REST APIs that don't set permissive CORS headers.
496
+ *
497
+ * `init.headers.Authorization` (caller-supplied) is preserved by prefixing
498
+ * with `X-Cors-Proxy-Authorization`, since the proxy strips `Authorization`
499
+ * on forwarding to avoid leaking the DXOS presentation upstream — the
500
+ * prefix carries the upstream's bot token / token through.
501
+ *
502
+ * TEMPORARY: routed through the legacy standalone proxy at
503
+ * `cors-proxy.dxos.workers.dev` (open, unauthenticated, path
504
+ * `/<host>/<path>`) so that integration plugins can be tested before the
505
+ * authenticated `/proxy/*` route on the main edge worker ships
506
+ * (https://github.com/dxos/edge/pull/576). When that PR deploys, restore
507
+ * the commented-out block below — it rewrites the target under
508
+ * `${this.baseUrl}/proxy/...` and signs the request with the cached
509
+ * verifiable presentation. The header-remap and `x-cors-proxy-*` override
510
+ * conventions are unchanged between the two paths.
511
+ */
512
+ public async proxyFetch(target: URL, init: RequestInit = {}): Promise<Response> {
513
+ return proxyFetchLegacy(target, init, this._clientTag);
514
+
515
+ //
516
+ // Restore once the authenticated route on the main edge worker is deployed:
517
+ //
518
+ // const proxyUrl = new URL(`/proxy/${target.host}${target.pathname}${target.search}`, this.baseUrl);
519
+ // if (target.protocol === 'http:') {
520
+ // proxyUrl.searchParams.set('scheme', 'http');
521
+ // }
522
+ // const headers = remapAuthorizationForProxy(new Headers(init.headers ?? undefined));
523
+ // let handledAuth = false;
524
+ // while (true) {
525
+ // if (!this._authHeader) {
526
+ // const authResponse = await fetch(new URL('/auth', this.baseUrl));
527
+ // if (authResponse.status === 401) {
528
+ // this._authHeader = await this._handleUnauthorized(authResponse);
529
+ // }
530
+ // }
531
+ // const requestHeaders = new Headers(headers);
532
+ // if (this._authHeader) {
533
+ // requestHeaders.set('Authorization', this._authHeader);
534
+ // }
535
+ // if (this._clientTag) {
536
+ // requestHeaders.set(EDGE_CLIENT_TAG_HEADER, this._clientTag);
537
+ // }
538
+ // const response = await fetch(proxyUrl, { ...init, headers: requestHeaders });
539
+ // if (response.status === 401 && !handledAuth) {
540
+ // this._authHeader = await this._handleUnauthorized(response);
541
+ // handledAuth = true;
542
+ // continue;
543
+ // }
544
+ // return response;
545
+ // }
546
+ }
547
+
270
548
  //
271
549
  // Internal
272
550
  //
273
551
 
274
- private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
275
- return pipe(
552
+ private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
553
+ return Function.pipe(
276
554
  HttpClient.get(url),
277
555
  withLogging,
278
556
  withRetryConfig,
279
557
  Effect.provide(FetchHttpClient.layer),
280
558
  Effect.provide(HttpConfig.default),
281
559
  Effect.withSpan('EdgeHttpClient'),
282
- Effect.runPromise,
560
+ runAndForwardErrors,
283
561
  ) as T;
284
562
  }
285
563
 
286
564
  // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
287
- private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
565
+ private async _call<T>(ctx: Context, url: URL, args: EdgeHttpRequestArgs): Promise<T> {
288
566
  const shouldRetry = createRetryHandler(args);
289
- const requestContext = args.context ?? new Context();
290
567
  log('fetch', { url, request: args.body });
291
568
 
569
+ const traceHeaders = getTraceHeaders(ctx);
570
+
292
571
  let handledAuth = false;
572
+ const tryCount = 1;
293
573
  while (true) {
294
- let processingError: EdgeCallFailedError;
295
- let retryAfterHeaderValue: number = Number.NaN;
574
+ let processingError: EdgeCallFailedError | undefined = undefined;
296
575
  try {
297
- const request = createRequest(args, this._authHeader);
576
+ if (!this._authHeader && args.auth) {
577
+ const response = await fetch(new URL(`/auth`, this.baseUrl));
578
+ if (response.status === 401) {
579
+ this._authHeader = await this._handleUnauthorized(response);
580
+ }
581
+ }
582
+
583
+ const request = createRequest(args, this._authHeader, traceHeaders, this._clientTag);
584
+ log('call edge', { url, tryCount, authHeader: !!this._authHeader });
298
585
  const response = await fetch(url, request);
299
- retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
586
+
300
587
  if (response.ok) {
301
- const body = (await response.json()) as EdgeHttpResponse<T>;
588
+ const body = await response.clone().json();
589
+ invariant(body, 'Expected body to be present');
590
+ if (!('success' in body)) {
591
+ return body;
592
+ }
302
593
  if (body.success) {
303
594
  return body.data;
304
595
  }
305
-
306
- log.warn('unsuccessful edge response', { url, body });
307
- if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
308
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
309
- } else {
310
- processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
311
- }
312
596
  } else if (response.status === 401 && !handledAuth) {
313
597
  this._authHeader = await this._handleUnauthorized(response);
314
598
  handledAuth = true;
315
599
  continue;
600
+ }
601
+
602
+ const body: EdgeFailure =
603
+ response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
604
+
605
+ invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
606
+
607
+ if (body?.data?.type === 'auth_challenge' && typeof body?.data?.challenge === 'string') {
608
+ processingError = new EdgeAuthChallengeError(body.data.challenge, body.data);
609
+ } else if (body?.success === false) {
610
+ processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
316
611
  } else {
317
- processingError = EdgeCallFailedError.fromHttpFailure(response);
612
+ invariant(!response.ok, 'Expected response to not be ok.');
613
+ processingError = await EdgeCallFailedError.fromHttpFailure(response);
318
614
  }
319
615
  } catch (error: any) {
320
616
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
321
617
  }
322
618
 
323
- if (processingError.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
324
- log('retrying edge request', { url, processingError });
619
+ if (processingError?.isRetryable && (await shouldRetry(ctx, processingError.retryAfterMs))) {
620
+ log.verbose('retrying edge request', { url, processingError });
325
621
  } else {
326
- throw processingError;
622
+ throw processingError!;
327
623
  }
328
624
  }
329
625
  }
@@ -331,7 +627,7 @@ export class EdgeHttpClient {
331
627
  private async _handleUnauthorized(response: Response): Promise<string> {
332
628
  if (!this._edgeIdentity) {
333
629
  log.warn('unauthorized response received before identity was set');
334
- throw EdgeCallFailedError.fromHttpFailure(response);
630
+ throw await EdgeCallFailedError.fromHttpFailure(response);
335
631
  }
336
632
 
337
633
  const challenge = await handleAuthChallenge(response, this._edgeIdentity);
@@ -339,14 +635,61 @@ export class EdgeHttpClient {
339
635
  }
340
636
  }
341
637
 
342
- const createRequest = ({ method, body }: EdgeHttpRequestArgs, authHeader: string | undefined): RequestInit => {
638
+ const createRequest = (
639
+ { method, body, json = true }: EdgeHttpRequestArgs,
640
+ authHeader: string | undefined,
641
+ traceHeaders?: Record<string, string>,
642
+ clientTag?: string,
643
+ ): RequestInit => {
644
+ let requestBody: BodyInit | undefined;
645
+ const headers: HeadersInit = {};
646
+
647
+ if (json) {
648
+ requestBody = body && JSON.stringify(body);
649
+ headers['Content-Type'] = 'application/json';
650
+ } else {
651
+ requestBody = body;
652
+ }
653
+
654
+ if (typeof requestBody === 'string' && requestBody.length > WARNING_BODY_SIZE) {
655
+ log.warn('Request with large body', { bodySize: requestBody.length });
656
+ }
657
+
658
+ if (authHeader) {
659
+ headers['Authorization'] = authHeader;
660
+ }
661
+
662
+ if (traceHeaders) {
663
+ Object.assign(headers, traceHeaders);
664
+ }
665
+
666
+ if (clientTag) {
667
+ headers[EDGE_CLIENT_TAG_HEADER] = clientTag;
668
+ }
669
+
343
670
  return {
344
671
  method,
345
- body: body && JSON.stringify(body),
346
- headers: authHeader ? { Authorization: authHeader } : undefined,
672
+ body: requestBody,
673
+ headers,
347
674
  };
348
675
  };
349
676
 
677
+ /**
678
+ * Extract W3C Trace Context headers (traceparent/tracestate) from a DXOS Context.
679
+ */
680
+ const getTraceHeaders = (ctx: Context): Record<string, string> | undefined => {
681
+ const traceCtx = ctx.getAttribute(TRACE_SPAN_ATTRIBUTE) as TraceContextData | undefined;
682
+ if (!traceCtx) {
683
+ return undefined;
684
+ }
685
+
686
+ const headers: Record<string, string> = { traceparent: traceCtx.traceparent };
687
+ if (traceCtx.tracestate) {
688
+ headers.tracestate = traceCtx.tracestate;
689
+ }
690
+ return headers;
691
+ };
692
+
350
693
  /**
351
694
  * @deprecated
352
695
  */
@@ -359,7 +702,7 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
359
702
  const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
360
703
  const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
361
704
  const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
362
- return async (ctx: Context, retryAfter: number) => {
705
+ return async (ctx: Context, retryAfter?: number) => {
363
706
  if (++retries > maxRetries || ctx.disposed) {
364
707
  return false;
365
708
  }
@@ -374,3 +717,52 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
374
717
  return true;
375
718
  };
376
719
  };
720
+
721
+ const getFileMimeType = (filename: string) =>
722
+ ['.js', '.mjs'].some((codeExtension) => filename.endsWith(codeExtension))
723
+ ? 'application/javascript+module'
724
+ : filename.endsWith('.wasm')
725
+ ? 'application/wasm'
726
+ : 'application/octet-stream';
727
+
728
+ /**
729
+ * Move any caller-supplied `Authorization` header to `X-Cors-Proxy-Authorization`
730
+ * so it survives the proxy hop. The edge proxy strips the top-level
731
+ * `Authorization` (it carries the DXOS presentation, never to be leaked
732
+ * upstream) and applies any `x-cors-proxy-*` override prefix as the actual
733
+ * upstream header — which is exactly the channel we want for forwarding bot
734
+ * tokens, OAuth tokens, etc.
735
+ */
736
+ const remapAuthorizationForProxy = (headers: Headers): Headers => {
737
+ const callerAuth = headers.get('Authorization');
738
+ if (callerAuth !== null) {
739
+ headers.delete('Authorization');
740
+ headers.set('X-Cors-Proxy-Authorization', callerAuth);
741
+ }
742
+ return headers;
743
+ };
744
+
745
+ /**
746
+ * Fetch through the legacy standalone open proxy at `cors-proxy.dxos.workers.dev`.
747
+ *
748
+ * No DXOS auth, no `EdgeHttpClient` instance required — pure URL rewrite +
749
+ * header remap + `fetch`. Used by integration plugins from contexts that
750
+ * don't have an `EdgeHttpClient` in scope (e.g. plugin-integration's
751
+ * `credentialForm.onSubmit` and `onTokenCreated`, which run inside the
752
+ * coordinator's runtime that does not provide `Capability.Service`).
753
+ *
754
+ * TEMPORARY — see `LEGACY_CORS_PROXY_URL`. When the authenticated `/proxy/*`
755
+ * route on edge ships (https://github.com/dxos/edge/pull/576), delete this
756
+ * function and route everything through `EdgeHttpClient.proxyFetch` again.
757
+ */
758
+ export const proxyFetchLegacy = (target: URL, init: RequestInit = {}, clientTag?: string): Promise<Response> => {
759
+ const proxyUrl = new URL(`/${target.host}${target.pathname}${target.search}`, LEGACY_CORS_PROXY_URL);
760
+ if (target.protocol === 'http:') {
761
+ proxyUrl.searchParams.set('scheme', 'http');
762
+ }
763
+ const requestHeaders = remapAuthorizationForProxy(new Headers(init.headers ?? undefined));
764
+ if (clientTag) {
765
+ requestHeaders.set(EDGE_CLIENT_TAG_HEADER, clientTag);
766
+ }
767
+ return fetch(proxyUrl, { ...init, headers: requestHeaders });
768
+ };