@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.
Files changed (56) hide show
  1. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  2. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  3. package/dist/lib/neutral/cors-proxy.mjs +1 -0
  4. package/dist/lib/neutral/edge-ws-muxer.mjs +1 -0
  5. package/dist/lib/neutral/index.mjs +111 -35
  6. package/dist/lib/neutral/index.mjs.map +3 -3
  7. package/dist/lib/neutral/meta.json +1 -1
  8. package/dist/lib/neutral/service/index.mjs +134 -0
  9. package/dist/lib/neutral/service/index.mjs.map +7 -0
  10. package/dist/lib/neutral/testing/index.mjs +2 -1
  11. package/dist/lib/neutral/testing/index.mjs.map +3 -3
  12. package/dist/types/src/auth.d.ts.map +1 -1
  13. package/dist/types/src/browser-rendering.d.ts +28 -0
  14. package/dist/types/src/browser-rendering.d.ts.map +1 -0
  15. package/dist/types/src/edge-ai-http-client.d.ts +39 -0
  16. package/dist/types/src/edge-ai-http-client.d.ts.map +1 -1
  17. package/dist/types/src/edge-ai-http-client.test.d.ts +2 -0
  18. package/dist/types/src/edge-ai-http-client.test.d.ts.map +1 -0
  19. package/dist/types/src/edge-client.d.ts +3 -2
  20. package/dist/types/src/edge-client.d.ts.map +1 -1
  21. package/dist/types/src/edge-http-client.d.ts +19 -4
  22. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  23. package/dist/types/src/edge-identity.d.ts +5 -1
  24. package/dist/types/src/edge-identity.d.ts.map +1 -1
  25. package/dist/types/src/hub-http-client.d.ts +8 -1
  26. package/dist/types/src/hub-http-client.d.ts.map +1 -1
  27. package/dist/types/src/service/Image.d.ts +25 -0
  28. package/dist/types/src/service/Image.d.ts.map +1 -0
  29. package/dist/types/src/service/edge-service.d.ts +36 -0
  30. package/dist/types/src/service/edge-service.d.ts.map +1 -0
  31. package/dist/types/src/service/edge-service.test.d.ts +2 -0
  32. package/dist/types/src/service/edge-service.test.d.ts.map +1 -0
  33. package/dist/types/src/service/image-service.e2e.test.d.ts +2 -0
  34. package/dist/types/src/service/image-service.e2e.test.d.ts.map +1 -0
  35. package/dist/types/src/service/index.d.ts +3 -0
  36. package/dist/types/src/service/index.d.ts.map +1 -0
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +22 -17
  39. package/src/auth.ts +6 -6
  40. package/src/base-http-client.ts +1 -1
  41. package/src/browser-rendering.ts +39 -0
  42. package/src/edge-ai-http-client.test.ts +37 -0
  43. package/src/edge-ai-http-client.ts +87 -2
  44. package/src/edge-client.test.ts +1 -1
  45. package/src/edge-client.ts +9 -7
  46. package/src/edge-http-client.ts +25 -10
  47. package/src/edge-identity.ts +5 -1
  48. package/src/edge-ws-connection.ts +1 -1
  49. package/src/edge-ws-muxer.test.ts +1 -1
  50. package/src/hub-http-client.ts +20 -0
  51. package/src/service/Image.ts +61 -0
  52. package/src/service/edge-service.test.ts +151 -0
  53. package/src/service/edge-service.ts +139 -0
  54. package/src/service/image-service.e2e.test.ts +53 -0
  55. package/src/service/index.ts +7 -0
  56. package/src/testing/test-utils.ts +1 -1
