@aithos/sdk 0.1.0-alpha.8 → 0.1.0-alpha.9

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.
@@ -63,10 +63,29 @@ export declare class ComputeNamespace {
63
63
  * Invoke a Bedrock model through the compute proxy. See
64
64
  * {@link InvokeBedrockArgs} and {@link InvokeBedrockResult}.
65
65
  *
66
+ * Two signer paths are supported:
67
+ *
68
+ * - **Owner**: when the caller is signed in as an owner, the
69
+ * envelope is signed with the owner's `#public` sphere key and
70
+ * no mandate is attached (the proxy resolves the mandate
71
+ * server-side from `params.mandate_id`).
72
+ * - **Delegate**: when the caller is delegate-only (mandate
73
+ * imported via `auth.importMandate`), the envelope is signed
74
+ * with the delegate's bound keypair and the full SignedMandate
75
+ * is attached so the proxy can verify both signature and
76
+ * authorisation in one pass. The mandate must carry the
77
+ * `compute.invoke` scope and the proxy enforces its constraints
78
+ * (caps, allowed models, …) at server-side.
79
+ *
80
+ * Owner takes precedence: if a session has BOTH an owner and a
81
+ * matching delegate session, we use the owner key (more flexible —
82
+ * the mandate is dereferenced via `mandate_id` and there's no
83
+ * lifetime cliff if the delegate seed has been wiped).
84
+ *
66
85
  * @throws {AithosSDKError} on protocol errors. The `code` field is one of
67
- * `sdk_no_owner`, `network`, `http`, `empty`, or any code returned by
68
- * the proxy (`quota_exceeded`, `mandate_revoked`, `insufficient_credits`,
69
- * …).
86
+ * `sdk_no_signer`, `sdk_no_delegate_for_mandate`, `network`, `http`,
87
+ * `empty`, or any code returned by the proxy (`quota_exceeded`,
88
+ * `mandate_revoked`, `insufficient_credits`, …).
70
89
  */
71
90
  invokeBedrock(args: InvokeBedrockArgs): Promise<InvokeBedrockResult>;
72
91
  }
@@ -16,9 +16,9 @@
16
16
  //
17
17
  // MVP scope: single-shot invocation. Multi-turn agentic loops (with native
18
18
  // tool calling on the proxy) will land in a follow-up.
19
- import { buildSignedEnvelope } from "@aithos/protocol-client";
19
+ import { buildSignedEnvelope, } from "@aithos/protocol-client";
20
20
  import { computeInvokeUrl, } from "./endpoints.js";
21
- import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
21
+ import { delegateKeyPair, ownerKeyPair, } from "./internal/protocol-client-bridge.js";
22
22
  import { AithosSDKError } from "./types.js";
23
23
  /**
24
24
  * `sdk.compute` namespace. Constructed once by the {@link AithosSDK}
@@ -35,18 +35,66 @@ export class ComputeNamespace {
35
35
  * Invoke a Bedrock model through the compute proxy. See
36
36
  * {@link InvokeBedrockArgs} and {@link InvokeBedrockResult}.
37
37
  *
38
+ * Two signer paths are supported:
39
+ *
40
+ * - **Owner**: when the caller is signed in as an owner, the
41
+ * envelope is signed with the owner's `#public` sphere key and
42
+ * no mandate is attached (the proxy resolves the mandate
43
+ * server-side from `params.mandate_id`).
44
+ * - **Delegate**: when the caller is delegate-only (mandate
45
+ * imported via `auth.importMandate`), the envelope is signed
46
+ * with the delegate's bound keypair and the full SignedMandate
47
+ * is attached so the proxy can verify both signature and
48
+ * authorisation in one pass. The mandate must carry the
49
+ * `compute.invoke` scope and the proxy enforces its constraints
50
+ * (caps, allowed models, …) at server-side.
51
+ *
52
+ * Owner takes precedence: if a session has BOTH an owner and a
53
+ * matching delegate session, we use the owner key (more flexible —
54
+ * the mandate is dereferenced via `mandate_id` and there's no
55
+ * lifetime cliff if the delegate seed has been wiped).
56
+ *
38
57
  * @throws {AithosSDKError} on protocol errors. The `code` field is one of
39
- * `sdk_no_owner`, `network`, `http`, `empty`, or any code returned by
40
- * the proxy (`quota_exceeded`, `mandate_revoked`, `insufficient_credits`,
41
- * …).
58
+ * `sdk_no_signer`, `sdk_no_delegate_for_mandate`, `network`, `http`,
59
+ * `empty`, or any code returned by the proxy (`quota_exceeded`,
60
+ * `mandate_revoked`, `insufficient_credits`, …).
42
61
  */
