@aithos/sdk 0.1.0-alpha.53 → 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}.
@@ -90,6 +90,89 @@ export interface InvokeBedrockResult {
90
90
  */
91
91
  readonly sponsoredRemainingForUser?: number;
92
92
  }
93
+ /** A decrypted ethos section in the working-set. */
94
+ export interface WorkingSetSection {
95
+ readonly id: string;
96
+ readonly title: string;
97
+ readonly body: string;
98
+ }
99
+ /**
100
+ * Client-decrypted data the agentic loop may read server-side. Built in the
101
+ * browser (where the keys live) from exactly the zones / collections the
102
+ * mandate grants — the proxy never holds a standing decryption key
103
+ * (see PLATFORM-COMPUTE-AGENTIC-MCP.md §4). Use {@link ComputeNamespace}
104
+ * with a `mcp` reference + this working-set.
105
+ */
106
+ export interface ComputeWorkingSet {
107
+ /** Decrypted ethos sections, per zone. Only granted zones appear. */
108
+ readonly ethos?: {
109
+ readonly public?: readonly WorkingSetSection[];
110
+ readonly circle?: readonly WorkingSetSection[];
111
+ readonly self?: readonly WorkingSetSection[];
112
+ };
113
+ /** Decrypted structured data, per collection name. */
114
+ readonly data?: Record<string, readonly Record<string, unknown>[]>;
115
+ }
116
+ /** A tool invocation the loop performed (trace for UI / debugging). */
117
+ export interface ConverseToolCall {
118
+ readonly name: string;
119
+ readonly ok: boolean;
120
+ readonly turn: number;
121
+ }
122
+ export type ConverseStopReason = "end_turn" | "max_tokens" | "stop_sequence" | "max_iterations" | "budget_exhausted";
123
+ export interface RunConversationArgs {
124
+ /** Mandate id — optional for owner sessions, required for delegate sessions. */
125
+ readonly mandateId?: string;
126
+ /** Claude model id (text or vision). */
127
+ readonly model: string;
128
+ /** Initial conversation. The loop's internal tool turns are server-side. */
129
+ readonly messages: readonly ComputeMessage[];
130
+ /** Optional system prompt. */
131
+ readonly system?: string;
132
+ /**
133
+ * MCP tools to expose to the model. v1 only supports the Aithos server.
134
+ * Omit `tools` to expose the full Aithos catalogue; pass a subset to narrow.
135
+ */
136
+ readonly mcp?: {
137
+ readonly server: "aithos";
138
+ readonly tools?: readonly string[];
139
+ };
140
+ /**
141
+ * Client-decrypted working-set the tools read from. Build it with
142
+ * {@link ComputeNamespace.buildWorkingSet}. Required for any tool that
143
+ * touches private (circle/self) or structured data.
144
+ */
145
+ readonly workingSet?: ComputeWorkingSet;
146
+ /** Cap on Bedrock turns. Server clamps to its own hard cap. Default 6. */
147
+ readonly maxIterations?: number;
148
+ /** Per-turn output token cap. */
149
+ readonly maxTokens?: number;
150
+ readonly temperature?: number;
151
+ /** Idempotency key for the WHOLE conversation (generated if omitted). */
152
+ readonly idempotencyKey?: string;
153
+ readonly signal?: AbortSignal;
154
+ }
155
+ export interface RunConversationResult {
156
+ /** Final assistant text. */
157
+ readonly content: string;
158
+ readonly stopReason: ConverseStopReason;
159
+ /** Number of Bedrock turns performed (each was a billed call). */
160
+ readonly iterations: number;
161
+ /** Token usage summed over all turns. */
162
+ readonly usage: {
163
+ readonly inputTokens: number;
164
+ readonly outputTokens: number;
165
+ };
166
+ /** Trace of the tool calls the loop made. */
167
+ readonly toolCalls: readonly ConverseToolCall[];
168
+ /** Total microcredits debited for the whole conversation. */
169
+ readonly creditsCharged: number;
170
+ readonly walletBalance: number;
171
+ readonly auditId: string;
172
+ readonly fundedBy?: "sponsored" | "grant" | "purchase";
173
+ readonly receiptId?: string;
174
+ readonly sponsoredBy?: string;
175
+ }
93
176
  /**
94
177
  * Stable cross-provider image model ids supported by the Aithos compute
95
178
  * proxy. New models can be added on the server side without an SDK
@@ -399,6 +482,21 @@ export declare class ComputeNamespace {
399
482
  * `mandate_revoked`, `insufficient_credits`, …).
400
483
  */
401
484
  invokeBedrock(args: InvokeBedrockArgs): Promise<InvokeBedrockResult>;
