@aithos/sdk 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (57) hide show
  1. package/README.md +45 -0
  2. package/dist/src/auth-api.d.ts +50 -0
  3. package/dist/src/auth-api.js +102 -0
  4. package/dist/src/auth.d.ts +253 -0
  5. package/dist/src/auth.js +940 -0
  6. package/dist/src/compute.d.ts +370 -9
  7. package/dist/src/compute.js +369 -16
  8. package/dist/src/ethos.d.ts +117 -1
  9. package/dist/src/ethos.js +646 -16
  10. package/dist/src/index.d.ts +11 -4
  11. package/dist/src/index.js +31 -5
  12. package/dist/src/internal/delegate-bundle.d.ts +18 -0
  13. package/dist/src/internal/delegate-bundle.js +94 -0
  14. package/dist/src/internal/delegate-state.d.ts +45 -0
  15. package/dist/src/internal/delegate-state.js +120 -0
  16. package/dist/src/internal/owner-signers.d.ts +78 -0
  17. package/dist/src/internal/owner-signers.js +179 -0
  18. package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
  19. package/dist/src/internal/protocol-client-bridge.js +20 -0
  20. package/dist/src/internal/recovery-file.d.ts +29 -0
  21. package/dist/src/internal/recovery-file.js +98 -0
  22. package/dist/src/internal/signer.d.ts +59 -0
  23. package/dist/src/internal/signer.js +86 -0
  24. package/dist/src/key-store.d.ts +128 -0
  25. package/dist/src/key-store.js +244 -0
  26. package/dist/src/mandates.d.ts +163 -1
  27. package/dist/src/mandates.js +286 -8
  28. package/dist/src/sdk.d.ts +36 -3
  29. package/dist/src/sdk.js +27 -23
  30. package/dist/src/session-store.d.ts +58 -0
  31. package/dist/src/session-store.js +158 -0
  32. package/dist/src/wallet.d.ts +4 -6
  33. package/dist/src/wallet.js +18 -8
  34. package/dist/test/auth-j3.test.d.ts +2 -0
  35. package/dist/test/auth-j3.test.js +391 -0
  36. package/dist/test/auth.test.d.ts +2 -0
  37. package/dist/test/auth.test.js +175 -0
  38. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  39. package/dist/test/compute-delegate-path.test.js +183 -0
  40. package/dist/test/compute.test.js +184 -11
  41. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  42. package/dist/test/ethos-first-edition.test.js +248 -0
  43. package/dist/test/ethos.test.d.ts +2 -0
  44. package/dist/test/ethos.test.js +219 -0
  45. package/dist/test/key-store.test.d.ts +2 -0
  46. package/dist/test/key-store.test.js +161 -0
  47. package/dist/test/mandates-compute.test.d.ts +2 -0
  48. package/dist/test/mandates-compute.test.js +256 -0
  49. package/dist/test/mandates.test.d.ts +2 -0
  50. package/dist/test/mandates.test.js +93 -0
  51. package/dist/test/sdk.test.js +70 -30
  52. package/dist/test/signer.test.d.ts +2 -0
  53. package/dist/test/signer.test.js +117 -0
  54. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  55. package/dist/test/signup-bootstrap.test.js +222 -0
  56. package/dist/test/wallet.test.js +20 -9
  57. package/package.json +4 -3
