@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,41 @@
|
|
|
1
|
+
export interface CmkWrap {
|
|
2
|
+
readonly recipient: string;
|
|
3
|
+
readonly alg: "x25519-hkdf-sha256-aead";
|
|
4
|
+
readonly ephemeral_public: string;
|
|
5
|
+
readonly wrap_nonce: string;
|
|
6
|
+
readonly wrapped_key: string;
|
|
7
|
+
}
|
|
8
|
+
/** The CMK envelope stored on a collection. */
|
|
9
|
+
export interface CmkEnvelope {
|
|
10
|
+
readonly alg: string;
|
|
11
|
+
readonly wraps: CmkWrap[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Derive a 32-byte X25519 private key from an Ed25519 seed via SHA-512
|
|
15
|
+
* truncation + clamping (libsodium crypto_sign_ed25519_sk_to_curve25519).
|
|
16
|
+
*/
|
|
17
|
+
export declare function ed25519SeedToX25519PrivateKey(seed: Uint8Array): Uint8Array;
|
|
18
|
+
export declare function ed25519SeedToX25519PublicKey(seed: Uint8Array): Uint8Array;
|
|
19
|
+
export declare function wrapCmkForRecipient(args: {
|
|
20
|
+
cmk: Uint8Array;
|
|
21
|
+
recipientPublicKey: Uint8Array;
|
|
22
|
+
recipientDidUrl: string;
|
|
23
|
+
collectionUrn: string;
|
|
24
|
+
}): CmkWrap;
|
|
25
|
+
/**
|
|
26
|
+
* Try to unwrap the CMK from `wrap` with `privateKey`. Returns the CMK bytes
|
|
27
|
+
* on success, or `null` when the AEAD tag fails (wrong key / AAD mismatch) —
|
|
28
|
+
* the migration relies on this null-return to AUTO-DISCOVER which legacy sphere
|
|
29
|
+
* seed sealed the wrap (try each, the right one verifies).
|
|
30
|
+
*/
|
|
31
|
+
export declare function tryUnwrapCmk(args: {
|
|
32
|
+
wrap: {
|
|
33
|
+
recipient: string;
|
|
34
|
+
ephemeral_public: string;
|
|
35
|
+
wrap_nonce: string;
|
|
36
|
+
wrapped_key: string;
|
|
37
|
+
};
|
|
38
|
+
collectionUrn: string;
|
|
39
|
+
privateKey: Uint8Array;
|
|
40
|
+
}): Uint8Array | null;
|
|
41
|
+
//# sourceMappingURL=cmk-wrap.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* CMK wrap / unwrap primitives for the data sub-protocol.
|
|
5
|
+
*
|
|
6
|
+
* These are a CANONICAL, byte-identical copy of the private crypto used by
|
|
7
|
+
* `DataClientImpl` in `../data.ts` (`#wrapCmkForRecipient` / `#unwrapCmk` and
|
|
8
|
+
* the X25519-seed derivation). They are factored out here so the legacy
|
|
9
|
+
* #data-sphere migration (`../migrate.ts`) can re-wrap a collection's CMK to
|
|
10
|
+
* the owner's `#data` key WITHOUT going through the high-level `DataClient`
|
|
11
|
+
* (which is hard-wired to a single sphere seed and so cannot unwrap a wrap
|
|
12
|
+
* sealed to a *different* legacy sphere).
|
|
13
|
+
*
|
|
14
|
+
* INVARIANT: the wire format produced here MUST stay byte-identical to
|
|
15
|
+
* `data.ts`. `test/cmk-wrap-conformance.test.ts` proves it by cross-unwrapping
|
|
16
|
+
* (data.ts wraps → cmk-wrap unwraps, and vice-versa). If you change one, change
|
|
17
|
+
* both and keep that test green — a drift here silently corrupts migrated data.
|
|
18
|
+
*/
|
|
19
|
+
import { x25519 } from "@noble/curves/ed25519.js";
|
|
20
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
21
|
+
import { sha256, sha512 } from "@noble/hashes/sha2.js";
|
|
22
|
+
import { XChaCha20Poly1305 } from "@stablelib/xchacha20poly1305";
|
|
23
|
+
/* -------------------------------------------------------------------------- */
|
|
24
|
+
/* X25519 seed derivation (mirrors data.ts) */
|
|
25
|
+
/* -------------------------------------------------------------------------- */
|
|
26
|
+
/**
|
|
27
|
+
* Derive a 32-byte X25519 private key from an Ed25519 seed via SHA-512
|
|
28
|
+
* truncation + clamping (libsodium crypto_sign_ed25519_sk_to_curve25519).
|
|
29
|
+
*/
|
|
30
|
+
export function ed25519SeedToX25519PrivateKey(seed) {
|
|
31
|
+
if (seed.length !== 32)
|
|
32
|
+
throw new Error("Ed25519 seed must be 32 bytes");
|
|
33
|
+
const h = sha512(seed);
|
|
34
|
+
const sk = new Uint8Array(h.slice(0, 32));
|
|
35
|
+
sk[0] = sk[0] & 248;
|
|
36
|
+
sk[31] = sk[31] & 127;
|
|
37
|
+
sk[31] = sk[31] | 64;
|
|
38
|
+
return sk;
|
|
39
|
+
}
|
|
40
|
+
export function ed25519SeedToX25519PublicKey(seed) {
|
|
41
|
+
const sk = ed25519SeedToX25519PrivateKey(seed);
|
|
42
|
+
try {
|
|
43
|
+
return x25519.getPublicKey(sk);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
sk.fill(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/* -------------------------------------------------------------------------- */
|
|
50
|
+
/* Wrap / unwrap (mirrors data.ts #wrapCmkForRecipient / #unwrapCmk) */
|
|
51
|
+
/* -------------------------------------------------------------------------- */
|
|
52
|
+
export function wrapCmkForRecipient(args) {
|
|
53
|
+
const ephSk = x25519.utils.randomSecretKey();
|
|
54
|
+
const ephPk = x25519.getPublicKey(ephSk);
|
|
55
|
+
const shared = x25519.getSharedSecret(ephSk, args.recipientPublicKey);
|
|
56
|
+
const wrapKey = hkdf(sha256, shared, utf8("aithos-data-cmk-wrap-v1"), utf8(args.recipientDidUrl), 32);
|
|
57
|
+
const wrapNonce = randomBytes24();
|
|
58
|
+
const aad = aadCmkWrap(args.collectionUrn, args.recipientDidUrl);
|
|
59
|
+
const aead = new XChaCha20Poly1305(wrapKey);
|
|
60
|
+
const wrapped = aead.seal(wrapNonce, args.cmk, aad);
|
|
61
|
+
wrapKey.fill(0);
|
|
62
|
+
shared.fill(0);
|
|
63
|
+
return {
|
|
64
|
+
recipient: args.recipientDidUrl,
|
|
65
|
+
alg: "x25519-hkdf-sha256-aead",
|
|
66
|
+
ephemeral_public: base64Std(ephPk),
|
|
67
|
+
wrap_nonce: base64Std(wrapNonce),
|
|
68
|
+
wrapped_key: base64Std(wrapped),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Try to unwrap the CMK from `wrap` with `privateKey`. Returns the CMK bytes
|
|
73
|
+
* on success, or `null` when the AEAD tag fails (wrong key / AAD mismatch) —
|
|
74
|
+
* the migration relies on this null-return to AUTO-DISCOVER which legacy sphere
|
|
75
|
+
* seed sealed the wrap (try each, the right one verifies).
|
|
76
|
+
*/
|
|
77
|
+
export function tryUnwrapCmk(args) {
|
|
78
|
+
const ephPk = fromBase64(args.wrap.ephemeral_public);
|
|
79
|
+
const wrapNonce = fromBase64(args.wrap.wrap_nonce);
|
|
80
|
+
const wrappedKey = fromBase64(args.wrap.wrapped_key);
|
|
81
|
+
const shared = x25519.getSharedSecret(args.privateKey, ephPk);
|
|
82
|
+
const wrapKey = hkdf(sha256, shared, utf8("aithos-data-cmk-wrap-v1"), utf8(args.wrap.recipient), 32);
|
|
83
|
+
const aad = aadCmkWrap(args.collectionUrn, args.wrap.recipient);
|
|
84
|
+
const aead = new XChaCha20Poly1305(wrapKey);
|
|
85
|
+
const cmk = aead.open(wrapNonce, wrappedKey, aad);
|
|
86
|
+
wrapKey.fill(0);
|
|
87
|
+
shared.fill(0);
|
|
88
|
+
return cmk ?? null;
|
|
89
|
+
}
|
|
90
|
+
/* -------------------------------------------------------------------------- */
|
|
91
|
+
/* Local helpers (byte-identical to data.ts) */
|
|
92
|
+
/* -------------------------------------------------------------------------- */
|
|
93
|
+
function aadCmkWrap(collectionUrn, recipient) {
|
|
94
|
+
const p = utf8("aithos-data-cmk-v1\0");
|
|
95
|
+
const c = utf8(collectionUrn);
|
|
96
|
+
const sep = new Uint8Array([0]);
|
|
97
|
+
const r = utf8(recipient);
|
|
98
|
+
const out = new Uint8Array(p.length + c.length + sep.length + r.length);
|
|
99
|
+
let off = 0;
|
|
100
|
+
out.set(p, off);
|
|
101
|
+
off += p.length;
|
|
102
|
+
out.set(c, off);
|
|
103
|
+
off += c.length;
|
|
104
|
+
out.set(sep, off);
|
|
105
|
+
off += sep.length;
|
|
106
|
+
out.set(r, off);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function utf8(s) {
|
|
110
|
+
return new TextEncoder().encode(s);
|
|
111
|
+
}
|
|
112
|
+
function randomBytes24() {
|
|
113
|
+
const buf = new Uint8Array(24);
|
|
114
|
+
globalThis.crypto?.getRandomValues(buf);
|
|
115
|
+
return buf;
|
|
116
|
+
}
|
|
117
|
+
function base64Std(bytes) {
|
|
118
|
+
let bin = "";
|
|
119
|
+
for (let i = 0; i < bytes.length; i++)
|
|
120
|
+
bin += String.fromCharCode(bytes[i]);
|
|
121
|
+
return btoa(bin);
|
|
122
|
+
}
|
|
123
|
+
function fromBase64(s) {
|
|
124
|
+
const std = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
125
|
+
const padded = std + "=".repeat((4 - (std.length % 4)) % 4);
|
|
126
|
+
const bin = atob(padded);
|
|
127
|
+
const out = new Uint8Array(bin.length);
|
|
128
|
+
for (let i = 0; i < bin.length; i++)
|
|
129
|
+
out[i] = bin.charCodeAt(i);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=cmk-wrap.js.map
|
|
@@ -50,13 +50,18 @@ export function parseDelegateBundle(text) {
|
|
|
50
50
|
}
|
|
51
51
|
const m = mandate;
|
|
52
52
|
const mandateId = m["id"];
|
|
53
|
-
|
|
53
|
+
// The mandate subject's DID is carried by `issuer` in the wire format
|
|
54
|
+
// emitted by `mintDelegateBundle()` (cf. SignedMandate.issuer in
|
|
55
|
+
// protocol-client). We accept the legacy `subject_did` / camelCase
|
|
56
|
+
// variants too so older test fixtures and any externally-minted
|
|
57
|
+
// bundles using the older shape keep working.
|
|
58
|
+
const subjectDid = m["issuer"] ?? m["subject_did"] ?? m["subjectDid"];
|
|
54
59
|
const grantee = m["grantee"];
|
|
55
60
|
if (typeof mandateId !== "string" || !mandateId) {
|
|
56
61
|
throw bad("mandate.id missing");
|
|
57
62
|
}
|
|
58
63
|
if (typeof subjectDid !== "string" || !subjectDid.startsWith("did:")) {
|
|
59
|
-
throw bad("mandate.
|
|
64
|
+
throw bad("mandate.issuer missing or malformed (expected a `did:` URL)");
|
|
60
65
|
}
|
|
61
66
|
if (typeof grantee !== "object" || grantee === null) {
|
|
62
67
|
throw bad("mandate.grantee missing");
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal signing surface required to produce an envelope's
|
|
3
|
+
* `proof.proofValue`. Intentionally narrower than {@link Signer} so
|
|
4
|
+
* callers that don't want to construct a full `RawSeedSigner` can pass
|
|
5
|
+
* a one-shot inline adapter. Every {@link Signer} satisfies this
|
|
6
|
+
* interface structurally.
|
|
7
|
+
*/
|
|
8
|
+
export interface EnvelopeSigner {
|
|
9
|
+
sign(message: Uint8Array): Promise<Uint8Array>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The wire-format envelope per spec §11.2. Apps that POST this to a
|
|
13
|
+
* backend serialize the whole object as JSON (alongside the rest of
|
|
14
|
+
* `params`) under `params._envelope`.
|
|
15
|
+
*/
|
|
16
|
+
export interface SignedEnvelope {
|
|
17
|
+
readonly "aithos-envelope": "0.1.0";
|
|
18
|
+
readonly iss: string;
|
|
19
|
+
readonly aud: string;
|
|
20
|
+
readonly method: string;
|
|
21
|
+
readonly iat: number;
|
|
22
|
+
readonly exp: number;
|
|
23
|
+
readonly nonce: string;
|
|
24
|
+
readonly params_hash: string;
|
|
25
|
+
/**
|
|
26
|
+
* Full signed mandate — present ONLY for a delegate-signed envelope
|
|
27
|
+
* (§11.6). When present, `proof.verificationMethod` is the delegate's
|
|
28
|
+
* bare Ed25519 multibase (matching `mandate.grantee.pubkey`) and the
|
|
29
|
+
* server resolves the signer to that key after verifying the mandate.
|
|
30
|
+
* The field is part of the signed bytes (the signature commits to the
|
|
31
|
+
* delegation context), so it cannot be swapped out in transit.
|
|
32
|
+
*/
|
|
33
|
+
readonly mandate?: unknown;
|
|
34
|
+
readonly proof: {
|
|
35
|
+
readonly type: "Ed25519Signature2020";
|
|
36
|
+
readonly verificationMethod: string;
|
|
37
|
+
readonly created: string;
|
|
38
|
+
readonly proofValue: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface SignOwnerEnvelopeArgs {
|
|
42
|
+
/** Subject DID — issuer of the envelope (`iss` field). */
|
|
43
|
+
readonly iss: string;
|
|
44
|
+
/**
|
|
45
|
+
* Absolute URL of the target endpoint (scheme + host + path, no query,
|
|
46
|
+
* no fragment). The server verifier rejects envelopes where `aud`
|
|
47
|
+
* does not match the actual endpoint being called.
|
|
48
|
+
*/
|
|
49
|
+
readonly aud: string;
|
|
50
|
+
/** Fully-qualified JSON-RPC method name. */
|
|
51
|
+
readonly method: string;
|
|
52
|
+
/** Tool payload — what `params_hash` commits to. */
|
|
53
|
+
readonly params: unknown;
|
|
54
|
+
/**
|
|
55
|
+
* Anything that can produce an Ed25519 signature over a byte
|
|
56
|
+
* sequence. In practice: one of the four owner sphere signers
|
|
57
|
+
* loaded post sign-in, or an inline adapter wrapping a raw seed.
|
|
58
|
+
*/
|
|
59
|
+
readonly signer: EnvelopeSigner;
|
|
60
|
+
/**
|
|
61
|
+
* Verification method URL — typically `${did}#${sphere}`. The server
|
|
62
|
+
* resolves this against the issuer's DID document to find the
|
|
63
|
+
* matching public key.
|
|
64
|
+
*/
|
|
65
|
+
readonly verificationMethod: string;
|
|
66
|
+
/**
|
|
67
|
+
* Full signed mandate, attached to the envelope for a delegate-signed
|
|
68
|
+
* call (§11.6). When set, `verificationMethod` MUST be the delegate's
|
|
69
|
+
* bare Ed25519 multibase (matching `mandate.grantee.pubkey`) and
|
|
70
|
+
* `signer` MUST be the delegate's key. Omit for owner-path envelopes.
|
|
71
|
+
*/
|
|
72
|
+
readonly mandate?: unknown;
|
|
73
|
+
/** Envelope lifetime in seconds. Default 60. Server caps at 300. */
|
|
74
|
+
readonly ttlSeconds?: number;
|
|
75
|
+
/** Clock override for deterministic tests. Defaults to `new Date()`. */
|
|
76
|
+
readonly now?: Date;
|
|
77
|
+
/** Nonce override for deterministic tests. Defaults to a fresh ULID. */
|
|
78
|
+
readonly nonce?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build, canonicalize and sign an owner-path envelope per spec §11.2.
|
|
82
|
+
*
|
|
83
|
+
* The unsigned envelope is JCS-canonicalized (RFC 8785 subset), the
|
|
84
|
+
* bytes signed with Ed25519, and the resulting signature attached as
|
|
85
|
+
* `proof.proofValue` (base64url).
|
|
86
|
+
*
|
|
87
|
+
* Async return type — even though `Signer.sign` may resolve
|
|
88
|
+
* synchronously today (via `RawSeedSigner`), the interface is shaped
|
|
89
|
+
* async so that future implementations backed by `crypto.subtle.sign`
|
|
90
|
+
* (non-extractable keys) drop in without breaking callers.
|
|
91
|
+
*/
|
|
92
|
+
export declare function signOwnerEnvelope(args: SignOwnerEnvelopeArgs): Promise<SignedEnvelope>;
|
|
93
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* Aithos signed-envelope helper — SDK-internal.
|
|
5
|
+
*
|
|
6
|
+
* Implements the `aithos-envelope` v0.1.0 wire format from spec §11.2.
|
|
7
|
+
* Used by SDK namespaces (sdk.data, sdk.ethos, sdk.mandates, ...) to
|
|
8
|
+
* sign their writes to api.aithos.be, AND by the public method
|
|
9
|
+
* {@link AithosAuth.signEnvelope} so apps can sign envelopes for
|
|
10
|
+
* arbitrary third-party Aithos-aware backends with the same primitive.
|
|
11
|
+
*
|
|
12
|
+
* NOT exported from the package barrel. The corresponding `SignedEnvelope`
|
|
13
|
+
* type IS re-exported from `src/index.ts` for consumer typing.
|
|
14
|
+
*
|
|
15
|
+
* Reference algorithm: `@aithos/protocol-core/envelope.ts:signEnvelope`.
|
|
16
|
+
* This file duplicates a self-contained subset of the helpers (JCS,
|
|
17
|
+
* SHA-256, base64url, ULID, CSPRNG) to keep the SDK's signing path
|
|
18
|
+
* decoupled from internal changes elsewhere in the SDK. The duplication
|
|
19
|
+
* with `src/data.ts` is intentional and tracked; consolidation into a
|
|
20
|
+
* shared `src/internal/canonical.ts` is planned for a follow-up release.
|
|
21
|
+
*/
|
|
22
|
+
import { signEnvelopeWith } from "@aithos/protocol-core/envelope";
|
|
23
|
+
/**
|
|
24
|
+
* Build, canonicalize and sign an owner-path envelope per spec §11.2.
|
|
25
|
+
*
|
|
26
|
+
* The unsigned envelope is JCS-canonicalized (RFC 8785 subset), the
|
|
27
|
+
* bytes signed with Ed25519, and the resulting signature attached as
|
|
28
|
+
* `proof.proofValue` (base64url).
|
|
29
|
+
*
|
|
30
|
+
* Async return type — even though `Signer.sign` may resolve
|
|
31
|
+
* synchronously today (via `RawSeedSigner`), the interface is shaped
|
|
32
|
+
* async so that future implementations backed by `crypto.subtle.sign`
|
|
33
|
+
* (non-extractable keys) drop in without breaking callers.
|
|
34
|
+
*/
|
|
35
|
+
export async function signOwnerEnvelope(args) {
|
|
36
|
+
// Delegate to the single envelope source of truth in @aithos/protocol-core.
|
|
37
|
+
// The canonicalization, params_hash, unsigned-envelope assembly and proof
|
|
38
|
+
// attachment all live there now; we only supply the pluggable async signer
|
|
39
|
+
// (preserving the WebCrypto-ready `EnvelopeSigner` abstraction). A
|
|
40
|
+
// conformance test proves this path is byte-identical to the seed-based
|
|
41
|
+
// core signer, which is itself byte-identical to the former hand-rolled
|
|
42
|
+
// implementation — so nothing changes on the wire.
|
|
43
|
+
const env = await signEnvelopeWith({
|
|
44
|
+
iss: args.iss,
|
|
45
|
+
aud: args.aud,
|
|
46
|
+
method: args.method,
|
|
47
|
+
params: args.params,
|
|
48
|
+
verificationMethod: args.verificationMethod,
|
|
49
|
+
sign: (bytes) => args.signer.sign(bytes),
|
|
50
|
+
ttlSeconds: args.ttlSeconds,
|
|
51
|
+
now: args.now,
|
|
52
|
+
nonce: args.nonce,
|
|
53
|
+
...(args.mandate !== undefined
|
|
54
|
+
? { mandate: args.mandate }
|
|
55
|
+
: {}),
|
|
56
|
+
});
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -17,6 +17,8 @@ export declare class OwnerSigners {
|
|
|
17
17
|
readonly public: Signer;
|
|
18
18
|
readonly circle: Signer;
|
|
19
19
|
readonly self: Signer;
|
|
20
|
+
/** Optional dedicated #data sphere signer (absent on legacy owners). */
|
|
21
|
+
readonly data?: Signer;
|
|
20
22
|
constructor(args: {
|
|
21
23
|
did: string;
|
|
22
24
|
handle: string;
|
|
@@ -25,6 +27,7 @@ export declare class OwnerSigners {
|
|
|
25
27
|
public: Signer;
|
|
26
28
|
circle: Signer;
|
|
27
29
|
self: Signer;
|
|
30
|
+
data?: Signer;
|
|
28
31
|
});
|
|
29
32
|
/**
|
|
30
33
|
* Build from a {@link BrowserIdentity}. The seeds inside `identity`
|
|
@@ -59,7 +62,7 @@ export declare class OwnerSigners {
|
|
|
59
62
|
* have the sphere as a string ("public" | "circle" | "self") rather
|
|
60
63
|
* than a typed accessor.
|
|
61
64
|
*/
|
|
62
|
-
signerForSphere(sphere: "root" | "public" | "circle" | "self"): Signer;
|
|
65
|
+
signerForSphere(sphere: "root" | "public" | "circle" | "self" | "data"): Signer;
|
|
63
66
|
/**
|
|
64
67
|
* Internal — project to a {@link StoredIdentity} for protocol-client
|
|
65
68
|
* interop. Reads seed bytes via {@link RawSeedSigner._unsafeKeyPair}
|
|
@@ -71,7 +74,7 @@ export declare class OwnerSigners {
|
|
|
71
74
|
* @internal
|
|
72
75
|
*/
|
|
73
76
|
_unsafeStoredIdentity(): StoredIdentity;
|
|
74
|
-
/** Zeroize all
|
|
77
|
+
/** Zeroize all private seeds. Idempotent. */
|
|
75
78
|
destroy(): void;
|
|
76
79
|
get destroyed(): boolean;
|
|
77
80
|
}
|
|
@@ -34,6 +34,8 @@ export class OwnerSigners {
|
|
|
34
34
|
public;
|
|
35
35
|
circle;
|
|
36
36
|
self;
|
|
37
|
+
/** Optional dedicated #data sphere signer (absent on legacy owners). */
|
|
38
|
+
data;
|
|
37
39
|
#destroyed = false;
|
|
38
40
|
constructor(args) {
|
|
39
41
|
this.did = args.did;
|
|
@@ -43,6 +45,7 @@ export class OwnerSigners {
|
|
|
43
45
|
this.public = args.public;
|
|
44
46
|
this.circle = args.circle;
|
|
45
47
|
this.self = args.self;
|
|
48
|
+
this.data = args.data;
|
|
46
49
|
}
|
|
47
50
|
/**
|
|
48
51
|
* Build from a {@link BrowserIdentity}. The seeds inside `identity`
|
|
@@ -59,6 +62,11 @@ export class OwnerSigners {
|
|
|
59
62
|
public: new RawSeedSigner(identity.public.seed, identity.public.publicKey),
|
|
60
63
|
circle: new RawSeedSigner(identity.circle.seed, identity.circle.publicKey),
|
|
61
64
|
self: new RawSeedSigner(identity.self.seed, identity.self.publicKey),
|
|
65
|
+
...(identity.data
|
|
66
|
+
? {
|
|
67
|
+
data: new RawSeedSigner(identity.data.seed, identity.data.publicKey),
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
62
70
|
});
|
|
63
71
|
}
|
|
64
72
|
/**
|
|
@@ -120,6 +128,11 @@ export class OwnerSigners {
|
|
|
120
128
|
return this.circle;
|
|
121
129
|
case "self":
|
|
122
130
|
return this.self;
|
|
131
|
+
case "data":
|
|
132
|
+
if (!this.data) {
|
|
133
|
+
throw new Error("OwnerSigners: this owner has no #data sphere (legacy account)");
|
|
134
|
+
}
|
|
135
|
+
return this.data;
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
/**
|
|
@@ -146,11 +159,18 @@ export class OwnerSigners {
|
|
|
146
159
|
public: bytesToHexLocal(asRawSeed(this.public, "public")._unsafeKeyPair().seed),
|
|
147
160
|
circle: bytesToHexLocal(asRawSeed(this.circle, "circle")._unsafeKeyPair().seed),
|
|
148
161
|
self: bytesToHexLocal(asRawSeed(this.self, "self")._unsafeKeyPair().seed),
|
|
162
|
+
// Preserve the optional #data seed on vault re-encryption — dropping it
|
|
163
|
+
// here would silently lose the data sphere on the next delegate-add.
|
|
164
|
+
...(this.data
|
|
165
|
+
? {
|
|
166
|
+
data: bytesToHexLocal(asRawSeed(this.data, "data")._unsafeKeyPair().seed),
|
|
167
|
+
}
|
|
168
|
+
: {}),
|
|
149
169
|
},
|
|
150
170
|
savedAt: new Date().toISOString(),
|
|
151
171
|
};
|
|
152
172
|
}
|
|
153
|
-
/** Zeroize all
|
|
173
|
+
/** Zeroize all private seeds. Idempotent. */
|
|
154
174
|
destroy() {
|
|
155
175
|
if (this.#destroyed)
|
|
156
176
|
return;
|
|
@@ -158,6 +178,7 @@ export class OwnerSigners {
|
|
|
158
178
|
this.public.destroy();
|
|
159
179
|
this.circle.destroy();
|
|
160
180
|
this.self.destroy();
|
|
181
|
+
this.data?.destroy();
|
|
161
182
|
this.#destroyed = true;
|
|
162
183
|
}
|
|
163
184
|
get destroyed() {
|
|
@@ -44,6 +44,11 @@ export function parseRecoveryFile(text) {
|
|
|
44
44
|
throw bad(`seeds_hex.${k}: expected 64-char lowercase hex`);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
// #data is optional (absent on legacy recovery files); validate when present.
|
|
48
|
+
const dataSeed = seeds["data"];
|
|
49
|
+
if (dataSeed !== undefined && (typeof dataSeed !== "string" || !HEX_64.test(dataSeed))) {
|
|
50
|
+
throw bad("seeds_hex.data: expected 64-char lowercase hex");
|
|
51
|
+
}
|
|
47
52
|
return {
|
|
48
53
|
handle,
|
|
49
54
|
displayName,
|
|
@@ -53,6 +58,7 @@ export function parseRecoveryFile(text) {
|
|
|
53
58
|
public: seeds["public"],
|
|
54
59
|
circle: seeds["circle"],
|
|
55
60
|
self: seeds["self"],
|
|
61
|
+
...(typeof dataSeed === "string" ? { data: dataSeed } : {}),
|
|
56
62
|
},
|
|
57
63
|
};
|
|
58
64
|
}
|
|
@@ -78,6 +84,7 @@ export function serializeRecoveryFile(identity) {
|
|
|
78
84
|
public: bytesToHex(identity.public.seed),
|
|
79
85
|
circle: bytesToHex(identity.circle.seed),
|
|
80
86
|
self: bytesToHex(identity.self.seed),
|
|
87
|
+
...(identity.data ? { data: bytesToHex(identity.data.seed) } : {}),
|
|
81
88
|
},
|
|
82
89
|
saved_at: new Date().toISOString(),
|
|
83
90
|
};
|
package/dist/src/key-store.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export interface StoredOwnerKeys {
|
|
|
19
19
|
readonly public: string;
|
|
20
20
|
readonly circle: string;
|
|
21
21
|
readonly self: string;
|
|
22
|
+
/**
|
|
23
|
+
* Dedicated `#data` sphere seed (spec/data/02-key-hierarchy.md) — the key
|
|
24
|
+
* that signs ALL data/asset PDS operations (create, edit, manage
|
|
25
|
+
* collections), keeping the root cold. Present on owners created since the
|
|
26
|
+
* `#data` sphere landed. Absent only on legacy owners, which predate it and
|
|
27
|
+
* therefore have no dedicated PDS key — migrate them to `#data` (the backend
|
|
28
|
+
* does this lazily on next custodial sign-in; self-custody/SSO already carry
|
|
29
|
+
* it). Signing PDS ops under any other sphere is a legacy anti-pattern.
|
|
30
|
+
*/
|
|
31
|
+
readonly data?: string;
|
|
22
32
|
};
|
|
23
33
|
/** ISO-8601 timestamp of the original save. Informational. */
|
|
24
34
|
readonly savedAt: string;
|
package/dist/src/key-store.js
CHANGED
|
@@ -213,6 +213,12 @@ function validOwnerOrNull(v) {
|
|
|
213
213
|
if (s[k].length !== 64)
|
|
214
214
|
return null;
|
|
215
215
|
}
|
|
216
|
+
// #data is optional (absent on legacy owners); validate only when present.
|
|
217
|
+
if (s["data"] !== undefined) {
|
|
218
|
+
if (typeof s["data"] !== "string" || s["data"].length !== 64) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
216
222
|
if (typeof o["savedAt"] !== "string")
|
|
217
223
|
return null;
|
|
218
224
|
return o;
|
package/dist/src/mandates.d.ts
CHANGED
|
@@ -8,7 +8,52 @@ import type { AithosSdkEndpoints } from "./endpoints.js";
|
|
|
8
8
|
* directly in `scopes` is rejected at runtime; the compiler can't enforce
|
|
9
9
|
* it (callers who up-cast to string[] would slip through), so the runtime
|
|
10
10
|
* check is the real gate. */
|
|
11
|
-
|
|
11
|
+
/** An ethos verb (draft `bundle-v0.3-section-verb-scopes.md` §4.8.2′). */
|
|
12
|
+
export type EthosVerb = "read" | "edit" | "append" | "delete" | "write";
|
|
13
|
+
/** A writable ethos zone. */
|
|
14
|
+
export type EthosZone = "public" | "circle" | "self";
|
|
15
|
+
/**
|
|
16
|
+
* An ethos scope: a whole-zone grant (`ethos.edit.self`) or one narrowed to a
|
|
17
|
+
* subset of sections by a per-scope selector (`ethos.edit.self#id=X`,
|
|
18
|
+
* `ethos.append.self#prefix=gmail:`, `ethos.read.circle#tag=bio`). Plus the
|
|
19
|
+
* legacy whole-everything read `ethos.read.all`. §4.8′.
|
|
20
|
+
*/
|
|
21
|
+
export type EthosScope = `ethos.${EthosVerb}.${EthosZone}` | `ethos.${EthosVerb}.${EthosZone}#${string}` | "ethos.read.all";
|
|
22
|
+
export type Scope = EthosScope | DataScope;
|
|
23
|
+
/** Action a data mandate may authorize on a collection. `write` implies
|
|
24
|
+
* `read`; `admin` implies `write`. Mirrors the data sub-protocol grammar
|
|
25
|
+
* `data.<collection>.<action>` (Aithos-protocol `spec/data/04-mandates.md`
|
|
26
|
+
* §4.2) and the server-side check `requireScope` in data-backend. */
|
|
27
|
+
export type DataAction = "read" | "write" | "admin";
|
|
28
|
+
/**
|
|
29
|
+
* A **lateral** data capability — deliberately OUTSIDE the
|
|
30
|
+
* `read ⊂ write ⊂ admin` hierarchy (the same way `gamma.write` sits beside
|
|
31
|
+
* the ethos scopes). Keeping it a separate type makes the security invariant
|
|
32
|
+
* structural rather than conventional: `append` can never be reached by
|
|
33
|
+
* widening a `write`/`admin` scope, so it cannot accidentally carry read.
|
|
34
|
+
*
|
|
35
|
+
* `append` authorizes `insert_record` ONLY (no read, update, or delete). The
|
|
36
|
+
* depositor seals each record's DEK to the owner's public key
|
|
37
|
+
* ({@link createAppendDataClient}) and holds no read capability — it cannot
|
|
38
|
+
* decrypt anything in the collection, not even its own deposit.
|
|
39
|
+
*/
|
|
40
|
+
export type DataLateralAction = "append";
|
|
41
|
+
/**
|
|
42
|
+
* A data-access scope: `data.<collection>.<action>`, or the cross-collection
|
|
43
|
+
* wildcard `data.*.<action>`. Examples: `data.contacts.read`,
|
|
44
|
+
* `data.depots.write`, `data.*.read`.
|
|
45
|
+
*
|
|
46
|
+
* Note on `actor_sphere`: data mandates are minted under `actor_sphere:
|
|
47
|
+
* "self"` (the owner's highest-authority sphere). The sphere is *not* the
|
|
48
|
+
* access axis for data — the collection is. `actor_sphere` is informative
|
|
49
|
+
* here; the cryptographic binding is the grantee's key + the CMK wrap, per
|
|
50
|
+
* spec §4.4. A dedicated `#data` sphere key (independent rotation) MAY be
|
|
51
|
+
* introduced later without changing this scope grammar.
|
|
52
|
+
*
|
|
53
|
+
* Collection names MUST NOT contain `.` (the server splits the scope on
|
|
54
|
+
* `.` and reads the first three segments).
|
|
55
|
+
*/
|
|
56
|
+
export type DataScope = `data.${string}.${DataAction}` | `data.${string}.${DataLateralAction}`;
|
|
12
57
|
/**
|
|
13
58
|
* The opt-in scope that authorizes a delegate to spend the subject's
|
|
14
59
|
* compute credits via the Aithos compute proxy. Mirror of
|
|
@@ -86,6 +131,18 @@ export interface CreateMandateInput {
|
|
|
86
131
|
* which is what a consent UI can review.
|
|
87
132
|
*/
|
|
88
133
|
readonly compute?: CreateMandateComputeInput;
|
|
134
|
+
/**
|
|
135
|
+
* When the mandate becomes valid. Optional — when omitted, the
|
|
136
|
+
* underlying mint helper signs with `not_before = now - 30s` (see
|
|
137
|
+
* `MANDATE_NOTBEFORE_OFFSET_SECONDS_DEFAULT` in
|
|
138
|
+
* `@aithos/protocol-client`) so a server whose clock runs slightly
|
|
139
|
+
* behind the client doesn't reject the freshly-minted mandate as
|
|
140
|
+
* `not yet valid`.
|
|
141
|
+
*
|
|
142
|
+
* Pass an explicit `Date` only for advanced flows (delayed-activation
|
|
143
|
+
* mandates, deterministic tests).
|
|
144
|
+
*/
|
|
145
|
+
readonly notBefore?: Date;
|
|
89
146
|
}
|
|
90
147
|
export interface MintedMandate {
|
|
91
148
|
/** Unique mandate id (matches `mandate.id` inside the bundle). */
|
package/dist/src/mandates.js
CHANGED
|
@@ -64,6 +64,17 @@ export class MandatesNamespace {
|
|
|
64
64
|
`not by adding "${COMPUTE_INVOKE_SCOPE}" to scopes[]. The namespace forces ` +
|
|
65
65
|
`an explicit budget and is what a consent UI reviews.`);
|
|
66
66
|
}
|
|
67
|
+
// Fail fast on malformed data scopes so the misuse surfaces at the SDK
|
|
68
|
+
// boundary, not as an opaque server rejection at first delegate call.
|
|
69
|
+
// Accepts `data.<collection>.<action>` and the wildcard `data.*.<action>`.
|
|
70
|
+
for (const s of input.scopes) {
|
|
71
|
+
if (s.startsWith("data.") &&
|
|
72
|
+
!isWellFormedDataScope(s)) {
|
|
73
|
+
throw new AithosSDKError("mandates_invalid_scopes", `Malformed data scope "${s}". Expected data.<collection>.<action> ` +
|
|
74
|
+
`(action = read | write | admin), e.g. "data.contacts.read" or ` +
|
|
75
|
+
`"data.*.read". Collection names must not contain ".".`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
67
78
|
// Validate + project the compute namespace if present, then derive
|
|
68
79
|
// the final scopes/constraints to send to the protocol layer.
|
|
69
80
|
const computeProjection = projectCompute(input.compute);
|
|
@@ -92,6 +103,7 @@ export class MandatesNamespace {
|
|
|
92
103
|
},
|
|
93
104
|
}
|
|
94
105
|
: {}),
|
|
106
|
+
...(input.notBefore ? { notBefore: input.notBefore } : {}),
|
|
95
107
|
});
|
|
96
108
|
const mandate = result.mandate;
|
|
97
109
|
return {
|
|
@@ -206,11 +218,42 @@ export class MandatesNamespace {
|
|
|
206
218
|
/* Helpers */
|
|
207
219
|
/* -------------------------------------------------------------------------- */
|
|
208
220
|
function defaultSphereFromScopes(scopes) {
|
|
209
|
-
|
|
221
|
+
// Data scopes are sphere-NEUTRAL: they're permitted under self & circle (and
|
|
222
|
+
// public once the allowlist includes them), and the data access axis is the
|
|
223
|
+
// collection, not the sphere (the sphere is informative — see {@link DataScope}).
|
|
224
|
+
// So the actor_sphere of a combined Ethos+data mandate is decided by the
|
|
225
|
+
// ETHOS scopes alone; data scopes neither raise nor lower it. This makes
|
|
226
|
+
// `ethos.read.public + data.X.read` default to `public`, `ethos.read.circle
|
|
227
|
+
// + data.X.read` to `circle`, etc. A caller can always override via
|
|
228
|
+
// `actorSphere`.
|
|
229
|
+
const ethos = scopes.filter((s) => !s.startsWith("data."));
|
|
230
|
+
// Ethos write scopes pin the sphere EXACTLY (a write mandate must be signed
|
|
231
|
+
// by the sphere it writes to — `validateScopesAgainstSphere`).
|
|
232
|
+
if (ethos.some((s) => s === "ethos.write.public"))
|
|
233
|
+
return "public";
|
|
234
|
+
if (ethos.some((s) => s === "ethos.write.circle"))
|
|
235
|
+
return "circle";
|
|
236
|
+
if (ethos.some((s) => s === "ethos.write.self"))
|
|
210
237
|
return "self";
|
|
211
|
-
|
|
238
|
+
// Ethos read scopes: narrowest sphere that permits them.
|
|
239
|
+
if (ethos.some((s) => s.endsWith(".self")))
|
|
240
|
+
return "self";
|
|
241
|
+
if (ethos.some((s) => s.endsWith(".circle")))
|
|
212
242
|
return "circle";
|
|
213
|
-
|
|
243
|
+
// Remaining Ethos scopes (ethos.read.public / .all / gamma.read) → public.
|
|
244
|
+
if (ethos.length > 0)
|
|
245
|
+
return "public";
|
|
246
|
+
// Data-only mandate: sign under `self`. self is the owner's highest-trust
|
|
247
|
+
// sphere, always permits the scope at mint time (no dependency on the
|
|
248
|
+
// public-sphere allowlist), and keeps the data grant off the most-exposed
|
|
249
|
+
// #public key. (Override with `actorSphere` for a public/circle label.)
|
|
250
|
+
return "self";
|
|
251
|
+
}
|
|
252
|
+
/** `true` iff `s` is a well-formed data scope `data.<collection>.<action>`
|
|
253
|
+
* with no filter suffix and a non-empty, dot-free collection name. The
|
|
254
|
+
* lateral `append` action is accepted alongside read/write/admin. */
|
|
255
|
+
function isWellFormedDataScope(s) {
|
|
256
|
+
return /^data\.[^.]+\.(read|write|admin|append)$/.test(s);
|
|
214
257
|
}
|
|
215
258
|
/**
|
|
216
259
|
* Validate the SDK-side `compute` namespace and project it onto the
|