@aithos/sdk 0.1.0-alpha.4 → 0.1.0-alpha.41

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.
Files changed (80) hide show
  1. package/README.md +211 -7
  2. package/dist/src/apps.d.ts +155 -0
  3. package/dist/src/apps.js +288 -0
  4. package/dist/src/assets.d.ts +207 -0
  5. package/dist/src/assets.js +533 -0
  6. package/dist/src/auth-api.d.ts +138 -0
  7. package/dist/src/auth-api.js +168 -0
  8. package/dist/src/auth.d.ts +536 -119
  9. package/dist/src/auth.js +1207 -152
  10. package/dist/src/compute.d.ts +251 -9
  11. package/dist/src/compute.js +293 -16
  12. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  13. package/dist/src/data-schema-contacts-v1.js +28 -0
  14. package/dist/src/data.d.ts +153 -0
  15. package/dist/src/data.js +670 -0
  16. package/dist/src/endpoints.d.ts +9 -0
  17. package/dist/src/endpoints.js +5 -0
  18. package/dist/src/ethos.d.ts +202 -1
  19. package/dist/src/ethos.js +821 -16
  20. package/dist/src/index.d.ts +18 -6
  21. package/dist/src/index.js +39 -6
  22. package/dist/src/internal/delegate-bundle.d.ts +18 -0
  23. package/dist/src/internal/delegate-bundle.js +94 -0
  24. package/dist/src/internal/delegate-state.d.ts +45 -0
  25. package/dist/src/internal/delegate-state.js +120 -0
  26. package/dist/src/internal/envelope.d.ts +77 -0
  27. package/dist/src/internal/envelope.js +154 -0
  28. package/dist/src/internal/owner-signers.d.ts +78 -0
  29. package/dist/src/internal/owner-signers.js +179 -0
  30. package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
  31. package/dist/src/internal/protocol-client-bridge.js +20 -0
  32. package/dist/src/internal/recovery-file.d.ts +29 -0
  33. package/dist/src/internal/recovery-file.js +98 -0
  34. package/dist/src/internal/signer.d.ts +59 -0
  35. package/dist/src/internal/signer.js +86 -0
  36. package/dist/src/key-store.d.ts +128 -0
  37. package/dist/src/key-store.js +244 -0
  38. package/dist/src/mandates.d.ts +163 -1
  39. package/dist/src/mandates.js +286 -8
  40. package/dist/src/react/AithosAsset.d.ts +66 -0
  41. package/dist/src/react/AithosAsset.js +67 -0
  42. package/dist/src/react/context.d.ts +29 -0
  43. package/dist/src/react/context.js +31 -0
  44. package/dist/src/react/index.d.ts +28 -0
  45. package/dist/src/react/index.js +30 -0
  46. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  47. package/dist/src/react/use-aithos-asset.js +118 -0
  48. package/dist/src/sdk.d.ts +46 -3
  49. package/dist/src/sdk.js +49 -23
  50. package/dist/src/wallet.d.ts +4 -6
  51. package/dist/src/wallet.js +18 -8
  52. package/dist/src/web.d.ts +279 -0
  53. package/dist/src/web.js +186 -0
  54. package/dist/test/auth-j3.test.d.ts +2 -0
  55. package/dist/test/auth-j3.test.js +391 -0
  56. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  57. package/dist/test/compute-delegate-path.test.js +183 -0
  58. package/dist/test/compute.test.js +26 -11
  59. package/dist/test/endpoints.test.js +20 -1
  60. package/dist/test/envelope.test.d.ts +2 -0
  61. package/dist/test/envelope.test.js +318 -0
  62. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  63. package/dist/test/ethos-first-edition.test.js +248 -0
  64. package/dist/test/ethos.test.d.ts +2 -0
  65. package/dist/test/ethos.test.js +219 -0
  66. package/dist/test/key-store.test.d.ts +2 -0
  67. package/dist/test/key-store.test.js +161 -0
  68. package/dist/test/mandates-compute.test.d.ts +2 -0
  69. package/dist/test/mandates-compute.test.js +256 -0
  70. package/dist/test/mandates.test.d.ts +2 -0
  71. package/dist/test/mandates.test.js +93 -0
  72. package/dist/test/sdk.test.js +70 -30
  73. package/dist/test/signer.test.d.ts +2 -0
  74. package/dist/test/signer.test.js +117 -0
  75. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  76. package/dist/test/signup-bootstrap.test.js +311 -0
  77. package/dist/test/wallet.test.js +20 -9
  78. package/dist/test/web.test.d.ts +2 -0
  79. package/dist/test/web.test.js +270 -0
  80. package/package.json +18 -3
