<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harald Roessler</title>
    <description>The latest articles on DEV Community by Harald Roessler (@haralderoessler).</description>
    <link>https://dev.to/haralderoessler</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3906400%2F05175deb-26ad-4159-b23d-5c7f81528819.png</url>
      <title>DEV Community: Harald Roessler</title>
      <link>https://dev.to/haralderoessler</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/haralderoessler"/>
    <language>en</language>
    <item>
      <title>Inside a2a-acl — a drop-in Express library for agent-to-agent authorization</title>
      <dc:creator>Harald Roessler</dc:creator>
      <pubDate>Thu, 30 Apr 2026 15:52:06 +0000</pubDate>
      <link>https://dev.to/haralderoessler/inside-a2a-acl-a-drop-in-express-library-for-agent-to-agent-authorization-1j8i</link>
      <guid>https://dev.to/haralderoessler/inside-a2a-acl-a-drop-in-express-library-for-agent-to-agent-authorization-1j8i</guid>
      <description>&lt;p&gt;A few days ago I &lt;a href="https://ownify.ai/blog/per-tool-acl-for-the-agent-web" rel="noopener noreferrer"&gt;wrote about the per-tool ACL design&lt;/a&gt; that fronts every inbound A2A call to an ownify agent. That post is about the &lt;strong&gt;architecture&lt;/strong&gt;: capabilities instead of trust scores, default-deny, hard-vs-soft enforcement, the order the firewall stages run in.&lt;/p&gt;

&lt;p&gt;This post is about the &lt;strong&gt;library&lt;/strong&gt;. We extracted the policy layer of that gateway into &lt;a href="https://github.com/HaraldeRoessler/a2a-acl" rel="noopener noreferrer"&gt;a2a-acl&lt;/a&gt; — a drop-in Express middleware package that runs in production at ownify.ai and is now available standalone. MIT-licensed, no runtime dependencies, on npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;a2a-acl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building a service that receives agent-to-agent traffic and you want the same authorization shape — without rolling your own AAE verifier, nonce cache, ACL evaluator, trust gate, sanitiser, depth guard, circuit breaker, rate limiter, and audit logger — this is the library. You bring your storage; the library brings the algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight stages
&lt;/h2&gt;

