@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.
- package/LICENSE +102 -5
- package/dist/lib/{browser/chunk-LMP5TVOP.mjs → neutral/chunk-ZIQ5T3A7.mjs} +13 -43
- package/dist/lib/{browser/chunk-LMP5TVOP.mjs.map → neutral/chunk-ZIQ5T3A7.mjs.map} +3 -3
- package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
- package/dist/lib/neutral/index.mjs +1238 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +6 -31
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/auth.d.ts.map +1 -1
- package/dist/types/src/edge-client.d.ts +18 -15
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +114 -21
- package/dist/types/src/edge-http-client.d.ts.map +1 -1
- package/dist/types/src/edge-identity.d.ts.map +1 -1
- package/dist/types/src/edge-ws-connection.d.ts +21 -0
- package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
- package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
- package/dist/types/src/errors.d.ts.map +1 -1
- package/dist/types/src/http-client.d.ts +10 -7
- package/dist/types/src/http-client.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +4 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/protocol.d.ts +1 -1
- package/dist/types/src/protocol.d.ts.map +1 -1
- package/dist/types/src/testing/test-server.d.ts.map +1 -1
- package/dist/types/src/testing/test-utils.d.ts +3 -3
- package/dist/types/src/testing/test-utils.d.ts.map +1 -1
- package/dist/types/src/utils.d.ts +1 -1
- package/dist/types/src/utils.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -32
- package/src/edge-client.test.ts +20 -15
- package/src/edge-client.ts +90 -43
- package/src/edge-http-client.test.ts +3 -2
- package/src/edge-http-client.ts +462 -70
- package/src/edge-ws-connection.ts +131 -9
- package/src/edge-ws-muxer.ts +1 -1
- package/src/http-client.test.ts +11 -8
- package/src/http-client.ts +18 -8
- package/src/index.ts +4 -3
- package/src/testing/test-utils.ts +8 -8
- package/src/websocket.test.ts +1 -1
- package/dist/lib/browser/index.mjs +0 -1089
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-X7J46ISZ.mjs +0 -299
- package/dist/lib/node-esm/chunk-X7J46ISZ.mjs.map +0 -7
- package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
- package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -1090
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -186
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
package/src/edge-http-client.ts
CHANGED
|
@@ -2,47 +2,78 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
|
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
|
|
32
|
-
type
|
|
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 {
|
|
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
|
|
70
|
-
|
|
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?:
|
|
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(
|
|
111
|
-
|
|
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?:
|
|
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(
|
|
129
|
-
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
191
|
-
): Promise<
|
|
192
|
-
const
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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), {
|
|
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?:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
+
|
|
300
587
|
if (response.ok) {
|
|
301
|
-
const body =
|
|
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
|
-
|
|
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
|
|
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 = (
|
|
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:
|
|
346
|
-
headers
|
|
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
|
|
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
|
+
};
|