@aithos/sdk 0.1.0-alpha.54 → 0.1.0-alpha.55
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 +5 -3
- package/dist/src/assets.d.ts +19 -3
- package/dist/src/auth.js +50 -72
- package/dist/src/data.d.ts +28 -7
- package/dist/src/data.js +85 -12
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +15 -2
- package/dist/src/internal/cmk-wrap.d.ts +41 -0
- package/dist/src/internal/cmk-wrap.js +132 -0
- package/dist/src/key-store.d.ts +7 -3
- package/dist/src/migrate.d.ts +105 -0
- package/dist/src/migrate.js +367 -0
- package/dist/src/rotate.d.ts +94 -0
- package/dist/src/rotate.js +298 -0
- package/dist/test/migrate.test.d.ts +2 -0
- package/dist/test/migrate.test.js +340 -0
- package/dist/test/rotate-ethos.test.d.ts +2 -0
- package/dist/test/rotate-ethos.test.js +151 -0
- package/dist/test/rotate.test.d.ts +2 -0
- package/dist/test/rotate.test.js +63 -0
- package/dist/test/schema-autoresolve.test.d.ts +2 -0
- package/dist/test/schema-autoresolve.test.js +136 -0
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Hermetic test of the rotateEthos orchestration, MINUS the live zone recopy
|
|
4
|
+
// (the protocol-client editor uses ambient fetch and can't be mocked here, so
|
|
5
|
+
// we drive the no-edition path). Covers: did.json rotation (rotate_sphere_key
|
|
6
|
+
// with a root-verifiable rotated doc) + collections dual-wrapped toward the NEW
|
|
7
|
+
// #data, readable under the new key while the old wrap is preserved.
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import { strict as assert } from "node:assert";
|
|
10
|
+
import { createBrowserIdentity, signedDidDocument } from "@aithos/protocol-client";
|
|
11
|
+
import { verifyDidDocument } from "@aithos/protocol-core";
|
|
12
|
+
import { createDataClient } from "../src/data.js";
|
|
13
|
+
import { rotateEthos } from "../src/rotate.js";
|
|
14
|
+
import { parseRecoveryFile } from "../src/internal/recovery-file.js";
|
|
15
|
+
const API = "https://api.test";
|
|
16
|
+
const PDS = "https://pds.test";
|
|
17
|
+
function hex(b) {
|
|
18
|
+
let s = "";
|
|
19
|
+
for (const x of b)
|
|
20
|
+
s += x.toString(16).padStart(2, "0");
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
23
|
+
function hexToBytes(h) {
|
|
24
|
+
const o = new Uint8Array(h.length / 2);
|
|
25
|
+
for (let i = 0; i < o.length; i++)
|
|
26
|
+
o[i] = parseInt(h.substr(i * 2, 2), 16);
|
|
27
|
+
return o;
|
|
28
|
+
}
|
|
29
|
+
/** Mock api.aithos.be (identity) + pds.aithos.be (data). No edition exists. */
|
|
30
|
+
function makeMock(initialDidDoc) {
|
|
31
|
+
let didDoc = initialDidDoc;
|
|
32
|
+
const collections = new Map();
|
|
33
|
+
const records = new Map();
|
|
34
|
+
let rotateCalls = 0;
|
|
35
|
+
const fetchImpl = (async (url, init) => {
|
|
36
|
+
const body = JSON.parse(init.body);
|
|
37
|
+
const m = body.method;
|
|
38
|
+
const p = body.params ?? {};
|
|
39
|
+
const ok = (result) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result }), { status: 200, headers: { "content-type": "application/json" } });
|
|
40
|
+
const rpcErr = (code, message) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, error: { code, message } }), { status: 200, headers: { "content-type": "application/json" } });
|
|
41
|
+
const urnFor = (did, name) => `urn:aithos:collection:${did}:${name}`;
|
|
42
|
+
switch (m) {
|
|
43
|
+
// --- identity (api) ---
|
|
44
|
+
case "aithos.get_identity":
|
|
45
|
+
return ok({ object: didDoc });
|
|
46
|
+
case "aithos.get_ethos_manifest":
|
|
47
|
+
return rpcErr(-32020, "not found: edition for " + p.did); // no edition
|
|
48
|
+
case "aithos.rotate_sphere_key":
|
|
49
|
+
didDoc = p.new_did_document;
|
|
50
|
+
rotateCalls++;
|
|
51
|
+
return ok({ did: p.new_did_document.id, rotated_spheres: ["public", "circle", "self"] });
|
|
52
|
+
// --- data (pds) ---
|
|
53
|
+
case "aithos.data.create_collection": {
|
|
54
|
+
const urn = urnFor(p.subject_did, p.collection_name);
|
|
55
|
+
collections.set(urn, { urn, name: p.collection_name, schema: p.schema, subject_did: p.subject_did, cmk_envelope: p.cmk_envelope, record_count: 0 });
|
|
56
|
+
records.set(urn, new Map());
|
|
57
|
+
return ok({ urn, name: p.collection_name, schema: p.schema, cmk_envelope: p.cmk_envelope });
|
|
58
|
+
}
|
|
59
|
+
case "aithos.data.get_collection": {
|
|
60
|
+
const c = collections.get(urnFor(p.subject_did, p.collection_name));
|
|
61
|
+
if (!c)
|
|
62
|
+
return rpcErr(-32020, "not found");
|
|
63
|
+
return ok({ urn: c.urn, name: c.name, schema: c.schema, cmk_envelope: c.cmk_envelope, record_count: c.record_count });
|
|
64
|
+
}
|
|
65
|
+
case "aithos.data.list_collections":
|
|
66
|
+
return ok({ items: [...collections.values()].filter((c) => c.subject_did === p.subject_did).map((c) => ({ name: c.name, schema: c.schema, record_count: c.record_count })) });
|
|
67
|
+
case "aithos.data.insert_record": {
|
|
68
|
+
const recs = records.get(p.collection_urn);
|
|
69
|
+
recs.set(p.record_id, { record_id: p.record_id, metadata: p.metadata, payload: p.payload });
|
|
70
|
+
return ok({ record_id: p.record_id });
|
|
71
|
+
}
|
|
72
|
+
case "aithos.data.get_record": {
|
|
73
|
+
const r = records.get(p.collection_urn)?.get(p.record_id);
|
|
74
|
+
return r ? ok(r) : rpcErr(-32020, "not found");
|
|
75
|
+
}
|
|
76
|
+
case "aithos.data.rotate_cmk": {
|
|
77
|
+
const c = [...collections.values()].find((x) => x.urn === p.collection_urn);
|
|
78
|
+
c.cmk_envelope = p.new_cmk_envelope;
|
|
79
|
+
return ok({ rotated_at: new Date().toISOString() });
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
return rpcErr(-32601, `method not found: ${m}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return { fetchImpl, getDidDoc: () => didDoc, collections, rotateCalls: () => rotateCalls };
|
|
86
|
+
}
|
|
87
|
+
test("rotateEthos: rotates did.json (verifiable) + dual-wraps collections to the new #data", async () => {
|
|
88
|
+
// Legacy identity: published did.json WITHOUT #data (strip it).
|
|
89
|
+
const id = createBrowserIdentity("rotee", "Rotee");
|
|
90
|
+
const legacyDoc = signedDidDocument({ ...id, data: undefined });
|
|
91
|
+
const mock = makeMock(legacyDoc);
|
|
92
|
+
const did = id.did;
|
|
93
|
+
const legacyRecovery = JSON.stringify({
|
|
94
|
+
aithos_recovery_version: "0.1.0-plaintext",
|
|
95
|
+
handle: id.handle,
|
|
96
|
+
display_name: id.displayName,
|
|
97
|
+
did,
|
|
98
|
+
seeds_hex: { root: hex(id.root.seed), public: hex(id.public.seed), circle: hex(id.circle.seed), self: hex(id.self.seed) },
|
|
99
|
+
});
|
|
100
|
+
// A collection created + sealed under the OLD #self key.
|
|
101
|
+
const oldClient = createDataClient({ pdsUrl: PDS, did, sphereSeed: id.self.seed, verificationMethod: `${did}#self`, fetch: mock.fetchImpl });
|
|
102
|
+
await oldClient.createCollection({ name: "contacts", schema: "aithos.contacts.v1" });
|
|
103
|
+
const recId = await oldClient.collection("contacts").insert({ name: "Iris", phone: "+33-rot-secret" });
|
|
104
|
+
// Rotate everything. No edition exists → zones skipped automatically.
|
|
105
|
+
const result = await rotateEthos(legacyRecovery, { apiBaseUrl: API, pdsUrl: PDS, fetch: mock.fetchImpl });
|
|
106
|
+
assert.equal(result.hadEdition, false);
|
|
107
|
+
assert.equal(result.editionRepublished, false);
|
|
108
|
+
assert.equal(mock.rotateCalls(), 1, "rotate_sphere_key called once");
|
|
109
|
+
// The newly published did.json verifies under root, rotated all 3 spheres, added #data.
|
|
110
|
+
const newDoc = mock.getDidDoc();
|
|
111
|
+
assert.equal(verifyDidDocument(newDoc), true, "rotated did.json verifies under root");
|
|
112
|
+
assert.equal(newDoc.aithos.rotated.length, 3);
|
|
113
|
+
assert.ok(newDoc.verificationMethod.some((v) => v.id === `${did}#data`), "#data added");
|
|
114
|
+
for (const s of ["public", "circle", "self"]) {
|
|
115
|
+
const oldK = legacyDoc.verificationMethod.find((v) => v.id === `${did}#${s}`).publicKeyMultibase;
|
|
116
|
+
const newK = newDoc.verificationMethod.find((v) => v.id === `${did}#${s}`).publicKeyMultibase;
|
|
117
|
+
assert.notEqual(newK, oldK, `${s} rotated`);
|
|
118
|
+
}
|
|
119
|
+
// Collections re-keyed toward the new #data (dual). Read under the NEW #data.
|
|
120
|
+
assert.equal(result.collections.collections[0].status, "data-wrap-added");
|
|
121
|
+
const newRec = parseRecoveryFile(result.updatedRecoveryFile);
|
|
122
|
+
assert.ok(newRec.seedsHex.data && newRec.seedsHex.data.length === 64, "new recovery carries #data");
|
|
123
|
+
// all sphere seeds changed
|
|
124
|
+
assert.notEqual(newRec.seedsHex.public, hex(id.public.seed));
|
|
125
|
+
const dataClient = createDataClient({ pdsUrl: PDS, did, sphereSeed: hexToBytes(newRec.seedsHex.data), verificationMethod: `${did}#data`, fetch: mock.fetchImpl });
|
|
126
|
+
const got = await dataClient.collection("contacts").get(recId);
|
|
127
|
+
assert.equal(got.phone, "+33-rot-secret", "record decrypts under the NEW #data");
|
|
128
|
+
// The OLD #self reader still works (dual: old wrap preserved).
|
|
129
|
+
const selfClient = createDataClient({ pdsUrl: PDS, did, sphereSeed: id.self.seed, verificationMethod: `${did}#self`, fetch: mock.fetchImpl });
|
|
130
|
+
const viaSelf = await selfClient.collection("contacts").get(recId);
|
|
131
|
+
assert.equal(viaSelf.phone, "+33-rot-secret", "old #self reader still decrypts");
|
|
132
|
+
});
|
|
133
|
+
test("rotateEthos: refuses to skip zones when an edition exists (no force)", async () => {
|
|
134
|
+
const id = createBrowserIdentity("rotee2", "Rotee2");
|
|
135
|
+
const mock = makeMock(signedDidDocument({ ...id, data: undefined }));
|
|
136
|
+
// Make get_ethos_manifest succeed → an edition "exists".
|
|
137
|
+
const patched = (async (url, init) => {
|
|
138
|
+
const body = JSON.parse(init.body);
|
|
139
|
+
if (body.method === "aithos.get_ethos_manifest") {
|
|
140
|
+
return new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result: { object: { edition: { height: 0 } } } }), { status: 200 });
|
|
141
|
+
}
|
|
142
|
+
return mock.fetchImpl(url, init);
|
|
143
|
+
});
|
|
144
|
+
const legacyRecovery = JSON.stringify({
|
|
145
|
+
aithos_recovery_version: "0.1.0-plaintext",
|
|
146
|
+
handle: id.handle, display_name: id.displayName, did: id.did,
|
|
147
|
+
seeds_hex: { root: hex(id.root.seed), public: hex(id.public.seed), circle: hex(id.circle.seed), self: hex(id.self.seed) },
|
|
148
|
+
});
|
|
149
|
+
await assert.rejects(() => rotateEthos(legacyRecovery, { apiBaseUrl: API, pdsUrl: PDS, fetch: patched, skipZones: true }), /would orphan that content/);
|
|
150
|
+
});
|
|
151
|
+
//# sourceMappingURL=rotate-ethos.test.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Unit tests for buildSignedRotatedDidDocument: the rotated did.json must (1)
|
|
4
|
+
// verify under the unchanged root key, (2) grow aithos.rotated[] by one entry
|
|
5
|
+
// per changed Ethos sphere, (3) carry the #data sphere, (4) keep the DID.
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { strict as assert } from "node:assert";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { createBrowserIdentity, signedDidDocument } from "@aithos/protocol-client";
|
|
10
|
+
import { verifyDidDocument } from "@aithos/protocol-core";
|
|
11
|
+
import { buildSignedRotatedDidDocument } from "../src/rotate.js";
|
|
12
|
+
function seed() {
|
|
13
|
+
return new Uint8Array(randomBytes(32));
|
|
14
|
+
}
|
|
15
|
+
test("rotate: rotated did.json verifies under root, grows rotated[], carries #data, keeps DID", () => {
|
|
16
|
+
// Baseline: a legacy-style identity (no rotation history).
|
|
17
|
+
const id = createBrowserIdentity("rot_user", "Rot User");
|
|
18
|
+
const previousDoc = signedDidDocument(id);
|
|
19
|
+
assert.equal(previousDoc.aithos.rotated.length, 0, "baseline has empty rotation history");
|
|
20
|
+
// Rotate all three Ethos spheres + #data; keep root.
|
|
21
|
+
const seeds = { root: id.root.seed, public: seed(), circle: seed(), self: seed(), data: seed() };
|
|
22
|
+
const rotated = buildSignedRotatedDidDocument(seeds, previousDoc, "Rot User", "key-rotation");
|
|
23
|
+
// 1. Root signature still valid (root unchanged).
|
|
24
|
+
assert.equal(verifyDidDocument(rotated), true, "rotated doc verifies under root");
|
|
25
|
+
// 2. DID unchanged.
|
|
26
|
+
assert.equal(rotated.id, previousDoc.id);
|
|
27
|
+
// 3. rotated[] grew by exactly 3 (public, circle, self all changed).
|
|
28
|
+
assert.equal(rotated.aithos.rotated.length, 3);
|
|
29
|
+
const spheres = rotated.aithos.rotated.map((r) => r.sphere).sort();
|
|
30
|
+
assert.deepEqual(spheres, ["circle", "public", "self"]);
|
|
31
|
+
// Each entry records the OLD key.
|
|
32
|
+
for (const entry of rotated.aithos.rotated) {
|
|
33
|
+
const prev = previousDoc.verificationMethod.find((v) => v.id === `${rotated.id}#${entry.sphere}`);
|
|
34
|
+
assert.equal(entry.previous_key, prev.publicKeyMultibase, `${entry.sphere} records previous key`);
|
|
35
|
+
}
|
|
36
|
+
// 4. New keys actually differ + #data present.
|
|
37
|
+
for (const sphere of ["public", "circle", "self"]) {
|
|
38
|
+
const oldVm = previousDoc.verificationMethod.find((v) => v.id === `${rotated.id}#${sphere}`);
|
|
39
|
+
const newVm = rotated.verificationMethod.find((v) => v.id === `${rotated.id}#${sphere}`);
|
|
40
|
+
assert.notEqual(newVm.publicKeyMultibase, oldVm.publicKeyMultibase, `${sphere} key rotated`);
|
|
41
|
+
}
|
|
42
|
+
assert.ok(rotated.verificationMethod.some((v) => v.id === `${rotated.id}#data`), "#data VM present");
|
|
43
|
+
assert.ok((rotated.keyAgreement ?? []).some((v) => v.id === `${rotated.id}#data-kex`), "#data-kex present");
|
|
44
|
+
});
|
|
45
|
+
test("rotate: history is carried forward across a second rotation", () => {
|
|
46
|
+
const id = createBrowserIdentity("rot2", "Rot2");
|
|
47
|
+
const doc0 = signedDidDocument(id);
|
|
48
|
+
const seeds1 = { root: id.root.seed, public: seed(), circle: seed(), self: seed(), data: seed() };
|
|
49
|
+
const doc1 = buildSignedRotatedDidDocument(seeds1, doc0, "Rot2");
|
|
50
|
+
assert.equal(doc1.aithos.rotated.length, 3);
|
|
51
|
+
// Second rotation from doc1 → 3 more entries (total 6).
|
|
52
|
+
const seeds2 = { root: id.root.seed, public: seed(), circle: seed(), self: seed(), data: seeds1.data };
|
|
53
|
+
const doc2 = buildSignedRotatedDidDocument(seeds2, doc1, "Rot2");
|
|
54
|
+
assert.equal(doc2.aithos.rotated.length, 6, "prior history preserved + new entries appended");
|
|
55
|
+
assert.equal(verifyDidDocument(doc2), true);
|
|
56
|
+
});
|
|
57
|
+
test("rotate: refuses if root seed does not match the DID", () => {
|
|
58
|
+
const id = createBrowserIdentity("rot3", "Rot3");
|
|
59
|
+
const doc0 = signedDidDocument(id);
|
|
60
|
+
const seeds = { root: seed() /* WRONG root */, public: seed(), circle: seed(), self: seed() };
|
|
61
|
+
assert.throws(() => buildSignedRotatedDidDocument(seeds, doc0, "Rot3"), /root .* never rotated/);
|
|
62
|
+
});
|
|
63
|
+
//# sourceMappingURL=rotate.test.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// A reader that did NOT bundle a vendor schema can still read a collection that
|
|
4
|
+
// uses it, by auto-resolving the PUBLISHED JSON Schema from the PDS and
|
|
5
|
+
// deriving the field split from its aithos:indexable / aithos:auto annotations.
|
|
6
|
+
// This is what lets the strangler reference app (and any latest-SDK client)
|
|
7
|
+
// read any collection without hardcoding VENDOR_SCHEMAS.
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import { strict as assert } from "node:assert";
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { createDataClient, liteFromPublishedSchema } from "../src/data.js";
|
|
12
|
+
import * as ed from "@noble/ed25519";
|
|
13
|
+
import { sha512 } from "@noble/hashes/sha2.js";
|
|
14
|
+
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
15
|
+
const MEMO_ID = "aithos.x.demo.memo.v1";
|
|
16
|
+
const memoLite = {
|
|
17
|
+
schema: MEMO_ID,
|
|
18
|
+
indexable: new Set(["title"]),
|
|
19
|
+
encrypted: new Set(["body"]),
|
|
20
|
+
auto: new Set(),
|
|
21
|
+
defaults: {},
|
|
22
|
+
};
|
|
23
|
+
const memoJson = {
|
|
24
|
+
"aithos:schema": MEMO_ID,
|
|
25
|
+
"aithos:version": "1.0.0",
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
required: ["title"],
|
|
29
|
+
properties: {
|
|
30
|
+
title: { type: "string", "aithos:indexable": true },
|
|
31
|
+
body: { type: "string" },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
test("liteFromPublishedSchema derives the field split from annotations", () => {
|
|
35
|
+
const lite = liteFromPublishedSchema(memoJson);
|
|
36
|
+
assert.equal(lite.schema, MEMO_ID);
|
|
37
|
+
assert.deepEqual([...lite.indexable].sort(), ["title"]);
|
|
38
|
+
assert.deepEqual([...lite.encrypted].sort(), ["body"]);
|
|
39
|
+
assert.equal(lite.auto.size, 0);
|
|
40
|
+
});
|
|
41
|
+
function makePds() {
|
|
42
|
+
const collections = new Map();
|
|
43
|
+
const records = new Map();
|
|
44
|
+
const schemas = new Map();
|
|
45
|
+
const fetchImpl = (async (_url, init) => {
|
|
46
|
+
const body = JSON.parse(init.body);
|
|
47
|
+
const p = body.params ?? {};
|
|
48
|
+
const ok = (result) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result }), { status: 200 });
|
|
49
|
+
const er = (code, m) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, error: { code, message: m } }), { status: 200 });
|
|
50
|
+
const urn = (did, n) => `urn:aithos:collection:${did}:${n}`;
|
|
51
|
+
switch (body.method) {
|
|
52
|
+
case "aithos.data.register_schema":
|
|
53
|
+
schemas.set(p.schema_doc["aithos:schema"], p.schema_doc);
|
|
54
|
+
return ok({ schema_id: p.schema_doc["aithos:schema"], doc_hash: "h", created: true });
|
|
55
|
+
case "aithos.data.get_schema": {
|
|
56
|
+
const d = schemas.get(p.schema);
|
|
57
|
+
return d ? ok({ schema: d }) : er(-32070, "unknown schema");
|
|
58
|
+
}
|
|
59
|
+
case "aithos.data.create_collection": {
|
|
60
|
+
const u = urn(p.subject_did, p.collection_name);
|
|
61
|
+
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 });
|
|
62
|
+
records.set(u, new Map());
|
|
63
|
+
return ok({ urn: u, name: p.collection_name, schema: p.schema, cmk_envelope: p.cmk_envelope });
|
|
64
|
+
}
|
|
65
|
+
case "aithos.data.get_collection": {
|
|
66
|
+
const c = collections.get(urn(p.subject_did, p.collection_name));
|
|
67
|
+
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");
|
|
68
|
+
}
|
|
69
|
+
case "aithos.data.insert_record": {
|
|
70
|
+
records.get(p.collection_urn).set(p.record_id, { record_id: p.record_id, metadata: p.metadata, payload: p.payload });
|
|
71
|
+
return ok({ record_id: p.record_id });
|
|
72
|
+
}
|
|
73
|
+
case "aithos.data.get_record": {
|
|
74
|
+
const r = records.get(p.collection_urn)?.get(p.record_id);
|
|
75
|
+
return r ? ok(r) : er(-32020, "nf");
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
return er(-32601, body.method);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return { fetchImpl, schemas };
|
|
82
|
+
}
|
|
83
|
+
test("a reader without the bundled vendor lite decodes via the published schema", async () => {
|
|
84
|
+
const seed = new Uint8Array(randomBytes(32));
|
|
85
|
+
const pub = ed.getPublicKey(seed);
|
|
86
|
+
// did:key style (single-key) — fine for the data PDS owner path in this mock.
|
|
87
|
+
let n = 0n;
|
|
88
|
+
const mc = new Uint8Array([0xed, 0x01, ...pub]);
|
|
89
|
+
for (const b of mc)
|
|
90
|
+
n = (n << 8n) | BigInt(b);
|
|
91
|
+
const alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
92
|
+
let mb = "";
|
|
93
|
+
let x = n;
|
|
94
|
+
while (x > 0n) {
|
|
95
|
+
mb = alpha[Number(x % 58n)] + mb;
|
|
96
|
+
x /= 58n;
|
|
97
|
+
}
|
|
98
|
+
const did = `did:key:z${mb}`;
|
|
99
|
+
const vm = `${did}#z${mb}`;
|
|
100
|
+
const pds = makePds();
|
|
101
|
+
// Writer: bundles the lite, registers the JSON schema, writes a record.
|
|
102
|
+
const writer = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, schemas: [memoLite], fetch: pds.fetchImpl });
|
|
103
|
+
await writer.registerSchema(memoJson);
|
|
104
|
+
await writer.createCollection({ name: "memos", schema: MEMO_ID });
|
|
105
|
+
const recId = await writer.collection("memos").insert({ title: "Hello", body: "top secret body" });
|
|
106
|
+
// Reader: NO bundled schema. Must auto-resolve from the PDS-published schema.
|
|
107
|
+
const reader = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, fetch: pds.fetchImpl });
|
|
108
|
+
const got = await reader.collection("memos").get(recId);
|
|
109
|
+
assert.equal(got.title, "Hello", "indexable field present");
|
|
110
|
+
assert.equal(got.body, "top secret body", "encrypted field decoded via auto-resolved schema");
|
|
111
|
+
});
|
|
112
|
+
test("a reader still fails cleanly when the schema is neither bundled nor published", async () => {
|
|
113
|
+
const seed = new Uint8Array(randomBytes(32));
|
|
114
|
+
const pub = ed.getPublicKey(seed);
|
|
115
|
+
let n = 0n;
|
|
116
|
+
const mc = new Uint8Array([0xed, 0x01, ...pub]);
|
|
117
|
+
for (const b of mc)
|
|
118
|
+
n = (n << 8n) | BigInt(b);
|
|
119
|
+
const alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
120
|
+
let mb = "";
|
|
121
|
+
let x = n;
|
|
122
|
+
while (x > 0n) {
|
|
123
|
+
mb = alpha[Number(x % 58n)] + mb;
|
|
124
|
+
x /= 58n;
|
|
125
|
+
}
|
|
126
|
+
const did = `did:key:z${mb}`;
|
|
127
|
+
const vm = `${did}#z${mb}`;
|
|
128
|
+
const pds = makePds();
|
|
129
|
+
// Writer creates the collection WITHOUT registering the schema on the PDS.
|
|
130
|
+
const writer = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, schemas: [memoLite], fetch: pds.fetchImpl });
|
|
131
|
+
await writer.createCollection({ name: "memos", schema: MEMO_ID });
|
|
132
|
+
await writer.collection("memos").insert({ title: "x", body: "y" });
|
|
133
|
+
const reader = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, fetch: pds.fetchImpl });
|
|
134
|
+
await assert.rejects(() => reader.collection("memos").get("anything"), /not known to the SDK and not published/);
|
|
135
|
+
});
|
|
136
|
+
//# 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.55",
|
|
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",
|