@dxos/edge-client 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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 (72) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/{node-esm/chunk-R6K4IIBW.mjs → neutral/chunk-L5ZHLJ4B.mjs} +57 -53
  3. package/dist/lib/neutral/chunk-L5ZHLJ4B.mjs.map +7 -0
  4. package/dist/lib/neutral/chunk-WQKMEZJR.mjs +30 -0
  5. package/dist/lib/neutral/chunk-WQKMEZJR.mjs.map +7 -0
  6. package/dist/lib/neutral/cors-proxy.mjs +7 -0
  7. package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
  8. package/dist/lib/neutral/index.mjs +1448 -0
  9. package/dist/lib/neutral/index.mjs.map +7 -0
  10. package/dist/lib/neutral/meta.json +1 -0
  11. package/dist/lib/{browser → neutral}/testing/index.mjs +6 -31
  12. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  13. package/dist/types/src/auth.d.ts.map +1 -1
  14. package/dist/types/src/base-http-client.d.ts +48 -0
  15. package/dist/types/src/base-http-client.d.ts.map +1 -0
  16. package/dist/types/src/cors-proxy.d.ts +6 -0
  17. package/dist/types/src/cors-proxy.d.ts.map +1 -0
  18. package/dist/types/src/edge-ai-http-client.d.ts +65 -0
  19. package/dist/types/src/edge-ai-http-client.d.ts.map +1 -0
  20. package/dist/types/src/edge-client.d.ts +17 -14
  21. package/dist/types/src/edge-client.d.ts.map +1 -1
  22. package/dist/types/src/edge-http-client.d.ts +81 -54
  23. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  24. package/dist/types/src/edge-identity.d.ts.map +1 -1
  25. package/dist/types/src/edge-ws-connection.d.ts +20 -0
  26. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  27. package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
  28. package/dist/types/src/errors.d.ts.map +1 -1
  29. package/dist/types/src/http-client.d.ts +10 -7
  30. package/dist/types/src/http-client.d.ts.map +1 -1
  31. package/dist/types/src/hub-http-client.d.ts +39 -0
  32. package/dist/types/src/hub-http-client.d.ts.map +1 -0
  33. package/dist/types/src/index.d.ts +8 -3
  34. package/dist/types/src/index.d.ts.map +1 -1
  35. package/dist/types/src/protocol.d.ts +1 -1
  36. package/dist/types/src/protocol.d.ts.map +1 -1
  37. package/dist/types/src/testing/test-server.d.ts.map +1 -1
  38. package/dist/types/src/testing/test-utils.d.ts +3 -3
  39. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  40. package/dist/types/src/utils.d.ts +1 -1
  41. package/dist/types/src/utils.d.ts.map +1 -1
  42. package/dist/types/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +33 -32
  44. package/src/base-http-client.ts +243 -0
  45. package/src/cors-proxy.ts +38 -0
  46. package/src/edge-ai-http-client.ts +129 -0
  47. package/src/edge-client.test.ts +20 -15
  48. package/src/edge-client.ts +105 -44
  49. package/src/edge-http-client.test.ts +37 -3
  50. package/src/edge-http-client.ts +312 -190
  51. package/src/edge-ws-connection.ts +120 -6
  52. package/src/edge-ws-muxer.ts +49 -5
  53. package/src/http-client.test.ts +11 -7
  54. package/src/http-client.ts +18 -8
  55. package/src/hub-http-client.ts +118 -0
  56. package/src/index.ts +8 -3
  57. package/src/testing/test-utils.ts +7 -7
  58. package/dist/lib/browser/chunk-SUXH7FH6.mjs +0 -304
  59. package/dist/lib/browser/chunk-SUXH7FH6.mjs.map +0 -7
  60. package/dist/lib/browser/index.mjs +0 -1124
  61. package/dist/lib/browser/index.mjs.map +0 -7
  62. package/dist/lib/browser/meta.json +0 -1
  63. package/dist/lib/browser/testing/index.mjs.map +0 -7
  64. package/dist/lib/node-esm/chunk-R6K4IIBW.mjs.map +0 -7
  65. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  66. package/dist/lib/node-esm/index.mjs +0 -1125
  67. package/dist/lib/node-esm/index.mjs.map +0 -7
  68. package/dist/lib/node-esm/meta.json +0 -1
  69. package/dist/lib/node-esm/testing/index.mjs +0 -186
  70. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  71. /package/dist/lib/{browser/edge-ws-muxer.mjs.map → neutral/cors-proxy.mjs.map} +0 -0
  72. /package/dist/lib/{node-esm → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -2,120 +2,120 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FetchHttpClient, HttpClient } from '@effect/platform';
