@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 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 4 Ed25519 sphere seeds
173
- // the user can publish ethos editions, mint mandates, invoke compute,
174
- // exactly as if they had signed in via a recovery file or Google SSO.
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.
@@ -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
- /** Subject DID. */
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
- /** Ed25519 sphere seed (32 bytes). */
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
- /** Verification method URL within the DID document (e.g. `<did>#<multibase>` for did:key). */
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
- seedsHex: {
994
- root: bytesToHex(seedRoot),
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
- seedsHex: {
1124
- root: bytesToHex(seedRoot),
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 128-byte seed bundle into the four sphere seeds. The
1216
- // backend lays them out in the canonical order
1217
- // [root || public || circle || self] (cf. seed-wrapper.ts).
1218
- if (resp.seed.byteLength !== 128) {
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
- seedsHex: {
1239
- root: bytesToHex(seedRoot),
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}.
@@ -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
- /** Subject DID — typically did:key:… in dev, did:aithos:… in prod. */
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). For did:key this is the same as
20
- * the key embedded in the DID itself. For did:aithos this is the
21
- * subject's `#data` (or `#public`) sphere key.
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:key this is `<did>#<multibase>`. For did:aithos
27
- * this is `<did>#data` (or `#public`).
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
- const wrap = meta.cmk_envelope.wraps.find((w) => w.recipient === ourRecipient);
374
- if (!wrap) {
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,18 +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 cmk = this.#unwrapCmk({
383
- wrap,
384
- collectionUrn: meta.urn,
385
- privateKey: ed25519SeedToX25519PrivateKey(unwrapSeed),
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
- const schema = this.#resolveSchema(meta.schema);
408
+ let schema = this.#resolveSchema(meta.schema);
409
+ if (!schema) {
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 — surface the friendly error below
423
+ }
424
+ }
389
425
  if (!schema) {
390
- throw new Error(`sdk.data: schema "${meta.schema}" not known to the SDK. ` +
391
- `Pass it via createDataClient({ schemas: [...] }) if it's an ` +
392
- `app-defined (vendor) schema, or upgrade the SDK if it's a core ` +
393
- `schema added in a later release.`);
426
+ throw new Error(`sdk.data: schema "${meta.schema}" not known to the SDK and not published ` +
427
+ `on the PDS for ${this.#did}. Pass it via createDataClient({ schemas: [...] }) ` +
428
+ `if it's an app-defined (vendor) schema, register it via registerSchema, ` +
429
+ `or upgrade the SDK if it's a core schema added in a later release.`);
394
430
  }
395
431
  const state = { name, urn: meta.urn, schema };
396
432
  this.#colCache.set(name, state);
@@ -775,6 +811,43 @@ class DataCollectionImpl {
775
811
  /* -------------------------------------------------------------------------- */
776
812
  /* Record split (metadata vs payload) */
777
813
  /* -------------------------------------------------------------------------- */
814
+ /**
815
+ * Derive an {@link AithosSchemaLite} from a PUBLISHED JSON Schema document (the
816
+ * shape `aithos.data.get_schema` / `registerSchema` round-trip). The field
817
+ * split is read from the per-property annotations:
818
+ * - `aithos:indexable: true` → indexable (server-visible, filter/sort)
819
+ * - `aithos:auto: …` → auto (server-populated, e.g. created_at)
820
+ * - anything else → encrypted (AEAD'd client-side)
821
+ *
822
+ * These annotations are authoritative — by convention they mirror the writer's
823
+ * own lite — so a reader that never bundled the schema can still split records
824
+ * correctly. `defaults` is left empty (it only matters for inserts; the writer
825
+ * supplies its own).
826
+ */
827
+ export function liteFromPublishedSchema(doc) {
828
+ const d = doc;
829
+ const props = d.properties ?? {};
830
+ const indexable = new Set();
831
+ const encrypted = new Set();
832
+ const auto = new Set();
833
+ for (const [k, v] of Object.entries(props)) {
834
+ const hasAuto = v?.["aithos:auto"] !== undefined;
835
+ const isIndexable = v?.["aithos:indexable"] === true;
836
+ if (hasAuto)
837
+ auto.add(k);
838
+ if (isIndexable)
839
+ indexable.add(k);
840
+ if (!isIndexable && !hasAuto)
841
+ encrypted.add(k);
842
+ }
843
+ return {
844
+ schema: d["aithos:schema"] ?? "",
845
+ indexable,
846
+ encrypted,
847
+ auto,
848
+ defaults: {},
849
+ };
850
+ }
778
851
  function splitRecord(record, schema) {
779
852
  const metadata = {};
780
853
  const payload = {};
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0-alpha.44";
1
+ export declare const VERSION = "0.1.0-alpha.55";
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.44";
20
+ export const VERSION = "0.1.0-alpha.55";
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, } from "./data.js";
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
@@ -0,0 +1,132 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ /**
4
+ * CMK wrap / unwrap primitives for the data sub-protocol.
5
+ *
6
+ * These are a CANONICAL, byte-identical copy of the private crypto used by
7
+ * `DataClientImpl` in `../data.ts` (`#wrapCmkForRecipient` / `#unwrapCmk` and
8
+ * the X25519-seed derivation). They are factored out here so the legacy
9
+ * #data-sphere migration (`../migrate.ts`) can re-wrap a collection's CMK to
10
+ * the owner's `#data` key WITHOUT going through the high-level `DataClient`
11
+ * (which is hard-wired to a single sphere seed and so cannot unwrap a wrap
12
+ * sealed to a *different* legacy sphere).
13
+ *
14
+ * INVARIANT: the wire format produced here MUST stay byte-identical to
15
+ * `data.ts`. `test/cmk-wrap-conformance.test.ts` proves it by cross-unwrapping
16
+ * (data.ts wraps → cmk-wrap unwraps, and vice-versa). If you change one, change
17
+ * both and keep that test green — a drift here silently corrupts migrated data.
18
+ */
19
+ import { x25519 } from "@noble/curves/ed25519.js";
20
+ import { hkdf } from "@noble/hashes/hkdf.js";
21
+ import { sha256, sha512 } from "@noble/hashes/sha2.js";
22
+ import { XChaCha20Poly1305 } from "@stablelib/xchacha20poly1305";
23
+ /* -------------------------------------------------------------------------- */
24
+ /* X25519 seed derivation (mirrors data.ts) */
25
+ /* -------------------------------------------------------------------------- */
26
+ /**
27
+ * Derive a 32-byte X25519 private key from an Ed25519 seed via SHA-512
28
+ * truncation + clamping (libsodium crypto_sign_ed25519_sk_to_curve25519).
29
+ */
30
+ export function ed25519SeedToX25519PrivateKey(seed) {
31
+ if (seed.length !== 32)
32
+ throw new Error("Ed25519 seed must be 32 bytes");
33
+ const h = sha512(seed);
34
+ const sk = new Uint8Array(h.slice(0, 32));
35
+ sk[0] = sk[0] & 248;
36
+ sk[31] = sk[31] & 127;
37
+ sk[31] = sk[31] | 64;
38
+ return sk;
39
+ }
40
+ export function ed25519SeedToX25519PublicKey(seed) {
41
+ const sk = ed25519SeedToX25519PrivateKey(seed);
42
+ try {
43
+ return x25519.getPublicKey(sk);
44
+ }
45
+ finally {
46
+ sk.fill(0);
47
+ }
48
+ }
49
+ /* -------------------------------------------------------------------------- */
50
+ /* Wrap / unwrap (mirrors data.ts #wrapCmkForRecipient / #unwrapCmk) */
51
+ /* -------------------------------------------------------------------------- */
52
+ export function wrapCmkForRecipient(args) {
53
+ const ephSk = x25519.utils.randomSecretKey();
54
+ const ephPk = x25519.getPublicKey(ephSk);
55
+ const shared = x25519.getSharedSecret(ephSk, args.recipientPublicKey);
56
+ const wrapKey = hkdf(sha256, shared, utf8("aithos-data-cmk-wrap-v1"), utf8(args.recipientDidUrl), 32);
57
+ const wrapNonce = randomBytes24();
58
+ const aad = aadCmkWrap(args.collectionUrn, args.recipientDidUrl);
59
+ const aead = new XChaCha20Poly1305(wrapKey);
60
+ const wrapped = aead.seal(wrapNonce, args.cmk, aad);
61
+ wrapKey.fill(0);
62
+ shared.fill(0);
63
+ return {
64
+ recipient: args.recipientDidUrl,
65
+ alg: "x25519-hkdf-sha256-aead",
66
+ ephemeral_public: base64Std(ephPk),
67
+ wrap_nonce: base64Std(wrapNonce),
68
+ wrapped_key: base64Std(wrapped),
69
+ };
70
+ }
71
+ /**
72
+ * Try to unwrap the CMK from `wrap` with `privateKey`. Returns the CMK bytes
73
+ * on success, or `null` when the AEAD tag fails (wrong key / AAD mismatch) —
74
+ * the migration relies on this null-return to AUTO-DISCOVER which legacy sphere
75
+ * seed sealed the wrap (try each, the right one verifies).
76
+ */
77
+ export function tryUnwrapCmk(args) {
78
+ const ephPk = fromBase64(args.wrap.ephemeral_public);
79
+ const wrapNonce = fromBase64(args.wrap.wrap_nonce);
80
+ const wrappedKey = fromBase64(args.wrap.wrapped_key);
81
+ const shared = x25519.getSharedSecret(args.privateKey, ephPk);
82
+ const wrapKey = hkdf(sha256, shared, utf8("aithos-data-cmk-wrap-v1"), utf8(args.wrap.recipient), 32);
83
+ const aad = aadCmkWrap(args.collectionUrn, args.wrap.recipient);
84
+ const aead = new XChaCha20Poly1305(wrapKey);
85
+ const cmk = aead.open(wrapNonce, wrappedKey, aad);
86
+ wrapKey.fill(0);
87
+ shared.fill(0);
88
+ return cmk ?? null;
89
+ }
90
+ /* -------------------------------------------------------------------------- */
91
+ /* Local helpers (byte-identical to data.ts) */
92
+ /* -------------------------------------------------------------------------- */
93
+ function aadCmkWrap(collectionUrn, recipient) {
94
+ const p = utf8("aithos-data-cmk-v1\0");
95
+ const c = utf8(collectionUrn);
96
+ const sep = new Uint8Array([0]);
97
+ const r = utf8(recipient);
98
+ const out = new Uint8Array(p.length + c.length + sep.length + r.length);
99
+ let off = 0;
100
+ out.set(p, off);
101
+ off += p.length;
102
+ out.set(c, off);
103
+ off += c.length;
104
+ out.set(sep, off);
105
+ off += sep.length;
106
+ out.set(r, off);
107
+ return out;
108
+ }
109
+ function utf8(s) {
110
+ return new TextEncoder().encode(s);
111
+ }
112
+ function randomBytes24() {
113
+ const buf = new Uint8Array(24);
114
+ globalThis.crypto?.getRandomValues(buf);
115
+ return buf;
116
+ }
117
+ function base64Std(bytes) {
118
+ let bin = "";
119
+ for (let i = 0; i < bytes.length; i++)
120
+ bin += String.fromCharCode(bytes[i]);
121
+ return btoa(bin);
122
+ }
123
+ function fromBase64(s) {
124
+ const std = s.replace(/-/g, "+").replace(/_/g, "/");
125
+ const padded = std + "=".repeat((4 - (std.length % 4)) % 4);
126
+ const bin = atob(padded);
127
+ const out = new Uint8Array(bin.length);
128
+ for (let i = 0; i < bin.length; i++)
129
+ out[i] = bin.charCodeAt(i);
130
+ return out;
131
+ }
132
+ //# sourceMappingURL=cmk-wrap.js.map
@@ -20,9 +20,13 @@ export interface StoredOwnerKeys {
20
20
  readonly circle: string;
21
21
  readonly self: string;
22
22
  /**
23
- * Optional dedicated #data sphere seed (spec/data/02-key-hierarchy.md).
24
- * Present on owners created since the #data sphere landed; absent on legacy
25
- * owners (which sign data/asset PDS ops under #root).
23
+ * Dedicated `#data` sphere seed (spec/data/02-key-hierarchy.md) — the key
24
+ * that signs ALL data/asset PDS operations (create, edit, manage
25
+ * collections), keeping the root cold. Present on owners created since the
26
+ * `#data` sphere landed. Absent only on legacy owners, which predate it and
27
+ * therefore have no dedicated PDS key — migrate them to `#data` (the backend
28
+ * does this lazily on next custodial sign-in; self-custody/SSO already carry
29
+ * it). Signing PDS ops under any other sphere is a legacy anti-pattern.
26
30
  */
27
31
  readonly data?: string;
28
32
  };