@dxos/edge-client 0.8.4-main.fffef41 → 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 (70) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/{node-esm/chunk-JTBFRYNM.mjs → neutral/chunk-L5ZHLJ4B.mjs} +54 -47
  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/{browser → neutral}/index.mjs +553 -467
  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 +6 -3
  21. package/dist/types/src/edge-client.d.ts.map +1 -1
  22. package/dist/types/src/edge-http-client.d.ts +76 -75
  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 +1 -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 +2 -2
  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 +4 -0
  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 +2 -2
  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 +16 -11
  48. package/src/edge-client.ts +37 -7
  49. package/src/edge-http-client.test.ts +36 -2
  50. package/src/edge-http-client.ts +237 -270
  51. package/src/edge-ws-connection.ts +2 -1
  52. package/src/edge-ws-muxer.ts +49 -5
  53. package/src/http-client.test.ts +3 -2
  54. package/src/hub-http-client.ts +118 -0
  55. package/src/index.ts +4 -0
  56. package/src/testing/test-utils.ts +4 -4
  57. package/dist/lib/browser/chunk-VESGVCLQ.mjs +0 -301
  58. package/dist/lib/browser/chunk-VESGVCLQ.mjs.map +0 -7
  59. package/dist/lib/browser/index.mjs.map +0 -7
  60. package/dist/lib/browser/meta.json +0 -1
  61. package/dist/lib/browser/testing/index.mjs.map +0 -7
  62. package/dist/lib/node-esm/chunk-JTBFRYNM.mjs.map +0 -7
  63. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  64. package/dist/lib/node-esm/index.mjs +0 -1363
  65. package/dist/lib/node-esm/index.mjs.map +0 -7
  66. package/dist/lib/node-esm/meta.json +0 -1
  67. package/dist/lib/node-esm/testing/index.mjs +0 -186
  68. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  69. /package/dist/lib/{browser/edge-ws-muxer.mjs.map → neutral/cors-proxy.mjs.map} +0 -0
  70. /package/dist/lib/{node-esm → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -4,27 +4,32 @@
4
4
 
5
5
  import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
6
  import * as HttpClient from '@effect/platform/HttpClient';
7
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
7
8
  import * as Effect from 'effect/Effect';
8
9
  import * as Function from 'effect/Function';
9
10
 
10
- import { sleep } from '@dxos/async';
11
- import { Context } from '@dxos/context';
11
+ import { type Context } from '@dxos/context';
12
+ import { EffectEx } 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';
15
16
  import {
17
+ type CompleteOAuthRegistrationRequest,
18
+ type CompleteOAuthRegistrationResponse,
16
19
  type CreateAgentRequestBody,
17
20
  type CreateAgentResponseBody,
18
21
  type CreateSpaceRequest,
19
22
  type CreateSpaceResponseBody,
20
- EdgeAuthChallengeError,
21
- EdgeCallFailedError,
23
+ EDGE_CLIENT_TAG_HEADER,
22
24
  type EdgeStatus,
23
25
  type ExecuteWorkflowResponseBody,
24
26
  type ExportBundleRequest,
25
27
  type ExportBundleResponse,
28
+ type FeedProtocol,
26
29
  type GetAgentStatusResponseBody,
27
30
  type GetNotarizationResponseBody,
31
+ type GetPluginVersionsResponseBody,
32
+ type GetPluginsResponseBody,
28
33
  type ImportBundleRequest,
29
34
  type InitiateOAuthFlowRequest,
30
35
  type InitiateOAuthFlowResponse,
@@ -32,113 +37,85 @@ import {
32
37
  type JoinSpaceResponseBody,
33
38
  type ObjectId,
34
39
  type PostNotarizationRequestBody,
35
- type QueryResult,
36
- type QueueQuery,
37
40
  type RecoverIdentityRequest,
38
41
  type RecoverIdentityResponseBody,
39
42
  type UploadFunctionRequest,
40
43
  type UploadFunctionResponseBody,
41
44
  } from '@dxos/protocols';
45
+ import {
46
+ type QueryRequest as QueryRequestProto,
47
+ type QueryResponse as QueryResponseProto,
48
+ } from '@dxos/protocols/proto/dxos/echo/query';
42
49
  import { createUrl } from '@dxos/util';
43
50
 
44
- import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
45
- import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
46
- 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';
47
54
 
48
- const DEFAULT_RETRY_TIMEOUT = 1500;
49
- const DEFAULT_RETRY_JITTER = 500;
50
- const DEFAULT_MAX_RETRIES_COUNT = 3;
51
- const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
55
+ export type { EdgeHttpCallArgs, RetryConfig } from './base-http-client';
52
56
 
53
- export type RetryConfig = {
54
- /**
55
- * A number of call retries, not counting the initial request.
56
- */
57
- count: number;
58
- /**
59
- * Delay before retries in ms.
60
- */
61
- timeout?: number;
62
- /**
63
- * A random amount of time before retrying to help prevent large bursts of requests.
64
- */
65
- 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;
66
64
  };
67
65
 
68
- type EdgeHttpRequestArgs = {
69
- method: string;
70
- context?: Context;
71
- retry?: RetryConfig;
72
- body?: any;
73
- /**
74
- * @default true
75
- */
76
- json?: boolean;
77
-
78
- /**
79
- * Do not expect a standard EDGE JSON response with a `success` field.
80
- * @deprecated Use only for debugging.
81
- */
82
- rawResponse?: boolean;
83
-
84
- /**
85
- * Force authentication.
86
- * This should be used for requests with large bodies to avoid sending the body twice.
87
- * The client will call /auth endpoint to generate the auth header.
88
- */
89
- auth?: boolean;
66
+ export type TriggersDispatcherStatus = {
67
+ isActive: boolean;
68
+ nextCronTaskRunTimestamp?: number;
69
+ registeredTriggers: string[];
70
+ stopAfterTimestamp?: number;
71
+ remainingMs?: number;
72
+ nextAlarmTimestamp?: number;
90
73
  };
91
74
 
92
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'auth'>;
93
- export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body' | 'auth'>;
94
-
95
- export class EdgeHttpClient {
96
- private readonly _baseUrl: string;
97
-
98
- private _edgeIdentity: EdgeIdentity | undefined;
99
-
100
- /**
101
- * Auth header is cached until receiving the next 401 from EDGE, at which point it gets refreshed.
102
- */
103
- private _authHeader: string | undefined;
104
-
105
- constructor(baseUrl: string) {
106
- this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
107
- log('created', { url: this._baseUrl });
108
- }
75
+ export type GetCronTriggersResponse = {
76
+ cronIds: string[];
77
+ };
109
78
 
110
- get baseUrl() {
111
- return this._baseUrl;
112
- }
79
+ export type EdgeHttpClientOptions = BaseHttpClientOptions;
113
80
 
114
- setIdentity(identity: EdgeIdentity): void {
115
- if (this._edgeIdentity?.identityKey !== identity.identityKey || this._edgeIdentity?.peerKey !== identity.peerKey) {
116
- this._edgeIdentity = identity;
117
- this._authHeader = undefined;
118
- }
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 });
119
91
  }
120
92
 
121
93
  //
122
94
  // Status
123
95
  //
124
96
 
125
- public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
126
- 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 });
127
99
  }