&lt;p&gt;The chain composes the same eight stages we run in production:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Enforces&lt;/th&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verifyAaeMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cryptographic envelope (Ed25519, replay + revocation + audience + expiry)&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aclCheckMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(receiver_slug, caller_did, capability)&lt;/code&gt; is in your ACL store&lt;/td&gt;
&lt;td&gt;403 &lt;code&gt;acl_no_capability_grant&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trustScoreGateMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Caller's trust score clears the matched rule's threshold (or your default)&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sanitiseMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strips prompt-injection markers from the request body&lt;/td&gt;
&lt;td&gt;(forwards, audit-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;depthGuardMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AAE chain hop count ≤ &lt;code&gt;maxHopCount&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;circuitOpenCheckMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upstream peer slug isn't in cooldown&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rateLimitMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-(caller, peer) requests/min + daily token budget&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auditMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fire-and-forget sink for every decision&lt;/td&gt;
&lt;td&gt;(logs only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Order matters and is non-trivial. The trust gate runs &lt;strong&gt;after&lt;/strong&gt; the ACL match because the matched ACL row carries an optional &lt;code&gt;threshold_override&lt;/code&gt; that lets an operator demand a higher score from one specific peer or lower it for a trusted bilateral pair. Earlier in development we ran trust before ACL and &lt;code&gt;threshold_override&lt;/code&gt; was silently dead code — that's the kind of thing baking the chain composition into the library is for. Use &lt;code&gt;firewallChain(config)&lt;/code&gt; and you can't accidentally re-introduce that bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring-your-own-storage
&lt;/h2&gt;

&lt;p&gt;The library doesn't open a database connection, doesn't talk to a key service, doesn't know what your trust algorithm is. Four async callbacks are everything you need to wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;firewallChain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;KeyResolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TrustResolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RevocationChecker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;NonceCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DailyTokenBudget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a2a-acl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Key lookup. Return { public_key_b64url, sig_alg: 'Ed25519' } or null.&lt;/span&gt;
&lt;span class="c1"&gt;//    Throw on transient failures — the library retries by not caching.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyResolver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KeyResolver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Trust score. Return a number 0..1 (or { score }), or null for unknown.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trustResolver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TrustResolver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;did&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;scoreLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;did&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Revocation. Return true if the envelope's jti has been revoked.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revocationChecker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RevocationChecker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revocations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;jti&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 4. ACL match. Return the rule row, or null if no rule grants.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;matchAcl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callerDid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;peer_slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;caller_did&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callerDid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/a2a/:slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/a2a/:slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;firewallChain&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;keyResolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revocationChecker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nonceCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NonceCache&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;trustResolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;rateLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;requestsPerMinute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;tokenBudget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DailyTokenBudget&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tokensPerDay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;matchAcl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaultThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxHopCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedAud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a2a-ingress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/a2a/:slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/a2a/:slug/message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The chain accepted. req.firewall.callerDid, .aae, .aclRule, .trustScore&lt;/span&gt;
  &lt;span class="c1"&gt;// are all populated. Forward to your agent runtime.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole shape. A complete, runnable example is in &lt;a href="https://github.com/HaraldeRoessler/a2a-acl/blob/main/examples/express-server.js" rel="noopener noreferrer"&gt;&lt;code&gt;examples/express-server.js&lt;/code&gt;&lt;/a&gt; in the repo (&lt;code&gt;npm run example&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Strict defaults, on purpose
&lt;/h2&gt;

&lt;p&gt;A library that ships permissive defaults gets cargo-culted into permissive deployments. So the defaults are the strictest setting that doesn't break the common case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audience required&lt;/strong&gt; by default (&lt;code&gt;expectedAud: 'a2a-ingress'&lt;/code&gt;). Set it to &lt;code&gt;null&lt;/code&gt; to opt out — explicit, not silent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry required&lt;/strong&gt; on every envelope. No "long-lived tokens" path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximum envelope lifetime: 5 minutes&lt;/strong&gt;. A signer who issues with a longer &lt;code&gt;exp&lt;/code&gt; gets rejected before signature verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ed25519 only.&lt;/strong&gt; No &lt;code&gt;none&lt;/code&gt;, no RS256, no algorithm-confusion class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-closed nonce cache.&lt;/strong&gt; If the in-memory replay cache is full, new envelopes are rejected rather than admitted with no replay protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded everything.&lt;/strong&gt; Rate-limit buckets, token-budget buckets, resolver caches, in-flight maps, circuit-breaker peers — all capped. An attacker flooding with unique caller DIDs can't OOM the gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opaque error bodies.&lt;/strong&gt; 401/403/503 don't echo &lt;code&gt;err.message&lt;/code&gt;, don't leak the trust score, don't tell the attacker how close they are to passing. Server logs the detail; client gets the bare result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query string stripped from audit&lt;/strong&gt; by default. Tokens and session IDs in URLs end up in audit logs only if you opt in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the kinds of things that would feel pedantic in a hand-rolled gateway — and that absolutely will get cut during "let's just get it working" pressure. Putting them in the library means the strict version is the cheap version.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we hardened it
&lt;/h2&gt;

&lt;p&gt;We shipped 0.1.0 internal-only, then ran four external review rounds before publishing 0.1.4 to npm. Total of &lt;strong&gt;38 distinct findings&lt;/strong&gt;, all fixed. Highlights from each round:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0.1.1&lt;/strong&gt; (internal hardening): aud required, exp required, fail-closed nonce, type checks at every boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1.2&lt;/strong&gt; (review #1, 10 findings): &lt;code&gt;err.message&lt;/code&gt; leakage in HTTP bodies, unbounded buckets across rate-limit/token-budget/resolvers, &lt;code&gt;__proto__&lt;/code&gt; in JSON canonicalisation, capability segment validation, query string in audit logs, sink error log level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1.3&lt;/strong&gt; (review #2, 14 findings): cross-peer replay (envelope captured for peer A replayed against peer B — fixed by &lt;code&gt;getExpectedSub&lt;/code&gt; callback), NaN bypasses across the trust gate and depth guard, content-length poisoning in the token estimator, length caps on every signed-payload string, public-path-under-mount confusion (a path ending in &lt;code&gt;/agent-card&lt;/code&gt; could bypass auth), score/threshold leak in 403 body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1.4&lt;/strong&gt; (review #3, 8 findings): &lt;code&gt;sig&lt;/code&gt; and &lt;code&gt;perm&lt;/code&gt; element caps on the signed payload, &lt;code&gt;coversOp&lt;/code&gt; defensive against malformed &lt;code&gt;perm&lt;/code&gt; arrays, &lt;code&gt;isPublic&lt;/code&gt; exact-match instead of &lt;code&gt;endsWith&lt;/code&gt; (which had the same shape as the public-path bug from 0.1.3), prototype-key skip in sanitiser, distinct revocation-failure reason for operator observability, numeric parameter validation at construct time so misconfigured thresholds fail loudly rather than silently bypass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detail in &lt;a href="https://github.com/HaraldeRoessler/a2a-acl/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;. After each round we ran the full test suite (90 tests, all passing) plus GitHub's CodeQL — currently zero open alerts, three historic alerts dismissed as false positives in test scaffolding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's deliberately not in this library
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; Your DB schema, your tables, your queries. We're not shipping a Postgres adapter or a Redis adapter. The four callbacks above are the contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A specific trust algorithm.&lt;/strong&gt; ownify uses MolTrust (DIDs anchored on Base L2 with a public reputation feed). The library doesn't care — return a number 0..1 from your &lt;code&gt;TrustResolver&lt;/code&gt;, the gate compares it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signing side.&lt;/strong&gt; This library is for the &lt;em&gt;receiving&lt;/em&gt; gateway. Outbound envelope signing belongs on the caller's control plane, with their key material. We export &lt;code&gt;signablePayload(env)&lt;/code&gt; and &lt;code&gt;SIGNED_FIELDS&lt;/code&gt; so a signer in a different language can produce bit-identical canonical bytes — but the signing logic itself is yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework adapters beyond Express.&lt;/strong&gt; Fastify and Hono are reasonable next bumps. The core (resolvers, sanitiser, rate-limit, AAE parse/verify, capability schema) is framework-agnostic — the middleware layer is the only Express-specific code, and it's small. v0.2 territory if there's demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-replica state.&lt;/strong&gt; The nonce cache and rate-limit buckets are in-process. For HA deployments running multiple replicas, you'd want a Redis-backed implementation of &lt;code&gt;NonceCache&lt;/code&gt; and &lt;code&gt;RateLimiter&lt;/code&gt; — same interface, different storage. v0.2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/HaraldeRoessler/a2a-acl" rel="noopener noreferrer"&gt;HaraldeRoessler/a2a-acl&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/a2a-acl" rel="noopener noreferrer"&gt;&lt;code&gt;npm install a2a-acl&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture write-up&lt;/strong&gt;: &lt;a href="https://ownify.ai/blog/per-tool-acl-for-the-agent-web" rel="noopener noreferrer"&gt;Per-tool ACL for the agent web&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working example&lt;/strong&gt;: &lt;a href="https://github.com/HaraldeRoessler/a2a-acl/blob/main/examples/express-server.js" rel="noopener noreferrer"&gt;&lt;code&gt;examples/express-server.js&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find a bug, open an issue. If you've integrated it and want something adapted to your stack — Fastify adapter, Redis-backed NonceCache, a different signature scheme — that's the kind of input that drives the v0.2 roadmap.&lt;/p&gt;

&lt;p&gt;The agent web only works if its identity-and-access layer is something operators trust enough to build on. We're trying to make that layer something you can &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