43
62
  async invokeBedrock(args) {
44
63
  const { auth, appDid, endpoints, fetch: fetchImpl } = this.#deps;
45
64
  const owner = auth._getOwnerSigners();
46
- if (!owner || owner.destroyed) {
47
- throw new AithosSDKError("sdk_no_owner", "no owner signed in; sign in via auth.signIn / signUp / signInWithGoogle / signInWithRecovery first");
65
+ const ownerLoaded = owner !== null && !owner.destroyed;
66
+ let choice;
67
+ if (ownerLoaded) {
68
+ const publicKp = ownerKeyPair(owner, "public");
69
+ choice = {
70
+ kind: "owner",
71
+ iss: owner.did,
72
+ verificationMethod: `${owner.did}#public`,
73
+ signer: publicKp,
74
+ mandate: undefined,
75
+ };
76
+ }
77
+ else {
78
+ // Delegate-only path. Find a session whose mandate id matches.
79
+ const actor = auth._getDelegateActor(args.mandateId);
80
+ if (!actor || actor.destroyed) {
81
+ throw new AithosSDKError("sdk_no_delegate_for_mandate", `no owner signed in and no imported delegate mandate matches '${args.mandateId}'. Sign in as an owner, or import a delegate bundle for that mandate via auth.importMandate.`);
82
+ }
83
+ const kp = delegateKeyPair(actor);
84
+ choice = {
85
+ kind: "delegate",
86
+ iss: actor.subjectDid,
87
+ verificationMethod: actor.granteePubkeyMultibase,
88
+ signer: kp,
89
+ // The DelegateActor stores the SignedMandate as a structurally
90
+ // opaque object so the SDK doesn't have to import the
91
+ // protocol-client type at the storage boundary. Round-trip
92
+ // through `unknown` for the TS cast — at runtime the bytes are
93
+ // the canonical SignedMandate the bundle parser already
94
+ // validated.
95
+ mandate: actor.mandate,
96
+ };
48
97
  }
49
- const publicKp = ownerKeyPair(owner, "public");
50
98
  const url = computeInvokeUrl(endpoints);
51
99
  const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
52
100
  const params = {
@@ -63,12 +111,13 @@ export class ComputeNamespace {
63
111
  if (args.temperature !== undefined)
64
112
  params.temperature = args.temperature;
65
113
  const envelope = buildSignedEnvelope({
66
- iss: owner.did,
114
+ iss: choice.iss,
67
115
  aud: url,
68
116
  method: "aithos.compute_invoke",
69
- verificationMethod: `${owner.did}#public`,
117
+ verificationMethod: choice.verificationMethod,
70
118
  params,
71
- signer: publicKp,
119
+ signer: choice.signer,
120
+ ...(choice.kind === "delegate" ? { mandate: choice.mandate } : {}),
72
121
  });
73
122
  let res;
74
123
  try {
@@ -86,6 +86,18 @@ export interface CreateMandateInput {
86
86
  * which is what a consent UI can review.
87
87
  */
88
88
  readonly compute?: CreateMandateComputeInput;
89
+ /**
90
+ * When the mandate becomes valid. Optional — when omitted, the
91
+ * underlying mint helper signs with `not_before = now - 30s` (see
92
+ * `MANDATE_NOTBEFORE_OFFSET_SECONDS_DEFAULT` in
93
+ * `@aithos/protocol-client`) so a server whose clock runs slightly
94
+ * behind the client doesn't reject the freshly-minted mandate as
95
+ * `not yet valid`.
96
+ *
97
+ * Pass an explicit `Date` only for advanced flows (delayed-activation
98
+ * mandates, deterministic tests).
99
+ */
100
+ readonly notBefore?: Date;
89
101
  }
90
102
  export interface MintedMandate {
91
103
  /** Unique mandate id (matches `mandate.id` inside the bundle). */
@@ -92,6 +92,7 @@ export class MandatesNamespace {
92
92
  },
93
93
  }
94
94
  : {}),
95
+ ...(input.notBefore ? { notBefore: input.notBefore } : {}),
95
96
  });
96
97
  const mandate = result.mandate;
97
98
  return {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compute-delegate-path.test.d.ts.map
@@ -0,0 +1,183 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the delegate signing path on sdk.compute.invokeBedrock.
4
+ // Until alpha.9 the method ALWAYS required an owner — a session that
5
+ // only held a mandate (no owner signers) failed with sdk_no_owner
6
+ // before any network call. From alpha.9 onward, when no owner is
7
+ // loaded but a delegate matches the requested mandate id, the SDK
8
+ // signs the envelope with the delegate's keypair and attaches the
9
+ // SignedMandate.
10
+ import { strict as assert } from "node:assert";
11
+ import { afterEach, beforeEach, describe, it } from "node:test";
12
+ import { createBrowserIdentity } from "@aithos/protocol-client";
13
+ import { AithosAuth, AithosSDKError, ComputeNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
14
+ import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
15
+ /* -------------------------------------------------------------------------- */
16
+ /* Test plumbing */
17
+ /* -------------------------------------------------------------------------- */
18
+ let savedFetch;
19
+ let lastRequestBody = null;
20
+ function installFetchMock(response) {
21
+ savedFetch = globalThis.fetch;
22
+ lastRequestBody = null;
23
+ globalThis.fetch = (async (_input, init) => {
24
+ lastRequestBody = JSON.parse(init?.body);
25
+ return new Response(JSON.stringify(response.json), {
26
+ status: response.status ?? 200,
27
+ headers: { "content-type": "application/json" },
28
+ });
29
+ });
30
+ }
31
+ function uninstallFetchMock() {
32
+ if (savedFetch)
33
+ globalThis.fetch = savedFetch;
34
+ savedFetch = undefined;
35
+ lastRequestBody = null;
36
+ }
37
+ const okResponse = {
38
+ json: {
39
+ jsonrpc: "2.0",
40
+ id: "x",
41
+ result: {
42
+ content: "ok",
43
+ stopReason: "end_turn",
44
+ usage: { inputTokens: 1, outputTokens: 1 },
45
+ creditsCharged: 1,
46
+ walletBalance: 999,
47
+ auditId: "audit-test",
48
+ },
49
+ },
50
+ };
51
+ function freshAuth() {
52
+ return new AithosAuth({
53
+ sessionStore: noopStore(),
54
+ keyStore: memoryKeyStore(),
55
+ });
56
+ }
57
+ function freshCompute(auth) {
58
+ return new ComputeNamespace({
59
+ auth,
60
+ appDid: "did:aithos:app:example-placeholder",
61
+ endpoints: DEFAULT_SDK_ENDPOINTS,
62
+ fetch: globalThis.fetch.bind(globalThis),
63
+ });
64
+ }
65
+ /**
66
+ * Build a delegate bundle JSON that matches the wire shape importMandate
67
+ * accepts. Uses a real SignedMandate-like object minted from a fresh
68
+ * issuer so signature verification on import succeeds.
69
+ */
70
+ function makeDelegateBundleText(args) {
71
+ const issuer = createBrowserIdentity("alice", "Alice");
72
+ return JSON.stringify({
73
+ aithos_delegate_version: "0.1.0",
74
+ mandate: {
75
+ "aithos-mandate": "0.4.0",
76
+ id: args.mandateId,
77
+ issuer: issuer.did,
78
+ issued_by_key: `${issuer.did}#self`,
79
+ grantee: {
80
+ id: args.granteeId ?? "urn:aithos:agent:test",
81
+ pubkey: "z6MkqGenericPubKey",
82
+ },
83
+ actor_sphere: "self",
84
+ scopes: args.scopes,
85
+ not_before: "2026-05-10T00:00:00Z",
86
+ not_after: "2026-05-11T00:00:00Z",
87
+ issued_at: "2026-05-10T00:00:00Z",
88
+ nonce: "abc",
89
+ signature: { alg: "ed25519", key: `${issuer.did}#self`, value: "..." },
90
+ },
91
+ delegate_seed_hex: "11".repeat(32),
92
+ });
93
+ }
94
+ /* -------------------------------------------------------------------------- */
95
+ /* Tests */
96
+ /* -------------------------------------------------------------------------- */
97
+ describe("ComputeNamespace.invokeBedrock — delegate path", () => {
98
+ beforeEach(() => {
99
+ installFetchMock(okResponse);
100
+ });
101
+ afterEach(() => {
102
+ uninstallFetchMock();
103
+ });
104
+ it("rejects with sdk_no_delegate_for_mandate when delegate-only and mandateId doesn't match", async () => {
105
+ const auth = freshAuth();
106
+ const compute = freshCompute(auth);
107
+ // Import a delegate with a DIFFERENT mandate id than the one the call
108
+ // requests. Should error with the new code (NOT sdk_no_owner anymore).
109
+ await auth.importMandate({
110
+ bundle: makeDelegateBundleText({
111
+ mandateId: "mandate:held",
112
+ scopes: ["compute.invoke"],
113
+ }),
114
+ });
115
+ await assert.rejects(() => compute.invokeBedrock({
116
+ mandateId: "mandate:other-not-held",
117
+ model: "claude-haiku-4-5",
118
+ messages: [{ role: "user", content: "Hi" }],
119
+ maxTokens: 1,
120
+ }), (e) => e instanceof AithosSDKError &&
121
+ e.code === "sdk_no_delegate_for_mandate");
122
+ });
123
+ it("signs with the delegate keypair + attaches the mandate when delegate-only matches", async () => {
124
+ const auth = freshAuth();
125
+ const compute = freshCompute(auth);
126
+ const mandateId = "mandate:matching";
127
+ await auth.importMandate({
128
+ bundle: makeDelegateBundleText({
129
+ mandateId,
130
+ scopes: ["compute.invoke"],
131
+ }),
132
+ });
133
+ await compute.invokeBedrock({
134
+ mandateId,
135
+ model: "claude-haiku-4-5",
136
+ messages: [{ role: "user", content: "Hi" }],
137
+ maxTokens: 1,
138
+ });
139
+ const env = lastRequestBody?.params?._envelope;
140
+ assert.ok(env, "envelope must be present");
141
+ // verificationMethod must be the delegate's bare multibase pubkey
142
+ // (NOT a `#sphere` DID URL). And the SignedMandate must be inside.
143
+ assert.match(env.proof.verificationMethod, /^z[1-9A-HJ-NP-Za-km-z]+$/, `expected multibase pubkey, got ${env.proof.verificationMethod}`);
144
+ assert.ok(env.mandate, "delegate envelopes must attach the SignedMandate");
145
+ assert.equal(env.mandate.id, mandateId);
146
+ });
147
+ it("still works the owner path when an owner is signed in", async () => {
148
+ const auth = freshAuth();
149
+ const compute = freshCompute(auth);
150
+ // Owner sign-in via recovery — picks up four sphere keys from the
151
+ // recovery file format. Build one inline.
152
+ const issuer = createBrowserIdentity("owner", "Owner");
153
+ const seedHex = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
154
+ const recoveryText = JSON.stringify({
155
+ aithos_recovery_version: "0.1.0-plaintext",
156
+ handle: issuer.handle,
157
+ display_name: issuer.displayName,
158
+ did: issuer.did,
159
+ created_at: new Date().toISOString(),
160
+ seeds_hex: {
161
+ root: seedHex(issuer.root.seed),
162
+ public: seedHex(issuer.public.seed),
163
+ circle: seedHex(issuer.circle.seed),
164
+ self: seedHex(issuer.self.seed),
165
+ },
166
+ });
167
+ await auth.signInWithRecovery({ file: recoveryText });
168
+ await compute.invokeBedrock({
169
+ mandateId: "mandate:owner-managed",
170
+ model: "claude-haiku-4-5",
171
+ messages: [{ role: "user", content: "Hi" }],
172
+ maxTokens: 1,
173
+ });
174
+ const env = lastRequestBody?.params?._envelope;
175
+ assert.ok(env, "envelope must be present");
176
+ // Owner path: verificationMethod is a `#public` DID URL, and the
177
+ // envelope MUST NOT carry a mandate (server resolves from
178
+ // params.mandate_id).
179
+ assert.match(env.proof.verificationMethod, /#public$/);
180
+ assert.equal(env.mandate, undefined, "owner-signed envelopes should not attach a mandate");
181
+ });
182
+ });
183
+ //# sourceMappingURL=compute-delegate-path.test.js.map
@@ -101,7 +101,12 @@ describe("AithosSDK constructor", () => {
101
101
  assert.equal(typeof sdk.ethos.me, "function");
102
102
  assert.equal(typeof sdk.mandates.create, "function");
103
103
  });
104
- it("compute.invokeBedrock rejects when no owner is signed in", async () => {
104
+ it("compute.invokeBedrock rejects when no owner AND no delegate matches the mandate", async () => {
105
+ // Since alpha.9, invokeBedrock supports a delegate signing path. The
106
+ // failure mode here (no owner loaded AND no imported mandate matching
107
+ // the requested id) returns code `sdk_no_delegate_for_mandate`.
108
+ // The previous `sdk_no_owner` code now applies only to other entry
109
+ // points where there's no delegate fallback.
105
110
  const auth = makeAuth();
106
111
  const sdk = new AithosSDK({
107
112
  auth,
@@ -114,7 +119,8 @@ describe("AithosSDK constructor", () => {
114
119
  mandateId: "mandate:x",
115
120
  model: "claude-sonnet-4-6",
116
121
  messages: [{ role: "user", content: "hi" }],
117
- }), (e) => e instanceof AithosSDKError && e.code === "sdk_no_owner");
122
+ }), (e) => e instanceof AithosSDKError &&
123
+ e.code === "sdk_no_delegate_for_mandate");
118
124
  });
119
125
  });
120
126
  //# sourceMappingURL=sdk.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0-alpha.9",
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",
@@ -52,10 +52,10 @@
52
52
  "node": ">=20"
53
53
  },
54
54
  "peerDependencies": {
55
- "@aithos/protocol-client": ">=0.1.0-alpha.12 <0.2.0"
55
+ "@aithos/protocol-client": ">=0.1.0-alpha.13 <0.2.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@aithos/protocol-client": "^0.1.0-alpha.12",
58
+ "@aithos/protocol-client": "^0.1.0-alpha.13",
59
59
  "@types/node": "^24.12.2",
60
60
  "fake-indexeddb": "^6.2.5",
61
61
  "typescript": "^5.9.2"