@dxos/edge-client 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc

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 (51) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/{browser/chunk-VESGVCLQ.mjs → neutral/chunk-ZIQ5T3A7.mjs} +6 -40
  3. package/dist/lib/{browser/chunk-VESGVCLQ.mjs.map → neutral/chunk-ZIQ5T3A7.mjs.map} +2 -2
  4. package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
  5. package/dist/lib/{browser → neutral}/index.mjs +208 -360
  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 +5 -2
  12. package/dist/types/src/edge-client.d.ts.map +1 -1
  13. package/dist/types/src/edge-http-client.d.ts +69 -31
  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 +1 -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 +2 -2
  21. package/dist/types/src/http-client.d.ts.map +1 -1
  22. package/dist/types/src/protocol.d.ts +1 -1
  23. package/dist/types/src/protocol.d.ts.map +1 -1
  24. package/dist/types/src/testing/test-server.d.ts.map +1 -1
  25. package/dist/types/src/testing/test-utils.d.ts +2 -2
  26. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  27. package/dist/types/src/utils.d.ts +1 -1
  28. package/dist/types/src/utils.d.ts.map +1 -1
  29. package/dist/types/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +29 -32
  31. package/src/edge-client.test.ts +16 -11
  32. package/src/edge-client.ts +19 -3
  33. package/src/edge-http-client.test.ts +3 -2
  34. package/src/edge-http-client.ts +210 -66
  35. package/src/edge-ws-connection.ts +2 -1
  36. package/src/http-client.test.ts +3 -2
  37. package/src/http-client.ts +5 -1
  38. package/src/testing/test-utils.ts +4 -4
  39. package/dist/lib/browser/index.mjs.map +0 -7
  40. package/dist/lib/browser/meta.json +0 -1
  41. package/dist/lib/browser/testing/index.mjs.map +0 -7
  42. package/dist/lib/node-esm/chunk-JTBFRYNM.mjs +0 -303
  43. package/dist/lib/node-esm/chunk-JTBFRYNM.mjs.map +0 -7
  44. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  45. package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
  46. package/dist/lib/node-esm/index.mjs +0 -1342
  47. package/dist/lib/node-esm/index.mjs.map +0 -7
  48. package/dist/lib/node-esm/meta.json +0 -1
  49. package/dist/lib/node-esm/testing/index.mjs +0 -186
  50. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  51. /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -8,7 +8,8 @@ import * as Effect from 'effect/Effect';
8
8
  import * as Function from 'effect/Function';
9
9
 
10
10
  import { sleep } from '@dxos/async';
11
- import { Context } from '@dxos/context';
11
+ import { Context, TRACE_SPAN_ATTRIBUTE, type TraceContextData } from '@dxos/context';
12
+ import { runAndForwardErrors } from '@dxos/effect';
12
13
  import { invariant } from '@dxos/invariant';
13
14
  import { type PublicKey, type SpaceId } from '@dxos/keys';
14
15
  import { log } from '@dxos/log';
