@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
package/README.md
CHANGED
|
@@ -169,9 +169,11 @@ const { session, passwordMustChange } = await sdk.auth.signInCustodial({
|
|
|
169
169
|
email: "alice@example.com",
|
|
170
170
|
password: "MyTempPass32chars",
|
|
171
171
|
});
|
|
172
|
-
// Local KeyStore is now hydrated with the
|
|
173
|
-
// the user can publish ethos
|
|
174
|
-
//
|
|
172
|
+
// Local KeyStore is now hydrated with the 5 Ed25519 sphere seeds
|
|
173
|
+
// (root, public, circle, self, #data) — the user can publish ethos
|
|
174
|
+
// editions, mint mandates, invoke compute, and own PDS data/asset
|
|
175
|
+
// collections (signed under the dedicated #data sphere), exactly as if
|
|
176
|
+
// they had signed in via a recovery file or Google SSO.
|
|
175
177
|
if (passwordMustChange) {
|
|
176
178
|
// Optional: nudge the user to set their own password via the
|
|
177
179
|
// standard reset flow.
|
package/dist/src/assets.d.ts
CHANGED
|
@@ -39,11 +39,27 @@ export interface CreateAssetsClientArgs {
|
|
|
39
39
|
* production vanity domain — SEPARATE from the data PDS) when omitted.
|
|
40
40
|
* Override for self-hosting/staging. */
|
|
41
41
|
readonly pdsUrl?: string;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Subject DID that owns the assets. The canonical owner is a `did:aithos:…`
|
|
44
|
+
* account signing under its dedicated `#data` sphere; a `did:key:…` is a
|
|
45
|
+
* throwaway identity for demos/tests only.
|
|
46
|
+
*/
|
|
43
47
|
readonly did: string;
|
|
44
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Ed25519 sphere seed (32 bytes) that signs every assets-PDS envelope. For a
|
|
50
|
+
* `did:aithos` account this MUST be the subject's dedicated **`#data`** sphere
|
|
51
|
+
* seed (root stays cold). For a `did:key` it is the single embedded key.
|
|
52
|
+
*
|
|
53
|
+
* Note: this is the SIGNING key. Per-asset AMKs for private uploads are
|
|
54
|
+
* wrapped to the attaching context's X25519 key (`#data-kex` / `#circle-kex`
|
|
55
|
+
* / `#self-kex`) by the RecipientResolver — a separate mechanism.
|
|
56
|
+
*/
|
|
45
57
|
readonly sphereSeed: Uint8Array;
|
|
46
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Verification method URL within the DID document used to sign envelopes.
|
|
60
|
+
* For a `did:aithos` account this is **`<did>#data`**; for a `did:key` it is
|
|
61
|
+
* `<did>#<multibase>`.
|
|
62
|
+
*/
|
|
47
63
|
readonly verificationMethod: string;
|
|
48
64
|
/** Optional fetch implementation. Defaults to globalThis.fetch. */
|
|
49
65
|
readonly fetch?: typeof fetch;
|
package/dist/src/auth.js
CHANGED
|
@@ -976,33 +976,15 @@ export class AithosAuth {
|
|
|
976
976
|
}
|
|
977
977
|
// Magic-link sign-in path. Mirror `signInCustodial` to materialise
|
|
978
978
|
// the 4 sphere seeds in the keystore.
|
|
979
|
-
if (resp.seed.byteLength !== 128) {
|
|
980
|
-
zeroize(resp.seed);
|
|
981
|
-
zeroize(resp.encKey);
|
|
982
|
-
throw new AithosSDKError("auth_custodial_seed_format", `verifyEmail: expected 128-byte seed bundle, got ${resp.seed.byteLength}`);
|
|
983
|
-
}
|
|
984
|
-
const seedRoot = resp.seed.slice(0, 32);
|
|
985
|
-
const seedPublic = resp.seed.slice(32, 64);
|
|
986
|
-
const seedCircle = resp.seed.slice(64, 96);
|
|
987
|
-
const seedSelf = resp.seed.slice(96, 128);
|
|
988
979
|
const stored = {
|
|
989
980
|
version: "0.1.0-hex",
|
|
990
981
|
did: resp.did,
|
|
991
982
|
handle: resp.handle,
|
|
992
983
|
displayName: resp.displayName,
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
public: bytesToHex(seedPublic),
|
|
996
|
-
circle: bytesToHex(seedCircle),
|
|
997
|
-
self: bytesToHex(seedSelf),
|
|
998
|
-
},
|
|
984
|
+
// Accepts 128 (legacy) or 160 (with #data); zeroizes resp.seed.
|
|
985
|
+
seedsHex: custodialSeedsHex(resp.seed),
|
|
999
986
|
savedAt: new Date().toISOString(),
|
|
1000
987
|
};
|
|
1001
|
-
zeroize(resp.seed);
|
|
1002
|
-
zeroize(seedRoot);
|
|
1003
|
-
zeroize(seedPublic);
|
|
1004
|
-
zeroize(seedCircle);
|
|
1005
|
-
zeroize(seedSelf);
|
|
1006
988
|
zeroize(resp.encKey);
|
|
1007
989
|
// Bootstrap the Ethos on api.aithos.be (cf. notes in signInCustodial).
|
|
1008
990
|
// The magic-link flow is the FIRST time the user actually has
|
|
@@ -1106,33 +1088,15 @@ export class AithosAuth {
|
|
|
1106
1088
|
});
|
|
1107
1089
|
// Materialise the 4 sphere seeds + session — same shape as the verifyEmail
|
|
1108
1090
|
// magic-link path. Kept inline (additive; verifyEmail is left untouched).
|
|
1109
|
-
if (resp.seed.byteLength !== 128) {
|
|
1110
|
-
zeroize(resp.seed);
|
|
1111
|
-
zeroize(resp.encKey);
|
|
1112
|
-
throw new AithosSDKError("auth_custodial_seed_format", `acceptInvite: expected 128-byte seed bundle, got ${resp.seed.byteLength}`);
|
|
1113
|
-
}
|
|
1114
|
-
const seedRoot = resp.seed.slice(0, 32);
|
|
1115
|
-
const seedPublic = resp.seed.slice(32, 64);
|
|
1116
|
-
const seedCircle = resp.seed.slice(64, 96);
|
|
1117
|
-
const seedSelf = resp.seed.slice(96, 128);
|
|
1118
1091
|
const stored = {
|
|
1119
1092
|
version: "0.1.0-hex",
|
|
1120
1093
|
did: resp.did,
|
|
1121
1094
|
handle: resp.handle,
|
|
1122
1095
|
displayName: resp.displayName,
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
public: bytesToHex(seedPublic),
|
|
1126
|
-
circle: bytesToHex(seedCircle),
|
|
1127
|
-
self: bytesToHex(seedSelf),
|
|
1128
|
-
},
|
|
1096
|
+
// Accepts 128 (legacy) or 160 (with #data); zeroizes resp.seed.
|
|
1097
|
+
seedsHex: custodialSeedsHex(resp.seed),
|
|
1129
1098
|
savedAt: new Date().toISOString(),
|
|
1130
1099
|
};
|
|
1131
|
-
zeroize(resp.seed);
|
|
1132
|
-
zeroize(seedRoot);
|
|
1133
|
-
zeroize(seedPublic);
|
|
1134
|
-
zeroize(seedCircle);
|
|
1135
|
-
zeroize(seedSelf);
|
|
1136
1100
|
zeroize(resp.encKey);
|
|
1137
1101
|
const identity = browserIdentityFromStored({
|
|
1138
1102
|
handle: stored.handle,
|
|
@@ -1212,45 +1176,19 @@ export class AithosAuth {
|
|
|
1212
1176
|
throw new AithosSDKError("auth_invalid_input", "signInCustodial: email and password are required");
|
|
1213
1177
|
}
|
|
1214
1178
|
const resp = await custodialSignIn({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input);
|
|
1215
|
-
// Split the
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1219
|
-
// Legacy 32-byte rows shouldn't happen in production (we wiped the
|
|
1220
|
-
// single test row before redeploying with the 4-seed bundle), but
|
|
1221
|
-
// we surface a clear error rather than silently corrupting the
|
|
1222
|
-
// identity.
|
|
1223
|
-
zeroize(resp.seed);
|
|
1224
|
-
zeroize(resp.encKey);
|
|
1225
|
-
throw new AithosSDKError("auth_custodial_seed_format", `signInCustodial: expected 128-byte seed bundle, got ${resp.seed.byteLength}`);
|
|
1226
|
-
}
|
|
1227
|
-
const seedRoot = resp.seed.slice(0, 32);
|
|
1228
|
-
const seedPublic = resp.seed.slice(32, 64);
|
|
1229
|
-
const seedCircle = resp.seed.slice(64, 96);
|
|
1230
|
-
const seedSelf = resp.seed.slice(96, 128);
|
|
1231
|
-
// Stored shape uses hex strings; round-trip through bytesToHex
|
|
1232
|
-
// so the keyStore record is identical to what signUp(zk) writes.
|
|
1179
|
+
// Split the custodial seed bundle into the sphere seeds. The backend
|
|
1180
|
+
// lays them out in the canonical order [root || public || circle || self]
|
|
1181
|
+
// (128 bytes), plus the dedicated #data sphere when migrated (160 bytes).
|
|
1182
|
+
// See seed-wrapper.ts / custodialSeedsHex.
|
|
1233
1183
|
const stored = {
|
|
1234
1184
|
version: "0.1.0-hex",
|
|
1235
1185
|
did: resp.did,
|
|
1236
1186
|
handle: resp.handle,
|
|
1237
1187
|
displayName: resp.displayName,
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
public: bytesToHex(seedPublic),
|
|
1241
|
-
circle: bytesToHex(seedCircle),
|
|
1242
|
-
self: bytesToHex(seedSelf),
|
|
1243
|
-
},
|
|
1188
|
+
// Accepts 128 (legacy, no #data) or 160 (with #data); zeroizes resp.seed.
|
|
1189
|
+
seedsHex: custodialSeedsHex(resp.seed),
|
|
1244
1190
|
savedAt: new Date().toISOString(),
|
|
1245
1191
|
};
|
|
1246
|
-
// Zeroize the raw bundle + the split copies now that they've been
|
|
1247
|
-
// serialised into the keyStore record (hex strings live in the
|
|
1248
|
-
// record; the original bytes can go).
|
|
1249
|
-
zeroize(resp.seed);
|
|
1250
|
-
zeroize(seedRoot);
|
|
1251
|
-
zeroize(seedPublic);
|
|
1252
|
-
zeroize(seedCircle);
|
|
1253
|
-
zeroize(seedSelf);
|
|
1254
1192
|
// The enc_key is informational here — the custodial blob is empty
|
|
1255
1193
|
// at first login. We still don't keep it in memory.
|
|
1256
1194
|
zeroize(resp.encKey);
|
|
@@ -1507,6 +1445,46 @@ function bytesToHex(b) {
|
|
|
1507
1445
|
out += b[i].toString(16).padStart(2, "0");
|
|
1508
1446
|
return out;
|
|
1509
1447
|
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Split a custodial seed bundle into the keystore's hex seeds, zeroizing the
|
|
1450
|
+
* raw bytes (the bundle and every slice) before returning.
|
|
1451
|
+
*
|
|
1452
|
+
* Two bundle shapes are accepted, in canonical sphere order:
|
|
1453
|
+
* - 128 bytes = root || public || circle || self (legacy, no #data)
|
|
1454
|
+
* - 160 bytes = root || public || circle || self || data (V2.2+, with #data)
|
|
1455
|
+
*
|
|
1456
|
+
* A 160-byte bundle yields a `data` seed so the custodial owner carries the
|
|
1457
|
+
* dedicated `#data` sphere — identical to a self-custody / SSO account. A
|
|
1458
|
+
* 128-byte bundle (an account the backend hasn't migrated yet) yields no
|
|
1459
|
+
* `data`; the backend upgrades it to 160 on the user's next sign-in.
|
|
1460
|
+
*/
|
|
1461
|
+
function custodialSeedsHex(seed) {
|
|
1462
|
+
const len = seed.byteLength;
|
|
1463
|
+
if (len !== 128 && len !== 160) {
|
|
1464
|
+
zeroize(seed);
|
|
1465
|
+
throw new AithosSDKError("auth_custodial_seed_format", `expected a 128- or 160-byte custodial seed bundle, got ${len}`);
|
|
1466
|
+
}
|
|
1467
|
+
const root = seed.slice(0, 32);
|
|
1468
|
+
const pub = seed.slice(32, 64);
|
|
1469
|
+
const circle = seed.slice(64, 96);
|
|
1470
|
+
const self = seed.slice(96, 128);
|
|
1471
|
+
const data = len === 160 ? seed.slice(128, 160) : undefined;
|
|
1472
|
+
const hex = {
|
|
1473
|
+
root: bytesToHex(root),
|
|
1474
|
+
public: bytesToHex(pub),
|
|
1475
|
+
circle: bytesToHex(circle),
|
|
1476
|
+
self: bytesToHex(self),
|
|
1477
|
+
...(data ? { data: bytesToHex(data) } : {}),
|
|
1478
|
+
};
|
|
1479
|
+
zeroize(root);
|
|
1480
|
+
zeroize(pub);
|
|
1481
|
+
zeroize(circle);
|
|
1482
|
+
zeroize(self);
|
|
1483
|
+
if (data)
|
|
1484
|
+
zeroize(data);
|
|
1485
|
+
zeroize(seed);
|
|
1486
|
+
return hex;
|
|
1487
|
+
}
|
|
1510
1488
|
/**
|
|
1511
1489
|
* Project a delegate as it appears in a `BlobPlaintext` (extension-kit
|
|
1512
1490
|
* `StoredDelegate` shape) onto the SDK's own {@link StoredDelegateKeys}.
|
package/dist/src/data.d.ts
CHANGED
|
@@ -13,18 +13,25 @@ export interface CreateDataClientArgs {
|
|
|
13
13
|
* staging (e.g. a raw `execute-api` URL).
|
|
14
14
|
*/
|
|
15
15
|
readonly pdsUrl?: string;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Subject DID that owns the data. The canonical owner is a `did:aithos:…`
|
|
18
|
+
* account signing under its dedicated `#data` sphere (see below). A
|
|
19
|
+
* `did:key:…` is a throwaway identity for quick demos/tests only — it has no
|
|
20
|
+
* sphere separation (every sphere collapses to the single embedded key).
|
|
21
|
+
*/
|
|
17
22
|
readonly did: string;
|
|
18
23
|
/**
|
|
19
|
-
* Ed25519 sphere seed (32 bytes)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
24
|
+
* Ed25519 sphere seed (32 bytes) that signs every PDS envelope. For a
|
|
25
|
+
* `did:aithos` account this MUST be the subject's dedicated **`#data`** sphere
|
|
26
|
+
* seed — never the root key nor an Ethos sphere — so the root stays cold and
|
|
27
|
+
* data operations are isolated to their own key. For a `did:key` it is the
|
|
28
|
+
* single key embedded in the DID.
|
|
22
29
|
*/
|
|
23
30
|
readonly sphereSeed: Uint8Array;
|
|
24
31
|
/**
|
|
25
|
-
* The verification method URL within the DID document used to sign
|
|
26
|
-
* envelopes. For did:
|
|
27
|
-
*
|
|
32
|
+
* The verification method URL within the DID document used to sign PDS
|
|
33
|
+
* envelopes. For a `did:aithos` account this is **`<did>#data`**. For a
|
|
34
|
+
* `did:key` it is `<did>#<multibase>`.
|
|
28
35
|
*/
|
|
29
36
|
readonly verificationMethod: string;
|
|
30
37
|
/** Optional fetch implementation. Defaults to globalThis.fetch. */
|
|
@@ -339,4 +346,18 @@ export interface CreateAppendDataClientArgs {
|
|
|
339
346
|
* (insert allowed; get/list/update/delete refused).
|
|
340
347
|
*/
|
|
341
348
|
export declare function createAppendDataClient(args: CreateAppendDataClientArgs): AppendOnlyDataClient;
|
|
349
|
+
/**
|
|
350
|
+
* Derive an {@link AithosSchemaLite} from a PUBLISHED JSON Schema document (the
|
|
351
|
+
* shape `aithos.data.get_schema` / `registerSchema` round-trip). The field
|
|
352
|
+
* split is read from the per-property annotations:
|
|
353
|
+
* - `aithos:indexable: true` → indexable (server-visible, filter/sort)
|
|
354
|
+
* - `aithos:auto: …` → auto (server-populated, e.g. created_at)
|
|
355
|
+
* - anything else → encrypted (AEAD'd client-side)
|
|
356
|
+
*
|
|
357
|
+
* These annotations are authoritative — by convention they mirror the writer's
|
|
358
|
+
* own lite — so a reader that never bundled the schema can still split records
|
|
359
|
+
* correctly. `defaults` is left empty (it only matters for inserts; the writer
|
|
360
|
+
* supplies its own).
|
|
361
|
+
*/
|
|
362
|
+
export declare function liteFromPublishedSchema(doc: object): AithosSchemaLite;
|
|
342
363
|
//# sourceMappingURL=data.d.ts.map
|
package/dist/src/data.js
CHANGED
|
@@ -370,8 +370,16 @@ class DataClientImpl {
|
|
|
370
370
|
const ourRecipient = this.#delegate
|
|
371
371
|
? delegateRecipientDidUrl(this.#delegate.granteePubkeyMultibase)
|
|
372
372
|
: `${this.#did}#data-kex`;
|
|
373
|
-
|
|
374
|
-
|
|
373
|
+
// A collection may carry MORE THAN ONE wrap under our recipient label —
|
|
374
|
+
// e.g. after the #data-sphere dual-read migration an owner collection holds
|
|
375
|
+
// both the legacy-sphere-sealed wrap (kept so the old app keeps reading) and
|
|
376
|
+
// a #data-sealed wrap (both labelled `${did}#data-kex`). The label alone
|
|
377
|
+
// can't tell them apart (it's fixed regardless of the sealing key), so we
|
|
378
|
+
// try EVERY matching wrap with our key and keep the one that actually
|
|
379
|
+
// decrypts. For the common single-wrap case this is identical to the old
|
|
380
|
+
// `find` + unwrap behaviour.
|
|
381
|
+
const matching = meta.cmk_envelope.wraps.filter((w) => w.recipient === ourRecipient);
|
|
382
|
+
if (matching.length === 0) {
|
|
375
383
|
throw new Error(this.#delegate
|
|
376
384
|
? `sdk.data: no CMK wrap for ${ourRecipient} in collection "${name}". ` +
|
|
377
385
|
`The owner has not authorized this delegate on the collection yet — ` +
|
|
@@ -379,20 +387,46 @@ class DataClientImpl {
|
|
|
379
387
|
: `sdk.data: no CMK wrap for ${ourRecipient} in collection ${name}. The collection was either created with a different recipient, or this client is not the owner.`);
|
|
380
388
|
}
|
|
381
389
|
const unwrapSeed = this.#delegate ? this.#delegate.delegateSeed : this.#seed;
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
390
|
+
const privateKey = ed25519SeedToX25519PrivateKey(unwrapSeed);
|
|
391
|
+
let cmk;
|
|
392
|
+
for (const wrap of matching) {
|
|
393
|
+
try {
|
|
394
|
+
cmk = this.#unwrapCmk({ wrap, collectionUrn: meta.urn, privateKey });
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Wrong wrap for this key (e.g. the legacy-sphere wrap when we hold the
|
|
399
|
+
// #data key, or vice-versa) — try the next same-label wrap.
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!cmk) {
|
|
403
|
+
throw new Error(`sdk.data: found ${matching.length} CMK wrap(s) for ${ourRecipient} in collection ${name}, ` +
|
|
404
|
+
`but none could be unwrapped with this client's key. The collection may be sealed to a ` +
|
|
405
|
+
`different sphere/key than the one this client was constructed with.`);
|
|
406
|
+
}
|
|
387
407
|
this.#cmkCache.set(name, cmk);
|
|
388
|
-
|
|
408
|
+
let schema = this.#resolveSchema(meta.schema);
|
|
389
409
|
if (!schema) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
// Auto-resolve a PUBLISHED vendor schema from the PDS. This lets any
|
|
411
|
+
// up-to-date client read ANY collection whose owner registered its schema
|
|
412
|
+
// (via registerSchema) without the reading app having to bundle the lite
|
|
413
|
+
// locally — the published JSON Schema's `aithos:indexable` / `aithos:auto`
|
|
414
|
+
// annotations are the authoritative field split (they mirror the writer's
|
|
415
|
+
// lite by convention). Falls through to the error if nothing is published.
|
|
416
|
+
try {
|
|
417
|
+
const published = await this.getSchema(meta.schema, { subjectDid: this.#did });
|
|
418
|
+
if (published)
|
|
419
|
+
schema = liteFromPublishedSchema(published);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// network / not-found — leave `schema` undefined (reads still work)
|
|
423
|
+
}
|
|
394
424
|
}
|
|
395
|
-
|
|
425
|
+
// Do NOT throw when the schema is unknown: reads decrypt from the CMK +
|
|
426
|
+
// metadata/payload alone (the schema is only needed to SPLIT on write).
|
|
427
|
+
// Leaving `schema` undefined keeps the collection fully readable; an
|
|
428
|
+
// insert/update will surface a precise "schema required to write" error.
|
|
429
|
+
const state = { name, urn: meta.urn, ...(schema ? { schema } : {}), schemaId: meta.schema };
|
|
396
430
|
this.#colCache.set(name, state);
|
|
397
431
|
return state;
|
|
398
432
|
}
|
|
@@ -401,7 +435,7 @@ class DataClientImpl {
|
|
|
401
435
|
const cmk = this.#cmkCache.get(state.name);
|
|
402
436
|
if (!cmk)
|
|
403
437
|
throw new Error("CMK not loaded");
|
|
404
|
-
const { metadata, payload } = splitRecord(record, state
|
|
438
|
+
const { metadata, payload } = splitRecord(record, requireSchema(state));
|
|
405
439
|
const recordId = `record_${makeUlid()}`;
|
|
406
440
|
const encrypted = this.#encryptPayload({
|
|
407
441
|
collectionName: state.name,
|
|
@@ -470,7 +504,7 @@ class DataClientImpl {
|
|
|
470
504
|
const cmk = this.#cmkCache.get(state.name);
|
|
471
505
|
if (!cmk)
|
|
472
506
|
throw new Error("CMK not loaded");
|
|
473
|
-
const { metadata, payload } = splitRecord(record, state
|
|
507
|
+
const { metadata, payload } = splitRecord(record, requireSchema(state));
|
|
474
508
|
const encrypted = this.#encryptPayload({
|
|
475
509
|
collectionName: state.name,
|
|
476
510
|
recordId,
|
|
@@ -721,7 +755,7 @@ class DataClientImpl {
|
|
|
721
755
|
if (!this.#deposit) {
|
|
722
756
|
throw new Error("sdk.data: _insertDeposit called without a deposit session");
|
|
723
757
|
}
|
|
724
|
-
const { metadata, payload } = splitRecord(record, state
|
|
758
|
+
const { metadata, payload } = splitRecord(record, requireSchema(state));
|
|
725
759
|
const recordId = `record_${makeUlid()}`;
|
|
726
760
|
const encrypted = this.#encryptPayloadForOwner({
|
|
727
761
|
collectionName: state.name,
|
|
@@ -741,7 +775,7 @@ class DataClientImpl {
|
|
|
741
775
|
/** Build a local collection state for the deposit path (no server fetch:
|
|
742
776
|
* append clients are not authorized to read collection metadata). */
|
|
743
777
|
_depositCollectionState(name, schema) {
|
|
744
|
-
return { name, urn: this.#collectionUrn(name), schema };
|
|
778
|
+
return { name, urn: this.#collectionUrn(name), schema, schemaId: schema.schema };
|
|
745
779
|
}
|
|
746
780
|
}
|
|
747
781
|
class DataCollectionImpl {
|
|
@@ -775,6 +809,58 @@ class DataCollectionImpl {
|
|
|
775
809
|
/* -------------------------------------------------------------------------- */
|
|
776
810
|
/* Record split (metadata vs payload) */
|
|
777
811
|
/* -------------------------------------------------------------------------- */
|
|
812
|
+
/**
|
|
813
|
+
* Derive an {@link AithosSchemaLite} from a PUBLISHED JSON Schema document (the
|
|
814
|
+
* shape `aithos.data.get_schema` / `registerSchema` round-trip). The field
|
|
815
|
+
* split is read from the per-property annotations:
|
|
816
|
+
* - `aithos:indexable: true` → indexable (server-visible, filter/sort)
|
|
817
|
+
* - `aithos:auto: …` → auto (server-populated, e.g. created_at)
|
|
818
|
+
* - anything else → encrypted (AEAD'd client-side)
|
|
819
|
+
*
|
|
820
|
+
* These annotations are authoritative — by convention they mirror the writer's
|
|
821
|
+
* own lite — so a reader that never bundled the schema can still split records
|
|
822
|
+
* correctly. `defaults` is left empty (it only matters for inserts; the writer
|
|
823
|
+
* supplies its own).
|
|
824
|
+
*/
|
|
825
|
+
export function liteFromPublishedSchema(doc) {
|
|
826
|
+
const d = doc;
|
|
827
|
+
const props = d.properties ?? {};
|
|
828
|
+
const indexable = new Set();
|
|
829
|
+
const encrypted = new Set();
|
|
830
|
+
const auto = new Set();
|
|
831
|
+
for (const [k, v] of Object.entries(props)) {
|
|
832
|
+
const hasAuto = v?.["aithos:auto"] !== undefined;
|
|
833
|
+
const isIndexable = v?.["aithos:indexable"] === true;
|
|
834
|
+
if (hasAuto)
|
|
835
|
+
auto.add(k);
|
|
836
|
+
if (isIndexable)
|
|
837
|
+
indexable.add(k);
|
|
838
|
+
if (!isIndexable && !hasAuto)
|
|
839
|
+
encrypted.add(k);
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
schema: d["aithos:schema"] ?? "",
|
|
843
|
+
indexable,
|
|
844
|
+
encrypted,
|
|
845
|
+
auto,
|
|
846
|
+
defaults: {},
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Return the collection's write schema or throw a precise error. Writes need
|
|
851
|
+
* the schema to split a record into indexable metadata vs encrypted payload (and
|
|
852
|
+
* so the PDS can validate); reads never call this.
|
|
853
|
+
*/
|
|
854
|
+
function requireSchema(state) {
|
|
855
|
+
if (!state.schema) {
|
|
856
|
+
const e = new Error(`sdk.data: writing to "${state.name}" needs its schema "${state.schemaId}", which is ` +
|
|
857
|
+
`neither bundled in this client (createDataClient({ schemas: [...] })) nor published on ` +
|
|
858
|
+
`the PDS (registerSchema). Reads work without it — only inserts/updates require it.`);
|
|
859
|
+
e.code = -32602;
|
|
860
|
+
throw e;
|
|
861
|
+
}
|
|
862
|
+
return state.schema;
|
|
863
|
+
}
|
|
778
864
|
function splitRecord(record, schema) {
|
|
779
865
|
const metadata = {};
|
|
780
866
|
const payload = {};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
1
|
+
export declare const VERSION = "0.1.0-alpha.56";
|
|
2
2
|
export { AithosSDK } from "./sdk.js";
|
|
3
3
|
export type { AithosSDKConfig } from "./types.js";
|
|
4
4
|
export { AithosSDKError } from "./types.js";
|
|
@@ -27,6 +27,8 @@ export type { AudienceSet, AppCreditPackId, CreateAppTopupSessionArgs, CreateApp
|
|
|
27
27
|
export * as onboarding from "./onboarding.js";
|
|
28
28
|
export { createBrowserIdentity, browserIdentityFromStored, type BrowserIdentity, } from "@aithos/protocol-client";
|
|
29
29
|
export type { Section } from "@aithos/protocol-client";
|
|
30
|
-
export { createDataClient, createDelegateDataClient, createAppendDataClient, type CreateDataClientArgs, type CreateDelegateDataClientArgs, type CreateAppendDataClientArgs, type DataClient, type DataCollection, type ReadonlyDataClient, type ReadonlyDataCollection, type AppendOnlyDataClient, type AppendOnlyDataCollection, type ListOpts, type AithosSchemaLite, } from "./data.js";
|
|
30
|
+
export { createDataClient, createDelegateDataClient, createAppendDataClient, type CreateDataClientArgs, type CreateDelegateDataClientArgs, type CreateAppendDataClientArgs, type DataClient, type DataCollection, type ReadonlyDataClient, type ReadonlyDataCollection, type AppendOnlyDataClient, type AppendOnlyDataCollection, type ListOpts, type AithosSchemaLite, liteFromPublishedSchema, } from "./data.js";
|
|
31
|
+
export { ensureDataSphere, rekeyLegacyCollections, addDataSphereWrap, migrateLegacyEthosToDataSphere, type MigrateOptions, type MigrateProgress, type EnsureDataSphereResult, type RekeyReport, type CollectionRekeyEntry, type CollectionRekeyStatus, type FullMigrationResult, } from "./migrate.js";
|
|
32
|
+
export { buildSignedRotatedDidDocument, rotateEthos, type RotationSeeds, type DidDocumentLike, type RotateEthosOptions, type RotateEthosResult, } from "./rotate.js";
|
|
31
33
|
export { createAssetsClient, AssetsClient, type CreateAssetsClientArgs, type AttachedContext, type AssetUploadInput, type AssetUploadResult, type AssetFetchResult, type AssetBrief, type ListAssetsOpts, type ThumbnailUploadInput, type ThumbnailUploadResult, type RecipientResolver, type RecipientSet, } from "./assets.js";
|
|
32
34
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
|
|
18
18
|
// are exported from here. Endpoint config (`AithosSdkEndpoints`,
|
|
19
19
|
// `DEFAULT_SDK_ENDPOINTS`) likewise.
|
|
20
|
-
export const VERSION = "0.1.0-alpha.
|
|
20
|
+
export const VERSION = "0.1.0-alpha.56";
|
|
21
21
|
export { AithosSDK } from "./sdk.js";
|
|
22
22
|
export { AithosSDKError } from "./types.js";
|
|
23
23
|
// Re-export protocol-client's JSON-RPC error type so consumers can
|
|
@@ -68,7 +68,20 @@ export { createBrowserIdentity, browserIdentityFromStored, } from "@aithos/proto
|
|
|
68
68
|
// `sdk.data` namespace — Aithos data sub-protocol PDS client. Manages
|
|
69
69
|
// the lifecycle of subject-owned, encrypted, schema-validated records.
|
|
70
70
|
// See spec/data/ in the aithos-protocol repo.
|
|
71
|
-
export { createDataClient, createDelegateDataClient, createAppendDataClient,
|
|
71
|
+
export { createDataClient, createDelegateDataClient, createAppendDataClient,
|
|
72
|
+
// Derive an AithosSchemaLite from a published JSON Schema (vendor schemas the
|
|
73
|
+
// client didn't bundle). The SDK uses this internally to auto-resolve unknown
|
|
74
|
+
// collection schemas from the PDS; exported for apps that want it directly.
|
|
75
|
+
liteFromPublishedSchema, } from "./data.js";
|
|
76
|
+
// Legacy `#data` sphere migration — add the dedicated #data sphere to a
|
|
77
|
+
// pre-#data identity and re-wrap its collections' CMK from the legacy sphere
|
|
78
|
+
// to #data. Idempotent, owner-only, dry-run capable. See ./migrate.ts.
|
|
79
|
+
export { ensureDataSphere, rekeyLegacyCollections, addDataSphereWrap, migrateLegacyEthosToDataSphere, } from "./migrate.js";
|
|
80
|
+
// Ethos rotation building blocks — buildSignedRotatedDidDocument produces a
|
|
81
|
+
// root-signed, rotated did.json (all spheres + #data, root unchanged) ready for
|
|
82
|
+
// aithos.rotate_sphere_key. The full rotateEthos orchestration (incl. zone
|
|
83
|
+
// recopy) builds on this. See ./rotate.ts.
|
|
84
|
+
export { buildSignedRotatedDidDocument, rotateEthos, } from "./rotate.js";
|
|
72
85
|
// `sdk.assets` — Aithos assets sub-protocol PDS client. Upload,
|
|
73
86
|
// fetch, list, ref/unref binary content (images, PDFs, audio, video)
|
|
74
87
|
// owned by a subject. AEAD-encrypted per-asset under AMKs wrapped for
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface CmkWrap {
|
|
2
|
+
readonly recipient: string;
|
|
3
|
+
readonly alg: "x25519-hkdf-sha256-aead";
|
|
4
|
+
readonly ephemeral_public: string;
|
|
5
|
+
readonly wrap_nonce: string;
|
|
6
|
+
readonly wrapped_key: string;
|
|
7
|
+
}
|
|
8
|
+
/** The CMK envelope stored on a collection. */
|
|
9
|
+
export interface CmkEnvelope {
|
|
10
|
+
readonly alg: string;
|
|
11
|
+
readonly wraps: CmkWrap[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Derive a 32-byte X25519 private key from an Ed25519 seed via SHA-512
|
|
15
|
+
* truncation + clamping (libsodium crypto_sign_ed25519_sk_to_curve25519).
|
|
16
|
+
*/
|
|
17
|
+
export declare function ed25519SeedToX25519PrivateKey(seed: Uint8Array): Uint8Array;
|
|
18
|
+
export declare function ed25519SeedToX25519PublicKey(seed: Uint8Array): Uint8Array;
|
|
19
|
+
export declare function wrapCmkForRecipient(args: {
|
|
20
|
+
cmk: Uint8Array;
|
|
21
|
+
recipientPublicKey: Uint8Array;
|
|
22
|
+
recipientDidUrl: string;
|
|
23
|
+
collectionUrn: string;
|
|
24
|
+
}): CmkWrap;
|
|
25
|
+
/**
|
|
26
|
+
* Try to unwrap the CMK from `wrap` with `privateKey`. Returns the CMK bytes
|
|
27
|
+
* on success, or `null` when the AEAD tag fails (wrong key / AAD mismatch) —
|
|
28
|
+
* the migration relies on this null-return to AUTO-DISCOVER which legacy sphere
|
|
29
|
+
* seed sealed the wrap (try each, the right one verifies).
|
|
30
|
+
*/
|
|
31
|
+
export declare function tryUnwrapCmk(args: {
|
|
32
|
+
wrap: {
|
|
33
|
+
recipient: string;
|
|
34
|
+
ephemeral_public: string;
|
|
35
|
+
wrap_nonce: string;
|
|
36
|
+
wrapped_key: string;
|
|
37
|
+
};
|
|
38
|
+
collectionUrn: string;
|
|
39
|
+
privateKey: Uint8Array;
|
|
40
|
+
}): Uint8Array | null;
|
|
41
|
+
//# sourceMappingURL=cmk-wrap.d.ts.map
|