485
+ /**
486
+ * Run an agentic conversation: a multi-turn Bedrock tool-calling loop that
487
+ * runs server-side in a single POST and is billed once for the cumulative
488
+ * token usage. Tools come from the Aithos MCP (declared via `mcp`); the
489
+ * model reads private user data from the client-decrypted `workingSet`.
490
+ *
491
+ * The loop, tool dispatch, and billing all happen on the proxy — the SDK
492
+ * makes ONE signed request and gets the final answer. Same signer paths as
493
+ * {@link invokeBedrock} (owner direct or delegate-under-mandate).
494
+ *
495
+ * @throws {AithosSDKError} `sdk_no_signer`, `sdk_no_delegate_for_mandate`,
496
+ * `network`, `http`, `empty`, or any proxy code (`quota_exceeded`,
497
+ * `mandate_revoked`, `insufficient_credits`, …).
498
+ */
499
+ runConversation(args: RunConversationArgs): Promise<RunConversationResult>;
402
500
  /**
403
501
  * Multimodal Bedrock invoke — image + text → text response.
404
502
  * Default model: `claude-sonnet-4-6` (vision-capable, reliable JSON).
@@ -101,6 +101,53 @@ export class ComputeNamespace {
101
101
  signal: args.signal,
102
102
  });
103
103
  }
104
+ /**
105
+ * Run an agentic conversation: a multi-turn Bedrock tool-calling loop that
106
+ * runs server-side in a single POST and is billed once for the cumulative
107
+ * token usage. Tools come from the Aithos MCP (declared via `mcp`); the
108
+ * model reads private user data from the client-decrypted `workingSet`.
109
+ *
110
+ * The loop, tool dispatch, and billing all happen on the proxy — the SDK
111
+ * makes ONE signed request and gets the final answer. Same signer paths as
112
+ * {@link invokeBedrock} (owner direct or delegate-under-mandate).
113
+ *
114
+ * @throws {AithosSDKError} `sdk_no_signer`, `sdk_no_delegate_for_mandate`,
115
+ * `network`, `http`, `empty`, or any proxy code (`quota_exceeded`,
116
+ * `mandate_revoked`, `insufficient_credits`, …).
117
+ */
118
+ async runConversation(args) {
119
+ const { endpoints, fetch: fetchImpl } = this.#deps;
120
+ const choice = this.#resolveSigner(args.mandateId);
121
+ const url = computeInvokeUrl(endpoints);
122
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
123
+ const params = {
124
+ app_did: this.#deps.appDid,
125
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
126
+ model: args.model,
127
+ messages: args.messages,
128
+ idempotency_key: idempotencyKey,
129
+ };
130
+ if (args.system !== undefined)
131
+ params.system = args.system;
132
+ if (args.mcp !== undefined)
133
+ params.mcp = args.mcp;
134
+ if (args.workingSet !== undefined)
135
+ params.working_set = args.workingSet;
136
+ if (args.maxTokens !== undefined)
137
+ params.max_tokens = args.maxTokens;
138
+ if (args.maxIterations !== undefined)
139
+ params.max_iterations = args.maxIterations;
140
+ if (args.temperature !== undefined)
141
+ params.temperature = args.temperature;
142
+ return await this.#signAndPost({
143
+ url,
144
+ method: "aithos.compute_converse",
145
+ params,
146
+ choice,
147
+ fetchImpl,
148
+ signal: args.signal,
149
+ });
150
+ }
104
151
  /**
105
152
  * Multimodal Bedrock invoke — image + text → text response.
106
153
  * Default model: `claude-sonnet-4-6` (vision-capable, reliable JSON).
@@ -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,11 +1,11 @@
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";
5
5
  export { AithosRpcError } from "@aithos/protocol-client";
6
6
  export type { AithosSdkEndpoints } from "./endpoints.js";
7
7
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
8
- export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, TranscribeModelId, TranscribeProgressState, TranscribeSegment, TranscribeWord, InvokeTranscribeArgs, InvokeTranscribeResult, PrepareTranscribeArgs, PrepareTranscribeResult, StartTranscribeArgs, StartTranscribeResult, TranscribeStatusResult, TranscribeJobSummary, } from "./compute.js";
8
+ export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, RunConversationArgs, RunConversationResult, ComputeWorkingSet, WorkingSetSection, ConverseToolCall, ConverseStopReason, TranscribeModelId, TranscribeProgressState, TranscribeSegment, TranscribeWord, InvokeTranscribeArgs, InvokeTranscribeResult, PrepareTranscribeArgs, PrepareTranscribeResult, StartTranscribeArgs, StartTranscribeResult, TranscribeStatusResult, TranscribeJobSummary, } from "./compute.js";
9
9
  export { ComputeNamespace } from "./compute.js";
10
10
  export type { LocalPendingEntry, LocalPendingStatus, TranscribeDraftMeta, TranscribeDraftRecord, } from "./transcribe-resilience.js";
11
11
  export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.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