@@ -0,0 +1,533 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ /**
4
+ * `sdk.assets` — high-level API for the Aithos assets sub-protocol PDS.
5
+ *
6
+ * Stores binary content (images, PDFs, audio, video) owned by a
7
+ * subject, encrypted client-side under per-asset AMKs (Asset Master
8
+ * Keys), accessible to authorized apps via signed mandates (v0.2).
9
+ *
10
+ * const assets = sdk.assets;
11
+ * const avatar = await assets.upload({
12
+ * bytes: pngBuffer,
13
+ * mediaType: "image/png",
14
+ * attachTo: { ethos: { zone: "public", sectionId: "sec_identity" } },
15
+ * });
16
+ * // avatar.url is a stable CloudFront URL (public asset)
17
+ *
18
+ * const cv = await assets.upload({
19
+ * bytes: pdfBuffer,
20
+ * mediaType: "application/pdf",
21
+ * attachTo: { ethos: { zone: "circle", sectionId: "sec_career_docs" } },
22
+ * });
23
+ * // cv is private: stored encrypted, fetched via short-lived presigned URL.
24
+ *
25
+ * const bytes = await assets.fetch(cv.urn);
26
+ * // decrypted plaintext returned to the caller
27
+ *
28
+ * The module wires:
29
+ * - AMK generation + wrap (via @aithos/assets-crypto)
30
+ * - Bytes encryption with the canonical nonce-prefix on-disk layout
31
+ * - RecipientResolver (v0.2-ethos: maps {zone} → recipient set)
32
+ * - Direct S3 PUT against the presigned URL returned by init_upload
33
+ * - In-memory AMK cache (per asset URN) for sub-second re-fetches
34
+ * - Signed envelope JSON-RPC dispatch to /mcp/primitives/{read,write}
35
+ *
36
+ * Spec ref: spec/assets/ in the aithos-protocol repo.
37
+ */
38
+ import { generateAMK, wrapAMKForRecipient, unwrapAMK, encryptAssetBytes, decryptAssetBytes, plaintextSha256Hex, } from "@aithos/assets-crypto";
39
+ import { x25519 } from "@noble/curves/ed25519.js";
40
+ import { sha512 } from "@noble/hashes/sha2.js";
41
+ import { signOwnerEnvelope } from "./internal/envelope.js";
42
+ /* -------------------------------------------------------------------------- */
43
+ /* Public factory */
44
+ /* -------------------------------------------------------------------------- */
45
+ export function createAssetsClient(args) {
46
+ return new AssetsClient(args);
47
+ }
48
+ /* -------------------------------------------------------------------------- */
49
+ /* AssetsClient implementation */
50
+ /* -------------------------------------------------------------------------- */
51
+ export class AssetsClient {
52
+ #pdsUrl;
53
+ #did;
54
+ #seed;
55
+ #verificationMethod;
56
+ #fetch;
57
+ #resolver;
58
+ /** Cache: URN → AMK (recovered on first fetch, reused thereafter). */
59
+ #amkCache = new Map();
60
+ constructor(args) {
61
+ this.#pdsUrl = args.pdsUrl.replace(/\/$/, "");
62
+ this.#did = args.did;
63
+ this.#seed = args.sphereSeed;
64
+ this.#verificationMethod = args.verificationMethod;
65
+ this.#fetch = args.fetch ?? globalThis.fetch.bind(globalThis);
66
+ this.#resolver = args.recipientResolver ?? defaultSelfResolver(args);
67
+ }
68
+ /* ------------------------------------------------------------------ */
69
+ /* upload */
70
+ /* ------------------------------------------------------------------ */
71
+ async upload(input) {
72
+ const regime = resolveRegime(input);
73
+ const sha = plaintextSha256Hex(input.bytes);
74
+ // For private regime, generate AMK + wraps eagerly so the
75
+ // init_upload call already carries the envelope (the server
76
+ // records it as part of the pending-upload metadata).
77
+ let amk;
78
+ let amkEnvelope;
79
+ let recipientSet;
80
+ if (regime === "private") {
81
+ recipientSet = await this.#resolver.resolve({
82
+ subjectDid: this.#did,
83
+ context: input.attachTo,
84
+ });
85
+ if (recipientSet.recipients.length === 0) {
86
+ throw new Error("sdk.assets.upload: private regime requires at least one recipient — check RecipientResolver");
87
+ }
88
+ amk = generateAMK();
89
+ // The bytes encryption nonce is computed inside encryptAssetBytes;
90
+ // the wrap list will be filled below once we know the final URN
91
+ // (it's bound into the AAD via assetUrn).
92
+ // We pre-compose a placeholder envelope to satisfy server
93
+ // validation, then re-emit with the real URN at init+complete.
94
+ amkEnvelope = {
95
+ alg: "xchacha20poly1305-ietf",
96
+ nonce: "",
97
+ wraps: [],
98
+ };
99
+ }
100
+ // init_upload
101
+ const initResp = (await this.#callWrite("aithos.assets.init_upload", {
102
+ subject_did: this.#did,
103
+ media_type: input.mediaType,
104
+ size_bytes: input.bytes.length,
105
+ sha256_of_plaintext: sha,
106
+ ...(input.attachTo ? { attached_context: serializeContext(input.attachTo) } : {}),
107
+ regime,
108
+ ...(input.forwardSecrecy ? { forward_secrecy: input.forwardSecrecy } : {}),
109
+ ...(amkEnvelope ? { amk_envelope: amkEnvelope } : {}),
110
+ }));
111
+ if (initResp.result === "dedup_hit") {
112
+ // Asset already exists — return existing URN; no PUT needed.
113
+ const asset = initResp.asset;
114
+ if (!asset) {
115
+ throw new Error("sdk.assets.upload: dedup_hit response is missing the asset metadata");
116
+ }
117
+ return {
118
+ urn: initResp.urn,
119
+ assetId: initResp.asset_id,
120
+ mediaType: asset.media_type,
121
+ sizeBytes: asset.size_bytes,
122
+ sha256OfPlaintext: asset.sha256_of_plaintext,
123
+ encrypted: asset.encrypted,
124
+ url: !asset.encrypted ? composePublicUrl(asset) : undefined,
125
+ dedupHit: true,
126
+ };
127
+ }
128
+ const finalUrn = initResp.urn;
129
+ // Build the real wraps using the final URN.
130
+ let blob;
131
+ let nonceB64;
132
+ let finalWraps = [];
133
+ if (regime === "private") {
134
+ finalWraps = recipientSet.recipients.map((r) => wrapAMKForRecipient({
135
+ amk: amk,
136
+ recipientPublicKey: r.x25519PublicKey,
137
+ recipientDidUrl: r.didUrl,
138
+ assetUrn: finalUrn,
139
+ }));
140
+ const enc = encryptAssetBytes({
141
+ amk: amk,
142
+ assetUrn: finalUrn,
143
+ plaintext: input.bytes,
144
+ });
145
+ blob = enc.blob;
146
+ nonceB64 = enc.nonce_b64;
147
+ }
148
+ else {
149
+ blob = input.bytes;
150
+ }
151
+ // PUT bytes to S3. TS lib.dom typings expect BodyInit; a plain
152
+ // Uint8Array is accepted at runtime by all modern fetch
153
+ // implementations (browser, Node 18+, undici), but the lib.dom
154
+ // signature requires a cast to a BodyInit-compatible view.
155
+ const putResp = await this.#fetch(initResp.upload_url, {
156
+ method: "PUT",
157
+ headers: {
158
+ "content-type": regime === "private" ? "application/octet-stream" : input.mediaType,
159
+ },
160
+ body: blob,
161
+ });
162
+ if (!putResp.ok) {
163
+ throw new Error(`sdk.assets.upload: S3 PUT failed with status ${putResp.status}`);
164
+ }
165
+ // complete_upload (with real AMK envelope for private uploads)
166
+ const completePayload = {
167
+ upload_session: initResp.upload_session,
168
+ observed_sha256_of_plaintext: sha,
169
+ };
170
+ if (regime === "private") {
171
+ // The server stores the amk_envelope from init_upload — for v0.1
172
+ // we don't have a separate "update_envelope_on_complete" hook,
173
+ // so we recompose it here client-side and call authorize_grantee-
174
+ // style updates if we need to push real wraps. The simpler v0.1
175
+ // workaround: include the envelope in init_upload from the start
176
+ // by precomputing the URN client-side — but the URN is allocated
177
+ // by the server. For now we ship the envelope on complete via a
178
+ // backwards-compatible extension field, and the backend stores
179
+ // it on the asset doc.
180
+ completePayload.amk_envelope = {
181
+ alg: "xchacha20poly1305-ietf",
182
+ nonce: nonceB64,
183
+ wraps: finalWraps,
184
+ };
185
+ }
186
+ const completeResp = (await this.#callWrite("aithos.assets.complete_upload", completePayload));
187
+ // Cache the AMK locally for the lifetime of this session.
188
+ if (amk) {
189
+ this.#amkCache.set(finalUrn, amk);
190
+ }
191
+ return {
192
+ urn: completeResp.urn,
193
+ assetId: initResp.asset_id,
194
+ mediaType: input.mediaType,
195
+ sizeBytes: input.bytes.length,
196
+ sha256OfPlaintext: sha,
197
+ encrypted: regime === "private",
198
+ url: regime === "public"
199
+ ? composePublicUrl(completeResp.asset)
200
+ : undefined,
201
+ dedupHit: false,
202
+ };
203
+ }
204
+ /* ------------------------------------------------------------------ */
205
+ /* uploadWithThumbnails */
206
+ /* ------------------------------------------------------------------ */
207
+ /**
208
+ * Upload a primary asset plus one or more thumbnails (downscaled
209
+ * client-side). Convenience method for the Deep avatar use case and
210
+ * any UI that displays the same asset at multiple resolutions.
211
+ *
212
+ * The thumbnails are attached to the same context as the primary
213
+ * and uploaded in parallel. They carry the {@link AssetReference}
214
+ * role `"thumbnail"` when referenced from a section (see
215
+ * spec/assets/03-asset-descriptors.md §3.2.3).
216
+ */
217
+ async uploadWithThumbnails(input) {
218
+ const primary = await this.upload(input);
219
+ const thumbnails = await Promise.all(input.sizes.map(async (size) => {
220
+ const scaledBytes = await input.downscale(input.bytes, size);
221
+ const thumb = await this.upload({
222
+ ...input,
223
+ bytes: scaledBytes,
224
+ // Thumbnails inherit the primary's attachTo so they live in
225
+ // the same recipient set.
226
+ });
227
+ return { size, result: thumb };
228
+ }));
229
+ return { primary, thumbnails };
230
+ }
231
+ /* ------------------------------------------------------------------ */
232
+ /* fetch */
233
+ /* ------------------------------------------------------------------ */
234
+ async fetch(urn) {
235
+ const resp = (await this.#callRead("aithos.assets.get_asset", {
236
+ urn,
237
+ }));
238
+ const asset = resp.asset;
239
+ const r = await this.#fetch(resp.fetch_url);
240
+ if (!r.ok) {
241
+ throw new Error(`sdk.assets.fetch: byte fetch failed with status ${r.status}`);
242
+ }
243
+ const blob = new Uint8Array(await r.arrayBuffer());
244
+ let bytes;
245
+ if (asset.encrypted) {
246
+ const amk = this.#amkCache.get(urn) ??
247
+ this.#unwrapAmkForSelf(urn, asset.amk_envelope);
248
+ this.#amkCache.set(urn, amk);
249
+ bytes = decryptAssetBytes({
250
+ amk,
251
+ assetUrn: urn,
252
+ blob,
253
+ expectedSha256Hex: asset.sha256_of_plaintext,
254
+ });
255
+ }
256
+ else {
257
+ bytes = blob;
258
+ // SHA verification still applies for public assets.
259
+ if (plaintextSha256Hex(bytes) !== asset.sha256_of_plaintext) {
260
+ throw new Error("sdk.assets.fetch: plaintext SHA-256 mismatch on public asset");
261
+ }
262
+ }
263
+ return {
264
+ urn,
265
+ mediaType: asset.media_type,
266
+ sizeBytes: asset.size_bytes,
267
+ bytes,
268
+ sha256OfPlaintext: asset.sha256_of_plaintext,
269
+ };
270
+ }
271
+ /* ------------------------------------------------------------------ */
272
+ /* head */
273
+ /* ------------------------------------------------------------------ */
274
+ async head(urn) {
275
+ const r = (await this.#callRead("aithos.assets.head_asset", {
276
+ urn,
277
+ }));
278
+ return r.asset;
279
+ }
280
+ /* ------------------------------------------------------------------ */
281
+ /* list */
282
+ /* ------------------------------------------------------------------ */
283
+ async list(opts = {}) {
284
+ const params = {
285
+ subject_did: this.#did,
286
+ };
287
+ if (opts.limit !== undefined)
288
+ params.limit = opts.limit;
289
+ if (opts.cursor)
290
+ params.cursor = opts.cursor;
291
+ if (opts.order)
292
+ params.order = opts.order;
293
+ if (opts.includeOrphaned)
294
+ params.include_orphaned = true;
295
+ if (opts.includeTombstoned)
296
+ params.include_tombstoned = true;
297
+ if (opts.filter) {
298
+ const f = {};
299
+ if (opts.filter.mediaTypePrefix)
300
+ f.media_type_prefix = opts.filter.mediaTypePrefix;
301
+ if (opts.filter.sizeBytes)
302
+ f.size_bytes = opts.filter.sizeBytes;
303
+ if (opts.filter.createdAfter)
304
+ f.created_after = opts.filter.createdAfter;
305
+ if (opts.filter.createdBefore)
306
+ f.created_before = opts.filter.createdBefore;
307
+ params.filter = f;
308
+ }
309
+ const r = (await this.#callRead("aithos.assets.list_assets", params));
310
+ return {
311
+ items: r.items.map((it) => ({
312
+ urn: it.urn,
313
+ assetId: it.asset_id,
314
+ mediaType: it.media_type,
315
+ sizeBytes: it.size_bytes,
316
+ sha256OfPlaintext: it.sha256_of_plaintext,
317
+ encrypted: it.encrypted,
318
+ state: it.state,
319
+ referenceCount: it.reference_count,
320
+ createdAt: it.created_at,
321
+ modifiedAt: it.modified_at,
322
+ })),
323
+ ...(r.next_cursor ? { nextCursor: r.next_cursor } : {}),
324
+ };
325
+ }
326
+ /* ------------------------------------------------------------------ */
327
+ /* ref / unref / listReferences */
328
+ /* ------------------------------------------------------------------ */
329
+ async ref(urn, reference) {
330
+ const r = (await this.#callWrite("aithos.assets.ref_asset", {
331
+ urn,
332
+ reference,
333
+ }));
334
+ return { referenceCount: r.reference_count, gammaRef: r.gamma_ref };
335
+ }
336
+ async unref(urn, reference) {
337
+ const r = (await this.#callWrite("aithos.assets.unref_asset", {
338
+ urn,
339
+ reference,
340
+ }));
341
+ return { referenceCount: r.reference_count, gammaRef: r.gamma_ref };
342
+ }
343
+ async listReferences(urn) {
344
+ const r = (await this.#callRead("aithos.assets.list_references", {
345
+ urn,
346
+ }));
347
+ return r.items;
348
+ }
349
+ /* ------------------------------------------------------------------ */
350
+ /* delete */
351
+ /* ------------------------------------------------------------------ */
352
+ async delete(urn) {
353
+ const r = (await this.#callWrite("aithos.assets.delete_asset", {
354
+ urn,
355
+ }));
356
+ this.#amkCache.delete(urn);
357
+ return { tombstonedAt: r.tombstoned_at, gammaRef: r.gamma_ref };
358
+ }
359
+ /* ------------------------------------------------------------------ */
360
+ /* reset cache */
361
+ /* ------------------------------------------------------------------ */
362
+ /** Zero in-memory AMK cache. Useful at user logout. */
363
+ reset() {
364
+ for (const k of this.#amkCache.values())
365
+ k.fill(0);
366
+ this.#amkCache.clear();
367
+ }
368
+ /* ------------------------------------------------------------------ */
369
+ /* Internals — envelope dispatch */
370
+ /* ------------------------------------------------------------------ */
371
+ async #callRead(method, params) {
372
+ return this.#dispatch("/mcp/primitives/read", method, params);
373
+ }
374
+ async #callWrite(method, params) {
375
+ return this.#dispatch("/mcp/primitives/write", method, params);
376
+ }
377
+ async #dispatch(path, method, params) {
378
+ const aud = `${this.#pdsUrl}${path}`;
379
+ const envelope = await this.#signEnvelope({ aud, method, params });
380
+ const body = {
381
+ jsonrpc: "2.0",
382
+ id: makeUlidLite(),
383
+ method,
384
+ params: { ...params, _envelope: envelope },
385
+ };
386
+ const r = await this.#fetch(aud, {
387
+ method: "POST",
388
+ headers: { "content-type": "application/json" },
389
+ body: JSON.stringify(body),
390
+ });
391
+ const json = (await r.json());
392
+ if (json.error) {
393
+ const err = new Error(json.error.message);
394
+ err.code = json.error.code;
395
+ err.data = json.error.data;
396
+ throw err;
397
+ }
398
+ return json.result ?? {};
399
+ }
400
+ async #signEnvelope(args) {
401
+ return signOwnerEnvelope({
402
+ iss: this.#did,
403
+ aud: args.aud,
404
+ method: args.method,
405
+ params: args.params,
406
+ verificationMethod: this.#verificationMethod,
407
+ signer: {
408
+ sign: async (msg) => {
409
+ // We delegate to @noble/ed25519 directly to avoid pulling the
410
+ // SDK's Signer abstraction surface into this module. The
411
+ // dependency is already present transitively via assets-crypto.
412
+ const { signAsync } = await import("@noble/ed25519");
413
+ return signAsync(msg, this.#seed);
414
+ },
415
+ },
416
+ });
417
+ }
418
+ /* ------------------------------------------------------------------ */
419
+ /* Internals — AMK unwrap for self */
420
+ /* ------------------------------------------------------------------ */
421
+ #unwrapAmkForSelf(urn, envelope) {
422
+ const ourPub = ed25519SeedToX25519PublicKey(this.#seed);
423
+ void ourPub;
424
+ const ourPriv = ed25519SeedToX25519PrivateKey(this.#seed);
425
+ // Try every wrap that names a recipient under our DID. The DID
426
+ // URL fragment is unconstrained — `#kex`, `#circle-kex`, `#self-kex`
427
+ // are all candidates. We attempt each wrap whose `recipient`
428
+ // starts with our DID; AAD binding makes wrong attempts fail
429
+ // cleanly.
430
+ const ours = envelope.wraps.filter((w) => w.recipient.startsWith(this.#did));
431
+ for (const wrap of ours) {
432
+ try {
433
+ return unwrapAMK({
434
+ wrap,
435
+ recipientPrivateKey: ourPriv,
436
+ assetUrn: urn,
437
+ });
438
+ }
439
+ catch {
440
+ // Wrong fragment / wrong key — try the next wrap.
441
+ }
442
+ }
443
+ throw new Error(`sdk.assets.fetch: no AMK wrap for ${this.#did} found in asset ${urn}`);
444
+ }
445
+ }
446
+ /* -------------------------------------------------------------------------- */
447
+ /* Default RecipientResolver — self-only */
448
+ /* -------------------------------------------------------------------------- */
449
+ function defaultSelfResolver(args) {
450
+ return {
451
+ async resolve(input) {
452
+ // For the default self-only resolver we always wrap to the
453
+ // subject's own X25519 sphere key. Other recipients (grantees)
454
+ // can be added explicitly later via authorize_grantee in v0.2.
455
+ const pub = ed25519SeedToX25519PublicKey(args.sphereSeed);
456
+ const didUrl = recipientDidUrlForContext(input.subjectDid, input.context);
457
+ return {
458
+ recipients: [{ didUrl, x25519PublicKey: pub }],
459
+ };
460
+ },
461
+ };
462
+ }
463
+ /** Pick a DID URL fragment matching the attaching zone. */
464
+ function recipientDidUrlForContext(subjectDid, context) {
465
+ if (context?.ethos?.zone === "circle")
466
+ return `${subjectDid}#circle-kex`;
467
+ if (context?.ethos?.zone === "self")
468
+ return `${subjectDid}#self-kex`;
469
+ if (context?.data)
470
+ return `${subjectDid}#data-kex`;
471
+ return `${subjectDid}#kex`;
472
+ }
473
+ function resolveRegime(input) {
474
+ if (input.regime === "public")
475
+ return "public";
476
+ if (input.regime === "private")
477
+ return "private";
478
+ if (input.attachTo?.ethos?.zone === "public")
479
+ return "public";
480
+ return "private";
481
+ }
482
+ function serializeContext(ctx) {
483
+ if (ctx.ethos) {
484
+ return {
485
+ kind: "ethos",
486
+ zone: ctx.ethos.zone,
487
+ ...(ctx.ethos.sectionId ? { section_id: ctx.ethos.sectionId } : {}),
488
+ };
489
+ }
490
+ if (ctx.data) {
491
+ return {
492
+ kind: "data",
493
+ collection_urn: ctx.data.collectionUrn,
494
+ ...(ctx.data.recordId ? { record_id: ctx.data.recordId } : {}),
495
+ };
496
+ }
497
+ return {};
498
+ }
499
+ function composePublicUrl(asset) {
500
+ return asset.public_url;
501
+ }
502
+ /**
503
+ * Derive a 32-byte X25519 private key from an Ed25519 seed via
504
+ * SHA-512 truncation + clamping. Same construction libsodium uses for
505
+ * `crypto_sign_ed25519_sk_to_curve25519`.
506
+ */
507
+ function ed25519SeedToX25519PrivateKey(seed) {
508
+ if (seed.length !== 32)
509
+ throw new Error("Ed25519 seed must be 32 bytes");
510
+ const h = sha512(seed);
511
+ const sk = new Uint8Array(h.slice(0, 32));
512
+ // Clamp per X25519 spec
513
+ sk[0] = sk[0] & 248;
514
+ sk[31] = sk[31] & 127;
515
+ sk[31] = sk[31] | 64;
516
+ return sk;
517
+ }
518
+ function ed25519SeedToX25519PublicKey(seed) {
519
+ const sk = ed25519SeedToX25519PrivateKey(seed);
520
+ try {
521
+ return x25519.getPublicKey(sk);
522
+ }
523
+ finally {
524
+ sk.fill(0);
525
+ }
526
+ }
527
+ function makeUlidLite() {
528
+ // Lightweight monotonic-ish id for JSON-RPC `id` field — not
529
+ // crypto-grade. Production code uses ulid().
530
+ return (Date.now().toString(36) +
531
+ Math.random().toString(36).substring(2, 10));
532
+ }
533
+ //# sourceMappingURL=assets.js.map
@@ -21,6 +21,15 @@ export interface RegisterApiResponse {
21
21
  readonly exp: number;
22
22
  }
23
23
  export declare function registerAccount(http: HttpClient, input: RegisterApiInput): Promise<RegisterApiResponse>;
24
+ export interface PutBlobApiInput {
25
+ readonly jwt: string;
26
+ readonly blob: Uint8Array;
27
+ readonly blobNonce: Uint8Array;
28
+ readonly blobVersion: number;
29
+ }
30
+ export declare function putBlob(http: HttpClient, input: PutBlobApiInput): Promise<{
31
+ ok: true;
32
+ }>;
24
33
  export interface LoginChallengeResponse {
25
34
  readonly authSalt: Uint8Array;
26
35
  readonly encSalt: Uint8Array;
@@ -37,5 +46,134 @@ export interface LoginVerifyResponse {
37
46
  readonly blobVersion: number;
38
47
  }
39
48
  export declare function loginVerify(http: HttpClient, email: string, authKey: Uint8Array): Promise<LoginVerifyResponse>;
49
+ /**
50
+ * Input for {@link custodialSignUp}. Caller authenticates the app via ONE
51
+ * of `apiKey` (server-only secret) or `publicKey` (browser-safe). The
52
+ * user's `password` is always required — sign-up no longer auto-generates
53
+ * one server-side. The created account starts in a *pending* state and
54
+ * the user must click the link sent to their inbox before they can
55
+ * sign in.
56
+ */
57
+ export interface CustodialSignUpApiInput {
58
+ /** Server-only Bearer secret: `aithos_<env>_<…>`. Mutually exclusive
59
+ * with `publicKey`. Use this from your backend. */
60
+ readonly apiKey?: string;
61
+ /** Browser-safe public client key: `pk_<env>_<…>`. Mutually exclusive
62
+ * with `apiKey`. The browser sends its `Origin` header alongside; the
63
+ * Aithos backend matches it against the app's allowed_origins list. */
64
+ readonly publicKey?: string;
65
+ readonly email: string;
66
+ /** Raw password the user chose. Must be ≥ 10 chars and mix letters
67
+ * with at least one digit or symbol (server-side rule). */
68
+ readonly password: string;
69
+ readonly displayName?: string;
70
+ readonly handleHint?: string;
71
+ }
72
+ export interface CustodialSignUpApiResponse {
73
+ /** Always "pending_verification" — sign-in is blocked until the
74
+ * user clicks the confirmation link. */
75
+ readonly status: "pending_verification";
76
+ readonly email: string;
77
+ readonly mailSent: boolean;
78
+ readonly mailMessageId?: string;
79
+ }
80
+ /**
81
+ * Provision a custodial-mode account on behalf of a registered app.
82
+ *
83
+ * Two integration paths:
84
+ * - **Backend** caller passes `apiKey` (server-only secret).
85
+ * - **Browser** caller passes `publicKey` (safe to ship in the bundle).
86
+ * The browser also sends its `Origin` header automatically and the
87
+ * auth backend validates it against the app's allowed_origins list.
88
+ *
89
+ * On success the account exists in DDB with `email_verified: false` and
90
+ * the response carries `status: "pending_verification"` — call
91
+ * {@link custodialVerifyEmail} after the user clicks the confirmation
92
+ * link before attempting sign-in.
93
+ */
94
+ export declare function custodialSignUp(http: HttpClient, input: CustodialSignUpApiInput): Promise<CustodialSignUpApiResponse>;
95
+ export interface CustodialVerifyEmailApiInput {
96
+ readonly email: string;
97
+ readonly token: string;
98
+ }
99
+ /**
100
+ * Result of consuming the verification link. Magic-link mode: a
101
+ * successful first-time consumption returns a full session payload
102
+ * (JWT + seeds) so the caller can sign the user in without prompting
103
+ * for the password. Replays of the same link (after first consumption)
104
+ * land on `status: "already_verified"` — the user is already verified
105
+ * and must use the regular sign-in flow from now on.
106
+ *
107
+ * The discriminator is the `status` field. Callers should pattern-match.
108
+ */
109
+ export type CustodialVerifyEmailApiResponse = {
110
+ readonly status: "signed_in";
111
+ readonly session: string;
112
+ readonly exp: number;
113
+ readonly did: string;
114
+ readonly handle: string;
115
+ readonly displayName: string;
116
+ readonly seed: Uint8Array;
117
+ readonly encKey: Uint8Array;
118
+ readonly blob: Uint8Array;
119
+ readonly blobNonce: Uint8Array;
120
+ readonly blobVersion: number;
121
+ } | {
122
+ readonly status: "already_verified";
123
+ readonly email: string;
124
+ };
125
+ /**
126
+ * Consume the verification token from the confirmation link. On a fresh
127
+ * click: returns a full session payload (magic-link auto-signin). On a
128
+ * replayed click of an already-consumed link: returns
129
+ * `{ status: "already_verified" }`.
130
+ *
131
+ * Throws `auth_token_invalid_or_expired` if the token is wrong, consumed,
132
+ * or past its TTL.
133
+ */
134
+ export declare function custodialVerifyEmail(http: HttpClient, input: CustodialVerifyEmailApiInput): Promise<CustodialVerifyEmailApiResponse>;
135
+ /** Re-send the verification mail for a pending account. The backend
136
+ * is anti-enum (always 200) and rate-limited 1/h/account, so this is
137
+ * safe to call even when the user state is unknown. Accepts the same
138
+ * credential families as {@link custodialSignUp}. */
139
+ export declare function custodialResendVerify(http: HttpClient, args: {
140
+ readonly email: string;
141
+ readonly apiKey?: string;
142
+ readonly publicKey?: string;
143
+ }): Promise<void>;
144
+ export interface CustodialSignInApiInput {
145
+ readonly email: string;
146
+ readonly password: string;
147
+ }
148
+ export interface CustodialSignInApiResponse {
149
+ readonly session: string;
150
+ readonly exp: number;
151
+ readonly did: string;
152
+ readonly handle: string;
153
+ readonly displayName: string;
154
+ /** Raw 32-byte Ed25519 seed — caller MUST hydrate its keystore and
155
+ * zeroize this buffer. */
156
+ readonly seed: Uint8Array;
157
+ /** Raw 32-byte vault encryption key — same lifecycle. */
158
+ readonly encKey: Uint8Array;
159
+ readonly blob: Uint8Array;
160
+ readonly blobNonce: Uint8Array;
161
+ readonly blobVersion: number;
162
+ readonly passwordMustChange: boolean;
163
+ }
164
+ export declare function custodialSignIn(http: HttpClient, input: CustodialSignInApiInput): Promise<CustodialSignInApiResponse>;
165
+ export declare function custodialResetRequest(http: HttpClient, email: string): Promise<void>;
166
+ export interface CustodialResetFinalizeApiInput {
167
+ readonly email: string;
168
+ readonly token: string;
169
+ readonly newPassword: string;
170
+ }
171
+ export interface CustodialResetFinalizeApiResponse {
172
+ readonly session: string;
173
+ readonly exp: number;
174
+ readonly did: string;
175
+ readonly handle: string;
176
+ }
177
+ export declare function custodialResetFinalize(http: HttpClient, input: CustodialResetFinalizeApiInput): Promise<CustodialResetFinalizeApiResponse>;
40
178
  export {};
41
179
  //# sourceMappingURL=auth-api.d.ts.map