@bondedhq/sdk 0.1.7 → 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 CHANGED
@@ -558,19 +558,20 @@ an episode. `mcpConfigJson(episode, options)` returns the same text.
558
558
 
559
559
  ### Agent URL
560
560
 
561
- In Agent URL mode Bonded `POST`s each episode to your HTTPS endpoint, signed with your agent
562
- secret (`bonded_as_...`, shown once when you register the URL). Registering is two steps: save
563
- the URL and copy the secret into your agent, then verify, when Bonded sends a signed challenge
564
- your endpoint must answer with a proof that it holds the secret. `createBondedHandler` is the whole
565
- endpoint: a standard Fetch API handler for Hono, Next.js route handlers, Bun, Deno and Cloudflare
566
- Workers, with a Node adapter for Express and `node:http`.
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`.
567
568
 
568
569
  ```ts
569
570
  import { createBondedHandler, toNodeHandler } from "@bondedhq/sdk/agent-url";
570
571
  import { bondedAiSdkTools } from "@bondedhq/sdk/ai-sdk";
571
572
 
572
573
  const handler = createBondedHandler({
573
- secret: process.env.BONDED_AGENT_SECRET!,
574
+ owner: "0xYourWallet", // the wallet you save this URL from on Bonded; public, not a secret
574
575
  version: process.env.GIT_SHA, // reported when Bonded verifies the URL; becomes the code hash
575
576
  async onEpisode(episode, { signal }) {
576
577
  const tools = await bondedAiSdkTools(episode);
@@ -590,15 +591,21 @@ expressApp.post("/bonded", toNodeHandler(handler)); // Express: mount before exp
590
591
 
591
592
  What the handler does with each request:
592
593
 
593
- 1. **Checks the signature** before reading the message: `bonded-timestamp` (unix seconds) must be
594
- within 300 seconds of the server's clock, and `bonded-signature`
595
- (`v1=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>`, up to four while a secret rotates)
596
- must match, compared in constant time. `secret` may be a list while you rotate it. A signature
597
- it has accepted before is refused as a replay. Failures answer `401`, and a body that isn't one
598
- of Bonded's messages `400`; nothing runs.
599
- 2. `{ type: "verify", challenge }`: answers `200 { challenge, proof, version }`. `proof` is the
600
- hex HMAC-SHA256 of `"verify." + challenge` under the secret (`agentUrlVerifyProof`), so only a
601
- server holding the secret passes; an echo server doesn't.
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.
602
609
  3. `{ type: "episode", ... }`: answers `202` at once (Bonded waits at most 10 seconds) and runs
603
610
  `onEpisode` in the background. When it resolves, the handler posts `/done` with the
604
611
  `finalMessage` and `usage` it returned. If it throws, `/done` is still posted, saying the agent
@@ -609,10 +616,30 @@ What the handler does with each request:
609
616
  Limits: 256 KiB bodies (`maxBodyBytes`, the most Bonded sends), 8 episodes at once (`maxConcurrent`; Bonded sends one at a
610
617
  time per agent), and the same episode delivered twice runs once. On platforms that stop work when
611
618
  the response is sent, pass their `waitUntil` (as the option, or the handler's second argument).
612
- `signBondedRequest(secret, body)` signs a request the way Bonded does, for testing your endpoint;
613
- `verifyBondedSignature(...)` is the signature check on its own. Both use the scheme Bonded's
614
- gateway signs with, from `@bondedhq/shared/agent-url` (a small entry point without the contract ABIs,
615
- for edge bundles). A Python endpoint with no dependencies is in
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
616
643
  [`examples/python-agent-url`](../../examples/python-agent-url/README.md).
617
644
 
618
645
  ## Vault trading tools for LangChain, LangGraph and the AI SDK
@@ -1,21 +1,23 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type AgentUrlPublicKey } from "@bondedhq/shared/agent-url";
2
3
  import type { BondedBrokerageEpisode, BondedEpisode } from "./episode.js";
3
4
  import type { EpisodeUsage } from "./mcp.js";
4
- /** For your own handler: the proof a verify answer carries, and the whole answer. */
5
- export { agentUrlVerifyProof, answerAgentUrlVerify } from "@bondedhq/shared/agent-url";
6
- export { SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, signBondedRequest, type SignatureCheck, TIMESTAMP_HEADER, verifyBondedSignature, } from "./signature.js";
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 registered: answer
12
- * `200 { challenge, proof, version? }`, where `proof` is the hex HMAC-SHA256 of
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
- * `createBondedHandler` does all of that: it checks signatures, answers challenges, acknowledges an
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,10 +36,18 @@ export interface EpisodeContext {
34
36
  }
35
37
  export interface BondedHandlerOptions {
36
38
  /**
37
- * The agent secret Bonded showed you when you registered the URL (`bonded_as_...`), or several
38
- * while you rotate it (any one may match).
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
- secret: string | string[];
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;
43
53
  /**
@@ -80,7 +90,7 @@ export type BondedHandler = (request: Request, context?: {
80
90
  *
81
91
  * ```ts
82
92
  * const handler = createBondedHandler({
83
- * secret: process.env.BONDED_AGENT_SECRET!,
93
+ * owner: "0xYourWallet",
84
94
  * version: process.env.GIT_SHA,
85
95
  * async onEpisode(episode, { signal }) {
86
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 proof a verify answer carries, and the whole answer. */
4
- export { agentUrlVerifyProof, answerAgentUrlVerify } from "@bondedhq/shared/agent-url";
5
- export { SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, signBondedRequest, TIMESTAMP_HEADER, verifyBondedSignature, } from "./signature.js";
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
- * secret: process.env.BONDED_AGENT_SECRET!,
12
+ * owner: "0xYourWallet",
12
13
  * version: process.env.GIT_SHA,
13
14
  * async onEpisode(episode, { signal }) {
14
15
  * const tools = await bondedAiSdkTools(episode);
@@ -20,15 +21,23 @@ export { SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, signBondedRequest, TIMES
20
21
  * ```
21
22
  */
22
23
  export function createBondedHandler(options) {
23
- if (!options.secret || (Array.isArray(options.secret) && options.secret.length === 0))
24
- throw new Error("createBondedHandler needs the agent secret");
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();
@@ -85,8 +94,19 @@ export function createBondedHandler(options) {
85
94
  const body = await readCapped(request, maxBody);
86
95
  if (body === undefined)
87
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
+ }
88
107
  const check = await verifyAgentUrlRequest({
89
- secret: options.secret,
108
+ owner: owners,
109
+ keys,
90
110
  body,
91
111
  headers: request.headers,
92
112
  nowMs: now(),
@@ -94,11 +114,15 @@ export function createBondedHandler(options) {
94
114
  replay,
95
115
  });
96
116
  if (!check.ok)
97
- return json(check.status, { error: check.reason });
117
+ return json(check.status, {
118
+ error: check.reason,
119
+ ...(check.code ? { code: check.code } : {}),
120
+ });
98
121
  const message = check.message;
99
- // The answer proves the secret (an HMAC of the challenge), so an echo server can't pass.
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.
100
124
  if (message.type === "verify")
101
- return json(200, await answerAgentUrlVerify(await signingSecret(secrets, body, request.headers, now(), tolerance), message, options.version));
125
+ return json(200, answerAgentUrlVerify(check.owner, message, options.version));
102
126
  if (message.type === "cancel") {
103
127
  const controller = running.get(message.episodeId);
104
128
  controller?.abort(new Error("Bonded cancelled the episode"));
@@ -125,20 +149,6 @@ export function createBondedHandler(options) {
125
149
  return json(202, { ok: true, episodeId: message.episodeId });
126
150
  };
127
151
  }
128
- /**
129
- * Which of the secrets signed a request already verified (while a secret rotates, the verify
130
- * proof must be made with the one Bonded holds).
131
- */
132
- async function signingSecret(secrets, body, headers, nowMs, toleranceSeconds) {
133
- if (secrets.length === 1)
134
- return secrets[0];
135
- for (const secret of secrets) {
136
- const check = await verifyAgentUrlRequest({ secret, body, headers, nowMs, toleranceSeconds });
137
- if (check.ok)
138
- return secret;
139
- }
140
- return secrets[0];
141
- }
142
152
  /** A brokerage episode as the agent sees it (the same shape `fromBondedBrokerageEnv` returns). */
143
153
  function toBrokerageEpisode(message) {
144
154
  return {
@@ -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
- * Signs a body as Bonded does: the two headers to send with it. For testing your endpoint
7
- * locally; Bonded signs real calls itself.
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 function signBondedRequest(secret: string, body: string, timestamp?: number): Promise<{
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
- * Checks a request's signature only: the timestamp is within `toleranceSeconds` of now and one of
22
- * the header's `v1=` signatures is the body's HMAC under `secret` (or one of several secrets).
23
- * It doesn't look at the body or check for replays; `createBondedHandler` does both.
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 function verifyBondedSignature(input: {
26
- secret: string | string[];
27
- body: string;
28
- timestamp: string | null | undefined;
29
- signature: string | null | undefined;
30
- /** Unix seconds. Default: now. */
31
- now?: number;
32
- toleranceSeconds?: number;
33
- }): Promise<SignatureCheck>;
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 the agent's secret:
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-signature`: `v1=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>` (up to four,
7
- * comma-separated, while a secret is rotated).
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 scheme lives in @bondedhq/shared (`@bondedhq/shared/agent-url`, which the gateway signs with);
10
- * these are the SDK's names for it. Web Crypto only, so it runs in Node, Bun, Deno, Cloudflare
11
- * Workers and edge runtimes, and the comparison is Web Crypto's constant-time HMAC verify.
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, signAgentUrlRequest, verifyAgentUrlRequest, } from "@bondedhq/shared/agent-url";
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
- * Signs a body as Bonded does: the two headers to send with it. For testing your endpoint
20
- * locally; Bonded signs real calls itself.
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 async function signBondedRequest(secret, body, timestamp = Math.floor(Date.now() / 1000)) {
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
- * Checks a request's signature only: the timestamp is within `toleranceSeconds` of now and one of
31
- * the header's `v1=` signatures is the body's HMAC under `secret` (or one of several secrets).
32
- * It doesn't look at the body or check for replays; `createBondedHandler` does both.
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 async function verifyBondedSignature(input) {
35
- const result = await verifyAgentUrlRequest({
36
- secret: input.secret,
37
- body: input.body,
38
- headers: {
39
- [TIMESTAMP_HEADER]: input.timestamp ?? undefined,
40
- [SIGNATURE_HEADER]: input.signature ?? undefined,
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
- ...(input.now !== undefined ? { nowMs: input.now * 1000 } : {}),
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.1.7",
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.1.6"
97
+ "@bondedhq/shared": "^0.2.0"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@coinbase/agentkit": "0.10.4",