128
100
 
129
101
  //
130
102
  // Agents
131
103
  //
132
104
 
133
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
134
- 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 });
135
111
  }
136
112
 
137
113
  public getAgentStatus(
114
+ ctx: Context,
138
115
  request: { ownerIdentityKey: PublicKey },
139
- args?: EdgeHttpGetArgs,
116
+ args?: EdgeHttpCallArgs,
140
117
  ): Promise<GetAgentStatusResponseBody> {
141
- 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), {
142
119
  ...args,
143
120
  method: 'GET',
144
121
  });
@@ -148,16 +125,21 @@ export class EdgeHttpClient {
148
125
  // Credentials
149
126
  //
150
127
 
151
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
152
- 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' });
153
134
  }
154
135
 
155
136
  public async notarizeCredentials(
137
+ ctx: Context,
156
138
  spaceId: SpaceId,
157
139
  body: PostNotarizationRequestBody,
158
- args?: EdgeHttpGetArgs,
140
+ args?: EdgeHttpCallArgs,
159
141
  ): Promise<void> {
160
- 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' });
161
143
  }
162
144
 
163
145
  //
@@ -165,41 +147,52 @@ export class EdgeHttpClient {
165
147
  //
166
148
 
167
149
  public async recoverIdentity(
150
+ ctx: Context,
168
151
  body: RecoverIdentityRequest,
169
- args?: EdgeHttpGetArgs,
152
+ args?: EdgeHttpCallArgs,
170
153
  ): Promise<RecoverIdentityResponseBody> {
171
- 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' });
172
155
  }
173
156
 
174
157
  //
175
- // Invitations
158
+ // Invitations (space join)
176
159
  //
177
160
 
178
161
  public async joinSpaceByInvitation(
162
+ ctx: Context,
179
163
  spaceId: SpaceId,
180
164
  body: JoinSpaceRequest,
181
- args?: EdgeHttpGetArgs,
165
+ args?: EdgeHttpCallArgs,
182
166
  ): Promise<JoinSpaceResponseBody> {
183
- 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' });
184
168
  }
185
169
 
186
170
  //
187
- // OAuth and credentials
171
+ // OAuth
188
172
  //
189
173
 
190
174
  public async initiateOAuthFlow(
175
+ ctx: Context,
191
176
  body: InitiateOAuthFlowRequest,
192
- args?: EdgeHttpGetArgs,
177
+ args?: EdgeHttpCallArgs,
193
178
  ): Promise<InitiateOAuthFlowResponse> {
194
- 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' });
195
188
  }
196
189
 
197
190
  //
198
191
  // Spaces
199
192
  //
200
193
 
201
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
202
- 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' });
203
196
  }
