@aithos/sdk 0.1.0-alpha.5 → 0.1.0-alpha.6
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 +45 -0
- package/dist/src/auth.d.ts +11 -0
- package/dist/src/auth.js +95 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +2 -2
- package/dist/src/mandates.d.ts +64 -1
- package/dist/src/mandates.js +104 -4
- package/dist/test/mandates-compute.test.d.ts +2 -0
- package/dist/test/mandates-compute.test.js +256 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,51 @@ const reply = await sdk.compute.invokeBedrock({
|
|
|
55
55
|
console.log(reply.content);
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
## Delegating compute to an agent — opt-in token spending
|
|
59
|
+
|
|
60
|
+
To let an agent (or another user, or a third-party app) invoke Bedrock
|
|
61
|
+
**in your name**, with **your credits**, you mint a mandate. Token
|
|
62
|
+
spending is its own opt-in capability — passing it is a separate,
|
|
63
|
+
named, validated input that a consent UI can review. It is NEVER an
|
|
64
|
+
implicit side-effect of an ethos read/write scope.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// Mint a mandate that lets agent Bob read your public ethos AND
|
|
68
|
+
// spend up to 5 000 microcredits/day on Haiku, capped at 100 000
|
|
69
|
+
// microcredits over the whole mandate lifetime.
|
|
70
|
+
const mandate = await sdk.mandates.create({
|
|
71
|
+
granteeId: "urn:agent:bob",
|
|
72
|
+
scopes: ["ethos.read.public"],
|
|
73
|
+
ttlSeconds: 86_400,
|
|
74
|
+
compute: {
|
|
75
|
+
dailyCapMicrocredits: 5_000,
|
|
76
|
+
totalCapMicrocredits: 100_000,
|
|
77
|
+
maxCreditsPerCall: 500,
|
|
78
|
+
allowedModels: ["claude-haiku-4-5"],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Hand `mandate.bundle` (a `.aithos-delegate.json` Blob) to Bob.
|
|
83
|
+
// He imports it, then signs his own envelopes and calls
|
|
84
|
+
// sdk.compute.invokeBedrock({ mandateId: mandate.mandateId, … })
|
|
85
|
+
// — every invocation debits *your* wallet, capped per the budget
|
|
86
|
+
// you set.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Three invariants the SDK enforces synchronously, before reaching the
|
|
90
|
+
network — they fail fast with a precise `AithosSDKError`:
|
|
91
|
+
|
|
92
|
+
- **No smuggling.** Adding `"compute.invoke"` directly to `scopes[]`
|
|
93
|
+
throws `mandates_invalid_scopes`. The `compute` namespace is the
|
|
94
|
+
only path, so a UI reviewing `compute` can never be bypassed.
|
|
95
|
+
- **No bearer compute.** A `compute` namespace without at least one
|
|
96
|
+
of `dailyCapMicrocredits` or `totalCapMicrocredits` throws
|
|
97
|
+
`mandates_invalid_compute`. Unbounded compute mandates are forbidden
|
|
98
|
+
by construction.
|
|
99
|
+
- **Compute-only is fine.** `scopes: []` is allowed when `compute` is
|
|
100
|
+
set — useful for agents that only consume tokens (e.g. creative
|
|
101
|
+
assistants) without seeing any of your data.
|
|
102
|
+
|
|
58
103
|
## What lives where
|
|
59
104
|
|
|
60
105
|
| Namespace | Purpose |
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -4,8 +4,18 @@ import { DelegateActor } from "./internal/delegate-state.js";
|
|
|
4
4
|
import { OwnerSigners } from "./internal/owner-signers.js";
|
|
5
5
|
/** Default URL of the Aithos auth backend. */
|
|
6
6
|
export declare const DEFAULT_AUTH_BASE_URL = "https://auth.aithos.be";
|
|
7
|
+
/** Default URL of the Aithos primitives API (publish_identity, publish_ethos_edition, etc.). */
|
|
8
|
+
export declare const DEFAULT_API_BASE_URL = "https://api.aithos.be";
|
|
7
9
|
export interface AithosAuthConfig {
|
|
8
10
|
readonly authBaseUrl?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Base URL of the Aithos primitives API (`api.aithos.be`). Used by
|
|
13
|
+
* {@link AithosAuth.signUp} to bootstrap the user's Ethos via
|
|
14
|
+
* `aithos.publish_identity` after the auth account is created. Override
|
|
15
|
+
* for staging or self-hosted deployments. Defaults to
|
|
16
|
+
* {@link DEFAULT_API_BASE_URL}.
|
|
17
|
+
*/
|
|
18
|
+
readonly apiBaseUrl?: string;
|
|
9
19
|
readonly fetch?: typeof fetch;
|
|
10
20
|
readonly window?: Pick<Window, "location" | "history">;
|
|
11
21
|
/** Pluggable JWT-session storage. Defaults to {@link defaultSessionStore}. */
|
|
@@ -82,6 +92,7 @@ export interface ImportMandateInput {
|
|
|
82
92
|
export declare class AithosAuth {
|
|
83
93
|
#private;
|
|
84
94
|
readonly authBaseUrl: string;
|
|
95
|
+
readonly apiBaseUrl: string;
|
|
85
96
|
constructor(config?: AithosAuthConfig);
|
|
86
97
|
/**
|
|
87
98
|
* Reload signing material and JWT session from the configured stores.
|
package/dist/src/auth.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// JWT-less sessions (recovery / mandate sign-ins) are valid: the
|
|
21
21
|
// keyStore is the source of truth for "is the user signed in", the
|
|
22
22
|
// JWT is auxiliary for compute/wallet.
|
|
23
|
-
import { buildBlobPlaintext, createBrowserIdentity, decryptBlob, DEFAULT_KDF, deriveAuthAndEncKeys, encryptBlob, parseBlob, randomNonce, randomSalt, serializeBlob, zeroize, } from "@aithos/protocol-client";
|
|
23
|
+
import { buildBlobPlaintext, buildSignedEnvelope, createBrowserIdentity, decryptBlob, DEFAULT_KDF, deriveAuthAndEncKeys, encryptBlob, parseBlob, randomNonce, randomSalt, serializeBlob, signedDidDocument, zeroize, } from "@aithos/protocol-client";
|
|
24
24
|
import { loginChallenge, loginVerify, registerAccount, } from "./auth-api.js";
|
|
25
25
|
import { defaultSessionStore, } from "./session-store.js";
|
|
26
26
|
import { defaultKeyStore, } from "./key-store.js";
|
|
@@ -31,11 +31,14 @@ import { parseRecoveryFile, readRecoveryFileText, serializeRecoveryFile, } from
|
|
|
31
31
|
import { AithosSDKError } from "./types.js";
|
|
32
32
|
/** Default URL of the Aithos auth backend. */
|
|
33
33
|
export const DEFAULT_AUTH_BASE_URL = "https://auth.aithos.be";
|
|
34
|
+
/** Default URL of the Aithos primitives API (publish_identity, publish_ethos_edition, etc.). */
|
|
35
|
+
export const DEFAULT_API_BASE_URL = "https://api.aithos.be";
|
|
34
36
|
/* -------------------------------------------------------------------------- */
|
|
35
37
|
/* AithosAuth */
|
|
36
38
|
/* -------------------------------------------------------------------------- */
|
|
37
39
|
export class AithosAuth {
|
|
38
40
|
authBaseUrl;
|
|
41
|
+
apiBaseUrl;
|
|
39
42
|
#fetchImpl;
|
|
40
43
|
#win;
|
|
41
44
|
#sessionStore;
|
|
@@ -46,6 +49,7 @@ export class AithosAuth {
|
|
|
46
49
|
#delegates = new DelegateRegistry();
|
|
47
50
|
constructor(config = {}) {
|
|
48
51
|
this.authBaseUrl = trimSlash(config.authBaseUrl ?? DEFAULT_AUTH_BASE_URL);
|
|
52
|
+
this.apiBaseUrl = trimSlash(config.apiBaseUrl ?? DEFAULT_API_BASE_URL);
|
|
49
53
|
this.#fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
50
54
|
this.#win =
|
|
51
55
|
config.window ??
|
|
@@ -306,6 +310,16 @@ export class AithosAuth {
|
|
|
306
310
|
zeroize(authKey);
|
|
307
311
|
zeroize(encKey);
|
|
308
312
|
}
|
|
313
|
+
// Bootstrap the Ethos on api.aithos.be. Without this, every subsequent
|
|
314
|
+
// write (publish_ethos_edition, etc.) errors out with -32020
|
|
315
|
+
// "subject identity not published". We do this BEFORE hydrating local
|
|
316
|
+
// state so a bootstrap failure leaves the SDK in a clean
|
|
317
|
+
// "not signed in" state — the dev shows an error, the user retries.
|
|
318
|
+
// The auth account on auth.aithos.be DOES exist at this point, but
|
|
319
|
+
// without the local hydrate the user can't act on it. Self-heal on
|
|
320
|
+
// signIn (re-attempt publish_identity if missing) is planned for a
|
|
321
|
+
// follow-up release.
|
|
322
|
+
await this.#publishIdentity(identity);
|
|
309
323
|
// Hydrate in-memory state from the fresh identity.
|
|
310
324
|
if (this.#ownerSigners)
|
|
311
325
|
this.#ownerSigners.destroy();
|
|
@@ -558,6 +572,83 @@ export class AithosAuth {
|
|
|
558
572
|
await this.#keyStore.clearOwner().catch(() => { });
|
|
559
573
|
await this.#keyStore.clearAllDelegates().catch(() => { });
|
|
560
574
|
}
|
|
575
|
+
/* ------------------------------------------------------------------------ */
|
|
576
|
+
/* Internal — Ethos bootstrap */
|
|
577
|
+
/* ------------------------------------------------------------------------ */
|
|
578
|
+
/**
|
|
579
|
+
* Provision the user's Ethos on `api.aithos.be` by signing and POSTing an
|
|
580
|
+
* `aithos.publish_identity` envelope. Required after a fresh sign-up so
|
|
581
|
+
* subsequent edition publishes (`me.publish()`) don't fail with
|
|
582
|
+
* `-32020 subject identity not published`.
|
|
583
|
+
*
|
|
584
|
+
* Retries twice with exponential backoff on transient errors (network or
|
|
585
|
+
* 5xx). Throws {@link AithosSDKError} with code `ethos_bootstrap_failed`
|
|
586
|
+
* on definitive failure — the caller is expected to abort sign-up.
|
|
587
|
+
*
|
|
588
|
+
* @internal
|
|
589
|
+
*/
|
|
590
|
+
async #publishIdentity(identity) {
|
|
591
|
+
const url = `${this.apiBaseUrl}/mcp/primitives/write`;
|
|
592
|
+
const signedDoc = signedDidDocument(identity);
|
|
593
|
+
const params = {
|
|
594
|
+
did_document: signedDoc,
|
|
595
|
+
handle: identity.handle,
|
|
596
|
+
display_name: identity.displayName,
|
|
597
|
+
};
|
|
598
|
+
const envelope = buildSignedEnvelope({
|
|
599
|
+
iss: identity.did,
|
|
600
|
+
aud: url,
|
|
601
|
+
method: "aithos.publish_identity",
|
|
602
|
+
verificationMethod: `${identity.did}#root`,
|
|
603
|
+
params,
|
|
604
|
+
signer: identity.root,
|
|
605
|
+
});
|
|
606
|
+
const body = JSON.stringify({
|
|
607
|
+
jsonrpc: "2.0",
|
|
608
|
+
id: "publish_identity",
|
|
609
|
+
method: "aithos.publish_identity",
|
|
610
|
+
params: { ...params, _envelope: envelope },
|
|
611
|
+
});
|
|
612
|
+
// Two retries with backoff (300ms, 1500ms). Idempotent on the server
|
|
613
|
+
// side — replaying the same publish_identity for an existing DID is a
|
|
614
|
+
// no-op, so retries are safe even if the first attempt actually
|
|
615
|
+
// succeeded but the response was lost.
|
|
616
|
+
const delays = [0, 300, 1500];
|
|
617
|
+
let lastError;
|
|
618
|
+
for (const delay of delays) {
|
|
619
|
+
if (delay > 0)
|
|
620
|
+
await sleep(delay);
|
|
621
|
+
try {
|
|
622
|
+
const res = await this.#fetchImpl(url, {
|
|
623
|
+
method: "POST",
|
|
624
|
+
headers: { "content-type": "application/json" },
|
|
625
|
+
body,
|
|
626
|
+
});
|
|
627
|
+
// Transport errors (5xx, no body) — retry. JSON-RPC errors come
|
|
628
|
+
// back with HTTP 200 and an `error` field.
|
|
629
|
+
if (!res.ok && res.status >= 500) {
|
|
630
|
+
lastError = new Error(`HTTP ${res.status}`);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const json = (await res.json());
|
|
634
|
+
if (json.error) {
|
|
635
|
+
// JSON-RPC error: don't retry — these are deterministic
|
|
636
|
+
// (validation, permission, identity-already-tombstoned, …).
|
|
637
|
+
throw new AithosSDKError("ethos_bootstrap_failed", `publish_identity rejected: ${json.error.message}`, {
|
|
638
|
+
status: res.status,
|
|
639
|
+
data: { rpc_code: json.error.code, ...(json.error.data ?? {}) },
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return; // success
|
|
643
|
+
}
|
|
644
|
+
catch (e) {
|
|
645
|
+
if (e instanceof AithosSDKError)
|
|
646
|
+
throw e;
|
|
647
|
+
lastError = e;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
throw new AithosSDKError("ethos_bootstrap_failed", `publish_identity unreachable after ${delays.length} attempts: ${lastError?.message ?? "unknown"}`);
|
|
651
|
+
}
|
|
561
652
|
}
|
|
562
653
|
/* -------------------------------------------------------------------------- */
|
|
563
654
|
/* Helpers */
|
|
@@ -565,6 +656,9 @@ export class AithosAuth {
|
|
|
565
656
|
function trimSlash(url) {
|
|
566
657
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
567
658
|
}
|
|
659
|
+
function sleep(ms) {
|
|
660
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
661
|
+
}
|
|
568
662
|
function cleanCallbackParams(win, url) {
|
|
569
663
|
url.searchParams.delete("aithos_code");
|
|
570
664
|
url.searchParams.delete("aithos_error");
|
package/dist/src/index.d.ts
CHANGED
|
@@ -8,14 +8,14 @@ export type { ComputeMessage, InvokeBedrockArgs, InvokeBedrockResult, StopReason
|
|
|
8
8
|
export { ComputeNamespace } from "./compute.js";
|
|
9
9
|
export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
|
|
10
10
|
export { WalletNamespace } from "./wallet.js";
|
|
11
|
-
export { AithosAuth, DEFAULT_AUTH_BASE_URL } from "./auth.js";
|
|
11
|
+
export { AithosAuth, DEFAULT_API_BASE_URL, DEFAULT_AUTH_BASE_URL, } from "./auth.js";
|
|
12
12
|
export type { AithosAuthConfig, AithosSession, DelegateInfo, ImportMandateInput, OwnerInfo, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, } from "./auth.js";
|
|
13
13
|
export { DEFAULT_SESSION_STORAGE_KEY, defaultSessionStore, localStorageStore, noopStore, sessionStorageStore, type AithosSessionStore, } from "./session-store.js";
|
|
14
14
|
export { DEFAULT_KEYSTORE_DB_NAME, defaultKeyStore, indexedDbKeyStore, memoryKeyStore, type AithosKeyStore, type StoredDelegateKeys, type StoredOwnerKeys, } from "./key-store.js";
|
|
15
15
|
export { EthosClient, EthosNamespace, EthosZone, ZONE_NAMES, } from "./ethos.js";
|
|
16
16
|
export type { AddSectionInput, PublishResult, StagedChange, UpdateSectionPatch, ZoneName, } from "./ethos.js";
|
|
17
|
-
export { MandatesNamespace } from "./mandates.js";
|
|
18
|
-
export type { ActorSphere, CreateMandateInput, MintedMandate, OwnedMandate, Scope, } from "./mandates.js";
|
|
17
|
+
export { COMPUTE_INVOKE_SCOPE, MandatesNamespace } from "./mandates.js";
|
|
18
|
+
export type { ActorSphere, CreateMandateComputeInput, CreateMandateInput, MintedMandate, OwnedMandate, Scope, } from "./mandates.js";
|
|
19
19
|
export * as onboarding from "./onboarding.js";
|
|
20
20
|
export { createBrowserIdentity, browserIdentityFromStored, type BrowserIdentity, } from "@aithos/protocol-client";
|
|
21
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export { WalletNamespace } from "./wallet.js";
|
|
|
28
28
|
// BrowserIdentity (sign-up creates one, sign-in restores it from the
|
|
29
29
|
// server). The class also owns the session store — see
|
|
30
30
|
// ./session-store.ts for pluggable persistence.
|
|
31
|
-
export { AithosAuth, DEFAULT_AUTH_BASE_URL } from "./auth.js";
|
|
31
|
+
export { AithosAuth, DEFAULT_API_BASE_URL, DEFAULT_AUTH_BASE_URL, } from "./auth.js";
|
|
32
32
|
// Session storage backends used by AithosAuth. `sessionStorageStore` is
|
|
33
33
|
// the default in browser environments ; pass another store at construction
|
|
34
34
|
// time if you need different persistence.
|
|
@@ -46,7 +46,7 @@ export { DEFAULT_KEYSTORE_DB_NAME, defaultKeyStore, indexedDbKeyStore, memoryKey
|
|
|
46
46
|
// the entry points.
|
|
47
47
|
export { EthosClient, EthosNamespace, EthosZone, ZONE_NAMES, } from "./ethos.js";
|
|
48
48
|
// `sdk.mandates` namespace — owner-side mandate lifecycle.
|
|
49
|
-
export { MandatesNamespace } from "./mandates.js";
|
|
49
|
+
export { COMPUTE_INVOKE_SCOPE, MandatesNamespace } from "./mandates.js";
|
|
50
50
|
// Onboarding re-exports kept for advanced callers — the curated API
|
|
51
51
|
// for first-run flows is `auth.signUp` (already shipped in J3).
|
|
52
52
|
export * as onboarding from "./onboarding.js";
|
package/dist/src/mandates.d.ts
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
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.
|
|
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. */
|
|
4
11
|
export type Scope = "ethos.read.public" | "ethos.read.circle" | "ethos.read.self" | "ethos.write.public" | "ethos.write.circle" | "ethos.write.self";
|
|
12
|
+
/**
|
|
13
|
+
* The opt-in scope that authorizes a delegate to spend the subject's
|
|
14
|
+
* compute credits via the Aithos compute proxy. Mirror of
|
|
15
|
+
* `COMPUTE_INVOKE_SCOPE` in `@aithos/protocol-core` v0.4.0.
|
|
16
|
+
*
|
|
17
|
+
* The SDK's `mandates.create()` injects this scope automatically when
|
|
18
|
+
* the caller passes a `compute` namespace, and refuses to mint a
|
|
19
|
+
* mandate where the caller put it directly into `scopes` — this is
|
|
20
|
+
* what makes "compute is a separate, conscious decision" hold at the
|
|
21
|
+
* API surface.
|
|
22
|
+
*/
|
|
23
|
+
export declare const COMPUTE_INVOKE_SCOPE: "compute.invoke";
|
|
5
24
|
/**
|
|
6
25
|
* Which sphere of the owner signs the mandate. Bounds the upper-most
|
|
7
26
|
* scope set the mandate can carry.
|
|
8
27
|
*/
|
|
9
28
|
export type ActorSphere = "public" | "circle" | "self";
|
|
29
|
+
/**
|
|
30
|
+
* Compute-spending capability — opt-in only, never implied by ethos
|
|
31
|
+
* scopes.
|
|
32
|
+
*
|
|
33
|
+
* When `compute` is set on {@link CreateMandateInput}, the SDK:
|
|
34
|
+
* 1. Adds the `compute.invoke` scope to the minted mandate.
|
|
35
|
+
* 2. Maps the caller's caps onto `constraints.compute` in the
|
|
36
|
+
* protocol's snake_case shape (= what the verifier reads).
|
|
37
|
+
* 3. Forbids the caller from passing `compute.invoke` in `scopes`
|
|
38
|
+
* directly — that would let an app slip the scope past a
|
|
39
|
+
* consent UI that only reviews `compute`.
|
|
40
|
+
*
|
|
41
|
+
* At least one of `dailyCapMicrocredits` or `totalCapMicrocredits` MUST
|
|
42
|
+
* be set: an unbounded compute mandate is the kind of bearer-token
|
|
43
|
+
* footgun this whole namespace exists to prevent. Validation happens
|
|
44
|
+
* at the SDK boundary (here) AND at the protocol layer (the
|
|
45
|
+
* server-side verifier rejects capless 0.4.0 mandates), so a bug in
|
|
46
|
+
* either tier still fails closed.
|
|
47
|
+
*
|
|
48
|
+
* `maxCreditsPerCall` is a per-invocation safety net for runaway
|
|
49
|
+
* single requests. `allowedModels`, when set, restricts which Bedrock
|
|
50
|
+
* model ids the delegate may target (the proxy's own allowlist still
|
|
51
|
+
* applies on top).
|
|
52
|
+
*/
|
|
53
|
+
export interface CreateMandateComputeInput {
|
|
54
|
+
/** Hard cap on credits debited per UTC day under this mandate. */
|
|
55
|
+
readonly dailyCapMicrocredits?: number;
|
|
56
|
+
/** Hard cap on credits debited over the whole mandate lifetime. */
|
|
57
|
+
readonly totalCapMicrocredits?: number;
|
|
58
|
+
/** Hard cap on credits debited by any single invocation. */
|
|
59
|
+
readonly maxCreditsPerCall?: number;
|
|
60
|
+
/** Allowlist of Bedrock model ids the delegate may invoke. */
|
|
61
|
+
readonly allowedModels?: readonly string[];
|
|
62
|
+
}
|
|
10
63
|
export interface CreateMandateInput {
|
|
11
64
|
/** Grantee URN — usually `urn:aithos:agent:<extension-id>` or similar. */
|
|
12
65
|
readonly granteeId: string;
|
|
@@ -23,6 +76,16 @@ export interface CreateMandateInput {
|
|
|
23
76
|
readonly scopes: readonly Scope[];
|
|
24
77
|
/** Lifetime in seconds. */
|
|
25
78
|
readonly ttlSeconds: number;
|
|
79
|
+
/**
|
|
80
|
+
* Opt-in compute (token-spending) capability — adds the
|
|
81
|
+
* `compute.invoke` scope and a bounded `constraints.compute` budget
|
|
82
|
+
* to the mandate. See {@link CreateMandateComputeInput}.
|
|
83
|
+
*
|
|
84
|
+
* NEVER add `compute.invoke` to `scopes` directly — the SDK rejects
|
|
85
|
+
* that path so the caller has to pass through this typed namespace,
|
|
86
|
+
* which is what a consent UI can review.
|
|
87
|
+
*/
|
|
88
|
+
readonly compute?: CreateMandateComputeInput;
|
|
26
89
|
}
|
|
27
90
|
export interface MintedMandate {
|
|
28
91
|
/** 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,36 @@ 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
|
+
// Validate + project the compute namespace if present, then derive
|
|
68
|
+
// the final scopes/constraints to send to the protocol layer.
|
|
69
|
+
const computeProjection = projectCompute(input.compute);
|
|
70
|
+
const projectedScopes = [...input.scopes];
|
|
71
|
+
if (computeProjection) {
|
|
72
|
+
projectedScopes.push(COMPUTE_INVOKE_SCOPE);
|
|
73
|
+
}
|
|
38
74
|
const actorSphere = input.actorSphere ?? defaultSphereFromScopes(input.scopes);
|
|
39
75
|
const ownerStored = owner._unsafeStoredIdentity();
|
|
40
76
|
const result = await mintDelegateBundle({
|
|
@@ -42,15 +78,27 @@ export class MandatesNamespace {
|
|
|
42
78
|
granteeId: input.granteeId,
|
|
43
79
|
...(input.granteeLabel ? { granteeLabel: input.granteeLabel } : {}),
|
|
44
80
|
actorSphere,
|
|
45
|
-
scopes:
|
|
81
|
+
scopes: projectedScopes,
|
|
46
82
|
ttlSeconds: input.ttlSeconds,
|
|
83
|
+
// protocol-client v0.1.0-alpha.11 ships MandateConstraints without
|
|
84
|
+
// the `compute` field; the wire format accepts it though (the
|
|
85
|
+
// canonicalizer just serializes whatever's in the object). We
|
|
86
|
+
// up-cast through `unknown` to bypass the structural check until
|
|
87
|
+
// protocol-client picks up protocol-core 0.4.0 types.
|
|
88
|
+
...(computeProjection
|
|
89
|
+
? {
|
|
90
|
+
constraints: {
|
|
91
|
+
compute: computeProjection,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
47
95
|
});
|
|
48
96
|
const mandate = result.mandate;
|
|
49
97
|
return {
|
|
50
98
|
mandateId: mandate.id,
|
|
51
99
|
subjectDid: mandate.subject_did,
|
|
52
100
|
granteeId: input.granteeId,
|
|
53
|
-
scopes:
|
|
101
|
+
scopes: projectedScopes,
|
|
54
102
|
expiresAt: mandate.not_after ?? null,
|
|
55
103
|
bundle: result.bundleBlob,
|
|
56
104
|
filename: `aithos-delegate-${mandate.id}.json`,
|
|
@@ -164,6 +212,58 @@ function defaultSphereFromScopes(scopes) {
|
|
|
164
212
|
return "circle";
|
|
165
213
|
return "public";
|
|
166
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Validate the SDK-side `compute` namespace and project it onto the
|
|
217
|
+
* snake_case shape the protocol layer canonicalizes into the mandate.
|
|
218
|
+
*
|
|
219
|
+
* Returns `null` if the caller passed nothing — that's the "no compute
|
|
220
|
+
* authorization" path. Throws {@link AithosSDKError} on any structural
|
|
221
|
+
* problem (camelCase mirror of the rules `validateComputeAuthorization`
|
|
222
|
+
* enforces server-side; we duplicate them here so a misuse fails at the
|
|
223
|
+
* SDK boundary with a precise error rather than only blowing up at mint).
|
|
224
|
+
*/
|
|
225
|
+
function projectCompute(c) {
|
|
226
|
+
if (c === undefined)
|
|
227
|
+
return null;
|
|
228
|
+
const hasDaily = typeof c.dailyCapMicrocredits === "number";
|
|
229
|
+
const hasTotal = typeof c.totalCapMicrocredits === "number";
|
|
230
|
+
if (!hasDaily && !hasTotal) {
|
|
231
|
+
throw new AithosSDKError("mandates_invalid_compute", "compute namespace requires at least one of dailyCapMicrocredits " +
|
|
232
|
+
"or totalCapMicrocredits — an unbounded compute mandate is a bearer " +
|
|
233
|
+
"token to drain the subject's wallet.");
|
|
234
|
+
}
|
|
235
|
+
for (const [field, value] of [
|
|
236
|
+
["dailyCapMicrocredits", c.dailyCapMicrocredits],
|
|
237
|
+
["totalCapMicrocredits", c.totalCapMicrocredits],
|
|
238
|
+
["maxCreditsPerCall", c.maxCreditsPerCall],
|
|
239
|
+
]) {
|
|
240
|
+
if (value === undefined)
|
|
241
|
+
continue;
|
|
242
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
243
|
+
throw new AithosSDKError("mandates_invalid_compute", `compute.${field} must be a positive integer (got ${value}).`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (c.allowedModels !== undefined) {
|
|
247
|
+
if (!Array.isArray(c.allowedModels)) {
|
|
248
|
+
throw new AithosSDKError("mandates_invalid_compute", "compute.allowedModels must be an array of strings.");
|
|
249
|
+
}
|
|
250
|
+
for (const m of c.allowedModels) {
|
|
251
|
+
if (typeof m !== "string" || m.length === 0) {
|
|
252
|
+
throw new AithosSDKError("mandates_invalid_compute", `compute.allowedModels entries must be non-empty strings (got ${JSON.stringify(m)}).`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const wire = {};
|
|
257
|
+
if (hasDaily)
|
|
258
|
+
wire.daily_cap_microcredits = c.dailyCapMicrocredits;
|
|
259
|
+
if (hasTotal)
|
|
260
|
+
wire.total_cap_microcredits = c.totalCapMicrocredits;
|
|
261
|
+
if (c.maxCreditsPerCall !== undefined)
|
|
262
|
+
wire.max_credits_per_call = c.maxCreditsPerCall;
|
|
263
|
+
if (c.allowedModels !== undefined)
|
|
264
|
+
wire.allowed_models = [...c.allowedModels];
|
|
265
|
+
return wire;
|
|
266
|
+
}
|
|
167
267
|
function toOwnedMandate(it) {
|
|
168
268
|
return {
|
|
169
269
|
mandateId: requireString(it, "mandate_id"),
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* Tests for the `compute` namespace on `MandatesNamespace.create`.
|
|
5
|
+
*
|
|
6
|
+
* Invariants under test (mirror of the protocol-core v0.4.0 invariants,
|
|
7
|
+
* enforced at the SDK boundary so callers fail fast with a precise
|
|
8
|
+
* error rather than only blowing up server-side):
|
|
9
|
+
*
|
|
10
|
+
* 1. Passing `compute.invoke` directly in `scopes[]` is rejected.
|
|
11
|
+
* The opt-in must go through the typed `compute` namespace, which
|
|
12
|
+
* is what a consent UI can review.
|
|
13
|
+
* 2. The `compute` namespace requires at least one of
|
|
14
|
+
* `dailyCapMicrocredits` or `totalCapMicrocredits` — an unbounded
|
|
15
|
+
* compute mandate is the bearer-token footgun this whole namespace
|
|
16
|
+
* exists to prevent.
|
|
17
|
+
* 3. Numeric caps must be positive integers.
|
|
18
|
+
* 4. `allowedModels`, when present, must be an array of non-empty
|
|
19
|
+
* strings.
|
|
20
|
+
* 5. A compute-only mandate (`scopes: []` + `compute: {…}`) is
|
|
21
|
+
* legitimate and accepted — the grantee gets no ethos data
|
|
22
|
+
* access, only bounded compute spending. Empty scopes WITHOUT
|
|
23
|
+
* compute is still rejected.
|
|
24
|
+
*
|
|
25
|
+
* Network-dependent paths (real mint, list, revoke) ride on the same
|
|
26
|
+
* integration suite as the rest of the namespace — see the note in
|
|
27
|
+
* `mandates.test.ts`. We intentionally don't go through `mintDelegateBundle`
|
|
28
|
+
* here; failures fire BEFORE that boundary.
|
|
29
|
+
*/
|
|
30
|
+
import { strict as assert } from "node:assert";
|
|
31
|
+
import { describe, it } from "node:test";
|
|
32
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
33
|
+
import { AithosAuth, AithosSDKError, COMPUTE_INVOKE_SCOPE, MandatesNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
34
|
+
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
35
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
36
|
+
function makeAuth() {
|
|
37
|
+
return new AithosAuth({
|
|
38
|
+
authBaseUrl: "https://auth.test",
|
|
39
|
+
fetch: (() => {
|
|
40
|
+
throw new Error("network not expected");
|
|
41
|
+
}),
|
|
42
|
+
sessionStore: noopStore(),
|
|
43
|
+
keyStore: memoryKeyStore(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function makeMandates(auth) {
|
|
47
|
+
return new MandatesNamespace({
|
|
48
|
+
auth,
|
|
49
|
+
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
50
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function signInAsAlice(auth) {
|
|
54
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
55
|
+
const { text } = serializeRecoveryFile(id);
|
|
56
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
57
|
+
return info.did;
|
|
58
|
+
}
|
|
59
|
+
/* -------------------------------------------------------------------------- */
|
|
60
|
+
/* Constant export sanity */
|
|
61
|
+
/* -------------------------------------------------------------------------- */
|
|
62
|
+
describe("COMPUTE_INVOKE_SCOPE constant", () => {
|
|
63
|
+
it("is the canonical 'compute.invoke' string", () => {
|
|
64
|
+
assert.equal(COMPUTE_INVOKE_SCOPE, "compute.invoke");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
/* -------------------------------------------------------------------------- */
|
|
68
|
+
/* Smuggling guard: compute.invoke in scopes[] is rejected */
|
|
69
|
+
/* -------------------------------------------------------------------------- */
|
|
70
|
+
describe("MandatesNamespace.create — compute.invoke smuggling guard", () => {
|
|
71
|
+
it("rejects a mandate that smuggles compute.invoke into scopes[]", async () => {
|
|
72
|
+
const auth = makeAuth();
|
|
73
|
+
await signInAsAlice(auth);
|
|
74
|
+
const m = makeMandates(auth);
|
|
75
|
+
await assert.rejects(() => m.create({
|
|
76
|
+
granteeId: "urn:agent:bob",
|
|
77
|
+
// Up-cast through string[] — this is exactly the path the
|
|
78
|
+
// runtime check has to catch. Type-checking can't (the union
|
|
79
|
+
// doesn't include "compute.invoke", but cast slips through).
|
|
80
|
+
scopes: ["ethos.read.public", "compute.invoke"],
|
|
81
|
+
ttlSeconds: 3600,
|
|
82
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
83
|
+
e.code === "mandates_invalid_scopes" &&
|
|
84
|
+
/compute' namespace/.test(e.message));
|
|
85
|
+
});
|
|
86
|
+
it("rejects even when 'compute.invoke' is the ONLY scope passed", async () => {
|
|
87
|
+
const auth = makeAuth();
|
|
88
|
+
await signInAsAlice(auth);
|
|
89
|
+
const m = makeMandates(auth);
|
|
90
|
+
await assert.rejects(() => m.create({
|
|
91
|
+
granteeId: "urn:agent:bob",
|
|
92
|
+
scopes: ["compute.invoke"],
|
|
93
|
+
ttlSeconds: 3600,
|
|
94
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
95
|
+
e.code === "mandates_invalid_scopes");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
/* -------------------------------------------------------------------------- */
|
|
99
|
+
/* Compute namespace — input validation */
|
|
100
|
+
/* -------------------------------------------------------------------------- */
|
|
101
|
+
describe("MandatesNamespace.create — compute namespace validation", () => {
|
|
102
|
+
it("rejects compute namespace with no caps at all", async () => {
|
|
103
|
+
const auth = makeAuth();
|
|
104
|
+
await signInAsAlice(auth);
|
|
105
|
+
const m = makeMandates(auth);
|
|
106
|
+
await assert.rejects(() => m.create({
|
|
107
|
+
granteeId: "urn:agent:bob",
|
|
108
|
+
scopes: ["ethos.read.public"],
|
|
109
|
+
ttlSeconds: 3600,
|
|
110
|
+
compute: {}, // empty — neither daily nor total cap
|
|
111
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
112
|
+
e.code === "mandates_invalid_compute" &&
|
|
113
|
+
/at least one of dailyCapMicrocredits or totalCapMicrocredits/.test(e.message));
|
|
114
|
+
});
|
|
115
|
+
it("rejects compute namespace with only allowedModels (no cap)", async () => {
|
|
116
|
+
const auth = makeAuth();
|
|
117
|
+
await signInAsAlice(auth);
|
|
118
|
+
const m = makeMandates(auth);
|
|
119
|
+
await assert.rejects(() => m.create({
|
|
120
|
+
granteeId: "urn:agent:bob",
|
|
121
|
+
scopes: ["ethos.read.public"],
|
|
122
|
+
ttlSeconds: 3600,
|
|
123
|
+
compute: { allowedModels: ["claude-haiku-4-5"] },
|
|
124
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
125
|
+
e.code === "mandates_invalid_compute");
|
|
126
|
+
});
|
|
127
|
+
it("rejects non-positive caps", async () => {
|
|
128
|
+
const auth = makeAuth();
|
|
129
|
+
await signInAsAlice(auth);
|
|
130
|
+
const m = makeMandates(auth);
|
|
131
|
+
for (const bad of [0, -1, 3.14]) {
|
|
132
|
+
await assert.rejects(() => m.create({
|
|
133
|
+
granteeId: "urn:agent:bob",
|
|
134
|
+
scopes: ["ethos.read.public"],
|
|
135
|
+
ttlSeconds: 3600,
|
|
136
|
+
compute: { dailyCapMicrocredits: bad },
|
|
137
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
138
|
+
e.code === "mandates_invalid_compute" &&
|
|
139
|
+
/must be a positive integer/.test(e.message), `expected rejection for dailyCapMicrocredits=${bad}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
it("rejects malformed allowedModels (empty string entry)", async () => {
|
|
143
|
+
const auth = makeAuth();
|
|
144
|
+
await signInAsAlice(auth);
|
|
145
|
+
const m = makeMandates(auth);
|
|
146
|
+
await assert.rejects(() => m.create({
|
|
147
|
+
granteeId: "urn:agent:bob",
|
|
148
|
+
scopes: ["ethos.read.public"],
|
|
149
|
+
ttlSeconds: 3600,
|
|
150
|
+
compute: {
|
|
151
|
+
dailyCapMicrocredits: 5_000,
|
|
152
|
+
allowedModels: ["claude-haiku-4-5", ""],
|
|
153
|
+
},
|
|
154
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
155
|
+
e.code === "mandates_invalid_compute" &&
|
|
156
|
+
/allowedModels entries must be non-empty strings/.test(e.message));
|
|
157
|
+
});
|
|
158
|
+
it("rejects allowedModels that is not an array", async () => {
|
|
159
|
+
const auth = makeAuth();
|
|
160
|
+
await signInAsAlice(auth);
|
|
161
|
+
const m = makeMandates(auth);
|
|
162
|
+
await assert.rejects(() => m.create({
|
|
163
|
+
granteeId: "urn:agent:bob",
|
|
164
|
+
scopes: ["ethos.read.public"],
|
|
165
|
+
ttlSeconds: 3600,
|
|
166
|
+
compute: {
|
|
167
|
+
dailyCapMicrocredits: 5_000,
|
|
168
|
+
// @ts-expect-error — runtime check guards against bad casts
|
|
169
|
+
allowedModels: "claude-haiku-4-5",
|
|
170
|
+
},
|
|
171
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
172
|
+
e.code === "mandates_invalid_compute");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
/* -------------------------------------------------------------------------- */
|
|
176
|
+
/* Read-only mandate is unaffected */
|
|
177
|
+
/* -------------------------------------------------------------------------- */
|
|
178
|
+
describe("MandatesNamespace.create — read-only mandates unaffected", () => {
|
|
179
|
+
it("does NOT reject a clean read-only mandate (no compute, no smuggling)", async () => {
|
|
180
|
+
// We can't run the full mint here because the installed
|
|
181
|
+
// @aithos/protocol-core (0.5.1, pre-0.4.0-mandate) doesn't yet
|
|
182
|
+
// accept compute.invoke on the public sphere whitelist — the real
|
|
183
|
+
// mint would still succeed for a read-only mandate, but it goes
|
|
184
|
+
// out to S3 etc. Instead we check the synchronous validation
|
|
185
|
+
// surface: passing a clean read-only input must not throw before
|
|
186
|
+
// the mint call.
|
|
187
|
+
//
|
|
188
|
+
// (The async `m.create(...)` would still error later on the
|
|
189
|
+
// network path — that's the integration suite's job.)
|
|
190
|
+
//
|
|
191
|
+
// What we ARE asserting here: no SDK-side validation rejection.
|
|
192
|
+
const auth = makeAuth();
|
|
193
|
+
await signInAsAlice(auth);
|
|
194
|
+
const m = makeMandates(auth);
|
|
195
|
+
// The mint will fail with a network error (fetch throws), but
|
|
196
|
+
// crucially NOT with a validation error — that proves our compute
|
|
197
|
+
// guard is precisely scoped to compute-related inputs.
|
|
198
|
+
await assert.rejects(() => m.create({
|
|
199
|
+
granteeId: "urn:viewer:bob",
|
|
200
|
+
scopes: ["ethos.read.public"],
|
|
201
|
+
ttlSeconds: 3600,
|
|
202
|
+
}), (e) => {
|
|
203
|
+
if (!(e instanceof AithosSDKError))
|
|
204
|
+
return true; // any non-validation error fine
|
|
205
|
+
const code = e.code;
|
|
206
|
+
// Anything except a validation rejection is fine — the network
|
|
207
|
+
// is not wired up in this fake auth.
|
|
208
|
+
return (code !== "mandates_invalid_scopes" &&
|
|
209
|
+
code !== "mandates_invalid_compute");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
/* -------------------------------------------------------------------------- */
|
|
214
|
+
/* Compute-only mandate (scopes: [] + compute namespace) */
|
|
215
|
+
/* -------------------------------------------------------------------------- */
|
|
216
|
+
describe("MandatesNamespace.create — compute-only mandate", () => {
|
|
217
|
+
it("accepts an empty scopes[] when compute is provided (no ethos data access, just bounded spending)", async () => {
|
|
218
|
+
// Same caveat as the "read-only unaffected" suite: the installed
|
|
219
|
+
// @aithos/protocol-core does not yet whitelist compute.invoke on
|
|
220
|
+
// the public sphere, so the real `mintDelegateBundle` call would
|
|
221
|
+
// still reject downstream. What this test proves is that the
|
|
222
|
+
// SDK validation layer (a) does NOT throw `mandates_invalid_scopes`
|
|
223
|
+
// for the empty list when compute is set, and (b) does NOT throw
|
|
224
|
+
// `mandates_invalid_compute` for a properly-capped budget. The
|
|
225
|
+
// request is allowed to proceed past validation.
|
|
226
|
+
const auth = makeAuth();
|
|
227
|
+
await signInAsAlice(auth);
|
|
228
|
+
const m = makeMandates(auth);
|
|
229
|
+
await assert.rejects(() => m.create({
|
|
230
|
+
granteeId: "urn:agent:creative",
|
|
231
|
+
scopes: [], // no ethos data access
|
|
232
|
+
ttlSeconds: 3600,
|
|
233
|
+
compute: { dailyCapMicrocredits: 1_000 },
|
|
234
|
+
}), (e) => {
|
|
235
|
+
if (!(e instanceof AithosSDKError))
|
|
236
|
+
return true;
|
|
237
|
+
const code = e.code;
|
|
238
|
+
// Validation must NOT have rejected this input.
|
|
239
|
+
return (code !== "mandates_invalid_scopes" &&
|
|
240
|
+
code !== "mandates_invalid_compute");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
it("STILL rejects empty scopes[] when compute is NOT provided", async () => {
|
|
244
|
+
const auth = makeAuth();
|
|
245
|
+
await signInAsAlice(auth);
|
|
246
|
+
const m = makeMandates(auth);
|
|
247
|
+
await assert.rejects(() => m.create({
|
|
248
|
+
granteeId: "urn:agent:bob",
|
|
249
|
+
scopes: [],
|
|
250
|
+
ttlSeconds: 3600,
|
|
251
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
252
|
+
e.code === "mandates_invalid_scopes" &&
|
|
253
|
+
/compute-only mandate/.test(e.message));
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
//# sourceMappingURL=mandates-compute.test.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aithos/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"description": "Aithos SDK — high-level TypeScript developer kit for building agentic apps on the Aithos protocol. Wraps @aithos/protocol-client and exposes the Aithos compute proxy and wallet (Stripe top-up) endpoints.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aithos",
|