@agentpress/sdk 0.2.113 → 0.3.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
@@ -320,29 +320,37 @@ try {
320
320
 
321
321
  ```
322
322
  AgentPressError (base)
323
- ConfigurationError -- invalid options or missing webhookSecret
324
- HttpError -- non-2xx response (has statusCode, responseBody, url)
325
- TimeoutError -- request exceeded timeout
326
- WebhookSignatureError -- invalid/expired signature (from verifyOrThrow only)
323
+ ConfigurationError -- invalid options or missing webhookSecret / partnerMcp
324
+ HttpError -- non-2xx response (has statusCode, responseBody, url)
325
+ TimeoutError -- request exceeded timeout
326
+ WebhookSignatureError -- invalid/expired signature (from verifyOrThrow only)
327
+ PartnerTokenError -- Partner MCP JWT verification failure (has typed `reason`)
328
+ KeyRotationVerifyError -- Key-rotation webhook verification failure (has typed `reason`)
327
329
  ```
328
330
 
329
331
  ## Exported Types
330
332
 
331
333
  ```typescript
332
334
  import type {
333
- ActionCallbackPayload, // Inbound webhook payload from constructEvent()
334
- ActionEventType, // "action.pending_approval" | "action.approved" | ...
335
- ActionManageResponse, // Response from actions.approve() / actions.reject()
336
- ActionStatus, // "pending" | "staged" | "approved" | "rejected" | "completed" | "failed" | "expired"
337
- AgentPressOptions, // Constructor options
338
- AgentResponse, // Agent text response + tool calls
339
- ApproveActionParams, // actions.approve() params
340
- RejectActionParams, // actions.reject() params
341
- StagedToolCall, // Tool call awaiting approval
342
- ToolCallResult, // Individual tool call (name, arguments, result)
343
- WebhookResponse, // Response from send
344
- WebhookSendParams, // send params
345
- WebhookVerifyParams, // verify / verifyOrThrow / constructEvent params
335
+ ActionCallbackPayload, // Inbound webhook payload from constructEvent()
336
+ ActionEventType, // "action.pending_approval" | "action.approved" | ...
337
+ ActionManageResponse, // Response from actions.approve() / actions.reject()
338
+ ActionStatus, // "pending" | "staged" | "approved" | "rejected" | "completed" | "failed" | "expired"
339
+ AgentPressOptions, // Constructor options
340
+ AgentResponse, // Agent text response + tool calls
341
+ ApproveActionParams, // actions.approve() params
342
+ KeyRotationEvent, // Verified signing_key_rotation webhook payload
343
+ KeyRotationVerifyErrorReason, // Union of KeyRotationVerifyError.reason values
344
+ KeyRotationVerifyParams, // verifyKeyRotation() params
345
+ PartnerMcpOptions, // AgentPressOptions.partnerMcp shape
346
+ PartnerTokenClaims, // Verified Partner MCP Spec v1 JWT claims
347
+ PartnerTokenErrorReason, // Union of PartnerTokenError.reason values
348
+ RejectActionParams, // actions.reject() params
349
+ StagedToolCall, // Tool call awaiting approval
350
+ ToolCallResult, // Individual tool call (name, arguments, result)
351
+ WebhookResponse, // Response from send
352
+ WebhookSendParams, // send params
353
+ WebhookVerifyParams, // verify / verifyOrThrow / constructEvent params
346
354
  } from "@agentpress/sdk";
347
355
  ```
348
356
 
@@ -384,6 +392,198 @@ app.post("/webhooks/agentpress", express.raw({ type: "application/json" }), (req
384
392
  });
385
393
  ```
386
394
 
395
+ ## Partner MCP Spec v1
396
+
397
+ Partners building an MCP server that federates with AgentPress can use the SDK's built-in verifiers for the two cryptographic primitives the spec requires: **EdDSA JWT verification** (against a remote JWKS) for inbound bearer tokens, and **HMAC-SHA256 webhook verification** for `signing_key_rotation` notifications.
398
+
399
+ This consolidates ~200 lines of hand-rolled JWT/JWKS/HMAC code that would otherwise live in every partner MCP integration. For the full wire protocol see the [Partner MCP Spec v1 guide](https://docs.agentpress.dev/guides/partner-mcp-spec-v1/).
400
+
401
+ ### Configuration
402
+
403
+ Partner MCP helpers require a `partnerMcp` block on the constructor. All fields except `clockTolerance` and `jwksCacheMaxAgeMs` are required.
404
+
405
+ ```typescript
406
+ import { AgentPress } from "@agentpress/sdk";
407
+
408
+ // Per-org as of Partner MCP Spec v1 — each AgentPress org that registers you
409
+ // as a partner has its own signing keypair and JWKS URL. The org admin who
410
+ // registers you will share the org slug; your `issuer` + `jwksUrl` are
411
+ // derived from it.
412
+ const client = new AgentPress({
413
+ webhookSecret: "whsec_...", // Needed for verifyKeyRotation
414
+ partnerMcp: {
415
+ jwksUrl: "https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
416
+ issuer: "https://api.agent.press/orgs/<org-slug>",
417
+ audience: "https://mcp.example.com", // Your MCP's stable URL
418
+ expectedExtProvider: "localfalcon", // Required ext_provider claim value
419
+ clockTolerance: "30s", // Default: "30s" (accepts number of seconds or jose duration string)
420
+ jwksCacheMaxAgeMs: 3_600_000, // Default: 1 hour
421
+ },
422
+ });
423
+ ```
424
+
425
+ ### Running staging and production side-by-side
426
+
427
+ Staging and production MUST be separate `AgentPress` client instances, each with its own `partnerMcp` block. JWKS cache state is per-instance (not module-global), so two clients can run side-by-side in the same process without cross-talk.
428
+
429
+ ```typescript
430
+ const staging = new AgentPress({
431
+ webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET_STAGING,
432
+ partnerMcp: {
433
+ jwksUrl: "https://stg-api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
434
+ issuer: "https://stg-api.agent.press/orgs/<org-slug>",
435
+ audience: "https://mcp-staging.example.com",
436
+ expectedExtProvider: "localfalcon",
437
+ },
438
+ });
439
+
440
+ const production = new AgentPress({
441
+ webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET_PROD,
442
+ partnerMcp: {
443
+ jwksUrl: "https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
444
+ issuer: "https://api.agent.press/orgs/<org-slug>",
445
+ audience: "https://mcp.example.com",
446
+ expectedExtProvider: "localfalcon",
447
+ },
448
+ });
449
+ ```
450
+
451
+ Route requests to the correct client by inspecting the JWT's `iss` claim (or by a request-scoped environment flag upstream of the verifier). If the same partner integration serves multiple AgentPress orgs, spin up one client per org — do NOT share a single client across orgs (JWKS caches, `iss`, and `aud` are all org-scoped).
452
+
453
+ ---
454
+
455
+ ### client.partners.verifyToken(token)
456
+
457
+ Verifies an EdDSA JWT issued by AgentPress against the configured remote JWKS. Returns typed `PartnerTokenClaims` on success; throws `PartnerTokenError` with a typed `reason` on failure.
458
+
459
+ Enforces the full spec: `alg` pinned to `EdDSA`, `iss` / `aud` exact-match, `ext_provider` exact-match, `sub` / `jti` / `scope` required and non-empty, `kid` header required, `exp` / `iat` validated with configurable skew (default ±30s).
460
+
461
+ ```typescript
462
+ import { AgentPress, PartnerTokenError } from "@agentpress/sdk";
463
+ import { Hono } from "hono";
464
+
465
+ const client = new AgentPress({ partnerMcp: { /* ... */ } });
466
+
467
+ const app = new Hono();
468
+
469
+ app.post("/mcp", async (c) => {
470
+ const auth = c.req.header("authorization") ?? "";
471
+ const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
472
+
473
+ if (!token) {
474
+ return c.text("Unauthorized", 401, {
475
+ "WWW-Authenticate": 'Bearer error="invalid_token", error_description="Missing bearer token"',
476
+ });
477
+ }
478
+
479
+ try {
480
+ const claims = await client.partners.verifyToken(token);
481
+
482
+ // claims is typed PartnerTokenClaims -- use `sub` to resolve the external user
483
+ // into your local user record (partner-domain logic):
484
+ const user = await resolveExternalUser(claims.ext_provider, claims.sub);
485
+ c.set("user", user);
486
+ c.set("claims", claims);
487
+
488
+ return handleMcpRequest(c);
489
+ } catch (err) {
490
+ if (err instanceof PartnerTokenError) {
491
+ const description = mapReasonToDescription(err.reason);
492
+ return c.text("Unauthorized", 401, {
493
+ "WWW-Authenticate": `Bearer error="invalid_token", error_description="${description}"`,
494
+ });
495
+ }
496
+ throw err;
497
+ }
498
+ });
499
+ ```
500
+
501
+ **Error handling table** — all `PartnerTokenError` reasons map to RFC 6750 `error="invalid_token"`. Use `error_description` to distinguish:
502
+
503
+ | `reason` | HTTP | `error` | `error_description` |
504
+ |---|---|---|---|
505
+ | `signature_invalid` | 401 | `invalid_token` | `Signature verification failed` |
506
+ | `issuer_mismatch` | 401 | `invalid_token` | `Issuer does not match` |
507
+ | `audience_mismatch` | 401 | `invalid_token` | `Audience does not match` |
508
+ | `expired` | 401 | `invalid_token` | `Token expired` |
509
+ | `not_yet_valid` | 401 | `invalid_token` | `Token not yet valid` |
510
+ | `missing_claim` | 401 | `invalid_token` | `Required claim missing or invalid` |
511
+ | `ext_provider_mismatch` | 401 | `invalid_token` | `ext_provider does not match` |
512
+ | `algorithm_not_allowed` | 401 | `invalid_token` | `Algorithm not allowed` |
513
+ | `kid_missing_or_unknown` | 401 | `invalid_token` | `Signing key id missing or unknown` |
514
+ | `malformed` | 401 | `invalid_token` | `Malformed token` |
515
+
516
+ ---
517
+
518
+ ### client.webhooks.verifyKeyRotation(params) + client.partners.refreshJwks()
519
+
520
+ Verifies a `signing_key_rotation` webhook signed by AgentPress with HMAC-SHA256 over the raw request body, then refreshes the JWKS cache so the next verified token picks up the new signing key without a failed-verify penalty.
521
+
522
+ The rotation webhook handler is **optional** — partners who don't implement it still pick up rotations automatically on the next `kid` miss (with one failed verify latency). Implement it to make rotations seamless.
523
+
524
+ ```typescript
525
+ import { AgentPress, KeyRotationVerifyError } from "@agentpress/sdk";
526
+ import { Hono } from "hono";
527
+
528
+ const client = new AgentPress({
529
+ webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET,
530
+ partnerMcp: { /* ... */ },
531
+ });
532
+
533
+ const app = new Hono();
534
+
535
+ app.post("/webhooks/agentpress/key-rotation", async (c) => {
536
+ // IMPORTANT: read the raw body, not c.req.json() -- HMAC is computed over
537
+ // the exact bytes AgentPress sent, not a re-serialized JSON object.
538
+ const rawBody = await c.req.text();
539
+
540
+ // Headers are passed verbatim as Record<string, string | undefined>; casing
541
+ // is handled internally.
542
+ const headers: Record<string, string | undefined> = {};
543
+ c.req.raw.headers.forEach((value, key) => {
544
+ headers[key] = value;
545
+ });
546
+
547
+ try {
548
+ const event = client.webhooks.verifyKeyRotation({
549
+ payload: rawBody,
550
+ headers,
551
+ });
552
+
553
+ // event is typed KeyRotationEvent
554
+ console.log(
555
+ `Rotation (${event.type}): ${event.retiredKid} -> ${event.newCurrentKid}`,
556
+ );
557
+
558
+ // Force-refetch the JWKS so the next verifyToken() picks up the new key.
559
+ await client.partners.refreshJwks();
560
+
561
+ return c.json({ received: true });
562
+ } catch (err) {
563
+ if (err instanceof KeyRotationVerifyError) {
564
+ const status = mapRotationReasonToStatus(err.reason);
565
+ return c.text(err.message, status);
566
+ }
567
+ throw err;
568
+ }
569
+ });
570
+ ```
571
+
572
+ **Error handling table** for `KeyRotationVerifyError` reasons:
573
+
574
+ | `reason` | HTTP | Notes |
575
+ |---|---|---|
576
+ | `invalid_signature` | 401 | HMAC mismatch |
577
+ | `invalid_signature_format` | 401 | `x-agentpress-signature` missing or not `sha256=<hex>` |
578
+ | `timestamp_out_of_window` | 401 | `x-agentpress-timestamp` outside ±5 min tolerance |
579
+ | `invalid_timestamp` | 401 | `x-agentpress-timestamp` missing or not a unix-seconds integer |
580
+ | `malformed_payload` | 400 | Body is not valid JSON or doesn't match the rotation event schema |
581
+ | `payload_too_large` | 413 | Body exceeds 8 KB cap |
582
+
583
+ ---
584
+
585
+ For the full Partner MCP Spec v1 — token shape, JWKS discovery, rotation semantics, and error protocol — see the [guide on docs.agentpress.dev](https://docs.agentpress.dev/guides/partner-mcp-spec-v1/).
586
+
387
587
  ## File Structure
388
588
 
389
589
  ```
@@ -396,7 +596,10 @@ packages/sdk/src/
396
596
  utils.ts -- randomMessageId() helper (msg_<uuid>)
397
597
  actions/
398
598
  client.ts -- ActionsClient (approve, reject)
599
+ partners/
600
+ client.ts -- PartnersClient (verifyToken, refreshJwks)
399
601
  webhooks/
400
- client.ts -- WebhooksClient (send, verify, verifyOrThrow, constructEvent)
602
+ client.ts -- WebhooksClient (send, verify, verifyOrThrow, constructEvent, verifyKeyRotation)
401
603
  signing.ts -- HMAC-SHA256 Svix-compatible sign + verify functions
604
+ keyRotation.ts -- Partner MCP Spec v1 rotation webhook HMAC + timestamp + payload validation
402
605
  ```
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let node_crypto = require("node:crypto");
3
+ let jose = require("jose");
3
4
  //#region src/errors.ts
4
5
  /**
5
6
  * Base error class for all SDK errors. Catch this to handle any error
@@ -60,6 +61,33 @@ var WebhookSignatureError = class extends AgentPressError {
60
61
  Object.setPrototypeOf(this, new.target.prototype);
61
62
  }
62
63
  };
64
+ /**
65
+ * Thrown by {@link PartnersClient.verifyToken} when a Partner MCP Spec v1 JWT
66
+ * fails verification. Partners map `reason` to their RFC 6750 `WWW-Authenticate`
67
+ * response (`401 invalid_token` for all reasons in this class).
68
+ */
69
+ var PartnerTokenError = class extends AgentPressError {
70
+ reason;
71
+ constructor(reason, message) {
72
+ super(message);
73
+ this.name = "PartnerTokenError";
74
+ this.reason = reason;
75
+ Object.setPrototypeOf(this, new.target.prototype);
76
+ }
77
+ };
78
+ /**
79
+ * Thrown by {@link WebhooksClient.verifyKeyRotation} when a `signing_key_rotation`
80
+ * webhook fails HMAC / timestamp / payload validation.
81
+ */
82
+ var KeyRotationVerifyError = class extends AgentPressError {
83
+ reason;
84
+ constructor(reason, message) {
85
+ super(message);
86
+ this.name = "KeyRotationVerifyError";
87
+ this.reason = reason;
88
+ Object.setPrototypeOf(this, new.target.prototype);
89
+ }
90
+ };
63
91
  //#endregion
64
92
  //#region src/utils.ts
65
93
  function randomMessageId() {
@@ -67,7 +95,7 @@ function randomMessageId() {
67
95
  }
68
96
  //#endregion
69
97
  //#region src/webhooks/signing.ts
70
- const SIGNATURE_PREFIX = "v1,";
98
+ const SIGNATURE_PREFIX$1 = "v1,";
71
99
  const DEFAULT_TOLERANCE_SECONDS = 300;
72
100
  /**
73
101
  * Sign a webhook payload using Svix-compatible HMAC-SHA256.
@@ -81,7 +109,7 @@ const DEFAULT_TOLERANCE_SECONDS = 300;
81
109
  function sign(secret, msgId, timestamp, body) {
82
110
  const secretBytes = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
83
111
  const message = `${msgId}.${timestamp}.${body}`;
84
- return `${SIGNATURE_PREFIX}${(0, node_crypto.createHmac)("sha256", secretBytes).update(message).digest("base64")}`;
112
+ return `${SIGNATURE_PREFIX$1}${(0, node_crypto.createHmac)("sha256", secretBytes).update(message).digest("base64")}`;
85
113
  }
86
114
  /**
87
115
  * Verify a Svix webhook signature.
@@ -234,6 +262,121 @@ var HttpClient = class {
234
262
  }
235
263
  };
236
264
  //#endregion
265
+ //#region src/partners/client.ts
266
+ const ALGORITHMS = ["EdDSA"];
267
+ /**
268
+ * Client for Partner MCP Spec v1 helpers. Created automatically when
269
+ * {@link AgentPressOptions.partnerMcp} is provided.
270
+ *
271
+ * State (JWKS cache) is per-instance, NOT module-global, so staging and
272
+ * production clients can run side-by-side without cross-talk.
273
+ */
274
+ var PartnersClient = class {
275
+ options;
276
+ partnerMcp;
277
+ jwks = null;
278
+ jwksKeyFn = null;
279
+ constructor(options) {
280
+ this.options = options;
281
+ this.partnerMcp = options.partnerMcp;
282
+ }
283
+ /**
284
+ * Verify an inbound Partner MCP Spec v1 JWT against AgentPress's JWKS.
285
+ *
286
+ * Enforces the full spec: `algorithms: ["EdDSA"]` pinned; `iss`, `aud`,
287
+ * `ext_provider` validated; `sub`, `jti`, `scope` required and non-empty;
288
+ * `kid` header required; `exp` / `iat` validated with ±30s skew (configurable).
289
+ *
290
+ * @param token - Bare JWT string (no `Bearer ` prefix).
291
+ * @returns Typed {@link PartnerTokenClaims}.
292
+ * @throws {PartnerTokenError} with a typed `reason` for RFC 6750 mapping.
293
+ * @throws {ConfigurationError} if `partnerMcp` is not configured.
294
+ */
295
+ async verifyToken(token) {
296
+ const cfg = this.requireConfig();
297
+ const jwks = this.getJwks();
298
+ let payload;
299
+ let protectedHeader;
300
+ try {
301
+ const result = await (0, jose.jwtVerify)(token, jwks, {
302
+ algorithms: [...ALGORITHMS],
303
+ issuer: cfg.issuer,
304
+ audience: cfg.audience,
305
+ clockTolerance: cfg.clockTolerance
306
+ });
307
+ payload = result.payload;
308
+ protectedHeader = result.protectedHeader;
309
+ } catch (err) {
310
+ throw mapJoseError(err);
311
+ }
312
+ if (!protectedHeader.kid || typeof protectedHeader.kid !== "string") throw new PartnerTokenError("kid_missing_or_unknown", "JWT header missing required 'kid'");
313
+ const claims = payload;
314
+ if (typeof claims.iat !== "number" || !Number.isFinite(claims.iat)) throw new PartnerTokenError("missing_claim", "'iat' claim must be a number");
315
+ const toleranceSeconds = toSeconds(cfg.clockTolerance);
316
+ const nowSec = Math.floor(Date.now() / 1e3);
317
+ if (claims.iat - nowSec > toleranceSeconds) throw new PartnerTokenError("not_yet_valid", `'iat' is ${claims.iat - nowSec}s in the future, beyond ${toleranceSeconds}s tolerance`);
318
+ if (typeof claims.sub !== "string" || claims.sub.length === 0) throw new PartnerTokenError("missing_claim", "'sub' claim is required");
319
+ if (typeof claims.jti !== "string" || claims.jti.length === 0) throw new PartnerTokenError("missing_claim", "'jti' claim is required");
320
+ if (typeof claims.scope !== "string") throw new PartnerTokenError("missing_claim", "'scope' claim must be a string");
321
+ if (typeof claims.ext_provider !== "string" || claims.ext_provider.length === 0) throw new PartnerTokenError("missing_claim", "'ext_provider' claim is required");
322
+ if (claims.ext_provider !== cfg.expectedExtProvider) throw new PartnerTokenError("ext_provider_mismatch", `ext_provider '${claims.ext_provider}' does not match expected '${cfg.expectedExtProvider}'`);
323
+ return claims;
324
+ }
325
+ /**
326
+ * Force the JWKS cache to refetch immediately. Call this from your
327
+ * `signing_key_rotation` webhook handler after verifying the webhook —
328
+ * otherwise the cache picks up new keys on the next `kid` miss (which
329
+ * works, but with one failed verify latency penalty).
330
+ */
331
+ async refreshJwks() {
332
+ const anyJwks = this.getJwks();
333
+ if (typeof anyJwks.reload === "function") await anyJwks.reload();
334
+ }
335
+ requireConfig() {
336
+ if (!this.partnerMcp) throw new ConfigurationError("partnerMcp options are required for partner token operations");
337
+ return this.partnerMcp;
338
+ }
339
+ getJwks() {
340
+ const cfg = this.requireConfig();
341
+ if (!this.jwks) {
342
+ this.jwks = (0, jose.createRemoteJWKSet)(new URL(cfg.jwksUrl), {
343
+ cacheMaxAge: cfg.jwksCacheMaxAgeMs,
344
+ cooldownDuration: 3e4
345
+ });
346
+ this.jwksKeyFn = this.jwks;
347
+ }
348
+ return this.jwks;
349
+ }
350
+ };
351
+ function toSeconds(tolerance) {
352
+ if (typeof tolerance === "number") return tolerance;
353
+ const match = tolerance.trim().match(/^(\d+)\s*(s|m|h)$/i);
354
+ if (!match) return 30;
355
+ const n = Number.parseInt(match[1], 10);
356
+ const unit = match[2].toLowerCase();
357
+ if (unit === "s") return n;
358
+ if (unit === "m") return n * 60;
359
+ if (unit === "h") return n * 3600;
360
+ return 30;
361
+ }
362
+ function mapJoseError(err) {
363
+ if (err instanceof PartnerTokenError) return err;
364
+ if (err instanceof jose.errors.JWSSignatureVerificationFailed) return new PartnerTokenError("signature_invalid", "JWT signature verification failed");
365
+ if (err instanceof jose.errors.JWKSNoMatchingKey) return new PartnerTokenError("kid_missing_or_unknown", "No JWKS key matches the JWT 'kid' header");
366
+ if (err instanceof jose.errors.JOSEAlgNotAllowed) return new PartnerTokenError("algorithm_not_allowed", "JWT 'alg' is not EdDSA");
367
+ if (err instanceof jose.errors.JWTExpired) return new PartnerTokenError("expired", "JWT has expired");
368
+ if (err instanceof jose.errors.JWTClaimValidationFailed) {
369
+ const claim = err.claim;
370
+ if (claim === "iss") return new PartnerTokenError("issuer_mismatch", "JWT 'iss' does not match expected issuer");
371
+ if (claim === "aud") return new PartnerTokenError("audience_mismatch", "JWT 'aud' does not match expected audience");
372
+ if (claim === "iat") return new PartnerTokenError("not_yet_valid", "JWT 'iat' is in the future beyond clock tolerance");
373
+ return new PartnerTokenError("missing_claim", err.message || "Claim validation failed");
374
+ }
375
+ if (err instanceof jose.errors.JWTInvalid || err instanceof jose.errors.JWSInvalid) return new PartnerTokenError("malformed", err.message || "Malformed JWT");
376
+ if (err instanceof Error) return new PartnerTokenError("malformed", err.message);
377
+ return new PartnerTokenError("malformed", String(err));
378
+ }
379
+ //#endregion
237
380
  //#region src/userApprovals/client.ts
238
381
  /**
239
382
  * Client for managing per-user auto-approval rules — the SDK equivalent of the
@@ -349,6 +492,74 @@ var UserApprovalsClient = class {
349
492
  }
350
493
  };
351
494
  //#endregion
495
+ //#region src/webhooks/keyRotation.ts
496
+ const MAX_PAYLOAD_BYTES = 8 * 1024;
497
+ const TIMESTAMP_SKEW_SECONDS = 300;
498
+ const SIGNATURE_PREFIX = "sha256=";
499
+ /**
500
+ * Verify a `signing_key_rotation` webhook signed by AgentPress with
501
+ * HMAC-SHA256 over the raw request body. Returns the typed
502
+ * {@link KeyRotationEvent} payload.
503
+ *
504
+ * @throws {KeyRotationVerifyError} with a typed `reason` for HTTP mapping.
505
+ * @throws {ConfigurationError} if `webhookSecret` is not configured.
506
+ */
507
+ function verifyKeyRotation(secret, params) {
508
+ if (!secret) throw new ConfigurationError("webhookSecret is required for key rotation webhook verification");
509
+ const body = typeof params.payload === "string" ? params.payload : params.payload.toString("utf-8");
510
+ if ((typeof params.payload === "string" ? Buffer.byteLength(params.payload, "utf-8") : params.payload.length) > MAX_PAYLOAD_BYTES) throw new KeyRotationVerifyError("payload_too_large", `Payload exceeds ${MAX_PAYLOAD_BYTES} bytes`);
511
+ const signatureHeader = readHeader(params.headers, "x-agentpress-signature");
512
+ const timestampHeader = readHeader(params.headers, "x-agentpress-timestamp");
513
+ if (!signatureHeader?.startsWith(SIGNATURE_PREFIX)) throw new KeyRotationVerifyError("invalid_signature_format", `Signature header missing or does not start with '${SIGNATURE_PREFIX}'`);
514
+ const providedHex = signatureHeader.slice(7).toLowerCase();
515
+ if (!/^[0-9a-f]+$/.test(providedHex)) throw new KeyRotationVerifyError("invalid_signature_format", "Signature is not a hex string");
516
+ if (!timestampHeader) throw new KeyRotationVerifyError("invalid_timestamp", "x-agentpress-timestamp header is required");
517
+ const timestamp = Number.parseInt(timestampHeader, 10);
518
+ if (!Number.isFinite(timestamp) || String(timestamp) !== timestampHeader.trim()) throw new KeyRotationVerifyError("invalid_timestamp", "x-agentpress-timestamp is not a valid unix seconds integer");
519
+ const now = Math.floor(Date.now() / 1e3);
520
+ if (Math.abs(now - timestamp) > TIMESTAMP_SKEW_SECONDS) throw new KeyRotationVerifyError("timestamp_out_of_window", `Timestamp skew ${Math.abs(now - timestamp)}s exceeds ${TIMESTAMP_SKEW_SECONDS}s tolerance`);
521
+ const expectedHex = (0, node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
522
+ const providedBuf = Buffer.from(providedHex, "hex");
523
+ const expectedBuf = Buffer.from(expectedHex, "hex");
524
+ if (providedBuf.length !== expectedBuf.length || !(0, node_crypto.timingSafeEqual)(providedBuf, expectedBuf)) throw new KeyRotationVerifyError("invalid_signature", "HMAC signature mismatch");
525
+ let parsed;
526
+ try {
527
+ parsed = JSON.parse(body);
528
+ } catch {
529
+ throw new KeyRotationVerifyError("malformed_payload", "Payload is not valid JSON");
530
+ }
531
+ return validatePayload(parsed);
532
+ }
533
+ function readHeader(headers, name) {
534
+ const lowered = name.toLowerCase();
535
+ for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === lowered && typeof v === "string" && v.length > 0) return v;
536
+ }
537
+ function validatePayload(raw) {
538
+ if (!raw || typeof raw !== "object") throw new KeyRotationVerifyError("malformed_payload", "Payload must be a JSON object");
539
+ const obj = raw;
540
+ if (obj.event !== "signing_key_rotation") throw new KeyRotationVerifyError("malformed_payload", `Unexpected event '${String(obj.event)}'`);
541
+ if (obj.type !== "scheduled" && obj.type !== "emergency") throw new KeyRotationVerifyError("malformed_payload", "'type' must be 'scheduled' or 'emergency'");
542
+ for (const key of [
543
+ "retiredKid",
544
+ "newCurrentKid",
545
+ "retiredAt",
546
+ "effectiveAt",
547
+ "jwksUrl"
548
+ ]) if (typeof obj[key] !== "string" || obj[key].length === 0) throw new KeyRotationVerifyError("malformed_payload", `'${key}' must be a non-empty string`);
549
+ if (obj.reason !== void 0 && typeof obj.reason !== "string") throw new KeyRotationVerifyError("malformed_payload", "'reason' must be a string when present");
550
+ const event = {
551
+ event: "signing_key_rotation",
552
+ type: obj.type,
553
+ retiredKid: obj.retiredKid,
554
+ newCurrentKid: obj.newCurrentKid,
555
+ retiredAt: obj.retiredAt,
556
+ effectiveAt: obj.effectiveAt,
557
+ jwksUrl: obj.jwksUrl
558
+ };
559
+ if (typeof obj.reason === "string") event.reason = obj.reason;
560
+ return event;
561
+ }
562
+ //#endregion
352
563
  //#region src/webhooks/client.ts
353
564
  var WebhooksClient = class {
354
565
  options;
@@ -420,6 +631,18 @@ var WebhooksClient = class {
420
631
  throw new AgentPressError("Webhook payload is not valid JSON");
421
632
  }
422
633
  }
634
+ /**
635
+ * Verify an inbound `signing_key_rotation` webhook (HMAC-SHA256 + timestamp
636
+ * + typed payload) from AgentPress. See the Partner MCP Spec v1 for the
637
+ * full contract.
638
+ *
639
+ * @throws {KeyRotationVerifyError} with a typed `reason` for HTTP mapping
640
+ * (401 for signature errors, 400 for malformed payload, 413 for oversize).
641
+ * @throws {ConfigurationError} if `webhookSecret` is not configured.
642
+ */
643
+ verifyKeyRotation(params) {
644
+ return verifyKeyRotation(this.options.webhookSecret, params);
645
+ }
423
646
  };
424
647
  //#endregion
425
648
  //#region src/client.ts
@@ -445,6 +668,8 @@ var AgentPress = class {
445
668
  actions;
446
669
  /** Per-user auto-approval rules (read / create / update / delete). */
447
670
  userApprovals;
671
+ /** Partner MCP Spec v1 helpers (inbound JWT verification, JWKS refresh). */
672
+ partners;
448
673
  /**
449
674
  * @param options - SDK configuration. All fields are optional with sensible defaults.
450
675
  * @throws {ConfigurationError} If `timeout` is non-positive or `webhookSecret` has an invalid prefix.
@@ -455,6 +680,7 @@ var AgentPress = class {
455
680
  this.webhooks = new WebhooksClient(resolved, http);
456
681
  this.actions = new ActionsClient(resolved, http);
457
682
  this.userApprovals = new UserApprovalsClient(resolved, http);
683
+ this.partners = new PartnersClient(resolved);
458
684
  }
459
685
  };
460
686
  function resolveOptions(options) {
@@ -469,18 +695,46 @@ function resolveOptions(options) {
469
695
  org,
470
696
  webhookSecret: options.webhookSecret,
471
697
  apiKey: options.apiKey,
698
+ partnerMcp: resolvePartnerMcp(options.partnerMcp),
472
699
  onRequest: options.onRequest,
473
700
  onResponse: options.onResponse
474
701
  };
475
702
  }
703
+ function resolvePartnerMcp(options) {
704
+ if (!options) return void 0;
705
+ if (typeof options.jwksUrl !== "string" || options.jwksUrl.length === 0) throw new ConfigurationError("partnerMcp.jwksUrl is required");
706
+ try {
707
+ new URL(options.jwksUrl);
708
+ } catch {
709
+ throw new ConfigurationError("partnerMcp.jwksUrl must be a valid URL");
710
+ }
711
+ if (typeof options.issuer !== "string" || options.issuer.length === 0) throw new ConfigurationError("partnerMcp.issuer is required");
712
+ if (typeof options.audience !== "string" || options.audience.length === 0) throw new ConfigurationError("partnerMcp.audience is required");
713
+ if (typeof options.expectedExtProvider !== "string" || options.expectedExtProvider.length === 0) throw new ConfigurationError("partnerMcp.expectedExtProvider is required");
714
+ const clockTolerance = options.clockTolerance ?? "30s";
715
+ const jwksCacheMaxAgeMs = options.jwksCacheMaxAgeMs ?? 36e5;
716
+ if (jwksCacheMaxAgeMs <= 0 || !Number.isFinite(jwksCacheMaxAgeMs)) throw new ConfigurationError("partnerMcp.jwksCacheMaxAgeMs must be a positive number");
717
+ return {
718
+ jwksUrl: options.jwksUrl,
719
+ issuer: options.issuer,
720
+ audience: options.audience,
721
+ expectedExtProvider: options.expectedExtProvider,
722
+ clockTolerance,
723
+ jwksCacheMaxAgeMs
724
+ };
725
+ }
476
726
  //#endregion
477
727
  exports.ActionsClient = ActionsClient;
478
728
  exports.AgentPress = AgentPress;
479
729
  exports.AgentPressError = AgentPressError;
480
730
  exports.ConfigurationError = ConfigurationError;
481
731
  exports.HttpError = HttpError;
732
+ exports.KeyRotationVerifyError = KeyRotationVerifyError;
733
+ exports.PartnerTokenError = PartnerTokenError;
734
+ exports.PartnersClient = PartnersClient;
482
735
  exports.TimeoutError = TimeoutError;
483
736
  exports.UserApprovalsClient = UserApprovalsClient;
484
737
  exports.WebhookSignatureError = WebhookSignatureError;
738
+ exports.WebhooksClient = WebhooksClient;
485
739
 
486
740
  //# sourceMappingURL=index.cjs.map