@bondedhq/sdk 0.1.6 → 0.2.0
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 +68 -20
- package/dist/agent-url.d.ts +30 -12
- package/dist/agent-url.js +73 -35
- package/dist/episode.d.ts +51 -0
- package/dist/episode.js +40 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/signature.d.ts +41 -25
- package/dist/signature.js +43 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -388,6 +388,27 @@ if (isBondedEpisode()) {
|
|
|
388
388
|
`isBondedEpisode()` is true when the runner set valid `BONDED_*` variables. `fromBondedEnv` reads
|
|
389
389
|
and checks them, naming every missing or malformed one (`BondedError`, code `EPISODE_ENV`).
|
|
390
390
|
|
|
391
|
+
That is the vault world, where ratings run. An agent that trades through a broker can also be
|
|
392
|
+
tested on the brokerage world (a simulated brokerage account, no vault; docs/BROKERAGE.md), with a
|
|
393
|
+
runner started with `--world brokerage`. Its episode is read with `fromBondedBrokerageEnv()`, and
|
|
394
|
+
`bondedEpisodeWorld()` says which world the runner set up:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { bondedEpisodeWorld, fromBondedBrokerageEnv, fromBondedEnv } from "@bondedhq/sdk";
|
|
398
|
+
|
|
399
|
+
const world = bondedEpisodeWorld(); // "vault", "brokerage", or undefined outside an episode
|
|
400
|
+
if (world === "brokerage") {
|
|
401
|
+
const episode = fromBondedBrokerageEnv(); // { world, episodeId, task, mode: "tools", endpoints, deadlineMs }
|
|
402
|
+
} else if (world === "vault") {
|
|
403
|
+
const episode = fromBondedEnv();
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`isBondedEpisode()` is true in both worlds, so an agent under test never falls through to its
|
|
408
|
+
production tools. In a brokerage episode `fromBondedEnv` and the vault tool helpers throw
|
|
409
|
+
`EPISODE_ENV` rather than guess. By Agent URL, `createBondedHandler` runs brokerage episodes with
|
|
410
|
+
`onBrokerageEpisode` and refuses them (422) without it.
|
|
411
|
+
|
|
391
412
|
The mode decides how the agent acts:
|
|
392
413
|
|
|
393
414
|
- **tools**: reads and writes are tool calls; `swap`, `transfer` and `approve` are tools, and the
|
|
@@ -537,19 +558,20 @@ an episode. `mcpConfigJson(episode, options)` returns the same text.
|
|
|
537
558
|
|
|
538
559
|
### Agent URL
|
|
539
560
|
|
|
540
|
-
In Agent URL mode Bonded `POST`s each episode to your HTTPS endpoint, signed with
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
endpoint: a standard Fetch API handler for
|
|
545
|
-
Workers, with a Node adapter for Express
|
|
561
|
+
In Agent URL mode Bonded `POST`s each episode to your HTTPS endpoint, signed with Bonded's own
|
|
562
|
+
key for the wallet that saved the URL. There is no secret: your handler names its `owner`, your
|
|
563
|
+
wallet, in code (it's public), and refuses calls signed for anyone else. Deploy it, save the URL
|
|
564
|
+
on Bonded from that wallet, then verify: Bonded sends a signed challenge and your endpoint answers
|
|
565
|
+
with its owner. `createBondedHandler` is the whole endpoint: a standard Fetch API handler for
|
|
566
|
+
Hono, Next.js route handlers, Bun, Deno and Cloudflare Workers, with a Node adapter for Express
|
|
567
|
+
and `node:http`.
|
|
546
568
|
|
|
547
569
|
```ts
|
|
548
570
|
import { createBondedHandler, toNodeHandler } from "@bondedhq/sdk/agent-url";
|
|
549
571
|
import { bondedAiSdkTools } from "@bondedhq/sdk/ai-sdk";
|
|
550
572
|
|
|
551
573
|
const handler = createBondedHandler({
|
|
552
|
-
|
|
574
|
+
owner: "0xYourWallet", // the wallet you save this URL from on Bonded; public, not a secret
|
|
553
575
|
version: process.env.GIT_SHA, // reported when Bonded verifies the URL; becomes the code hash
|
|
554
576
|
async onEpisode(episode, { signal }) {
|
|
555
577
|
const tools = await bondedAiSdkTools(episode);
|
|
@@ -569,15 +591,21 @@ expressApp.post("/bonded", toNodeHandler(handler)); // Express: mount before exp
|
|
|
569
591
|
|
|
570
592
|
What the handler does with each request:
|
|
571
593
|
|
|
572
|
-
1. **Checks the
|
|
573
|
-
within 300 seconds of the server's clock
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
594
|
+
1. **Checks the call** before reading the message. `bonded-timestamp` (unix seconds) must be
|
|
595
|
+
within 300 seconds of the server's clock; `bonded-owner` must be your `owner` (a list works,
|
|
596
|
+
for a team); and `bonded-signature` (`v2=<key id>:<base64url Ed25519 signature>` over
|
|
597
|
+
`v2.<key id>.<timestamp>.<owner>.<raw body>`, up to four while a key rotates) must verify
|
|
598
|
+
under one of Bonded's public keys. The handler fetches those from
|
|
599
|
+
`https://www.bondedhq.xyz/.well-known/bonded-agent-keys.json` (`apiUrl` takes them from
|
|
600
|
+
another Bonded deployment instead, for a local Arena run), keeps them for a day, and fetches
|
|
601
|
+
again at once (at most once a minute) for a key it hasn't seen. A signature it has accepted before is refused as
|
|
602
|
+
a replay. Failures answer `401` (with `code: "wrong-owner"` when the call is
|
|
603
|
+
for a wallet the handler isn't set up for, so Bonded can tell you exactly that), a body that
|
|
604
|
+
isn't one of Bonded's messages `400`, and keys it can't fetch `503`; nothing runs.
|
|
605
|
+
2. `{ type: "verify", challenge }`: answers `200 { challenge, owner, version }`. Bonded marks the
|
|
606
|
+
URL verified when `owner` is the wallet that saved it. The owner is only in the signed
|
|
607
|
+
headers, never the body, so an echo server can't pass, and someone who saves your URL gets
|
|
608
|
+
calls signed for their own wallet, which your handler refuses.
|
|
581
609
|
3. `{ type: "episode", ... }`: answers `202` at once (Bonded waits at most 10 seconds) and runs
|
|
582
610
|
`onEpisode` in the background. When it resolves, the handler posts `/done` with the
|
|
583
611
|
`finalMessage` and `usage` it returned. If it throws, `/done` is still posted, saying the agent
|
|
@@ -588,10 +616,30 @@ What the handler does with each request:
|
|
|
588
616
|
Limits: 256 KiB bodies (`maxBodyBytes`, the most Bonded sends), 8 episodes at once (`maxConcurrent`; Bonded sends one at a
|
|
589
617
|
time per agent), and the same episode delivered twice runs once. On platforms that stop work when
|
|
590
618
|
the response is sent, pass their `waitUntil` (as the option, or the handler's second argument).
|
|
591
|
-
|
|
592
|
-
`
|
|
593
|
-
|
|
594
|
-
|
|
619
|
+
|
|
620
|
+
To test your endpoint, `createTestSigner()` stands in for Bonded: give the handler its `keys`, and
|
|
621
|
+
`sign(owner, body)` returns the headers Bonded would send.
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
import { createBondedHandler, createTestSigner } from "@bondedhq/sdk/agent-url";
|
|
625
|
+
|
|
626
|
+
const owner = "0x1111111111111111111111111111111111111111"; // your wallet
|
|
627
|
+
const bonded = await createTestSigner();
|
|
628
|
+
const handler = createBondedHandler({ owner, keys: bonded.keys, onEpisode });
|
|
629
|
+
const body = JSON.stringify({ type: "verify", challenge: "a-test-challenge-0001" });
|
|
630
|
+
const res = await handler(
|
|
631
|
+
new Request("http://localhost/bonded", {
|
|
632
|
+
method: "POST",
|
|
633
|
+
headers: { "content-type": "application/json", ...(await bonded.sign(owner, body)) },
|
|
634
|
+
body,
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
// 200 { challenge: "a-test-challenge-0001", owner: "0x1111…1111" }
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`verifyAgentUrlRequest(...)` is the check on its own and `answerAgentUrlVerify(...)` the verify
|
|
641
|
+
answer, from `@bondedhq/shared/agent-url` (a small entry point without the contract ABIs, for
|
|
642
|
+
edge bundles). A Python endpoint (the standard library and `cryptography`) is in
|
|
595
643
|
[`examples/python-agent-url`](../../examples/python-agent-url/README.md).
|
|
596
644
|
|
|
597
645
|
## Vault trading tools for LangChain, LangGraph and the AI SDK
|
package/dist/agent-url.d.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type
|
|
2
|
+
import { type AgentUrlPublicKey } from "@bondedhq/shared/agent-url";
|
|
3
|
+
import type { BondedBrokerageEpisode, BondedEpisode } from "./episode.js";
|
|
3
4
|
import type { EpisodeUsage } from "./mcp.js";
|
|
4
|
-
/** For your own handler: the
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
5
|
+
/** For your own handler: the verify answer, and the request check. */
|
|
6
|
+
export { AGENT_URL_KEYS_PATH, AgentUrlKeys, type AgentUrlPublicKey, answerAgentUrlVerify, verifyAgentUrlRequest, } from "@bondedhq/shared/agent-url";
|
|
7
|
+
export { BONDED_AGENT_KEYS, BONDED_AGENT_KEYS_URL, createTestSigner, keysUrlOf, OWNER_HEADER, SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, TIMESTAMP_HEADER, } from "./signature.js";
|
|
7
8
|
/**
|
|
8
9
|
* Agent URL mode: instead of running the Bonded runner, an agent serves one HTTPS endpoint and
|
|
9
10
|
* Bonded calls it once per episode. Every call is a signed `POST` of one JSON message:
|
|
10
11
|
*
|
|
11
|
-
* - `{ type: "verify", challenge }` when the URL is
|
|
12
|
-
* `200 { challenge,
|
|
13
|
-
* `"verify." + challenge` under the secret.
|
|
12
|
+
* - `{ type: "verify", challenge }` when the URL is saved: answer
|
|
13
|
+
* `200 { challenge, owner, version? }`, where `owner` is your wallet.
|
|
14
14
|
* - `{ type: "episode", ... }` (the runner protocol's `EpisodeMessage`): answer `2xx` within
|
|
15
15
|
* 10 seconds, then work: call the episode's tools and `POST {done}` when finished.
|
|
16
16
|
* - `{ type: "cancel", episodeId }`: stop that episode (best effort).
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Every call is signed by Bonded's own key and names the owner it is for; there is no secret to
|
|
19
|
+
* set. `createBondedHandler` does all of that: it checks signatures against Bonded's published
|
|
20
|
+
* keys, refuses calls for any wallet but `owner`, answers challenges, acknowledges an
|
|
19
21
|
* episode at once and runs your `onEpisode` in the background, aborts it on cancel or at the
|
|
20
22
|
* deadline, and posts `/done` (with the usage you return) when it resolves.
|
|
21
23
|
*/
|
|
@@ -34,12 +36,28 @@ export interface EpisodeContext {
|
|
|
34
36
|
}
|
|
35
37
|
export interface BondedHandlerOptions {
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
+
* Your wallet: the one you sign in to Bonded with and save this URL from (or several, for a
|
|
40
|
+
* team). Bonded signs each call for the wallet that saved the URL, and the handler refuses calls
|
|
41
|
+
* for anyone else, so nobody can claim your URL as theirs. It's public, not a secret.
|
|
39
42
|
*/
|
|
40
|
-
|
|
43
|
+
owner: string | string[];
|
|
44
|
+
/**
|
|
45
|
+
* Another Bonded deployment to take the signing keys from (a local Arena, a staging API). Leave
|
|
46
|
+
* it out for Bonded itself: the keys then come from BONDED_AGENT_KEYS_URL.
|
|
47
|
+
*/
|
|
48
|
+
apiUrl?: string;
|
|
49
|
+
/** Bonded's public keys, given directly instead of fetched (for tests: `createTestSigner`). */
|
|
50
|
+
keys?: AgentUrlPublicKey[];
|
|
41
51
|
/** Runs one episode. Resolve when the agent is finished; the handler then posts `/done`. */
|
|
42
52
|
onEpisode(episode: BondedEpisode, context: EpisodeContext): Promise<EpisodeOutcome | void> | EpisodeOutcome | void;
|
|
53
|
+
/**
|
|
54
|
+
* Runs one brokerage episode (a simulated brokerage account; docs/BROKERAGE.md). Without it, the
|
|
55
|
+
* handler refuses brokerage episodes with 422, so an agent built for the vault world never gets
|
|
56
|
+
* one by surprise. Errors are passed to `onBrokerageError` (default `console.error`).
|
|
57
|
+
*/
|
|
58
|
+
onBrokerageEpisode?(episode: BondedBrokerageEpisode, context: EpisodeContext): Promise<EpisodeOutcome | void> | EpisodeOutcome | void;
|
|
59
|
+
/** Called when `onBrokerageEpisode` throws (default: `console.error`). `/done` is still posted. */
|
|
60
|
+
onBrokerageError?: (error: unknown, episode: BondedBrokerageEpisode) => void;
|
|
43
61
|
/**
|
|
44
62
|
* Your agent's version (a release tag, a git commit). Reported when Bonded verifies the URL;
|
|
45
63
|
* it becomes the rating's code hash when you don't declare one.
|
|
@@ -72,7 +90,7 @@ export type BondedHandler = (request: Request, context?: {
|
|
|
72
90
|
*
|
|
73
91
|
* ```ts
|
|
74
92
|
* const handler = createBondedHandler({
|
|
75
|
-
*
|
|
93
|
+
* owner: "0xYourWallet",
|
|
76
94
|
* version: process.env.GIT_SHA,
|
|
77
95
|
* async onEpisode(episode, { signal }) {
|
|
78
96
|
* const tools = await bondedAiSdkTools(episode);
|
package/dist/agent-url.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { AGENT_URL_MAX_BODY_BYTES, AGENT_URL_TOLERANCE_SECONDS, AgentUrlReplayGuard, answerAgentUrlVerify, verifyAgentUrlRequest, } from "@bondedhq/shared/agent-url";
|
|
1
|
+
import { AGENT_URL_MAX_BODY_BYTES, AGENT_URL_TOLERANCE_SECONDS, AgentUrlKeys, AgentUrlReplayGuard, agentUrlSignatureKeyIds, answerAgentUrlVerify, isAgentUrlOwner, verifyAgentUrlRequest, } from "@bondedhq/shared/agent-url";
|
|
2
|
+
import { BONDED_AGENT_KEYS, BONDED_AGENT_KEYS_URL, keysUrlOf } from "./signature.js";
|
|
2
3
|
import { finishEpisode } from "./done.js";
|
|
3
|
-
/** For your own handler: the
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
4
|
+
/** For your own handler: the verify answer, and the request check. */
|
|
5
|
+
export { AGENT_URL_KEYS_PATH, AgentUrlKeys, answerAgentUrlVerify, verifyAgentUrlRequest, } from "@bondedhq/shared/agent-url";
|
|
6
|
+
export { BONDED_AGENT_KEYS, BONDED_AGENT_KEYS_URL, createTestSigner, keysUrlOf, OWNER_HEADER, SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, TIMESTAMP_HEADER, } from "./signature.js";
|
|
6
7
|
/**
|
|
7
8
|
* An Agent URL endpoint for your agent.
|
|
8
9
|
*
|
|
9
10
|
* ```ts
|
|
10
11
|
* const handler = createBondedHandler({
|
|
11
|
-
*
|
|
12
|
+
* owner: "0xYourWallet",
|
|
12
13
|
* version: process.env.GIT_SHA,
|
|
13
14
|
* async onEpisode(episode, { signal }) {
|
|
14
15
|
* const tools = await bondedAiSdkTools(episode);
|
|
@@ -20,29 +21,49 @@ export { SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, signBondedRequest, TIMES
|
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
22
23
|
export function createBondedHandler(options) {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const owners = Array.isArray(options.owner) ? options.owner : [options.owner];
|
|
25
|
+
if (owners.length === 0 || !owners.every((o) => typeof o === "string" && isAgentUrlOwner(o)))
|
|
26
|
+
throw new Error("createBondedHandler needs owner: your wallet address (0x…), the one you save this URL from");
|
|
27
|
+
const keyStore = options.keys
|
|
28
|
+
? undefined
|
|
29
|
+
: new AgentUrlKeys(options.apiUrl ? keysUrlOf(options.apiUrl) : BONDED_AGENT_KEYS_URL, {
|
|
30
|
+
// The built-in keys are Bonded's own: never a fallback for another deployment's.
|
|
31
|
+
...(options.apiUrl ? {} : { fallback: BONDED_AGENT_KEYS }),
|
|
32
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
33
|
+
...(options.now ? { now: options.now } : {}),
|
|
34
|
+
});
|
|
25
35
|
const now = options.now ?? Date.now;
|
|
26
36
|
const tolerance = options.toleranceSeconds ?? AGENT_URL_TOLERANCE_SECONDS;
|
|
27
37
|
const maxBody = options.maxBodyBytes ?? AGENT_URL_MAX_BODY_BYTES;
|
|
28
38
|
const maxConcurrent = options.maxConcurrent ?? 8;
|
|
29
39
|
const onError = options.onError ??
|
|
30
40
|
((error, episode) => console.error(`Bonded episode ${episode.episodeId} failed:`, error));
|
|
31
|
-
const secrets = Array.isArray(options.secret) ? options.secret : [options.secret];
|
|
32
41
|
/** Signatures already accepted, until they fall out of the window: replays are refused. */
|
|
33
42
|
const replay = new AgentUrlReplayGuard(tolerance);
|
|
34
43
|
const running = new Map();
|
|
35
|
-
const
|
|
44
|
+
const onBrokerageError = options.onBrokerageError ??
|
|
45
|
+
((error, episode) => console.error(`Bonded brokerage episode ${episode.episodeId} failed:`, error));
|
|
46
|
+
const run = async (episode, controller, work, reportError) => {
|
|
36
47
|
const deadline = setTimeout(() => controller.abort(new Error("the episode's deadline passed")), episode.deadlineMs);
|
|
48
|
+
// A reporter that throws must not stop `/done` from being posted: the gateway would wait out
|
|
49
|
+
// the deadline for an episode that has already failed.
|
|
50
|
+
const report = (error) => {
|
|
51
|
+
try {
|
|
52
|
+
reportError(error, episode);
|
|
53
|
+
}
|
|
54
|
+
catch (reporterError) {
|
|
55
|
+
console.error(`Bonded episode ${episode.episodeId}: the error reporter threw:`, reporterError);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
37
58
|
let outcome;
|
|
38
59
|
let failed = false;
|
|
39
60
|
try {
|
|
40
|
-
outcome = await
|
|
61
|
+
outcome = await work(episode, { signal: controller.signal });
|
|
41
62
|
}
|
|
42
63
|
catch (error) {
|
|
43
64
|
failed = true;
|
|
44
65
|
if (!controller.signal.aborted)
|
|
45
|
-
|
|
66
|
+
report(error);
|
|
46
67
|
outcome = {
|
|
47
68
|
finalMessage: `the agent failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 4_000),
|
|
48
69
|
};
|
|
@@ -64,7 +85,7 @@ export function createBondedHandler(options) {
|
|
|
64
85
|
}
|
|
65
86
|
catch (error) {
|
|
66
87
|
if (!failed)
|
|
67
|
-
|
|
88
|
+
report(error);
|
|
68
89
|
}
|
|
69
90
|
};
|
|
70
91
|
return async (request, context) => {
|
|
@@ -73,8 +94,19 @@ export function createBondedHandler(options) {
|
|
|
73
94
|
const body = await readCapped(request, maxBody);
|
|
74
95
|
if (body === undefined)
|
|
75
96
|
return json(413, { error: `the body is over ${maxBody} bytes` });
|
|
97
|
+
let keys;
|
|
98
|
+
try {
|
|
99
|
+
keys =
|
|
100
|
+
options.keys ??
|
|
101
|
+
(await keyStore.get(agentUrlSignatureKeyIds(request.headers).find(Boolean)));
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error("Bonded: couldn't fetch Bonded's signing keys:", error);
|
|
105
|
+
return json(503, { error: "the agent couldn't fetch Bonded's signing keys; try again" });
|
|
106
|
+
}
|
|
76
107
|
const check = await verifyAgentUrlRequest({
|
|
77
|
-
|
|
108
|
+
owner: owners,
|
|
109
|
+
keys,
|
|
78
110
|
body,
|
|
79
111
|
headers: request.headers,
|
|
80
112
|
nowMs: now(),
|
|
@@ -82,45 +114,51 @@ export function createBondedHandler(options) {
|
|
|
82
114
|
replay,
|
|
83
115
|
});
|
|
84
116
|
if (!check.ok)
|
|
85
|
-
return json(check.status, {
|
|
117
|
+
return json(check.status, {
|
|
118
|
+
error: check.reason,
|
|
119
|
+
...(check.code ? { code: check.code } : {}),
|
|
120
|
+
});
|
|
86
121
|
const message = check.message;
|
|
87
|
-
// The answer
|
|
122
|
+
// The answer names the owner the call was signed for (one of ours, checked above). The verify
|
|
123
|
+
// body carries no owner, so a server that echoes it back can't pass.
|
|
88
124
|
if (message.type === "verify")
|
|
89
|
-
return json(200,
|
|
125
|
+
return json(200, answerAgentUrlVerify(check.owner, message, options.version));
|
|
90
126
|
if (message.type === "cancel") {
|
|
91
127
|
const controller = running.get(message.episodeId);
|
|
92
128
|
controller?.abort(new Error("Bonded cancelled the episode"));
|
|
93
129
|
return json(200, { ok: true, cancelled: controller !== undefined });
|
|
94
130
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
if (message.world === "brokerage" && !options.onBrokerageEpisode)
|
|
132
|
+
return json(422, {
|
|
133
|
+
error: "this agent doesn't take brokerage episodes (no onBrokerageEpisode)",
|
|
134
|
+
});
|
|
135
|
+
if (running.has(message.episodeId))
|
|
136
|
+
return json(200, { ok: true, episodeId: message.episodeId, alreadyRunning: true });
|
|
98
137
|
if (running.size >= maxConcurrent)
|
|
99
138
|
return json(429, { error: `already running ${running.size} episodes` });
|
|
100
139
|
const controller = new AbortController();
|
|
101
|
-
running.set(
|
|
102
|
-
const work =
|
|
140
|
+
running.set(message.episodeId, controller);
|
|
141
|
+
const work = message.world === "brokerage"
|
|
142
|
+
? run(toBrokerageEpisode(message), controller, (e, c) => options.onBrokerageEpisode(e, c), onBrokerageError)
|
|
143
|
+
: run(toEpisode(message), controller, (e, c) => options.onEpisode(e, c), onError);
|
|
103
144
|
// Called as a method: Cloudflare's ctx.waitUntil needs its `this`.
|
|
104
145
|
if (context?.waitUntil)
|
|
105
146
|
context.waitUntil(work);
|
|
106
147
|
else
|
|
107
148
|
options.waitUntil?.(work);
|
|
108
|
-
return json(202, { ok: true, episodeId:
|
|
149
|
+
return json(202, { ok: true, episodeId: message.episodeId });
|
|
109
150
|
};
|
|
110
151
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return secret;
|
|
122
|
-
}
|
|
123
|
-
return secrets[0];
|
|
152
|
+
/** A brokerage episode as the agent sees it (the same shape `fromBondedBrokerageEnv` returns). */
|
|
153
|
+
function toBrokerageEpisode(message) {
|
|
154
|
+
return {
|
|
155
|
+
world: "brokerage",
|
|
156
|
+
episodeId: message.episodeId,
|
|
157
|
+
task: message.task,
|
|
158
|
+
mode: "tools",
|
|
159
|
+
endpoints: message.endpoints,
|
|
160
|
+
deadlineMs: message.deadlineMs,
|
|
161
|
+
};
|
|
124
162
|
}
|
|
125
163
|
/** The episode as the agent sees it (the same shape `fromBondedEnv` returns). */
|
|
126
164
|
function toEpisode(message) {
|
package/dist/episode.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface BondedEpisode {
|
|
|
41
41
|
* next to `episodeEnv` that writes them, so the runner, the gateway and the SDK can't drift.
|
|
42
42
|
*/
|
|
43
43
|
export declare const bondedEpisodeEnv: import("zod").ZodObject<{
|
|
44
|
+
BONDED_WORLD: import("zod").ZodOptional<import("zod").ZodLiteral<"vault">>;
|
|
44
45
|
BONDED_EPISODE_ID: import("zod").ZodString;
|
|
45
46
|
BONDED_MODE: import("zod").ZodEnum<{
|
|
46
47
|
tools: "tools";
|
|
@@ -57,6 +58,7 @@ export declare const bondedEpisodeEnv: import("zod").ZodObject<{
|
|
|
57
58
|
approvalAllowlist: import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<`0x${string}`, string>>>;
|
|
58
59
|
}, import("zod/v4/core").$strip>>;
|
|
59
60
|
maxSteps: import("zod").ZodNumber;
|
|
61
|
+
tools: import("zod").ZodOptional<import("zod").ZodReadonly<import("zod").ZodArray<import("zod").ZodString>>>;
|
|
60
62
|
}, import("zod/v4/core").$strip>>;
|
|
61
63
|
BONDED_MCP_URL: import("zod").ZodURL;
|
|
62
64
|
BONDED_TOOLS_URL: import("zod").ZodURL;
|
|
@@ -84,6 +86,11 @@ export declare const bondedEpisodeEnv: import("zod").ZodObject<{
|
|
|
84
86
|
* ```ts
|
|
85
87
|
* const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
|
|
86
88
|
* ```
|
|
89
|
+
*
|
|
90
|
+
* True in every world, so an agent under test never falls through to its production tools. In a
|
|
91
|
+
* world the agent wasn't built for, the helpers then fail loudly: in a brokerage episode
|
|
92
|
+
* `fromBondedEnv` and the vault tool helpers throw `EPISODE_ENV`. `bondedEpisodeWorld()` says
|
|
93
|
+
* which world it is.
|
|
87
94
|
*/
|
|
88
95
|
export declare function isBondedEpisode(env?: Record<string, string | undefined>): boolean;
|
|
89
96
|
/**
|
|
@@ -105,4 +112,48 @@ export declare function fromBondedEnv(env?: Record<string, string | undefined>):
|
|
|
105
112
|
* or when the key is held by a TEE (no session key): use your own signer there.
|
|
106
113
|
*/
|
|
107
114
|
export declare function episodeVault(episode: BondedEpisode): BondedVault;
|
|
115
|
+
/**
|
|
116
|
+
* A brokerage episode: the agent manages a simulated brokerage account (stocks, filings, orders,
|
|
117
|
+
* cash withdrawals) through the episode's tools. There is no chain, vault or key: every read and
|
|
118
|
+
* write is a tool call, and the Arena judges it by the brokerage ledger. Needs runner protocol 2
|
|
119
|
+
* (`@bondedhq/runner` 0.1.4 or later), started with `--world brokerage`.
|
|
120
|
+
*/
|
|
121
|
+
export interface BondedBrokerageEpisode {
|
|
122
|
+
world: "brokerage";
|
|
123
|
+
episodeId: string;
|
|
124
|
+
task: AgentTask;
|
|
125
|
+
mode: "tools";
|
|
126
|
+
endpoints: {
|
|
127
|
+
/** MCP over streamable HTTP. */
|
|
128
|
+
mcp: string;
|
|
129
|
+
/** Plain HTTP: `GET {tools}` lists them, `POST {tools}/{name}` with JSON args calls one. */
|
|
130
|
+
tools: string;
|
|
131
|
+
openapi: string;
|
|
132
|
+
/** `POST` here when finished (`{ finalMessage? }`). */
|
|
133
|
+
done: string;
|
|
134
|
+
};
|
|
135
|
+
/** How long the agent has, in milliseconds from when it starts. */
|
|
136
|
+
deadlineMs: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Which world the runner's episode is for: `vault` (on-chain trading from a Mandate Vault, read
|
|
140
|
+
* with `fromBondedEnv`), `brokerage` (read with `fromBondedBrokerageEnv`), or undefined when this
|
|
141
|
+
* process isn't running a Bonded episode.
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* const world = bondedEpisodeWorld();
|
|
145
|
+
* const tools =
|
|
146
|
+
* world === "brokerage"
|
|
147
|
+
* ? await myBrokerageTools(fromBondedBrokerageEnv())
|
|
148
|
+
* : world === "vault"
|
|
149
|
+
* ? await bondedLangChainTools()
|
|
150
|
+
* : productionTools;
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export declare function bondedEpisodeWorld(env?: Record<string, string | undefined>): "vault" | "brokerage" | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* Reads a brokerage episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
156
|
+
* (`EPISODE_ENV`) naming every missing or malformed variable.
|
|
157
|
+
*/
|
|
158
|
+
export declare function fromBondedBrokerageEnv(env?: Record<string, string | undefined>): BondedBrokerageEpisode;
|
|
108
159
|
//# sourceMappingURL=episode.d.ts.map
|
package/dist/episode.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EpisodeEnv, parseEpisodeEnv } from "@bondedhq/shared";
|
|
1
|
+
import { EpisodeEnv, parseBrokerageEpisodeEnv, parseEpisodeEnv, } from "@bondedhq/shared";
|
|
2
2
|
import { defineChain } from "viem";
|
|
3
3
|
import { privateKeyToAccount } from "viem/accounts";
|
|
4
4
|
import { BondedError } from "./errors.js";
|
|
@@ -15,9 +15,14 @@ export const bondedEpisodeEnv = EpisodeEnv;
|
|
|
15
15
|
* ```ts
|
|
16
16
|
* const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
|
|
17
17
|
* ```
|
|
18
|
+
*
|
|
19
|
+
* True in every world, so an agent under test never falls through to its production tools. In a
|
|
20
|
+
* world the agent wasn't built for, the helpers then fail loudly: in a brokerage episode
|
|
21
|
+
* `fromBondedEnv` and the vault tool helpers throw `EPISODE_ENV`. `bondedEpisodeWorld()` says
|
|
22
|
+
* which world it is.
|
|
18
23
|
*/
|
|
19
24
|
export function isBondedEpisode(env = typeof process === "undefined" ? {} : process.env) {
|
|
20
|
-
return
|
|
25
|
+
return bondedEpisodeWorld(env) !== undefined;
|
|
21
26
|
}
|
|
22
27
|
/**
|
|
23
28
|
* Reads the episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
@@ -62,4 +67,37 @@ export function episodeVault(episode) {
|
|
|
62
67
|
router: episode.contracts.exchange,
|
|
63
68
|
});
|
|
64
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Which world the runner's episode is for: `vault` (on-chain trading from a Mandate Vault, read
|
|
72
|
+
* with `fromBondedEnv`), `brokerage` (read with `fromBondedBrokerageEnv`), or undefined when this
|
|
73
|
+
* process isn't running a Bonded episode.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* const world = bondedEpisodeWorld();
|
|
77
|
+
* const tools =
|
|
78
|
+
* world === "brokerage"
|
|
79
|
+
* ? await myBrokerageTools(fromBondedBrokerageEnv())
|
|
80
|
+
* : world === "vault"
|
|
81
|
+
* ? await bondedLangChainTools()
|
|
82
|
+
* : productionTools;
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function bondedEpisodeWorld(env = typeof process === "undefined" ? {} : process.env) {
|
|
86
|
+
// Fails closed: once the runner has named the brokerage world for an episode, that is the
|
|
87
|
+
// answer even if another variable is malformed. `fromBondedBrokerageEnv` then names the bad
|
|
88
|
+
// variable, where "not an episode" would send the agent to its production tools.
|
|
89
|
+
if (env.BONDED_WORLD === "brokerage" && env.BONDED_EPISODE_ID?.trim())
|
|
90
|
+
return "brokerage";
|
|
91
|
+
return parseEpisodeEnv(env).ok ? "vault" : undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reads a brokerage episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
95
|
+
* (`EPISODE_ENV`) naming every missing or malformed variable.
|
|
96
|
+
*/
|
|
97
|
+
export function fromBondedBrokerageEnv(env = process.env) {
|
|
98
|
+
const parsed = parseBrokerageEpisodeEnv(env);
|
|
99
|
+
if (!parsed.ok)
|
|
100
|
+
throw new BondedError("EPISODE_ENV", `not a valid Bonded brokerage episode environment: ${parsed.problems.join("; ")}`);
|
|
101
|
+
return parsed.episode;
|
|
102
|
+
}
|
|
65
103
|
//# sourceMappingURL=episode.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,6 @@ export { BONDED_API_URL, PaymentGate, paymentGate, type GateAnswer, type GateChe
|
|
|
24
24
|
export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, type GuardPaymentsOptions, type GuardVendorChangesOptions, type PaymentBreach, type PaymentGuardOptions, type PaymentGuardState, type PaymentMandate, type PaymentSource, type PaymentVendor, type PaymentVerdict, } from "./payments.js";
|
|
25
25
|
export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, type GateRule, type PaidPayment, type PayableInvoice, type PayableRefund, type PaymentAttempt, type PaymentRule, type VerifiedChange, } from "@bondedhq/shared";
|
|
26
26
|
export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, type BondedVaultTool, type BondedVaultToolName, type BondedVaultToolsOptions, } from "./vault-tools.js";
|
|
27
|
-
export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, type BondedEpisode, } from "./episode.js";
|
|
27
|
+
export { bondedEpisodeEnv, bondedEpisodeWorld, episodeVault, fromBondedBrokerageEnv, fromBondedEnv, isBondedEpisode, type BondedBrokerageEpisode, type BondedEpisode, } from "./episode.js";
|
|
28
28
|
export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, type AllowanceCheck, type AllowanceFinding, type TokenRef, installMandateModule, type InstallMandateModuleInput, type ModuleAccountType, type ModuleInstallPlan, } from "./module.js";
|
|
29
29
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,6 @@ export { BONDED_API_URL, PaymentGate, paymentGate, } from "./gate.js";
|
|
|
23
23
|
export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, } from "./payments.js";
|
|
24
24
|
export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, } from "@bondedhq/shared";
|
|
25
25
|
export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, } from "./vault-tools.js";
|
|
26
|
-
export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, } from "./episode.js";
|
|
26
|
+
export { bondedEpisodeEnv, bondedEpisodeWorld, episodeVault, fromBondedBrokerageEnv, fromBondedEnv, isBondedEpisode, } from "./episode.js";
|
|
27
27
|
export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, installMandateModule, } from "./module.js";
|
|
28
28
|
//# sourceMappingURL=index.js.map
|
package/dist/signature.d.ts
CHANGED
|
@@ -1,34 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent URL request signatures: how an agent knows a request really comes from Bonded, for its
|
|
3
|
+
* owner.
|
|
4
|
+
*
|
|
5
|
+
* Bonded signs every call to an Agent URL with its own Ed25519 key (no shared secret):
|
|
6
|
+
* - `bonded-timestamp`: unix seconds;
|
|
7
|
+
* - `bonded-owner`: the wallet that saved the URL on Bonded;
|
|
8
|
+
* - `bonded-signature`: `v2=<key id>:<base64url signature>` over
|
|
9
|
+
* `v2.<key id>.<timestamp>.<owner>.<raw body>`.
|
|
10
|
+
*
|
|
11
|
+
* The handler checks it against Bonded's published keys (BONDED_AGENT_KEYS_URL) and refuses calls
|
|
12
|
+
* for any owner but its own. The scheme lives in @bondedhq/shared
|
|
13
|
+
* (`@bondedhq/shared/agent-url`, which the gateway signs with). Web Crypto only, so it runs in
|
|
14
|
+
* Node 22+, Bun, Deno, Cloudflare Workers and edge runtimes.
|
|
15
|
+
*/
|
|
16
|
+
import { type AgentUrlPublicKey } from "@bondedhq/shared/agent-url";
|
|
1
17
|
export declare const SIGNATURE_HEADER = "bonded-signature";
|
|
2
18
|
export declare const TIMESTAMP_HEADER = "bonded-timestamp";
|
|
19
|
+
export declare const OWNER_HEADER = "bonded-owner";
|
|
3
20
|
/** How far a request's timestamp may be from the agent's clock, either way. */
|
|
4
21
|
export declare const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
5
22
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
23
|
+
* Where Bonded publishes the keys that sign calls to agents. It is on Bonded's own domain, not the
|
|
24
|
+
* API's host name, because this address is built into every deployed handler: the API can move
|
|
25
|
+
* without breaking them.
|
|
8
26
|
*/
|
|
9
|
-
export declare
|
|
10
|
-
"bonded-timestamp": string;
|
|
11
|
-
"bonded-signature": string;
|
|
12
|
-
}>;
|
|
13
|
-
export type SignatureCheck = {
|
|
14
|
-
ok: true;
|
|
15
|
-
timestamp: number;
|
|
16
|
-
} | {
|
|
17
|
-
ok: false;
|
|
18
|
-
reason: string;
|
|
19
|
-
};
|
|
27
|
+
export declare const BONDED_AGENT_KEYS_URL = "https://www.bondedhq.xyz/.well-known/bonded-agent-keys.json";
|
|
20
28
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
29
|
+
* Bonded's production keys, built in: used only when BONDED_AGENT_KEYS_URL can't be reached and
|
|
30
|
+
* no key set was fetched before, so a handler still starts then. The same set the keys address
|
|
31
|
+
* publishes (docs/AGENT-URL-SIGNED-CALLS.md, Section 6): on a key rotation, add the new public key
|
|
32
|
+
* here before a release.
|
|
24
33
|
*/
|
|
25
|
-
export declare
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
export declare const BONDED_AGENT_KEYS: AgentUrlPublicKey[];
|
|
35
|
+
/** Where another Bonded deployment (a local Arena, a staging API) publishes its keys. */
|
|
36
|
+
export declare const keysUrlOf: (apiUrl: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* A stand-in for Bonded, for testing your endpoint locally: a fresh key, `sign` to sign a body
|
|
39
|
+
* for an owner the way Bonded does, and `keys` to give the handler (`createBondedHandler({ keys })`).
|
|
40
|
+
* Bonded signs real calls itself, with keys your handler fetches.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createTestSigner(): Promise<{
|
|
43
|
+
keys: AgentUrlPublicKey[];
|
|
44
|
+
sign(owner: string, body: string, timestamp?: number): Promise<{
|
|
45
|
+
"bonded-timestamp": string;
|
|
46
|
+
"bonded-owner": string;
|
|
47
|
+
"bonded-signature": string;
|
|
48
|
+
}>;
|
|
49
|
+
}>;
|
|
34
50
|
//# sourceMappingURL=signature.d.ts.map
|
package/dist/signature.js
CHANGED
|
@@ -1,52 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent URL request signatures: how an agent knows a request really comes from Bonded
|
|
2
|
+
* Agent URL request signatures: how an agent knows a request really comes from Bonded, for its
|
|
3
|
+
* owner.
|
|
3
4
|
*
|
|
4
|
-
* Bonded signs every call to an Agent URL with
|
|
5
|
+
* Bonded signs every call to an Agent URL with its own Ed25519 key (no shared secret):
|
|
5
6
|
* - `bonded-timestamp`: unix seconds;
|
|
6
|
-
* - `bonded-
|
|
7
|
-
*
|
|
7
|
+
* - `bonded-owner`: the wallet that saved the URL on Bonded;
|
|
8
|
+
* - `bonded-signature`: `v2=<key id>:<base64url signature>` over
|
|
9
|
+
* `v2.<key id>.<timestamp>.<owner>.<raw body>`.
|
|
8
10
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* The handler checks it against Bonded's published keys (BONDED_AGENT_KEYS_URL) and refuses calls
|
|
12
|
+
* for any owner but its own. The scheme lives in @bondedhq/shared
|
|
13
|
+
* (`@bondedhq/shared/agent-url`, which the gateway signs with). Web Crypto only, so it runs in
|
|
14
|
+
* Node 22+, Bun, Deno, Cloudflare Workers and edge runtimes.
|
|
12
15
|
*/
|
|
13
|
-
import { AGENT_URL_SIGNATURE_HEADER, AGENT_URL_TIMESTAMP_HEADER, AGENT_URL_TOLERANCE_SECONDS,
|
|
16
|
+
import { AGENT_URL_KEYS_PATH, AGENT_URL_OWNER_HEADER, AGENT_URL_SIGNATURE_HEADER, AGENT_URL_TIMESTAMP_HEADER, AGENT_URL_TOLERANCE_SECONDS, agentUrlSigner, newAgentUrlSigningKey, } from "@bondedhq/shared/agent-url";
|
|
14
17
|
export const SIGNATURE_HEADER = AGENT_URL_SIGNATURE_HEADER;
|
|
15
18
|
export const TIMESTAMP_HEADER = AGENT_URL_TIMESTAMP_HEADER;
|
|
19
|
+
export const OWNER_HEADER = AGENT_URL_OWNER_HEADER;
|
|
16
20
|
/** How far a request's timestamp may be from the agent's clock, either way. */
|
|
17
21
|
export const SIGNATURE_TOLERANCE_SECONDS = AGENT_URL_TOLERANCE_SECONDS;
|
|
18
22
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
23
|
+
* Where Bonded publishes the keys that sign calls to agents. It is on Bonded's own domain, not the
|
|
24
|
+
* API's host name, because this address is built into every deployed handler: the API can move
|
|
25
|
+
* without breaking them.
|
|
21
26
|
*/
|
|
22
|
-
export
|
|
23
|
-
const headers = await signAgentUrlRequest(secret, body, timestamp * 1000);
|
|
24
|
-
return {
|
|
25
|
-
"bonded-timestamp": headers[TIMESTAMP_HEADER],
|
|
26
|
-
"bonded-signature": headers[SIGNATURE_HEADER],
|
|
27
|
-
};
|
|
28
|
-
}
|
|
27
|
+
export const BONDED_AGENT_KEYS_URL = `https://www.bondedhq.xyz${AGENT_URL_KEYS_PATH}`;
|
|
29
28
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* Bonded's production keys, built in: used only when BONDED_AGENT_KEYS_URL can't be reached and
|
|
30
|
+
* no key set was fetched before, so a handler still starts then. The same set the keys address
|
|
31
|
+
* publishes (docs/AGENT-URL-SIGNED-CALLS.md, Section 6): on a key rotation, add the new public key
|
|
32
|
+
* here before a release.
|
|
33
33
|
*/
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
export const BONDED_AGENT_KEYS = [
|
|
35
|
+
// The first production key, made 2026-11-14.
|
|
36
|
+
{ kid: "ind7eq0xdV0-", alg: "Ed25519", x: "tWhxWPMrq6BKcQs838cUlF_5SecIG1NBy8L_IRIkSBg" },
|
|
37
|
+
];
|
|
38
|
+
/** Where another Bonded deployment (a local Arena, a staging API) publishes its keys. */
|
|
39
|
+
export const keysUrlOf = (apiUrl) => `${apiUrl.replace(/\/+$/, "")}${AGENT_URL_KEYS_PATH}`;
|
|
40
|
+
/**
|
|
41
|
+
* A stand-in for Bonded, for testing your endpoint locally: a fresh key, `sign` to sign a body
|
|
42
|
+
* for an owner the way Bonded does, and `keys` to give the handler (`createBondedHandler({ keys })`).
|
|
43
|
+
* Bonded signs real calls itself, with keys your handler fetches.
|
|
44
|
+
*/
|
|
45
|
+
export async function createTestSigner() {
|
|
46
|
+
const signer = await agentUrlSigner(await newAgentUrlSigningKey());
|
|
47
|
+
return {
|
|
48
|
+
keys: signer.keySet().keys,
|
|
49
|
+
async sign(owner, body, timestamp = Math.floor(Date.now() / 1000)) {
|
|
50
|
+
const headers = await signer.sign(owner, body, timestamp * 1000);
|
|
51
|
+
return {
|
|
52
|
+
"bonded-timestamp": headers[TIMESTAMP_HEADER],
|
|
53
|
+
"bonded-owner": headers[OWNER_HEADER],
|
|
54
|
+
"bonded-signature": headers[SIGNATURE_HEADER],
|
|
55
|
+
};
|
|
41
56
|
},
|
|
42
|
-
|
|
43
|
-
...(input.toleranceSeconds !== undefined ? { toleranceSeconds: input.toleranceSeconds } : {}),
|
|
44
|
-
});
|
|
45
|
-
if (result.ok)
|
|
46
|
-
return { ok: true, timestamp: result.timestamp };
|
|
47
|
-
// 400 means the signature matched but the body isn't one of Bonded's messages.
|
|
48
|
-
if (result.status === 400)
|
|
49
|
-
return { ok: true, timestamp: Number(input.timestamp) };
|
|
50
|
-
return { ok: false, reason: result.reason };
|
|
57
|
+
};
|
|
51
58
|
}
|
|
52
59
|
//# sourceMappingURL=signature.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondedhq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Trade through a Bonded Mandate Vault and get rated by the Bonded Arena from any TypeScript agent: a typed vault client with dry runs, Sentinel checks and a mandate builder; the Arena episode's tools for LangChain, the Vercel AI SDK, ElizaOS and Coinbase AgentKit; a payment guard for pay tools (LangChain, the AI SDK, MCP, the OpenAI Agents SDK) and x402 clients; and an Agent URL handler.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@modelcontextprotocol/sdk": "1.30.1",
|
|
95
95
|
"viem": "^2.56.9",
|
|
96
96
|
"zod": "^4.6.5",
|
|
97
|
-
"@bondedhq/shared": "^0.
|
|
97
|
+
"@bondedhq/shared": "^0.2.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@coinbase/agentkit": "0.10.4",
|