6
- import { Effect, pipe } from 'effect';
7
-
8
- import { sleep } from '@dxos/async';
9
- import { Context } from '@dxos/context';
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import * as HttpClient from '@effect/platform/HttpClient';
7
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Function from 'effect/Function';
10
+
11
+ import { type Context } from '@dxos/context';
12
+ import { EffectEx } 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 {
17
+ type CompleteOAuthRegistrationRequest,
18
+ type CompleteOAuthRegistrationResponse,
13
19
  type CreateAgentRequestBody,
14
20
  type CreateAgentResponseBody,
15
21
  type CreateSpaceRequest,
16
22
  type CreateSpaceResponseBody,
17
- EdgeAuthChallengeError,
18
- EdgeCallFailedError,
19
- type EdgeHttpResponse,
23
+ EDGE_CLIENT_TAG_HEADER,
20
24
  type EdgeStatus,
21
25
  type ExecuteWorkflowResponseBody,
26
+ type ExportBundleRequest,
27
+ type ExportBundleResponse,
28
+ type FeedProtocol,
22
29
  type GetAgentStatusResponseBody,
23
30
  type GetNotarizationResponseBody,
31
+ type GetPluginVersionsResponseBody,
32
+ type GetPluginsResponseBody,
33
+ type ImportBundleRequest,
24
34
  type InitiateOAuthFlowRequest,
25
35
  type InitiateOAuthFlowResponse,
26
36
  type JoinSpaceRequest,
27
37
  type JoinSpaceResponseBody,
28
38
  type ObjectId,
29
39
  type PostNotarizationRequestBody,
30
- type QueryResult,
31
- type QueueQuery,
32
40
  type RecoverIdentityRequest,
33
41
  type RecoverIdentityResponseBody,
34
42
  type UploadFunctionRequest,
35
43
  type UploadFunctionResponseBody,
36
44
  } from '@dxos/protocols';
45
+ import {
46
+ type QueryRequest as QueryRequestProto,
47
+ type QueryResponse as QueryResponseProto,
48
+ } from '@dxos/protocols/proto/dxos/echo/query';
37
49
  import { createUrl } from '@dxos/util';
38
50
 
39
- import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
40
- import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
41
- import { getEdgeUrlWithProtocol } from './utils';
51
+ import { BaseHttpClient, type BaseHttpClientOptions, type EdgeHttpCallArgs } from './base-http-client';
52
+ import { proxyFetchLegacy } from './cors-proxy';
53
+ import { HttpConfig, withLogging, withRetryConfig } from './http-client';
42
54
 
43
- const DEFAULT_RETRY_TIMEOUT = 1500;
44
- const DEFAULT_RETRY_JITTER = 500;
45
- const DEFAULT_MAX_RETRIES_COUNT = 3;
55
+ export type { EdgeHttpCallArgs, RetryConfig } from './base-http-client';
46
56
 
