@blamejs/core 0.8.12 → 0.8.15
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 +6 -0
- package/README.md +3 -1
- package/index.js +12 -1
- package/lib/a2a.js +272 -0
- package/lib/ai-input.js +151 -0
- package/lib/audit.js +6 -0
- package/lib/dark-patterns.js +357 -0
- package/lib/framework-error.js +34 -0
- package/lib/graphql-federation.js +176 -0
- package/lib/http-client.js +230 -33
- package/lib/mcp.js +301 -0
- package/lib/middleware/sse.js +18 -20
- package/lib/request-helpers.js +34 -0
- package/lib/router.js +28 -0
- package/lib/sse.js +349 -0
- package/lib/vault/index.js +4 -0
- package/lib/vault/seal-pem-file.js +283 -0
- package/lib/websocket.js +15 -0
- package/package.json +2 -2
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,12 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.8.x
|
|
10
10
|
|
|
11
|
+
- **0.8.15** (2026-05-08) — Transport-layer CVE absorption + AI-protocol primitives. **`b.sse`** — Server-Sent Events transport with newline-injection refusal in `event:` / `id:` / `data:` fields and the `Last-Event-ID` reconnect header (CVE-2026-33128 h3, CVE-2026-29085 Hono, CVE-2026-44217 sse-channel — three CVEs published in the same vulnerability class). Channel API: `channel.send({event,id,data,retry})` validates each field, refuses LF/CR/NUL, splits multi-line `data` into per-spec multiple `data:` lines, drives a `:keepalive` heartbeat with operator-tunable interval. `b.sse.serializeEvent({...})` exposes the encoder for buffered pipelines. **`b.mcp.serverGuard`** — Model Context Protocol server-side hardening. Bearer auth required by default (CVE-2026-33032 nginx-ui auth-bypass class), `redirect_uri` exact-match allowlist enforced per OAuth 2.1 / RFC 9700 §4.1.1 (CVE-2025-6514 mcp-remote OAuth RCE class), dynamic client-registration refused unless `allowDynamicRegister: true` with operator-supplied registration allowlist (confused-deputy class), tool/resource name allowlists at the guard layer. JSON-RPC 2.0 envelope validator. **`b.graphqlFederation.guardSdl`** — refuses `_service.sdl` / `_entities` probes without a router-token Bearer + optional single-use nonce; closes the schema-leak class where operators disable introspection thinking the schema is hidden. **`b.ai.input.classify`** — pattern-based prompt-injection classifier covering OWASP LLM01:2025 + NIST COSAIS RFI shapes: instruction-override / persona-jailbreak / role-reset markers / OpenAI system-tag templates / tool-call injection / exfil-callback / encoded-bypass (base64/rot13) / markdown+HTML smuggling / BIDI/zero-width/control char density. Severity-3 hits → `verdict: "malicious"`; 2+ severity-2 hits → `"suspicious"`; otherwise `"clean"`. Inline-on-every-request perf cost — no LLM, no network. **`b.a2a`** — A2A (Linux Foundation Agentic AI Foundation) v1.x signed agent-card primitive. `signCard` produces an envelope with a detached ML-DSA-87 signature over the SHA3-512 of the canonical-JSON serialization (RFC 8785-aligned); `verifyCard` validates signature + expiry + issuer match. Endpoints HTTPS-only (or localhost) at validation time. **`b.darkPatterns`** — FTC Negative Option Rule click-to-cancel UX-parity attestation primitive. `recordSignupFlow` / `recordCancelFlow` capture operator-attested click counts, CTA contrast / font weight, channel, confirmation steps; `assertParity` returns `{ ok, breaches }` against `ftc-2024` / `ca-sb942` / `strict` postures. `middleware({lookupAttestation, resourceIdFromReq})` refuses cancel-endpoint requests with HTTP 451 if no parity attestation is on file. **WebSocket control-frame size cap** — `lib/websocket.js` `_handleFrame` refuses any control frame (opcodes ≥ 0x8: CLOSE/PING/PONG) with payload length > 125 or `fin = false` (RFC 6455 §5.5). Closes the 2× outbound-bandwidth amplification class where a 1 MiB PING was echoed verbatim as PONG. **`b.requestHelpers.safeHeadersDistinct(req)`** — defensive accessor for `req.headersDistinct` that bypasses Node's faulty getter (Node CVE-2026-21710 — reading `__proto__` on the underlying header bag throws synchronously inside the getter, escaping handler-level try/catch). Computes the same null-prototype shape directly from `req.rawHeaders`. **TLS / SNI hardening** — `router.listen()` now wraps any operator-supplied `tlsOptions.SNICallback` so synchronous throws (Node CVE-2026-21637) become a clean async `(err, null)` callback rather than crashing the listener. Inbound TLS now defaults to `minVersion: "TLSv1.3"` when the operator's `tlsOptions` doesn't pin one (closes the gap where bare `{key, cert}` inherited Node's TLSv1.2 default). **httpClient identity decoding** — `b.httpClient` now sends `Accept-Encoding: identity` by default (h1 + h2 paths) — refuses compressed responses unless the operator explicitly opts in. Closes the undici unbounded-decompression amplification class (CVE-2026-22036). Operators that need compressed responses pass an explicit `Accept-Encoding` header. **Engine pin** — `engines.node` raised from `>=24.0.0` to `>=24.4.0` to ensure the undici fix is bundled.
|
|
12
|
+
|
|
13
|
+
- **0.8.14** (2026-05-07) — `b.vault.sealPemFile` — auto-resealing wrapper for at-rest PEM files. Operators with ACME / Let's Encrypt renewals get fresh certs every 30-60 days; the renewal writes plaintext PEM to disk, signals the application to reload, and leaves the cleartext file unencrypted between the renewal write and the next manual re-seal. `b.vault.sealPemFile({ source, destination })` closes that window: the framework reads the source, vault-seals it, atomically writes `<destination>` (`.tmp` + `fsync` + `rename` + `fsyncDir`), and registers an `fs.watchFile` poll on the source. Every mtime change triggers an automatic re-seal — the operator-visible `<destination>.rewriting` marker is created before the rename and removed after, giving crash recovery a signal: when `sealPemFile()` starts and the marker is present, it re-seals from source idempotently. Returns `{ stop, generation, lastResealedAt, lastError, watching, forceReseal }` so operators can wire the watcher into existing lifecycle hooks (`b.appShutdown.addPhase({ name: "pem-watcher", run: () => watcher.stop() })`). `pollInterval` defaults to 2s — ACME renewal cadence is days, so polling latency is irrelevant against the renewal interval; operators with sub-second requirements override. `fs.watchFile` (the polling backend) is used instead of `fs.watch` (inotify / kqueue) because watchFile is consistent across platforms — Linux fires multiple change events per rename, macOS doesn't fire on renamed-into files, and the polling cadence is acceptable here.
|
|
14
|
+
|
|
15
|
+
- **0.8.13** (2026-05-07) — Streaming multipart uploads + `onChunk` response hook on `b.httpClient`. **Streaming multipart** — `b.httpClient.request({ multipart: { files: [...] } })` now accepts file entries in three shapes: the existing `{ field, content: Buffer | string }` (in-memory), plus new `{ field, filePath: string }` (stream-from-disk via `fs.createReadStream`) and `{ field, stream: Readable, size?: number }` (operator-supplied stream). When every entry's size is statically resolvable (Buffer length / `fs.statSync().size` / explicit `opts.size`), the framework sets `Content-Length` and uses identity transfer; otherwise the framework omits the header and Node's HTTP layer falls back to chunked transfer. Closes the `Buffer.concat` OOM class on large uploads — the body is materialized one chunk at a time through a `Readable.from(asyncIterator)` that yields boundary headers, source bytes, and CRLF in order. Fast-path preserved: when no streaming source is involved, `_buildMultipartBody` still returns a single Buffer with a known `Content-Length`. **`onChunk(chunk)` response hook** — fires for each response data chunk in BOTH `responseMode: "buffer"` and `responseMode: "stream"`. Use case: hash bytes during pipe-to-disk without an extra Transform pass (`onChunk: (c) => hasher.update(c)`). Throws inside the hook are caught and dropped — a hash-mismatch detector can raise without breaking the pipe; callers surface the error through their own pipe handler. Verified-already-shipped during the v0.8.x framework-gap audit: `b.httpClient` `responseMode: "always-resolve"`, `onRedirect({from, to, hop, headersStripped, statusCode})` hook, `body: Readable` upload path; `b.cryptoField` derived-hash domain separation (`bj-<table>-<field>:` per-field namespace prefix matches the indexed-lookup requirement); `b.config` `redactKeys` allowlist + `redacted()` view.
|
|
16
|
+
|
|
11
17
|
- **0.8.12** (2026-05-07) — WebSocket upgrade refuses credential-shaped query parameters by default. `validateUpgradeRequest(req, opts)` now scans the request URL for the credential-leak names `access_token`, `bearer`, `bearer_token`, `apikey`, `api_key`, `api-key`, `authorization` (case-insensitive, with percent-decoding) and refuses the upgrade with HTTP 400 when one is present. URL query strings leak through web-server access logs, browser history, the Referer header forwarded to third-party CDN / analytics, in-process / proxy log captures, and crash dumps — RFC 6750 §2.3 explicitly cautions against bearer tokens in URI query parameters for these reasons. Operators with a non-credential parameter that happens to share a credential-shaped name opt out per route via `opts.allowQueryAuthParams: true` with an audited operator reason. The refused list is deliberately narrow: overloaded names (`token`, `auth`, `key`, `session`) have non-credential meanings (CSRF tokens, file-share tokens, session-resume identifiers) and are NOT refused.
|
|
12
18
|
|
|
13
19
|
- **0.8.11** (2026-05-07) — Three new state-and-federal regulatory primitives + a per-primitive test-coverage gate. **`b.breach.deadline` + `b.breach.report`** — all-50-states data-breach-notification deadline registry. `b.breach.deadline.forStates(states, detectedAt)` returns per-state `{ state, kind, dueBy, citation }` records (`kind: "as-soon-as-possible"` for AS-OF / `"hard-deadline"` for fixed-day deadlines like Texas / Florida / Maine). `b.breach.report.create()` opens a multi-state breach with a single record, tracks per-state filings via `fileNotice(id, state, ...)`, exposes `pending(id)` for dashboards, and auto-closes once every affected state has filed. Every transition records a `breach.report.*` audit event. Statutory citations + day counts wired in `lib/breach-deadline.js` per-state. **`b.ai.adverseDecision`** — wraps an operator-supplied `decide(subject)` predicate, automatically attaches a consumer-rights notice when the outcome is `"adverse"` / `"denied"` / `"rejected"`. Built-in regulation templates for `gdpr-22` (Article 22 automated-decision rights), `ai-act-86` (EU AI Act high-risk consumer recourse), `ecoa-1002.9` (US Equal Credit Opportunity Act adverse-action notice), `colorado-ai-act` (CO SB 24-205 §6-1-1701), `nyc-ll-144` (NYC Local Law 144 employment AEDT), `fcra-615` (US FCRA adverse action), and `operator-defined`. Notice carries `principalReasons` + `consumerRights: { requestData, requestExplanation, contestDecision, requestHumanReview }` shaped per regime. **`b.middleware.ageGate`** — request-level age-classification middleware. Operator-supplied `getAge(req)` returns the subject age (or null/undefined when unknown); middleware classifies as `"above-threshold"` / `"below-threshold"` / `"unknown"` against `consentRequired`, sets `X-Privacy-Posture` header, and refuses with 451 + audited reason when `requireAge` is set and `hasParentalConsent(req)` is unmet. Composes upstream of session / authn for COPPA / AADC / UK Children's Code postures. **Per-primitive test-coverage gate** — new `test/layer-0-primitives/test-coverage.test.js` walks every operator-facing `b.*` primitive and refuses release unless the primitive has at least one test reference (or an explicit `UNTESTED_BACKLOG` entry naming the reason). Closes the drift class where a primitive landed on `b.*` but never gained a unit test.
|
package/README.md
CHANGED
|
@@ -47,7 +47,9 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
47
47
|
- **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log, request-time DB role binding via `b.middleware.dbRoleFor`, in-process CIDR fence via `b.middleware.networkAllowlist`) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate (cloud-metadata IPs hard-denied unconditionally; private / loopback / link-local overridable per call), scheme + userinfo + per-host (wildcard / per-method) destination allowlist, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`); operator-tunable network configurability — env-driven NTP / NTS (RFC 8915 authenticated time), IPv4-or-IPv6 NTP servers, DNS with IPv6 / DoH / DoT (private-CA trust pinning via `opts.ca`) / cache / lookup timeout, outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`), runtime DPI trust-store CA additions, application-level heartbeats, TCP socket defaults (`b.network`).
|
|
48
48
|
- **Defensive parsers** — `b.safeJson`, `b.safeBuffer`, `b.safeSql`, `b.safeSchema`, `b.parsers` (XML / TOML / YAML / .env), `b.config` (schema-validated env), `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.).
|
|
49
49
|
- **Content-safety gates** — `b.gateContract` uniform composition contract (mode posture / hooks / forensic snapshot / decision cache / runtime cap). Family members: `b.guardCsv` (formula injection ASCII + full-width prefixes, dangerous-function denylist, bidi / homoglyph / control / null / BOM / zero-width detection, dialect ambiguity, CSV-bombs, numeric precision, schema-bound serializer); `b.guardHtml` (XSS / mXSS / DOM-clobbering / dangerous-tag / event-handler-family / dangerous-URL-scheme with entity-decode / CSS-injection in style attribute / IE conditional comments + token-level sanitize + always-correct `escapeText` / `escapeAttr` entity encoders); `b.guardSvg` (script / foreignObject / animation-element href hijack / DOCTYPE billion-laughs / XXE / SVGZ / cross-origin `<use>` SSRF / event-handlers + token-level sanitize). `b.guardArchive` (zip-slip / symlink + hardlink escape / decompression-ratio bombs (per-entry + aggregate) / nested-archive depth / duplicate-entry / case-insensitive collision / encryption-claim mismatch / format-claim mismatch via magic-byte detection — composes `b.guardFilename` for per-entry-name validation); `b.guardJson` (source-level prototype-pollution detection, duplicate-key, NaN/Infinity, JSON5 syntax, BOM, bidi, numeric-precision-loss, top-level-key allowlist, depth/breadth/array/string/node-count caps); `b.guardYaml` (deserialization-tag RCE via language-specific tag prefixes, billion-laughs alias recursion, Norway-problem implicit booleans, leading-zero octals, multi-document streams, duplicate keys, merge-key chains, depth+anchor+node caps); `b.guardXml` (XXE / billion-laughs / external-entity / parameter-entity / XInclude / xsi:schemaLocation / processing-instruction / CDATA / XML-signature-wrapping detection — DOCTYPE refused at all profile levels); `b.guardMarkdown` (source-level scan run BEFORE any markdown renderer sees the input — dangerous URL schemes in inline links + images + autolinks + reference-link definitions with HTML-entity decode bypass; whitespace-tolerant dangerous-tag matching per CVE-2026-30838; front-matter; HTML comments; code-fence language injection; catastrophic emphasis-run ReDoS per CVE-2025-6493 class; inline DOCTYPE; depth + link + image + autolink + ref-def caps); `b.guardEmail` (single-address + full RFC 822/5322 message validation — SMTP smuggling per CVE-2023-51764 / 51765 / 51766 / CVE-2026-32178 class via bare-CR + bare-LF + smuggled SMTP verbs; CRLF header injection; IDN homograph mixed-script domains with operator-opt-in `allowedScripts`; Punycode flag; display-name spoofing; IP-literal addresses; RFC 5322 comment syntax; multi-@; RFC 5321 length caps + RFC 5322 line cap; BOM injection). Filename safety: `b.guardFilename` (path traversal raw + percent-encoded + overlong-UTF-8 + null-byte truncation + Windows reserved names + NTFS ADS + RTLO bidi spoofing + shell-exec / double-extension detection — standalone, wires into `b.fileUpload` via `filenameSafety`). All members ship strict / balanced / permissive profiles plus hipaa / pci-dss / gdpr / soc2 compliance postures. `b.guardAll` is the registry + aggregator: every shipped guard ON by default; opt-out per guard with audited reason via `exceptFor: { name: { reason } }`. **As of v0.7.12, `b.fileUpload` and `b.staticServe` wire `b.guardAll.byExtension({ profile: "strict" })` automatically + `b.fileUpload` also wires `b.guardFilename.gate({ profile: "strict" })` as `filenameSafety`** — defense-in-depth applied without any explicit operator wiring. Operators opt out per host-primitive via `contentSafety: null` / `filenameSafety: null` (audited at create() with operator-supplied reason).
|
|
50
|
-
- **Communication** — WebSockets with channel/room fan-out across cluster replicas (`b.websocket`, `b.websocketChannels`); outbound WebSocket client (RFC 6455) with PQC-TLS handshake, permessage-deflate negotiation with decompression-bomb cap, fatal UTF-8 validation, control-frame size + FIN enforcement, permanent-error classifier that skips reconnect on 4xx handshake / accept mismatch / bad-subprotocol, exponential-backoff with full jitter (`b.wsClient`); generic distributed pub/sub with cluster-table / Redis PUB/SUB / custom backends (`b.pubsub`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); inbound mail authentication — SPF / DMARC / ARC verify + ARC chain signing for relays (`b.mail.spf`, `b.mail.dmarc`, `b.mail.arc`); generic notification dispatcher with operator-supplied transports (`b.notify`); chunked file uploads with per-chunk SHA3-512 verification + atomic finalize + tombstone cleanup (`b.fileUpload`).
|
|
50
|
+
- **Communication** — WebSockets with channel/room fan-out across cluster replicas + RFC 6455 §5.5 control-frame size + FIN enforcement on inbound (defends 1 MiB-PING-echoed-as-PONG 2× amplification class) (`b.websocket`, `b.websocketChannels`); outbound WebSocket client (RFC 6455) with PQC-TLS handshake, permessage-deflate negotiation with decompression-bomb cap, fatal UTF-8 validation, control-frame size + FIN enforcement, permanent-error classifier that skips reconnect on 4xx handshake / accept mismatch / bad-subprotocol, exponential-backoff with full jitter (`b.wsClient`); generic distributed pub/sub with cluster-table / Redis PUB/SUB / custom backends (`b.pubsub`); Server-Sent Events with newline-injection refusal in `event:` / `id:` / `data:` / `Last-Event-ID` per CVE-2026-33128 / 29085 / 44217 class (`b.sse`, `b.middleware.sse`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); inbound mail authentication — SPF / DMARC / ARC verify + ARC chain signing for relays (`b.mail.spf`, `b.mail.dmarc`, `b.mail.arc`); generic notification dispatcher with operator-supplied transports (`b.notify`); chunked file uploads with per-chunk SHA3-512 verification + atomic finalize + tombstone cleanup (`b.fileUpload`).
|
|
51
|
+
- **AI / agentic** — Model Context Protocol server-guard with bearer auth + redirect_uri allowlist + dynamic-register refusal + tool/resource allowlists (CVE-2026-33032 / CVE-2025-6514 / confused-deputy class) (`b.mcp.serverGuard`); GraphQL Federation `_service.sdl` trust-boundary with router-token + nonce store (`b.graphqlFederation`); prompt-injection input classifier (OWASP LLM01:2025 / NIST COSAIS RFI) (`b.ai.input.classify`); A2A signed agent-card primitive (Linux Foundation Agentic AI Foundation v1.x, ML-DSA-87) (`b.a2a`).
|
|
52
|
+
- **FTC compliance** — click-to-cancel UX-parity attestation with `ftc-2024` / `ca-sb942` / `strict` postures (`b.darkPatterns`).
|
|
51
53
|
- **Observability** — tamper-evident audit chain with SLH-DSA-signed checkpoints, metrics, tracing (OTel pass-through when wired), PII redaction, log-stream sinks (local file rotation, generic webhook, OTLP/HTTP-JSON OR OTLP/gRPC to an OTel collector, AWS CloudWatch Logs via SigV4 with optional autoCreate, RFC 5424 syslog over UDP/TCP/TLS), OTLP/HTTP-JSON exporter for traces + metrics (`b.audit`, `b.metrics`, `b.tracing`, `b.redact`, `b.logStream`, `b.otelExport`); operator-callable boot-time security policy assertions (`b.security.assertProduction`) and tamper-evident config-baseline drift detection signed with the audit-signing key (`b.configDrift`).
|
|
52
54
|
- **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`).
|
|
53
55
|
- **Format helpers** — RFC 4180 CSV with Excel formula-injection prevention (`b.csv`), RFC 9562 UUID v4 + v7 (`b.uuid`), URL-safe slugs (`b.slug`), TZ-aware datetime (`b.time`), ZIP creation (`b.archive`), HMAC-signed cursor pagination (`b.pagination`), HTML form rendering + validation + CSRF (`b.forms`).
|
package/index.js
CHANGED
|
@@ -94,6 +94,12 @@ var httpClient = require("./lib/http-client");
|
|
|
94
94
|
httpClient.encrypted = require("./lib/middleware/api-encrypt").httpClient;
|
|
95
95
|
httpClient.cookieJar = require("./lib/http-client-cookie-jar");
|
|
96
96
|
var websocket = require("./lib/websocket");
|
|
97
|
+
var sse = require("./lib/sse");
|
|
98
|
+
var mcp = require("./lib/mcp");
|
|
99
|
+
var graphqlFederation = require("./lib/graphql-federation");
|
|
100
|
+
var aiInput = require("./lib/ai-input");
|
|
101
|
+
var a2a = require("./lib/a2a");
|
|
102
|
+
var darkPatterns = require("./lib/dark-patterns");
|
|
97
103
|
var safeUrl = require("./lib/safe-url");
|
|
98
104
|
var safeRedirect = require("./lib/safe-redirect");
|
|
99
105
|
var pick = require("./lib/pick");
|
|
@@ -255,7 +261,7 @@ module.exports = {
|
|
|
255
261
|
nis2: { report: require("./lib/nis2-report") },
|
|
256
262
|
gdpr: { ropa: require("./lib/gdpr-ropa") },
|
|
257
263
|
breach: require("./lib/breach-deadline"),
|
|
258
|
-
ai: { adverseDecision: require("./lib/ai-adverse-decision") },
|
|
264
|
+
ai: { adverseDecision: require("./lib/ai-adverse-decision"), input: aiInput },
|
|
259
265
|
queue: queue,
|
|
260
266
|
logStream: logStream,
|
|
261
267
|
redact: redact,
|
|
@@ -276,6 +282,11 @@ module.exports = {
|
|
|
276
282
|
frameworkError: frameworkError,
|
|
277
283
|
httpClient: httpClient,
|
|
278
284
|
websocket: websocket,
|
|
285
|
+
sse: sse,
|
|
286
|
+
mcp: mcp,
|
|
287
|
+
graphqlFederation: graphqlFederation,
|
|
288
|
+
a2a: a2a,
|
|
289
|
+
darkPatterns: darkPatterns,
|
|
279
290
|
safeUrl: safeUrl,
|
|
280
291
|
safeRedirect: safeRedirect,
|
|
281
292
|
pick: pick,
|
package/lib/a2a.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A2A (Agent-to-Agent) v1.x signed agent-card primitive.
|
|
4
|
+
*
|
|
5
|
+
* Linux Foundation Agentic AI Foundation A2A protocol — agents
|
|
6
|
+
* advertise their capabilities, identity, endpoints, and policies via
|
|
7
|
+
* an "agent card" that another agent fetches before initiating
|
|
8
|
+
* collaboration. The 1.x protocol moved to required signed cards: the
|
|
9
|
+
* card is a JSON document signed (detached signature) by the issuing
|
|
10
|
+
* agent's identity key. Verifiers reject unsigned or expired cards.
|
|
11
|
+
*
|
|
12
|
+
* Public API:
|
|
13
|
+
*
|
|
14
|
+
* a2a.signCard(card, privateKeyPem, opts) -> { card, signature, signedAt, expiresAt }
|
|
15
|
+
* Canonicalizes the card and returns a signed envelope. The
|
|
16
|
+
* signature is over the SHA3-512 hash of the canonical-JSON
|
|
17
|
+
* serialization (RFC 8785). Algorithm is whatever's pinned in
|
|
18
|
+
* privateKeyPem (defaults to ML-DSA-87 per framework crypto
|
|
19
|
+
* defaults). opts:
|
|
20
|
+
* ttlMs — default 24 hours.
|
|
21
|
+
* audit — bool, default true.
|
|
22
|
+
* errorClass — A2aError by default.
|
|
23
|
+
*
|
|
24
|
+
* a2a.verifyCard(envelope, publicKeyPem, opts) -> { valid, claims, reason? }
|
|
25
|
+
* Verifies the signature, expiry, and required-fields shape.
|
|
26
|
+
* opts:
|
|
27
|
+
* maxBytes — card cap (default 64 KiB).
|
|
28
|
+
* clockSkewMs — allowance on expiresAt (default 5 minutes).
|
|
29
|
+
* expectedIssuer — optional string; refuse if card.issuer !== this.
|
|
30
|
+
*
|
|
31
|
+
* a2a.canonicalize(card) -> string
|
|
32
|
+
* RFC 8785-aligned canonical JSON (sorted keys, no whitespace).
|
|
33
|
+
* Exposed for operators that store the canonical form alongside
|
|
34
|
+
* the signature.
|
|
35
|
+
*
|
|
36
|
+
* a2a.createCard(opts) -> card
|
|
37
|
+
* Convenience constructor:
|
|
38
|
+
* opts: { issuer, agentId, capabilities, endpoints, policies, contact, version }
|
|
39
|
+
* All fields validated for shape.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
var crypto = require("./crypto");
|
|
43
|
+
var canonicalJson = require("./canonical-json");
|
|
44
|
+
var C = require("./constants");
|
|
45
|
+
var nb = require("./numeric-bounds");
|
|
46
|
+
var audit = require("./audit");
|
|
47
|
+
var { A2aError } = require("./framework-error");
|
|
48
|
+
|
|
49
|
+
var REQUIRED_CARD_FIELDS = ["issuer", "agentId", "capabilities", "version"];
|
|
50
|
+
var ID_MAX = 256; // allow:raw-byte-literal — string-length cap, not bytes
|
|
51
|
+
var SEMVER_MAX = 64; // allow:raw-byte-literal — string-length cap, not bytes
|
|
52
|
+
var CAP_NAME_MAX = 128; // allow:raw-byte-literal — string-length cap, not bytes
|
|
53
|
+
var SHAKE256_BYTES = 64; // allow:raw-byte-literal — SHA3-512 output is 64 bytes (FIPS 202)
|
|
54
|
+
var ID_RE = /^[a-zA-Z0-9._:/-]{1,256}$/;
|
|
55
|
+
var SEMVER_RE = /^[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:-[A-Za-z0-9.-]+)?$/;
|
|
56
|
+
|
|
57
|
+
function _validateCardShape(card, errorClass) {
|
|
58
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) {
|
|
59
|
+
throw errorClass.factory("BAD_CARD",
|
|
60
|
+
"a2a: card must be an object");
|
|
61
|
+
}
|
|
62
|
+
for (var i = 0; i < REQUIRED_CARD_FIELDS.length; i += 1) {
|
|
63
|
+
var f = REQUIRED_CARD_FIELDS[i];
|
|
64
|
+
if (typeof card[f] === "undefined" || card[f] === null) {
|
|
65
|
+
throw errorClass.factory("MISSING_FIELD",
|
|
66
|
+
"a2a: card." + f + " is required");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (typeof card.issuer !== "string" || card.issuer.length > ID_MAX || !ID_RE.test(card.issuer)) {
|
|
70
|
+
throw errorClass.factory("BAD_FIELD",
|
|
71
|
+
"a2a: card.issuer shape (must match " + ID_RE + ")");
|
|
72
|
+
}
|
|
73
|
+
if (typeof card.agentId !== "string" || card.agentId.length > ID_MAX || !ID_RE.test(card.agentId)) {
|
|
74
|
+
throw errorClass.factory("BAD_FIELD",
|
|
75
|
+
"a2a: card.agentId shape");
|
|
76
|
+
}
|
|
77
|
+
if (typeof card.version !== "string" || card.version.length > SEMVER_MAX || !SEMVER_RE.test(card.version)) {
|
|
78
|
+
throw errorClass.factory("BAD_FIELD",
|
|
79
|
+
"a2a: card.version must be semver");
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(card.capabilities)) {
|
|
82
|
+
throw errorClass.factory("BAD_FIELD",
|
|
83
|
+
"a2a: card.capabilities must be an array");
|
|
84
|
+
}
|
|
85
|
+
for (var c = 0; c < card.capabilities.length; c += 1) {
|
|
86
|
+
var cap = card.capabilities[c];
|
|
87
|
+
if (typeof cap !== "string" || cap.length === 0 || cap.length > CAP_NAME_MAX) {
|
|
88
|
+
throw errorClass.factory("BAD_FIELD",
|
|
89
|
+
"a2a: card.capabilities[" + c + "] must be 1-128 char string");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (card.endpoints !== undefined) {
|
|
93
|
+
if (!Array.isArray(card.endpoints)) {
|
|
94
|
+
throw errorClass.factory("BAD_FIELD",
|
|
95
|
+
"a2a: card.endpoints must be an array");
|
|
96
|
+
}
|
|
97
|
+
for (var e = 0; e < card.endpoints.length; e += 1) {
|
|
98
|
+
var ep = card.endpoints[e];
|
|
99
|
+
if (!ep || typeof ep !== "object" || typeof ep.url !== "string") {
|
|
100
|
+
throw errorClass.factory("BAD_FIELD",
|
|
101
|
+
"a2a: card.endpoints[" + e + "] must have a string url");
|
|
102
|
+
}
|
|
103
|
+
if (!/^https:\/\//.test(ep.url) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])/.test(ep.url)) {
|
|
104
|
+
throw errorClass.factory("INSECURE_ENDPOINT",
|
|
105
|
+
"a2a: card.endpoints[" + e + "].url must be HTTPS (or localhost)");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function canonicalize(card) {
|
|
112
|
+
return canonicalJson.stringify(card);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createCard(opts) {
|
|
116
|
+
opts = opts || {};
|
|
117
|
+
var card = {
|
|
118
|
+
issuer: opts.issuer,
|
|
119
|
+
agentId: opts.agentId,
|
|
120
|
+
version: opts.version || "1.0.0",
|
|
121
|
+
capabilities: Array.isArray(opts.capabilities) ? opts.capabilities.slice() : [],
|
|
122
|
+
};
|
|
123
|
+
if (opts.endpoints) card.endpoints = opts.endpoints;
|
|
124
|
+
if (opts.policies) card.policies = opts.policies;
|
|
125
|
+
if (opts.contact) card.contact = opts.contact;
|
|
126
|
+
if (opts.metadata) card.metadata = opts.metadata;
|
|
127
|
+
_validateCardShape(card, A2aError);
|
|
128
|
+
return card;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function signCard(card, privateKeyPem, opts) {
|
|
132
|
+
opts = opts || {};
|
|
133
|
+
var errorClass = opts.errorClass || A2aError;
|
|
134
|
+
var auditOn = opts.audit !== false;
|
|
135
|
+
_validateCardShape(card, errorClass);
|
|
136
|
+
|
|
137
|
+
if (typeof privateKeyPem !== "string" || privateKeyPem.length === 0) {
|
|
138
|
+
throw errorClass.factory("BAD_KEY",
|
|
139
|
+
"a2a.signCard: privateKeyPem required");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
nb.requirePositiveFiniteIntIfPresent(opts.ttlMs, "a2a.signCard: opts.ttlMs", errorClass, "BAD_TTL");
|
|
143
|
+
var ttlMs = opts.ttlMs || C.TIME.hours(24);
|
|
144
|
+
var signedAt = Date.now();
|
|
145
|
+
var expiresAt = signedAt + ttlMs;
|
|
146
|
+
|
|
147
|
+
var envelopePayload = {
|
|
148
|
+
card: card,
|
|
149
|
+
signedAt: signedAt,
|
|
150
|
+
expiresAt: expiresAt,
|
|
151
|
+
};
|
|
152
|
+
var canonical = canonicalize(envelopePayload);
|
|
153
|
+
var digest = crypto.shake256
|
|
154
|
+
? crypto.shake256(Buffer.from(canonical, "utf8"), SHAKE256_BYTES)
|
|
155
|
+
: null;
|
|
156
|
+
var dataToSign = digest ? digest : Buffer.from(canonical, "utf8");
|
|
157
|
+
var signature = crypto.sign(dataToSign, privateKeyPem);
|
|
158
|
+
|
|
159
|
+
if (auditOn) {
|
|
160
|
+
audit.safeEmit({
|
|
161
|
+
action: "a2a.card_signed",
|
|
162
|
+
outcome: "success",
|
|
163
|
+
metadata: { issuer: card.issuer, agentId: card.agentId, expiresAt: expiresAt },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
card: card,
|
|
169
|
+
signedAt: signedAt,
|
|
170
|
+
expiresAt: expiresAt,
|
|
171
|
+
signature: signature.toString("base64"),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function verifyCard(envelope, publicKeyPem, opts) {
|
|
176
|
+
opts = opts || {};
|
|
177
|
+
var errorClass = opts.errorClass || A2aError;
|
|
178
|
+
nb.requirePositiveFiniteIntIfPresent(opts.maxBytes, "a2a.verifyCard: opts.maxBytes", errorClass, "BAD_MAX_BYTES");
|
|
179
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.clockSkewMs, "a2a.verifyCard: opts.clockSkewMs", errorClass, "BAD_SKEW");
|
|
180
|
+
var maxBytes = opts.maxBytes || C.BYTES.kib(64);
|
|
181
|
+
var clockSkewMs = opts.clockSkewMs !== undefined ? opts.clockSkewMs : C.TIME.minutes(5);
|
|
182
|
+
var expectedIssuer = typeof opts.expectedIssuer === "string" ? opts.expectedIssuer : null;
|
|
183
|
+
var auditOn = opts.audit !== false;
|
|
184
|
+
|
|
185
|
+
if (!envelope || typeof envelope !== "object") {
|
|
186
|
+
return { valid: false, claims: null, reason: "envelope-not-object" };
|
|
187
|
+
}
|
|
188
|
+
if (!envelope.card || !envelope.signature ||
|
|
189
|
+
typeof envelope.signedAt !== "number" || typeof envelope.expiresAt !== "number") {
|
|
190
|
+
return { valid: false, claims: null, reason: "envelope-shape" };
|
|
191
|
+
}
|
|
192
|
+
try { _validateCardShape(envelope.card, errorClass); }
|
|
193
|
+
catch (e) {
|
|
194
|
+
return { valid: false, claims: null, reason: "card-shape:" + e.code };
|
|
195
|
+
}
|
|
196
|
+
if (expectedIssuer && envelope.card.issuer !== expectedIssuer) {
|
|
197
|
+
if (auditOn) {
|
|
198
|
+
audit.safeEmit({
|
|
199
|
+
action: "a2a.card_rejected",
|
|
200
|
+
outcome: "denied",
|
|
201
|
+
reason: "issuer-mismatch",
|
|
202
|
+
metadata: { expected: expectedIssuer, got: envelope.card.issuer },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return { valid: false, claims: null, reason: "issuer-mismatch" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var now = Date.now();
|
|
209
|
+
if (envelope.expiresAt + clockSkewMs < now) {
|
|
210
|
+
if (auditOn) {
|
|
211
|
+
audit.safeEmit({
|
|
212
|
+
action: "a2a.card_rejected",
|
|
213
|
+
outcome: "denied",
|
|
214
|
+
reason: "expired",
|
|
215
|
+
metadata: { expiresAt: envelope.expiresAt, now: now },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return { valid: false, claims: null, reason: "expired" };
|
|
219
|
+
}
|
|
220
|
+
if (envelope.signedAt - clockSkewMs > now) {
|
|
221
|
+
return { valid: false, claims: null, reason: "future-signed" };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
var canonical = canonicalize({
|
|
225
|
+
card: envelope.card,
|
|
226
|
+
signedAt: envelope.signedAt,
|
|
227
|
+
expiresAt: envelope.expiresAt,
|
|
228
|
+
});
|
|
229
|
+
if (Buffer.byteLength(canonical, "utf8") > maxBytes) {
|
|
230
|
+
return { valid: false, claims: null, reason: "card-too-large" };
|
|
231
|
+
}
|
|
232
|
+
var digest = crypto.shake256
|
|
233
|
+
? crypto.shake256(Buffer.from(canonical, "utf8"), SHAKE256_BYTES)
|
|
234
|
+
: null;
|
|
235
|
+
var dataToVerify = digest ? digest : Buffer.from(canonical, "utf8");
|
|
236
|
+
var sigBuf;
|
|
237
|
+
try { sigBuf = Buffer.from(envelope.signature, "base64"); }
|
|
238
|
+
catch (_e) {
|
|
239
|
+
return { valid: false, claims: null, reason: "signature-base64-bad" };
|
|
240
|
+
}
|
|
241
|
+
var ok = crypto.verify(dataToVerify, sigBuf, publicKeyPem);
|
|
242
|
+
if (!ok) {
|
|
243
|
+
if (auditOn) {
|
|
244
|
+
audit.safeEmit({
|
|
245
|
+
action: "a2a.card_rejected",
|
|
246
|
+
outcome: "denied",
|
|
247
|
+
reason: "signature-mismatch",
|
|
248
|
+
metadata: { issuer: envelope.card.issuer, agentId: envelope.card.agentId },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return { valid: false, claims: null, reason: "signature-mismatch" };
|
|
252
|
+
}
|
|
253
|
+
if (auditOn) {
|
|
254
|
+
audit.safeEmit({
|
|
255
|
+
action: "a2a.card_verified",
|
|
256
|
+
outcome: "success",
|
|
257
|
+
metadata: { issuer: envelope.card.issuer, agentId: envelope.card.agentId },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
valid: true,
|
|
262
|
+
claims: envelope.card,
|
|
263
|
+
reason: null,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
signCard: signCard,
|
|
269
|
+
verifyCard: verifyCard,
|
|
270
|
+
canonicalize: canonicalize,
|
|
271
|
+
createCard: createCard,
|
|
272
|
+
};
|
package/lib/ai-input.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AI input classifier for prompt-injection detection on operator
|
|
4
|
+
* input flowing into LLM prompts. OWASP LLM01:2025 + NIST COSAIS RFI.
|
|
5
|
+
*
|
|
6
|
+
* Public API:
|
|
7
|
+
* aiInput.classify(input, opts) -> { verdict, signals, features, confidence }
|
|
8
|
+
* aiInput.refuseIfMalicious(input, opts) -> result | throws
|
|
9
|
+
*
|
|
10
|
+
* Severity 3 = malicious-by-default; 2 = suspicious. Verdict is
|
|
11
|
+
* "malicious" with any severity-3 hit, "suspicious" with 2+ severity-2
|
|
12
|
+
* hits, otherwise "clean".
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var C = require("./constants");
|
|
16
|
+
var nb = require("./numeric-bounds");
|
|
17
|
+
var audit = require("./audit");
|
|
18
|
+
var { AiInputError } = require("./framework-error");
|
|
19
|
+
|
|
20
|
+
var SAMPLE_TRUNC = 80; // allow:raw-byte-literal — sample truncation length, not bytes
|
|
21
|
+
var CONFIDENCE_BASE = 60; // allow:raw-byte-literal — confidence percentage base / allow:raw-time-literal — not seconds
|
|
22
|
+
|
|
23
|
+
var PATTERNS = [
|
|
24
|
+
{ id: "ignore-prior-instructions", severity: 3, re:
|
|
25
|
+
/\b(?:ignore|disregard|forget|bypass|override|skip|drop)\b[\s\S]{0,40}\b(?:prior|previous|above|all|earlier|prev|original|system|instructions?|prompt|context|rules?|directives?|guidelines?)\b/i },
|
|
26
|
+
{ id: "act-as-different-system", severity: 3, re:
|
|
27
|
+
/\byou\s+(?:are|will\s+be|must\s+be)\s+(?:now|from\s+now\s+on)?\s*(?:a|an)\s+\w{2,40}/i },
|
|
28
|
+
{ id: "jailbreak-persona", severity: 3, re:
|
|
29
|
+
/\b(?:DAN|do\s+anything\s+now|developer\s+mode|sudo\s+mode|jailbroken|unfiltered|uncensored|unrestricted)\b/i },
|
|
30
|
+
{ id: "role-reset-marker", severity: 3, re:
|
|
31
|
+
/<\s*\/?\s*(?:system|user|assistant|sys|im_(?:start|end)|\|im_(?:start|end)\|)\s*>/i },
|
|
32
|
+
{ id: "openai-system-tag", severity: 3, re:
|
|
33
|
+
/\b(?:<\|im_start\|>|<\|im_end\|>|\[INST\]|\[\/INST\]|<\|user\|>|<\|assistant\|>|<\|system\|>)\b/ },
|
|
34
|
+
{ id: "tool-call-injection", severity: 3, re:
|
|
35
|
+
/\b(?:tool|function|action)\s*[:=]\s*["']?(?:exec|eval|read_file|exfil|leak|extract)\b/i },
|
|
36
|
+
{ id: "exfil-callback", severity: 3, re:
|
|
37
|
+
/\b(?:send|post|fetch|exfil|leak|paste|forward)\b[\s\S]{0,40}(?:secret|key|token|password|cred|env|\.ssh|private)/i },
|
|
38
|
+
{ id: "base64-marker-around-instructions", severity: 2, re:
|
|
39
|
+
/(?:[A-Za-z0-9+/]{40,}={0,2})\s+(?:means|decodes?\s+to|=)/i }, // allow:raw-byte-literal — regex repetition floor, not bytes
|
|
40
|
+
{ id: "rot13-shape", severity: 2, re:
|
|
41
|
+
/\b(?:rot13|rotcipher|cipher|caesar)\s*[:=]\s*[a-zA-Z]{20,}/i },
|
|
42
|
+
{ id: "markdown-injection", severity: 2, re:
|
|
43
|
+
/!\[[^\]]{0,40}\]\((?:javascript:|data:|file:)/i },
|
|
44
|
+
{ id: "html-script-shape", severity: 2, re:
|
|
45
|
+
/<script[\s>]|on\w+\s*=\s*["'][^"']*\b(?:fetch|xhr|eval|location)\b/i },
|
|
46
|
+
{ id: "stop-helping", severity: 2, re:
|
|
47
|
+
/\b(?:stop|cease|quit)\s+(?:helping|assisting|following)\b/i },
|
|
48
|
+
{ id: "now-instead", severity: 2, re:
|
|
49
|
+
/\b(?:instead|rather|now\s+do|new\s+task)\b[\s\S]{0,40}\b(?:of|than)\b/i },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function _featuresOf(input) {
|
|
53
|
+
var bidi = 0, zw = 0, ctrl = 0;
|
|
54
|
+
for (var i = 0; i < input.length; i += 1) {
|
|
55
|
+
var cp = input.charCodeAt(i);
|
|
56
|
+
if ((cp >= 0x202a && cp <= 0x202e) || (cp >= 0x2066 && cp <= 0x2069)) bidi++;
|
|
57
|
+
else if (cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0xfeff || cp === 0x2060) zw++;
|
|
58
|
+
else if (cp < 0x20 && cp !== 0x09 && cp !== 0x0a && cp !== 0x0d) ctrl++;
|
|
59
|
+
else if (cp === 0x7f) ctrl++;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
length: input.length,
|
|
63
|
+
lines: input.split("\n").length,
|
|
64
|
+
bidiCount: bidi,
|
|
65
|
+
zwCount: zw,
|
|
66
|
+
ctrlCount: ctrl,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function classify(input, opts) {
|
|
71
|
+
opts = opts || {};
|
|
72
|
+
var errorClass = opts.errorClass || AiInputError;
|
|
73
|
+
nb.requirePositiveFiniteIntIfPresent(opts.maxBytes, "aiInput.classify: opts.maxBytes", errorClass, "BAD_MAX_BYTES");
|
|
74
|
+
var maxBytes = opts.maxBytes || C.BYTES.kib(64);
|
|
75
|
+
var auditOn = opts.audit !== false;
|
|
76
|
+
|
|
77
|
+
if (typeof input !== "string") {
|
|
78
|
+
throw errorClass.factory("BAD_INPUT",
|
|
79
|
+
"aiInput.classify: input must be a string");
|
|
80
|
+
}
|
|
81
|
+
var byteLen = Buffer.byteLength(input, "utf8");
|
|
82
|
+
if (byteLen > maxBytes) {
|
|
83
|
+
throw errorClass.factory("INPUT_TOO_LARGE",
|
|
84
|
+
"aiInput.classify: input exceeds " + maxBytes + " bytes (got " + byteLen + ")");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var features = _featuresOf(input);
|
|
88
|
+
var signals = [];
|
|
89
|
+
|
|
90
|
+
for (var i = 0; i < PATTERNS.length; i += 1) {
|
|
91
|
+
var p = PATTERNS[i];
|
|
92
|
+
var m = p.re.exec(input);
|
|
93
|
+
if (m) {
|
|
94
|
+
signals.push({
|
|
95
|
+
id: p.id,
|
|
96
|
+
severity: p.severity,
|
|
97
|
+
sample: m[0].slice(0, SAMPLE_TRUNC),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (features.bidiCount > 0) signals.push({ id: "bidi-controls", severity: 2, sample: null });
|
|
103
|
+
if (features.zwCount > 5) signals.push({ id: "zero-width-density", severity: 2, sample: null });
|
|
104
|
+
if (features.ctrlCount > 0) signals.push({ id: "control-chars", severity: 2, sample: null });
|
|
105
|
+
|
|
106
|
+
var sev3 = 0, sev2 = 0;
|
|
107
|
+
for (var j = 0; j < signals.length; j += 1) {
|
|
108
|
+
if (signals[j].severity === 3) sev3++;
|
|
109
|
+
else if (signals[j].severity === 2) sev2++;
|
|
110
|
+
}
|
|
111
|
+
var verdict = sev3 > 0 ? "malicious" : (sev2 >= 2 ? "suspicious" : "clean");
|
|
112
|
+
var confidence = sev3 === 0 && sev2 === 0 ? 0 : Math.min(100, CONFIDENCE_BASE + sev3 * 15 + sev2 * 5); // allow:raw-byte-literal — confidence ceiling 100, not bytes/seconds
|
|
113
|
+
|
|
114
|
+
if (auditOn && verdict !== "clean") {
|
|
115
|
+
audit.safeEmit({
|
|
116
|
+
action: "aiinput.classify",
|
|
117
|
+
outcome: verdict === "malicious" ? "denied" : "warning",
|
|
118
|
+
metadata: {
|
|
119
|
+
verdict: verdict,
|
|
120
|
+
signalIds: signals.map(function (s) { return s.id; }),
|
|
121
|
+
confidence: confidence,
|
|
122
|
+
length: features.length,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
verdict: verdict,
|
|
129
|
+
signals: signals,
|
|
130
|
+
features: features,
|
|
131
|
+
confidence: confidence,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function refuseIfMalicious(input, opts) {
|
|
136
|
+
opts = opts || {};
|
|
137
|
+
var errorClass = opts.errorClass || AiInputError;
|
|
138
|
+
var result = classify(input, opts);
|
|
139
|
+
if (result.verdict === "malicious") {
|
|
140
|
+
throw errorClass.factory("MALICIOUS_INPUT",
|
|
141
|
+
"aiInput: input flagged as malicious (signals: " +
|
|
142
|
+
result.signals.map(function (s) { return s.id; }).join(", ") + ")");
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
classify: classify,
|
|
149
|
+
refuseIfMalicious: refuseIfMalicious,
|
|
150
|
+
PATTERN_IDS: PATTERNS.map(function (p) { return p.id; }),
|
|
151
|
+
};
|
package/lib/audit.js
CHANGED
|
@@ -232,6 +232,12 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
232
232
|
// tick/task events use "system.scheduler.*")
|
|
233
233
|
"seeders", // b.seeders
|
|
234
234
|
"webhook", // b.webhook
|
|
235
|
+
"sse", // b.sse (sse.channel_opened / closed / injection_refused)
|
|
236
|
+
"mcp", // b.mcp.serverGuard (mcp.auth.* / mcp.tool.* / mcp.resource.* / mcp.register.* / mcp.envelope.*)
|
|
237
|
+
"graphqlfederation", // b.graphqlFederation.guardSdl (sdl-refused / sdl-allowed)
|
|
238
|
+
"aiinput", // b.ai.input.classify (aiInput.classify)
|
|
239
|
+
"a2a", // b.a2a (a2a.card_signed / verified / rejected)
|
|
240
|
+
"darkpatterns", // b.darkPatterns (darkPatterns.attest / cancel-blocked)
|
|
235
241
|
];
|
|
236
242
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
237
243
|
|