204
197
 
205
198
  //
@@ -207,13 +200,16 @@ export class EdgeHttpClient {
207
200
  //
208
201
 
209
202
  public async queryQueue(
203
+ ctx: Context,
210
204
  subspaceTag: string,
211
205
  spaceId: SpaceId,
212
- query: QueueQuery,
213
- args?: EdgeHttpGetArgs,
214
- ): Promise<QueryResult> {
215
- const { queueId } = query;
206
+ query: FeedProtocol.QueueQuery,
207
+ args?: EdgeHttpCallArgs,
208
+ ): Promise<EdgeQueryQueueResponse> {
209
+ const queueId = query.queueIds?.[0];
210
+ invariant(queueId, 'queueId required');
216
211
  return this._call(
212
+ ctx,
217
213
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
218
214
  after: query.after,
219
215
  before: query.before,
@@ -221,21 +217,19 @@ export class EdgeHttpClient {
221
217
  reverse: query.reverse,
222
218
  objectIds: query.objectIds?.join(','),
223
219
  }),
224
- {
225
- ...args,
226
- method: 'GET',
227
- },
220
+ { ...args, method: 'GET' },
228
221
  );
229
222
  }
230
223
 
231
224
  public async insertIntoQueue(
225
+ ctx: Context,
232
226
  subspaceTag: string,
233
227
  spaceId: SpaceId,
234
228
  queueId: ObjectId,
235
229
  objects: unknown[],
236
- args?: EdgeHttpGetArgs,
230
+ args?: EdgeHttpCallArgs,
237
231
  ): Promise<void> {
238
- 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), {
239
233
  ...args,
240
234
  body: { objects },
241
235
  method: 'POST',
@@ -243,20 +237,19 @@ export class EdgeHttpClient {
243
237
  }
244
238
 
245
239
  public async deleteFromQueue(
240
+ ctx: Context,
246
241
  subspaceTag: string,
247
242
  spaceId: SpaceId,
248
243
  queueId: ObjectId,
249
244
  objectIds: ObjectId[],
250
- args?: EdgeHttpGetArgs,
245
+ args?: EdgeHttpCallArgs,
251
246
  ): Promise<void> {
252
247
  return this._call(
248
+ ctx,
253
249
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
254
250
  ids: objectIds.join(','),
255
251
  }),
256
- {
257
- ...args,
258
- method: 'DELETE',
259
- },
252
+ { ...args, method: 'DELETE' },
260
253
  );
261
254
  }
262
255
 
@@ -265,9 +258,10 @@ export class EdgeHttpClient {
265
258
  //
266
259
 
267
260
  public async uploadFunction(
261
+ ctx: Context,
268
262
  pathParts: { functionId?: string },
269
263
  body: UploadFunctionRequest,
270
- args?: EdgeHttpGetArgs,
264
+ args?: EdgeHttpCallArgs,
271
265
  ): Promise<UploadFunctionResponseBody> {
272
266
  const formData = new FormData();
273
267
  formData.append('name', body.name ?? '');
@@ -282,21 +276,16 @@ export class EdgeHttpClient {
282
276
  filename,
283
277
  );
284
278
  }
285
-
286
279
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
287
- return this._call(new URL(path, this.baseUrl), {
288
- ...args,
289
- body: formData,
290
- method: 'PUT',
291
- json: false,
292
- });
280
+ return this._call(ctx, new URL(path, this.baseUrl), { ...args, body: formData, method: 'PUT', json: false });
293
281
  }
