@aithos/sdk 0.1.0-alpha.4 → 0.1.0-alpha.5
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/dist/src/auth.d.ts +94 -136
- package/dist/src/auth.js +440 -159
- package/dist/src/compute.d.ts +8 -6
- package/dist/src/compute.js +19 -11
- package/dist/src/ethos.d.ts +117 -1
- package/dist/src/ethos.js +417 -16
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.js +17 -5
- package/dist/src/internal/delegate-bundle.d.ts +18 -0
- package/dist/src/internal/delegate-bundle.js +89 -0
- package/dist/src/internal/delegate-state.d.ts +45 -0
- package/dist/src/internal/delegate-state.js +120 -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 +88 -1
- package/dist/src/mandates.js +185 -8
- package/dist/src/sdk.d.ts +36 -3
- package/dist/src/sdk.js +27 -23
- package/dist/src/wallet.d.ts +4 -6
- package/dist/src/wallet.js +18 -8
- package/dist/test/auth-j3.test.d.ts +2 -0
- package/dist/test/auth-j3.test.js +360 -0
- package/dist/test/compute.test.js +22 -11
- package/dist/test/ethos.test.d.ts +2 -0
- package/dist/test/ethos.test.js +219 -0
- package/dist/test/key-store.test.d.ts +2 -0
- package/dist/test/key-store.test.js +161 -0
- package/dist/test/mandates.test.d.ts +2 -0
- package/dist/test/mandates.test.js +93 -0
- package/dist/test/sdk.test.js +64 -30
- package/dist/test/signer.test.d.ts +2 -0
- package/dist/test/signer.test.js +117 -0
- package/dist/test/wallet.test.js +20 -9
- package/package.json +2 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
import { AithosSDKError } from "../types.js";
|
|
4
|
+
const HEX_64 = /^[0-9a-f]{64}$/;
|
|
5
|
+
/**
|
|
6
|
+
* Parse a recovery JSON string. Throws {@link AithosSDKError} with
|
|
7
|
+
* `code === "auth_invalid_recovery_file"` if the input is malformed.
|
|
8
|
+
*/
|
|
9
|
+
export function parseRecoveryFile(text) {
|
|
10
|
+
let obj;
|
|
11
|
+
try {
|
|
12
|
+
obj = JSON.parse(text);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw bad("not valid JSON");
|
|
16
|
+
}
|
|
17
|
+
if (typeof obj !== "object" || obj === null) {
|
|
18
|
+
throw bad("not a JSON object");
|
|
19
|
+
}
|
|
20
|
+
const o = obj;
|
|
21
|
+
const ver = o["aithos_recovery_version"];
|
|
22
|
+
if (typeof ver !== "string" || !ver.startsWith("0.1.0")) {
|
|
23
|
+
throw bad(`unsupported aithos_recovery_version: ${String(ver)}`);
|
|
24
|
+
}
|
|
25
|
+
// Both shapes use snake_case for `display_name` and `seeds_hex`.
|
|
26
|
+
const handle = o["handle"];
|
|
27
|
+
const displayName = o["display_name"];
|
|
28
|
+
const did = o["did"];
|
|
29
|
+
const seedsRaw = o["seeds_hex"];
|
|
30
|
+
if (typeof handle !== "string" || !handle)
|
|
31
|
+
throw bad("missing handle");
|
|
32
|
+
if (typeof displayName !== "string")
|
|
33
|
+
throw bad("missing display_name");
|
|
34
|
+
if (typeof did !== "string" || !did.startsWith("did:")) {
|
|
35
|
+
throw bad("missing or malformed did");
|
|
36
|
+
}
|
|
37
|
+
if (typeof seedsRaw !== "object" || seedsRaw === null) {
|
|
38
|
+
throw bad("missing seeds_hex");
|
|
39
|
+
}
|
|
40
|
+
const seeds = seedsRaw;
|
|
41
|
+
for (const k of ["root", "public", "circle", "self"]) {
|
|
42
|
+
const v = seeds[k];
|
|
43
|
+
if (typeof v !== "string" || !HEX_64.test(v)) {
|
|
44
|
+
throw bad(`seeds_hex.${k}: expected 64-char lowercase hex`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
handle,
|
|
49
|
+
displayName,
|
|
50
|
+
did,
|
|
51
|
+
seedsHex: {
|
|
52
|
+
root: seeds["root"],
|
|
53
|
+
public: seeds["public"],
|
|
54
|
+
circle: seeds["circle"],
|
|
55
|
+
self: seeds["self"],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Read the file (Blob or already-decoded string) into UTF-8 text. */
|
|
60
|
+
export async function readRecoveryFileText(file) {
|
|
61
|
+
if (typeof file === "string")
|
|
62
|
+
return file;
|
|
63
|
+
return file.text();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the recovery-file JSON blob from a fresh BrowserIdentity. Used
|
|
67
|
+
* by `signUp` so the same writer/reader code handles both ends of the
|
|
68
|
+
* round-trip.
|
|
69
|
+
*/
|
|
70
|
+
export function serializeRecoveryFile(identity) {
|
|
71
|
+
const payload = {
|
|
72
|
+
aithos_recovery_version: "0.1.0-plaintext",
|
|
73
|
+
handle: identity.handle,
|
|
74
|
+
display_name: identity.displayName,
|
|
75
|
+
did: identity.did,
|
|
76
|
+
seeds_hex: {
|
|
77
|
+
root: bytesToHex(identity.root.seed),
|
|
78
|
+
public: bytesToHex(identity.public.seed),
|
|
79
|
+
circle: bytesToHex(identity.circle.seed),
|
|
80
|
+
self: bytesToHex(identity.self.seed),
|
|
81
|
+
},
|
|
82
|
+
saved_at: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
text: JSON.stringify(payload, null, 2),
|
|
86
|
+
filename: `aithos-recovery-${identity.handle}.json`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function bad(detail) {
|
|
90
|
+
return new AithosSDKError("auth_invalid_recovery_file", `recovery file is invalid: ${detail}`);
|
|
91
|
+
}
|
|
92
|
+
function bytesToHex(b) {
|
|
93
|
+
let out = "";
|
|
94
|
+
for (let i = 0; i < b.length; i++)
|
|
95
|
+
out += b[i].toString(16).padStart(2, "0");
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=recovery-file.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type KeyPair } from "@aithos/protocol-client";
|
|
2
|
+
/**
|
|
3
|
+
* Capability to produce Ed25519 signatures over a fixed key. The key
|
|
4
|
+
* material itself is not exposed — only the public key (which is, by
|
|
5
|
+
* definition, public) and the {@link sign} method.
|
|
6
|
+
*
|
|
7
|
+
* `sign` returns a Promise<Uint8Array> even when the underlying
|
|
8
|
+
* implementation is synchronous (e.g. {@link RawSeedSigner}) so future
|
|
9
|
+
* implementations backed by `crypto.subtle.sign` can drop in without
|
|
10
|
+
* breaking callers.
|
|
11
|
+
*/
|
|
12
|
+
export interface Signer {
|
|
13
|
+
/** 32-byte Ed25519 public key. */
|
|
14
|
+
readonly publicKey: Uint8Array;
|
|
15
|
+
/** Sign `message` and return the 64-byte Ed25519 signature. */
|
|
16
|
+
sign(message: Uint8Array): Promise<Uint8Array>;
|
|
17
|
+
/** Zeroize any private material the signer holds. Idempotent. */
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Today's implementation: wraps a raw 32-byte Ed25519 seed and signs
|
|
22
|
+
* via `@noble/ed25519`. Holds the seed in a defensively-copied
|
|
23
|
+
* Uint8Array bound to a private field, so mutating the constructor
|
|
24
|
+
* input afterwards does not affect the signer.
|
|
25
|
+
*
|
|
26
|
+
* Migration note: when we move to Web Crypto non-extractable keys,
|
|
27
|
+
* this class is replaced by a `SubtleSigner` that holds a `CryptoKey`
|
|
28
|
+
* reference and calls `crypto.subtle.sign("Ed25519", key, message)`.
|
|
29
|
+
* The {@link Signer} interface stays the same, so all callers (the
|
|
30
|
+
* SessionVault, the auth namespace, the ethos publish path) keep
|
|
31
|
+
* working unchanged.
|
|
32
|
+
*/
|
|
33
|
+
export declare class RawSeedSigner implements Signer {
|
|
34
|
+
#private;
|
|
35
|
+
readonly publicKey: Uint8Array;
|
|
36
|
+
/**
|
|
37
|
+
* @param seed 32-byte Ed25519 seed (the private half).
|
|
38
|
+
* @param publicKey 32-byte Ed25519 public key matching `seed`.
|
|
39
|
+
* Both arrays are defensively copied — the caller may zeroize their
|
|
40
|
+
* originals immediately.
|
|
41
|
+
*/
|
|
42
|
+
constructor(seed: Uint8Array, publicKey: Uint8Array);
|
|
43
|
+
sign(message: Uint8Array): Promise<Uint8Array>;
|
|
44
|
+
destroy(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Internal escape hatch for protocol-client interop. Returns a KeyPair
|
|
47
|
+
* usable with `buildSignedEnvelope({ signer })` and friends, which
|
|
48
|
+
* today take a raw seed. Marked `_unsafe` because it surfaces the
|
|
49
|
+
* private seed bytes — only callers within `@aithos/sdk` should use
|
|
50
|
+
* it, and never propagate the result outside the SDK boundary.
|
|
51
|
+
*
|
|
52
|
+
* When protocol-client gains a Signer-shaped API (post-alpha), this
|
|
53
|
+
* method goes away and SubtleSigner becomes pluggable upstream.
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
_unsafeKeyPair(): KeyPair;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=signer.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Internal Signer abstraction.
|
|
4
|
+
//
|
|
5
|
+
// SDK-internal only — NOT exported from the package barrel. The whole
|
|
6
|
+
// point of this file is to draw the line between "what the SDK consumer
|
|
7
|
+
// sees" (no seeds, no key bytes, just verbs like `addSection` and
|
|
8
|
+
// `publish`) and "how the SDK signs internally" (today: raw Ed25519
|
|
9
|
+
// seeds via @noble; tomorrow: non-extractable CryptoKeys via
|
|
10
|
+
// crypto.subtle).
|
|
11
|
+
//
|
|
12
|
+
// The {@link Signer} interface is the contract. {@link RawSeedSigner}
|
|
13
|
+
// is today's implementation. When we migrate to Web Crypto
|
|
14
|
+
// non-extractable keys, a {@link SubtleSigner} drops in beside it
|
|
15
|
+
// implementing the same interface, and nothing visible to the SDK
|
|
16
|
+
// consumer changes.
|
|
17
|
+
//
|
|
18
|
+
// `sign` is async-shaped from the start so the migration to
|
|
19
|
+
// `crypto.subtle.sign` (which is async) doesn't ripple through the
|
|
20
|
+
// caller graph as a breaking interface change.
|
|
21
|
+
import { sign as ed25519Sign } from "@aithos/protocol-client";
|
|
22
|
+
/**
|
|
23
|
+
* Today's implementation: wraps a raw 32-byte Ed25519 seed and signs
|
|
24
|
+
* via `@noble/ed25519`. Holds the seed in a defensively-copied
|
|
25
|
+
* Uint8Array bound to a private field, so mutating the constructor
|
|
26
|
+
* input afterwards does not affect the signer.
|
|
27
|
+
*
|
|
28
|
+
* Migration note: when we move to Web Crypto non-extractable keys,
|
|
29
|
+
* this class is replaced by a `SubtleSigner` that holds a `CryptoKey`
|
|
30
|
+
* reference and calls `crypto.subtle.sign("Ed25519", key, message)`.
|
|
31
|
+
* The {@link Signer} interface stays the same, so all callers (the
|
|
32
|
+
* SessionVault, the auth namespace, the ethos publish path) keep
|
|
33
|
+
* working unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export class RawSeedSigner {
|
|
36
|
+
publicKey;
|
|
37
|
+
#seed;
|
|
38
|
+
#destroyed = false;
|
|
39
|
+
/**
|
|
40
|
+
* @param seed 32-byte Ed25519 seed (the private half).
|
|
41
|
+
* @param publicKey 32-byte Ed25519 public key matching `seed`.
|
|
42
|
+
* Both arrays are defensively copied — the caller may zeroize their
|
|
43
|
+
* originals immediately.
|
|
44
|
+
*/
|
|
45
|
+
constructor(seed, publicKey) {
|
|
46
|
+
if (seed.length !== 32) {
|
|
47
|
+
throw new Error(`RawSeedSigner: seed must be 32 bytes, got ${seed.length}`);
|
|
48
|
+
}
|
|
49
|
+
if (publicKey.length !== 32) {
|
|
50
|
+
throw new Error(`RawSeedSigner: publicKey must be 32 bytes, got ${publicKey.length}`);
|
|
51
|
+
}
|
|
52
|
+
this.#seed = new Uint8Array(seed);
|
|
53
|
+
this.publicKey = new Uint8Array(publicKey);
|
|
54
|
+
}
|
|
55
|
+
async sign(message) {
|
|
56
|
+
if (this.#destroyed) {
|
|
57
|
+
throw new Error("RawSeedSigner: cannot sign with a destroyed signer");
|
|
58
|
+
}
|
|
59
|
+
return ed25519Sign(message, this.#seed);
|
|
60
|
+
}
|
|
61
|
+
destroy() {
|
|
62
|
+
if (this.#destroyed)
|
|
63
|
+
return;
|
|
64
|
+
this.#seed.fill(0);
|
|
65
|
+
this.#destroyed = true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Internal escape hatch for protocol-client interop. Returns a KeyPair
|
|
69
|
+
* usable with `buildSignedEnvelope({ signer })` and friends, which
|
|
70
|
+
* today take a raw seed. Marked `_unsafe` because it surfaces the
|
|
71
|
+
* private seed bytes — only callers within `@aithos/sdk` should use
|
|
72
|
+
* it, and never propagate the result outside the SDK boundary.
|
|
73
|
+
*
|
|
74
|
+
* When protocol-client gains a Signer-shaped API (post-alpha), this
|
|
75
|
+
* method goes away and SubtleSigner becomes pluggable upstream.
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
_unsafeKeyPair() {
|
|
80
|
+
if (this.#destroyed) {
|
|
81
|
+
throw new Error("RawSeedSigner: cannot use a destroyed signer");
|
|
82
|
+
}
|
|
83
|
+
return { seed: this.#seed, publicKey: this.publicKey };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=signer.js.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stored owner identity material. Treat as opaque from the consumer's
|
|
3
|
+
* point of view — its internal shape may evolve between SDK versions
|
|
4
|
+
* (e.g. when we migrate to `CryptoKey` references). Custom KeyStore
|
|
5
|
+
* implementations should round-trip the value as-is rather than
|
|
6
|
+
* peeking inside.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface StoredOwnerKeys {
|
|
11
|
+
/** Schema version. Today: `"0.1.0-hex"`. */
|
|
12
|
+
readonly version: "0.1.0-hex";
|
|
13
|
+
readonly did: string;
|
|
14
|
+
readonly handle: string;
|
|
15
|
+
readonly displayName: string;
|
|
16
|
+
/** Hex-encoded 32-byte Ed25519 seeds — one per sphere. */
|
|
17
|
+
readonly seedsHex: {
|
|
18
|
+
readonly root: string;
|
|
19
|
+
readonly public: string;
|
|
20
|
+
readonly circle: string;
|
|
21
|
+
readonly self: string;
|
|
22
|
+
};
|
|
23
|
+
/** ISO-8601 timestamp of the original save. Informational. */
|
|
24
|
+
readonly savedAt: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Stored delegate session material. Opaque from the consumer's
|
|
28
|
+
* point of view; see {@link StoredOwnerKeys}.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export interface StoredDelegateKeys {
|
|
33
|
+
readonly version: "0.1.0-hex";
|
|
34
|
+
/** DID of the subject whose ethos this mandate authorizes. */
|
|
35
|
+
readonly subjectDid: string;
|
|
36
|
+
/** Mandate id — used as the storage key. */
|
|
37
|
+
readonly mandateId: string;
|
|
38
|
+
/** Full §4.2 SignedMandate, stored as opaque JSON. */
|
|
39
|
+
readonly mandate: Record<string, unknown>;
|
|
40
|
+
/** Grantee URN, e.g. `urn:aithos:agent:bob1`. */
|
|
41
|
+
readonly granteeId: string;
|
|
42
|
+
/** Multibase-encoded Ed25519 grantee pubkey, matches `mandate.grantee.pubkey`. */
|
|
43
|
+
readonly granteePubkeyMultibase: string;
|
|
44
|
+
/** Hex-encoded 32-byte delegate Ed25519 seed. */
|
|
45
|
+
readonly delegateSeedHex: string;
|
|
46
|
+
readonly importedAt: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Persistence backend for owner identity material and delegate
|
|
50
|
+
* bundles. Methods are async because an IndexedDB-backed
|
|
51
|
+
* implementation is the default and IDB's API is async — sync stores
|
|
52
|
+
* are still trivial to write (`memoryKeyStore`) by returning
|
|
53
|
+
* pre-resolved promises.
|
|
54
|
+
*
|
|
55
|
+
* Implementations should never throw on missing records: a fresh
|
|
56
|
+
* device returns `null` from `loadOwner` and `[]` from
|
|
57
|
+
* `listDelegates`. They should only throw on hard I/O failures
|
|
58
|
+
* (quota exceeded, schema corruption). The SDK treats throws as
|
|
59
|
+
* "store unavailable, fall through to re-auth".
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
export interface AithosKeyStore {
|
|
64
|
+
loadOwner(): Promise<StoredOwnerKeys | null>;
|
|
65
|
+
saveOwner(owner: StoredOwnerKeys): Promise<void>;
|
|
66
|
+
clearOwner(): Promise<void>;
|
|
67
|
+
listDelegates(): Promise<readonly StoredDelegateKeys[]>;
|
|
68
|
+
loadDelegate(mandateId: string): Promise<StoredDelegateKeys | null>;
|
|
69
|
+
saveDelegate(d: StoredDelegateKeys): Promise<void>;
|
|
70
|
+
removeDelegate(mandateId: string): Promise<void>;
|
|
71
|
+
clearAllDelegates(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* In-memory key store. Loses everything on page reload — useful for
|
|
75
|
+
* tests, SSR, ephemeral CLI tools, and any workflow where you want a
|
|
76
|
+
* deliberately short-lived session.
|
|
77
|
+
*
|
|
78
|
+
* @public
|
|
79
|
+
*/
|
|
80
|
+
export declare function memoryKeyStore(): AithosKeyStore;
|
|
81
|
+
/**
|
|
82
|
+
* Default IndexedDB database name used by {@link indexedDbKeyStore}.
|
|
83
|
+
* Apps that want to coexist with other Aithos-aware libs (or scope
|
|
84
|
+
* sessions per-tenant) can pass a custom `dbName`.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_KEYSTORE_DB_NAME = "aithos-sdk-keys";
|
|
87
|
+
interface IndexedDbKeyStoreOptions {
|
|
88
|
+
/** Database name. Defaults to {@link DEFAULT_KEYSTORE_DB_NAME}. */
|
|
89
|
+
readonly dbName?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Inject a non-default IDBFactory — used by tests. Apps should not
|
|
92
|
+
* pass this; the global `indexedDB` is the right choice everywhere
|
|
93
|
+
* else.
|
|
94
|
+
*
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
readonly factory?: IDBFactory;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* IndexedDB-backed key store. Default in browser environments —
|
|
101
|
+
* persists across reloads and browser restarts, scoped to the
|
|
102
|
+
* page's origin.
|
|
103
|
+
*
|
|
104
|
+
* Storage shape today:
|
|
105
|
+
*
|
|
106
|
+
* - object store `owner` — single record at key `"self"`,
|
|
107
|
+
* shape {@link StoredOwnerKeys}
|
|
108
|
+
* - object store `delegates` — keyPath `mandateId`, shape
|
|
109
|
+
* {@link StoredDelegateKeys}
|
|
110
|
+
*
|
|
111
|
+
* Both stores live in DB `aithos-sdk-keys` (configurable via
|
|
112
|
+
* `dbName`) at version 1. Future versions will trigger an upgrade
|
|
113
|
+
* transaction that migrates records into the new shape.
|
|
114
|
+
*
|
|
115
|
+
* @public
|
|
116
|
+
*/
|
|
117
|
+
export declare function indexedDbKeyStore(opts?: IndexedDbKeyStoreOptions): AithosKeyStore;
|
|
118
|
+
/**
|
|
119
|
+
* Pick a sensible default: {@link indexedDbKeyStore} when an IDBFactory
|
|
120
|
+
* is reachable (browser environments), {@link memoryKeyStore} otherwise
|
|
121
|
+
* (Node, edge runtimes — apps running there should pass their own
|
|
122
|
+
* keystore explicitly).
|
|
123
|
+
*
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
export declare function defaultKeyStore(): AithosKeyStore;
|
|
127
|
+
export {};
|
|
128
|
+
//# sourceMappingURL=key-store.d.ts.map
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/* -------------------------------------------------------------------------- */
|
|
4
|
+
/* In-memory implementation */
|
|
5
|
+
/* -------------------------------------------------------------------------- */
|
|
6
|
+
/**
|
|
7
|
+
* In-memory key store. Loses everything on page reload — useful for
|
|
8
|
+
* tests, SSR, ephemeral CLI tools, and any workflow where you want a
|
|
9
|
+
* deliberately short-lived session.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export function memoryKeyStore() {
|
|
14
|
+
let owner = null;
|
|
15
|
+
const delegates = new Map();
|
|
16
|
+
return {
|
|
17
|
+
async loadOwner() {
|
|
18
|
+
return owner;
|
|
19
|
+
},
|
|
20
|
+
async saveOwner(o) {
|
|
21
|
+
owner = o;
|
|
22
|
+
},
|
|
23
|
+
async clearOwner() {
|
|
24
|
+
owner = null;
|
|
25
|
+
},
|
|
26
|
+
async listDelegates() {
|
|
27
|
+
return [...delegates.values()];
|
|
28
|
+
},
|
|
29
|
+
async loadDelegate(mandateId) {
|
|
30
|
+
return delegates.get(mandateId) ?? null;
|
|
31
|
+
},
|
|
32
|
+
async saveDelegate(d) {
|
|
33
|
+
delegates.set(d.mandateId, d);
|
|
34
|
+
},
|
|
35
|
+
async removeDelegate(mandateId) {
|
|
36
|
+
delegates.delete(mandateId);
|
|
37
|
+
},
|
|
38
|
+
async clearAllDelegates() {
|
|
39
|
+
delegates.clear();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/* -------------------------------------------------------------------------- */
|
|
44
|
+
/* IndexedDB implementation */
|
|
45
|
+
/* -------------------------------------------------------------------------- */
|
|
46
|
+
/**
|
|
47
|
+
* Default IndexedDB database name used by {@link indexedDbKeyStore}.
|
|
48
|
+
* Apps that want to coexist with other Aithos-aware libs (or scope
|
|
49
|
+
* sessions per-tenant) can pass a custom `dbName`.
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_KEYSTORE_DB_NAME = "aithos-sdk-keys";
|
|
52
|
+
const STORE_OWNER = "owner";
|
|
53
|
+
const STORE_DELEGATES = "delegates";
|
|
54
|
+
/** The single owner record always lives at this key — the store has no keyPath. */
|
|
55
|
+
const OWNER_KEY = "self";
|
|
56
|
+
/**
|
|
57
|
+
* IndexedDB-backed key store. Default in browser environments —
|
|
58
|
+
* persists across reloads and browser restarts, scoped to the
|
|
59
|
+
* page's origin.
|
|
60
|
+
*
|
|
61
|
+
* Storage shape today:
|
|
62
|
+
*
|
|
63
|
+
* - object store `owner` — single record at key `"self"`,
|
|
64
|
+
* shape {@link StoredOwnerKeys}
|
|
65
|
+
* - object store `delegates` — keyPath `mandateId`, shape
|
|
66
|
+
* {@link StoredDelegateKeys}
|
|
67
|
+
*
|
|
68
|
+
* Both stores live in DB `aithos-sdk-keys` (configurable via
|
|
69
|
+
* `dbName`) at version 1. Future versions will trigger an upgrade
|
|
70
|
+
* transaction that migrates records into the new shape.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export function indexedDbKeyStore(opts = {}) {
|
|
75
|
+
const dbName = opts.dbName ?? DEFAULT_KEYSTORE_DB_NAME;
|
|
76
|
+
const factory = opts.factory ??
|
|
77
|
+
(typeof indexedDB !== "undefined" ? indexedDB : undefined);
|
|
78
|
+
if (!factory) {
|
|
79
|
+
throw new Error("indexedDbKeyStore: no IDBFactory available — pass `factory` for non-browser environments or use memoryKeyStore() instead.");
|
|
80
|
+
}
|
|
81
|
+
const open = () => new Promise((resolve, reject) => {
|
|
82
|
+
const req = factory.open(dbName, 1);
|
|
83
|
+
req.onupgradeneeded = () => {
|
|
84
|
+
const db = req.result;
|
|
85
|
+
if (!db.objectStoreNames.contains(STORE_OWNER)) {
|
|
86
|
+
db.createObjectStore(STORE_OWNER);
|
|
87
|
+
}
|
|
88
|
+
if (!db.objectStoreNames.contains(STORE_DELEGATES)) {
|
|
89
|
+
db.createObjectStore(STORE_DELEGATES, { keyPath: "mandateId" });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
req.onsuccess = () => resolve(req.result);
|
|
93
|
+
req.onerror = () => reject(req.error ?? new Error("indexedDB.open failed"));
|
|
94
|
+
});
|
|
95
|
+
const tx = async (stores, mode, run) => {
|
|
96
|
+
const db = await open();
|
|
97
|
+
try {
|
|
98
|
+
const t = db.transaction([...stores], mode);
|
|
99
|
+
const result = await Promise.resolve(run(t));
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
t.oncomplete = () => resolve();
|
|
102
|
+
t.onerror = () => reject(t.error ?? new Error("transaction failed"));
|
|
103
|
+
t.onabort = () => reject(t.error ?? new Error("transaction aborted"));
|
|
104
|
+
});
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const reqAsPromise = (req) => new Promise((resolve, reject) => {
|
|
112
|
+
req.onsuccess = () => resolve(req.result);
|
|
113
|
+
req.onerror = () => reject(req.error ?? new Error("IDBRequest failed"));
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
async loadOwner() {
|
|
117
|
+
return tx([STORE_OWNER], "readonly", async (t) => {
|
|
118
|
+
const v = await reqAsPromise(t.objectStore(STORE_OWNER).get(OWNER_KEY));
|
|
119
|
+
return validOwnerOrNull(v);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
async saveOwner(owner) {
|
|
123
|
+
await tx([STORE_OWNER], "readwrite", (t) => {
|
|
124
|
+
t.objectStore(STORE_OWNER).put(owner, OWNER_KEY);
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
async clearOwner() {
|
|
128
|
+
await tx([STORE_OWNER], "readwrite", (t) => {
|
|
129
|
+
t.objectStore(STORE_OWNER).delete(OWNER_KEY);
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
async listDelegates() {
|
|
133
|
+
return tx([STORE_DELEGATES], "readonly", async (t) => {
|
|
134
|
+
const all = await reqAsPromise(t.objectStore(STORE_DELEGATES).getAll());
|
|
135
|
+
return all
|
|
136
|
+
.map(validDelegateOrNull)
|
|
137
|
+
.filter((d) => d !== null);
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
async loadDelegate(mandateId) {
|
|
141
|
+
return tx([STORE_DELEGATES], "readonly", async (t) => {
|
|
142
|
+
const v = await reqAsPromise(t.objectStore(STORE_DELEGATES).get(mandateId));
|
|
143
|
+
return validDelegateOrNull(v);
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
async saveDelegate(d) {
|
|
147
|
+
await tx([STORE_DELEGATES], "readwrite", (t) => {
|
|
148
|
+
t.objectStore(STORE_DELEGATES).put(d);
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
async removeDelegate(mandateId) {
|
|
152
|
+
await tx([STORE_DELEGATES], "readwrite", (t) => {
|
|
153
|
+
t.objectStore(STORE_DELEGATES).delete(mandateId);
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
async clearAllDelegates() {
|
|
157
|
+
await tx([STORE_DELEGATES], "readwrite", (t) => {
|
|
158
|
+
t.objectStore(STORE_DELEGATES).clear();
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/* -------------------------------------------------------------------------- */
|
|
164
|
+
/* Default picker */
|
|
165
|
+
/* -------------------------------------------------------------------------- */
|
|
166
|
+
/**
|
|
167
|
+
* Pick a sensible default: {@link indexedDbKeyStore} when an IDBFactory
|
|
168
|
+
* is reachable (browser environments), {@link memoryKeyStore} otherwise
|
|
169
|
+
* (Node, edge runtimes — apps running there should pass their own
|
|
170
|
+
* keystore explicitly).
|
|
171
|
+
*
|
|
172
|
+
* @public
|
|
173
|
+
*/
|
|
174
|
+
export function defaultKeyStore() {
|
|
175
|
+
if (typeof indexedDB !== "undefined") {
|
|
176
|
+
try {
|
|
177
|
+
return indexedDbKeyStore();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return memoryKeyStore();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return memoryKeyStore();
|
|
184
|
+
}
|
|
185
|
+
/* -------------------------------------------------------------------------- */
|
|
186
|
+
/* Runtime validators */
|
|
187
|
+
/* */
|
|
188
|
+
/* Storage values come from JSON-shaped records that may have been written */
|
|
189
|
+
/* by an older SDK version, by a corrupted process, or by an attacker with */
|
|
190
|
+
/* IDB write access. The SDK trusts only records that round-trip through */
|
|
191
|
+
/* these validators; anything else is treated as "no record" so the resume */
|
|
192
|
+
/* path falls through to re-auth. */
|
|
193
|
+
/* -------------------------------------------------------------------------- */
|
|
194
|
+
function validOwnerOrNull(v) {
|
|
195
|
+
if (typeof v !== "object" || v === null)
|
|
196
|
+
return null;
|
|
197
|
+
const o = v;
|
|
198
|
+
if (o["version"] !== "0.1.0-hex")
|
|
199
|
+
return null;
|
|
200
|
+
if (typeof o["did"] !== "string")
|
|
201
|
+
return null;
|
|
202
|
+
if (typeof o["handle"] !== "string")
|
|
203
|
+
return null;
|
|
204
|
+
if (typeof o["displayName"] !== "string")
|
|
205
|
+
return null;
|
|
206
|
+
const seeds = o["seedsHex"];
|
|
207
|
+
if (typeof seeds !== "object" || seeds === null)
|
|
208
|
+
return null;
|
|
209
|
+
const s = seeds;
|
|
210
|
+
for (const k of ["root", "public", "circle", "self"]) {
|
|
211
|
+
if (typeof s[k] !== "string")
|
|
212
|
+
return null;
|
|
213
|
+
if (s[k].length !== 64)
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
if (typeof o["savedAt"] !== "string")
|
|
217
|
+
return null;
|
|
218
|
+
return o;
|
|
219
|
+
}
|
|
220
|
+
function validDelegateOrNull(v) {
|
|
221
|
+
if (typeof v !== "object" || v === null)
|
|
222
|
+
return null;
|
|
223
|
+
const o = v;
|
|
224
|
+
if (o["version"] !== "0.1.0-hex")
|
|
225
|
+
return null;
|
|
226
|
+
if (typeof o["subjectDid"] !== "string")
|
|
227
|
+
return null;
|
|
228
|
+
if (typeof o["mandateId"] !== "string")
|
|
229
|
+
return null;
|
|
230
|
+
if (typeof o["granteeId"] !== "string")
|
|
231
|
+
return null;
|
|
232
|
+
if (typeof o["granteePubkeyMultibase"] !== "string")
|
|
233
|
+
return null;
|
|
234
|
+
if (typeof o["delegateSeedHex"] !== "string")
|
|
235
|
+
return null;
|
|
236
|
+
if (o["delegateSeedHex"].length !== 64)
|
|
237
|
+
return null;
|
|
238
|
+
if (typeof o["mandate"] !== "object" || o["mandate"] === null)
|
|
239
|
+
return null;
|
|
240
|
+
if (typeof o["importedAt"] !== "string")
|
|
241
|
+
return null;
|
|
242
|
+
return o;
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=key-store.js.map
|