@aithos/sdk 0.1.0-alpha.54 → 0.1.0-alpha.56
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 +103 -17
- 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 +146 -0
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
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("schema-less READ still works (decrypts); only WRITE requires the schema", 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 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.
|
|
132
|
+
const writer = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, schemas: [memoLite], fetch: pds.fetchImpl });
|
|
133
|
+
await writer.createCollection({ name: "memos", schema: MEMO_ID });
|
|
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.
|
|
136
|
+
const reader = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, fetch: pds.fetchImpl });
|
|
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/);
|
|
145
|
+
});
|
|
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.56",
|
|
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",
|