@@ -0,0 +1,151 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Schema from 'effect/Schema';
7
+ import { describe, test } from 'vitest';
8
+
9
+ import { EffectEx } from '@dxos/effect';
10
+
11
+ import { EdgeServiceClient, EdgeServiceError } from './edge-service';
12
+ import * as Image from './Image';
13
+
14
+ const Echo = Schema.Struct({ value: Schema.String });
15
+
16
+ // Minimal Response stub so tests stay free of a real fetch implementation.
17
+ type StubResponse = {
18
+ ok: boolean;
19
+ status: number;
20
+ json?: () => Promise<unknown>;
21
+ text?: () => Promise<string>;
22
+ };
23
+
24
+ type Captured = { url: string; init: RequestInit };
25
+
26
+ const stubFetch = (
27
+ handler: (captured: Captured) => StubResponse | Promise<StubResponse>,
28
+ ): { fetch: typeof globalThis.fetch; calls: Captured[] } => {
29
+ const calls: Captured[] = [];
30
+ const fetch = (async (input: any, init: any) => {
31
+ const captured: Captured = { url: input.toString(), init: init ?? {} };
32
+ calls.push(captured);
33
+ return (await handler(captured)) as unknown as Response;
34
+ }) as typeof globalThis.fetch;
35
+ return { fetch, calls };
36
+ };
37
+
38
+ const json = (body: unknown, status = 200): StubResponse => ({
39
+ ok: status >= 200 && status < 300,
40
+ status,
41
+ json: () => Promise.resolve(body),
42
+ text: () => Promise.resolve(JSON.stringify(body)),
43
+ });
44
+
45
+ describe('EdgeServiceClient', () => {
46
+ test('postJson resolves and decodes the response', async ({ expect }) => {
47
+ const { fetch, calls } = stubFetch(() => json({ value: 'pong' }));
48
+ const client = new EdgeServiceClient({ baseUrl: 'https://edge.test', fetch });
49
+
50
+ const result = await EffectEx.runPromise(client.postJson('/ping', { ping: true }, Echo));
51
+ expect(result).toEqual({ value: 'pong' });
52
+ expect(calls[0].url).toBe('https://edge.test/ping');
53
+ expect((calls[0].init.headers as Headers).get('Content-Type')).toBe('application/json');
54
+ });
55
+
56
+ test('sets the client-tag header when configured', async ({ expect }) => {
57
+ const { fetch, calls } = stubFetch(() => json({ value: 'ok' }));
58
+ const client = new EdgeServiceClient({ baseUrl: 'https://edge.test', clientTag: 'ci-e2e', fetch });
59
+
60
+ await EffectEx.runPromise(client.postJson('/ping', {}, Echo));
61
+ expect((calls[0].init.headers as Headers).get('X-DXOS-Client-Tag')).toBe('ci-e2e');
62
+ });
63
+
64
+ test('merges authHeaders when provided', async ({ expect }) => {
65
+ const { fetch, calls } = stubFetch(() => json({ value: 'ok' }));
66
+ const client = new EdgeServiceClient({
67
+ baseUrl: 'https://edge.test',
68
+ fetch,
69
+ authHeaders: () => Effect.succeed({ Authorization: 'Bearer xyz' }),
70
+ });
71
+
72
+ await EffectEx.runPromise(client.postJson('/ping', {}, Echo));
73
+ expect((calls[0].init.headers as Headers).get('Authorization')).toBe('Bearer xyz');
74
+ });
75
+
76
+ test('non-2xx fails with EdgeServiceError carrying the status', async ({ expect }) => {
77
+ const { fetch } = stubFetch(() => ({ ok: false, status: 422, text: () => Promise.resolve('bad image') }));
78
+ const client = new EdgeServiceClient({ baseUrl: 'https://edge.test', fetch });
79
+
80
+ const error = await EffectEx.runPromise(Effect.flip(client.postForm('/upload', new FormData(), Echo)));
81
+ expect(error).toBeInstanceOf(EdgeServiceError);
82
+ expect(error.status).toBe(422);
83
+ expect(error.message).toContain('bad image');
84
+ });
85
+
86
+ test('malformed JSON fails with EdgeServiceError', async ({ expect }) => {
87
+ const { fetch } = stubFetch(() => ({
88
+ ok: true,
89
+ status: 200,
90
+ json: () => Promise.reject(new Error('Unexpected token')),
91
+ }));
92
+ const client = new EdgeServiceClient({ baseUrl: 'https://edge.test', fetch });
93
+
94
+ const error = await EffectEx.runPromise(Effect.flip(client.postJson('/ping', {}, Echo)));
95
+ expect(error).toBeInstanceOf(EdgeServiceError);
96
+ expect(error.message).toContain('parse JSON');
97
+ });
98
+
99
+ test('schema mismatch fails with EdgeServiceError', async ({ expect }) => {
100
+ const { fetch } = stubFetch(() => json({ wrong: 'shape' }));
101
+ const client = new EdgeServiceClient({ baseUrl: 'https://edge.test', fetch });
102
+
103
+ const error = await EffectEx.runPromise(Effect.flip(client.postJson('/ping', {}, Echo)));
104
+ expect(error).toBeInstanceOf(EdgeServiceError);
105
+ expect(error.message).toContain('schema');
106
+ });
107
+
108
+ test('non-serializable body fails with EdgeServiceError, not a synchronous throw', async ({ expect }) => {
109
+ const { fetch } = stubFetch(() => json({ value: 'ok' }));
110
+ const client = new EdgeServiceClient({ baseUrl: 'https://edge.test', fetch });
111
+ const circular: Record<string, unknown> = {};
112
+ circular.self = circular;
113
+
114
+ const error = await EffectEx.runPromise(Effect.flip(client.postJson('/ping', circular, Echo)));
115
+ expect(error).toBeInstanceOf(EdgeServiceError);
116
+ expect(error.message).toContain('serialize');
117
+ });
118
+
119
+ test('transport rejection fails with EdgeServiceError', async ({ expect }) => {
120
+ const { fetch } = stubFetch(() => Promise.reject(new Error('ECONNREFUSED')));
121
+ const client = new EdgeServiceClient({ baseUrl: 'https://edge.test', fetch });
122
+
123
+ const error = await EffectEx.runPromise(Effect.flip(client.postJson('/ping', {}, Echo)));
124
+ expect(error).toBeInstanceOf(EdgeServiceError);
125
+ expect(error.cause).toBeInstanceOf(Error);
126
+ });
127
+ });
128
+
129
+ describe('Image', () => {
130
+ test('upload posts file field to /upload and returns the hosted URL', async ({ expect }) => {
131
+ const { fetch, calls } = stubFetch(() => json({ id: 'abc', url: 'https://cdn.test/abc/public' }));
132
+ const client = new EdgeServiceClient({ baseUrl: Image.DEFAULT_IMAGE_SERVICE_URL, fetch });
133
+ const blob = new Blob([new Uint8Array([1, 2, 3])], { type: 'image/png' });
134
+
135
+ const result = await EffectEx.runPromise(Image.upload(client, blob, { filename: 'pic.png' }));
136
+ expect(result).toEqual({ id: 'abc', url: 'https://cdn.test/abc/public' });
137
+ expect(calls[0].url).toBe(`${Image.DEFAULT_IMAGE_SERVICE_URL}/upload`);
138
+ const form = calls[0].init.body as FormData;
139
+ expect(form.get('file')).toBeInstanceOf(Blob);
140
+ });
141
+
142
+ test('thumbnail posts to /thumbnail', async ({ expect }) => {
143
+ const { fetch, calls } = stubFetch(() => json({ url: 'https://cdn.test/x/public' }));
144
+ const client = new EdgeServiceClient({ baseUrl: Image.DEFAULT_IMAGE_SERVICE_URL, fetch });
145
+ const blob = new Blob([new Uint8Array([1])], { type: 'image/png' });
146
+
147
+ const result = await EffectEx.runPromise(Image.thumbnail(client, blob));
148
+ expect(result.url).toBe('https://cdn.test/x/public');
149
+ expect(calls[0].url).toBe(`${Image.DEFAULT_IMAGE_SERVICE_URL}/thumbnail`);
150
+ });
151
+ });
@@ -0,0 +1,139 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // Lightweight client for EDGE-hosted HTTP services — intentionally free of the
6
+ // heavy transitive dependencies (`@dxos/context`, `@dxos/protocols`) that
7
+ // `BaseHttpClient` pulls in, so it bundles cleanly into browser / workerd. The
8
+ // only runtime dependencies are `effect` and `@dxos/log`.
9
+
10
+ import * as Data from 'effect/Data';
11
+ import * as Duration from 'effect/Duration';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Schema from 'effect/Schema';
14
+
15
+ // Matches EDGE_CLIENT_TAG_HEADER from @dxos/protocols. Duplicated here (as in
16
+ // `cors-proxy`) to avoid importing the heavy protocols bundle.
17
+ const EDGE_CLIENT_TAG_HEADER = 'X-DXOS-Client-Tag';
18
+
19
+ const DEFAULT_TIMEOUT = Duration.seconds(15);
20
+
21
+ /** Number of body characters retained for the error message on a non-2xx response. */
22
+ const ERROR_BODY_LIMIT = 512;
23
+
24
+ export type EdgeServiceClientOptions = {
25
+ /** Base URL the service is hosted at; request paths resolve against it. */
26
+ baseUrl: string;
27
+ /** Tag included in the {@link EDGE_CLIENT_TAG_HEADER} header for metering. */
28
+ clientTag?: string;
29
+ /** Per-request timeout. */
30
+ timeout?: Duration.DurationInput;
31
+ /** Injectable for deterministic tests; defaults to `globalThis.fetch`. */
32
+ fetch?: typeof globalThis.fetch;
33
+ /** Auth seam invoked per request; unused by the image service today. */
34
+ authHeaders?: () => Effect.Effect<Record<string, string>>;
35
+ };
36
+
37
+ /** Single failure type for every transport, status, and decode error. */
38
+ export class EdgeServiceError extends Data.TaggedError('EdgeServiceError')<{
39
+ message: string;
40
+ status?: number;
41
+ cause?: unknown;
42
+ }> {}
43
+
44
+ export class EdgeServiceClient {
45
+ readonly #baseUrl: string;
46
+ readonly #clientTag: string | undefined;
47
+ readonly #timeout: Duration.Duration;
48
+ readonly #fetch: typeof globalThis.fetch;
49
+ readonly #authHeaders: (() => Effect.Effect<Record<string, string>>) | undefined;
50
+
51
+ constructor(options: EdgeServiceClientOptions) {
52
+ this.#baseUrl = options.baseUrl;
53
+ this.#clientTag = options.clientTag;
54
+ this.#timeout = Duration.decode(options.timeout ?? DEFAULT_TIMEOUT);
55
+ // Bind so `fetch` keeps its expected `this` when injected as a method reference.
56
+ const fetchImpl = options.fetch ?? globalThis.fetch;
57
+ this.#fetch = fetchImpl.bind(globalThis);
58
+ this.#authHeaders = options.authHeaders;
59
+ }
60
+
61
+ get baseUrl(): string {
62
+ return this.#baseUrl;
63
+ }
64
+
65
+ /** POST a `FormData` (multipart) body and decode the JSON response. */
66
+ postForm<A>(path: string, form: FormData, schema: Schema.Schema<A>): Effect.Effect<A, EdgeServiceError> {
67
+ // The runtime sets `Content-Type: multipart/form-data` with the boundary;
68
+ // setting it manually here would omit the boundary and break parsing.
69
+ return this.#request(path, schema, { method: 'POST', body: form });
70
+ }
71
+
72
+ /** POST a JSON body and decode the JSON response. */
73
+ postJson<A>(path: string, body: unknown, schema: Schema.Schema<A>): Effect.Effect<A, EdgeServiceError> {
74
+ // Serialize inside the Effect so a circular/non-serializable body surfaces as
75
+ // EdgeServiceError rather than throwing synchronously from the call site.
76
+ return Effect.try({
77
+ try: () => JSON.stringify(body),
78
+ catch: (cause) => new EdgeServiceError({ message: 'Failed to serialize JSON request body', cause }),
79
+ }).pipe(
80
+ Effect.flatMap((json) =>
81
+ this.#request(path, schema, {
82
+ method: 'POST',
83
+ body: json,
84
+ headers: { 'Content-Type': 'application/json' },
85
+ }),
86
+ ),
87
+ );
88
+ }
89
+
90
+ #request<A>(path: string, schema: Schema.Schema<A>, init: RequestInit): Effect.Effect<A, EdgeServiceError> {
91
+ return Effect.gen(this, function* () {
92
+ const url = new URL(path, this.#baseUrl);
93
+ const headers = new Headers(init.headers ?? undefined);
94
+ if (this.#clientTag) {
95
+ headers.set(EDGE_CLIENT_TAG_HEADER, this.#clientTag);
96
+ }
97
+ if (this.#authHeaders) {
98
+ const auth = yield* this.#authHeaders();
99
+ for (const [key, value] of Object.entries(auth)) {
100
+ headers.set(key, value);
101
+ }
102
+ }
103
+
104
+ const response = yield* Effect.tryPromise({
105
+ try: (signal) => this.#fetch(url, { ...init, headers, signal }),
106
+ catch: (cause) => new EdgeServiceError({ message: `Request to ${url.pathname} failed`, cause }),
107
+ });
108
+
109
+ if (!response.ok) {
110
+ // Body text aids debugging but may be large; truncate and never throw on read.
111
+ const detail = yield* Effect.promise(() => response.text().catch(() => '')).pipe(
112
+ Effect.map((text) => text.slice(0, ERROR_BODY_LIMIT)),
113
+ );
114
+ return yield* new EdgeServiceError({
115
+ message: `Service responded ${response.status}${detail ? `: ${detail}` : ''}`,
116
+ status: response.status,
117
+ });
118
+ }
119
+
120
+ const json = yield* Effect.tryPromise({
121
+ try: () => response.json(),
122
+ catch: (cause) =>
123
+ new EdgeServiceError({ message: 'Failed to parse JSON response', status: response.status, cause }),
124
+ });
125
+
126
+ return yield* Schema.decodeUnknown(schema)(json).pipe(
127
+ Effect.mapError(
128
+ (cause) =>
129
+ new EdgeServiceError({ message: 'Response did not match expected schema', status: response.status, cause }),
130
+ ),
131
+ );
132
+ }).pipe(
133
+ Effect.timeoutFail({
134
+ duration: this.#timeout,
135
+ onTimeout: () => new EdgeServiceError({ message: `Request to ${path} timed out` }),
136
+ }),
137
+ );
138
+ }
139
+ }
@@ -0,0 +1,53 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { EffectEx } from '@dxos/effect';
8
+
9
+ import { EdgeServiceClient } from './edge-service';
10
+ import * as Image from './Image';
11
+
12
+ // Live end-to-end checks against the shared Composer image service (the Cloudflare
13
+ // Worker at DEFAULT_IMAGE_SERVICE_URL). These hit the public network, so they are
14
+ // gated behind DX_RUN_IMAGE_SERVICE_E2E and skipped in CI by default.
15
+ //
16
+ // DX_RUN_IMAGE_SERVICE_E2E=1 moon run edge-client:test -- image-service.e2e.test.ts
17
+ //
18
+ // Override the target with DX_IMAGE_SERVICE_URL to point at a staging worker.
19
+
20
+ // Require an explicit opt-in value so `DX_RUN_IMAGE_SERVICE_E2E=0` doesn't run live tests.
21
+ const ENABLED = /^(1|true)$/i.test(process.env.DX_RUN_IMAGE_SERVICE_E2E ?? '');
22
+ const SERVICE_URL = process.env.DX_IMAGE_SERVICE_URL ?? Image.DEFAULT_IMAGE_SERVICE_URL;
23
+
24
+ // 32x32 RGB gradient PNG. The worker decodes + resizes the upload, so it rejects
25
+ // degenerate inputs (a 1x1 PNG yields a 422); a real raster of modest dimensions
26
+ // is the smallest payload it reliably accepts.
27
+ const PNG_32X32_BASE64 =
28
+ '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';
29
+
30
+ const pngBlob = (): Blob => {
31
+ const binary = atob(PNG_32X32_BASE64);
32
+ const bytes = new Uint8Array(binary.length);
33
+ for (let i = 0; i < binary.length; i++) {
34
+ bytes[i] = binary.charCodeAt(i);
35
+ }
36
+ return new Blob([bytes], { type: 'image/png' });
37
+ };
38
+
39
+ const isAbsoluteHttpUrl = (value: string): boolean => /^https?:\/\//.test(value);
40
+
41
+ describe.skipIf(!ENABLED)('image service (live)', () => {
42
+ const client = new EdgeServiceClient({ baseUrl: SERVICE_URL });
43
+
44
+ test('Image.upload stores an image and returns a hosted URL', async ({ expect }) => {
45
+ const result = await EffectEx.runPromise(Image.upload(client, pngBlob(), { filename: `e2e-${Date.now()}.png` }));
46
+ expect(isAbsoluteHttpUrl(result.url)).toBe(true);
47
+ });
48
+
49
+ test('Image.thumbnail stores an image and returns a hosted URL', async ({ expect }) => {
50
+ const result = await EffectEx.runPromise(Image.thumbnail(client, pngBlob(), { filename: `e2e-${Date.now()}.png` }));
51
+ expect(isAbsoluteHttpUrl(result.url)).toBe(true);
52
+ });
53
+ });
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './edge-service';
6
+
7
+ export * as Image from './Image';
@@ -106,7 +106,7 @@ const createResponseSender = (connection: () => WebSocketMuxer) => {
106
106
  void connection().send(
107
107
  buf.create(MessageSchema, {
108
108
  source: {
109
- identityKey: recipient.identityKey,
109
+ identityDid: recipient.identityDid,
110
110
  peerKey: recipient.peerKey,
111
111
  },
112
112
  serviceId: request.serviceId!,