@dxos/edge-client 0.9.0 → 0.9.1-staging.ee54ba693a
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/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/cors-proxy.mjs +1 -0
- package/dist/lib/neutral/edge-ws-muxer.mjs +1 -0
- package/dist/lib/neutral/index.mjs +111 -35
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/service/index.mjs +134 -0
- package/dist/lib/neutral/service/index.mjs.map +7 -0
- package/dist/lib/neutral/testing/index.mjs +2 -1
- package/dist/lib/neutral/testing/index.mjs.map +3 -3
- package/dist/types/src/auth.d.ts.map +1 -1
- package/dist/types/src/browser-rendering.d.ts +28 -0
- package/dist/types/src/browser-rendering.d.ts.map +1 -0
- package/dist/types/src/edge-ai-http-client.d.ts +39 -0
- package/dist/types/src/edge-ai-http-client.d.ts.map +1 -1
- package/dist/types/src/edge-ai-http-client.test.d.ts +2 -0
- package/dist/types/src/edge-ai-http-client.test.d.ts.map +1 -0
- package/dist/types/src/edge-client.d.ts +3 -2
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +19 -4
- package/dist/types/src/edge-http-client.d.ts.map +1 -1
- package/dist/types/src/edge-identity.d.ts +5 -1
- package/dist/types/src/edge-identity.d.ts.map +1 -1
- package/dist/types/src/hub-http-client.d.ts +8 -1
- package/dist/types/src/hub-http-client.d.ts.map +1 -1
- package/dist/types/src/service/Image.d.ts +25 -0
- package/dist/types/src/service/Image.d.ts.map +1 -0
- package/dist/types/src/service/edge-service.d.ts +36 -0
- package/dist/types/src/service/edge-service.d.ts.map +1 -0
- package/dist/types/src/service/edge-service.test.d.ts +2 -0
- package/dist/types/src/service/edge-service.test.d.ts.map +1 -0
- package/dist/types/src/service/image-service.e2e.test.d.ts +2 -0
- package/dist/types/src/service/image-service.e2e.test.d.ts.map +1 -0
- package/dist/types/src/service/index.d.ts +3 -0
- package/dist/types/src/service/index.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -17
- package/src/auth.ts +6 -6
- package/src/base-http-client.ts +1 -1
- package/src/browser-rendering.ts +39 -0
- package/src/edge-ai-http-client.test.ts +37 -0
- package/src/edge-ai-http-client.ts +87 -2
- package/src/edge-client.test.ts +1 -1
- package/src/edge-client.ts +9 -7
- package/src/edge-http-client.ts +25 -10
- package/src/edge-identity.ts +5 -1
- package/src/edge-ws-connection.ts +1 -1
- package/src/edge-ws-muxer.test.ts +1 -1
- package/src/hub-http-client.ts +20 -0
- package/src/service/Image.ts +61 -0
- package/src/service/edge-service.test.ts +151 -0
- package/src/service/edge-service.ts +139 -0
- package/src/service/image-service.e2e.test.ts +53 -0
- package/src/service/index.ts +7 -0
- package/src/testing/test-utils.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/edge-client",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
4
4
|
"description": "EDGE Client",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"types": "./dist/types/src/edge-ws-muxer.d.ts",
|
|
29
29
|
"default": "./dist/lib/neutral/edge-ws-muxer.mjs"
|
|
30
30
|
},
|
|
31
|
+
"./service": {
|
|
32
|
+
"source": "./src/service/index.ts",
|
|
33
|
+
"types": "./dist/types/src/service/index.d.ts",
|
|
34
|
+
"default": "./dist/lib/neutral/service/index.mjs"
|
|
35
|
+
},
|
|
31
36
|
"./testing": {
|
|
32
37
|
"source": "./src/testing/index.ts",
|
|
33
38
|
"types": "./dist/types/src/testing/index.d.ts",
|
|
@@ -41,29 +46,29 @@
|
|
|
41
46
|
"README.md"
|
|
42
47
|
],
|
|
43
48
|
"dependencies": {
|
|
44
|
-
"@effect/platform": "0.96.
|
|
49
|
+
"@effect/platform": "0.96.2",
|
|
45
50
|
"@opentelemetry/api": "^1.9.1",
|
|
46
51
|
"isomorphic-ws": "^5.0.0",
|
|
47
52
|
"ws": "^8.17.1",
|
|
48
|
-
"@dxos/async": "0.9.
|
|
49
|
-
"@dxos/context": "0.9.
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/keyring": "0.9.
|
|
56
|
-
"@dxos/keys": "0.9.
|
|
57
|
-
"@dxos/log": "0.9.
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/protocols": "0.9.
|
|
53
|
+
"@dxos/async": "0.9.1-staging.ee54ba693a",
|
|
54
|
+
"@dxos/context": "0.9.1-staging.ee54ba693a",
|
|
55
|
+
"@dxos/crypto": "0.9.1-staging.ee54ba693a",
|
|
56
|
+
"@dxos/effect": "0.9.1-staging.ee54ba693a",
|
|
57
|
+
"@dxos/invariant": "0.9.1-staging.ee54ba693a",
|
|
58
|
+
"@dxos/credentials": "0.9.1-staging.ee54ba693a",
|
|
59
|
+
"@dxos/errors": "0.9.1-staging.ee54ba693a",
|
|
60
|
+
"@dxos/keyring": "0.9.1-staging.ee54ba693a",
|
|
61
|
+
"@dxos/keys": "0.9.1-staging.ee54ba693a",
|
|
62
|
+
"@dxos/log": "0.9.1-staging.ee54ba693a",
|
|
63
|
+
"@dxos/node-std": "0.9.1-staging.ee54ba693a",
|
|
64
|
+
"@dxos/util": "0.9.1-staging.ee54ba693a",
|
|
65
|
+
"@dxos/protocols": "0.9.1-staging.ee54ba693a"
|
|
61
66
|
},
|
|
62
67
|
"devDependencies": {
|
|
63
|
-
"@dxos/test-utils": "0.9.
|
|
68
|
+
"@dxos/test-utils": "0.9.1-staging.ee54ba693a"
|
|
64
69
|
},
|
|
65
70
|
"peerDependencies": {
|
|
66
|
-
"effect": "3.21.
|
|
71
|
+
"effect": "3.21.4"
|
|
67
72
|
},
|
|
68
73
|
"publishConfig": {
|
|
69
74
|
"access": "public"
|
package/src/auth.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { createCredential, signPresentation } from '@dxos/credentials';
|
|
5
|
+
import { createCredential, createDidFromIdentityKey, signPresentation } from '@dxos/credentials';
|
|
6
6
|
import { type Signer } from '@dxos/crypto';
|
|
7
7
|
import { invariant } from '@dxos/invariant';
|
|
8
8
|
import { Keyring } from '@dxos/keyring';
|
|
9
|
-
import { PublicKey } from '@dxos/keys';
|
|
9
|
+
import { IdentityDid, PublicKey } from '@dxos/keys';
|
|
10
10
|
import { type Chain, type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
11
11
|
|
|
12
12
|
import type { EdgeIdentity } from './edge-identity';
|
|
@@ -16,7 +16,7 @@ import type { EdgeIdentity } from './edge-identity';
|
|
|
16
16
|
*/
|
|
17
17
|
export const createDeviceEdgeIdentity = async (signer: Signer, key: PublicKey): Promise<EdgeIdentity> => {
|
|
18
18
|
return {
|
|
19
|
-
|
|
19
|
+
identityDid: await createDidFromIdentityKey(key),
|
|
20
20
|
peerKey: key.toHex(),
|
|
21
21
|
presentCredentials: async ({ challenge }) => {
|
|
22
22
|
return signPresentation({
|
|
@@ -68,7 +68,7 @@ export const createChainEdgeIdentity = async (
|
|
|
68
68
|
];
|
|
69
69
|
|
|
70
70
|
return {
|
|
71
|
-
|
|
71
|
+
identityDid: await createDidFromIdentityKey(identityKey),
|
|
72
72
|
peerKey: peerKey.toHex(),
|
|
73
73
|
presentCredentials: async ({ challenge }) => {
|
|
74
74
|
// TODO: make chain required after device invitation flow update release
|
|
@@ -126,10 +126,10 @@ export const createTestHaloEdgeIdentity = async (
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
export const createStubEdgeIdentity = (): EdgeIdentity => {
|
|
129
|
-
const identityKey = PublicKey.random();
|
|
130
129
|
const deviceKey = PublicKey.random();
|
|
131
130
|
return {
|
|
132
|
-
|
|
131
|
+
// Random placeholder DID — the stub never authenticates or connects; a real identity replaces it.
|
|
132
|
+
identityDid: IdentityDid.random(),
|
|
133
133
|
peerKey: deviceKey.toHex(),
|
|
134
134
|
presentCredentials: async () => {
|
|
135
135
|
throw new Error('Stub identity does not support authentication.');
|
package/src/base-http-client.ts
CHANGED
|
@@ -71,7 +71,7 @@ export abstract class BaseHttpClient {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
setIdentity(identity: EdgeIdentity): void {
|
|
74
|
-
if (this._edgeIdentity?.
|
|
74
|
+
if (this._edgeIdentity?.identityDid !== identity.identityDid || this._edgeIdentity?.peerKey !== identity.peerKey) {
|
|
75
75
|
this._edgeIdentity = identity;
|
|
76
76
|
this._authHeader = undefined;
|
|
77
77
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Context from 'effect/Context';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
|
|
8
|
+
import { type EdgeHttpClient } from './edge-http-client';
|
|
9
|
+
|
|
10
|
+
/** Request body for EDGE `/ai/browser-rendering/markdown` (ai-service Browser Run markdown quick action). */
|
|
11
|
+
export const MarkdownRequest = Schema.Struct({
|
|
12
|
+
url: Schema.optional(Schema.String),
|
|
13
|
+
html: Schema.optional(Schema.String),
|
|
14
|
+
gotoOptions: Schema.optional(
|
|
15
|
+
Schema.Struct({
|
|
16
|
+
waitUntil: Schema.optional(Schema.Literal('load', 'domcontentloaded', 'networkidle0', 'networkidle2')),
|
|
17
|
+
timeout: Schema.optional(Schema.Number),
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
rejectRequestPattern: Schema.optional(Schema.Array(Schema.String)),
|
|
21
|
+
userAgent: Schema.optional(Schema.String),
|
|
22
|
+
waitForSelector: Schema.optional(Schema.String),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type MarkdownRequest = Schema.Schema.Type<typeof MarkdownRequest>;
|
|
26
|
+
|
|
27
|
+
/** JSON body returned by Cloudflare Browser Run markdown quick action. */
|
|
28
|
+
export const MarkdownResponse = Schema.Struct({
|
|
29
|
+
success: Schema.Boolean,
|
|
30
|
+
result: Schema.String,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type MarkdownResponse = Schema.Schema.Type<typeof MarkdownResponse>;
|
|
34
|
+
|
|
35
|
+
/** Authenticated EDGE HTTP client for operation handlers (e.g. browser markdown fetch). */
|
|
36
|
+
export class EdgeHttpClientService extends Context.Tag('@dxos/edge-client/EdgeHttpClientService')<
|
|
37
|
+
EdgeHttpClientService,
|
|
38
|
+
EdgeHttpClient
|
|
39
|
+
>() {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { patchAnthropicMessagesRequestBody } from './edge-ai-http-client';
|
|
8
|
+
|
|
9
|
+
describe('patchAnthropicMessagesRequestBody', () => {
|
|
10
|
+
test('adds eager_input_streaming to user-defined tools', ({ expect }) => {
|
|
11
|
+
const body = JSON.stringify({
|
|
12
|
+
model: 'claude-sonnet-4-6',
|
|
13
|
+
stream: true,
|
|
14
|
+
tools: [
|
|
15
|
+
{
|
|
16
|
+
name: 'delegate-task',
|
|
17
|
+
description: 'Delegate work',
|
|
18
|
+
input_schema: { type: 'object', properties: { title: { type: 'string' } } },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'bash',
|
|
22
|
+
type: 'bash_20250124',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const patched = JSON.parse(patchAnthropicMessagesRequestBody(body) as string);
|
|
28
|
+
|
|
29
|
+
expect(patched.tools[0]).toMatchObject({ eager_input_streaming: true });
|
|
30
|
+
expect(patched.tools[1]).not.toHaveProperty('eager_input_streaming');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('leaves non-json bodies unchanged', ({ expect }) => {
|
|
34
|
+
const body = 'not-json';
|
|
35
|
+
expect(patchAnthropicMessagesRequestBody(body)).toBe(body);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -31,11 +31,72 @@ export class ByokError extends BaseError.extend('ByokError', 'BYOK authenticatio
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Thrown by {@link EdgeAiHttpClient} when EDGE rejects an AI request with 429 because the
|
|
36
|
+
* authenticated profile exceeded a metering limit. Wrapped as the `cause` of an
|
|
37
|
+
* {@link HttpClientError.ResponseError} so it survives `@effect/ai`'s error mapping.
|
|
38
|
+
*/
|
|
39
|
+
export class UsageQuotaExceededError extends BaseError.extend('UsageQuotaExceededError', 'Usage quota exceeded') {}
|
|
40
|
+
|
|
34
41
|
/**
|
|
35
42
|
* Copy pasted from https://github.com/Effect-TS/effect/blob/main/packages/platform/src/internal/fetchHttpClient.ts
|
|
36
43
|
*/
|
|
37
44
|
export const requestInitTagKey = '@effect/platform/FetchHttpClient/FetchOptions';
|
|
38
45
|
|
|
46
|
+
type AnthropicMessagesPayload = {
|
|
47
|
+
tools?: ReadonlyArray<Record<string, unknown>>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isUserDefinedAnthropicTool = (tool: Record<string, unknown>): boolean =>
|
|
51
|
+
tool.input_schema != null && typeof tool.input_schema === 'object';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enables Anthropic fine-grained tool input streaming for client-defined tools.
|
|
55
|
+
* Provider tools (bash, web_search, etc.) are left unchanged.
|
|
56
|
+
*/
|
|
57
|
+
export const patchAnthropicMessagesRequestBody = (body: BodyInit | undefined): BodyInit | undefined => {
|
|
58
|
+
if (body == null) {
|
|
59
|
+
return body;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const decodeBody = (): string | undefined => {
|
|
63
|
+
if (typeof body === 'string') {
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
if (body instanceof Uint8Array) {
|
|
67
|
+
return new TextDecoder().decode(body);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const text = decodeBody();
|
|
73
|
+
if (text == null) {
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const payload = JSON.parse(text) as AnthropicMessagesPayload;
|
|
79
|
+
if (!Array.isArray(payload.tools)) {
|
|
80
|
+
return body;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
payload.tools = payload.tools.map((tool) =>
|
|
84
|
+
isUserDefinedAnthropicTool(tool) ? { ...tool, eager_input_streaming: true } : tool,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const patched = JSON.stringify(payload);
|
|
88
|
+
return typeof body === 'string' ? patched : new TextEncoder().encode(patched);
|
|
89
|
+
} catch {
|
|
90
|
+
return body;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const readStreamBody = (stream: ReadableStream<Uint8Array>): Effect.Effect<string | undefined> =>
|
|
95
|
+
Effect.promise(async () => {
|
|
96
|
+
const response = new Response(stream);
|
|
97
|
+
return await response.text();
|
|
98
|
+
});
|
|
99
|
+
|
|
39
100
|
/**
|
|
40
101
|
* An `@effect/platform` {@link HttpClient.HttpClient} that routes requests through the
|
|
41
102
|
* authenticated EDGE AI endpoint via {@link EdgeHttpClient.anthropicAiRequest}, instead of
|
|
@@ -67,7 +128,7 @@ export class EdgeAiHttpClient {
|
|
|
67
128
|
...options,
|
|
68
129
|
method: request.method,
|
|
69
130
|
headers,
|
|
70
|
-
body,
|
|
131
|
+
body: patchAnthropicMessagesRequestBody(body),
|
|
71
132
|
signal,
|
|
72
133
|
}),
|
|
73
134
|
),
|
|
@@ -107,6 +168,27 @@ export class EdgeAiHttpClient {
|
|
|
107
168
|
),
|
|
108
169
|
);
|
|
109
170
|
}
|
|
171
|
+
// Platform quota (not BYOK): reserve/commit rejected the request before upstream AI.
|
|
172
|
+
if (!carriedByok && response.status === 429) {
|
|
173
|
+
return Effect.tryPromise({
|
|
174
|
+
try: () => response.clone().json() as Promise<{ error?: { message?: string } } | undefined>,
|
|
175
|
+
catch: () => undefined,
|
|
176
|
+
}).pipe(
|
|
177
|
+
Effect.orElseSucceed(() => undefined),
|
|
178
|
+
Effect.flatMap((body) =>
|
|
179
|
+
Effect.fail(
|
|
180
|
+
new HttpClientError.ResponseError({
|
|
181
|
+
request,
|
|
182
|
+
response: httpResponse,
|
|
183
|
+
reason: 'StatusCode',
|
|
184
|
+
cause: new UsageQuotaExceededError({
|
|
185
|
+
message: body?.error?.message,
|
|
186
|
+
}),
|
|
187
|
+
}),
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
110
192
|
return Effect.succeed(httpResponse);
|
|
111
193
|
}),
|
|
112
194
|
);
|
|
@@ -118,7 +200,10 @@ export class EdgeAiHttpClient {
|
|
|
118
200
|
case 'FormData':
|
|
119
201
|
return send(request.body.formData);
|
|
120
202
|
case 'Stream':
|
|
121
|
-
return Stream.toReadableStreamEffect(request.body.stream).pipe(
|
|
203
|
+
return Stream.toReadableStreamEffect(request.body.stream).pipe(
|
|
204
|
+
Effect.flatMap((readable) => readStreamBody(readable)),
|
|
205
|
+
Effect.flatMap((text) => send(text)),
|
|
206
|
+
);
|
|
122
207
|
}
|
|
123
208
|
|
|
124
209
|
return send(undefined);
|
package/src/edge-client.test.ts
CHANGED
|
@@ -158,7 +158,7 @@ describe('EdgeClient', () => {
|
|
|
158
158
|
|
|
159
159
|
const textMessage = (message: string, source?: EdgeIdentity) =>
|
|
160
160
|
protocol.createMessage(TextMessageSchema, {
|
|
161
|
-
source: source && { peerKey: source.peerKey,
|
|
161
|
+
source: source && { peerKey: source.peerKey, identityDid: source.identityDid },
|
|
162
162
|
payload: { message },
|
|
163
163
|
});
|
|
164
164
|
|
package/src/edge-client.ts
CHANGED
|
@@ -43,7 +43,8 @@ export type MessengerConfig = {
|
|
|
43
43
|
export interface EdgeConnection extends Required<Lifecycle> {
|
|
44
44
|
statusChanged: Event<EdgeStatus>;
|
|
45
45
|
get info(): any;
|
|
46
|
-
|
|
46
|
+
/** Identity DID (`did:halo:…`) of the connected identity. */
|
|
47
|
+
get identityDid(): string;
|
|
47
48
|
get peerKey(): string;
|
|
48
49
|
get isOpen(): boolean;
|
|
49
50
|
get status(): EdgeStatus;
|
|
@@ -88,7 +89,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
88
89
|
return {
|
|
89
90
|
open: this.isOpen,
|
|
90
91
|
status: this.status,
|
|
91
|
-
identity: this._identity.
|
|
92
|
+
identity: this._identity.identityDid,
|
|
92
93
|
device: this._identity.peerKey,
|
|
93
94
|
};
|
|
94
95
|
}
|
|
@@ -108,8 +109,8 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
get
|
|
112
|
-
return this._identity.
|
|
112
|
+
get identityDid() {
|
|
113
|
+
return this._identity.identityDid;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
get peerKey() {
|
|
@@ -117,7 +118,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
setIdentity(identity: EdgeIdentity) {
|
|
120
|
-
if (identity.
|
|
121
|
+
if (identity.identityDid !== this._identity.identityDid || identity.peerKey !== this._identity.peerKey) {
|
|
121
122
|
log('Edge identity changed', { identity, oldIdentity: this._identity });
|
|
122
123
|
this._identity = identity;
|
|
123
124
|
this._closeCurrentConnection(new EdgeIdentityChangedError());
|
|
@@ -139,9 +140,10 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
139
140
|
throw new EdgeConnectionClosedError();
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// DX-1059: sources are DID-only; validate against identityDid.
|
|
142
144
|
if (
|
|
143
145
|
message.source &&
|
|
144
|
-
(message.source.peerKey !== this._identity.peerKey || message.source.
|
|
146
|
+
(message.source.peerKey !== this._identity.peerKey || message.source.identityDid !== this.identityDid)
|
|
145
147
|
) {
|
|
146
148
|
throw new EdgeIdentityChangedError();
|
|
147
149
|
}
|
|
@@ -219,7 +221,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
const identity = this._identity;
|
|
222
|
-
const path = `/ws/${identity.
|
|
224
|
+
const path = `/ws/${identity.identityDid}/${identity.peerKey}`;
|
|
223
225
|
const protocolHeader = this._config.disableAuth ? undefined : await this._createAuthHeader(path);
|
|
224
226
|
if (this._identity !== identity) {
|
|
225
227
|
log('identity changed during auth header request');
|
package/src/edge-http-client.ts
CHANGED
|
@@ -11,7 +11,7 @@ import * as Function from 'effect/Function';
|
|
|
11
11
|
import { type Context } from '@dxos/context';
|
|
12
12
|
import { EffectEx } from '@dxos/effect';
|
|
13
13
|
import { invariant } from '@dxos/invariant';
|
|
14
|
-
import { type
|
|
14
|
+
import { type SpaceId } from '@dxos/keys';
|
|
15
15
|
import { log } from '@dxos/log';
|
|
16
16
|
import {
|
|
17
17
|
type CompleteOAuthRegistrationRequest,
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
type FeedProtocol,
|
|
29
29
|
type GetAgentStatusResponseBody,
|
|
30
30
|
type GetNotarizationResponseBody,
|
|
31
|
-
type GetPluginVersionsResponseBody,
|
|
32
31
|
type GetPluginsResponseBody,
|
|
33
32
|
type ImportBundleRequest,
|
|
34
33
|
type InitiateOAuthFlowRequest,
|
|
@@ -63,6 +62,12 @@ export type EdgeQueryQueueResponse = {
|
|
|
63
62
|
prevCursor?: string;
|
|
64
63
|
};
|
|
65
64
|
|
|
65
|
+
export type UploadPluginBundleRequest = {
|
|
66
|
+
slug: string;
|
|
67
|
+
version: string;
|
|
68
|
+
files: { path: string; content: string }[];
|
|
69
|
+
};
|
|
70
|
+
|
|
66
71
|
export type TriggersDispatcherStatus = {
|
|
67
72
|
isActive: boolean;
|
|
68
73
|
nextCronTaskRunTimestamp?: number;
|
|
@@ -112,10 +117,10 @@ export class EdgeHttpClient extends BaseHttpClient {
|
|
|
112
117
|
|
|
113
118
|
public getAgentStatus(
|
|
114
119
|
ctx: Context,
|
|
115
|
-
request: {
|
|
120
|
+
request: { ownerIdentityDid: string },
|
|
116
121
|
args?: EdgeHttpCallArgs,
|
|
117
122
|
): Promise<GetAgentStatusResponseBody> {
|
|
118
|
-
return this._call(ctx, new URL(`/users/${request.
|
|
123
|
+
return this._call(ctx, new URL(`/users/${request.ownerIdentityDid}/agent/status`, this.baseUrl), {
|
|
119
124
|
...args,
|
|
120
125
|
method: 'GET',
|
|
121
126
|
});
|
|
@@ -266,7 +271,10 @@ export class EdgeHttpClient extends BaseHttpClient {
|
|
|
266
271
|
const formData = new FormData();
|
|
267
272
|
formData.append('name', body.name ?? '');
|
|
268
273
|
formData.append('version', body.version);
|
|
269
|
-
|
|
274
|
+
// The function owner is the authenticated identity (edge requires ownerUri === presenter DID).
|
|
275
|
+
// Prefer the connected identity's DID; otherwise use the DID supplied on the request body.
|
|
276
|
+
const ownerUri = this._edgeIdentity?.identityDid ?? body.ownerUri;
|
|
277
|
+
formData.append('ownerUri', ownerUri);
|
|
270
278
|
formData.append('entryPoint', body.entryPoint);
|
|
271
279
|
body.runtime && formData.append('runtime', body.runtime);
|
|
272
280
|
for (const [filename, content] of Object.entries(body.assets)) {
|
|
@@ -379,14 +387,21 @@ export class EdgeHttpClient extends BaseHttpClient {
|
|
|
379
387
|
return this._call(ctx, new URL('/registry/plugins', this.baseUrl), { ...args, method: 'GET' });
|
|
380
388
|
}
|
|
381
389
|
|
|
382
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Uploads a built plugin bundle to the registry's R2-backed hosting. Authenticated
|
|
392
|
+
* with the caller's hub identity (verifiable presentation) — `setIdentity` must
|
|
393
|
+
* have been called. Returns the canonical `moduleUrl` (the hosted `manifest.json`).
|
|
394
|
+
*/
|
|
395
|
+
public async uploadPluginBundle(
|
|
383
396
|
ctx: Context,
|
|
384
|
-
|
|
397
|
+
request: UploadPluginBundleRequest,
|
|
385
398
|
args?: EdgeHttpCallArgs,
|
|
386
|
-
): Promise<
|
|
387
|
-
return this._call(ctx, new URL(
|
|
399
|
+
): Promise<{ moduleUrl: string }> {
|
|
400
|
+
return this._call(ctx, new URL('/registry/upload', this.baseUrl), {
|
|
401
|
+
body: request,
|
|
402
|
+
method: 'POST',
|
|
403
|
+
auth: true,
|
|
388
404
|
...args,
|
|
389
|
-
method: 'GET',
|
|
390
405
|
});
|
|
391
406
|
}
|
|
392
407
|
|
package/src/edge-identity.ts
CHANGED
|
@@ -8,7 +8,11 @@ import { type Presentation } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
|
8
8
|
|
|
9
9
|
export interface EdgeIdentity {
|
|
10
10
|
peerKey: string;
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Identity DID (`did:halo:…`) — the public identity segment of the edge WebSocket path.
|
|
13
|
+
* The router keys connections by the DID.
|
|
14
|
+
*/
|
|
15
|
+
identityDid: string;
|
|
12
16
|
/**
|
|
13
17
|
* Returns credential presentation issued by the identity key.
|
|
14
18
|
* Presentation must have the provided challenge.
|
|
@@ -35,7 +35,7 @@ describe('WebSocketMuxerTest', () => {
|
|
|
35
35
|
|
|
36
36
|
const textMessage = (message: string, source?: EdgeIdentity) =>
|
|
37
37
|
protocol.createMessage(TextMessageSchema, {
|
|
38
|
-
source: source && { peerKey: source.peerKey,
|
|
38
|
+
source: source && { peerKey: source.peerKey, identityDid: source.identityDid },
|
|
39
39
|
serviceId: 'test-service',
|
|
40
40
|
payload: { message },
|
|
41
41
|
});
|
package/src/hub-http-client.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type Context } from '@dxos/context';
|
|
|
6
6
|
import {
|
|
7
7
|
type CheckEmailExistsResponse,
|
|
8
8
|
type GetAccountResponse,
|
|
9
|
+
type GetProfileUsageResponse,
|
|
9
10
|
type IssueInvitationResponse,
|
|
10
11
|
type ListAccountInvitationsResponse,
|
|
11
12
|
type LoginRequest,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type ResendVerificationEmailResponse,
|
|
18
19
|
type ValidateInvitationCodeResponse,
|
|
19
20
|
} from '@dxos/protocols';
|
|
21
|
+
import { createUrl } from '@dxos/util';
|
|
20
22
|
|
|
21
23
|
import { BaseHttpClient, type BaseHttpClientOptions, type EdgeHttpCallArgs } from './base-http-client';
|
|
22
24
|
|
|
@@ -115,4 +117,22 @@ export class HubHttpClient extends BaseHttpClient {
|
|
|
115
117
|
): Promise<ResendVerificationEmailResponse> {
|
|
116
118
|
return this._call(ctx, new URL('/account/email/resend-verification', this.baseUrl), { ...args, method: 'POST' });
|
|
117
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Rolling-window usage and effective limits for the authenticated identity.
|
|
123
|
+
* Served from the per-user metering DO; optional `windowSeconds` defaults to the largest limit window.
|
|
124
|
+
*/
|
|
125
|
+
public async getProfileUsage(
|
|
126
|
+
ctx: Context,
|
|
127
|
+
query?: { windowSeconds?: number },
|
|
128
|
+
args?: EdgeHttpCallArgs,
|
|
129
|
+
): Promise<GetProfileUsageResponse> {
|
|
130
|
+
return this._call(
|
|
131
|
+
ctx,
|
|
132
|
+
createUrl(new URL('/api/metering/profile/usage', this.baseUrl), {
|
|
133
|
+
windowSeconds: query?.windowSeconds,
|
|
134
|
+
}),
|
|
135
|
+
{ ...args, method: 'GET' },
|
|
136
|
+
);
|
|
137
|
+
}
|
|
118
138
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
// Image service helpers built on the generic {@link EdgeServiceClient}. The
|
|
8
|
+
// worker exposes two contract-identical routes (`/upload` and `/thumbnail`,
|
|
9
|
+
// both returning `{ id, url }`); we mirror them so callers stay on whichever
|
|
10
|
+
// route they already use.
|
|
11
|
+
|
|
12
|
+
import * as Effect from 'effect/Effect';
|
|
13
|
+
import * as Schema from 'effect/Schema';
|
|
14
|
+
|
|
15
|
+
import { type EdgeServiceClient, type EdgeServiceError } from './edge-service';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default Composer image-service base URL — the Cloudflare Worker used in
|
|
19
|
+
* production. Override per-environment via the client's `baseUrl`.
|
|
20
|
+
*/
|
|
21
|
+
// TODO(burdon): Get from config.
|
|
22
|
+
export const DEFAULT_IMAGE_SERVICE_URL = 'https://image-service-main.dxos.workers.dev';
|
|
23
|
+
|
|
24
|
+
/** Hosted image returned by the service: a CDN `url` and its storage `id`. */
|
|
25
|
+
export const Result = Schema.Struct({
|
|
26
|
+
id: Schema.optional(Schema.String),
|
|
27
|
+
url: Schema.String,
|
|
28
|
+
});
|
|
29
|
+
export type Result = typeof Result.Type;
|
|
30
|
+
|
|
31
|
+
export type UploadOptions = {
|
|
32
|
+
/** Multipart field name. */
|
|
33
|
+
field?: string;
|
|
34
|
+
/** Upload filename. */
|
|
35
|
+
filename?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Store an image and return its hosted CDN URL (POST `/upload`). */
|
|
39
|
+
export const upload = (
|
|
40
|
+
client: EdgeServiceClient,
|
|
41
|
+
blob: Blob,
|
|
42
|
+
opts?: UploadOptions,
|
|
43
|
+
): Effect.Effect<Result, EdgeServiceError> => uploadToPath(client, blob, '/upload', opts);
|
|
44
|
+
|
|
45
|
+
/** Store an image and create a thumbnail, returning its hosted CDN URL (POST `/thumbnail`). */
|
|
46
|
+
export const thumbnail = (
|
|
47
|
+
client: EdgeServiceClient,
|
|
48
|
+
blob: Blob,
|
|
49
|
+
opts?: UploadOptions,
|
|
50
|
+
): Effect.Effect<Result, EdgeServiceError> => uploadToPath(client, blob, '/thumbnail', opts);
|
|
51
|
+
|
|
52
|
+
const uploadToPath = (
|
|
53
|
+
client: EdgeServiceClient,
|
|
54
|
+
blob: Blob,
|
|
55
|
+
path: string,
|
|
56
|
+
opts?: UploadOptions,
|
|
57
|
+
): Effect.Effect<Result, EdgeServiceError> => {
|
|
58
|
+
const form = new FormData();
|
|
59
|
+
form.append(opts?.field ?? 'file', blob, opts?.filename ?? 'image');
|
|
60
|
+
return client.postForm(path, form, Result);
|
|
61
|
+
};
|