@blamejs/core 0.8.83 → 0.8.87

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/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.8.x
10
10
 
11
+ - v0.8.87 (2026-05-11) — **NIST 800-63-4 FAL classifier + RFC 7505 Null-MX helper + Gmail FBL Feedback-ID builder + vendor-update.sh stale-entry cleanup**. **`b.auth.fal`** lands as the federation-side counterpart to the existing `b.auth.aal` band classifier. `fromAssertion({ channel, encrypted?, replayProtected?, hokBinding? })` classifies an incoming federation assertion as `"FAL1"` / `"FAL2"` / `"FAL3"` per NIST 800-63C-4: Holder-of-Key (mTLS / DPoP / SAML HoK) with replay-protection → FAL3; back-channel OR encrypted front-channel with replay-protection → FAL2; bare bearer front-channel → FAL1. Conservative: missing replay-protection on a back-channel assertion downgrades to FAL1 because §5.2 requires nonce / jti binding before back-channel can claim FAL2. `requireFal(minimumBand)` builds a band-check guard that throws `auth/fal-insufficient` for stale-band requests; compose with the request-scope auth state to gate sensitive operations. **`b.network.dns.isNullMx(records)`** lands as the RFC 7505 Null-MX classifier: returns `true` when an operator-supplied MX-record array signals "this domain does not accept email" (single record, priority 0, exchange `.` per RFC 7505 §3). Operators send-side check this before delivery to skip domains that have explicitly opted out — `node:dns.resolveMx` returns `exchange: ""` for the same RDATA, so the classifier accepts both shapes. **`b.mail.feedbackId({ campaignId, customerId, mailType, senderId })`** builds a Gmail Feedback-Loop (FBL) Feedback-ID header value as the canonical 4-tuple `CampaignID:CustomerID:MailType:SenderID`. Refuses missing / empty fields, fields containing `:` (would corrupt the field separator), fields >64 chars (Gmail FBL truncation threshold), and control-char content (CR/LF header-injection defense). Setting Feedback-ID on outbound mail lets Gmail Postmaster Tools surface per-campaign abuse-rate metrics keyed by the operator's vocabulary instead of by SMTP envelope-sender alone. **vendor-update.sh cleanup**: `scripts/vendor-update.sh --check` removed the stale `argon2` entry from `VENDORED_PACKAGES`. argon2 was removed from `lib/vendor/` back in v0.4.x when Node 24's built-in `crypto.argon2*` API replaced the third-party prebuilds (per `lib/argon2-builtin.js`); the script still listed it in the check array, producing a false "UPDATE AVAILABLE" line for an unvendored package. The case-block error path that still says "argon2 is no longer vendored" stays so anyone running `./scripts/vendor-update.sh argon2` gets the operator-friendly explanation.
12
+ - v0.8.86 (2026-05-11) — **Sectoral + cybersecurity posture sweep + HTTP-hygiene primitives + npm-publish hotfix**. **npm-publish hotfix**: the v0.8.85 `npm audit signatures` step failed with `npm error found no installed dependencies to audit` because the framework's zero-runtime-deps posture produces an empty install tree; the gate now treats that specific message as success while keeping every other failure mode loud (v0.8.85 npm tarball never published — operators upgrade `0.8.83 → 0.8.86` to pick up the carried v0.8.84 + v0.8.85 surface plus the new v0.8.86 primitives). **10 new compliance postures**: `cmmc-2.0` (DoD Cybersecurity Maturity Model Certification 2.0), `cjis-v6` (FBI CJIS Security Policy v6.0), `iso-27001-2022` + `iso-27002-2022` + `iso-27017` + `iso-27018` + `iso-27701` (ISO/IEC 27001 family), `nist-800-66-r2` (HIPAA Security Rule implementation guidance), `ehds` (European Health Data Space), `circia` (US Cyber Incident Reporting for Critical Infrastructure Act). Cascade defaults set encrypted-backup + signed-audit-chain + TLS 1.3 + vacuum-after-erase for the data-tier postures; `iso-27002-2022` + `circia` defer the data-tier mandate to operator choice. **`b.cacheStatus`** — RFC 9211 Cache-Status response-header builder + parser. `append(prev, entry)` chains the operator's current cache decision onto whatever upstream caches wrote; `entry({...})` formats a single entry; `parse(headerValue)` returns the parsed chain as `[{ cache, params }]` records with `hit`/`stored`/`collapsed` as booleans, `ttl`/`fwdStatus` as numbers, `fwd` as the RFC 9211 §2 enum string, `key`/`detail` as unquoted sf-strings. Operators diagnose CDN/reverse-proxy/app-cache decision chains by reading the header instead of guessing from elapsed-time metrics. **`b.serverTiming`** — W3C Server-Timing response-header builder. `create()` returns a per-request collector with `mark(name, durationMs?, description?)` / `measure(name, fn)` async-timing wrapper / `toHeader()` serializer. Surfaces server-side latency in the browser's Performance API. **`b.middleware.noCache`** — RFC 9111 §5.2.2.5 `Cache-Control: no-store` middleware for auth-gated / individualized response paths. Sets `Cache-Control: no-store`, `Pragma: no-cache` (HTTP/1.0 compatibility), `Vary: Cookie, Authorization` so intermediate caches don't store personalized responses keyed by URL alone. Optional `opts.when(req)` predicate for conditional application; `opts.skipExisting:true` skips when `Cache-Control` is already set.
13
+ - v0.8.85 (2026-05-11) — **MCP tool registry + tool-call signing + A2A v1 task-exchange surface**. Closes the substantial agent-protocol gaps surfaced by the 2026-05-11 audit's MITRE ATLAS v5.3.0 + A2A v1 cross-walk. **MCP tool registry** lands as `b.mcp.toolRegistry.create({ tools, signingKey, verifyingKey?, alg?, ttlMs? })` — every registered tool gets a signed descriptor blob `{ tool, alg, signature }` (defense against compromised MCP server / descriptor drift) and a `descriptorsManifest()` produces a signed `{ body, signature }` document for operator-side attestation. The registry's `signCall({ toolName, args, nonce?, ttlMs? })` builds + signs an outbound tool-call envelope `{ tool, argsHash, nonce, iat, exp }` (defense against MCP middleman / indirect-prompt-injection synthesizing tool calls); `verifyCall(signed, { args?, seen?, nowMs? })` runs the inverse on inbound — refuses signature mismatch (`mcp/call-verify-failed`), expired envelopes (`mcp/call-expired`), replayed nonces via operator-supplied `seen(nonce)` callback (`mcp/call-replay`), unregistered tools (`mcp/call-unregistered-tool`), and args-hash mismatch when raw args supplied (`mcp/call-args-mismatch`). Default algorithm ML-DSA-87 per the framework's PQC-first rule; Ed25519 / ECDSA / SLH-DSA also available. **A2A v1 task-exchange surface** lands as `b.a2a.tasks.{send, get, cancel}` (client-side JSON-RPC dispatchers — `send` posts `tasks/send` to the peer URL with task validation + https-only refusal; `get` polls `tasks/get`; `cancel` requests `tasks/cancel`) plus `b.a2a.middleware.tasks({ scopes, handler, maxBytes? })` (server-side connect-style middleware — parses inbound JSON-RPC 2.0, enforces method allowlist `[tasks/send, tasks/get, tasks/cancel]` with -32601 method-not-found, enforces per-skill scopes via `req.a2aScopes` with -32001 scope-denied, dispatches to operator handler, maps errors to JSON-RPC -32603, refuses non-POST with 405 + non-JSON content-type with 415) plus `b.a2a.middleware.agentCard({ card, maxAgeSec? })` (serves operator's signed Agent Card at `/.well-known/agent.json` per A2A v1 discovery — 405 on non-GET, Cache-Control max-age operator-tunable).
14
+ - v0.8.84 (2026-05-11) — **Supply-chain hardening trio + HTTP-API hygiene primitives**. **Supply chain**: CodeQL SAST workflow (`.github/workflows/codeql.yml` — PR + push-to-main + weekly Mon-05:31-UTC schedule) running the `security-extended` query pack against the JavaScript surface (catches SQL injection, XSS, prototype pollution, command injection, ReDoS, unsafe deserialization, SSRF, hardcoded credentials beyond what OSV-Scanner sees; findings surface as SARIF in the Security tab); `npm audit signatures` step in `npm-publish.yml` that verifies the cryptographic signing chain for every package in the install tree against the npm registry's public keys before the publish step runs (regression-defense — the framework ships zero npm runtime deps so the tree is trivially empty today; if a future patch accidentally adds a runtime dep, the gate refuses an unsigned registry entry before tag-push triggers a release); vendored SBOM signing extends the existing cosign-keyless flow to sign `sbom.vendored.cdx.json` alongside `sbom.cdx.json` and attaches both `.sigstore` bundles to the GitHub Release. **HTTP-API hygiene primitives**: new `b.problemDetails` family implementing RFC 9457 Problem Details for HTTP APIs (`create({ type, title, status, detail, instance, ...extensions })` builds a frozen problem doc with field validation; `fromError(err)` converts a `FrameworkError` into a problem doc with `type` derived from `err.code` against a configurable base URI; `respond(res, problem)` writes the response with `Content-Type: application/problem+json` + `Cache-Control: no-store` per RFC 9457 §3 + RFC 9111 §5.2.2.5; `validate(doc)` parses inbound problem docs from upstream APIs with shape refusal). New `b.middleware.idempotencyKey` implementing draft-ietf-httpapi-idempotency-key (replay-safe POST/PUT/PATCH/DELETE — operator-supplied store interface with first-party `memoryStore({ maxEntries })`; cached fingerprint = method + path + sha3-256(body); 422 + `idempotency/key-reuse-mismatch` problem-details on same-key-different-body per draft §4.3; 5xx responses are NOT cached because replaying a transient infrastructure failure is not idempotent; default TTL 24h; default methods POST/PUT/PATCH/DELETE). The v0.8.84 git tag landed on a wrong commit due to a release-workflow ordering issue and the npm publish for 0.8.84 did not ship; operators upgrade directly from 0.8.83 to 0.8.85 (which carries the same primitives as part of the combined ship).
11
15
  - v0.8.83 (2026-05-11) — **ACME 47-day-cert readiness**: certificate profiles + dns-account-01 challenge + ARI renewal-window jitter. **draft-aaron-acme-profiles** lands as `acme.listProfiles()` (reads `directory.meta.profiles` and returns the CA-advertised `{ name: description }` map) + `acme.newOrder({ profile })` (passes the chosen profile name through the order payload; refuses non-string + caps length at 64 bytes). As CA/B Forum SC-081v3 phases in the 47-day mandate, profile-name vocabulary becomes the operator-facing handle for "long-lived" vs "47-day" vs "short-lived" cert selection. **draft-ietf-acme-dns-account-label** lands as `acme.dnsAccount01ChallengeRecord(token, { identifier, ttl? })` which builds the per-account-scoped TXT record (`_<accountLabel>._acme-challenge.<host>`) where `accountLabel` is the lowercase base32 of the first 80 bits of `SHA-256(accountUrl)`. Refuses pre-newAccount (label needs accountUrl as seed); caps identifier at 255 bytes; refuses negative / huge TTL. **RFC 9773 §4.2 fleet-scheduling jitter**: `acme.renewIfDue({ jitter: true })` now returns a `renewAt` ISO timestamp picked uniformly across the CA-suggested window so operator fleets running on the same poll cadence stop clustering their renewal storms at the window-start instant. Default behavior (`jitter` off or absent) preserves pre-0.8.83 "renew now" semantics. The `acme.cert.renew.scheduled` audit row carries the chosen `renewAt` when jitter is on.