294
282
 
295
- public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
296
- return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
283
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
284
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
297
285
  }
298
286
 
299
287
  public async invokeFunction(
288
+ ctx: Context,
300
289
  params: {
301
290
  functionId: string;
302
291
  version?: string;
@@ -305,7 +294,7 @@ export class EdgeHttpClient {
305
294
  subrequestsLimit?: number;
306
295
  },
307
296
  input: unknown,
308
- args?: EdgeHttpGetArgs,
297
+ args?: EdgeHttpCallArgs,
309
298
  ): Promise<any> {
310
299
  const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
311
300
  if (params.version) {
@@ -320,13 +309,7 @@ export class EdgeHttpClient {
320
309
  if (params.subrequestsLimit) {
321
310
  url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
322
311
  }
323
-
324
- return this._call(url, {
325
- ...args,
326
- body: input,
327
- method: 'POST',
328
- rawResponse: true,
329
- });
312
+ return this._call(ctx, url, { ...args, body: input, method: 'POST' });
330
313
  }
331
314
 
332
315
  //
@@ -334,12 +317,13 @@ export class EdgeHttpClient {
334
317
  //
335
318
 
336
319
  public async executeWorkflow(
320
+ ctx: Context,
337
321
  spaceId: SpaceId,
338
322
  graphId: ObjectId,
339
323
  input: any,
340
- args?: EdgeHttpGetArgs,
324
+ args?: EdgeHttpCallArgs,
341
325
  ): Promise<ExecuteWorkflowResponseBody> {
342
- return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
326
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
343
327
  ...args,
344
328
  body: input,
345
329
  method: 'POST',
@@ -350,185 +334,168 @@ export class EdgeHttpClient {
350
334
  // Triggers
351
335
  //
352
336
 
353
- public async getCronTriggers(spaceId: SpaceId) {
354
- return this._call(new URL(`/test/functions/${spaceId}/triggers/crons`, this.baseUrl), { method: 'GET' });
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
+ });
341
+ }
342
+
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
+ });
355
359
  }
356
360
 
357
361
  //
358
- // Import/Export space.
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
359
395
  //
360
396
 
361
397
  public async importBundle(
362
- spaceId: SpaceId, //
398
+ ctx: Context,
399
+ spaceId: SpaceId,
363
400
  body: ImportBundleRequest,
364
- args?: EdgeHttpGetArgs,
401
+ args?: EdgeHttpCallArgs,
365
402
  ): Promise<void> {
366
- return this._call(new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
403
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
367
404
  }
368
405
 
369
406
  public async exportBundle(
407
+ ctx: Context,
370
408
  spaceId: SpaceId,
371
409
  body: ExportBundleRequest,
372
- args?: EdgeHttpGetArgs,
410
+ args?: EdgeHttpCallArgs,
373
411
  ): Promise<ExportBundleResponse> {
374
- return this._call(new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
375
- ...args,
376
- body,
377
- method: 'POST',
378
- });
412
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), { ...args, body, method: 'POST' });
379
413
  }
380
414
 
381
415
  //
382
- // Internal
416
+ // Proxy
383
417
  //
384
418
 
385
- private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
386
- return Function.pipe(
387
- HttpClient.get(url),
388
- withLogging,
389
- withRetryConfig,
390
- Effect.provide(FetchHttpClient.layer),
391
- Effect.provide(HttpConfig.default),
392
- Effect.withSpan('EdgeHttpClient'),
393
- Effect.runPromise,
394
- ) as T;
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);
395
425
  }
396
426
 
397
- // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
398
- private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
399
- const shouldRetry = createRetryHandler(args);
400
- const requestContext = args.context ?? Context.default();
401
- log('fetch', { url, request: args.body });
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();
402
448
 
403
449
  let handledAuth = false;
