@dxos/edge-client 0.8.4-main.ead640a → 0.8.4-main.f466a3d56e
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-IKP53CBQ.mjs → neutral/chunk-ZIQ5T3A7.mjs} +20 -83
- package/dist/lib/{browser/chunk-IKP53CBQ.mjs.map → neutral/chunk-ZIQ5T3A7.mjs.map} +2 -2
- package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
- package/dist/lib/{browser → neutral}/index.mjs +318 -436
- 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 +5 -2
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +103 -30
- 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 +1 -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 +2 -2
- package/dist/types/src/http-client.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 +2 -2
- 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 +29 -32
- package/src/edge-client.test.ts +16 -11
- package/src/edge-client.ts +19 -3
- package/src/edge-http-client.test.ts +3 -2
- package/src/edge-http-client.ts +328 -75
- package/src/edge-ws-connection.ts +2 -1
- package/src/http-client.test.ts +3 -2
- package/src/http-client.ts +5 -1
- package/src/testing/test-utils.ts +4 -4
- 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-DR5YNW5K.mjs +0 -332
- package/dist/lib/node-esm/chunk-DR5YNW5K.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 -1357
- 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
|
@@ -8,7 +8,9 @@ import * as Effect from 'effect/Effect';
|
|
|
8
8
|
import * as Function from 'effect/Function';
|
|
9
9
|
|
|
10
10
|
import { sleep } from '@dxos/async';
|
|
11
|
-
import { Context } from '@dxos/context';
|
|
11
|
+
import { Context, TRACE_SPAN_ATTRIBUTE, type TraceContextData } from '@dxos/context';
|
|
12
|
+
import { runAndForwardErrors } from '@dxos/effect';
|
|
13
|
+
import { invariant } from '@dxos/invariant';
|
|
12
14
|
import { type PublicKey, type SpaceId } from '@dxos/keys';
|
|
13
15
|
import { log } from '@dxos/log';
|
|
14
16
|
import {
|
|
@@ -16,14 +18,18 @@ import {
|
|
|
16
18
|
type CreateAgentResponseBody,
|
|
17
19
|
type CreateSpaceRequest,
|
|
18
20
|
type CreateSpaceResponseBody,
|
|
21
|
+
EDGE_CLIENT_TAG_HEADER,
|
|
19
22
|
EdgeAuthChallengeError,
|
|
20
23
|
EdgeCallFailedError,
|
|
21
|
-
type
|
|
24
|
+
type EdgeFailure,
|
|
22
25
|
type EdgeStatus,
|
|
23
26
|
type ExecuteWorkflowResponseBody,
|
|
24
27
|
type ExportBundleRequest,
|
|
25
28
|
type ExportBundleResponse,
|
|
29
|
+
type FeedProtocol,
|
|
26
30
|
type GetAgentStatusResponseBody,
|
|
31
|
+
type GetPluginVersionsResponseBody,
|
|
32
|
+
type GetPluginsResponseBody,
|
|
27
33
|
type GetNotarizationResponseBody,
|
|
28
34
|
type ImportBundleRequest,
|
|
29
35
|
type InitiateOAuthFlowRequest,
|
|
@@ -32,24 +38,43 @@ import {
|
|
|
32
38
|
type JoinSpaceResponseBody,
|
|
33
39
|
type ObjectId,
|
|
34
40
|
type PostNotarizationRequestBody,
|
|
35
|
-
type QueryResult,
|
|
36
|
-
type QueueQuery,
|
|
37
41
|
type RecoverIdentityRequest,
|
|
38
42
|
type RecoverIdentityResponseBody,
|
|
39
43
|
type UploadFunctionRequest,
|
|
40
44
|
type UploadFunctionResponseBody,
|
|
41
45
|
} from '@dxos/protocols';
|
|
46
|
+
import {
|
|
47
|
+
type QueryRequest as QueryRequestProto,
|
|
48
|
+
type QueryResponse as QueryResponseProto,
|
|
49
|
+
} from '@dxos/protocols/proto/dxos/echo/query';
|
|
42
50
|
import { createUrl } from '@dxos/util';
|
|
43
51
|
|
|
44
52
|
import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
|
|
45
53
|
import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
|
|
46
54
|
import { getEdgeUrlWithProtocol } from './utils';
|
|
47
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
|
+
|
|
48
67
|
const DEFAULT_RETRY_TIMEOUT = 1500;
|
|
49
68
|
const DEFAULT_RETRY_JITTER = 500;
|
|
50
69
|
const DEFAULT_MAX_RETRIES_COUNT = 3;
|
|
51
70
|
const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
|
|
52
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';
|
|
77
|
+
|
|
53
78
|
export type RetryConfig = {
|
|
54
79
|
/**
|
|
55
80
|
* A number of call retries, not counting the initial request.
|
|
@@ -67,7 +92,6 @@ export type RetryConfig = {
|
|
|
67
92
|
|
|
68
93
|
type EdgeHttpRequestArgs = {
|
|
69
94
|
method: string;
|
|
70
|
-
context?: Context;
|
|
71
95
|
retry?: RetryConfig;
|
|
72
96
|
body?: any;
|
|
73
97
|
/**
|
|
@@ -76,16 +100,30 @@ type EdgeHttpRequestArgs = {
|
|
|
76
100
|
json?: boolean;
|
|
77
101
|
|
|
78
102
|
/**
|
|
79
|
-
*
|
|
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.
|
|
80
106
|
*/
|
|
81
|
-
|
|
107
|
+
auth?: boolean;
|
|
82
108
|
};
|
|
83
109
|
|
|
84
|
-
export type
|
|
85
|
-
|
|
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
|
+
};
|
|
86
123
|
|
|
87
124
|
export class EdgeHttpClient {
|
|
88
125
|
private readonly _baseUrl: string;
|
|
126
|
+
private readonly _clientTag: string | undefined;
|
|
89
127
|
|
|
90
128
|
private _edgeIdentity: EdgeIdentity | undefined;
|
|
91
129
|
|
|
@@ -94,8 +132,9 @@ export class EdgeHttpClient {
|
|
|
94
132
|
*/
|
|
95
133
|
private _authHeader: string | undefined;
|
|
96
134
|
|
|
97
|
-
constructor(baseUrl: string) {
|
|
135
|
+
constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
|
|
98
136
|
this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
|
|
137
|
+
this._clientTag = options?.clientTag;
|
|
99
138
|
log('created', { url: this._baseUrl });
|
|
100
139
|
}
|
|
101
140
|
|
|
@@ -114,23 +153,28 @@ export class EdgeHttpClient {
|
|
|
114
153
|
// Status
|
|
115
154
|
//
|
|
116
155
|
|
|
117
|
-
public async getStatus(args?:
|
|
118
|
-
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 });
|
|
119
158
|
}
|
|
120
159
|
|
|
121
160
|
//
|
|
122
161
|
// Agents
|
|
123
162
|
//
|
|
124
163
|
|
|
125
|
-
public createAgent(
|
|
126
|
-
|
|
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 });
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
public getAgentStatus(
|
|
173
|
+
ctx: Context,
|
|
130
174
|
request: { ownerIdentityKey: PublicKey },
|
|
131
|
-
args?:
|
|
175
|
+
args?: EdgeHttpCallArgs,
|
|
132
176
|
): Promise<GetAgentStatusResponseBody> {
|
|
133
|
-
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), {
|
|
134
178
|
...args,
|
|
135
179
|
method: 'GET',
|
|
136
180
|
});
|
|
@@ -140,16 +184,21 @@ export class EdgeHttpClient {
|
|
|
140
184
|
// Credentials
|
|
141
185
|
//
|
|
142
186
|
|
|
143
|
-
public getCredentialsForNotarization(
|
|
144
|
-
|
|
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' });
|
|
145
193
|
}
|
|
146
194
|
|
|
147
195
|
public async notarizeCredentials(
|
|
196
|
+
ctx: Context,
|
|
148
197
|
spaceId: SpaceId,
|
|
149
198
|
body: PostNotarizationRequestBody,
|
|
150
|
-
args?:
|
|
199
|
+
args?: EdgeHttpCallArgs,
|
|
151
200
|
): Promise<void> {
|
|
152
|
-
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' });
|
|
153
202
|
}
|
|
154
203
|
|
|
155
204
|
//
|
|
@@ -157,10 +206,11 @@ export class EdgeHttpClient {
|
|
|
157
206
|
//
|
|
158
207
|
|
|
159
208
|
public async recoverIdentity(
|
|
209
|
+
ctx: Context,
|
|
160
210
|
body: RecoverIdentityRequest,
|
|
161
|
-
args?:
|
|
211
|
+
args?: EdgeHttpCallArgs,
|
|
162
212
|
): Promise<RecoverIdentityResponseBody> {
|
|
163
|
-
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' });
|
|
164
214
|
}
|
|
165
215
|
|
|
166
216
|
//
|
|
@@ -168,11 +218,12 @@ export class EdgeHttpClient {
|
|
|
168
218
|
//
|
|
169
219
|
|
|
170
220
|
public async joinSpaceByInvitation(
|
|
221
|
+
ctx: Context,
|
|
171
222
|
spaceId: SpaceId,
|
|
172
223
|
body: JoinSpaceRequest,
|
|
173
|
-
args?:
|
|
224
|
+
args?: EdgeHttpCallArgs,
|
|
174
225
|
): Promise<JoinSpaceResponseBody> {
|
|
175
|
-
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' });
|
|
176
227
|
}
|
|
177
228
|
|
|
178
229
|
//
|
|
@@ -180,18 +231,19 @@ export class EdgeHttpClient {
|
|
|
180
231
|
//
|
|
181
232
|
|
|
182
233
|
public async initiateOAuthFlow(
|
|
234
|
+
ctx: Context,
|
|
183
235
|
body: InitiateOAuthFlowRequest,
|
|
184
|
-
args?:
|
|
236
|
+
args?: EdgeHttpCallArgs,
|
|
185
237
|
): Promise<InitiateOAuthFlowResponse> {
|
|
186
|
-
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' });
|
|
187
239
|
}
|
|
188
240
|
|
|
189
241
|
//
|
|
190
242
|
// Spaces
|
|
191
243
|
//
|
|
192
244
|
|
|
193
|
-
async createSpace(body: CreateSpaceRequest, args?:
|
|
194
|
-
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' });
|
|
195
247
|
}
|
|
196
248
|
|
|
197
249
|
//
|
|
@@ -199,13 +251,16 @@ export class EdgeHttpClient {
|
|
|
199
251
|
//
|
|
200
252
|
|
|
201
253
|
public async queryQueue(
|
|
254
|
+
ctx: Context,
|
|
202
255
|
subspaceTag: string,
|
|
203
256
|
spaceId: SpaceId,
|
|
204
|
-
query: QueueQuery,
|
|
205
|
-
args?:
|
|
206
|
-
): Promise<
|
|
207
|
-
const
|
|
257
|
+
query: FeedProtocol.QueueQuery,
|
|
258
|
+
args?: EdgeHttpCallArgs,
|
|
259
|
+
): Promise<EdgeQueryQueueResponse> {
|
|
260
|
+
const queueId = query.queueIds?.[0];
|
|
261
|
+
invariant(queueId, 'queueId required');
|
|
208
262
|
return this._call(
|
|
263
|
+
ctx,
|
|
209
264
|
createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
|
|
210
265
|
after: query.after,
|
|
211
266
|
before: query.before,
|
|
@@ -221,13 +276,14 @@ export class EdgeHttpClient {
|
|
|
221
276
|
}
|
|
222
277
|
|
|
223
278
|
public async insertIntoQueue(
|
|
279
|
+
ctx: Context,
|
|
224
280
|
subspaceTag: string,
|
|
225
281
|
spaceId: SpaceId,
|
|
226
282
|
queueId: ObjectId,
|
|
227
283
|
objects: unknown[],
|
|
228
|
-
args?:
|
|
284
|
+
args?: EdgeHttpCallArgs,
|
|
229
285
|
): Promise<void> {
|
|
230
|
-
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), {
|
|
231
287
|
...args,
|
|
232
288
|
body: { objects },
|
|
233
289
|
method: 'POST',
|
|
@@ -235,13 +291,15 @@ export class EdgeHttpClient {
|
|
|
235
291
|
}
|
|
236
292
|
|
|
237
293
|
public async deleteFromQueue(
|
|
294
|
+
ctx: Context,
|
|
238
295
|
subspaceTag: string,
|
|
239
296
|
spaceId: SpaceId,
|
|
240
297
|
queueId: ObjectId,
|
|
241
298
|
objectIds: ObjectId[],
|
|
242
|
-
args?:
|
|
299
|
+
args?: EdgeHttpCallArgs,
|
|
243
300
|
): Promise<void> {
|
|
244
301
|
return this._call(
|
|
302
|
+
ctx,
|
|
245
303
|
createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
|
|
246
304
|
ids: objectIds.join(','),
|
|
247
305
|
}),
|
|
@@ -257,15 +315,17 @@ export class EdgeHttpClient {
|
|
|
257
315
|
//
|
|
258
316
|
|
|
259
317
|
public async uploadFunction(
|
|
318
|
+
ctx: Context,
|
|
260
319
|
pathParts: { functionId?: string },
|
|
261
320
|
body: UploadFunctionRequest,
|
|
262
|
-
args?:
|
|
321
|
+
args?: EdgeHttpCallArgs,
|
|
263
322
|
): Promise<UploadFunctionResponseBody> {
|
|
264
323
|
const formData = new FormData();
|
|
265
324
|
formData.append('name', body.name ?? '');
|
|
266
325
|
formData.append('version', body.version);
|
|
267
326
|
formData.append('ownerPublicKey', body.ownerPublicKey);
|
|
268
327
|
formData.append('entryPoint', body.entryPoint);
|
|
328
|
+
body.runtime && formData.append('runtime', body.runtime);
|
|
269
329
|
for (const [filename, content] of Object.entries(body.assets)) {
|
|
270
330
|
formData.append(
|
|
271
331
|
'assets',
|
|
@@ -275,7 +335,7 @@ export class EdgeHttpClient {
|
|
|
275
335
|
}
|
|
276
336
|
|
|
277
337
|
const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
|
|
278
|
-
return this._call(new URL(path, this.baseUrl), {
|
|
338
|
+
return this._call(ctx, new URL(path, this.baseUrl), {
|
|
279
339
|
...args,
|
|
280
340
|
body: formData,
|
|
281
341
|
method: 'PUT',
|
|
@@ -283,11 +343,12 @@ export class EdgeHttpClient {
|
|
|
283
343
|
});
|
|
284
344
|
}
|
|
285
345
|
|
|
286
|
-
public async listFunctions(args?:
|
|
287
|
-
return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
|
|
346
|
+
public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
|
|
347
|
+
return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
|
|
288
348
|
}
|
|
289
349
|
|
|
290
350
|
public async invokeFunction(
|
|
351
|
+
ctx: Context,
|
|
291
352
|
params: {
|
|
292
353
|
functionId: string;
|
|
293
354
|
version?: string;
|
|
@@ -296,7 +357,7 @@ export class EdgeHttpClient {
|
|
|
296
357
|
subrequestsLimit?: number;
|
|
297
358
|
},
|
|
298
359
|
input: unknown,
|
|
299
|
-
args?:
|
|
360
|
+
args?: EdgeHttpCallArgs,
|
|
300
361
|
): Promise<any> {
|
|
301
362
|
const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
|
|
302
363
|
if (params.version) {
|
|
@@ -312,11 +373,10 @@ export class EdgeHttpClient {
|
|
|
312
373
|
url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
|
|
313
374
|
}
|
|
314
375
|
|
|
315
|
-
return this._call(url, {
|
|
376
|
+
return this._call(ctx, url, {
|
|
316
377
|
...args,
|
|
317
378
|
body: input,
|
|
318
379
|
method: 'POST',
|
|
319
|
-
rawResponse: true,
|
|
320
380
|
});
|
|
321
381
|
}
|
|
322
382
|
|
|
@@ -325,12 +385,13 @@ export class EdgeHttpClient {
|
|
|
325
385
|
//
|
|
326
386
|
|
|
327
387
|
public async executeWorkflow(
|
|
388
|
+
ctx: Context,
|
|
328
389
|
spaceId: SpaceId,
|
|
329
390
|
graphId: ObjectId,
|
|
330
391
|
input: any,
|
|
331
|
-
args?:
|
|
392
|
+
args?: EdgeHttpCallArgs,
|
|
332
393
|
): Promise<ExecuteWorkflowResponseBody> {
|
|
333
|
-
return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
|
|
394
|
+
return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
|
|
334
395
|
...args,
|
|
335
396
|
body: input,
|
|
336
397
|
method: 'POST',
|
|
@@ -341,8 +402,62 @@ export class EdgeHttpClient {
|
|
|
341
402
|
// Triggers
|
|
342
403
|
//
|
|
343
404
|
|
|
344
|
-
public async getCronTriggers(spaceId: SpaceId) {
|
|
345
|
-
return this._call(new URL(`/
|
|
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
|
+
});
|
|
346
461
|
}
|
|
347
462
|
|
|
348
463
|
//
|
|
@@ -350,30 +465,91 @@ export class EdgeHttpClient {
|
|
|
350
465
|
//
|
|
351
466
|
|
|
352
467
|
public async importBundle(
|
|
353
|
-
|
|
468
|
+
ctx: Context,
|
|
469
|
+
spaceId: SpaceId,
|
|
354
470
|
body: ImportBundleRequest,
|
|
355
|
-
args?:
|
|
471
|
+
args?: EdgeHttpCallArgs,
|
|
356
472
|
): Promise<void> {
|
|
357
|
-
return this._call(new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
|
|
473
|
+
return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
|
|
358
474
|
}
|
|
359
475
|
|
|
360
476
|
public async exportBundle(
|
|
477
|
+
ctx: Context,
|
|
361
478
|
spaceId: SpaceId,
|
|
362
479
|
body: ExportBundleRequest,
|
|
363
|
-
args?:
|
|
480
|
+
args?: EdgeHttpCallArgs,
|
|
364
481
|
): Promise<ExportBundleResponse> {
|
|
365
|
-
return this._call(new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
|
|
482
|
+
return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
|
|
366
483
|
...args,
|
|
367
484
|
body,
|
|
368
485
|
method: 'POST',
|
|
369
486
|
});
|
|
370
487
|
}
|
|
371
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
|
+
|
|
372
548
|
//
|
|
373
549
|
// Internal
|
|
374
550
|
//
|
|
375
551
|
|
|
376
|
-
private async _fetch<T>(url: URL,
|
|
552
|
+
private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
|
|
377
553
|
return Function.pipe(
|
|
378
554
|
HttpClient.get(url),
|
|
379
555
|
withLogging,
|
|
@@ -381,58 +557,67 @@ export class EdgeHttpClient {
|
|
|
381
557
|
Effect.provide(FetchHttpClient.layer),
|
|
382
558
|
Effect.provide(HttpConfig.default),
|
|
383
559
|
Effect.withSpan('EdgeHttpClient'),
|
|
384
|
-
|
|
560
|
+
runAndForwardErrors,
|
|
385
561
|
) as T;
|
|
386
562
|
}
|
|
387
563
|
|
|
388
564
|
// TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
|
|
389
|
-
private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
|
|
565
|
+
private async _call<T>(ctx: Context, url: URL, args: EdgeHttpRequestArgs): Promise<T> {
|
|
390
566
|
const shouldRetry = createRetryHandler(args);
|
|
391
|
-
const requestContext = args.context ?? new Context();
|
|
392
567
|
log('fetch', { url, request: args.body });
|
|
393
568
|
|
|
569
|
+
const traceHeaders = getTraceHeaders(ctx);
|
|
570
|
+
|
|
394
571
|
let handledAuth = false;
|
|
572
|
+
const tryCount = 1;
|
|
395
573
|
while (true) {
|
|
396
574
|
let processingError: EdgeCallFailedError | undefined = undefined;
|
|
397
|
-
let retryAfterHeaderValue: number = Number.NaN;
|
|
398
575
|
try {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const body = (await response.json()) as EdgeHttpResponse<T>;
|
|
404
|
-
|
|
405
|
-
if (args.rawResponse) {
|
|
406
|
-
return body as any;
|
|
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);
|
|
407
580
|
}
|
|
581
|
+
}
|
|
408
582
|
|
|
583
|
+
const request = createRequest(args, this._authHeader, traceHeaders, this._clientTag);
|
|
584
|
+
log('call edge', { url, tryCount, authHeader: !!this._authHeader });
|
|
585
|
+
const response = await fetch(url, request);
|
|
586
|
+
|
|
587
|
+
if (response.ok) {
|
|
588
|
+
const body = await response.clone().json();
|
|
589
|
+
invariant(body, 'Expected body to be present');
|
|
409
590
|
if (!('success' in body)) {
|
|
410
591
|
return body;
|
|
411
592
|
}
|
|
412
|
-
|
|
413
593
|
if (body.success) {
|
|
414
594
|
return body.data;
|
|
415
595
|
}
|
|
416
|
-
|
|
417
|
-
log.warn('unsuccessful edge response', { url, body });
|
|
418
|
-
if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
|
|
419
|
-
processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
|
|
420
|
-
} else if (body.errorData) {
|
|
421
|
-
processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
|
|
422
|
-
}
|
|
423
596
|
} else if (response.status === 401 && !handledAuth) {
|
|
424
597
|
this._authHeader = await this._handleUnauthorized(response);
|
|
425
598
|
handledAuth = true;
|
|
426
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);
|
|
427
611
|
} else {
|
|
612
|
+
invariant(!response.ok, 'Expected response to not be ok.');
|
|
428
613
|
processingError = await EdgeCallFailedError.fromHttpFailure(response);
|
|
429
614
|
}
|
|
430
615
|
} catch (error: any) {
|
|
431
616
|
processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
|
|
432
617
|
}
|
|
433
618
|
|
|
434
|
-
if (processingError?.isRetryable && (await shouldRetry(
|
|
435
|
-
log('retrying edge request', { url, processingError });
|
|
619
|
+
if (processingError?.isRetryable && (await shouldRetry(ctx, processingError.retryAfterMs))) {
|
|
620
|
+
log.verbose('retrying edge request', { url, processingError });
|
|
436
621
|
} else {
|
|
437
622
|
throw processingError!;
|
|
438
623
|
}
|
|
@@ -453,6 +638,8 @@ export class EdgeHttpClient {
|
|
|
453
638
|
const createRequest = (
|
|
454
639
|
{ method, body, json = true }: EdgeHttpRequestArgs,
|
|
455
640
|
authHeader: string | undefined,
|
|
641
|
+
traceHeaders?: Record<string, string>,
|
|
642
|
+
clientTag?: string,
|
|
456
643
|
): RequestInit => {
|
|
457
644
|
let requestBody: BodyInit | undefined;
|
|
458
645
|
const headers: HeadersInit = {};
|
|
@@ -472,6 +659,14 @@ const createRequest = (
|
|
|
472
659
|
headers['Authorization'] = authHeader;
|
|
473
660
|
}
|
|
474
661
|
|
|
662
|
+
if (traceHeaders) {
|
|
663
|
+
Object.assign(headers, traceHeaders);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (clientTag) {
|
|
667
|
+
headers[EDGE_CLIENT_TAG_HEADER] = clientTag;
|
|
668
|
+
}
|
|
669
|
+
|
|
475
670
|
return {
|
|
476
671
|
method,
|
|
477
672
|
body: requestBody,
|
|
@@ -479,6 +674,22 @@ const createRequest = (
|
|
|
479
674
|
};
|
|
480
675
|
};
|
|
481
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
|
+
|
|
482
693
|
/**
|
|
483
694
|
* @deprecated
|
|
484
695
|
*/
|
|
@@ -491,7 +702,7 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
|
|
|
491
702
|
const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
|
|
492
703
|
const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
|
|
493
704
|
const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
|
|
494
|
-
return async (ctx: Context, retryAfter
|
|
705
|
+
return async (ctx: Context, retryAfter?: number) => {
|
|
495
706
|
if (++retries > maxRetries || ctx.disposed) {
|
|
496
707
|
return false;
|
|
497
708
|
}
|
|
@@ -513,3 +724,45 @@ const getFileMimeType = (filename: string) =>
|
|
|
513
724
|
: filename.endsWith('.wasm')
|
|
514
725
|
? 'application/wasm'
|
|
515
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
|
+
};
|