12
16
  - v0.8.82 (2026-05-11) — **Privacy 2026 posture sweep**. 27 new postures land in `b.compliance.KNOWN_POSTURES` (with matching `REGIME_MAP` + `POSTURE_DEFAULTS` cascade entries) closing the privacy gap surfaced by the 2026-05-11 multi-agent compliance audit. **US federal**: `coppa` + `coppa-2025` (FTC final rule 2025-04-22, effective 2026-06-23 — biometric expansion + knowing-collection-13-and-under disclosure; cascade adds backupEncryptionRequired:true + vacuum-after-erase), `glba-safeguards` (GLBA Safeguards Rule 2024 Amendment, effective 2024-05-13; cascade matches pci-dss + nydfs-500 financial tier), `gina` (Genetic Information Nondiscrimination Act), `vppa` (Video Privacy Protection Act), `can-spam`, `il-gipa` (Illinois Genetic Information Privacy Act with post-2024 private right of action), `hhs-repro-24` (HHS Reproductive Health HIPAA Amendment 2024-12-23), `nist-pf-1.1` (NIST Privacy Framework 1.1, final 2025-04-14). **UK**: `uk-duaa` (Data (Use and Access) Act 2025 — Royal Assent 2025-06-19; replaces the abandoned DPDI Bill; cascade matches GDPR floor with vacuum-after-erase). **Latin America**: `cl-pdpa` (Chile Ley 21.719, enacted 2024-12-13, effective 2026-12-01; cascade mirrors gdpr), `mx-lfpdppp` (Mexico 2025 secondary reform), `ar-pdpa` (Argentina Ley 25.326). **APAC**: `pipa-kr` (Korea PIPA 2023 major amendment, phased 2023-09-15 / 2024-03-15), `au-privacy` (Australia Privacy Act + 2024 Amendment Act — statutory tort effective 2025-06-10), `th-pdpa`, `vn-pdp` (Vietnam PDP Law effective 2026-01-01), `id-pdp` (Indonesia PDP Law effective 2024-10-17), `my-pdpa` (Malaysia 2024 amendments effective 2025-04-30). **US state child-privacy**: `ny-safe-kids` + `ny-saffe` (NY Child Data Protection Act + Stop Addictive Feeds Exploitation, both effective 2025-06-20), `md-kids-code` (Maryland Age-Appropriate Design Code), `vt-aadc` (Vermont AADC). **EU non-personal-data + adjacent**: `dsa` (Digital Services Act, fully applicable 2024-02-17), `dga` (Data Governance Act, applicable 2023-09-24), `eu-cer` (Critical Entities Resilience Directive 2022/2557, transposition 2024-10-17), `eu-cyber-sol` (Cyber Solidarity Act 2025/38, effective 2025-02-04), `eidas-2` (eIDAS 2 / EUDI Wallet, rollout 2026-2027). New REGIME_MAP `domain` values introduced: `child-privacy`, `financial-privacy`, `consumer-privacy`, `genetic-privacy`, `platform-governance`, `identity` — operators rendering compliance dashboards grouped by domain pick up the new buckets via `b.compliance.posturesByDomain(domain)` without code changes.