@@ -0,0 +1,175 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Unit tests for AithosAuth — Sign in with Google flow.
4
+ import { strict as assert } from "node:assert";
5
+ import { describe, it } from "node:test";
6
+ import { AithosAuth, AithosSDKError } from "../src/index.js";
7
+ /** Tiny window-shim that records calls instead of actually navigating. */
8
+ function makeFakeWindow(initialHref) {
9
+ let href = initialHref;
10
+ let assigned = null;
11
+ let replacedHref = null;
12
+ const win = {
13
+ location: {
14
+ get href() {
15
+ return href;
16
+ },
17
+ assign(target) {
18
+ assigned = target;
19
+ },
20
+ },
21
+ history: {
22
+ replaceState(_state, _title, url) {
23
+ replacedHref = url;
24
+ href = url;
25
+ },
26
+ },
27
+ };
28
+ return {
29
+ win: win,
30
+ get assigned() {
31
+ return assigned;
32
+ },
33
+ get replacedHref() {
34
+ return replacedHref;
35
+ },
36
+ };
37
+ }
38
+ function fakeSession(overrides = {}) {
39
+ return {
40
+ session: "jwt-token-here",
41
+ exp: Math.floor(Date.now() / 1000) + 3600,
42
+ did: "did:aithos:zABC123",
43
+ handle: "alice-x9y2",
44
+ blob_b64: "",
45
+ blob_nonce_b64: "",
46
+ blob_version: 0,
47
+ enc_key_b64: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=",
48
+ is_first_login: true,
49
+ ...overrides,
50
+ };
51
+ }
52
+ /* -------------------------------------------------------------------------- */
53
+ /* signInWithGoogle */
54
+ /* -------------------------------------------------------------------------- */
55
+ describe("AithosAuth.signInWithGoogle", () => {
56
+ it("navigates to /auth/sso/google/start with no params by default", () => {
57
+ const w = makeFakeWindow("https://app.aithos.be/login");
58
+ const auth = new AithosAuth({
59
+ authBaseUrl: "https://auth.example.test",
60
+ window: w.win,
61
+ });
62
+ assert.throws(() => auth.signInWithGoogle(), AithosSDKError);
63
+ assert.equal(w.assigned, "https://auth.example.test/auth/sso/google/start");
64
+ });
65
+ it("forwards appState as the app_state query param", () => {
66
+ const w = makeFakeWindow("https://app.aithos.be/login");
67
+ const auth = new AithosAuth({
68
+ authBaseUrl: "https://auth.example.test",
69
+ window: w.win,
70
+ });
71
+ assert.throws(() => auth.signInWithGoogle({ appState: "/dashboard" }), AithosSDKError);
72
+ const url = new URL(w.assigned);
73
+ assert.equal(url.searchParams.get("app_state"), "/dashboard");
74
+ });
75
+ it("rejects appState longer than 1024 chars without navigating", () => {
76
+ const w = makeFakeWindow("https://app.aithos.be/login");
77
+ const auth = new AithosAuth({
78
+ authBaseUrl: "https://auth.example.test",
79
+ window: w.win,
80
+ });
81
+ const tooLong = "x".repeat(1025);
82
+ assert.throws(() => auth.signInWithGoogle({ appState: tooLong }), (e) => e instanceof AithosSDKError && e.code === "auth_app_state_too_long");
83
+ assert.equal(w.assigned, null, "must not have navigated");
84
+ });
85
+ it("trims a trailing slash from authBaseUrl", () => {
86
+ const w = makeFakeWindow("https://app.aithos.be/");
87
+ const auth = new AithosAuth({
88
+ authBaseUrl: "https://auth.example.test/",
89
+ window: w.win,
90
+ });
91
+ assert.throws(() => auth.signInWithGoogle(), AithosSDKError);
92
+ assert.equal(w.assigned, "https://auth.example.test/auth/sso/google/start");
93
+ });
94
+ });
95
+ /* -------------------------------------------------------------------------- */
96
+ /* handleCallback */
97
+ /* -------------------------------------------------------------------------- */
98
+ describe("AithosAuth.handleCallback", () => {
99
+ it("returns null when the URL has no aithos_code", async () => {
100
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback");
101
+ const auth = new AithosAuth({ window: w.win, fetch: undefinedFetch() });
102
+ const session = await auth.handleCallback();
103
+ assert.equal(session, null);
104
+ });
105
+ it("exchanges the code, returns the session, and strips query params", async () => {
106
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback?aithos_code=abc123XYZ_-456&app_state=/dashboard");
107
+ const session = fakeSession({ is_first_login: true });
108
+ let capturedBody;
109
+ const fakeFetch = async (input, init) => {
110
+ assert.equal(typeof input === "string" ? input : input.toString(), "https://auth.example.test/auth/sso/exchange");
111
+ capturedBody = JSON.parse(init?.body);
112
+ return new Response(JSON.stringify(session), {
113
+ status: 200,
114
+ headers: { "content-type": "application/json" },
115
+ });
116
+ };
117
+ const auth = new AithosAuth({
118
+ authBaseUrl: "https://auth.example.test",
119
+ window: w.win,
120
+ fetch: fakeFetch,
121
+ });
122
+ const out = await auth.handleCallback();
123
+ assert.deepEqual(out, session);
124
+ assert.equal(capturedBody?.aithos_code, "abc123XYZ_-456");
125
+ assert.equal(w.replacedHref, "https://app.aithos.be/auth/callback", "callback params must be stripped from the URL");
126
+ });
127
+ it("throws AithosSDKError with the backend code on aithos_error", async () => {
128
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback?aithos_error=google_id_token&app_state=/dashboard");
129
+ const auth = new AithosAuth({
130
+ authBaseUrl: "https://auth.example.test",
131
+ window: w.win,
132
+ fetch: undefinedFetch(),
133
+ });
134
+ await assert.rejects(auth.handleCallback(), (e) => e instanceof AithosSDKError && e.code === "auth_google_id_token");
135
+ // URL is cleaned even on error so a refresh doesn't loop the message.
136
+ assert.equal(w.replacedHref, "https://app.aithos.be/auth/callback");
137
+ });
138
+ it("wraps a 410 'code_consumed' as AithosSDKError(code='auth_code_consumed')", async () => {
139
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback?aithos_code=abc123XYZ_-456");
140
+ const fakeFetch = async () => new Response(JSON.stringify({ error: "aithos_code expired or already used", code: "code_consumed" }), { status: 410, headers: { "content-type": "application/json" } });
141
+ const auth = new AithosAuth({
142
+ authBaseUrl: "https://auth.example.test",
143
+ window: w.win,
144
+ fetch: fakeFetch,
145
+ });
146
+ await assert.rejects(auth.handleCallback(), (e) => e instanceof AithosSDKError &&
147
+ e.code === "auth_code_consumed" &&
148
+ e.status === 410);
149
+ });
150
+ it("returns null in non-browser environments (no window)", async () => {
151
+ // No `window` injected and `globalThis.window` is undefined under Node test.
152
+ const auth = new AithosAuth({ window: undefined, fetch: undefinedFetch() });
153
+ const session = await auth.handleCallback();
154
+ assert.equal(session, null);
155
+ });
156
+ });
157
+ /* -------------------------------------------------------------------------- */
158
+ /* signOut */
159
+ /* -------------------------------------------------------------------------- */
160
+ describe("AithosAuth.signOut", () => {
161
+ it("resolves immediately (sessions are stateless)", async () => {
162
+ const auth = new AithosAuth({ window: undefined, fetch: undefinedFetch() });
163
+ await auth.signOut();
164
+ });
165
+ });
166
+ /* -------------------------------------------------------------------------- */
167
+ /* Helpers */
168
+ /* -------------------------------------------------------------------------- */
169
+ /** A fetch that fails the test if invoked — for code paths that mustn't fetch. */
170
+ function undefinedFetch() {
171
+ return async () => {
172
+ throw new Error("fetch should not have been called");
173
+ };
174
+ }
175
+ //# sourceMappingURL=auth.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compute-delegate-path.test.d.ts.map
@@ -0,0 +1,183 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the delegate signing path on sdk.compute.invokeBedrock.
4
+ // Until alpha.9 the method ALWAYS required an owner — a session that
5
+ // only held a mandate (no owner signers) failed with sdk_no_owner
6
+ // before any network call. From alpha.9 onward, when no owner is
7
+ // loaded but a delegate matches the requested mandate id, the SDK
8
+ // signs the envelope with the delegate's keypair and attaches the
9
+ // SignedMandate.
10
+ import { strict as assert } from "node:assert";
11
+ import { afterEach, beforeEach, describe, it } from "node:test";
12
+ import { createBrowserIdentity } from "@aithos/protocol-client";
13
+ import { AithosAuth, AithosSDKError, ComputeNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
14
+ import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
15
+ /* -------------------------------------------------------------------------- */
16
+ /* Test plumbing */
17
+ /* -------------------------------------------------------------------------- */
18
+ let savedFetch;
19
+ let lastRequestBody = null;
20
+ function installFetchMock(response) {
21
+ savedFetch = globalThis.fetch;
22
+ lastRequestBody = null;
23
+ globalThis.fetch = (async (_input, init) => {
24
+ lastRequestBody = JSON.parse(init?.body);
25
+ return new Response(JSON.stringify(response.json), {
26
+ status: response.status ?? 200,
27
+ headers: { "content-type": "application/json" },
28
+ });
29
+ });
30
+ }
31
+ function uninstallFetchMock() {
32
+ if (savedFetch)
33
+ globalThis.fetch = savedFetch;
34
+ savedFetch = undefined;
35
+ lastRequestBody = null;
36
+ }
37
+ const okResponse = {
38
+ json: {
39
+ jsonrpc: "2.0",
40
+ id: "x",
41
+ result: {
42
+ content: "ok",
43
+ stopReason: "end_turn",
44
+ usage: { inputTokens: 1, outputTokens: 1 },
45
+ creditsCharged: 1,
46
+ walletBalance: 999,
47
+ auditId: "audit-test",
48
+ },
49
+ },
50
+ };
51
+ function freshAuth() {
52
+ return new AithosAuth({
53
+ sessionStore: noopStore(),
54
+ keyStore: memoryKeyStore(),
55
+ });
56
+ }
57
+ function freshCompute(auth) {
58
+ return new ComputeNamespace({
59
+ auth,
60
+ appDid: "did:aithos:app:example-placeholder",
61
+ endpoints: DEFAULT_SDK_ENDPOINTS,
62
+ fetch: globalThis.fetch.bind(globalThis),
63
+ });
64
+ }
65
+ /**
66
+ * Build a delegate bundle JSON that matches the wire shape importMandate
67
+ * accepts. Uses a real SignedMandate-like object minted from a fresh
68
+ * issuer so signature verification on import succeeds.
69
+ */
70
+ function makeDelegateBundleText(args) {
71
+ const issuer = createBrowserIdentity("alice", "Alice");
72
+ return JSON.stringify({
73
+ aithos_delegate_version: "0.1.0",
74
+ mandate: {
75
+ "aithos-mandate": "0.4.0",
76
+ id: args.mandateId,
77
+ issuer: issuer.did,
78
+ issued_by_key: `${issuer.did}#self`,
79
+ grantee: {
80
+ id: args.granteeId ?? "urn:aithos:agent:test",
81
+ pubkey: "z6MkqGenericPubKey",
82
+ },
83
+ actor_sphere: "self",
84
+ scopes: args.scopes,
85
+ not_before: "2026-05-10T00:00:00Z",
86
+ not_after: "2026-05-11T00:00:00Z",
87
+ issued_at: "2026-05-10T00:00:00Z",
88
+ nonce: "abc",
89
+ signature: { alg: "ed25519", key: `${issuer.did}#self`, value: "..." },
90
+ },
91
+ delegate_seed_hex: "11".repeat(32),
92
+ });
93
+ }
94
+ /* -------------------------------------------------------------------------- */
95
+ /* Tests */
96
+ /* -------------------------------------------------------------------------- */
97
+ describe("ComputeNamespace.invokeBedrock — delegate path", () => {
98
+ beforeEach(() => {
99
+ installFetchMock(okResponse);
100
+ });
101
+ afterEach(() => {
102
+ uninstallFetchMock();
103
+ });
104
+ it("rejects with sdk_no_delegate_for_mandate when delegate-only and mandateId doesn't match", async () => {
105
+ const auth = freshAuth();
106
+ const compute = freshCompute(auth);
107
+ // Import a delegate with a DIFFERENT mandate id than the one the call
108
+ // requests. Should error with the new code (NOT sdk_no_owner anymore).
109
+ await auth.importMandate({
110
+ bundle: makeDelegateBundleText({
111
+ mandateId: "mandate:held",
112
+ scopes: ["compute.invoke"],
113
+ }),
114
+ });
115
+ await assert.rejects(() => compute.invokeBedrock({
116
+ mandateId: "mandate:other-not-held",
117
+ model: "claude-haiku-4-5",
118
+ messages: [{ role: "user", content: "Hi" }],
119
+ maxTokens: 1,
120
+ }), (e) => e instanceof AithosSDKError &&
121
+ e.code === "sdk_no_delegate_for_mandate");
122
+ });
123
+ it("signs with the delegate keypair + attaches the mandate when delegate-only matches", async () => {
124
+ const auth = freshAuth();
125
+ const compute = freshCompute(auth);
126
+ const mandateId = "mandate:matching";
127
+ await auth.importMandate({
128
+ bundle: makeDelegateBundleText({
129
+ mandateId,
130
+ scopes: ["compute.invoke"],
131
+ }),
132
+ });
133
+ await compute.invokeBedrock({
134
+ mandateId,
135
+ model: "claude-haiku-4-5",
136
+ messages: [{ role: "user", content: "Hi" }],
137
+ maxTokens: 1,
138
+ });
139
+ const env = lastRequestBody?.params?._envelope;
140
+ assert.ok(env, "envelope must be present");
141
+ // verificationMethod must be the delegate's bare multibase pubkey
142
+ // (NOT a `#sphere` DID URL). And the SignedMandate must be inside.
143
+ assert.match(env.proof.verificationMethod, /^z[1-9A-HJ-NP-Za-km-z]+$/, `expected multibase pubkey, got ${env.proof.verificationMethod}`);
144
+ assert.ok(env.mandate, "delegate envelopes must attach the SignedMandate");
145
+ assert.equal(env.mandate.id, mandateId);
146
+ });
147
+ it("still works the owner path when an owner is signed in", async () => {
148
+ const auth = freshAuth();
149
+ const compute = freshCompute(auth);
150
+ // Owner sign-in via recovery — picks up four sphere keys from the
151
+ // recovery file format. Build one inline.
152
+ const issuer = createBrowserIdentity("owner", "Owner");
153
+ const seedHex = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
154
+ const recoveryText = JSON.stringify({
155
+ aithos_recovery_version: "0.1.0-plaintext",
156
+ handle: issuer.handle,
157
+ display_name: issuer.displayName,
158
+ did: issuer.did,
159
+ created_at: new Date().toISOString(),
160
+ seeds_hex: {
161
+ root: seedHex(issuer.root.seed),
162
+ public: seedHex(issuer.public.seed),
163
+ circle: seedHex(issuer.circle.seed),
164
+ self: seedHex(issuer.self.seed),
165
+ },
166
+ });
167
+ await auth.signInWithRecovery({ file: recoveryText });
168
+ await compute.invokeBedrock({
169
+ mandateId: "mandate:owner-managed",
170
+ model: "claude-haiku-4-5",
171
+ messages: [{ role: "user", content: "Hi" }],
172
+ maxTokens: 1,
173
+ });
174
+ const env = lastRequestBody?.params?._envelope;
175
+ assert.ok(env, "envelope must be present");
176
+ // Owner path: verificationMethod is a `#public` DID URL, and the
177
+ // envelope MUST NOT carry a mandate (server resolves from
178
+ // params.mandate_id).
179
+ assert.match(env.proof.verificationMethod, /#public$/);
180
+ assert.equal(env.mandate, undefined, "owner-signed envelopes should not attach a mandate");
181
+ });
182
+ });
183
+ //# sourceMappingURL=compute-delegate-path.test.js.map
@@ -9,12 +9,23 @@
9
9
  import { strict as assert } from "node:assert";
10
10
  import { describe, it } from "node:test";
11
11
  import { createBrowserIdentity } from "@aithos/protocol-client";
12
- import { AithosSDK, AithosSDKError } from "../src/index.js";
12
+ import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
13
+ import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
13
14
  const APP_DID = "did:aithos:app:test";
14
- function makeSdk(fetchImpl) {
15
- const identity = createBrowserIdentity("test-handle", "Test User");
15
+ async function makeSdk(fetchImpl) {
16
+ const id = createBrowserIdentity("test-handle", "Test User");
17
+ const auth = new AithosAuth({
18
+ authBaseUrl: "https://auth.test",
19
+ fetch: (() => {
20
+ throw new Error("auth not used in compute tests");
21
+ }),
22
+ sessionStore: noopStore(),
23
+ keyStore: memoryKeyStore(),
24
+ });
25
+ const { text } = serializeRecoveryFile(id);
26
+ await auth.signInWithRecovery({ file: text });
16
27
  return new AithosSDK({
17
- identity,
28
+ auth,
18
29
  appDid: APP_DID,
19
30
  endpoints: { compute: "https://compute.example.test" },
20
31
  fetch: fetchImpl,
@@ -40,7 +51,7 @@ describe("compute.invokeBedrock — happy path", () => {
40
51
  headers: { "content-type": "application/json" },
41
52
  });
42
53
  };
43
- const sdk = makeSdk(fakeFetch);
54
+ const sdk = await makeSdk(fakeFetch);
44
55
  const out = await sdk.compute.invokeBedrock({
45
56
  mandateId: "mandate:abc",
46
57
  model: "claude-sonnet-4-6",
@@ -71,7 +82,7 @@ describe("compute.invokeBedrock — happy path", () => {
71
82
  headers: { "content-type": "application/json" },
72
83
  });
73
84
  };
74
- const sdk = makeSdk(fakeFetch);
85
+ const sdk = await makeSdk(fakeFetch);
75
86
  await sdk.compute.invokeBedrock({
76
87
  mandateId: "mandate:abc",
77
88
  model: "claude-sonnet-4-6",
@@ -92,7 +103,7 @@ describe("compute.invokeBedrock — errors", () => {
92
103
  const fakeFetch = async () => {
93
104
  throw new Error("fetch failed: ECONNREFUSED");
94
105
  };
95
- const sdk = makeSdk(fakeFetch);
106
+ const sdk = await makeSdk(fakeFetch);
96
107
  await assert.rejects(sdk.compute.invokeBedrock({
97
108
  mandateId: "mandate:abc",
98
109
  model: "claude-sonnet-4-6",
@@ -106,7 +117,7 @@ describe("compute.invokeBedrock — errors", () => {
106
117
  });
107
118
  it("wraps an HTTP non-2xx as AithosSDKError(code='http')", async () => {
108
119
  const fakeFetch = async () => new Response("nope", { status: 503, statusText: "Service Unavailable" });
109
- const sdk = makeSdk(fakeFetch);
120
+ const sdk = await makeSdk(fakeFetch);
110
121
  await assert.rejects(sdk.compute.invokeBedrock({
111
122
  mandateId: "mandate:abc",
112
123
  model: "claude-sonnet-4-6",
@@ -126,7 +137,7 @@ describe("compute.invokeBedrock — errors", () => {
126
137
  data: { balance: 0, required: 1 },
127
138
  },
128
139
  }), { status: 200, headers: { "content-type": "application/json" } });
129
- const sdk = makeSdk(fakeFetch);
140
+ const sdk = await makeSdk(fakeFetch);
130
141
  await assert.rejects(sdk.compute.invokeBedrock({
131
142
  mandateId: "mandate:abc",
132
143
  model: "claude-sonnet-4-6",
@@ -143,7 +154,7 @@ describe("compute.invokeBedrock — errors", () => {
143
154
  status: 200,
144
155
  headers: { "content-type": "application/json" },
145
156
  });
146
- const sdk = makeSdk(fakeFetch);
157
+ const sdk = await makeSdk(fakeFetch);
147
158
  await assert.rejects(sdk.compute.invokeBedrock({
148
159
  mandateId: "mandate:abc",
149
160
  model: "claude-sonnet-4-6",
@@ -165,7 +176,7 @@ describe("compute.invokeBedrock — abort", () => {
165
176
  headers: { "content-type": "application/json" },
166
177
  });
167
178
  };
168
- const sdk = makeSdk(fakeFetch);
179
+ const sdk = await makeSdk(fakeFetch);
169
180
  const ac = new AbortController();
170
181
  await sdk.compute.invokeBedrock({
171
182
  mandateId: "mandate:abc",
@@ -176,4 +187,166 @@ describe("compute.invokeBedrock — abort", () => {
176
187
  assert.equal(receivedSignal, ac.signal);
177
188
  });
178
189
  });
190
+ /* -------------------------------------------------------------------------- */
191
+ /* invokeUrlFetch */
192
+ /* -------------------------------------------------------------------------- */
193
+ const URL_FETCH_HAPPY_RESULT = {
194
+ content: "Tata.com propose un service Y avec une couleur primaire bleu (#1E40AF).",
195
+ citations: [
196
+ {
197
+ url: "https://tata.com",
198
+ citedText: "Notre service révolutionne...",
199
+ documentTitle: "Tata — Home",
200
+ startCharIndex: 0,
201
+ endCharIndex: 28,
202
+ },
203
+ ],
204
+ urlsFetched: [
205
+ {
206
+ url: "https://tata.com",
207
+ retrievedAt: "2026-05-12T10:00:00Z",
208
+ title: "Tata — Home",
209
+ },
210
+ ],
211
+ stopReason: "end_turn",
212
+ usage: { inputTokens: 28_500, outputTokens: 420, webFetchInvocations: 1 },
213
+ creditsCharged: 35,
214
+ walletBalance: 99_965,
215
+ auditId: "audit-url-1",
216
+ };
217
+ describe("compute.invokeUrlFetch — happy path", () => {
218
+ it("posts to ${compute}/v1/invoke with method=aithos.compute_invoke_url_fetch", async () => {
219
+ let capturedUrl;
220
+ let capturedInit;
221
+ const fakeFetch = async (input, init) => {
222
+ capturedUrl = typeof input === "string" ? input : input.toString();
223
+ capturedInit = init;
224
+ return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
225
+ status: 200,
226
+ headers: { "content-type": "application/json" },
227
+ });
228
+ };
229
+ const sdk = await makeSdk(fakeFetch);
230
+ const out = await sdk.compute.invokeUrlFetch({
231
+ mandateId: "mandate:abc",
232
+ prompt: "Voici l'URL https://tata.com — résume.",
233
+ });
234
+ assert.deepEqual(out, URL_FETCH_HAPPY_RESULT);
235
+ assert.equal(capturedUrl, "https://compute.example.test/v1/invoke");
236
+ const body = JSON.parse(capturedInit?.body);
237
+ assert.equal(body.jsonrpc, "2.0");
238
+ assert.equal(body.method, "aithos.compute_invoke_url_fetch");
239
+ assert.equal(body.params.app_did, APP_DID);
240
+ assert.equal(body.params.mandate_id, "mandate:abc");
241
+ // Default model is Haiku 4.5 (cheapest model that supports web_fetch).
242
+ assert.equal(body.params.model, "claude-haiku-4-5");
243
+ assert.equal(body.params.prompt, "Voici l'URL https://tata.com — résume.");
244
+ // Auto-generated idempotency key.
245
+ assert.match(body.params.idempotency_key, /^[0-9a-f]{32}$/);
246
+ // Envelope is present on the wire.
247
+ assert.ok(body.params._envelope, "request must carry a signed envelope");
248
+ // Optional params NOT forwarded when not provided.
249
+ assert.equal(body.params.system, undefined);
250
+ assert.equal(body.params.max_fetches, undefined);
251
+ assert.equal(body.params.max_content_tokens, undefined);
252
+ assert.equal(body.params.citations, undefined);
253
+ assert.equal(body.params.allowed_domains, undefined);
254
+ assert.equal(body.params.blocked_domains, undefined);
255
+ });
256
+ it("forwards all optional knobs: system / maxTokens / maxFetches / maxContentTokens / citations / allowedDomains / idempotencyKey / model", async () => {
257
+ let capturedBody;
258
+ const fakeFetch = async (_input, init) => {
259
+ capturedBody = JSON.parse(init?.body).params;
260
+ return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
261
+ status: 200,
262
+ headers: { "content-type": "application/json" },
263
+ });
264
+ };
265
+ const sdk = await makeSdk(fakeFetch);
266
+ await sdk.compute.invokeUrlFetch({
267
+ mandateId: "mandate:abc",
268
+ model: "claude-sonnet-4-6",
269
+ prompt: "Analyse https://tata.com",
270
+ system: "You are a brand analyst.",
271
+ maxTokens: 1024,
272
+ temperature: 0.1,
273
+ maxFetches: 3,
274
+ maxContentTokens: 50_000,
275
+ citations: false,
276
+ allowedDomains: ["tata.com", "*.tata.com"],
277
+ idempotencyKey: "idem-url-1",
278
+ });
279
+ assert.equal(capturedBody?.model, "claude-sonnet-4-6");
280
+ assert.equal(capturedBody?.system, "You are a brand analyst.");
281
+ assert.equal(capturedBody?.max_tokens, 1024);
282
+ assert.equal(capturedBody?.temperature, 0.1);
283
+ assert.equal(capturedBody?.max_fetches, 3);
284
+ assert.equal(capturedBody?.max_content_tokens, 50_000);
285
+ assert.equal(capturedBody?.citations, false);
286
+ assert.deepEqual(capturedBody?.allowed_domains, ["tata.com", "*.tata.com"]);
287
+ assert.equal(capturedBody?.idempotency_key, "idem-url-1");
288
+ // Empty / undefined arrays must NOT bleed through as `[]` on the wire.
289
+ assert.equal(capturedBody?.blocked_domains, undefined);
290
+ });
291
+ it("omits empty allowedDomains / blockedDomains arrays from the wire payload", async () => {
292
+ let capturedBody;
293
+ const fakeFetch = async (_input, init) => {
294
+ capturedBody = JSON.parse(init?.body).params;
295
+ return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
296
+ status: 200,
297
+ headers: { "content-type": "application/json" },
298
+ });
299
+ };
300
+ const sdk = await makeSdk(fakeFetch);
301
+ await sdk.compute.invokeUrlFetch({
302
+ mandateId: "mandate:abc",
303
+ prompt: "Analyse https://tata.com",
304
+ allowedDomains: [],
305
+ blockedDomains: [],
306
+ });
307
+ assert.equal(capturedBody?.allowed_domains, undefined);
308
+ assert.equal(capturedBody?.blocked_domains, undefined);
309
+ });
310
+ });
311
+ describe("compute.invokeUrlFetch — errors", () => {
312
+ it("wraps a JSON-RPC error from the proxy as AithosSDKError with the proxy code", async () => {
313
+ const fakeFetch = async () => new Response(JSON.stringify({
314
+ error: {
315
+ code: -32074,
316
+ message: "web_fetch failed: robots.txt disallows /api/* — fetch refused",
317
+ data: { detail: "robots_blocked" },
318
+ },
319
+ }), { status: 200, headers: { "content-type": "application/json" } });
320
+ const sdk = await makeSdk(fakeFetch);
321
+ await assert.rejects(sdk.compute.invokeUrlFetch({
322
+ mandateId: "mandate:abc",
323
+ prompt: "Analyse https://tata.com/api/secret",
324
+ }), (err) => {
325
+ assert.ok(err instanceof AithosSDKError);
326
+ assert.equal(err.code, "-32074");
327
+ assert.match(err.message, /robots\.txt/);
328
+ return true;
329
+ });
330
+ });
331
+ });
332
+ describe("compute.invokeUrlFetch — abort", () => {
333
+ it("propagates an AbortSignal to fetch", async () => {
334
+ let receivedSignal;
335
+ const fakeFetch = async (_input, init) => {
336
+ receivedSignal = init?.signal;
337
+ return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
338
+ status: 200,
339
+ headers: { "content-type": "application/json" },
340
+ });
341
+ };
342
+ const sdk = await makeSdk(fakeFetch);
343
+ const ac = new AbortController();
344
+ await sdk.compute.invokeUrlFetch({
345
+ mandateId: "mandate:abc",
346
+ prompt: "Analyse https://tata.com",
347
+ signal: ac.signal,
348
+ });
349
+ assert.equal(receivedSignal, ac.signal);
350
+ });
351
+ });
179
352
  //# sourceMappingURL=compute.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ethos-first-edition.test.d.ts.map