@aithos/sdk 0.1.0-alpha.3 → 0.1.0-alpha.30
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/README.md +159 -0
- package/dist/src/auth-api.d.ts +105 -0
- package/dist/src/auth-api.js +178 -0
- package/dist/src/auth.d.ts +359 -68
- package/dist/src/auth.js +1035 -69
- package/dist/src/compute.d.ts +221 -9
- package/dist/src/compute.js +293 -16
- package/dist/src/data-schema-contacts-v1.d.ts +14 -0
- package/dist/src/data-schema-contacts-v1.js +28 -0
- package/dist/src/data.d.ts +97 -0
- package/dist/src/data.js +634 -0
- package/dist/src/endpoints.d.ts +9 -0
- package/dist/src/endpoints.js +5 -0
- package/dist/src/ethos.d.ts +202 -1
- package/dist/src/ethos.js +821 -16
- package/dist/src/index.d.ts +15 -6
- package/dist/src/index.js +36 -9
- package/dist/src/internal/delegate-bundle.d.ts +18 -0
- package/dist/src/internal/delegate-bundle.js +94 -0
- package/dist/src/internal/delegate-state.d.ts +45 -0
- package/dist/src/internal/delegate-state.js +120 -0
- package/dist/src/internal/owner-signers.d.ts +78 -0
- package/dist/src/internal/owner-signers.js +179 -0
- package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
- package/dist/src/internal/protocol-client-bridge.js +20 -0
- package/dist/src/internal/recovery-file.d.ts +29 -0
- package/dist/src/internal/recovery-file.js +98 -0
- package/dist/src/internal/signer.d.ts +59 -0
- package/dist/src/internal/signer.js +86 -0
- package/dist/src/key-store.d.ts +128 -0
- package/dist/src/key-store.js +244 -0
- package/dist/src/mandates.d.ts +163 -1
- package/dist/src/mandates.js +286 -8
- package/dist/src/sdk.d.ts +39 -3
- package/dist/src/sdk.js +36 -23
- package/dist/src/session-store.d.ts +58 -0
- package/dist/src/session-store.js +158 -0
- package/dist/src/wallet.d.ts +4 -6
- package/dist/src/wallet.js +18 -8
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/dist/test/auth-j3.test.d.ts +2 -0
- package/dist/test/auth-j3.test.js +391 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +26 -11
- package/dist/test/endpoints.test.js +20 -1
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +248 -0
- package/dist/test/ethos.test.d.ts +2 -0
- package/dist/test/ethos.test.js +219 -0
- package/dist/test/key-store.test.d.ts +2 -0
- package/dist/test/key-store.test.js +161 -0
- package/dist/test/mandates-compute.test.d.ts +2 -0
- package/dist/test/mandates-compute.test.js +256 -0
- package/dist/test/mandates.test.d.ts +2 -0
- package/dist/test/mandates.test.js +93 -0
- package/dist/test/sdk.test.js +70 -30
- package/dist/test/signer.test.d.ts +2 -0
- package/dist/test/signer.test.js +117 -0
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +222 -0
- package/dist/test/wallet.test.js +20 -9
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +5 -4
|
@@ -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
|
|
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
|
-
|
|
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,8 @@ describe("compute.invokeBedrock — abort", () => {
|
|
|
176
187
|
assert.equal(receivedSignal, ac.signal);
|
|
177
188
|
});
|
|
178
189
|
});
|
|
190
|
+
// `compute.invokeUrlFetch` was removed in alpha.24 (BREAKING). The
|
|
191
|
+
// Anthropic API-direct + web_fetch tool path is replaced by the
|
|
192
|
+
// `sdk.web.extract` namespace which routes through the deterministic
|
|
193
|
+
// web-extractor Lambda. No tests here anymore.
|
|
179
194
|
//# sourceMappingURL=compute.test.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { strict as assert } from "node:assert";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { DEFAULT_SDK_ENDPOINTS } from "../src/index.js";
|
|
7
|
-
import { computeInvokeUrl, resolveEndpoints, walletTopupCheckoutUrl, } from "../src/endpoints.js";
|
|
7
|
+
import { computeInvokeUrl, resolveEndpoints, walletTopupCheckoutUrl, webInvokeUrl, } from "../src/endpoints.js";
|
|
8
8
|
describe("resolveEndpoints", () => {
|
|
9
9
|
it("returns a fresh copy of the defaults when no override is given", () => {
|
|
10
10
|
const a = resolveEndpoints();
|
|
@@ -23,12 +23,14 @@ describe("computeInvokeUrl", () => {
|
|
|
23
23
|
assert.equal(computeInvokeUrl({
|
|
24
24
|
compute: "https://compute.aithos.be",
|
|
25
25
|
wallet: "https://wallet.aithos.be",
|
|
26
|
+
web: "https://extract.aithos.be",
|
|
26
27
|
}), "https://compute.aithos.be/v1/invoke");
|
|
27
28
|
});
|
|
28
29
|
it("trims a trailing slash on the compute base", () => {
|
|
29
30
|
assert.equal(computeInvokeUrl({
|
|
30
31
|
compute: "https://compute.aithos.be/",
|
|
31
32
|
wallet: "https://wallet.aithos.be",
|
|
33
|
+
web: "https://extract.aithos.be",
|
|
32
34
|
}), "https://compute.aithos.be/v1/invoke");
|
|
33
35
|
});
|
|
34
36
|
});
|
|
@@ -37,7 +39,24 @@ describe("walletTopupCheckoutUrl", () => {
|
|
|
37
39
|
assert.equal(walletTopupCheckoutUrl({
|
|
38
40
|
compute: "https://compute.aithos.be",
|
|
39
41
|
wallet: "https://wallet.aithos.be",
|
|
42
|
+
web: "https://extract.aithos.be",
|
|
40
43
|
}), "https://wallet.aithos.be/v1/wallet/topup/checkout");
|
|
41
44
|
});
|
|
42
45
|
});
|
|
46
|
+
describe("webInvokeUrl", () => {
|
|
47
|
+
it("appends /v1/invoke to the web base", () => {
|
|
48
|
+
assert.equal(webInvokeUrl({
|
|
49
|
+
compute: "https://compute.aithos.be",
|
|
50
|
+
wallet: "https://wallet.aithos.be",
|
|
51
|
+
web: "https://extract.aithos.be",
|
|
52
|
+
}), "https://extract.aithos.be/v1/invoke");
|
|
53
|
+
});
|
|
54
|
+
it("trims a trailing slash on the web base", () => {
|
|
55
|
+
assert.equal(webInvokeUrl({
|
|
56
|
+
compute: "https://compute.aithos.be",
|
|
57
|
+
wallet: "https://wallet.aithos.be",
|
|
58
|
+
web: "https://extract.aithos.be/",
|
|
59
|
+
}), "https://extract.aithos.be/v1/invoke");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
43
62
|
//# sourceMappingURL=endpoints.test.js.map
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for the alpha.7 first-edition path in EthosClient — the case
|
|
4
|
+
// where an Ethos identity exists on api.aithos.be (provisioned by
|
|
5
|
+
// auth.signUp() since alpha.6) but no edition has been published yet.
|
|
6
|
+
//
|
|
7
|
+
// Two flows must work:
|
|
8
|
+
// 1. Reading any zone returns an empty list (instead of throwing
|
|
9
|
+
// "not found: edition").
|
|
10
|
+
// 2. Publishing for the first time builds height=1 from staged
|
|
11
|
+
// mutations and POSTs publish_ethos_edition directly, instead
|
|
12
|
+
// of going through publishZoneEdit (which requires a previous
|
|
13
|
+
// manifest).
|
|
14
|
+
//
|
|
15
|
+
// We mock global fetch end-to-end so the tests run offline.
|
|
16
|
+
import { strict as assert } from "node:assert";
|
|
17
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
18
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
19
|
+
import { AithosAuth, AithosSDKError, EthosNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
20
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
21
|
+
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
22
|
+
let fetchCalls = [];
|
|
23
|
+
let savedFetch;
|
|
24
|
+
function installFetchMock(handlers) {
|
|
25
|
+
savedFetch = globalThis.fetch;
|
|
26
|
+
fetchCalls = [];
|
|
27
|
+
globalThis.fetch = (async (input, init) => {
|
|
28
|
+
const url = String(input);
|
|
29
|
+
const method = init?.method ?? "GET";
|
|
30
|
+
const bodyText = typeof init?.body === "string"
|
|
31
|
+
? init.body
|
|
32
|
+
: init?.body == null
|
|
33
|
+
? null
|
|
34
|
+
: String(init.body);
|
|
35
|
+
const body = bodyText ? JSON.parse(bodyText) : null;
|
|
36
|
+
const call = { url, method, body };
|
|
37
|
+
fetchCalls.push(call);
|
|
38
|
+
for (const h of handlers) {
|
|
39
|
+
if (!url.includes(h.url))
|
|
40
|
+
continue;
|
|
41
|
+
if (h.rpcMethod && body?.method !== h.rpcMethod)
|
|
42
|
+
continue;
|
|
43
|
+
const out = h.respond(call);
|
|
44
|
+
const status = out.status ?? 200;
|
|
45
|
+
return new Response(JSON.stringify(out.json), {
|
|
46
|
+
status,
|
|
47
|
+
headers: { "content-type": "application/json" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`unhandled fetch: ${method} ${url} (rpc: ${body?.method ?? "n/a"})`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function uninstallFetchMock() {
|
|
54
|
+
if (savedFetch) {
|
|
55
|
+
globalThis.fetch = savedFetch;
|
|
56
|
+
savedFetch = undefined;
|
|
57
|
+
}
|
|
58
|
+
fetchCalls = [];
|
|
59
|
+
}
|
|
60
|
+
function makeAuth() {
|
|
61
|
+
return new AithosAuth({
|
|
62
|
+
authBaseUrl: "https://auth.test",
|
|
63
|
+
apiBaseUrl: "https://api.test",
|
|
64
|
+
fetch: (() => {
|
|
65
|
+
throw new Error("AithosAuth.fetch must not be called in these tests");
|
|
66
|
+
}),
|
|
67
|
+
sessionStore: noopStore(),
|
|
68
|
+
keyStore: memoryKeyStore(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function makeNamespace(auth) {
|
|
72
|
+
return new EthosNamespace({
|
|
73
|
+
auth,
|
|
74
|
+
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
75
|
+
// EthosNamespace itself doesn't read fetch from this slot today;
|
|
76
|
+
// protocol-client uses the global fetch we mock above.
|
|
77
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function signInAsAlice(auth) {
|
|
81
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
82
|
+
const { text } = serializeRecoveryFile(id);
|
|
83
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
84
|
+
return { did: info.did };
|
|
85
|
+
}
|
|
86
|
+
function noEditionYetResponse() {
|
|
87
|
+
// Mirrors the server's primitives-read `notFound("edition for <did>")`:
|
|
88
|
+
// JSON-RPC error code -32020, message starts with "not found: edition for ".
|
|
89
|
+
return {
|
|
90
|
+
json: {
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
id: "aithos.get_ethos_manifest",
|
|
93
|
+
error: {
|
|
94
|
+
code: -32020,
|
|
95
|
+
message: "not found: edition for did:aithos:zSomething",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function publishOkResponse() {
|
|
101
|
+
return {
|
|
102
|
+
json: {
|
|
103
|
+
jsonrpc: "2.0",
|
|
104
|
+
id: "publish_ethos_edition",
|
|
105
|
+
result: { ok: true, height: 1, manifest_uri: "s3://aithos/.../manifest.json" },
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/* -------------------------------------------------------------------------- */
|
|
110
|
+
/* Tests */
|
|
111
|
+
/* -------------------------------------------------------------------------- */
|
|
112
|
+
describe("EthosClient — fresh Ethos (no edition published yet)", () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
fetchCalls = [];
|
|
115
|
+
});
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
uninstallFetchMock();
|
|
118
|
+
});
|
|
119
|
+
it("zone(public).sections() returns [] when server says 'not found: edition'", async () => {
|
|
120
|
+
installFetchMock([
|
|
121
|
+
{
|
|
122
|
+
url: "/mcp/primitives/read",
|
|
123
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
124
|
+
respond: noEditionYetResponse,
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
const auth = makeAuth();
|
|
128
|
+
await signInAsAlice(auth);
|
|
129
|
+
const me = makeNamespace(auth).me();
|
|
130
|
+
const sections = await me.zone("public").sections();
|
|
131
|
+
assert.deepEqual(sections, []);
|
|
132
|
+
});
|
|
133
|
+
it("zone(public).sections() reflects locally staged adds even when no edition exists", async () => {
|
|
134
|
+
installFetchMock([
|
|
135
|
+
{
|
|
136
|
+
url: "/mcp/primitives/read",
|
|
137
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
138
|
+
respond: noEditionYetResponse,
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
const auth = makeAuth();
|
|
142
|
+
await signInAsAlice(auth);
|
|
143
|
+
const me = makeNamespace(auth).me();
|
|
144
|
+
me.zone("public").addSection({ title: "Hello", body: "World" });
|
|
145
|
+
const sections = await me.zone("public").sections();
|
|
146
|
+
assert.equal(sections.length, 1);
|
|
147
|
+
assert.equal(sections[0].title, "Hello");
|
|
148
|
+
});
|
|
149
|
+
it("publish() routes to publish_ethos_edition with height=1 on first publish", async () => {
|
|
150
|
+
let publishBody = null;
|
|
151
|
+
installFetchMock([
|
|
152
|
+
{
|
|
153
|
+
url: "/mcp/primitives/read",
|
|
154
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
155
|
+
respond: noEditionYetResponse,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
url: "/mcp/primitives/write",
|
|
159
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
160
|
+
respond: (call) => {
|
|
161
|
+
publishBody = call.body;
|
|
162
|
+
return publishOkResponse();
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
const auth = makeAuth();
|
|
167
|
+
const alice = await signInAsAlice(auth);
|
|
168
|
+
const me = makeNamespace(auth).me();
|
|
169
|
+
me.zone("public").addSection({ title: "First", body: "Hello." });
|
|
170
|
+
me.zone("public").addSection({ title: "Second", body: "World." });
|
|
171
|
+
const r = await me.publish();
|
|
172
|
+
assert.equal(r.editionHeight, 1);
|
|
173
|
+
assert.equal(r.subjectDid, alice.did);
|
|
174
|
+
assert.deepEqual(r.zonesPublished, ["public"]);
|
|
175
|
+
// Verify the wire shape: JSON-RPC publish_ethos_edition with a height=1
|
|
176
|
+
// manifest containing both staged sections.
|
|
177
|
+
assert.equal(publishBody.method, "aithos.publish_ethos_edition");
|
|
178
|
+
const manifest = publishBody.params.manifest;
|
|
179
|
+
assert.equal(manifest.edition.height, 1);
|
|
180
|
+
assert.equal(manifest.edition.prev_hash, null);
|
|
181
|
+
assert.equal(manifest.edition.supersedes, null);
|
|
182
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["First", "Second"]);
|
|
183
|
+
// Envelope is signed under #public.
|
|
184
|
+
const env = publishBody.params._envelope;
|
|
185
|
+
assert.equal(env.method, "aithos.publish_ethos_edition");
|
|
186
|
+
assert.match(env.proof.verificationMethod, /#public$/);
|
|
187
|
+
});
|
|
188
|
+
it("publish() rejects circle/self mutations on first edition", async () => {
|
|
189
|
+
installFetchMock([
|
|
190
|
+
{
|
|
191
|
+
url: "/mcp/primitives/read",
|
|
192
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
193
|
+
respond: noEditionYetResponse,
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
const auth = makeAuth();
|
|
197
|
+
await signInAsAlice(auth);
|
|
198
|
+
const me = makeNamespace(auth).me();
|
|
199
|
+
me.zone("circle").addSection({ title: "Private", body: "..." });
|
|
200
|
+
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_public_only");
|
|
201
|
+
});
|
|
202
|
+
it("publish() rejects update/delete operations on a fresh Ethos", async () => {
|
|
203
|
+
installFetchMock([
|
|
204
|
+
{
|
|
205
|
+
url: "/mcp/primitives/read",
|
|
206
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
207
|
+
respond: noEditionYetResponse,
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
const auth = makeAuth();
|
|
211
|
+
await signInAsAlice(auth);
|
|
212
|
+
const me = makeNamespace(auth).me();
|
|
213
|
+
// Stage a delete for a section that doesn't exist (no edition exists at all).
|
|
214
|
+
me.zone("public")["_parent"]; // type-safety placeholder; we use the public API
|
|
215
|
+
// EthosZone exposes deleteSection — go via that.
|
|
216
|
+
me.zone("public").deleteSection("sec_doesnotexist000");
|
|
217
|
+
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_invalid_op");
|
|
218
|
+
});
|
|
219
|
+
it("publish() surfaces server JSON-RPC errors as ethos_first_edition_rejected", async () => {
|
|
220
|
+
installFetchMock([
|
|
221
|
+
{
|
|
222
|
+
url: "/mcp/primitives/read",
|
|
223
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
224
|
+
respond: noEditionYetResponse,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
url: "/mcp/primitives/write",
|
|
228
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
229
|
+
respond: () => ({
|
|
230
|
+
json: {
|
|
231
|
+
jsonrpc: "2.0",
|
|
232
|
+
id: "publish_ethos_edition",
|
|
233
|
+
error: {
|
|
234
|
+
code: -32020,
|
|
235
|
+
message: "subject identity not published (call publish_identity first)",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
]);
|
|
241
|
+
const auth = makeAuth();
|
|
242
|
+
await signInAsAlice(auth);
|
|
243
|
+
const me = makeNamespace(auth).me();
|
|
244
|
+
me.zone("public").addSection({ title: "Hi", body: "There." });
|
|
245
|
+
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_rejected");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
//# sourceMappingURL=ethos-first-edition.test.js.map
|