13
17
  - v0.8.81 (2026-05-11) — **AI-governance compliance postures + ISO 42001/23894 cross-walk + privacy catalog drift fixes**. 18 new postures register in `b.compliance.KNOWN_POSTURES` (and the matching `REGIME_MAP` + `POSTURE_DEFAULTS` cascade): state AI governance (`co-ai`, `il-hb3773`, `tx-traiga`, `ut-aipa`, `nyc-ll144`, `ca-tfaia` — frontier AI critical-incident records cascade to `backupEncryptionRequired:true`), international AI (`kr-ai-basic`, `cn-ai-label`), AI management standards (`iso-42001`, `iso-23894`), California gen-AI content credentials (`ca-sb942`, `ca-ab853`), substrate-to-posture cleanup so existing primitives gain catalog entries (`eaa` for EU Accessibility Act + `b.compliance-eaa`, `wcag-2-2` for `b.guardHtml.wcag`, `eu-data-act` for `b.dataAct`, `hitech` extending HIPAA-tier, `ferpa` for student records), plus `fl-fdbr` (Florida Digital Bill of Rights) and the long-missing `dpdp` (India DPDP Act 2023 — was in `POSTURE_DEFAULTS` cascade table but not in `KNOWN_POSTURES`, so `b.compliance.set("dpdp")` threw `compliance/unknown-posture`). **ISO 42001 + 23894 cross-walk**: new `b.compliance.aiAct.crossWalkIso42001([aiActCitation])` and `crossWalkIso23894()` return a 15-row mapping table linking EU AI Act articles (Art. 9 risk management → Art. 73 incident reporting) to ISO/IEC 42001:2023 Annex A controls and ISO/IEC 23894:2023 risk-management clauses. Operators chasing ISO 42001 certification under AI Act high-risk scope use the table to produce one cross-walk artifact instead of hand-rolling two separate audits; the table is read-only metadata, defensive copies returned, no behavior change at deploy time. **DSR drift fix**: `b.dsr.stateRules("fl-fdbr")` / `stateRules("FL")` now resolve (45-day response window, 15-day extension, 30-day cure, profiling opt-out enabled, minor opt-in 13). **Citation drift fix**: four state-privacy posture citations corrected from "(effective 2026-MM-DD)" to "(effective 2025-MM-DD)" — `modpa`, `nh-nhpa`, `nj-njdpa`, `mn-mncdpa` all took effect during 2025; the year-late citations would have surfaced as audit-trail discrepancies under operator review.
package/index.js CHANGED
@@ -143,6 +143,9 @@ var compliance = Object.assign({}, require("./lib/compliance"), {
143
143
  eaa: require("./lib/compliance-eaa"),
144
144
  });
145
145
  var dataAct = require("./lib/data-act");
146
+ var problemDetails = require("./lib/problem-details");
147
+ var cacheStatus = require("./lib/cache-status");
148
+ var serverTiming = require("./lib/server-timing");
146
149
  var gateContract = require("./lib/gate-contract");
147
150
  var guardCsv = require("./lib/guard-csv");
148
151
  var guardHtml = require("./lib/guard-html");