404
- const tryCount = 1;
405
450
  while (true) {
406
- let processingError: EdgeCallFailedError | undefined = undefined;
407
- try {
408
- if (!this._authHeader && args.auth) {
409
- const response = await fetch(new URL(`/auth`, this.baseUrl));
410
- if (response.status === 401) {
411
- this._authHeader = await this._handleUnauthorized(response);
412
- }
413
- }
414
-
415
- const request = createRequest(args, this._authHeader);
416
- log('call edge', { url, tryCount, authHeader: !!this._authHeader });
417
- const response = await fetch(url, request);
418
-
419
- if (response.ok) {
420
- const body = await response.clone().json();
421
- if (args.rawResponse) {
422
- return body as any;
423
- }
424
- invariant(body, 'Expected body to be present');
425
- if (!('success' in body)) {
426
- return body;
427
- }
428
- if (body.success) {
429
- return body.data;
430
- }
431
- } else if (response.status === 401 && !handledAuth) {
432
- this._authHeader = await this._handleUnauthorized(response);
433
- handledAuth = true;
434
- continue;
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);
435
455
  }
456
+ }
436
457
 
437
- const body =
438
- response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
439
-
440
- invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
441
-
442
- if (body?.errorData?.type === 'auth_challenge' && typeof body?.errorData?.challenge === 'string') {
443
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
444
- } else if (body?.success === false) {
445
- processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
446
- } else {
447
- invariant(!response.ok, 'Expected response to not be ok.');
448
- processingError = await EdgeCallFailedError.fromHttpFailure(response);
449
- }
450
- } catch (error: any) {
451
- processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
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);
452
464
  }
453
465
 
454
- if (processingError?.isRetryable && (await shouldRetry(requestContext, processingError.retryAfterMs))) {
455
- log.verbose('retrying edge request', { url, processingError });
456
- } else {
457
- throw processingError!;
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;
458
474
  }
459
- }
460
- }
461
475
 
462
- private async _handleUnauthorized(response: Response): Promise<string> {
463
- if (!this._edgeIdentity) {
464
- log.warn('unauthorized response received before identity was set');
465
- throw await EdgeCallFailedError.fromHttpFailure(response);
476
+ return response;
466
477
  }
467
-
468
- const challenge = await handleAuthChallenge(response, this._edgeIdentity);
469
- return encodeAuthHeader(challenge);
470
- }
471
- }
472
-
473
- const createRequest = (
474
- { method, body, json = true }: EdgeHttpRequestArgs,
475
- authHeader: string | undefined,
476
- ): RequestInit => {
477
- let requestBody: BodyInit | undefined;
478
- const headers: HeadersInit = {};
479
-
480
- if (json) {
481
- requestBody = body && JSON.stringify(body);
482
- headers['Content-Type'] = 'application/json';
483
- } else {
484
- requestBody = body;
485
478
  }
486
479
 
487
- if (typeof requestBody === 'string' && requestBody.length > WARNING_BODY_SIZE) {
488
- log.warn('Request with large body', { bodySize: requestBody.length });
489
- }
490
-
491
- if (authHeader) {
492
- headers['Authorization'] = authHeader;
493
- }
494
-
495
- return {
496
- method,
497
- body: requestBody,
498
- headers,
499
- };
500
- };
480
+ //
481
+ // Internal (Effect-based, used by tests)
482
+ //
501
483
 
502
- /**
503
- * @deprecated
504
- */
505
- const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
506
- if (!retry || retry.count < 1) {
507
- 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;
508
494
  }
509
-
510
- let retries = 0;
511
- const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
512
- const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
513
- const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
514
- return async (ctx: Context, retryAfter?: number) => {
515
- if (++retries > maxRetries || ctx.disposed) {
516
- return false;
517
- }
518
-
519
- if (retryAfter) {
520
- await sleep(retryAfter);
521
- } else {
522
- const timeout = baseTimeout + Math.random() * jitter;
523
- await sleep(timeout);
524
- }
525
-
526
- return true;
527
- };
528
- };
495
+ }
529
496
 
530
497
  const getFileMimeType = (filename: string) =>
531
- ['.js', '.mjs'].some((codeExtension) => filename.endsWith(codeExtension))
498
+ ['.js', '.mjs'].some((ext) => filename.endsWith(ext))
532
499
  ? 'application/javascript+module'
533
500
  : filename.endsWith('.wasm')
534
501
  ? 'application/wasm'