@aithos/sdk 0.1.0-alpha.4 → 0.1.0-alpha.40
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 +211 -7
- package/dist/src/assets.d.ts +207 -0
- package/dist/src/assets.js +533 -0
- package/dist/src/auth-api.d.ts +138 -0
- package/dist/src/auth-api.js +168 -0
- package/dist/src/auth.d.ts +536 -119
- package/dist/src/auth.js +1207 -152
- package/dist/src/compute.d.ts +221 -9
- package/dist/src/compute.js +293 -16
- package/dist/src/data-schema-contacts-v1.d.ts +14 -0
- package/dist/src/data-schema-contacts-v1.js +28 -0
- package/dist/src/data.d.ts +153 -0
- package/dist/src/data.js +670 -0
- package/dist/src/endpoints.d.ts +9 -0
- package/dist/src/endpoints.js +5 -0
- package/dist/src/ethos.d.ts +202 -1
- package/dist/src/ethos.js +821 -16
- package/dist/src/index.d.ts +16 -6
- package/dist/src/index.js +33 -6
- package/dist/src/internal/delegate-bundle.d.ts +18 -0
- package/dist/src/internal/delegate-bundle.js +94 -0
- package/dist/src/internal/delegate-state.d.ts +45 -0
- package/dist/src/internal/delegate-state.js +120 -0
- package/dist/src/internal/envelope.d.ts +77 -0
- package/dist/src/internal/envelope.js +154 -0
- package/dist/src/internal/owner-signers.d.ts +78 -0
- package/dist/src/internal/owner-signers.js +179 -0
- package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
- package/dist/src/internal/protocol-client-bridge.js +20 -0
- package/dist/src/internal/recovery-file.d.ts +29 -0
- package/dist/src/internal/recovery-file.js +98 -0
- package/dist/src/internal/signer.d.ts +59 -0
- package/dist/src/internal/signer.js +86 -0
- package/dist/src/key-store.d.ts +128 -0
- package/dist/src/key-store.js +244 -0
- package/dist/src/mandates.d.ts +163 -1
- package/dist/src/mandates.js +286 -8
- package/dist/src/react/AithosAsset.d.ts +66 -0
- package/dist/src/react/AithosAsset.js +67 -0
- package/dist/src/react/context.d.ts +29 -0
- package/dist/src/react/context.js +31 -0
- package/dist/src/react/index.d.ts +28 -0
- package/dist/src/react/index.js +30 -0
- package/dist/src/react/use-aithos-asset.d.ts +39 -0
- package/dist/src/react/use-aithos-asset.js +118 -0
- package/dist/src/sdk.d.ts +39 -3
- package/dist/src/sdk.js +36 -23
- package/dist/src/wallet.d.ts +4 -6
- package/dist/src/wallet.js +18 -8
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/package.json +18 -3
- package/dist/test/auth.test.d.ts +0 -2
- package/dist/test/auth.test.js +0 -175
- package/dist/test/compute.test.d.ts +0 -2
- package/dist/test/compute.test.js +0 -179
- package/dist/test/endpoints.test.d.ts +0 -2
- package/dist/test/endpoints.test.js +0 -43
- package/dist/test/sdk.test.d.ts +0 -2
- package/dist/test/sdk.test.js +0 -86
- package/dist/test/wallet.test.d.ts +0 -2
- package/dist/test/wallet.test.js +0 -110
|
@@ -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
|
package/dist/src/auth-api.d.ts
CHANGED
|
@@ -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
|