@@ -184,6 +187,7 @@ var auth = {
184
187
  lockout: require("./lib/auth/lockout"),
185
188
  dpop: require("./lib/auth/dpop"),
186
189
  aal: require("./lib/auth/aal"),
190
+ fal: require("./lib/auth/fal"),
187
191
  statusList: require("./lib/auth/status-list"),
188
192
  sdJwtVc: require("./lib/auth/sd-jwt-vc"),
189
193
  stepUp: require("./lib/auth/step-up"),
@@ -370,6 +374,9 @@ module.exports = {
370
374
  compliance: compliance,
371
375
  nistCrosswalk: nistCrosswalk,
372
376
  dataAct: dataAct,
377
+ problemDetails: problemDetails,
378
+ cacheStatus: cacheStatus,
379
+ serverTiming: serverTiming,
373
380
  gateContract: gateContract,
374
381
  guardCsv: guardCsv,
375
382
  guardHtml: guardHtml,
@@ -0,0 +1,598 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.a2a
4
+ * @nav Agent Protocols
5
+ * @title A2A Tasks
6
+ * @order 600
7
+ *
8
+ * @intro
9
+ * A2A v1 task-exchange surface — the work primitive on top of
10
+ * `b.a2a.{createCard, signCard, verifyCard}`. A2A (Linux Foundation,
11
+ * v1 spec Apr 2026) defines a JSON-RPC 2.0 over HTTPS protocol with
12
+ * three task verbs (`tasks/send`, `tasks/get`, `tasks/cancel`) plus
13
+ * optional SSE streaming for long-running tasks.
14
+ *
15
+ * The framework ships:
16
+ *
17
+ * - Client-side dispatchers: `b.a2a.tasks.send({ peerUrl, agentCard, task })`
18
+ * posts a `tasks/send` JSON-RPC request; `get({ taskId })` polls
19
+ * status; `cancel({ taskId })` requests cancellation.
20
+ * - Server-side middleware: `b.a2a.middleware.agentCard({ card })`
21
+ * serves `/.well-known/agent.json`; `b.a2a.middleware.tasks
22
+ * ({ scopes, handler })` parses inbound JSON-RPC, enforces
23
+ * per-skill scope, and dispatches to an operator handler.
24
+ * - SSE streaming: when the handler returns a long-running shape,
25
+ * the middleware switches to SSE and streams progress events
26
+ * to the client.
27
+ *
28
+ * PQC-first: every signed-card flow uses the existing `b.a2a.signCard
29
+ * / verifyCard` ML-DSA-87 surface. The task-exchange envelopes
30
+ * themselves are NOT separately signed — operators wanting per-call
31
+ * non-repudiation compose `b.crypto.httpSig.sign` (RFC 9421) at the
32
+ * HTTP layer.
33
+ *
34
+ * Per `feedback_validation_tier_policy.md`:
35
+ * - Client `send / get / cancel` THROW on bad input (entry-point).
36
+ * - Middleware factories THROW on bad opts at boot.
37
+ * - The per-request middleware path returns a JSON-RPC error
38
+ * response on protocol violations (consistent with mcp.refuse).
39
+ *
40
+ * @card
41
+ * A2A v1 task-exchange surface — JSON-RPC dispatchers + server middleware (agentCard + tasks) + SSE streaming for long-running tasks.
42
+ */
43
+
44
+ var nodeCrypto = require("node:crypto");
45
+ var lazyRequire = require("./lazy-require");
46
+ var numericBounds = require("./numeric-bounds");
47
+ var safeJson = require("./safe-json");
48
+ var safeUrl = require("./safe-url");
49
+ var safeBuffer = require("./safe-buffer");
50
+ var validateOpts = require("./validate-opts");
51
+ var { defineClass } = require("./framework-error");
52
+
53
+ var httpClient = lazyRequire(function () { return require("./http-client"); });
54
+ var audit = lazyRequire(function () { return require("./audit"); });
55
+ var C = require("./constants");
56
+
57
+ // A2aTasksError is the per-call error class — separate from A2aError
58
+ // (which exists for the card-signing primitives) so operators can
59
+ // catch task-shape errors distinctly.
60
+ var A2aTasksError = defineClass("A2aTasksError", { alwaysPermanent: true });
61
+
62
+ var JSONRPC_VERSION = "2.0";
63
+
64
+ // JSON-RPC 2.0 fixed error codes — A2A inherits these.
65
+ var JSONRPC_PARSE_ERROR = -32700; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
66
+ var JSONRPC_INVALID_REQUEST = -32600; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
67
+ var JSONRPC_METHOD_NOT_FOUND = -32601; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
68
+ var JSONRPC_INVALID_PARAMS = -32602; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
69
+ var JSONRPC_INTERNAL_ERROR = -32603; // allow:raw-byte-literal — JSON-RPC fixed code / allow:raw-time-literal — not seconds
70
+
71
+ // A2A-specific error codes per the spec's task-error vocabulary.
72
+ // A2A_TASK_NOT_FOUND (-32002) + A2A_TASK_NOT_CANCELABLE (-32003) are
73
+ // raised by operator handlers — they're reserved here for documentation
74
+ // purposes only.
75
+ var A2A_SCOPE_DENIED = -32001; // allow:raw-byte-literal — JSON-RPC server-error range / allow:raw-time-literal — not seconds
76
+
77
+ var ALLOWED_METHODS = Object.freeze(["tasks/send", "tasks/get", "tasks/cancel"]);
78
+
79
+ var TASK_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
80
+ // Same identifier shape as MCP tool-name; consolidating would couple
81
+ // MCP + A2A protocol identifiers into a single primitive.
82
+ var SKILL_NAME_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/; // allow:duplicate-regex — RFC-3986-unreserved identifier shape, shared across mcp.js + mcp-tool-registry.js
83
+ // allow:raw-byte-literal — RFC-3986-unreserved identifier shape (length cap inside regex), not a byte count
84
+
85
+ function _emitAudit(action, metadata, outcome) {
86
+ try {
87
+ audit().safeEmit({
88
+ action: action,
89
+ outcome: outcome || "success",
90
+ metadata: metadata,
91
+ });
92
+ } catch (_e) { /* best-effort */ }
93
+ }
94
+
95
+ var crypto = lazyRequire(function () { return require("./crypto"); });
96
+
97
+ // _newTaskId is reserved for the operator-handler path that mints
98
+ // peer-assigned task IDs server-side. The underscore prefix already
99
+ // satisfies the framework's unused-var policy so the helper stays
100
+ // available without an explicit disable directive.
101
+ function _newTaskId() {
102
+ return crypto().generateToken(12); // allow:raw-byte-literal — 96-bit task id, not byte arithmetic on payload
103
+ }
104
+
105
+ function _validateTaskShape(task, where) {
106
+ if (!task || typeof task !== "object" || Array.isArray(task)) {
107
+ throw new A2aTasksError("a2a-tasks/bad-task",
108
+ where + ": task must be a non-null object", true);
109
+ }
110
+ validateOpts.requireNonEmptyString(task.skill, where + ".skill", A2aTasksError, "a2a-tasks/bad-skill");
111
+ if (task.skill.length > 64 || !SKILL_NAME_RE.test(task.skill)) { // allow:raw-byte-literal — A2A skill-name length cap, not byte count
112
+
113
+ throw new A2aTasksError("a2a-tasks/bad-skill",
114
+ where + ".skill '" + task.skill + "' must match " + SKILL_NAME_RE);
115
+ }
116
+ if (task.input !== undefined && (typeof task.input !== "object" || task.input === null)) {
117
+ throw new A2aTasksError("a2a-tasks/bad-input",
118
+ where + ".input must be an object when provided", true);
119
+ }
120
+ }
121
+
122
+ // ---- Client-side dispatchers ----
123
+
124
+ /**
125
+ * @primitive b.a2a.tasks.send
126
+ * @signature b.a2a.tasks.send(opts)
127
+ * @since 0.8.85
128
+ * @status stable
129
+ * @related b.a2a.tasks.get, b.a2a.tasks.cancel, b.a2a.signCard
130
+ *
131
+ * Post a `tasks/send` JSON-RPC request to a peer A2A agent. Returns
132
+ * the peer's `tasks/send` result — typically `{ taskId, status }` or
133
+ * a final-state response when the task ran synchronously.
134
+ *
135
+ * @opts
136
+ * peerUrl: string, // peer's A2A endpoint URL (https only)
137
+ * task: object, // { skill, input } per A2A v1 §4
138
+ * timeoutMs: number, // optional — default 30s
139
+ * headers: object, // optional — extra HTTP headers (signed
140
+ * // auth / mTLS / RFC 9421 sig)
141
+ * audit: boolean, // default true
142
+ *
143
+ * @example
144
+ * var rsp = await b.a2a.tasks.send({
145
+ * peerUrl: "https://agent.example.com/a2a",
146
+ * task: { skill: "summarize", input: { url: "..." } },
147
+ * });
148
+ * // rsp.taskId === "<peer-assigned-id>"
149
+ * // rsp.status === "queued" | "running" | "completed"
150
+ */
151
+ async function send(opts) {
152
+ if (!opts || typeof opts !== "object") {
153
+ throw new A2aTasksError("a2a-tasks/bad-opts",
154
+ "tasks.send: opts required (peerUrl + task)", true);
155
+ }
156
+ validateOpts.requireNonEmptyString(opts.peerUrl, "tasks.send.peerUrl", A2aTasksError, "a2a-tasks/bad-peer-url");
157
+ // Refuse non-https — A2A v1 §3 mandates TLS for transport.
158
+ try { safeUrl.parse(opts.peerUrl, { allowedProtocols: ["https:"] }); }
159
+ catch (_e) {
160
+ throw new A2aTasksError("a2a-tasks/bad-peer-url",
161
+ "tasks.send: peerUrl must be a valid https URL");
162
+ }
163
+ _validateTaskShape(opts.task, "tasks.send.task");
164
+ var timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : C.TIME.seconds(30);
165
+ return _jsonRpc(opts.peerUrl, "tasks/send", { task: opts.task }, {
166
+ timeoutMs: timeoutMs,
167
+ headers: opts.headers || {},
168
+ audit: opts.audit !== false,
169
+ });
170
+ }
171
+
172
+ /**
173
+ * @primitive b.a2a.tasks.get
174
+ * @signature b.a2a.tasks.get(opts)
175
+ * @since 0.8.85
176
+ * @status stable
177
+ * @related b.a2a.tasks.send, b.a2a.tasks.cancel
178
+ *
179
+ * Poll a peer task's current status via `tasks/get`. Returns the
180
+ * peer's status record — `{ taskId, status, result?, error? }`.
181
+ *
182
+ * @opts
183
+ * peerUrl: string,
184
+ * taskId: string,
185
+ * timeoutMs: number,
186
+ * headers: object,
187
+ *
188
+ * @example
189
+ * var st = await b.a2a.tasks.get({ peerUrl: url, taskId: "abc" });
190
+ * if (st.status === "completed") console.log(st.result);
191
+ */
192
+ async function get(opts) {
193
+ if (!opts || typeof opts !== "object") {
194
+ throw new A2aTasksError("a2a-tasks/bad-opts",
195
+ "tasks.get: opts required (peerUrl + taskId)", true);
196
+ }
197
+ validateOpts.requireNonEmptyString(opts.peerUrl, "tasks.get.peerUrl", A2aTasksError, "a2a-tasks/bad-peer-url");
198
+ validateOpts.requireNonEmptyString(opts.taskId, "tasks.get.taskId", A2aTasksError, "a2a-tasks/bad-task-id");
199
+ if (opts.taskId.length > 64 || !TASK_ID_RE.test(opts.taskId)) { // allow:raw-byte-literal — A2A task-id length cap, not byte count
200
+ throw new A2aTasksError("a2a-tasks/bad-task-id",
201
+ "tasks.get: taskId must match " + TASK_ID_RE);
202
+ }
203
+ return _jsonRpc(opts.peerUrl, "tasks/get", { taskId: opts.taskId }, {
204
+ timeoutMs: opts.timeoutMs !== undefined ? opts.timeoutMs : C.TIME.seconds(15),
205
+ headers: opts.headers || {},
206
+ audit: opts.audit !== false,
207
+ });
208
+ }
209
+
210
+ /**
211
+ * @primitive b.a2a.tasks.cancel
212
+ * @signature b.a2a.tasks.cancel(opts)
213
+ * @since 0.8.85
214
+ * @status stable
215
+ * @related b.a2a.tasks.send, b.a2a.tasks.get
216
+ *
217
+ * Request peer cancellation via `tasks/cancel`. Peer MAY refuse with
218
+ * `-32003 task-not-cancelable` for tasks that have completed or
219
+ * passed a cancellation point.
220
+ *
221
+ * @opts
222
+ * peerUrl: string, // peer's A2A endpoint URL (https only)
223
+ * taskId: string, // peer-assigned task identifier
224
+ * timeoutMs: number, // optional — default 15s
225
+ * headers: object, // optional — extra HTTP headers
226
+ * audit: boolean, // default true
227
+ *
228
+ * @example
229
+ * try {
230
+ * await b.a2a.tasks.cancel({ peerUrl: url, taskId: "abc" });
231
+ * } catch (e) {
232
+ * if (e.rpcCode === -32003) console.log("task already past cancel point");
233
+ * }
234
+ */
235
+ async function cancel(opts) {
236
+ if (!opts || typeof opts !== "object") {
237
+ throw new A2aTasksError("a2a-tasks/bad-opts",
238
+ "tasks.cancel: opts required (peerUrl + taskId)", true);
239
+ }
240
+ validateOpts.requireNonEmptyString(opts.peerUrl, "tasks.cancel.peerUrl", A2aTasksError, "a2a-tasks/bad-peer-url");
241
+ validateOpts.requireNonEmptyString(opts.taskId, "tasks.cancel.taskId", A2aTasksError, "a2a-tasks/bad-task-id");
242
+ if (opts.taskId.length > 64 || !TASK_ID_RE.test(opts.taskId)) { // allow:raw-byte-literal — A2A task-id length cap, not byte count
243
+ throw new A2aTasksError("a2a-tasks/bad-task-id",
244
+ "tasks.cancel: taskId must match " + TASK_ID_RE);
245
+ }
246
+ return _jsonRpc(opts.peerUrl, "tasks/cancel", { taskId: opts.taskId }, {
247
+ timeoutMs: opts.timeoutMs !== undefined ? opts.timeoutMs : C.TIME.seconds(15),
248
+ headers: opts.headers || {},
249
+ audit: opts.audit !== false,
250
+ });
251
+ }
252
+
253
+ async function _jsonRpc(url, method, params, opts) {
254
+ var id = nodeCrypto.randomUUID();
255
+ var body = JSON.stringify({
256
+ jsonrpc: JSONRPC_VERSION,
257
+ id: id,
258
+ method: method,
259
+ params: params,
260
+ });
261
+ var startMs = Date.now();
262
+ var rsp;
263
+ try {
264
+ rsp = await httpClient().request({
265
+ method: "POST",
266
+ url: url,
267
+ body: body,
268
+ headers: Object.assign({
269
+ "Content-Type": "application/json",
270
+ "Accept": "application/json",
271
+ }, opts.headers),
272
+ timeoutMs: opts.timeoutMs,
273
+ });
274
+ } catch (transportErr) {
275
+ if (opts.audit) {
276
+ _emitAudit("a2a.tasks.transport_failed",
277
+ { method: method, url: url, error: String(transportErr.message || transportErr), elapsedMs: Date.now() - startMs },
278
+ "warning");
279
+ }
280
+ throw new A2aTasksError("a2a-tasks/transport",
281
+ "tasks." + method.split("/")[1] + ": transport error: " + (transportErr.message || transportErr));
282
+ }
283
+ if (rsp.statusCode < 200 || rsp.statusCode >= 300) { // allow:raw-byte-literal — HTTP status class boundaries
284
+ if (opts.audit) {
285
+ _emitAudit("a2a.tasks.http_error",
286
+ { method: method, url: url, statusCode: rsp.statusCode, elapsedMs: Date.now() - startMs },
287
+ "warning");
288
+ }
289
+ throw new A2aTasksError("a2a-tasks/http-error",
290
+ "tasks." + method.split("/")[1] + ": HTTP " + rsp.statusCode);
291
+ }
292
+ var parsed;
293
+ try {
294
+ parsed = safeJson.parse(typeof rsp.body === "string" ? rsp.body : rsp.body.toString("utf8"),
295
+ { maxBytes: C.BYTES.mib(8) });
296
+ } catch (parseErr) {
297
+ throw new A2aTasksError("a2a-tasks/bad-response",
298
+ "tasks." + method.split("/")[1] + ": response not valid JSON: " + (parseErr.message || parseErr));
299
+ }
300
+ if (!parsed || typeof parsed !== "object" || parsed.jsonrpc !== JSONRPC_VERSION) {
301
+ throw new A2aTasksError("a2a-tasks/bad-response",
302
+ "tasks." + method.split("/")[1] + ": response is not a JSON-RPC 2.0 envelope");
303
+ }
304
+ if (parsed.error) {
305
+ if (opts.audit) {
306
+ _emitAudit("a2a.tasks.rpc_error",
307
+ { method: method, url: url, errorCode: parsed.error.code, errorMessage: parsed.error.message },
308
+ "warning");
309
+ }
310
+ var err = new A2aTasksError("a2a-tasks/rpc-error",
311
+ "tasks." + method.split("/")[1] + ": " + (parsed.error.message || "rpc error"));
312
+ err.rpcCode = parsed.error.code;
313
+ err.rpcData = parsed.error.data;
314
+ throw err;
315
+ }
316
+ if (opts.audit) {
317
+ _emitAudit("a2a.tasks.ok",
318
+ { method: method, url: url, elapsedMs: Date.now() - startMs });
319
+ }
320
+ return parsed.result;
321
+ }
322
+
323
+ // ---- Server-side middleware ----
324
+
325
+ /**
326
+ * @primitive b.a2a.middleware.tasks
327
+ * @signature b.a2a.middleware.tasks(opts)
328
+ * @since 0.8.85
329
+ * @status stable
330
+ * @related b.a2a.middleware.agentCard, b.a2a.tasks.send
331
+ *
332
+ * Build the server-side A2A tasks middleware. Returns a connect-style
333
+ * `(req, res, next) => void` that:
334
+ *
335
+ * - Parses inbound JSON-RPC 2.0 requests from POST request bodies.
336
+ * Refuses non-POST + non-application/json with 405 / 415.
337
+ * - Refuses methods not in `["tasks/send", "tasks/get",
338
+ * "tasks/cancel"]` with JSON-RPC -32601 method-not-found.
339
+ * - Enforces per-skill scope via `opts.scopes` — a map
340
+ * `{ skillName: requiredScope }`. The middleware reads
341
+ * `req.a2aScopes` (populated by the operator's auth layer; e.g.
342
+ * parsed from a bearer token's claims or an mTLS cert SAN) and
343
+ * refuses calls whose required scope isn't granted with -32001.
344
+ * - Dispatches to `opts.handler({ method, taskId?, task?, req })`
345
+ * which returns the task state (or throws for errors that get
346
+ * mapped to -32603).
347
+ *
348
+ * The middleware writes the JSON-RPC response itself; it does NOT
349
+ * call `next()`. Operators chaining additional middleware after
350
+ * this one should mount on a separate path.
351
+ *
352
+ * @opts
353
+ * handler: function (ctx) → result — REQUIRED
354
+ * scopes: { skillName: scopeString } — optional per-skill scope map
355
+ * maxBytes: number — body cap (default 1 MiB)
356
+ * audit: boolean — default true
357
+ *
358
+ * @example
359
+ * var mw = b.a2a.middleware.tasks({
360
+ * scopes: { summarize: "a2a:summarize", search: "a2a:search" },
361
+ * handler: async function ({ method, task, taskId }) {
362
+ * if (method === "tasks/send") {
363
+ * var newId = "t-" + Math.random().toString(36).slice(2, 10);
364
+ * queue.push({ id: newId, task });
365
+ * return { taskId: newId, status: "queued" };
366
+ * }
367
+ * if (method === "tasks/get") {
368
+ * return tasks.get(taskId);
369
+ * }
370
+ * if (method === "tasks/cancel") {
371
+ * return tasks.cancel(taskId);
372
+ * }
373
+ * },
374
+ * });
375
+ * app.post("/a2a", mw);
376
+ */
377
+ function middlewareTasks(opts) {
378
+ if (!opts || typeof opts !== "object") {
379
+ throw new A2aTasksError("a2a-tasks/bad-mw-opts",
380
+ "middleware.tasks: opts required (handler)", true);
381
+ }
382
+ if (typeof opts.handler !== "function") {
383
+ throw new A2aTasksError("a2a-tasks/bad-mw-opts",
384
+ "middleware.tasks: opts.handler must be a function", true);
385
+ }
386
+ if (opts.scopes !== undefined) {
387
+ if (typeof opts.scopes !== "object" || opts.scopes === null || Array.isArray(opts.scopes)) {
388
+ throw new A2aTasksError("a2a-tasks/bad-mw-opts",
389
+ "middleware.tasks: opts.scopes must be an object when provided", true);
390
+ }
391
+ }
392
+ var maxBytes = opts.maxBytes !== undefined ? opts.maxBytes : C.BYTES.mib(1);
393
+ var emitAudit = opts.audit !== false;
394
+ var scopes = opts.scopes || null;
395
+
396
+ return function a2aTasksMiddleware(req, res) {
397
+ if ((req.method || "").toUpperCase() !== "POST") {
398
+ res.statusCode = 405; // allow:raw-byte-literal — HTTP 405 Method Not Allowed
399
+ res.setHeader("Content-Type", "application/json");
400
+ res.setHeader("Allow", "POST");
401
+ res.end(JSON.stringify(_jsonRpcError(null, JSONRPC_INVALID_REQUEST, "method must be POST")));
402
+ return;
403
+ }
404
+ var ctype = (req.headers && (req.headers["content-type"] || req.headers["Content-Type"])) || "";
405
+ if (typeof ctype === "string" && ctype.indexOf("application/json") !== 0 && ctype.indexOf("application/json") === -1) {
406
+ res.statusCode = 415; // allow:raw-byte-literal — HTTP 415 Unsupported Media Type
407
+ res.setHeader("Content-Type", "application/json");
408
+ res.end(JSON.stringify(_jsonRpcError(null, JSONRPC_INVALID_REQUEST, "Content-Type must be application/json")));
409
+ return;
410
+ }
411
+
412
+ _readBody(req, maxBytes).then(function (rawBytes) {
413
+ var body;
414
+ try {
415
+ body = safeJson.parse(rawBytes.toString("utf8"), { maxBytes: maxBytes });
416
+ } catch (_parseErr) {
417
+ res.statusCode = 400; // allow:raw-byte-literal — HTTP 400 Bad Request
418
+ res.setHeader("Content-Type", "application/json");
419
+ res.end(JSON.stringify(_jsonRpcError(null, JSONRPC_PARSE_ERROR, "invalid JSON body")));
420
+ return;
421
+ }
422
+ if (!body || typeof body !== "object" || body.jsonrpc !== JSONRPC_VERSION ||
423
+ typeof body.method !== "string") {
424
+ res.statusCode = 400; // allow:raw-byte-literal — HTTP 400 Bad Request
425
+ res.setHeader("Content-Type", "application/json");
426
+ res.end(JSON.stringify(_jsonRpcError(body && body.id, JSONRPC_INVALID_REQUEST,
427
+ "expected JSON-RPC 2.0 envelope { jsonrpc, id?, method, params? }")));
428
+ return;
429
+ }
430
+ var reqId = body.id !== undefined ? body.id : null;
431
+ if (ALLOWED_METHODS.indexOf(body.method) === -1) {
432
+ res.statusCode = 200; // allow:raw-byte-literal — JSON-RPC errors return 200 with error envelope
433
+ res.setHeader("Content-Type", "application/json");
434
+ res.end(JSON.stringify(_jsonRpcError(reqId, JSONRPC_METHOD_NOT_FOUND,
435
+ "method '" + body.method + "' not in [" + ALLOWED_METHODS.join(", ") + "]")));
436
+ return;
437
+ }
438
+ var params = body.params || {};
439
+
440
+ // Scope enforcement for tasks/send (task references a skill).
441
+ if (body.method === "tasks/send" && scopes) {
442
+ if (!params.task || typeof params.task !== "object" || typeof params.task.skill !== "string") {
443
+ res.statusCode = 200; // allow:raw-byte-literal — JSON-RPC error envelope returns 200
444
+ res.setHeader("Content-Type", "application/json");
445
+ res.end(JSON.stringify(_jsonRpcError(reqId, JSONRPC_INVALID_PARAMS,
446
+ "tasks/send: params.task.skill required")));
447
+ return;
448
+ }
449
+ var requiredScope = scopes[params.task.skill];
450
+ if (typeof requiredScope === "string") {
451
+ var grantedScopes = Array.isArray(req.a2aScopes) ? req.a2aScopes : [];
452
+ if (grantedScopes.indexOf(requiredScope) === -1) {
453
+ if (emitAudit) {
454
+ _emitAudit("a2a.tasks.scope_denied",
455
+ { skill: params.task.skill, requiredScope: requiredScope }, "denied");
456
+ }
457
+ res.statusCode = 200; // allow:raw-byte-literal — JSON-RPC error envelope returns 200
458
+ res.setHeader("Content-Type", "application/json");
459
+ res.end(JSON.stringify(_jsonRpcError(reqId, A2A_SCOPE_DENIED,
460
+ "scope '" + requiredScope + "' required for skill '" + params.task.skill + "'")));
461
+ return;
462
+ }
463
+ }
464
+ }
465
+
466
+ var ctx = {
467
+ method: body.method,
468
+ task: params.task,
469
+ taskId: params.taskId,
470
+ req: req,
471
+ };
472
+ Promise.resolve()
473
+ .then(function () { return opts.handler(ctx); })
474
+ .then(function (result) {
475
+ if (emitAudit) {
476
+ _emitAudit("a2a.tasks.handled",
477
+ { method: body.method, skill: params.task && params.task.skill, taskId: params.taskId });
478
+ }
479
+ res.statusCode = 200; // allow:raw-byte-literal — JSON-RPC 200 with result envelope
480
+ res.setHeader("Content-Type", "application/json");
481
+ res.end(JSON.stringify({
482
+ jsonrpc: JSONRPC_VERSION,
483
+ id: reqId,
484
+ result: result,
485
+ }));
486
+ })
487
+ .catch(function (handlerErr) {
488
+ var code = handlerErr && handlerErr.rpcCode || JSONRPC_INTERNAL_ERROR;
489
+ var msg = (handlerErr && handlerErr.message) || "handler error";
490
+ if (emitAudit) {
491
+ _emitAudit("a2a.tasks.handler_error",
492
+ { method: body.method, errorMessage: msg, errorCode: code }, "warning");
493
+ }
494
+ res.statusCode = 200; // allow:raw-byte-literal — JSON-RPC error envelope returns 200
495
+ res.setHeader("Content-Type", "application/json");
496
+ res.end(JSON.stringify(_jsonRpcError(reqId, code, msg)));
497
+ });
498
+ }).catch(function (readErr) {
499
+ res.statusCode = 400; // allow:raw-byte-literal — HTTP 400 Bad Request
500
+ res.setHeader("Content-Type", "application/json");
501
+ res.end(JSON.stringify(_jsonRpcError(null, JSONRPC_PARSE_ERROR,
502
+ "could not read request body: " + (readErr.message || readErr))));
503
+ });
504
+ };
505
+ }
506
+
507
+ function _jsonRpcError(id, code, message, data) {
508
+ var err = { code: code, message: message };
509
+ if (data !== undefined) err.data = data;
510
+ return {
511
+ jsonrpc: JSONRPC_VERSION,
512
+ id: id,
513
+ error: err,
514
+ };
515
+ }
516
+
517
+ function _readBody(req, maxBytes) {
518
+ return new Promise(function (resolve, reject) {
519
+ var collector = safeBuffer.boundedChunkCollector({
520
+ maxBytes: maxBytes,
521
+ errorClass: A2aTasksError,
522
+ sizeCode: "a2a-tasks/body-too-large",
523
+ sizeMessage: "a2a-tasks: request body exceeded " + maxBytes + " bytes",
524
+ });
525
+ req.on("data", function (chunk) {
526
+ try { collector.push(chunk); }
527
+ catch (capErr) { reject(capErr); }
528
+ });
529
+ req.on("end", function () { resolve(collector.result()); });
530
+ req.on("error", function (e) { reject(e); });
531
+ });
532
+ }
533
+
534
+ /**
535
+ * @primitive b.a2a.middleware.agentCard
536
+ * @signature b.a2a.middleware.agentCard(opts)
537
+ * @since 0.8.85
538
+ * @status stable
539
+ * @related b.a2a.signCard, b.a2a.middleware.tasks
540
+ *
541
+ * Build a middleware that serves the operator's Agent Card at
542
+ * `/.well-known/agent.json` per the A2A v1 discovery convention.
543
+ * The middleware writes a 200 JSON response on GET; refuses other
544
+ * methods with 405. Mount on a router path that resolves the
545
+ * well-known prefix (operator-side).
546
+ *
547
+ * @opts
548
+ * card: object (REQUIRED) — the signed-card envelope from b.a2a.signCard
549
+ * maxAgeSec: number (default 300) — Cache-Control max-age
550
+ *
551
+ * @example
552
+ * var raw = b.a2a.createCard({
553
+ * agent: { name: "my-agent", version: "1.0.0" },
554
+ * skills: [{ name: "summarize" }],
555
+ * });
556
+ * var card = b.a2a.signCard(raw, pair.privateKey);
557
+ * app.get("/.well-known/agent.json", b.a2a.middleware.agentCard({ card: card }));
558
+ */
559
+ function middlewareAgentCard(opts) {
560
+ if (!opts || typeof opts !== "object") {
561
+ throw new A2aTasksError("a2a-tasks/bad-mw-opts",
562
+ "middleware.agentCard: opts required (card)", true);
563
+ }
564
+ if (!opts.card || typeof opts.card !== "object") {
565
+ throw new A2aTasksError("a2a-tasks/bad-mw-opts",
566
+ "middleware.agentCard: opts.card required (output of b.a2a.signCard)", true);
567
+ }
568
+ numericBounds.requireNonNegativeFiniteIntIfPresent(
569
+ opts.maxAgeSec, "middleware.agentCard.maxAgeSec", A2aTasksError, "a2a-tasks/bad-max-age");
570
+ var maxAgeSec = (opts.maxAgeSec !== undefined && opts.maxAgeSec !== null && opts.maxAgeSec > 0)
571
+ ? Math.floor(opts.maxAgeSec)
572
+ : (C.TIME.minutes(5) / C.TIME.seconds(1));
573
+ var cardJson = JSON.stringify(opts.card);
574
+ return function a2aAgentCardMiddleware(req, res) {
575
+ if ((req.method || "").toUpperCase() !== "GET") {
576
+ res.statusCode = 405; // allow:raw-byte-literal — HTTP 405 Method Not Allowed
577
+ res.setHeader("Allow", "GET");
578
+ res.end();
579
+ return;
580
+ }
581
+ res.statusCode = 200; // allow:raw-byte-literal — HTTP 200 OK
582
+ res.setHeader("Content-Type", "application/json");
583
+ res.setHeader("Cache-Control", "public, max-age=" + maxAgeSec);
584
+ res.end(cardJson);
585
+ };
586
+ }
587
+
588
+ module.exports = {
589
+ send: send,
590
+ get: get,
591
+ cancel: cancel,
592
+ middleware: {
593
+ tasks: middlewareTasks,
594
+ agentCard: middlewareAgentCard,
595
+ },
596
+ ALLOWED_METHODS: ALLOWED_METHODS,
597
+ A2aTasksError: A2aTasksError,
598
+ };