-
Notifications
You must be signed in to change notification settings - Fork 4
Add opt-in AURA trust-check adapter (integrations/aura) #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
luisllaver
wants to merge
32
commits into
RBKunnela:main
Choose a base branch
from
luisllaver:aura-trust-check-adapter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 6a77dd1
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 3e5db35
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver d034a67
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 912ed21
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 6f21060
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 309fd84
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver b876219
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 04d8d66
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 3d3ee3e
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver eb9357a
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver f6aa29c
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 9faa419
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver f86bc9c
Add opt-in AURA trust-check adapter (integrations/aura) (remove integ…
luisllaver eae1025
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 16d5955
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver c22af6b
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 8de1fb3
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver b7e1f2d
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 16e2586
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 7832206
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 8164176
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 8988523
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 823f298
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 8dc40e5
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver dcb5ee3
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 66e276e
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 751df52
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver 8ce8cce
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver f6b1a32
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver e0a4a23
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver bce2131
Add opt-in AURA trust-check adapter (integrations/aura)
luisllaver File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| 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 | ||
| [](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 | ||
| [](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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
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:
"./integrations/aura": { "import": "./dist/integrations/aura/index.js", "types": "./dist/integrations/aura/index.d.ts" }'paybot-sdk'Confirm which pattern aligns with your SDK's public API design.
🤖 Prompt for AI Agents