@@ -17,14 +18,18 @@ import {
17
18
  type CreateAgentResponseBody,
18
19
  type CreateSpaceRequest,
19
20
  type CreateSpaceResponseBody,
21
+ EDGE_CLIENT_TAG_HEADER,
20
22
  EdgeAuthChallengeError,
21
- type EdgeBody,
22
23
  EdgeCallFailedError,
24
+ type EdgeFailure,
23
25
  type EdgeStatus,
24
26
  type ExecuteWorkflowResponseBody,
25
27
  type ExportBundleRequest,
26
28
  type ExportBundleResponse,
29
+ type FeedProtocol,
27
30
  type GetAgentStatusResponseBody,
31
+ type GetPluginVersionsResponseBody,
32
+ type GetPluginsResponseBody,
28
33
  type GetNotarizationResponseBody,
29
34
  type ImportBundleRequest,
30
35
  type InitiateOAuthFlowRequest,
@@ -33,19 +38,32 @@ import {
33
38
  type JoinSpaceResponseBody,
34
39
  type ObjectId,
35
40
  type PostNotarizationRequestBody,
36
- type QueryResult,
37
- type QueueQuery,
38
41
  type RecoverIdentityRequest,
39
42
  type RecoverIdentityResponseBody,
40
43
  type UploadFunctionRequest,
41
44
  type UploadFunctionResponseBody,
42
45
  } from '@dxos/protocols';
46
+ import {
47
+ type QueryRequest as QueryRequestProto,
48
+ type QueryResponse as QueryResponseProto,
49
+ } from '@dxos/protocols/proto/dxos/echo/query';
43
50
  import { createUrl } from '@dxos/util';
44
51
 
45
52
  import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
46
53
  import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
47
54
  import { getEdgeUrlWithProtocol } from './utils';
48
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
+
49
67
  const DEFAULT_RETRY_TIMEOUT = 1500;
50
68
  const DEFAULT_RETRY_JITTER = 500;
51
69
  const DEFAULT_MAX_RETRIES_COUNT = 3;
@@ -68,7 +86,6 @@ export type RetryConfig = {
68
86
 
69
87
  type EdgeHttpRequestArgs = {
70
88
  method: string;
71
- context?: Context;
72
89
  retry?: RetryConfig;
73
90
  body?: any;
74
91
  /**
@@ -77,17 +94,30 @@ type EdgeHttpRequestArgs = {
77
94
  json?: boolean;
78
95
 
79
96
  /**
80
- * Do not expect a standard EDGE JSON response with a `success` field.
81
- * @deprecated Use only for debugging.
97
+ * Force authentication.
98
+ * This should be used for requests with large bodies to avoid sending the body twice.
99
+ * The client will call /auth endpoint to generate the auth header.
82
100
  */
83
- rawResponse?: boolean;
101
+ auth?: boolean;
84
102
  };
85
103
 
86
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
87
- export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body'>;
104
+ export type EdgeHttpCallArgs = Pick<EdgeHttpRequestArgs, 'retry' | 'auth'>;
105
+
106
+ export type GetCronTriggersResponse = {
107
+ cronIds: string[];
108
+ };
109
+
110
+ export type EdgeHttpClientOptions = {
111
+ /**
112
+ * Tag included in the {@link EDGE_CLIENT_TAG_HEADER} header on every request.
113
+ * Used on Edge to classify traffic for metering (e.g. `ci-e2e`).
114
+ */
115
+ clientTag?: string;
116
+ };
88
117
 
89
118
  export class EdgeHttpClient {
90
119
  private readonly _baseUrl: string;
120
+ private readonly _clientTag: string | undefined;
91
121
 
92
122
  private _edgeIdentity: EdgeIdentity | undefined;
93
123
 
@@ -96,8 +126,9 @@ export class EdgeHttpClient {
96
126
  */
97
127
  private _authHeader: string | undefined;
98
128
 
99
- constructor(baseUrl: string) {
129
+ constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
100
130
  this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
131
+ this._clientTag = options?.clientTag;
101
132
  log('created', { url: this._baseUrl });
102
133
  }
103
134
 
@@ -116,23 +147,28 @@ export class EdgeHttpClient {
116
147
  // Status
117
148
  //
118
149
 
119
- public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
120
- return this._call(new URL('/status', this.baseUrl), { ...args, method: 'GET' });
150
+ public async getStatus(ctx: Context, args?: EdgeHttpCallArgs): Promise<EdgeStatus> {
151
+ return this._call(ctx, new URL('/status', this.baseUrl), { ...args, method: 'GET', auth: true });
121
152
  }
122
153
 
123
154
  //
124
155
  // Agents
125
156
  //
126
157
 
127
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
128
- return this._call(new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
158
+ public createAgent(
159
+ ctx: Context,
160
+ body: CreateAgentRequestBody,
161
+ args?: EdgeHttpCallArgs,
162
+ ): Promise<CreateAgentResponseBody> {
163
+ return this._call(ctx, new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
129
164
  }
130
165
 
131
166
  public getAgentStatus(
167
+ ctx: Context,
132
168
  request: { ownerIdentityKey: PublicKey },
133
- args?: EdgeHttpGetArgs,
169
+ args?: EdgeHttpCallArgs,
134
170
  ): Promise<GetAgentStatusResponseBody> {
135
- return this._call(new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
171
+ return this._call(ctx, new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
136
172
  ...args,
137
173
  method: 'GET',
138
174
  });
@@ -142,16 +178,21 @@ export class EdgeHttpClient {
142
178
  // Credentials
143
179
  //
144
180
 
145
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
146
- return this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
181
+ public getCredentialsForNotarization(
182
+ ctx: Context,
183
+ spaceId: SpaceId,
184
+ args?: EdgeHttpCallArgs,
185
+ ): Promise<GetNotarizationResponseBody> {
186
+ return this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
147
187
  }
148
188
 
149
189
  public async notarizeCredentials(
190
+ ctx: Context,
150
191
  spaceId: SpaceId,
151
192
  body: PostNotarizationRequestBody,
152
- args?: EdgeHttpGetArgs,
193
+ args?: EdgeHttpCallArgs,
153
194
  ): Promise<void> {
154
- await this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
195
+ await this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
155
196
  }
156
197
 
157
198
  //
@@ -159,10 +200,11 @@ export class EdgeHttpClient {
159
200
  //
160
201
 
161
202
  public async recoverIdentity(
203
+ ctx: Context,
162
204
  body: RecoverIdentityRequest,
163
- args?: EdgeHttpGetArgs,
205
+ args?: EdgeHttpCallArgs,
164
206
  ): Promise<RecoverIdentityResponseBody> {
165
- return this._call(new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
207
+ return this._call(ctx, new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
166
208
  }
167
209
 
168
210
  //
@@ -170,11 +212,12 @@ export class EdgeHttpClient {
170
212
  //
171
213
 
172
214
  public async joinSpaceByInvitation(
215
+ ctx: Context,
173
216
  spaceId: SpaceId,
174
217
  body: JoinSpaceRequest,
175
- args?: EdgeHttpGetArgs,
218
+ args?: EdgeHttpCallArgs,
176
219
  ): Promise<JoinSpaceResponseBody> {
177
- return this._call(new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
220
+ return this._call(ctx, new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
178
221
  }
179
222
 
180
223
  //
@@ -182,18 +225,19 @@ export class EdgeHttpClient {
182
225
  //
183
226
 
184
227
  public async initiateOAuthFlow(
228
+ ctx: Context,
185
229
  body: InitiateOAuthFlowRequest,
186
- args?: EdgeHttpGetArgs,
230
+ args?: EdgeHttpCallArgs,
187
231
  ): Promise<InitiateOAuthFlowResponse> {
188
- return this._call(new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
232
+ return this._call(ctx, new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
189
233
  }
190
234
 
191
235
  //
192
236
  // Spaces
193
237
  //
194
238
 
195
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
196
- return this._call(new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
239
+ async createSpace(ctx: Context, body: CreateSpaceRequest, args?: EdgeHttpCallArgs): Promise<CreateSpaceResponseBody> {
240
+ return this._call(ctx, new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
197
241
  }
198
242
 
199
243
  //
@@ -201,13 +245,16 @@ export class EdgeHttpClient {
201
245
  //
202
246
 
203
247
  public async queryQueue(
248
+ ctx: Context,
204
249
  subspaceTag: string,
205
250
  spaceId: SpaceId,
206
- query: QueueQuery,
207
- args?: EdgeHttpGetArgs,
208
- ): Promise<QueryResult> {
209
- const { queueId } = query;
251
+ query: FeedProtocol.QueueQuery,
252
+ args?: EdgeHttpCallArgs,
253
+ ): Promise<EdgeQueryQueueResponse> {
254
+ const queueId = query.queueIds?.[0];
255
+ invariant(queueId, 'queueId required');
210
256
  return this._call(
257
+ ctx,
211
258
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
212
259
  after: query.after,
213
260
  before: query.before,
@@ -223,13 +270,14 @@ export class EdgeHttpClient {
223
270
  }
224
271
 
225
272
  public async insertIntoQueue(
273
+ ctx: Context,
226
274
  subspaceTag: string,
227
275
  spaceId: SpaceId,
228
276
  queueId: ObjectId,
229
277
  objects: unknown[],
230
- args?: EdgeHttpGetArgs,
278
+ args?: EdgeHttpCallArgs,
231
279
  ): Promise<void> {
232
- return this._call(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
280
+ return this._call(ctx, new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
233
281
  ...args,
234
282
  body: { objects },
235
283
  method: 'POST',
@@ -237,13 +285,15 @@ export class EdgeHttpClient {
237
285
  }
238
286
 
239
287
  public async deleteFromQueue(
288
+ ctx: Context,
240
289
  subspaceTag: string,
241
290
  spaceId: SpaceId,
242
291
  queueId: ObjectId,
243
292
  objectIds: ObjectId[],
244
- args?: EdgeHttpGetArgs,
293
+ args?: EdgeHttpCallArgs,
245
294
  ): Promise<void> {
246
295
  return this._call(
296
+ ctx,
247
297
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
248
298
  ids: objectIds.join(','),
249
299
  }),
@@ -259,15 +309,17 @@ export class EdgeHttpClient {
259
309
  //
260
310
 
261
311
  public async uploadFunction(
312
+ ctx: Context,
262
313
  pathParts: { functionId?: string },
263
314
  body: UploadFunctionRequest,
264
- args?: EdgeHttpGetArgs,
315
+ args?: EdgeHttpCallArgs,
265
316
  ): Promise<UploadFunctionResponseBody> {
266
317
  const formData = new FormData();
267
318
  formData.append('name', body.name ?? '');
268
319
  formData.append('version', body.version);
269
320
  formData.append('ownerPublicKey', body.ownerPublicKey);
270
321
  formData.append('entryPoint', body.entryPoint);
322
+ body.runtime && formData.append('runtime', body.runtime);
271
323
  for (const [filename, content] of Object.entries(body.assets)) {
272
324
  formData.append(
273
325
  'assets',
@@ -277,7 +329,7 @@ export class EdgeHttpClient {
277
329
  }
278
330
 
279
331
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
280
- return this._call(new URL(path, this.baseUrl), {
332
+ return this._call(ctx, new URL(path, this.baseUrl), {
281
333
  ...args,
282
334
  body: formData,
283
335
  method: 'PUT',
@@ -285,11 +337,12 @@ export class EdgeHttpClient {
285
337
  });
286
338
  }
287
339
 
288
- public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
289
- return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
340
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
341
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
290
342
  }
291
343
 
292
344
  public async invokeFunction(
345
+ ctx: Context,
293
346
  params: {
294
347
  functionId: string;
295
348
  version?: string;
@@ -298,7 +351,7 @@ export class EdgeHttpClient {
298
351
  subrequestsLimit?: number;
299
352
  },
300
353
  input: unknown,
301
- args?: EdgeHttpGetArgs,
354
+ args?: EdgeHttpCallArgs,
302
355
  ): Promise<any> {
303
356
  const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
304
357
  if (params.version) {
@@ -314,11 +367,10 @@ export class EdgeHttpClient {
314
367
  url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
315
368
  }
316
369
 
317
- return this._call(url, {
370
+ return this._call(ctx, url, {
318
371
  ...args,
319
372
  body: input,
320
373
  method: 'POST',
321
- rawResponse: true,
322
374
  });
323
375
  }
324
376
 
@@ -327,12 +379,13 @@ export class EdgeHttpClient {
327
379
  //
328
380
 
329
381
  public async executeWorkflow(
382
+ ctx: Context,
330
383
  spaceId: SpaceId,
331
384
  graphId: ObjectId,
332
385
  input: any,
333
- args?: EdgeHttpGetArgs,
386
+ args?: EdgeHttpCallArgs,
334
387
  ): Promise<ExecuteWorkflowResponseBody> {
335
- return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
388
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
336
389
  ...args,
337
390
  body: input,
338
391
  method: 'POST',
@@ -343,8 +396,62 @@ export class EdgeHttpClient {
343
396
  // Triggers
344
397
  //
345
398
 
346
- public async getCronTriggers(spaceId: SpaceId) {
347
- return this._call(new URL(`/test/functions/${spaceId}/triggers/crons`, this.baseUrl), { method: 'GET' });
399
+ public async getCronTriggers(ctx: Context, spaceId: SpaceId): Promise<GetCronTriggersResponse> {
400
+ return this._call<GetCronTriggersResponse>(ctx, new URL(`/functions/${spaceId}/triggers/crons`, this.baseUrl), {
401
+ method: 'GET',
402
+ });
403
+ }
404
+
405
+ public async forceRunCronTrigger(ctx: Context, spaceId: SpaceId, triggerId: ObjectId) {
406
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons/${triggerId}/run`, this.baseUrl), {
407
+ method: 'POST',
408
+ });
409
+ }
410
+
411
+ //
412
+ // Query
413
+ //
414
+
415
+ /**
416
+ * Execute a QueryAST query against a space.
417
+ */
418
+ public async execQuery(
419
+ ctx: Context,
420
+ spaceId: SpaceId,
421
+ body: QueryRequestProto,
422
+ args?: EdgeHttpCallArgs,
423
+ ): Promise<QueryResponseProto> {
424
+ return this._call(ctx, new URL(`/spaces/${spaceId}/exec-query`, this.baseUrl), { ...args, body, method: 'POST' });
425
+ }
426
+
427
+ //
428
+ // Registry
429
+ //
430
+
431
+ /**
432
+ * Fetches the hydrated plugin directory from the Edge registry service.
433
+ * Unauthenticated; safe to call without an identity.
434
+ */
435
+ public async getRegistryPlugins(ctx: Context, args?: EdgeHttpCallArgs): Promise<GetPluginsResponseBody> {
436
+ return this._call(ctx, new URL('/registry/plugins', this.baseUrl), { ...args, method: 'GET' });
437
+ }
438
+
439
+ /**
440
+ * Fetches the available release versions for a given plugin repo. `repo` is the
441
+ * GitHub `owner/name` form; this method takes care of URL-encoding before issuing
442
+ * the request. Unauthenticated; same surface area as {@link getRegistryPlugins}.
443
+ *
444
+ * Versions are returned newest first, suitable for direct rendering in a picker.
445
+ */
446
+ public async getRegistryPluginVersions(
447
+ ctx: Context,
448
+ repo: string,
449
+ args?: EdgeHttpCallArgs,
450
+ ): Promise<GetPluginVersionsResponseBody> {
451
+ return this._call(ctx, new URL(`/registry/plugins/${encodeURIComponent(repo)}/versions`, this.baseUrl), {
452
+ ...args,
453
+ method: 'GET',
454
+ });
348
455
  }
349
456
 
350
457
  //
@@ -352,19 +459,21 @@ export class EdgeHttpClient {
352
459
  //
353
460
 
354
461
  public async importBundle(
355
- spaceId: SpaceId, //
462
+ ctx: Context,
463
+ spaceId: SpaceId,
356
464
  body: ImportBundleRequest,
357
- args?: EdgeHttpGetArgs,
465
+ args?: EdgeHttpCallArgs,
358
466
  ): Promise<void> {
359
- return this._call(new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
467
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
360
468
  }
361
469
 
362
470
  public async exportBundle(
471
+ ctx: Context,
363
472
  spaceId: SpaceId,
364
473
  body: ExportBundleRequest,
365
- args?: EdgeHttpGetArgs,
474
+ args?: EdgeHttpCallArgs,
366
475
  ): Promise<ExportBundleResponse> {
367
- return this._call(new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
476
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
368
477
  ...args,
369
478
  body,
370
479
  method: 'POST',
@@ -375,7 +484,7 @@ export class EdgeHttpClient {
375
484
  // Internal
376
485
  //
377
486
 
378
- private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
487
+ private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
379
488
  return Function.pipe(
380
489
  HttpClient.get(url),
381
490
  withLogging,
@@ -383,29 +492,35 @@ export class EdgeHttpClient {
383
492
  Effect.provide(FetchHttpClient.layer),
384
493
  Effect.provide(HttpConfig.default),
385
494
  Effect.withSpan('EdgeHttpClient'),
386
- Effect.runPromise,
495
+ runAndForwardErrors,
387
496
  ) as T;
388
497
  }
389
498
 
390
499
  // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
391
- private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
500
+ private async _call<T>(ctx: Context, url: URL, args: EdgeHttpRequestArgs): Promise<T> {
392
501
  const shouldRetry = createRetryHandler(args);
393
- const requestContext = args.context ?? Context.default();
394
502
  log('fetch', { url, request: args.body });
395
503
 
504
+ const traceHeaders = getTraceHeaders(ctx);
505
+
396
506
  let handledAuth = false;
507
+ const tryCount = 1;
397
508
  while (true) {
398
509
  let processingError: EdgeCallFailedError | undefined = undefined;
399
510
  try {
400
- const request = createRequest(args, this._authHeader);
511
+ if (!this._authHeader && args.auth) {
512
+ const response = await fetch(new URL(`/auth`, this.baseUrl));
513
+ if (response.status === 401) {
514
+ this._authHeader = await this._handleUnauthorized(response);
515
+ }
516
+ }
517
+
518
+ const request = createRequest(args, this._authHeader, traceHeaders, this._clientTag);
519
+ log('call edge', { url, tryCount, authHeader: !!this._authHeader });
401
520
  const response = await fetch(url, request);
402
- const body: EdgeBody<T> | undefined =
403
- response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
404
521
 
405
522
  if (response.ok) {
406
- if (args.rawResponse) {
407
- return body as any;
408
- }
523
+ const body = await response.clone().json();
409
524
  invariant(body, 'Expected body to be present');
410
525
  if (!('success' in body)) {
411
526
  return body;
@@ -419,10 +534,13 @@ export class EdgeHttpClient {
419
534
  continue;
420
535
  }
421
536
 
537
+ const body: EdgeFailure =
538
+ response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
539
+
422
540
  invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
423
541
 
424
- if (body?.errorData?.type === 'auth_challenge' && typeof body?.errorData?.challenge === 'string') {
425
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
542
+ if (body?.data?.type === 'auth_challenge' && typeof body?.data?.challenge === 'string') {
543
+ processingError = new EdgeAuthChallengeError(body.data.challenge, body.data);
426
544
  } else if (body?.success === false) {
427
545
  processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
428
546
  } else {
@@ -433,8 +551,8 @@ export class EdgeHttpClient {
433
551
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
434
552
  }
435
553
 
436
- if (processingError?.isRetryable && (await shouldRetry(requestContext, processingError.retryAfterMs))) {
437
- log('retrying edge request', { url, processingError });
554
+ if (processingError?.isRetryable && (await shouldRetry(ctx, processingError.retryAfterMs))) {
555
+ log.verbose('retrying edge request', { url, processingError });
438
556
  } else {
439
557
  throw processingError!;
440
558
  }
@@ -455,6 +573,8 @@ export class EdgeHttpClient {
455
573
  const createRequest = (
456
574
  { method, body, json = true }: EdgeHttpRequestArgs,
457
575
  authHeader: string | undefined,
576
+ traceHeaders?: Record<string, string>,
577
+ clientTag?: string,
458
578
  ): RequestInit => {
459
579
  let requestBody: BodyInit | undefined;
460
580
  const headers: HeadersInit = {};
@@ -474,6 +594,14 @@ const createRequest = (
474
594
  headers['Authorization'] = authHeader;
475
595
  }
476
596
 
597
+ if (traceHeaders) {
598
+ Object.assign(headers, traceHeaders);
599
+ }
600
+
601
+ if (clientTag) {
602
+ headers[EDGE_CLIENT_TAG_HEADER] = clientTag;
603
+ }
604
+
477
605
  return {
478
606
  method,
479
607
  body: requestBody,
@@ -481,6 +609,22 @@ const createRequest = (
481
609
  };
482
610
  };
483
611
 
612
+ /**
613
+ * Extract W3C Trace Context headers (traceparent/tracestate) from a DXOS Context.
614
+ */
615
+ const getTraceHeaders = (ctx: Context): Record<string, string> | undefined => {
616
+ const traceCtx = ctx.getAttribute(TRACE_SPAN_ATTRIBUTE) as TraceContextData | undefined;
617
+ if (!traceCtx) {
618
+ return undefined;
619
+ }
620
+
621
+ const headers: Record<string, string> = { traceparent: traceCtx.traceparent };
622
+ if (traceCtx.tracestate) {
623
+ headers.tracestate = traceCtx.tracestate;
624
+ }
625
+ return headers;
626
+ };
627
+
484
628
  /**
485
629
  * @deprecated
486
630
  */
@@ -50,7 +50,7 @@ export class EdgeWsConnection extends Resource {
50
50
 
51
51
  constructor(
52
52
  private readonly _identity: EdgeIdentity,
53
- private readonly _connectionInfo: { url: URL; protocolHeader?: string },
53
+ private readonly _connectionInfo: { url: URL; protocolHeader?: string; headers?: Record<string, string> },
54
54
  private readonly _callbacks: EdgeWsConnectionCallbacks,
55
55
  ) {
56
56
  super();
@@ -121,6 +121,7 @@ export class EdgeWsConnection extends Resource {
121
121
  this._connectionInfo.protocolHeader
122
122
  ? [...baseProtocols, this._connectionInfo.protocolHeader]
123
123
  : [...baseProtocols],
124
+ this._connectionInfo.headers ? { headers: this._connectionInfo.headers } : undefined,
124
125
  );
125
126
  const muxer = new WebSocketMuxer(this._ws);
126
127
  this._wsMuxer = muxer;
@@ -8,6 +8,7 @@ import * as Effect from 'effect/Effect';
8
8
  import * as Function from 'effect/Function';
9
9
  import { afterEach, beforeEach, describe, it } from 'vitest';
10
10
 
11
+ import { runAndForwardErrors } from '@dxos/effect';
11
12
  import { invariant } from '@dxos/invariant';
12
13
 
13
14
  import { HttpConfig, withLogging, withRetry, withRetryConfig } from './http-client';
@@ -36,7 +37,7 @@ describe('HttpClient', () => {
36
37
  withRetry(HttpClient.get(server.url)),
37
38
  Effect.provide(FetchHttpClient.layer),
38
39
  Effect.withSpan('EdgeHttpClient'),
39
- Effect.runPromise,
40
+ runAndForwardErrors,
40
41
  );
41
42
  expect(result).toMatchObject({ success: true, data: { value: 100 } });
42
43
  }
@@ -49,7 +50,7 @@ describe('HttpClient', () => {
49
50
  Effect.provide(FetchHttpClient.layer),
50
51
  Effect.provide(HttpConfig.default), // TODO(burdon): Swap out to mock.
51
52
  Effect.withSpan('EdgeHttpClient'), // TODO(burdon): OTEL.
52
- Effect.runPromise,
53
+ runAndForwardErrors,
53
54
  );
54
55
  expect(result).toMatchObject({ success: true, data: { value: 100 } });
55
56
  }
@@ -61,7 +61,11 @@ export const withRetryConfig = (
61
61
  });
62
62
 
63
63
  export const withLogging = <A extends HttpClientResponse.HttpClientResponse, E, R>(effect: Effect.Effect<A, E, R>) =>
64
- effect.pipe(Effect.tap((res) => log.info('response', { status: res.status })));
64
+ effect.pipe(
65
+ Effect.tap((res) => {
66
+ log.info('response', { status: res.status });
67
+ }),
68
+ );
65
69
 
66
70
  /**
67
71
  *
@@ -16,13 +16,13 @@ import { toUint8Array } from '../protocol';
16
16
 
17
17
  export const DEFAULT_PORT = 8080;
18
18
 
19
- type TestEdgeWsServerParams = {
19
+ type TestEdgeWsServerProps = {
20
20
  admitConnection?: Trigger;
21
21
  payloadDecoder?: (payload: Uint8Array) => any;
22
22
  messageHandler?: (payload: any) => Promise<Uint8Array | undefined>;
23
23
  };
24
24
 
25
- export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestEdgeWsServerParams) => {
25
+ export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestEdgeWsServerProps) => {
26
26
  const wsServer = new WebSocket.Server({
27
27
  port,
28
28
  verifyClient: createConnectionDelayHandler(params),
@@ -86,7 +86,7 @@ export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestE
86
86
  };
87
87
  };
88
88
 
89
- const createConnectionDelayHandler = (params: TestEdgeWsServerParams | undefined) => {
89
+ const createConnectionDelayHandler = (params: TestEdgeWsServerProps | undefined) => {
90
90
  return (_: any, callback: (admit: boolean) => void) => {
91
91
  if (params?.admitConnection) {
92
92
  log('delaying edge connection admission');
@@ -116,7 +116,7 @@ const createResponseSender = (connection: () => WebSocketMuxer) => {
116
116
  };
117
117
  };
118
118
 
119
- const decodePayload = async (request: Message, params: TestEdgeWsServerParams | undefined) => {
119
+ const decodePayload = async (request: Message, params: TestEdgeWsServerProps | undefined) => {
120
120
  const requestPayload = params?.payloadDecoder
121
121
  ? params.payloadDecoder(request.payload!.value!)
122
122
  : protocol.getPayload(request, TextMessageSchema);