@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 +221 -18
- package/dist/index.cjs +256 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +177 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +177 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +253 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -320,29 +320,37 @@ try {
|
|
|
320
320
|
|
|
321
321
|
```
|
|
322
322
|
AgentPressError (base)
|
|
323
|
-
ConfigurationError
|
|
324
|
-
HttpError
|
|
325
|
-
TimeoutError
|
|
326
|
-
WebhookSignatureError
|
|
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,
|
|
334
|
-
ActionEventType,
|
|
335
|
-
ActionManageResponse,
|
|
336
|
-
ActionStatus,
|
|
337
|
-
AgentPressOptions,
|
|
338
|
-
AgentResponse,
|
|
339
|
-
ApproveActionParams,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|