@aithos/sdk 0.1.0-alpha.5 → 0.1.0-alpha.50
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 +245 -7
- package/dist/src/apps.d.ts +224 -0
- package/dist/src/apps.js +432 -0
- package/dist/src/assets.d.ts +208 -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 +543 -0
- package/dist/src/auth.js +937 -31
- package/dist/src/compute.d.ts +464 -6
- package/dist/src/compute.js +746 -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 +342 -0
- package/dist/src/data.js +1002 -0
- package/dist/src/endpoints.d.ts +18 -0
- package/dist/src/endpoints.js +6 -0
- package/dist/src/ethos.d.ts +85 -0
- package/dist/src/ethos.js +463 -7
- package/dist/src/index.d.ts +17 -6
- package/dist/src/index.js +25 -3
- 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/mandates.d.ts +111 -2
- package/dist/src/mandates.js +150 -7
- 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/sdk.d.ts +10 -0
- package/dist/src/sdk.js +22 -0
- 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/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/endpoints.test.js +25 -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/mandates-compute.test.d.ts +2 -0
- package/dist/test/mandates-compute.test.js +256 -0
- package/dist/test/sdk.test.js +10 -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,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
|
package/dist/src/mandates.d.ts
CHANGED
|
@@ -1,12 +1,99 @@
|
|
|
1
1
|
import type { AithosAuth } from "./auth.js";
|
|
2
2
|
import type { AithosSdkEndpoints } from "./endpoints.js";
|
|
3
|
-
/** Capability scope the SDK accepts. Server-side ultimately decides.
|
|
4
|
-
|
|
3
|
+
/** Capability scope the SDK accepts. Server-side ultimately decides.
|
|
4
|
+
*
|
|
5
|
+
* Note: `compute.invoke` is intentionally NOT in this union. The token-
|
|
6
|
+
* spending capability is opt-in via the dedicated {@link CreateMandateInput.compute}
|
|
7
|
+
* namespace — see {@link MandatesNamespace.create}. Passing `compute.invoke`
|
|
8
|
+
* directly in `scopes` is rejected at runtime; the compiler can't enforce
|
|
9
|
+
* it (callers who up-cast to string[] would slip through), so the runtime
|
|
10
|
+
* check is the real gate. */
|
|
11
|
+
export type Scope = "ethos.read.public" | "ethos.read.circle" | "ethos.read.self" | "ethos.write.public" | "ethos.write.circle" | "ethos.write.self" | DataScope;
|
|
12
|
+
/** Action a data mandate may authorize on a collection. `write` implies
|
|
13
|
+
* `read`; `admin` implies `write`. Mirrors the data sub-protocol grammar
|
|
14
|
+
* `data.<collection>.<action>` (Aithos-protocol `spec/data/04-mandates.md`
|
|
15
|
+
* §4.2) and the server-side check `requireScope` in data-backend. */
|
|
16
|
+
export type DataAction = "read" | "write" | "admin";
|
|
17
|
+
/**
|
|
18
|
+
* A **lateral** data capability — deliberately OUTSIDE the
|
|
19
|
+
* `read ⊂ write ⊂ admin` hierarchy (the same way `gamma.write` sits beside
|
|
20
|
+
* the ethos scopes). Keeping it a separate type makes the security invariant
|
|
21
|
+
* structural rather than conventional: `append` can never be reached by
|
|
22
|
+
* widening a `write`/`admin` scope, so it cannot accidentally carry read.
|
|
23
|
+
*
|
|
24
|
+
* `append` authorizes `insert_record` ONLY (no read, update, or delete). The
|
|
25
|
+
* depositor seals each record's DEK to the owner's public key
|
|
26
|
+
* ({@link createAppendDataClient}) and holds no read capability — it cannot
|
|
27
|
+
* decrypt anything in the collection, not even its own deposit.
|
|
28
|
+
*/
|
|
29
|
+
export type DataLateralAction = "append";
|
|
30
|
+
/**
|
|
31
|
+
* A data-access scope: `data.<collection>.<action>`, or the cross-collection
|
|
32
|
+
* wildcard `data.*.<action>`. Examples: `data.contacts.read`,
|
|
33
|
+
* `data.depots.write`, `data.*.read`.
|
|
34
|
+
*
|
|
35
|
+
* Note on `actor_sphere`: data mandates are minted under `actor_sphere:
|
|
36
|
+
* "self"` (the owner's highest-authority sphere). The sphere is *not* the
|
|
37
|
+
* access axis for data — the collection is. `actor_sphere` is informative
|
|
38
|
+
* here; the cryptographic binding is the grantee's key + the CMK wrap, per
|
|
39
|
+
* spec §4.4. A dedicated `#data` sphere key (independent rotation) MAY be
|
|
40
|
+
* introduced later without changing this scope grammar.
|
|
41
|
+
*
|
|
42
|
+
* Collection names MUST NOT contain `.` (the server splits the scope on
|
|
43
|
+
* `.` and reads the first three segments).
|
|
44
|
+
*/
|
|
45
|
+
export type DataScope = `data.${string}.${DataAction}` | `data.${string}.${DataLateralAction}`;
|
|
46
|
+
/**
|
|
47
|
+
* The opt-in scope that authorizes a delegate to spend the subject's
|
|
48
|
+
* compute credits via the Aithos compute proxy. Mirror of
|
|
49
|
+
* `COMPUTE_INVOKE_SCOPE` in `@aithos/protocol-core` v0.4.0.
|
|
50
|
+
*
|
|
51
|
+
* The SDK's `mandates.create()` injects this scope automatically when
|
|
52
|
+
* the caller passes a `compute` namespace, and refuses to mint a
|
|
53
|
+
* mandate where the caller put it directly into `scopes` — this is
|
|
54
|
+
* what makes "compute is a separate, conscious decision" hold at the
|
|
55
|
+
* API surface.
|
|
56
|
+
*/
|
|
57
|
+
export declare const COMPUTE_INVOKE_SCOPE: "compute.invoke";
|
|
5
58
|
/**
|
|
6
59
|
* Which sphere of the owner signs the mandate. Bounds the upper-most
|
|
7
60
|
* scope set the mandate can carry.
|
|
8
61
|
*/
|
|
9
62
|
export type ActorSphere = "public" | "circle" | "self";
|
|
63
|
+
/**
|
|
64
|
+
* Compute-spending capability — opt-in only, never implied by ethos
|
|
65
|
+
* scopes.
|
|
66
|
+
*
|
|
67
|
+
* When `compute` is set on {@link CreateMandateInput}, the SDK:
|
|
68
|
+
* 1. Adds the `compute.invoke` scope to the minted mandate.
|
|
69
|
+
* 2. Maps the caller's caps onto `constraints.compute` in the
|
|
70
|
+
* protocol's snake_case shape (= what the verifier reads).
|
|
71
|
+
* 3. Forbids the caller from passing `compute.invoke` in `scopes`
|
|
72
|
+
* directly — that would let an app slip the scope past a
|
|
73
|
+
* consent UI that only reviews `compute`.
|
|
74
|
+
*
|
|
75
|
+
* At least one of `dailyCapMicrocredits` or `totalCapMicrocredits` MUST
|
|
76
|
+
* be set: an unbounded compute mandate is the kind of bearer-token
|
|
77
|
+
* footgun this whole namespace exists to prevent. Validation happens
|
|
78
|
+
* at the SDK boundary (here) AND at the protocol layer (the
|
|
79
|
+
* server-side verifier rejects capless 0.4.0 mandates), so a bug in
|
|
80
|
+
* either tier still fails closed.
|
|
81
|
+
*
|
|
82
|
+
* `maxCreditsPerCall` is a per-invocation safety net for runaway
|
|
83
|
+
* single requests. `allowedModels`, when set, restricts which Bedrock
|
|
84
|
+
* model ids the delegate may target (the proxy's own allowlist still
|
|
85
|
+
* applies on top).
|
|
86
|
+
*/
|
|
87
|
+
export interface CreateMandateComputeInput {
|
|
88
|
+
/** Hard cap on credits debited per UTC day under this mandate. */
|
|
89
|
+
readonly dailyCapMicrocredits?: number;
|
|
90
|
+
/** Hard cap on credits debited over the whole mandate lifetime. */
|
|
91
|
+
readonly totalCapMicrocredits?: number;
|
|
92
|
+
/** Hard cap on credits debited by any single invocation. */
|
|
93
|
+
readonly maxCreditsPerCall?: number;
|
|
94
|
+
/** Allowlist of Bedrock model ids the delegate may invoke. */
|
|
95
|
+
readonly allowedModels?: readonly string[];
|
|
96
|
+
}
|
|
10
97
|
export interface CreateMandateInput {
|
|
11
98
|
/** Grantee URN — usually `urn:aithos:agent:<extension-id>` or similar. */
|
|
12
99
|
readonly granteeId: string;
|
|
@@ -23,6 +110,28 @@ export interface CreateMandateInput {
|
|
|
23
110
|
readonly scopes: readonly Scope[];
|
|
24
111
|
/** Lifetime in seconds. */
|
|
25
112
|
readonly ttlSeconds: number;
|
|
113
|
+
/**
|
|
114
|
+
* Opt-in compute (token-spending) capability — adds the
|
|
115
|
+
* `compute.invoke` scope and a bounded `constraints.compute` budget
|
|
116
|
+
* to the mandate. See {@link CreateMandateComputeInput}.
|
|
117
|
+
*
|
|
118
|
+
* NEVER add `compute.invoke` to `scopes` directly — the SDK rejects
|
|
119
|
+
* that path so the caller has to pass through this typed namespace,
|
|
120
|
+
* which is what a consent UI can review.
|
|
121
|
+
*/
|
|
122
|
+
readonly compute?: CreateMandateComputeInput;
|
|
123
|
+
/**
|
|
124
|
+
* When the mandate becomes valid. Optional — when omitted, the
|
|
125
|
+
* underlying mint helper signs with `not_before = now - 30s` (see
|
|
126
|
+
* `MANDATE_NOTBEFORE_OFFSET_SECONDS_DEFAULT` in
|
|
127
|
+
* `@aithos/protocol-client`) so a server whose clock runs slightly
|
|
128
|
+
* behind the client doesn't reject the freshly-minted mandate as
|
|
129
|
+
* `not yet valid`.
|
|
130
|
+
*
|
|
131
|
+
* Pass an explicit `Date` only for advanced flows (delayed-activation
|
|
132
|
+
* mandates, deterministic tests).
|
|
133
|
+
*/
|
|
134
|
+
readonly notBefore?: Date;
|
|
26
135
|
}
|
|
27
136
|
export interface MintedMandate {
|
|
28
137
|
/** Unique mandate id (matches `mandate.id` inside the bundle). */
|
package/dist/src/mandates.js
CHANGED
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
import { buildSignedEnvelope, mintDelegateBundle, readRpc, } from "@aithos/protocol-client";
|
|
17
17
|
import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
|
|
18
18
|
import { AithosSDKError } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* The opt-in scope that authorizes a delegate to spend the subject's
|
|
21
|
+
* compute credits via the Aithos compute proxy. Mirror of
|
|
22
|
+
* `COMPUTE_INVOKE_SCOPE` in `@aithos/protocol-core` v0.4.0.
|
|
23
|
+
*
|
|
24
|
+
* The SDK's `mandates.create()` injects this scope automatically when
|
|
25
|
+
* the caller passes a `compute` namespace, and refuses to mint a
|
|
26
|
+
* mandate where the caller put it directly into `scopes` — this is
|
|
27
|
+
* what makes "compute is a separate, conscious decision" hold at the
|
|
28
|
+
* API surface.
|
|
29
|
+
*/
|
|
30
|
+
export const COMPUTE_INVOKE_SCOPE = "compute.invoke";
|
|
19
31
|
export class MandatesNamespace {
|
|
20
32
|
#deps;
|
|
21
33
|
constructor(deps) {
|
|
@@ -29,12 +41,47 @@ export class MandatesNamespace {
|
|
|
29
41
|
*/
|
|
30
42
|
async create(input) {
|
|
31
43
|
const owner = this.#requireOwner();
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
// A mandate must carry at least one capability — either ethos
|
|
45
|
+
// scopes, the compute namespace, or both. A "compute-only" mandate
|
|
46
|
+
// (`scopes: []` + `compute: { ... }`) is legitimate: it gives the
|
|
47
|
+
// grantee no access to the subject's ethos data, only the right
|
|
48
|
+
// to spend a bounded amount of compute credits in their name.
|
|
49
|
+
// Useful for creative assistants, brainstorming agents, etc.
|
|
50
|
+
if (input.scopes.length === 0 && input.compute === undefined) {
|
|
51
|
+
throw new AithosSDKError("mandates_invalid_scopes", "scopes must be a non-empty list (or pass `compute` for a compute-only mandate)");
|
|
34
52
|
}
|
|
35
53
|
if (input.ttlSeconds <= 0) {
|
|
36
54
|
throw new AithosSDKError("mandates_invalid_ttl", "ttlSeconds must be > 0");
|
|
37
55
|
}
|
|
56
|
+
// Forbid `compute.invoke` smuggled in via `scopes[]`. The whole
|
|
57
|
+
// point of the dedicated `compute` namespace is that adding token-
|
|
58
|
+
// spending capability requires a typed, named, reviewable input —
|
|
59
|
+
// not a string lost in a generic list. Type-checking can't catch
|
|
60
|
+
// this (the union doesn't include the literal, but callers can
|
|
61
|
+
// up-cast); the runtime check is what holds.
|
|
62
|
+
if (input.scopes.some((s) => s === COMPUTE_INVOKE_SCOPE)) {
|
|
63
|
+
throw new AithosSDKError("mandates_invalid_scopes", `Pass token-spending capability via the dedicated 'compute' namespace, ` +
|
|
64
|
+
`not by adding "${COMPUTE_INVOKE_SCOPE}" to scopes[]. The namespace forces ` +
|
|
65
|
+
`an explicit budget and is what a consent UI reviews.`);
|
|
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
|
+
}
|
|
78
|
+
// Validate + project the compute namespace if present, then derive
|
|
79
|
+
// the final scopes/constraints to send to the protocol layer.
|
|
80
|
+
const computeProjection = projectCompute(input.compute);
|
|
81
|
+
const projectedScopes = [...input.scopes];
|
|
82
|
+
if (computeProjection) {
|
|
83
|
+
projectedScopes.push(COMPUTE_INVOKE_SCOPE);
|
|
84
|
+
}
|
|
38
85
|
const actorSphere = input.actorSphere ?? defaultSphereFromScopes(input.scopes);
|
|
39
86
|
const ownerStored = owner._unsafeStoredIdentity();
|
|
40
87
|
const result = await mintDelegateBundle({
|
|
@@ -42,15 +89,28 @@ export class MandatesNamespace {
|
|
|
42
89
|
granteeId: input.granteeId,
|
|
43
90
|
...(input.granteeLabel ? { granteeLabel: input.granteeLabel } : {}),
|
|
44
91
|
actorSphere,
|
|
45
|
-
scopes:
|
|
92
|
+
scopes: projectedScopes,
|
|
46
93
|
ttlSeconds: input.ttlSeconds,
|
|
94
|
+
// protocol-client v0.1.0-alpha.11 ships MandateConstraints without
|
|
95
|
+
// the `compute` field; the wire format accepts it though (the
|
|
96
|
+
// canonicalizer just serializes whatever's in the object). We
|
|
97
|
+
// up-cast through `unknown` to bypass the structural check until
|
|
98
|
+
// protocol-client picks up protocol-core 0.4.0 types.
|
|
99
|
+
...(computeProjection
|
|
100
|
+
? {
|
|
101
|
+
constraints: {
|
|
102
|
+
compute: computeProjection,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
...(input.notBefore ? { notBefore: input.notBefore } : {}),
|
|
47
107
|
});
|
|
48
108
|
const mandate = result.mandate;
|
|
49
109
|
return {
|
|
50
110
|
mandateId: mandate.id,
|
|
51
111
|
subjectDid: mandate.subject_did,
|
|
52
112
|
granteeId: input.granteeId,
|
|
53
|
-
scopes:
|
|
113
|
+
scopes: projectedScopes,
|
|
54
114
|
expiresAt: mandate.not_after ?? null,
|
|
55
115
|
bundle: result.bundleBlob,
|
|
56
116
|
filename: `aithos-delegate-${mandate.id}.json`,
|
|
@@ -158,11 +218,94 @@ export class MandatesNamespace {
|
|
|
158
218
|
/* Helpers */
|
|
159
219
|
/* -------------------------------------------------------------------------- */
|
|
160
220
|
function defaultSphereFromScopes(scopes) {
|
|
161
|
-
|
|
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"))
|
|
237
|
+
return "self";
|
|
238
|
+
// Ethos read scopes: narrowest sphere that permits them.
|
|
239
|
+
if (ethos.some((s) => s.endsWith(".self")))
|
|
162
240
|
return "self";
|
|
163
|
-
if (
|
|
241
|
+
if (ethos.some((s) => s.endsWith(".circle")))
|
|
164
242
|
return "circle";
|
|
165
|
-
|
|
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);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Validate the SDK-side `compute` namespace and project it onto the
|
|
260
|
+
* snake_case shape the protocol layer canonicalizes into the mandate.
|
|
261
|
+
*
|
|
262
|
+
* Returns `null` if the caller passed nothing — that's the "no compute
|
|
263
|
+
* authorization" path. Throws {@link AithosSDKError} on any structural
|
|
264
|
+
* problem (camelCase mirror of the rules `validateComputeAuthorization`
|
|
265
|
+
* enforces server-side; we duplicate them here so a misuse fails at the
|
|
266
|
+
* SDK boundary with a precise error rather than only blowing up at mint).
|
|
267
|
+
*/
|
|
268
|
+
function projectCompute(c) {
|
|
269
|
+
if (c === undefined)
|
|
270
|
+
return null;
|
|
271
|
+
const hasDaily = typeof c.dailyCapMicrocredits === "number";
|
|
272
|
+
const hasTotal = typeof c.totalCapMicrocredits === "number";
|
|
273
|
+
if (!hasDaily && !hasTotal) {
|
|
274
|
+
throw new AithosSDKError("mandates_invalid_compute", "compute namespace requires at least one of dailyCapMicrocredits " +
|
|
275
|
+
"or totalCapMicrocredits — an unbounded compute mandate is a bearer " +
|
|
276
|
+
"token to drain the subject's wallet.");
|
|
277
|
+
}
|
|
278
|
+
for (const [field, value] of [
|
|
279
|
+
["dailyCapMicrocredits", c.dailyCapMicrocredits],
|
|
280
|
+
["totalCapMicrocredits", c.totalCapMicrocredits],
|
|
281
|
+
["maxCreditsPerCall", c.maxCreditsPerCall],
|
|
282
|
+
]) {
|
|
283
|
+
if (value === undefined)
|
|
284
|
+
continue;
|
|
285
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
286
|
+
throw new AithosSDKError("mandates_invalid_compute", `compute.${field} must be a positive integer (got ${value}).`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (c.allowedModels !== undefined) {
|
|
290
|
+
if (!Array.isArray(c.allowedModels)) {
|
|
291
|
+
throw new AithosSDKError("mandates_invalid_compute", "compute.allowedModels must be an array of strings.");
|
|
292
|
+
}
|
|
293
|
+
for (const m of c.allowedModels) {
|
|
294
|
+
if (typeof m !== "string" || m.length === 0) {
|
|
295
|
+
throw new AithosSDKError("mandates_invalid_compute", `compute.allowedModels entries must be non-empty strings (got ${JSON.stringify(m)}).`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const wire = {};
|
|
300
|
+
if (hasDaily)
|
|
301
|
+
wire.daily_cap_microcredits = c.dailyCapMicrocredits;
|
|
302
|
+
if (hasTotal)
|
|
303
|
+
wire.total_cap_microcredits = c.totalCapMicrocredits;
|
|
304
|
+
if (c.maxCreditsPerCall !== undefined)
|
|
305
|
+
wire.max_credits_per_call = c.maxCreditsPerCall;
|
|
306
|
+
if (c.allowedModels !== undefined)
|
|
307
|
+
wire.allowed_models = [...c.allowedModels];
|
|
308
|
+
return wire;
|
|
166
309
|
}
|
|
167
310
|
function toOwnedMandate(it) {
|
|
168
311
|
return {
|
|
@@ -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
|