@aithos/sdk 0.1.0-alpha.6 → 0.1.0-alpha.60
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 +202 -7
- package/dist/src/agent-dispatch.d.ts +18 -0
- package/dist/src/agent-dispatch.js +178 -0
- package/dist/src/agent-loop.d.ts +94 -0
- package/dist/src/agent-loop.js +95 -0
- package/dist/src/agent-tools.d.ts +24 -0
- package/dist/src/agent-tools.js +147 -0
- package/dist/src/apps.d.ts +224 -0
- package/dist/src/apps.js +432 -0
- package/dist/src/assets.d.ts +225 -0
- package/dist/src/assets.js +534 -0
- package/dist/src/auth-api.d.ts +219 -0
- package/dist/src/auth-api.js +248 -0
- package/dist/src/auth.d.ts +591 -0
- package/dist/src/auth.js +947 -31
- package/dist/src/compute.d.ts +674 -6
- package/dist/src/compute.js +968 -20
- 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 +368 -0
- package/dist/src/data.js +1124 -0
- package/dist/src/endpoints.d.ts +43 -0
- package/dist/src/endpoints.js +23 -0
- package/dist/src/ethos.d.ts +85 -0
- package/dist/src/ethos.js +463 -7
- package/dist/src/index.d.ts +22 -4
- package/dist/src/index.js +47 -2
- package/dist/src/internal/cmk-wrap.d.ts +41 -0
- package/dist/src/internal/cmk-wrap.js +132 -0
- package/dist/src/internal/delegate-bundle.js +7 -2
- package/dist/src/internal/envelope.d.ts +93 -0
- package/dist/src/internal/envelope.js +59 -0
- package/dist/src/internal/owner-signers.d.ts +5 -2
- package/dist/src/internal/owner-signers.js +22 -1
- package/dist/src/internal/recovery-file.d.ts +2 -0
- package/dist/src/internal/recovery-file.js +7 -0
- package/dist/src/key-store.d.ts +10 -0
- package/dist/src/key-store.js +6 -0
- package/dist/src/mandates.d.ts +58 -1
- package/dist/src/mandates.js +46 -3
- package/dist/src/migrate.d.ts +105 -0
- package/dist/src/migrate.js +367 -0
- 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 +29 -0
- package/dist/src/react/index.js +31 -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/react/use-transcribe-pending.d.ts +21 -0
- package/dist/src/react/use-transcribe-pending.js +47 -0
- package/dist/src/rotate.d.ts +94 -0
- package/dist/src/rotate.js +298 -0
- package/dist/src/sdk.d.ts +36 -2
- package/dist/src/sdk.js +72 -1
- package/dist/src/transcribe-resilience.d.ts +57 -0
- package/dist/src/transcribe-resilience.js +203 -0
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/dist/test/agent-dispatch.test.d.ts +2 -0
- package/dist/test/agent-dispatch.test.js +222 -0
- package/dist/test/agent-loop.test.d.ts +2 -0
- package/dist/test/agent-loop.test.js +117 -0
- package/dist/test/agent-tools.test.d.ts +2 -0
- package/dist/test/agent-tools.test.js +50 -0
- package/dist/test/auth-j3.test.js +32 -1
- package/dist/test/canonical-conformance.test.d.ts +2 -0
- package/dist/test/canonical-conformance.test.js +86 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +4 -0
- package/dist/test/converse.test.d.ts +2 -0
- package/dist/test/converse.test.js +162 -0
- package/dist/test/data-sphere.test.d.ts +2 -0
- package/dist/test/data-sphere.test.js +57 -0
- package/dist/test/endpoints.test.js +40 -1
- package/dist/test/envelope-core-conformance.test.d.ts +2 -0
- package/dist/test/envelope-core-conformance.test.js +75 -0
- package/dist/test/envelope.test.d.ts +2 -0
- package/dist/test/envelope.test.js +318 -0
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +371 -0
- package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
- package/dist/test/invoke-turn-sdk.test.js +177 -0
- package/dist/test/migrate.test.d.ts +2 -0
- package/dist/test/migrate.test.js +340 -0
- package/dist/test/owner-data-client.test.d.ts +2 -0
- package/dist/test/owner-data-client.test.js +88 -0
- package/dist/test/rotate-ethos.test.d.ts +2 -0
- package/dist/test/rotate-ethos.test.js +151 -0
- package/dist/test/rotate.test.d.ts +2 -0
- package/dist/test/rotate.test.js +63 -0
- package/dist/test/schema-autoresolve.test.d.ts +2 -0
- package/dist/test/schema-autoresolve.test.js +146 -0
- package/dist/test/sdk.test.js +11 -2
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +311 -0
- package/dist/test/transcribe-invoke.test.d.ts +2 -0
- package/dist/test/transcribe-invoke.test.js +204 -0
- package/dist/test/transcribe.test.d.ts +2 -0
- package/dist/test/transcribe.test.js +186 -0
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +20 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type ParsedRecoveryFile } from "./internal/recovery-file.js";
|
|
2
|
+
import { type CmkEnvelope } from "./internal/cmk-wrap.js";
|
|
3
|
+
export interface MigrateOptions {
|
|
4
|
+
/** api.aithos.be base URL. Defaults to {@link DEFAULT_API_BASE_URL}. */
|
|
5
|
+
readonly apiBaseUrl?: string;
|
|
6
|
+
/** PDS base URL. Defaults to `https://pds.aithos.be`. Pass the raw
|
|
7
|
+
* execute-api URL when the vanity host isn't live in your environment. */
|
|
8
|
+
readonly pdsUrl?: string;
|
|
9
|
+
/** Custom fetch (tests / SSR). Defaults to globalThis.fetch. */
|
|
10
|
+
readonly fetch?: typeof fetch;
|
|
11
|
+
/** When true, compute and report planned rekeys but DO NOT write. */
|
|
12
|
+
readonly dryRun?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* How to onboard a collection onto #data:
|
|
15
|
+
* - "replace" (default): swap the owner wrap from the legacy sphere to
|
|
16
|
+
* #data. Use for a clean cutover when NOTHING still reads the collection
|
|
17
|
+
* under the old key. WARNING: breaks any app still reading under the
|
|
18
|
+
* legacy key (e.g. linkedone reading under #root).
|
|
19
|
+
* - "add": KEEP the legacy owner wrap and ADD a second owner wrap sealed to
|
|
20
|
+
* #data (dual-read). Both the legacy app and a #data client can read the
|
|
21
|
+
* same data. Requires @aithos/sdk with the multi-wrap owner reader
|
|
22
|
+
* (this release). Safe, non-destructive — recommended for live accounts.
|
|
23
|
+
*/
|
|
24
|
+
readonly mode?: "replace" | "add";
|
|
25
|
+
/**
|
|
26
|
+
* Re-key collections toward a DIFFERENT `#data` key than the one in the
|
|
27
|
+
* recovery file. Used by `rotateEthos`: unwrap with the recovery's (old)
|
|
28
|
+
* sphere seeds, wrap toward this NEW `#data` seed. When omitted, the target
|
|
29
|
+
* is the recovery file's own `#data` seed (the normal migration case).
|
|
30
|
+
*/
|
|
31
|
+
readonly targetDataSeedHex?: string;
|
|
32
|
+
/** Progress callback for CLIs / UIs. */
|
|
33
|
+
readonly onProgress?: (e: MigrateProgress) => void;
|
|
34
|
+
}
|
|
35
|
+
export type MigrateProgress = {
|
|
36
|
+
phase: "ensure-data-sphere";
|
|
37
|
+
status: "start" | "added" | "already-present";
|
|
38
|
+
} | {
|
|
39
|
+
phase: "rekey";
|
|
40
|
+
status: "collection";
|
|
41
|
+
collection: string;
|
|
42
|
+
result: CollectionRekeyStatus;
|
|
43
|
+
};
|
|
44
|
+
export interface EnsureDataSphereResult {
|
|
45
|
+
readonly did: string;
|
|
46
|
+
/** True when this call added #data (false = already present server-side). */
|
|
47
|
+
readonly added: boolean;
|
|
48
|
+
/** The #data sphere seed (hex). The caller MUST persist this — it's the new
|
|
49
|
+
* key the account signs/reads data under. Returned even when `added` is
|
|
50
|
+
* false (it's the same seed that's now published). */
|
|
51
|
+
readonly dataSeedHex: string;
|
|
52
|
+
/** Convenience: the recovery file serialized WITH the #data seed merged in. */
|
|
53
|
+
readonly updatedRecoveryFile: string;
|
|
54
|
+
}
|
|
55
|
+
export type CollectionRekeyStatus = "rekeyed" | "data-wrap-added" | "already-data" | "planned" | "skipped-no-owner-wrap" | "unwrap-failed";
|
|
56
|
+
export interface CollectionRekeyEntry {
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly urn: string;
|
|
59
|
+
readonly status: CollectionRekeyStatus;
|
|
60
|
+
/** Which seed successfully unwrapped the CMK (auto-discovery result). */
|
|
61
|
+
readonly unwrappedWith?: SphereName;
|
|
62
|
+
/** Count of non-owner (delegate) wraps preserved into the new envelope. */
|
|
63
|
+
readonly delegateWrapsPreserved?: number;
|
|
64
|
+
/** Pre-mutation cmk_envelope, kept so a human can roll back if needed. */
|
|
65
|
+
readonly backupEnvelope?: CmkEnvelope;
|
|
66
|
+
}
|
|
67
|
+
export interface RekeyReport {
|
|
68
|
+
readonly did: string;
|
|
69
|
+
readonly dryRun: boolean;
|
|
70
|
+
readonly collections: readonly CollectionRekeyEntry[];
|
|
71
|
+
}
|
|
72
|
+
export interface FullMigrationResult {
|
|
73
|
+
readonly ensure: EnsureDataSphereResult;
|
|
74
|
+
readonly rekey: RekeyReport;
|
|
75
|
+
/** Recovery file with the #data seed — persist this for the user. */
|
|
76
|
+
readonly updatedRecoveryFile: string;
|
|
77
|
+
}
|
|
78
|
+
type SphereName = "data" | "data-old" | "self" | "circle" | "public" | "root";
|
|
79
|
+
/**
|
|
80
|
+
* Idempotently add the `#data` sphere to a (possibly legacy) identity and
|
|
81
|
+
* re-publish its did.json. Accepts either a parsed recovery file or the raw
|
|
82
|
+
* recovery JSON text.
|
|
83
|
+
*/
|
|
84
|
+
export declare function ensureDataSphere(owner: ParsedRecoveryFile | string, opts?: MigrateOptions): Promise<EnsureDataSphereResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Re-wrap every collection's CMK from its legacy sphere to the owner's `#data`
|
|
87
|
+
* key. Idempotent: collections already readable under `#data` are skipped.
|
|
88
|
+
* Requires that the recovery file carries a `#data` seed (run
|
|
89
|
+
* {@link ensureDataSphere} first, or pass {@link FullMigrationResult}).
|
|
90
|
+
*/
|
|
91
|
+
export declare function rekeyLegacyCollections(owner: ParsedRecoveryFile | string, opts?: MigrateOptions): Promise<RekeyReport>;
|
|
92
|
+
/**
|
|
93
|
+
* Dual-read onboarding: KEEP each collection's legacy owner wrap and ADD a
|
|
94
|
+
* second owner wrap sealed to #data, so the existing app (reading under the
|
|
95
|
+
* legacy key) AND a #data client both decrypt the same data. Non-destructive.
|
|
96
|
+
* Thin wrapper over {@link rekeyLegacyCollections} with `mode: "add"`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function addDataSphereWrap(owner: ParsedRecoveryFile | string, opts?: MigrateOptions): Promise<RekeyReport>;
|
|
99
|
+
/**
|
|
100
|
+
* Run the full migration: ensureDataSphere then rekeyLegacyCollections.
|
|
101
|
+
* Returns the updated recovery file (with the #data seed) — persist it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function migrateLegacyEthosToDataSphere(recoveryText: string, opts?: MigrateOptions): Promise<FullMigrationResult>;
|
|
104
|
+
export {};
|
|
105
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* Legacy `#data` sphere migration.
|
|
5
|
+
*
|
|
6
|
+
* Identities created before the dedicated `#data` sphere landed
|
|
7
|
+
* (spec/data/02-key-hierarchy.md) have only root/public/circle/self keys and
|
|
8
|
+
* therefore:
|
|
9
|
+
* 1. their published did.json has no `#data` / `#data-kex` entry, and
|
|
10
|
+
* 2. their existing data collections' CMK is wrapped to the X25519 key
|
|
11
|
+
* derived from whatever sphere seed the *old* app passed to
|
|
12
|
+
* `createDataClient({ sphereSeed })` (in practice `#self`/`#public`), even
|
|
13
|
+
* though the wrap is LABELLED `${did}#data-kex` (the label is hard-coded
|
|
14
|
+
* in the SDK regardless of the seed).
|
|
15
|
+
*
|
|
16
|
+
* This module performs a two-step, idempotent, owner-only migration so the
|
|
17
|
+
* account converges on the protocol-intended `#data` convention:
|
|
18
|
+
*
|
|
19
|
+
* A. {@link ensureDataSphere} — generate a `#data` keypair (if absent),
|
|
20
|
+
* re-publish the did.json with the appended `#data` + `#data-kex` entries
|
|
21
|
+
* (root-signed) via the additive `aithos.augment_identity` primitive.
|
|
22
|
+
*
|
|
23
|
+
* B. {@link rekeyLegacyCollections} — for each collection, unwrap the CMK
|
|
24
|
+
* with the (auto-discovered) legacy sphere seed, re-wrap the SAME CMK to
|
|
25
|
+
* the `#data` X25519 key (label unchanged: `${did}#data-kex`), and persist
|
|
26
|
+
* via the existing `aithos.data.rotate_cmk` primitive. The CMK value is
|
|
27
|
+
* unchanged, so per-record DEKs stay valid — no record rewrap. Delegate
|
|
28
|
+
* wraps are preserved.
|
|
29
|
+
*
|
|
30
|
+
* Guardrails: dry-run mode, a pre-mutation backup of every `cmk_envelope`, a
|
|
31
|
+
* LOCAL round-trip verification (the freshly built `#data` wrap is unwrapped
|
|
32
|
+
* with the data seed and checked byte-equal to the CMK) BEFORE any server
|
|
33
|
+
* write, and idempotence (a collection already readable under `#data` is left
|
|
34
|
+
* untouched).
|
|
35
|
+
*
|
|
36
|
+
* Nothing here mutates any existing handler or primitive: A uses the new
|
|
37
|
+
* additive `aithos.augment_identity`, B reuses the existing `rotate_cmk`.
|
|
38
|
+
*/
|
|
39
|
+
import * as ed from "@noble/ed25519";
|
|
40
|
+
import { sha512 } from "@noble/hashes/sha2.js";
|
|
41
|
+
import { browserIdentityFromStored, signedDidDocument, } from "@aithos/protocol-client";
|
|
42
|
+
import { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
|
|
43
|
+
import { DEFAULT_API_BASE_URL } from "./auth.js";
|
|
44
|
+
import { signOwnerEnvelope } from "./internal/envelope.js";
|
|
45
|
+
import { parseRecoveryFile, serializeRecoveryFile, } from "./internal/recovery-file.js";
|
|
46
|
+
import { ed25519SeedToX25519PrivateKey, ed25519SeedToX25519PublicKey, tryUnwrapCmk, wrapCmkForRecipient, } from "./internal/cmk-wrap.js";
|
|
47
|
+
// noble/ed25519 v2 needs a sync sha512 for sync sign.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
ed.etc.sha512Sync = (...m) =>
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
sha512(ed.etc.concatBytes(...m));
|
|
52
|
+
/* -------------------------------------------------------------------------- */
|
|
53
|
+
/* A — ensureDataSphere */
|
|
54
|
+
/* -------------------------------------------------------------------------- */
|
|
55
|
+
/**
|
|
56
|
+
* Idempotently add the `#data` sphere to a (possibly legacy) identity and
|
|
57
|
+
* re-publish its did.json. Accepts either a parsed recovery file or the raw
|
|
58
|
+
* recovery JSON text.
|
|
59
|
+
*/
|
|
60
|
+
export async function ensureDataSphere(owner, opts = {}) {
|
|
61
|
+
const rec = typeof owner === "string" ? parseRecoveryFile(owner) : owner;
|
|
62
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
63
|
+
const apiBaseUrl = (opts.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
64
|
+
opts.onProgress?.({ phase: "ensure-data-sphere", status: "start" });
|
|
65
|
+
// Generate a #data seed if the recovery file doesn't carry one yet.
|
|
66
|
+
const hadData = typeof rec.seedsHex.data === "string";
|
|
67
|
+
const dataSeedHex = hadData ? rec.seedsHex.data : bytesToHex(randomSeed32());
|
|
68
|
+
// Rebuild the full identity (incl. #data) and produce a root-signed did.json
|
|
69
|
+
// that carries the appended #data + #data-kex entries.
|
|
70
|
+
const identity = browserIdentityFromStored({
|
|
71
|
+
handle: rec.handle,
|
|
72
|
+
displayName: rec.displayName,
|
|
73
|
+
did: rec.did,
|
|
74
|
+
seeds: {
|
|
75
|
+
root: rec.seedsHex.root,
|
|
76
|
+
public: rec.seedsHex.public,
|
|
77
|
+
circle: rec.seedsHex.circle,
|
|
78
|
+
self: rec.seedsHex.self,
|
|
79
|
+
data: dataSeedHex,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const signedDoc = signedDidDocument(identity);
|
|
83
|
+
// Publish via the additive augment_identity primitive (idempotent server-side).
|
|
84
|
+
const rootSeed = hexToBytes(rec.seedsHex.root);
|
|
85
|
+
const url = `${apiBaseUrl}/mcp/primitives/write`;
|
|
86
|
+
const params = {
|
|
87
|
+
did_document: signedDoc,
|
|
88
|
+
handle: rec.handle,
|
|
89
|
+
display_name: rec.displayName,
|
|
90
|
+
};
|
|
91
|
+
const res = await jsonRpc(fetchImpl, url, "aithos.augment_identity", params, {
|
|
92
|
+
iss: rec.did,
|
|
93
|
+
aud: url,
|
|
94
|
+
verificationMethod: `${rec.did}#root`,
|
|
95
|
+
signSeed: rootSeed,
|
|
96
|
+
});
|
|
97
|
+
const added = res.data_sphere_added ?? !hadData;
|
|
98
|
+
// Merge the #data seed into a recovery file the caller can persist.
|
|
99
|
+
const updatedRecoveryFile = serializeRecoveryWithData(rec, dataSeedHex);
|
|
100
|
+
opts.onProgress?.({
|
|
101
|
+
phase: "ensure-data-sphere",
|
|
102
|
+
status: added ? "added" : "already-present",
|
|
103
|
+
});
|
|
104
|
+
return { did: rec.did, added, dataSeedHex, updatedRecoveryFile };
|
|
105
|
+
}
|
|
106
|
+
/* -------------------------------------------------------------------------- */
|
|
107
|
+
/* B — rekeyLegacyCollections */
|
|
108
|
+
/* -------------------------------------------------------------------------- */
|
|
109
|
+
/**
|
|
110
|
+
* Re-wrap every collection's CMK from its legacy sphere to the owner's `#data`
|
|
111
|
+
* key. Idempotent: collections already readable under `#data` are skipped.
|
|
112
|
+
* Requires that the recovery file carries a `#data` seed (run
|
|
113
|
+
* {@link ensureDataSphere} first, or pass {@link FullMigrationResult}).
|
|
114
|
+
*/
|
|
115
|
+
export async function rekeyLegacyCollections(owner, opts = {}) {
|
|
116
|
+
const rec = typeof owner === "string" ? parseRecoveryFile(owner) : owner;
|
|
117
|
+
if (!rec.seedsHex.data && !opts.targetDataSeedHex) {
|
|
118
|
+
throw new Error("rekeyLegacyCollections: recovery file has no #data seed. Run ensureDataSphere first and persist its updatedRecoveryFile (or pass targetDataSeedHex for a rotation).");
|
|
119
|
+
}
|
|
120
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
121
|
+
const pdsUrl = (opts.pdsUrl ?? DEFAULT_SDK_ENDPOINTS.pds).replace(/\/$/, "");
|
|
122
|
+
const dryRun = opts.dryRun ?? false;
|
|
123
|
+
const mode = opts.mode ?? "replace";
|
|
124
|
+
const did = rec.did;
|
|
125
|
+
const rootSeed = hexToBytes(rec.seedsHex.root);
|
|
126
|
+
const dataKexRecipient = `${did}#data-kex`;
|
|
127
|
+
// Target #data key the wraps will be sealed TO. Defaults to the recovery's
|
|
128
|
+
// own #data; rotateEthos passes a fresh one (rotation).
|
|
129
|
+
const targetDataSeed = opts.targetDataSeedHex
|
|
130
|
+
? hexToBytes(opts.targetDataSeedHex)
|
|
131
|
+
: hexToBytes(rec.seedsHex.data); // guard above guarantees one of the two
|
|
132
|
+
const dataPub = ed25519SeedToX25519PublicKey(targetDataSeed);
|
|
133
|
+
const dataPriv = ed25519SeedToX25519PrivateKey(targetDataSeed);
|
|
134
|
+
// Candidate seeds to UNWRAP an existing owner wrap with (auto-discovery). The
|
|
135
|
+
// target #data key is NOT in this list — "already sealed to target" is
|
|
136
|
+
// detected separately via `dataPriv`. When rotating (target ≠ recovery
|
|
137
|
+
// #data), the recovery's OLD #data is a valid unwrap key, included as
|
|
138
|
+
// "data-old".
|
|
139
|
+
const candidates = [
|
|
140
|
+
{ name: "self", priv: ed25519SeedToX25519PrivateKey(hexToBytes(rec.seedsHex.self)) },
|
|
141
|
+
{ name: "circle", priv: ed25519SeedToX25519PrivateKey(hexToBytes(rec.seedsHex.circle)) },
|
|
142
|
+
{ name: "public", priv: ed25519SeedToX25519PrivateKey(hexToBytes(rec.seedsHex.public)) },
|
|
143
|
+
{ name: "root", priv: ed25519SeedToX25519PrivateKey(rootSeed) },
|
|
144
|
+
...(opts.targetDataSeedHex && rec.seedsHex.data
|
|
145
|
+
? [{ name: "data-old", priv: ed25519SeedToX25519PrivateKey(hexToBytes(rec.seedsHex.data)) }]
|
|
146
|
+
: []),
|
|
147
|
+
];
|
|
148
|
+
const readUrl = `${pdsUrl}/mcp/primitives/read`;
|
|
149
|
+
const writeUrl = `${pdsUrl}/mcp/primitives/write`;
|
|
150
|
+
const signRoot = {
|
|
151
|
+
iss: did,
|
|
152
|
+
verificationMethod: `${did}#root`,
|
|
153
|
+
signSeed: rootSeed,
|
|
154
|
+
};
|
|
155
|
+
// 1. Enumerate collections.
|
|
156
|
+
const listed = (await jsonRpc(fetchImpl, readUrl, "aithos.data.list_collections", { subject_did: did }, { ...signRoot, aud: readUrl }));
|
|
157
|
+
const names = (listed.items ?? []).map((c) => c.name);
|
|
158
|
+
const collections = [];
|
|
159
|
+
for (const name of names) {
|
|
160
|
+
const entry = await rekeyOne({
|
|
161
|
+
fetchImpl,
|
|
162
|
+
readUrl,
|
|
163
|
+
writeUrl,
|
|
164
|
+
signRoot,
|
|
165
|
+
did,
|
|
166
|
+
name,
|
|
167
|
+
candidates,
|
|
168
|
+
dataPub,
|
|
169
|
+
dataPriv,
|
|
170
|
+
dataKexRecipient,
|
|
171
|
+
dryRun,
|
|
172
|
+
mode,
|
|
173
|
+
});
|
|
174
|
+
collections.push(entry);
|
|
175
|
+
opts.onProgress?.({ phase: "rekey", status: "collection", collection: name, result: entry.status });
|
|
176
|
+
}
|
|
177
|
+
return { did, dryRun, collections };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Dual-read onboarding: KEEP each collection's legacy owner wrap and ADD a
|
|
181
|
+
* second owner wrap sealed to #data, so the existing app (reading under the
|
|
182
|
+
* legacy key) AND a #data client both decrypt the same data. Non-destructive.
|
|
183
|
+
* Thin wrapper over {@link rekeyLegacyCollections} with `mode: "add"`.
|
|
184
|
+
*/
|
|
185
|
+
export async function addDataSphereWrap(owner, opts = {}) {
|
|
186
|
+
return rekeyLegacyCollections(owner, { ...opts, mode: "add" });
|
|
187
|
+
}
|
|
188
|
+
async function rekeyOne(a) {
|
|
189
|
+
const meta = (await jsonRpc(a.fetchImpl, a.readUrl, "aithos.data.get_collection", { subject_did: a.did, collection_name: a.name }, { ...a.signRoot, aud: a.readUrl }));
|
|
190
|
+
const urn = meta.urn ?? `urn:aithos:collection:${a.did}:${a.name}`;
|
|
191
|
+
const env = meta.cmk_envelope;
|
|
192
|
+
if (!env || !Array.isArray(env.wraps)) {
|
|
193
|
+
return { name: a.name, urn, status: "skipped-no-owner-wrap" };
|
|
194
|
+
}
|
|
195
|
+
// Owner wraps carry the label ${did}#data-kex (delegate wraps use a
|
|
196
|
+
// did:key:...#data-kex recipient). After a dual-read "add" there can be more
|
|
197
|
+
// than one owner wrap (legacy-sealed + #data-sealed).
|
|
198
|
+
const ownerWraps = env.wraps.filter((w) => w.recipient === a.dataKexRecipient);
|
|
199
|
+
if (ownerWraps.length === 0) {
|
|
200
|
+
return { name: a.name, urn, status: "skipped-no-owner-wrap", backupEnvelope: env };
|
|
201
|
+
}
|
|
202
|
+
const dataPriv = a.dataPriv; // private key of the TARGET #data
|
|
203
|
+
// Idempotence: is any owner wrap ALREADY readable under the target #data?
|
|
204
|
+
const alreadyData = ownerWraps.some((w) => tryUnwrapCmk({ wrap: w, collectionUrn: urn, privateKey: dataPriv }) !== null);
|
|
205
|
+
if (alreadyData) {
|
|
206
|
+
// add mode: a #data reader already exists → nothing to do.
|
|
207
|
+
// replace mode: if the SOLE owner wrap is #data we're done; if a legacy
|
|
208
|
+
// wrap also lingers we still consider it "already-data" (a #data reader
|
|
209
|
+
// exists) and leave it — replace's job is to *enable* #data, already true.
|
|
210
|
+
return { name: a.name, urn, status: "already-data", unwrappedWith: "data" };
|
|
211
|
+
}
|
|
212
|
+
// Auto-discover the legacy seed that sealed an owner wrap (try each wrap with
|
|
213
|
+
// each non-#data candidate). #data already excluded by the check above.
|
|
214
|
+
let cmk = null;
|
|
215
|
+
let usedSeed;
|
|
216
|
+
outer: for (const w of ownerWraps) {
|
|
217
|
+
for (const cand of a.candidates) {
|
|
218
|
+
const out = tryUnwrapCmk({ wrap: w, collectionUrn: urn, privateKey: cand.priv });
|
|
219
|
+
if (out) {
|
|
220
|
+
cmk = out;
|
|
221
|
+
usedSeed = cand.name;
|
|
222
|
+
break outer;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!cmk || !usedSeed) {
|
|
227
|
+
// Sealed to a key we don't hold (app-derived / foreign) — do NOT touch it.
|
|
228
|
+
return { name: a.name, urn, status: "unwrap-failed", backupEnvelope: env };
|
|
229
|
+
}
|
|
230
|
+
// Build the #data wrap of the SAME CMK (recipient label unchanged).
|
|
231
|
+
const dataWrap = wrapCmkForRecipient({
|
|
232
|
+
cmk,
|
|
233
|
+
recipientPublicKey: a.dataPub,
|
|
234
|
+
recipientDidUrl: a.dataKexRecipient,
|
|
235
|
+
collectionUrn: urn,
|
|
236
|
+
});
|
|
237
|
+
// GUARDRAIL: verify the new #data wrap round-trips to EXACTLY this CMK before
|
|
238
|
+
// any server write.
|
|
239
|
+
const verify = tryUnwrapCmk({ wrap: dataWrap, collectionUrn: urn, privateKey: dataPriv });
|
|
240
|
+
if (!verify || !bytesEqual(verify, cmk)) {
|
|
241
|
+
return { name: a.name, urn, status: "unwrap-failed", backupEnvelope: env };
|
|
242
|
+
}
|
|
243
|
+
let newEnvelope;
|
|
244
|
+
let delegateWrapsPreserved;
|
|
245
|
+
if (a.mode === "add") {
|
|
246
|
+
// Keep EVERYTHING (legacy owner wrap + delegate wraps) and append #data.
|
|
247
|
+
newEnvelope = { alg: env.alg, wraps: [...env.wraps, dataWrap] };
|
|
248
|
+
delegateWrapsPreserved = env.wraps.filter((w) => w.recipient !== a.dataKexRecipient).length;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// replace: drop the legacy owner wrap(s), keep delegates, set #data as the
|
|
252
|
+
// sole owner wrap.
|
|
253
|
+
const delegateWraps = env.wraps.filter((w) => w.recipient !== a.dataKexRecipient);
|
|
254
|
+
newEnvelope = { alg: env.alg, wraps: [dataWrap, ...delegateWraps] };
|
|
255
|
+
delegateWrapsPreserved = delegateWraps.length;
|
|
256
|
+
}
|
|
257
|
+
if (a.dryRun) {
|
|
258
|
+
return { name: a.name, urn, status: "planned", unwrappedWith: usedSeed, delegateWrapsPreserved, backupEnvelope: env };
|
|
259
|
+
}
|
|
260
|
+
await jsonRpc(a.fetchImpl, a.writeUrl, "aithos.data.rotate_cmk", { collection_urn: urn, new_cmk_envelope: newEnvelope }, { ...a.signRoot, aud: a.writeUrl });
|
|
261
|
+
return {
|
|
262
|
+
name: a.name,
|
|
263
|
+
urn,
|
|
264
|
+
status: a.mode === "add" ? "data-wrap-added" : "rekeyed",
|
|
265
|
+
unwrappedWith: usedSeed,
|
|
266
|
+
delegateWrapsPreserved,
|
|
267
|
+
backupEnvelope: env,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/* -------------------------------------------------------------------------- */
|
|
271
|
+
/* Combined helper */
|
|
272
|
+
/* -------------------------------------------------------------------------- */
|
|
273
|
+
/**
|
|
274
|
+
* Run the full migration: ensureDataSphere then rekeyLegacyCollections.
|
|
275
|
+
* Returns the updated recovery file (with the #data seed) — persist it.
|
|
276
|
+
*/
|
|
277
|
+
export async function migrateLegacyEthosToDataSphere(recoveryText, opts = {}) {
|
|
278
|
+
const ensure = await ensureDataSphere(recoveryText, opts);
|
|
279
|
+
// Re-parse with the freshly merged #data seed so rekey has it.
|
|
280
|
+
const recWithData = parseRecoveryFile(ensure.updatedRecoveryFile);
|
|
281
|
+
const rekey = await rekeyLegacyCollections(recWithData, opts);
|
|
282
|
+
return { ensure, rekey, updatedRecoveryFile: ensure.updatedRecoveryFile };
|
|
283
|
+
}
|
|
284
|
+
async function jsonRpc(fetchImpl, url, method, params, ctx) {
|
|
285
|
+
const envelope = await signOwnerEnvelope({
|
|
286
|
+
iss: ctx.iss,
|
|
287
|
+
aud: ctx.aud,
|
|
288
|
+
method,
|
|
289
|
+
params,
|
|
290
|
+
verificationMethod: ctx.verificationMethod,
|
|
291
|
+
signer: { sign: async (msg) => ed.sign(msg, ctx.signSeed) },
|
|
292
|
+
});
|
|
293
|
+
const body = {
|
|
294
|
+
jsonrpc: "2.0",
|
|
295
|
+
id: `migrate_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
296
|
+
method,
|
|
297
|
+
params: { ...params, _envelope: envelope },
|
|
298
|
+
};
|
|
299
|
+
const r = await fetchImpl(url, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: { "content-type": "application/json" },
|
|
302
|
+
body: JSON.stringify(body),
|
|
303
|
+
});
|
|
304
|
+
const json = (await r.json());
|
|
305
|
+
if (json.error) {
|
|
306
|
+
const err = new Error(`${method} failed: ${json.error.message}`);
|
|
307
|
+
err.code = json.error.code;
|
|
308
|
+
err.data = json.error.data;
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
return json.result ?? {};
|
|
312
|
+
}
|
|
313
|
+
/* -------------------------------------------------------------------------- */
|
|
314
|
+
/* Small utils */
|
|
315
|
+
/* -------------------------------------------------------------------------- */
|
|
316
|
+
function randomSeed32() {
|
|
317
|
+
const buf = new Uint8Array(32);
|
|
318
|
+
globalThis.crypto?.getRandomValues(buf);
|
|
319
|
+
return buf;
|
|
320
|
+
}
|
|
321
|
+
function hexToBytes(hex) {
|
|
322
|
+
if (hex.length % 2 !== 0)
|
|
323
|
+
throw new Error("hex must be even-length");
|
|
324
|
+
const out = new Uint8Array(hex.length / 2);
|
|
325
|
+
for (let i = 0; i < out.length; i++)
|
|
326
|
+
out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
function bytesToHex(b) {
|
|
330
|
+
let out = "";
|
|
331
|
+
for (let i = 0; i < b.length; i++)
|
|
332
|
+
out += b[i].toString(16).padStart(2, "0");
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
function bytesEqual(a, b) {
|
|
336
|
+
if (a.length !== b.length)
|
|
337
|
+
return false;
|
|
338
|
+
let diff = 0;
|
|
339
|
+
for (let i = 0; i < a.length; i++)
|
|
340
|
+
diff |= a[i] ^ b[i];
|
|
341
|
+
return diff === 0;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Serialize a recovery file (SDK shape) with the #data seed merged in. We don't
|
|
345
|
+
* have a BrowserIdentity here, so we build the canonical SDK shape directly —
|
|
346
|
+
* matching `serializeRecoveryFile`'s output shape (used elsewhere for the
|
|
347
|
+
* round-trip parse test).
|
|
348
|
+
*/
|
|
349
|
+
function serializeRecoveryWithData(rec, dataSeedHex) {
|
|
350
|
+
void serializeRecoveryFile; // referenced for shape parity; we emit the same fields
|
|
351
|
+
const payload = {
|
|
352
|
+
aithos_recovery_version: "0.1.0-plaintext",
|
|
353
|
+
handle: rec.handle,
|
|
354
|
+
display_name: rec.displayName,
|
|
355
|
+
did: rec.did,
|
|
356
|
+
seeds_hex: {
|
|
357
|
+
root: rec.seedsHex.root,
|
|
358
|
+
public: rec.seedsHex.public,
|
|
359
|
+
circle: rec.seedsHex.circle,
|
|
360
|
+
self: rec.seedsHex.self,
|
|
361
|
+
data: dataSeedHex,
|
|
362
|
+
},
|
|
363
|
+
saved_at: new Date().toISOString(),
|
|
364
|
+
};
|
|
365
|
+
return JSON.stringify(payload, null, 2);
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<AithosAsset>` — drop-in component for displaying Aithos-hosted
|
|
3
|
+
* binary content with the simplest possible API:
|
|
4
|
+
*
|
|
5
|
+
* <AithosAsset urn={cv.urn} alt="CV" className="w-full" />
|
|
6
|
+
* <AithosAsset urn={video.urn} as="video" controls />
|
|
7
|
+
* <AithosAsset urn={song.urn} as="audio" controls />
|
|
8
|
+
* <AithosAsset urn={cv.urn} as="download" filename="cv.pdf">
|
|
9
|
+
* Download my CV (PDF)
|
|
10
|
+
* </AithosAsset>
|
|
11
|
+
*
|
|
12
|
+
* The component handles the full lifecycle:
|
|
13
|
+
* - Fetch + decrypt via the assets client (from context or `client` prop).
|
|
14
|
+
* - Show `fallback` while loading.
|
|
15
|
+
* - Show `errorFallback` (or alt text in img-mode) on failure.
|
|
16
|
+
* - Revoke the `blob:` URL on unmount or URN change.
|
|
17
|
+
*
|
|
18
|
+
* For public assets attached to the Ethos public zone, you can also
|
|
19
|
+
* just use the stable CloudFront URL directly (`<img src={asset.url} />`).
|
|
20
|
+
* This component is meant for private regime assets that require
|
|
21
|
+
* client-side decryption.
|
|
22
|
+
*/
|
|
23
|
+
import type { ReactNode, ImgHTMLAttributes, VideoHTMLAttributes, AudioHTMLAttributes, AnchorHTMLAttributes } from "react";
|
|
24
|
+
import type { AssetsClient } from "../assets.js";
|
|
25
|
+
interface AithosAssetBaseProps {
|
|
26
|
+
/** Asset URN, e.g. `urn:aithos:asset:did:aithos:z6Mkr…:asset_01J…`. */
|
|
27
|
+
readonly urn: string;
|
|
28
|
+
/** Override the assets client from context. */
|
|
29
|
+
readonly client?: AssetsClient | null;
|
|
30
|
+
/** Rendered while the fetch+decrypt is in flight. */
|
|
31
|
+
readonly fallback?: ReactNode;
|
|
32
|
+
/** Rendered on fetch/decrypt failure. `as="img"` defaults to showing alt instead. */
|
|
33
|
+
readonly errorFallback?: ReactNode;
|
|
34
|
+
/** Called once the asset is decrypted and ready to display. */
|
|
35
|
+
readonly onLoad?: () => void;
|
|
36
|
+
/** Called when the fetch/decrypt fails. */
|
|
37
|
+
readonly onError?: (err: Error) => void;
|
|
38
|
+
/**
|
|
39
|
+
* If `true`, keep the previous asset visible while a new URN is
|
|
40
|
+
* being fetched, instead of flashing the fallback. Default `false`.
|
|
41
|
+
*/
|
|
42
|
+
readonly keepPreviousOnUrnChange?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface AithosImageProps extends AithosAssetBaseProps, Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "onLoad" | "onError"> {
|
|
45
|
+
readonly as?: "img";
|
|
46
|
+
}
|
|
47
|
+
export interface AithosVideoProps extends AithosAssetBaseProps, Omit<VideoHTMLAttributes<HTMLVideoElement>, "src" | "onLoad" | "onError"> {
|
|
48
|
+
readonly as: "video";
|
|
49
|
+
}
|
|
50
|
+
export interface AithosAudioProps extends AithosAssetBaseProps, Omit<AudioHTMLAttributes<HTMLAudioElement>, "src" | "onLoad" | "onError"> {
|
|
51
|
+
readonly as: "audio";
|
|
52
|
+
}
|
|
53
|
+
export interface AithosDownloadProps extends AithosAssetBaseProps, Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "onLoad" | "onError"> {
|
|
54
|
+
readonly as: "download";
|
|
55
|
+
/** Suggested filename in the browser's download dialog. */
|
|
56
|
+
readonly filename?: string;
|
|
57
|
+
readonly children?: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
export type AithosAssetProps = AithosImageProps | AithosVideoProps | AithosAudioProps | AithosDownloadProps;
|
|
60
|
+
/**
|
|
61
|
+
* One component, four media kinds. The `as` prop selects between
|
|
62
|
+
* `<img>` (default), `<video>`, `<audio>`, and a styled `<a download>`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function AithosAsset(props: AithosAssetProps): ReactNode;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=AithosAsset.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useAithosAsset } from "./use-aithos-asset.js";
|
|
4
|
+
/* -------------------------------------------------------------------------- */
|
|
5
|
+
/* Component */
|
|
6
|
+
/* -------------------------------------------------------------------------- */
|
|
7
|
+
/**
|
|
8
|
+
* One component, four media kinds. The `as` prop selects between
|
|
9
|
+
* `<img>` (default), `<video>`, `<audio>`, and a styled `<a download>`.
|
|
10
|
+
*/
|
|
11
|
+
export function AithosAsset(props) {
|
|
12
|
+
const { urn, client, fallback, errorFallback, onLoad, onError, keepPreviousOnUrnChange, ...rest } = props;
|
|
13
|
+
const state = useAithosAsset(urn, {
|
|
14
|
+
client: client ?? undefined,
|
|
15
|
+
keepPreviousOnUrnChange,
|
|
16
|
+
});
|
|
17
|
+
// onLoad / onError side-effects
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (state.url && onLoad)
|
|
20
|
+
onLoad();
|
|
21
|
+
}, [state.url, onLoad]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (state.error && onError)
|
|
24
|
+
onError(state.error);
|
|
25
|
+
}, [state.error, onError]);
|
|
26
|
+
// Loading
|
|
27
|
+
if (state.loading) {
|
|
28
|
+
return (fallback ?? null);
|
|
29
|
+
}
|
|
30
|
+
// Error
|
|
31
|
+
if (state.error) {
|
|
32
|
+
if (errorFallback !== undefined) {
|
|
33
|
+
return errorFallback;
|
|
34
|
+
}
|
|
35
|
+
// For img-mode, the alt attribute is a sensible default error
|
|
36
|
+
// surface (the browser renders the alt text when src is broken).
|
|
37
|
+
if (rest.as === undefined || rest.as === "img") {
|
|
38
|
+
const imgProps = rest;
|
|
39
|
+
// eslint-disable-next-line jsx-a11y/alt-text
|
|
40
|
+
return _jsx("img", { ...imgProps, src: undefined });
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// No URL yet (no URN supplied → idle state)
|
|
45
|
+
if (!state.url) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// Dispatch on `as`
|
|
49
|
+
const as = rest.as ?? "img";
|
|
50
|
+
const { as: _consumed, ...domProps } = rest;
|
|
51
|
+
void _consumed;
|
|
52
|
+
if (as === "img") {
|
|
53
|
+
return (_jsx("img", { ...domProps, src: state.url }));
|
|
54
|
+
}
|
|
55
|
+
if (as === "video") {
|
|
56
|
+
return (_jsx("video", { ...domProps, src: state.url }));
|
|
57
|
+
}
|
|
58
|
+
if (as === "audio") {
|
|
59
|
+
return (_jsx("audio", { ...domProps, src: state.url }));
|
|
60
|
+
}
|
|
61
|
+
if (as === "download") {
|
|
62
|
+
const { filename, children, ...anchorRest } = domProps;
|
|
63
|
+
return (_jsx("a", { ...anchorRest, href: state.url, download: filename ?? true, children: children }));
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=AithosAsset.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React context for the Aithos assets client.
|
|
3
|
+
*
|
|
4
|
+
* App roots wrap their tree with `<AssetsClientProvider client={sdk.assets}>`
|
|
5
|
+
* once, then child components use `<AithosAsset urn="..." />` (or the
|
|
6
|
+
* `useAithosAsset` hook) without having to thread the client through
|
|
7
|
+
* props.
|
|
8
|
+
*/
|
|
9
|
+
import { type ReactNode } from "react";
|
|
10
|
+
import type { AssetsClient } from "../assets.js";
|
|
11
|
+
export interface AssetsClientProviderProps {
|
|
12
|
+
/** The SDK's assets client — typically `sdk.assets`. */
|
|
13
|
+
readonly client: AssetsClient;
|
|
14
|
+
readonly children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Provider that exposes an {@link AssetsClient} to descendants.
|
|
18
|
+
*
|
|
19
|
+
* Typical placement: as high in the React tree as the authenticated
|
|
20
|
+
* session lives (e.g. inside the auth boundary).
|
|
21
|
+
*/
|
|
22
|
+
export declare function AssetsClientProvider(props: AssetsClientProviderProps): any;
|
|
23
|
+
/**
|
|
24
|
+
* Return the {@link AssetsClient} from the nearest provider, or `null`
|
|
25
|
+
* if no provider is present. Components SHOULD accept an override via
|
|
26
|
+
* an explicit `client` prop and fall back to this hook when omitted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useAssetsClient(): AssetsClient | null;
|
|
29
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Copyright 2026 Mathieu Colla
|
|
4
|
+
/**
|
|
5
|
+
* React context for the Aithos assets client.
|
|
6
|
+
*
|
|
7
|
+
* App roots wrap their tree with `<AssetsClientProvider client={sdk.assets}>`
|
|
8
|
+
* once, then child components use `<AithosAsset urn="..." />` (or the
|
|
9
|
+
* `useAithosAsset` hook) without having to thread the client through
|
|
10
|
+
* props.
|
|
11
|
+
*/
|
|
12
|
+
import { createContext, useContext } from "react";
|
|
13
|
+
const AssetsClientContext = createContext(null);
|
|
14
|
+
/**
|
|
15
|
+
* Provider that exposes an {@link AssetsClient} to descendants.
|
|
16
|
+
*
|
|
17
|
+
* Typical placement: as high in the React tree as the authenticated
|
|
18
|
+
* session lives (e.g. inside the auth boundary).
|
|
19
|
+
*/
|
|
20
|
+
export function AssetsClientProvider(props) {
|
|
21
|
+
return (_jsx(AssetsClientContext.Provider, { value: props.client, children: props.children }));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Return the {@link AssetsClient} from the nearest provider, or `null`
|
|
25
|
+
* if no provider is present. Components SHOULD accept an override via
|
|
26
|
+
* an explicit `client` prop and fall back to this hook when omitted.
|
|
27
|
+
*/
|
|
28
|
+
export function useAssetsClient() {
|
|
29
|
+
return useContext(AssetsClientContext);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=context.js.map
|