Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2b18edf
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
6a77dd1
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
3e5db35
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
d034a67
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
912ed21
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
6f21060
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
309fd84
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
b876219
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
04d8d66
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
3d3ee3e
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
eb9357a
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
f6aa29c
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
9faa419
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
f86bc9c
Add opt-in AURA trust-check adapter (integrations/aura) (remove integ…
luisllaver May 20, 2026
eae1025
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
16d5955
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
c22af6b
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
8de1fb3
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
b7e1f2d
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
16e2586
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 20, 2026
7832206
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
8164176
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
8988523
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
823f298
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
8dc40e5
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
dcb5ee3
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
66e276e
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
751df52
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
8ce8cce
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
f6b1a32
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
e0a4a23
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
bce2131
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions integrations/aura/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# AURA trust-check adapter (TypeScript)

Opt-in, **read-only** counterparty reputation for PayBot. One HTTPS GET
answers *"can I trust this agent before I settle a payment to it?"* — a
natural `beforeSettle` gate in front of `client.pay()`.

- **Zero dependencies** — global `fetch` (Node 18+), no new packages.
- **Read-only** — the only network call is `GET /check?did=...`. No auth, no key.
- **No coupling** — does not sign, hold keys, move USDC, or touch the wallet.
PayBot's `pay()` flow is untouched; this sits *in front* of it.
- **Off by default** — nothing runs until you call it.

## Enable (opt-in)

Gate the settlement at the call site. No global hooks, no monkey-patching:

```ts
import { PayBotClient, type PaymentRequest } from 'paybot-sdk';
import { beforeSettle, AuraUntrusted } from './integrations/aura';

const client = new PayBotClient(config); // your existing PayBot config

async function payChecked(counterpartyDid: string, req: PaymentRequest) {
try {
await beforeSettle(counterpartyDid); // rejects high_risk + unknown
} catch (e) {
if (e instanceof AuraUntrusted) {
console.warn('blocked:', e.message); // your policy decides
return;
}
throw e;
}
return client.pay(req); // existing flow, unchanged
}
```

`payTo` in a `PaymentRequest` is a wallet address; the AURA DID is the
counterparty's portable identity, supplied by your own mapping. The gate keys
on that DID — it composes cleanly with PayBot's existing `TRUST_VIOLATION`
error model as a *pre-flight* reputation axis.
Comment on lines +17 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if package.json has subpath exports configured for integrations/aura
if [ -f package.json ]; then
  echo "=== Checking package.json exports configuration ==="
  jq -r '.exports // "No exports field"' package.json
else
  echo "No package.json found at root"
fi

# Check for any existing integration imports in the codebase
echo -e "\n=== Searching for existing integration import patterns ==="
rg -n --type=ts "from ['\"].*integrations/" -C 2 || echo "No existing integration imports found"

Repository: RBKunnela/paybot-sdk

Length of output: 570


Fix import path: package.json lacks subpath exports for the documented example.

The README example (line 19) shows import { beforeSettle, AuraUntrusted } from './integrations/aura', but package.json currently exports only the root entry point. SDK consumers cannot use the relative import path as documented.

Either:

  1. Add subpath export to package.json: "./integrations/aura": { "import": "./dist/integrations/aura/index.js", "types": "./dist/integrations/aura/index.d.ts" }
  2. Or re-export from the main index.ts so users import from 'paybot-sdk'
  3. Or update the example to match the actual consumable import path

Confirm which pattern aligns with your SDK's public API design.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/aura/README.md` around lines 17 - 40, The README uses a relative
import for beforeSettle and AuraUntrusted from './integrations/aura' which isn't
exposed by package.json, so consumers cannot import those symbols; fix by either
adding a subpath export for "./integrations/aura" in package.json pointing to
dist/integrations/aura/index.{js,d.ts}, or re-export beforeSettle and
AuraUntrusted from the package's main entry (index.ts) so users can import them
from 'paybot-sdk', or update the README example to import from the actual public
path your package exposes; ensure the referenced symbols beforeSettle and
AuraUntrusted are exported where you add the subpath or re-export.


Prefer to read the verdict instead of throwing?

```ts
import { auraVerdict } from './integrations/aura';

const v = await auraVerdict(counterpartyDid);
console.log(v.verdict); // trusted | caution | high_risk | new | unknown
console.log(v.reason, v.score, v.ok);

// v.dimensions tells you *which* axis is weak, not just the aggregate:
if ((v.dimensions?.financial_integrity ?? 1) < 0.4) requireManualReview(); // placeholder for your own policy
```

> `v.ok` reflects the *verdict class* (true for `trusted`/`caution`), not the
> outcome of `beforeSettle` — the gate's default `allow` also lets `new`
> through. Use the gate's return/throw for the decision, `v.ok` for display.

## Verdicts

| verdict | meaning | `ok` |
|---|---|---|
| `trusted` | strong on-chain track record (composite ≥ 0.70) | ✅ |
| `caution` | mixed history (0.40–0.70) | ✅ |
| `high_risk` | poor track record (< 0.40) | ❌ |
| `new` | registered identity, no interactions yet | ❌ |
| `unknown` | no track record — or AURA was unreachable | ❌ |

## Policy knobs

```ts
await beforeSettle(did, { allow: ['trusted', 'caution'] }); // reject new too
await beforeSettle(did, { failOpen: true }); // unreachable => pass
await beforeSettle(did, { baseUrl: 'https://my-mirror', timeoutMs: 5000 });
```

`requireTrust` is an alias of `beforeSettle` for non-payment call sites.

## Failure behavior

`auraVerdict()` **never rejects on a network error** — it resolves to an
`unknown` verdict with the reason set. The gate then decides:

- **default (`failOpen: false`)** — `unknown` rejected → unreachable AURA
blocks the settlement. *Fail-closed.*
- **`failOpen: true`** — `unknown` from an unreachable endpoint passes, so AURA
can never take your payment flow down. *Fail-open.*

The signal is **purely additive**: remove the adapter or take AURA down, and
PayBot behaves exactly as before.

## Tests

Offline — every call replays a recorded `/check` body via the `fetchImpl`
injection seam:

```bash
npx vitest run --config integrations/aura/vitest.config.ts
```

17 tests: all five verdict classes, the gate's allow-list + `failOpen`, the
unreachable path, and input validation.

## Boundary & threats

See [THREAT_MODEL.md](./THREAT_MODEL.md) — what the verdict does and does not
prove, and the failure modes a verifier should account for.

## Carry the AURA badge

Show your live trust verdict in your own README — it updates automatically and
links back to your AURA profile:

```markdown
[![AURA Verified](https://agent.auraopenprotocol.org/badge?did=YOUR_DID)](https://agent.auraopenprotocol.org/check?did=YOUR_DID)
```

A shields-style badge colored by verdict (`trusted` green, `caution` amber,
`high_risk` red, `new` blue, `unknown` grey). Add `&score=1` to show the
composite score. No DID yet? The bare badge is a generic mark:

```markdown
[![Powered by AURA](https://agent.auraopenprotocol.org/badge)](https://auraopenprotocol.org)
```

## What's behind the verdict

[AURA Open Protocol](https://auraopenprotocol.org) — W3C DID identity plus 8
on-chain reputation dimensions on Base L2. Docs: https://dev.auraopenprotocol.org
52 changes: 52 additions & 0 deletions integrations/aura/THREAT_MODEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Threat model — AURA trust-check adapter

A short, honest boundary statement. The verdict is **one backward-looking
signal**, not a security guarantee. Read this before treating `trusted` as a
green light for an irreversible settlement.

## What the verdict proves

- The DID has (or lacks) an on-chain interaction history on AURA, summarized
into a composite score and per-dimension breakdown.
- It is **backward-looking**: a statement about past recorded behavior, not a
prediction or an authorization for the *current* payment.

## What it explicitly does NOT prove

- **Not payment-safety.** A `trusted` agent can still be the wrong recipient or
request a bad amount. Keep this separate from PayBot's own checks
(`TRUST_VIOLATION`, limits) so the settlement decision stays auditable.
- **Not execution quality.** It says nothing about whether *this* settle succeeds.
- **Not identity proof of the live caller.** It checks a DID's reputation, not
that the entity you're paying controls that DID (see "Spoofed DID").

## Failure modes a caller must account for

| # | Threat | Mitigation in this adapter | Residual risk owned by caller |
|---|---|---|---|
| 1 | **Endpoint unreachable / timeout** | Resolves to `unknown` (never rejects). Gate is fail-closed by default; `AbortController` enforces `timeoutMs`. | Choose `failOpen` deliberately; pick a sane `timeoutMs`. |
| 2 | **Spoofed DID** — recipient claims a DID it doesn't control | Out of scope: adapter checks reputation, not control of the key. | Bind the DID to the `payTo` address / verify control before trusting. |
| 3 | **Stale verdict** — score lags very recent bad behavior | Each call is live (no caching here). | If you cache, bound the TTL; don't reuse a verdict across sessions. |
| 4 | **Endpoint MITM / response tampering** | HTTPS to a pinned host (`agent.auraopenprotocol.org`). Verdict strings validated against a fixed allow-list; unknown values collapse to `unknown`. | Don't point `baseUrl` at an untrusted mirror. |
| 5 | **Score gaming / Sybil** — cheap DIDs farming a `trusted` score | Inherited from AURA's on-chain cost + dispute dimension; not solvable in the adapter. | Weight `dimensions` (e.g. require non-trivial history) for high-value settlements rather than trusting the aggregate alone. |
| 6 | **Over-trust** — using the verdict as sole gate for an irreversible payment | `new`/`unknown` rejected by default; `dimensions` exposed. | Combine with PayBot limits + escrow + manual review for high-value flows. |

## Data handled

- **Sent:** only the counterparty DID, as a query parameter to `/check`. No
PII, no payment payload, no secrets, no keys.
- **Stored:** nothing. The adapter is stateless.
- **Received:** the public `/check` JSON body. Surfaced verbatim on `.raw`.

## Trust boundary summary

```
PayBot host --(DID only, HTTPS GET)--> AURA /check --> verdict
| |
| PayBot limits / TRUST_VIOLATION (separate, yours) |
v v
settle decision (auditable, your code)
```

The adapter sits on the read-only reputation edge. Signing, USDC movement, and
the final settle decision stay in PayBot, where they can be audited.
147 changes: 147 additions & 0 deletions integrations/aura/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Offline tests for the AURA trust-check adapter (vitest).
*
* No network: every call replays a recorded /check body via the `fetchImpl`
* injection seam. Run with `vitest run`.
*
* Coverage: one assertion per verdict class, the beforeSettle gate
* (allow-list pass/reject, custom allow, failOpen), the network-failure path
* (fail-closed by default), and input validation.
*/

import { describe, it, expect } from 'vitest';
import {
auraVerdict,
beforeSettle,
AuraUntrusted,
type FetchLike,
} from './adapter.js';

// ── recorded /check bodies, one per verdict class ────────────────────────────
const RECORDED: Record<string, Record<string, unknown>> = {
'did:aura:trusted-bot': {
did: 'did:aura:trusted-bot', verdict: 'trusted',
reason: 'strong on-chain track record (composite 0.86)',
has_history: true, score: 0.86, interactions: 142,
dimensions: { financial_integrity: 0.95, task_completion: 0.92 },
},
'did:aura:caution-bot': {
did: 'did:aura:caution-bot', verdict: 'caution',
reason: 'mixed history (composite 0.55)', has_history: true, score: 0.55,
},
'did:aura:risky-bot': {
did: 'did:aura:risky-bot', verdict: 'high_risk',
reason: 'poor track record (composite 0.22)', has_history: true, score: 0.22,
dimensions: { financial_integrity: 0.12 },
},
'did:aura:fresh-bot': {
did: 'did:aura:fresh-bot', verdict: 'new',
reason: 'registered identity, no interactions yet', has_history: false, score: null,
},
'did:aura:ghost-bot': {
did: 'did:aura:ghost-bot', verdict: 'unknown',
reason: 'no track record — unverified counterparty', has_history: false, score: null,
},
};

const okFetch: FetchLike = async (url) => {
const did = new URL(url).searchParams.get('did') ?? '';
const body = RECORDED[did] ?? RECORDED['did:aura:ghost-bot'];
return { ok: true, status: 200, json: async () => body };
};

const failFetch: FetchLike = async () => {
throw new Error('connection refused');
};

// ── verdict classes ──────────────────────────────────────────────────────────
describe('verdict classes', () => {
const cases: [string, string, boolean][] = [
['did:aura:trusted-bot', 'trusted', true],
['did:aura:caution-bot', 'caution', true],
['did:aura:risky-bot', 'high_risk', false],
['did:aura:fresh-bot', 'new', false],
['did:aura:ghost-bot', 'unknown', false],
];
it.each(cases)('%s -> %s', async (did, expected, ok) => {
const v = await auraVerdict(did, { fetchImpl: okFetch });
expect(v.verdict).toBe(expected);
expect(v.ok).toBe(ok);
expect(v.did).toBe(did);
expect(v.reason.length).toBeGreaterThan(0);
});

it('exposes dimensions for agents with history', async () => {
const v = await auraVerdict('did:aura:risky-bot', { fetchImpl: okFetch });
expect(v.hasHistory).toBe(true);
expect(v.dimensions?.financial_integrity).toBe(0.12);
});

it('new agent has null score', async () => {
const v = await auraVerdict('did:aura:fresh-bot', { fetchImpl: okFetch });
expect(v.score).toBeNull();
expect(v.hasHistory).toBe(false);
});
});

// ── the beforeSettle gate ─────────────────────────────────────────────────────
describe('beforeSettle gate', () => {
it('allows trusted / caution / new by default', async () => {
expect((await beforeSettle('did:aura:trusted-bot', { fetchImpl: okFetch })).verdict).toBe('trusted');
expect((await beforeSettle('did:aura:caution-bot', { fetchImpl: okFetch })).verdict).toBe('caution');
expect((await beforeSettle('did:aura:fresh-bot', { fetchImpl: okFetch })).verdict).toBe('new');
});

it('rejects high_risk', async () => {
await expect(beforeSettle('did:aura:risky-bot', { fetchImpl: okFetch })).rejects.toBeInstanceOf(AuraUntrusted);
});

it('rejects unknown by default', async () => {
await expect(beforeSettle('did:aura:ghost-bot', { fetchImpl: okFetch })).rejects.toBeInstanceOf(AuraUntrusted);
});

it('strict allow rejects new', async () => {
await expect(
beforeSettle('did:aura:fresh-bot', { allow: ['trusted', 'caution'], fetchImpl: okFetch }),
).rejects.toBeInstanceOf(AuraUntrusted);
});
});

// ── network-failure path ───────────────────────────────────────────────────────
describe('network failure', () => {
it('auraVerdict returns unknown, does not throw', async () => {
const v = await auraVerdict('did:aura:trusted-bot', { fetchImpl: failFetch });
expect(v.verdict).toBe('unknown');
expect(v.reason.toLowerCase()).toContain('unreachable');
});

it('gate is fail-closed by default on unreachable', async () => {
await expect(beforeSettle('did:aura:trusted-bot', { fetchImpl: failFetch })).rejects.toBeInstanceOf(AuraUntrusted);
});

it('gate passes on unreachable when failOpen', async () => {
const v = await beforeSettle('did:aura:trusted-bot', { failOpen: true, fetchImpl: failFetch });
expect(v.verdict).toBe('unknown');
expect(v.reachable).toBe(false);
});

it('failOpen does NOT pass a reachable unknown (ghost DID)', async () => {
// A reachable AURA that returns `unknown` is still rejected even with
// failOpen — failOpen only excuses transport failures.
await expect(
beforeSettle('did:aura:ghost-bot', { failOpen: true, fetchImpl: okFetch }),
).rejects.toBeInstanceOf(AuraUntrusted);
});

it('reachable verdict is marked reachable', async () => {
const v = await auraVerdict('did:aura:ghost-bot', { fetchImpl: okFetch });
expect(v.reachable).toBe(true);
});
});

// ── input validation ────────────────────────────────────────────────────────────
describe('input validation', () => {
it.each(['', 'not-a-did', 'z6Mk-no-prefix'])('rejects bad DID %s', async (bad) => {
await expect(auraVerdict(bad, { fetchImpl: okFetch })).rejects.toThrow();
});
});
Loading