@aithos/sdk 0.1.0-alpha.55 → 0.1.0-alpha.58
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/src/agent-dispatch.d.ts +18 -0
- package/dist/src/agent-dispatch.js +178 -0
- package/dist/src/agent-loop.d.ts +94 -0
- package/dist/src/agent-loop.js +95 -0
- package/dist/src/agent-tools.d.ts +24 -0
- package/dist/src/agent-tools.js +147 -0
- package/dist/src/auth.d.ts +59 -0
- package/dist/src/auth.js +121 -0
- package/dist/src/compute.d.ts +112 -0
- package/dist/src/compute.js +175 -0
- package/dist/src/data.d.ts +14 -9
- package/dist/src/data.js +102 -53
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +12 -2
- package/dist/test/agent-dispatch.test.d.ts +2 -0
- package/dist/test/agent-dispatch.test.js +222 -0
- package/dist/test/agent-loop.test.d.ts +2 -0
- package/dist/test/agent-loop.test.js +117 -0
- package/dist/test/agent-tools.test.d.ts +2 -0
- package/dist/test/agent-tools.test.js +50 -0
- package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
- package/dist/test/invoke-turn-sdk.test.js +177 -0
- package/dist/test/owner-data-client.test.d.ts +2 -0
- package/dist/test/owner-data-client.test.js +88 -0
- package/dist/test/schema-autoresolve.test.js +14 -4
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for the pure CLIENT-SIDE agentic loop (ported from the proxy's
|
|
4
|
+
// converse-loop tests, adapted to the async local dispatch + per-turn billing
|
|
5
|
+
// accumulation).
|
|
6
|
+
import { strict as assert } from "node:assert";
|
|
7
|
+
import { describe, it } from "node:test";
|
|
8
|
+
import { runAgenticLoopLocal, } from "../src/index.js";
|
|
9
|
+
function turn(content, stopReason, i = 100, o = 50, credits = 10, balance = 1000) {
|
|
10
|
+
return {
|
|
11
|
+
content,
|
|
12
|
+
stopReason,
|
|
13
|
+
usage: { inputTokens: i, outputTokens: o },
|
|
14
|
+
creditsCharged: credits,
|
|
15
|
+
walletBalance: balance,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const okDispatch = async () => ({
|
|
19
|
+
payload: '{"ok":true}',
|
|
20
|
+
isError: false,
|
|
21
|
+
});
|
|
22
|
+
describe("runAgenticLoopLocal", () => {
|
|
23
|
+
it("returns immediately on a single end_turn (no tools)", async () => {
|
|
24
|
+
let calls = 0;
|
|
25
|
+
const r = await runAgenticLoopLocal({
|
|
26
|
+
messages: [{ role: "user", content: "salut" }],
|
|
27
|
+
maxIterations: 6,
|
|
28
|
+
invokeTurn: async () => {
|
|
29
|
+
calls++;
|
|
30
|
+
return turn([{ type: "text", text: "Réponse directe." }], "end_turn");
|
|
31
|
+
},
|
|
32
|
+
dispatch: okDispatch,
|
|
33
|
+
});
|
|
34
|
+
assert.equal(r.iterations, 1);
|
|
35
|
+
assert.equal(r.stopReason, "end_turn");
|
|
36
|
+
assert.equal(r.finalContent, "Réponse directe.");
|
|
37
|
+
assert.deepEqual(r.toolCalls, []);
|
|
38
|
+
assert.equal(calls, 1);
|
|
39
|
+
assert.equal(r.creditsCharged, 10);
|
|
40
|
+
assert.equal(r.walletBalance, 1000);
|
|
41
|
+
});
|
|
42
|
+
it("runs a tool turn then concludes, summing usage + per-turn credits", async () => {
|
|
43
|
+
const seq = [
|
|
44
|
+
turn([
|
|
45
|
+
{ type: "text", text: "je lis" },
|
|
46
|
+
{ type: "tool_use", id: "tu1", name: "ethos_read_section", input: { section_id: "s1" } },
|
|
47
|
+
], "tool_use", 100, 20, 7, 993),
|
|
48
|
+
turn([{ type: "text", text: "Fini." }], "end_turn", 50, 30, 5, 988),
|
|
49
|
+
];
|
|
50
|
+
let n = 0;
|
|
51
|
+
const dispatched = [];
|
|
52
|
+
const r = await runAgenticLoopLocal({
|
|
53
|
+
messages: [{ role: "user", content: "lis s1" }],
|
|
54
|
+
maxIterations: 6,
|
|
55
|
+
invokeTurn: async (messages) => {
|
|
56
|
+
// After the first turn, the running list must carry the assistant
|
|
57
|
+
// tool_use turn + the user tool_result turn.
|
|
58
|
+
if (n === 1) {
|
|
59
|
+
assert.equal(messages.length, 3);
|
|
60
|
+
assert.equal(messages[1].role, "assistant");
|
|
61
|
+
assert.equal(messages[2].role, "user");
|
|
62
|
+
}
|
|
63
|
+
return seq[n++];
|
|
64
|
+
},
|
|
65
|
+
dispatch: async (name) => {
|
|
66
|
+
dispatched.push(name);
|
|
67
|
+
return { payload: '{"body":"x"}', isError: false };
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
assert.equal(r.iterations, 2);
|
|
71
|
+
assert.equal(r.stopReason, "end_turn");
|
|
72
|
+
assert.equal(r.finalContent, "Fini.");
|
|
73
|
+
assert.deepEqual(dispatched, ["ethos_read_section"]);
|
|
74
|
+
assert.deepEqual(r.toolCalls, [{ name: "ethos_read_section", ok: true, turn: 1 }]);
|
|
75
|
+
assert.equal(r.usage.inputTokens, 150);
|
|
76
|
+
assert.equal(r.usage.outputTokens, 50);
|
|
77
|
+
assert.equal(r.creditsCharged, 12); // 7 + 5
|
|
78
|
+
assert.equal(r.walletBalance, 988); // last turn
|
|
79
|
+
});
|
|
80
|
+
it("a tool error becomes is_error and does NOT stop the loop", async () => {
|
|
81
|
+
const seq = [
|
|
82
|
+
turn([{ type: "tool_use", id: "tu1", name: "ethos_add_section", input: {} }], "tool_use"),
|
|
83
|
+
turn([{ type: "text", text: "ok malgré erreur" }], "end_turn"),
|
|
84
|
+
];
|
|
85
|
+
let n = 0;
|
|
86
|
+
const r = await runAgenticLoopLocal({
|
|
87
|
+
messages: [{ role: "user", content: "ajoute" }],
|
|
88
|
+
maxIterations: 6,
|
|
89
|
+
invokeTurn: async () => seq[n++],
|
|
90
|
+
dispatch: async () => ({ payload: '{"error":"nope"}', isError: true }),
|
|
91
|
+
});
|
|
92
|
+
assert.equal(r.stopReason, "end_turn");
|
|
93
|
+
assert.deepEqual(r.toolCalls, [{ name: "ethos_add_section", ok: false, turn: 1 }]);
|
|
94
|
+
assert.equal(r.finalContent, "ok malgré erreur");
|
|
95
|
+
});
|
|
96
|
+
it("stops at the iteration cap when the model keeps calling tools", async () => {
|
|
97
|
+
let calls = 0;
|
|
98
|
+
const r = await runAgenticLoopLocal({
|
|
99
|
+
messages: [{ role: "user", content: "boucle" }],
|
|
100
|
+
maxIterations: 3,
|
|
101
|
+
invokeTurn: async () => {
|
|
102
|
+
calls++;
|
|
103
|
+
return turn([
|
|
104
|
+
{ type: "text", text: `t${calls}` },
|
|
105
|
+
{ type: "tool_use", id: `tu${calls}`, name: "ethos_list_sections", input: {} },
|
|
106
|
+
], "tool_use");
|
|
107
|
+
},
|
|
108
|
+
dispatch: okDispatch,
|
|
109
|
+
});
|
|
110
|
+
assert.equal(r.stopReason, "max_iterations");
|
|
111
|
+
assert.equal(r.iterations, 3);
|
|
112
|
+
assert.equal(calls, 3);
|
|
113
|
+
assert.equal(r.toolCalls.length, 3);
|
|
114
|
+
assert.equal(r.finalContent, "t3");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=agent-loop.test.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
import { strict as assert } from "node:assert";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { AITHOS_AGENT_TOOLS, AITHOS_AGENT_READ_TOOLS, AITHOS_AGENT_WRITE_TOOLS, selectAgentTools, isWriteTool, } from "../src/index.js";
|
|
6
|
+
const names = (ts) => ts.map((t) => t.name).sort();
|
|
7
|
+
describe("agent tool catalogue", () => {
|
|
8
|
+
it("the full catalogue = read + write families", () => {
|
|
9
|
+
assert.equal(AITHOS_AGENT_TOOLS.length, AITHOS_AGENT_READ_TOOLS.length + AITHOS_AGENT_WRITE_TOOLS.length);
|
|
10
|
+
assert.deepEqual(names(AITHOS_AGENT_READ_TOOLS), [
|
|
11
|
+
"data_query",
|
|
12
|
+
"ethos_list_sections",
|
|
13
|
+
"ethos_read_section",
|
|
14
|
+
]);
|
|
15
|
+
assert.deepEqual(names(AITHOS_AGENT_WRITE_TOOLS), [
|
|
16
|
+
"ethos_add_section",
|
|
17
|
+
"ethos_delete_section",
|
|
18
|
+
"ethos_update_section",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
it("every tool has a name, description, and object input_schema", () => {
|
|
22
|
+
for (const t of AITHOS_AGENT_TOOLS) {
|
|
23
|
+
assert.ok(t.name.length > 0);
|
|
24
|
+
assert.ok(t.description.length > 0);
|
|
25
|
+
assert.equal(typeof t.input_schema, "object");
|
|
26
|
+
assert.equal(t.input_schema.type, "object");
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it("isWriteTool flags only the write family", () => {
|
|
30
|
+
assert.equal(isWriteTool("ethos_add_section"), true);
|
|
31
|
+
assert.equal(isWriteTool("ethos_update_section"), true);
|
|
32
|
+
assert.equal(isWriteTool("ethos_delete_section"), true);
|
|
33
|
+
assert.equal(isWriteTool("ethos_read_section"), false);
|
|
34
|
+
assert.equal(isWriteTool("data_query"), false);
|
|
35
|
+
assert.equal(isWriteTool("bogus"), false);
|
|
36
|
+
});
|
|
37
|
+
it("selectAgentTools: default = full catalogue", () => {
|
|
38
|
+
assert.equal(selectAgentTools().length, AITHOS_AGENT_TOOLS.length);
|
|
39
|
+
assert.equal(selectAgentTools({}).length, AITHOS_AGENT_TOOLS.length);
|
|
40
|
+
assert.equal(selectAgentTools({ tools: [] }).length, AITHOS_AGENT_TOOLS.length);
|
|
41
|
+
});
|
|
42
|
+
it("selectAgentTools: readOnly = read family only", () => {
|
|
43
|
+
assert.deepEqual(names(selectAgentTools({ readOnly: true })), names(AITHOS_AGENT_READ_TOOLS));
|
|
44
|
+
});
|
|
45
|
+
it("selectAgentTools: subset by name, unknown names ignored", () => {
|
|
46
|
+
const sel = selectAgentTools({ tools: ["ethos_add_section", "nope", "data_query"] });
|
|
47
|
+
assert.deepEqual(names(sel), ["data_query", "ethos_add_section"]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=agent-tools.test.js.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Wire tests for sdk.compute.invokeTurn + sdk.compute.runConversationLocal,
|
|
4
|
+
// mirroring converse.test.ts: a real BrowserIdentity drives envelope signing
|
|
5
|
+
// and we assert on the JSON-RPC body posted to the proxy.
|
|
6
|
+
import { strict as assert } from "node:assert";
|
|
7
|
+
import { describe, it } from "node:test";
|
|
8
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
9
|
+
import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, AITHOS_AGENT_TOOLS, } from "../src/index.js";
|
|
10
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
11
|
+
const APP_DID = "did:aithos:app:test";
|
|
12
|
+
async function makeSdk(fetchImpl) {
|
|
13
|
+
const id = createBrowserIdentity("turn-handle", "Turn User");
|
|
14
|
+
const auth = new AithosAuth({
|
|
15
|
+
authBaseUrl: "https://auth.test",
|
|
16
|
+
fetch: (() => {
|
|
17
|
+
throw new Error("auth not used");
|
|
18
|
+
}),
|
|
19
|
+
sessionStore: noopStore(),
|
|
20
|
+
keyStore: memoryKeyStore(),
|
|
21
|
+
});
|
|
22
|
+
const { text } = serializeRecoveryFile(id);
|
|
23
|
+
await auth.signInWithRecovery({ file: text });
|
|
24
|
+
const sdk = new AithosSDK({
|
|
25
|
+
auth,
|
|
26
|
+
appDid: APP_DID,
|
|
27
|
+
endpoints: { compute: "https://compute.example.test" },
|
|
28
|
+
fetch: fetchImpl,
|
|
29
|
+
});
|
|
30
|
+
return { sdk, did: id.did };
|
|
31
|
+
}
|
|
32
|
+
const TURN_RESULT = {
|
|
33
|
+
content: [{ type: "text", text: "Réponse." }],
|
|
34
|
+
stopReason: "end_turn",
|
|
35
|
+
usage: { inputTokens: 100, outputTokens: 20 },
|
|
36
|
+
creditsCharged: 12,
|
|
37
|
+
walletBalance: 9_988,
|
|
38
|
+
auditId: "audit-turn-1",
|
|
39
|
+
fundedBy: "purchase",
|
|
40
|
+
};
|
|
41
|
+
describe("compute.invokeTurn — wire", () => {
|
|
42
|
+
it("posts aithos.compute_invoke_turn with tools + mapped params", async () => {
|
|
43
|
+
let capturedUrl;
|
|
44
|
+
let capturedInit;
|
|
45
|
+
const fakeFetch = async (input, init) => {
|
|
46
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
47
|
+
capturedInit = init;
|
|
48
|
+
return new Response(JSON.stringify({ result: TURN_RESULT }), {
|
|
49
|
+
status: 200,
|
|
50
|
+
headers: { "content-type": "application/json" },
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const { sdk } = await makeSdk(fakeFetch);
|
|
54
|
+
const out = await sdk.compute.invokeTurn({
|
|
55
|
+
mandateId: "mandate:abc",
|
|
56
|
+
model: "claude-sonnet-4-6",
|
|
57
|
+
system: "sys",
|
|
58
|
+
messages: [{ role: "user", content: "salut" }],
|
|
59
|
+
tools: [
|
|
60
|
+
{
|
|
61
|
+
name: "ethos_list_sections",
|
|
62
|
+
description: "list",
|
|
63
|
+
input_schema: { type: "object", properties: {} },
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
maxTokens: 512,
|
|
67
|
+
temperature: 0.2,
|
|
68
|
+
});
|
|
69
|
+
assert.deepEqual(out, TURN_RESULT);
|
|
70
|
+
assert.equal(capturedUrl, "https://compute.example.test/v1/invoke");
|
|
71
|
+
const body = JSON.parse(capturedInit?.body);
|
|
72
|
+
assert.equal(body.method, "aithos.compute_invoke_turn");
|
|
73
|
+
assert.equal(body.params.app_did, APP_DID);
|
|
74
|
+
assert.equal(body.params.mandate_id, "mandate:abc");
|
|
75
|
+
assert.equal(body.params.model, "claude-sonnet-4-6");
|
|
76
|
+
assert.equal(body.params.system, "sys");
|
|
77
|
+
assert.equal(body.params.max_tokens, 512);
|
|
78
|
+
assert.equal(body.params.temperature, 0.2);
|
|
79
|
+
assert.ok(Array.isArray(body.params.tools), "tools must be on the wire");
|
|
80
|
+
assert.equal(body.params.tools.length, 1);
|
|
81
|
+
assert.match(body.params.idempotency_key, /^[0-9a-f]{32}$/);
|
|
82
|
+
assert.ok(body.params._envelope, "must carry a signed envelope");
|
|
83
|
+
// SDK-only camelCase keys must not leak.
|
|
84
|
+
assert.equal(body.params.maxTokens, undefined);
|
|
85
|
+
});
|
|
86
|
+
it("maps a JSON-RPC error to AithosSDKError", async () => {
|
|
87
|
+
const fakeFetch = async () => new Response(JSON.stringify({ error: { code: -32071, message: "insufficient credits" } }), {
|
|
88
|
+
status: 200,
|
|
89
|
+
headers: { "content-type": "application/json" },
|
|
90
|
+
});
|
|
91
|
+
const { sdk } = await makeSdk(fakeFetch);
|
|
92
|
+
await assert.rejects(() => sdk.compute.invokeTurn({
|
|
93
|
+
model: "claude-sonnet-4-6",
|
|
94
|
+
messages: [{ role: "user", content: "hi" }],
|
|
95
|
+
tools: [],
|
|
96
|
+
}), (err) => {
|
|
97
|
+
assert.ok(err instanceof AithosSDKError);
|
|
98
|
+
assert.equal(err.code, "-32071");
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("compute.runConversationLocal — wiring", () => {
|
|
104
|
+
it("drives the loop: single end_turn → 1 iteration, full catalogue forwarded", async () => {
|
|
105
|
+
const bodies = [];
|
|
106
|
+
const fakeFetch = async (_input, init) => {
|
|
107
|
+
bodies.push(JSON.parse(init?.body));
|
|
108
|
+
return new Response(JSON.stringify({ result: TURN_RESULT }), {
|
|
109
|
+
status: 200,
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const { sdk } = await makeSdk(fakeFetch);
|
|
114
|
+
const out = await sdk.compute.runConversationLocal({
|
|
115
|
+
model: "claude-sonnet-4-6",
|
|
116
|
+
messages: [{ role: "user", content: "Mets à jour ma bio si besoin." }],
|
|
117
|
+
system: "Écris seulement si nécessaire.",
|
|
118
|
+
});
|
|
119
|
+
assert.equal(out.iterations, 1);
|
|
120
|
+
assert.equal(out.stopReason, "end_turn");
|
|
121
|
+
assert.equal(out.content, "Réponse.");
|
|
122
|
+
assert.equal(out.creditsCharged, 12);
|
|
123
|
+
assert.equal(out.walletBalance, 9_988);
|
|
124
|
+
assert.equal(out.auditId, "audit-turn-1");
|
|
125
|
+
assert.equal(out.fundedBy, "purchase");
|
|
126
|
+
assert.deepEqual(out.toolCalls, []);
|
|
127
|
+
// One proxy turn, the right method, the full tool catalogue forwarded.
|
|
128
|
+
assert.equal(bodies.length, 1);
|
|
129
|
+
assert.equal(bodies[0].method, "aithos.compute_invoke_turn");
|
|
130
|
+
assert.equal(bodies[0].params.tools.length, AITHOS_AGENT_TOOLS.length);
|
|
131
|
+
});
|
|
132
|
+
it("readOnly forwards only the read family", async () => {
|
|
133
|
+
const bodies = [];
|
|
134
|
+
const fakeFetch = async (_input, init) => {
|
|
135
|
+
bodies.push(JSON.parse(init?.body));
|
|
136
|
+
return new Response(JSON.stringify({ result: TURN_RESULT }), {
|
|
137
|
+
status: 200,
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const { sdk } = await makeSdk(fakeFetch);
|
|
142
|
+
await sdk.compute.runConversationLocal({
|
|
143
|
+
model: "claude-sonnet-4-6",
|
|
144
|
+
messages: [{ role: "user", content: "résume" }],
|
|
145
|
+
readOnly: true,
|
|
146
|
+
});
|
|
147
|
+
const toolNames = bodies[0].params.tools.map((t) => t.name).sort();
|
|
148
|
+
assert.deepEqual(toolNames, ["data_query", "ethos_list_sections", "ethos_read_section"]);
|
|
149
|
+
});
|
|
150
|
+
it("throws sdk_no_signer when no owner and no mandate/subject", async () => {
|
|
151
|
+
const auth = new AithosAuth({
|
|
152
|
+
authBaseUrl: "https://auth.test",
|
|
153
|
+
fetch: (() => {
|
|
154
|
+
throw new Error("unused");
|
|
155
|
+
}),
|
|
156
|
+
sessionStore: noopStore(),
|
|
157
|
+
keyStore: memoryKeyStore(),
|
|
158
|
+
});
|
|
159
|
+
const sdk = new AithosSDK({
|
|
160
|
+
auth,
|
|
161
|
+
appDid: APP_DID,
|
|
162
|
+
endpoints: { compute: "https://compute.example.test" },
|
|
163
|
+
fetch: (() => {
|
|
164
|
+
throw new Error("fetch must not be reached");
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
await assert.rejects(() => sdk.compute.runConversationLocal({
|
|
168
|
+
model: "claude-sonnet-4-6",
|
|
169
|
+
messages: [{ role: "user", content: "hi" }],
|
|
170
|
+
}), (err) => {
|
|
171
|
+
assert.ok(err instanceof AithosSDKError);
|
|
172
|
+
assert.equal(err.code, "sdk_no_signer");
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
//# sourceMappingURL=invoke-turn-sdk.test.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Phase D — auth.ownerDataClient() returns a session-bound data client that
|
|
4
|
+
// signs + seals under the dedicated #data sphere (not #root). This is the
|
|
5
|
+
// ergonomic factory apps use so collections are created under #data by default.
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { strict as assert } from "node:assert";
|
|
8
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
9
|
+
import { AithosAuth, memoryKeyStore, noopStore } from "../src/index.js";
|
|
10
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
11
|
+
import { ed25519SeedToX25519PrivateKey, tryUnwrapCmk, } from "../src/internal/cmk-wrap.js";
|
|
12
|
+
function makePds() {
|
|
13
|
+
const collections = new Map();
|
|
14
|
+
const records = new Map();
|
|
15
|
+
const fetchImpl = (async (_url, init) => {
|
|
16
|
+
const body = JSON.parse(init.body);
|
|
17
|
+
const p = body.params ?? {};
|
|
18
|
+
const ok = (r) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result: r }), { status: 200 });
|
|
19
|
+
const er = (c, m) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, error: { code: c, message: m } }), { status: 200 });
|
|
20
|
+
const urn = (d, n) => `urn:aithos:collection:${d}:${n}`;
|
|
21
|
+
switch (body.method) {
|
|
22
|
+
case "aithos.data.create_collection": {
|
|
23
|
+
const u = urn(p.subject_did, p.collection_name);
|
|
24
|
+
collections.set(u, { urn: u, name: p.collection_name, schema: p.schema, subject_did: p.subject_did, cmk_envelope: p.cmk_envelope, record_count: 0 });
|
|
25
|
+
records.set(u, new Map());
|
|
26
|
+
return ok({ urn: u, name: p.collection_name, schema: p.schema, cmk_envelope: p.cmk_envelope });
|
|
27
|
+
}
|
|
28
|
+
case "aithos.data.get_collection": {
|
|
29
|
+
const c = collections.get(urn(p.subject_did, p.collection_name));
|
|
30
|
+
return c ? ok({ urn: c.urn, name: c.name, schema: c.schema, cmk_envelope: c.cmk_envelope, record_count: c.record_count }) : er(-32020, "nf");
|
|
31
|
+
}
|
|
32
|
+
case "aithos.data.insert_record": {
|
|
33
|
+
records.get(p.collection_urn).set(p.record_id, { record_id: p.record_id, metadata: p.metadata, payload: p.payload });
|
|
34
|
+
return ok({ record_id: p.record_id });
|
|
35
|
+
}
|
|
36
|
+
case "aithos.data.get_record": {
|
|
37
|
+
const r = records.get(p.collection_urn)?.get(p.record_id);
|
|
38
|
+
return r ? ok(r) : er(-32020, "nf");
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
return er(-32601, body.method);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return { fetchImpl, collections };
|
|
45
|
+
}
|
|
46
|
+
function authWith(fetchImpl) {
|
|
47
|
+
return new AithosAuth({
|
|
48
|
+
authBaseUrl: "https://auth.test",
|
|
49
|
+
apiBaseUrl: "https://api.test",
|
|
50
|
+
fetch: fetchImpl,
|
|
51
|
+
sessionStore: noopStore(),
|
|
52
|
+
keyStore: memoryKeyStore(),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
test("ownerDataClient seals collections under #data (not #root)", async () => {
|
|
56
|
+
const id = createBrowserIdentity("phased", "Phase D");
|
|
57
|
+
assert.ok(id.data, "fresh identity has #data");
|
|
58
|
+
const { text } = serializeRecoveryFile(id);
|
|
59
|
+
const pds = makePds();
|
|
60
|
+
const auth = authWith(pds.fetchImpl);
|
|
61
|
+
await auth.signInWithRecovery({ file: text });
|
|
62
|
+
const data = auth.ownerDataClient({ pdsUrl: "https://pds.test" });
|
|
63
|
+
await data.createCollection({ name: "contacts", schema: "aithos.contacts.v1" });
|
|
64
|
+
const recId = await data.collection("contacts").insert({ name: "Iris", phone: "+33-phaseD" });
|
|
65
|
+
// Round-trips under #data.
|
|
66
|
+
const got = await data.collection("contacts").get(recId);
|
|
67
|
+
assert.equal(got.phone, "+33-phaseD");
|
|
68
|
+
// The owner wrap opens with #data and NOT with #root → proves the sealing key.
|
|
69
|
+
const urn = `urn:aithos:collection:${id.did}:contacts`;
|
|
70
|
+
const env = pds.collections.get(urn).cmk_envelope;
|
|
71
|
+
const ownerWrap = env.wraps.find((w) => w.recipient === `${id.did}#data-kex`);
|
|
72
|
+
const opensWithData = tryUnwrapCmk({ wrap: ownerWrap, collectionUrn: urn, privateKey: ed25519SeedToX25519PrivateKey(id.data.seed) });
|
|
73
|
+
const opensWithRoot = tryUnwrapCmk({ wrap: ownerWrap, collectionUrn: urn, privateKey: ed25519SeedToX25519PrivateKey(id.root.seed) });
|
|
74
|
+
assert.ok(opensWithData, "owner wrap opens with #data");
|
|
75
|
+
assert.equal(opensWithRoot, null, "owner wrap does NOT open with #root");
|
|
76
|
+
});
|
|
77
|
+
test("ownerDataClient throws for a legacy account (no #data sphere)", async () => {
|
|
78
|
+
const id = { ...createBrowserIdentity("legacy", "Legacy"), data: undefined };
|
|
79
|
+
const { text } = serializeRecoveryFile(id);
|
|
80
|
+
const auth = authWith(makePds().fetchImpl);
|
|
81
|
+
await auth.signInWithRecovery({ file: text });
|
|
82
|
+
assert.throws(() => auth.ownerDataClient(), /no #data sphere/);
|
|
83
|
+
});
|
|
84
|
+
test("ownerDataClient throws when no owner is signed in", () => {
|
|
85
|
+
const auth = authWith(makePds().fetchImpl);
|
|
86
|
+
assert.throws(() => auth.ownerDataClient(), /no owner is signed in/);
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=owner-data-client.test.js.map
|
|
@@ -109,7 +109,7 @@ test("a reader without the bundled vendor lite decodes via the published schema"
|
|
|
109
109
|
assert.equal(got.title, "Hello", "indexable field present");
|
|
110
110
|
assert.equal(got.body, "top secret body", "encrypted field decoded via auto-resolved schema");
|
|
111
111
|
});
|
|
112
|
-
test("
|
|
112
|
+
test("schema-less READ still works (decrypts); only WRITE requires the schema", async () => {
|
|
113
113
|
const seed = new Uint8Array(randomBytes(32));
|
|
114
114
|
const pub = ed.getPublicKey(seed);
|
|
115
115
|
let n = 0n;
|
|
@@ -126,11 +126,21 @@ test("a reader still fails cleanly when the schema is neither bundled nor publis
|
|
|
126
126
|
const did = `did:key:z${mb}`;
|
|
127
127
|
const vm = `${did}#z${mb}`;
|
|
128
128
|
const pds = makePds();
|
|
129
|
-
// Writer creates the collection
|
|
129
|
+
// Writer bundles the lite, creates the collection + a record, but does NOT
|
|
130
|
+
// register the schema on the PDS (simulates an app like delie that keeps its
|
|
131
|
+
// schema local). So no client can auto-resolve it.
|
|
130
132
|
const writer = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, schemas: [memoLite], fetch: pds.fetchImpl });
|
|
131
133
|
await writer.createCollection({ name: "memos", schema: MEMO_ID });
|
|
132
|
-
await writer.collection("memos").insert({ title: "
|
|
134
|
+
const recId = await writer.collection("memos").insert({ title: "Hi", body: "encrypted secret" });
|
|
135
|
+
// Reader has NEITHER the bundled lite NOR a published schema to fetch.
|
|
133
136
|
const reader = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, fetch: pds.fetchImpl });
|
|
134
|
-
|
|
137
|
+
// READ still works — records decrypt from the CMK + metadata/payload; the
|
|
138
|
+
// schema is only needed to SPLIT on write. This is what makes a migrated
|
|
139
|
+
// collection directly usable under #data by any client.
|
|
140
|
+
const got = await reader.collection("memos").get(recId);
|
|
141
|
+
assert.equal(got.title, "Hi", "indexable field (plaintext metadata) present");
|
|
142
|
+
assert.equal(got.body, "encrypted secret", "encrypted field decrypted WITHOUT the schema");
|
|
143
|
+
// WRITE fails cleanly (can't split/validate without the schema).
|
|
144
|
+
await assert.rejects(() => reader.collection("memos").insert({ title: "no", body: "go" }), /needs its schema/);
|
|
135
145
|
});
|
|
136
146
|
//# sourceMappingURL=schema-autoresolve.test.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aithos/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.58",
|
|
4
4
|
"description": "Aithos SDK — high-level TypeScript developer kit for building agentic apps on the Aithos protocol. Wraps @aithos/protocol-client and exposes the Aithos compute proxy and wallet (Stripe top-up) endpoints.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aithos",
|