47
- export type RetryConfig = {
48
- /**
49
- * A number of call retries, not counting the initial request.
50
- */
51
- count: number;
52
- /**
53
- * Delay before retries in ms.
54
- */
55
- timeout?: number;
56
- /**
57
- * A random amount of time before retrying to help prevent large bursts of requests.
58
- */
59
- jitter?: number;
57
+ /**
58
+ * HTTP wire shape returned by `/queue/.../query`.
59
+ */
60
+ export type EdgeQueryQueueResponse = {
61
+ objects?: unknown[];
62
+ nextCursor?: string;
63
+ prevCursor?: string;
60
64
  };
61
65
 
62
- type EdgeHttpRequestArgs = {
63
- method: string;
64
- context?: Context;
65
- retry?: RetryConfig;
66
- body?: any;
66
+ export type TriggersDispatcherStatus = {
67
+ isActive: boolean;
68
+ nextCronTaskRunTimestamp?: number;
69
+ registeredTriggers: string[];
70
+ stopAfterTimestamp?: number;
71
+ remainingMs?: number;
72
+ nextAlarmTimestamp?: number;
67
73
  };
68
74
 
69
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
70
- export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body'>;
71
-
72
- export class EdgeHttpClient {
73
- private readonly _baseUrl: string;
74
-
75
- private _edgeIdentity: EdgeIdentity | undefined;
76
-
77
- /**
78
- * Auth header is cached until receiving the next 401 from EDGE, at which point it gets refreshed.
79
- */
80
- private _authHeader: string | undefined;
81
-
82
- constructor(baseUrl: string) {
83
- this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
84
- log('created', { url: this._baseUrl });
85
- }
75
+ export type GetCronTriggersResponse = {
76
+ cronIds: string[];
77
+ };
86
78
 
87
- get baseUrl() {
88
- return this._baseUrl;
89
- }
79
+ export type EdgeHttpClientOptions = BaseHttpClientOptions;
90
80
 
91
- setIdentity(identity: EdgeIdentity): void {
92
- if (this._edgeIdentity?.identityKey !== identity.identityKey || this._edgeIdentity?.peerKey !== identity.peerKey) {
93
- this._edgeIdentity = identity;
94
- this._authHeader = undefined;
95
- }
81
+ /**
82
+ * HTTP client for the edge worker API (spaces, queues, functions, agents, etc.).
83
+ *
84
+ * Hub-service API (accounts, invitations) lives in {@link HubHttpClient} — the two
85
+ * services run at different URLs and are never both available from the same base URL.
86
+ */
87
+ export class EdgeHttpClient extends BaseHttpClient {
88
+ constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
89
+ super(baseUrl, options);
90
+ log('created', { url: this.baseUrl });
96
91
  }
97
92
 
98
93
  //
99
94
  // Status
100
95
  //
101
96
 
102
- public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
103
- return this._call(new URL('/status', this.baseUrl), { ...args, method: 'GET' });
97
+ public async getStatus(ctx: Context, args?: EdgeHttpCallArgs): Promise<EdgeStatus> {
98
+ return this._call(ctx, new URL('/status', this.baseUrl), { ...args, method: 'GET', auth: true });
104
99
  }
105
100
 
106
101
  //
107
102
  // Agents
108
103
  //
109
104
 
110
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
111
- return this._call(new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
105
+ public createAgent(
106
+ ctx: Context,
107
+ body: CreateAgentRequestBody,
108
+ args?: EdgeHttpCallArgs,
109
+ ): Promise<CreateAgentResponseBody> {
110
+ return this._call(ctx, new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
112
111
  }
113
112
 
114
113
  public getAgentStatus(
114
+ ctx: Context,
115
115
  request: { ownerIdentityKey: PublicKey },
116
- args?: EdgeHttpGetArgs,
116
+ args?: EdgeHttpCallArgs,
117
117
  ): Promise<GetAgentStatusResponseBody> {
118
- return this._call(new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
118
+ return this._call(ctx, new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
119
119
  ...args,
120
120
  method: 'GET',
121
121
  });
@@ -125,16 +125,21 @@ export class EdgeHttpClient {
125
125
  // Credentials
126
126
  //
127
127
 
128
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
129
- return this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
128
+ public getCredentialsForNotarization(
129
+ ctx: Context,
130
+ spaceId: SpaceId,
131
+ args?: EdgeHttpCallArgs,
132
+ ): Promise<GetNotarizationResponseBody> {
133
+ return this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
130
134
  }
131
135
 
132
136
  public async notarizeCredentials(
137
+ ctx: Context,
133
138
  spaceId: SpaceId,
134
139
  body: PostNotarizationRequestBody,
135
- args?: EdgeHttpGetArgs,
140
+ args?: EdgeHttpCallArgs,
136
141
  ): Promise<void> {
137
- await this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
142
+ await this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
138
143
  }
139
144
 
140
145
  //
@@ -142,45 +147,52 @@ export class EdgeHttpClient {
142
147
  //
143
148
 
144
149
  public async recoverIdentity(
150
+ ctx: Context,
145
151
  body: RecoverIdentityRequest,
146
- args?: EdgeHttpGetArgs,
152
+ args?: EdgeHttpCallArgs,
147
153
  ): Promise<RecoverIdentityResponseBody> {
148
- return this._call(new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
154
+ return this._call(ctx, new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
149
155
  }
150
156
 
151
157
  //
152
- // Invitations
158
+ // Invitations (space join)
153
159
  //
154
160
 
155
161
  public async joinSpaceByInvitation(
162
+ ctx: Context,
156
163
  spaceId: SpaceId,
157
164
  body: JoinSpaceRequest,
158
- args?: EdgeHttpGetArgs,
165
+ args?: EdgeHttpCallArgs,
159
166
  ): Promise<JoinSpaceResponseBody> {
160
- return this._call(new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
167
+ return this._call(ctx, new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
161
168
  }
162
169
 
163
170
  //
164
- // OAuth and credentials
171
+ // OAuth
165
172
  //
166
173
 
167
- public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
168
- return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
169
- }
170
-
171
174
  public async initiateOAuthFlow(
175
+ ctx: Context,
172
176
  body: InitiateOAuthFlowRequest,
173
- args?: EdgeHttpGetArgs,
177
+ args?: EdgeHttpCallArgs,
174
178
  ): Promise<InitiateOAuthFlowResponse> {
175
- return this._call(new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
179
+ return this._call(ctx, new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
180
+ }
181
+
182
+ public async completeOAuthRegistration(
183
+ ctx: Context,
184
+ body: CompleteOAuthRegistrationRequest,
185
+ args?: EdgeHttpCallArgs,
186
+ ): Promise<CompleteOAuthRegistrationResponse> {
187
+ return this._call(ctx, new URL('/oauth/registration/complete', this.baseUrl), { ...args, body, method: 'POST' });
176
188
  }
177
189
 
178
190
  //
179
191
  // Spaces
180
192
  //
181
193
 
182
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
183
- return this._call(new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
194
+ async createSpace(ctx: Context, body: CreateSpaceRequest, args?: EdgeHttpCallArgs): Promise<CreateSpaceResponseBody> {
195
+ return this._call(ctx, new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
184
196
  }
185
197
 
186
198
  //
@@ -188,13 +200,16 @@ export class EdgeHttpClient {
188
200
  //
189
201
 
190
202
  public async queryQueue(
203
+ ctx: Context,
191
204
  subspaceTag: string,
192
205
  spaceId: SpaceId,
193
- query: QueueQuery,
194
- args?: EdgeHttpGetArgs,
195
- ): Promise<QueryResult> {
196
- const { queueId } = query;
206
+ query: FeedProtocol.QueueQuery,
207
+ args?: EdgeHttpCallArgs,
208
+ ): Promise<EdgeQueryQueueResponse> {
209
+ const queueId = query.queueIds?.[0];
210
+ invariant(queueId, 'queueId required');
197
211
  return this._call(
212
+ ctx,
198
213
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
199
214
  after: query.after,
200
215
  before: query.before,
@@ -202,21 +217,19 @@ export class EdgeHttpClient {
202
217
  reverse: query.reverse,
203
218
  objectIds: query.objectIds?.join(','),
204
219
  }),
205
- {
206
- ...args,
207
- method: 'GET',
208
- },
220
+ { ...args, method: 'GET' },
209
221
  );
210
222
  }
211
223
 
212
224
  public async insertIntoQueue(
225
+ ctx: Context,
213
226
  subspaceTag: string,
214
227
  spaceId: SpaceId,
215
228
  queueId: ObjectId,
216
229
  objects: unknown[],
217
- args?: EdgeHttpGetArgs,
230
+ args?: EdgeHttpCallArgs,
218
231
  ): Promise<void> {
219
- return this._call(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
232
+ return this._call(ctx, new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
220
233
  ...args,
221
234
  body: { objects },
222
235
  method: 'POST',
@@ -224,20 +237,19 @@ export class EdgeHttpClient {
224
237
  }
225
238
 
226
239
  public async deleteFromQueue(
240
+ ctx: Context,
227
241
  subspaceTag: string,
228
242
  spaceId: SpaceId,
229
243
  queueId: ObjectId,
230
244
  objectIds: ObjectId[],
231
- args?: EdgeHttpGetArgs,
245
+ args?: EdgeHttpCallArgs,
232
246
  ): Promise<void> {
233
247
  return this._call(
248
+ ctx,
234
249
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
235
250
  ids: objectIds.join(','),
236
251
  }),
237
- {
238
- ...args,
239
- method: 'DELETE',
240
- },
252
+ { ...args, method: 'DELETE' },
241
253
  );
242
254
  }
243
255
 
@@ -246,12 +258,58 @@ export class EdgeHttpClient {
246
258
  //
247
259
 
248
260
  public async uploadFunction(
261
+ ctx: Context,
249
262
  pathParts: { functionId?: string },
250
263
  body: UploadFunctionRequest,
251
- args?: EdgeHttpGetArgs,
264
+ args?: EdgeHttpCallArgs,
252
265
  ): Promise<UploadFunctionResponseBody> {
266
+ const formData = new FormData();
267
+ formData.append('name', body.name ?? '');
268
+ formData.append('version', body.version);
269
+ formData.append('ownerPublicKey', body.ownerPublicKey);
270
+ formData.append('entryPoint', body.entryPoint);
271
+ body.runtime && formData.append('runtime', body.runtime);
272
+ for (const [filename, content] of Object.entries(body.assets)) {
273
+ formData.append(
274
+ 'assets',
275
+ new Blob([content as Uint8Array<ArrayBuffer>], { type: getFileMimeType(filename) }),
276
+ filename,
277
+ );
278
+ }
253
279
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
254
- return this._call(new URL(path, this.baseUrl), { ...args, body, method: 'PUT' });
280
+ return this._call(ctx, new URL(path, this.baseUrl), { ...args, body: formData, method: 'PUT', json: false });
281
+ }
282
+
283
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
284
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
285
+ }
286
+
287
+ public async invokeFunction(
288
+ ctx: Context,
289
+ params: {
290
+ functionId: string;
291
+ version?: string;
292
+ spaceId?: SpaceId;
293
+ cpuTimeLimit?: number;
294
+ subrequestsLimit?: number;
295
+ },
296
+ input: unknown,
297
+ args?: EdgeHttpCallArgs,
298
+ ): Promise<any> {
299
+ const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
300
+ if (params.version) {
301
+ url.searchParams.set('version', params.version);
302
+ }
303
+ if (params.spaceId) {
304
+ url.searchParams.set('spaceId', params.spaceId.toString());
305
+ }
306
+ if (params.cpuTimeLimit) {
307
+ url.searchParams.set('cpuTimeLimit', params.cpuTimeLimit.toString());
308
+ }
309
+ if (params.subrequestsLimit) {
310
+ url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
311
+ }
312
+ return this._call(ctx, url, { ...args, body: input, method: 'POST' });
255
313
  }
256
314
 
257
315
  //
@@ -259,12 +317,13 @@ export class EdgeHttpClient {
259
317
  //
260
318
 
261
319
  public async executeWorkflow(
320
+ ctx: Context,
262
321
  spaceId: SpaceId,
263
322
  graphId: ObjectId,
264
323
  input: any,
265
- args?: EdgeHttpGetArgs,
324
+ args?: EdgeHttpCallArgs,
266
325
  ): Promise<ExecuteWorkflowResponseBody> {
267
- return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
326
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
268
327
  ...args,
269
328
  body: input,
270
329
  method: 'POST',
@@ -272,109 +331,172 @@ export class EdgeHttpClient {
272
331
  }
273
332
 
274
333
  //
275
- // Internal
334
+ // Triggers
276
335
  //
277
336
 
278
- private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
279
- return pipe(
280
- HttpClient.get(url),
281
- withLogging,
282
- withRetryConfig,
283
- Effect.provide(FetchHttpClient.layer),
284
- Effect.provide(HttpConfig.default),
285
- Effect.withSpan('EdgeHttpClient'),
286
- Effect.runPromise,
287
- ) as T;
337
+ public async getCronTriggers(ctx: Context, spaceId: SpaceId): Promise<GetCronTriggersResponse> {
338
+ return this._call<GetCronTriggersResponse>(ctx, new URL(`/functions/${spaceId}/triggers/crons`, this.baseUrl), {
339
+ method: 'GET',
340
+ });
288
341
  }
289
342
 
290
- // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
291
- private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
292
- const shouldRetry = createRetryHandler(args);
293
- const requestContext = args.context ?? new Context();
294
- log('fetch', { url, request: args.body });
343
+ public async getTriggersDispatcherStatus(
344
+ ctx: Context,
345
+ spaceId: SpaceId,
346
+ args?: EdgeHttpCallArgs,
347
+ ): Promise<TriggersDispatcherStatus> {
348
+ return this._call<TriggersDispatcherStatus>(ctx, new URL(`/triggers/${spaceId}/status`, this.baseUrl), {
349
+ ...args,
350
+ method: 'GET',
351
+ auth: true,
352
+ });
353
+ }
354
+
355
+ public async forceRunCronTrigger(ctx: Context, spaceId: SpaceId, triggerId: ObjectId) {
356
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons/${triggerId}/run`, this.baseUrl), {
357
+ method: 'POST',
358
+ });
359
+ }
360
+
361
+ //
362
+ // Query
363
+ //
364
+
365
+ public async execQuery(
366
+ ctx: Context,
367
+ spaceId: SpaceId,
368
+ body: QueryRequestProto,
369
+ args?: EdgeHttpCallArgs,
370
+ ): Promise<QueryResponseProto> {
371
+ return this._call(ctx, new URL(`/spaces/${spaceId}/exec-query`, this.baseUrl), { ...args, body, method: 'POST' });
372
+ }
373
+
374
+ //
375
+ // Registry
376
+ //
377
+
378
+ public async getRegistryPlugins(ctx: Context, args?: EdgeHttpCallArgs): Promise<GetPluginsResponseBody> {
379
+ return this._call(ctx, new URL('/registry/plugins', this.baseUrl), { ...args, method: 'GET' });
380
+ }
381
+
382
+ public async getRegistryPluginVersions(
383
+ ctx: Context,
384
+ repo: string,
385
+ args?: EdgeHttpCallArgs,
386
+ ): Promise<GetPluginVersionsResponseBody> {
387
+ return this._call(ctx, new URL(`/registry/plugins/${encodeURIComponent(repo)}/versions`, this.baseUrl), {
388
+ ...args,
389
+ method: 'GET',
390
+ });
391
+ }
392
+
393
+ //
394
+ // Import/Export
395
+ //
396
+
397
+ public async importBundle(
398
+ ctx: Context,
399
+ spaceId: SpaceId,
400
+ body: ImportBundleRequest,
401
+ args?: EdgeHttpCallArgs,
402
+ ): Promise<void> {
403
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
404
+ }
405
+
406
+ public async exportBundle(
407
+ ctx: Context,
408
+ spaceId: SpaceId,
409
+ body: ExportBundleRequest,
410
+ args?: EdgeHttpCallArgs,
411
+ ): Promise<ExportBundleResponse> {
412
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), { ...args, body, method: 'POST' });
413
+ }
414
+
415
+ //
416
+ // Proxy
417
+ //
418
+
419
+ /**
420
+ * Fetch through the edge proxy for third-party REST APIs.
421
+ * TEMPORARY: currently routes through legacy open proxy. See https://github.com/dxos/edge/pull/576.
422
+ */
423
+ public async proxyFetch(target: URL, init: RequestInit = {}): Promise<Response> {
424
+ return proxyFetchLegacy(target, init, this._clientTag);
425
+ }
426
+
427
+ //
428
+ // AI service.
429
+ //
430
+
431
+ /**
432
+ * Issue an authenticated request to the EDGE AI route (`/ai/generate/anthropic/*`), which
433
+ * proxies to the AI service. Used as the backend HTTP client for the Anthropic AI provider
434
+ * (see {@link EdgeAiHttpClient}).
435
+ *
436
+ * Returns the raw `Response` so streaming bodies are forwarded unchanged to `@effect/ai`.
437
+ * Requires an identity to have been set via {@link setIdentity}.
438
+ */
439
+ // TODO(mykola): Merge into `BaseHttpClient._call` once it can return a streaming/raw `Response`;
440
+ // the auth/retry loop below duplicates the one in `_call`.
441
+ public async anthropicAiRequest(request: Request): Promise<Response> {
442
+ const incoming = new URL(request.url);
443
+ const base = this.baseUrl.replace(/\/$/, '');
444
+ const target = new URL(`${base}/ai/generate/anthropic${incoming.pathname}${incoming.search}`);
445
+
446
+ const method = request.method;
447
+ const body = method === 'GET' || method === 'HEAD' ? undefined : await request.arrayBuffer();
295
448
 
296
449
  let handledAuth = false;
297
450
  while (true) {
298
- let processingError: EdgeCallFailedError;
299
- let retryAfterHeaderValue: number = Number.NaN;
300
- try {
301
- const request = createRequest(args, this._authHeader);
302
- const response = await fetch(url, request);
303
- retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
304
- if (response.ok) {
305
- const body = (await response.json()) as EdgeHttpResponse<T>;
306
- if (body.success) {
307
- return body.data;
308
- }
309
-
310
- log.warn('unsuccessful edge response', { url, body });
311
- if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
312
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
313
- } else {
314
- processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
315
- }
316
- } else if (response.status === 401 && !handledAuth) {
317
- this._authHeader = await this._handleUnauthorized(response);
318
- handledAuth = true;
319
- continue;
320
- } else {
321
- processingError = EdgeCallFailedError.fromHttpFailure(response);
451
+ if (!this._authHeader) {
452
+ const authResponse = await fetch(new URL('/auth', this.baseUrl));
453
+ if (authResponse.status === 401) {
454
+ this._authHeader = await this._handleUnauthorized(authResponse);
322
455
  }
323
- } catch (error: any) {
324
- processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
325
456
  }
326
457
 
327
- if (processingError.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
328
- log('retrying edge request', { url, processingError });
329
- } else {
330
- throw processingError;
458
+ const headers = new Headers(request.headers);
459
+ if (this._authHeader) {
460
+ headers.set('Authorization', this._authHeader);
461
+ }
462
+ if (this._clientTag) {
463
+ headers.set(EDGE_CLIENT_TAG_HEADER, this._clientTag);
331
464
  }
332
- }
333
- }
334
465
 
335
- private async _handleUnauthorized(response: Response): Promise<string> {
336
- if (!this._edgeIdentity) {
337
- log.warn('unauthorized response received before identity was set');
338
- throw EdgeCallFailedError.fromHttpFailure(response);
339
- }
466
+ const response = await fetch(target, { method, headers, body, signal: request.signal });
467
+ // Only retry edge auth when the 401 came from edge's own auth layer. Edge always sets
468
+ // `WWW-Authenticate` on its own 401s; upstream-forwarded 401s (e.g. invalid BYOK rejected
469
+ // by Anthropic) lack it and must be surfaced verbatim.
470
+ if (response.status === 401 && response.headers.get('WWW-Authenticate') !== null && !handledAuth) {
471
+ this._authHeader = await this._handleUnauthorized(response);
472
+ handledAuth = true;
473
+ continue;
474
+ }
340
475
 
341
- const challenge = await handleAuthChallenge(response, this._edgeIdentity);
342
- return encodeAuthHeader(challenge);
476
+ return response;
477
+ }
343
478
  }
344
- }
345
479
 
346
- const createRequest = ({ method, body }: EdgeHttpRequestArgs, authHeader: string | undefined): RequestInit => {
347
- return {
348
- method,
349
- body: body && JSON.stringify(body),
350
- headers: authHeader ? { Authorization: authHeader } : undefined,
351
- };
352
- };
480
+ //
481
+ // Internal (Effect-based, used by tests)
482
+ //
353
483
 
354
- /**
355
- * @deprecated
356
- */
357
- const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
358
- if (!retry || retry.count < 1) {
359
- return async () => false;
484
+ public async _fetch<T>(url: URL, _args: { method: string }): Promise<T> {
485
+ return Function.pipe(
486
+ HttpClient.execute(HttpClientRequest.make(_args.method as any)(url.toString())),
487
+ withLogging,
488
+ withRetryConfig,
489
+ Effect.provide(FetchHttpClient.layer),
490
+ Effect.provide(HttpConfig.default),
491
+ Effect.withSpan('EdgeHttpClient'),
492
+ EffectEx.runAndForwardErrors,
493
+ ) as T;
360
494
  }
495
+ }
361
496
 
362
- let retries = 0;
363
- const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
364
- const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
365
- const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
366
- return async (ctx: Context, retryAfter: number) => {
367
- if (++retries > maxRetries || ctx.disposed) {
368
- return false;
369
- }
370
-
371
- if (retryAfter) {
372
- await sleep(retryAfter);
373
- } else {
374
- const timeout = baseTimeout + Math.random() * jitter;
375
- await sleep(timeout);
376
- }
377
-
378
- return true;
379
- };
380
- };
497
+ const getFileMimeType = (filename: string) =>
498
+ ['.js', '.mjs'].some((ext) => filename.endsWith(ext))
499
+ ? 'application/javascript+module'
500
+ : filename.endsWith('.wasm')
501
+ ? 'application/wasm'
502
+ : 'application/octet-stream';