triage-ows 1.0.0
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/README.md +786 -0
- package/bin/triage-ows.js +218 -0
- package/bin/triage-policy.js +66 -0
- package/bin/triage-server.js +4 -0
- package/dashboard-dist/assets/index-Dnhi_dJQ.css +2 -0
- package/dashboard-dist/assets/index-g_2MwC-o.js +9 -0
- package/dashboard-dist/favicon.svg +7 -0
- package/dashboard-dist/index.html +16 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -0
- package/dist/emitter.d.ts +7 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +41 -0
- package/dist/emitter.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/scoring/behavior.d.ts +3 -0
- package/dist/scoring/behavior.d.ts.map +1 -0
- package/dist/scoring/behavior.js +13 -0
- package/dist/scoring/behavior.js.map +1 -0
- package/dist/scoring/compliance.d.ts +3 -0
- package/dist/scoring/compliance.d.ts.map +1 -0
- package/dist/scoring/compliance.js +11 -0
- package/dist/scoring/compliance.js.map +1 -0
- package/dist/scoring/identity.d.ts +3 -0
- package/dist/scoring/identity.d.ts.map +1 -0
- package/dist/scoring/identity.js +16 -0
- package/dist/scoring/identity.js.map +1 -0
- package/dist/scoring/index.d.ts +10 -0
- package/dist/scoring/index.d.ts.map +1 -0
- package/dist/scoring/index.js +35 -0
- package/dist/scoring/index.js.map +1 -0
- package/dist/scoring/limits.d.ts +7 -0
- package/dist/scoring/limits.d.ts.map +1 -0
- package/dist/scoring/limits.js +9 -0
- package/dist/scoring/limits.js.map +1 -0
- package/dist/scoring/network.d.ts +3 -0
- package/dist/scoring/network.d.ts.map +1 -0
- package/dist/scoring/network.js +16 -0
- package/dist/scoring/network.js.map +1 -0
- package/dist/scoring/onchain.d.ts +4 -0
- package/dist/scoring/onchain.d.ts.map +1 -0
- package/dist/scoring/onchain.js +35 -0
- package/dist/scoring/onchain.js.map +1 -0
- package/dist/scoring/risk.d.ts +3 -0
- package/dist/scoring/risk.d.ts.map +1 -0
- package/dist/scoring/risk.js +22 -0
- package/dist/scoring/risk.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +405 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +13 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +177 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/webbotauth.d.ts +38 -0
- package/dist/webbotauth.d.ts.map +1 -0
- package/dist/webbotauth.js +120 -0
- package/dist/webbotauth.js.map +1 -0
- package/dist/xmtp.d.ts +6 -0
- package/dist/xmtp.d.ts.map +1 -0
- package/dist/xmtp.js +161 -0
- package/dist/xmtp.js.map +1 -0
- package/package.json +58 -0
- package/policy-template.json +14 -0
- package/src/config.ts +86 -0
- package/src/emitter.ts +40 -0
- package/src/index.ts +18 -0
- package/src/scoring/behavior.ts +15 -0
- package/src/scoring/compliance.ts +12 -0
- package/src/scoring/identity.ts +12 -0
- package/src/scoring/index.ts +31 -0
- package/src/scoring/limits.ts +10 -0
- package/src/scoring/network.ts +18 -0
- package/src/scoring/onchain.ts +44 -0
- package/src/scoring/risk.ts +25 -0
- package/src/server.ts +410 -0
- package/src/store.ts +197 -0
- package/src/types.ts +137 -0
- package/src/webbotauth.ts +128 -0
- package/src/xmtp.ts +188 -0
- package/triage.config.example.json +22 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { AgentProfile, TrustBreakdown, PolicyContext } from './types'
|
|
2
|
+
import { computeTrustScore } from './scoring/index'
|
|
3
|
+
import { getSpendingLimits } from './scoring/limits'
|
|
4
|
+
import { refreshOnChainData } from './scoring/onchain'
|
|
5
|
+
import { getConfig } from './config'
|
|
6
|
+
|
|
7
|
+
const agents = new Map<string, AgentProfile>()
|
|
8
|
+
|
|
9
|
+
// --- Helpers ---
|
|
10
|
+
|
|
11
|
+
function todayString(): string {
|
|
12
|
+
return new Date().toISOString().split('T')[0]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function yesterdayString(): string {
|
|
16
|
+
const d = new Date()
|
|
17
|
+
d.setDate(d.getDate() - 1)
|
|
18
|
+
return d.toISOString().split('T')[0]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function defaultBreakdown(): TrustBreakdown {
|
|
22
|
+
return { identity: 4, onChain: 0, behavior: 0, compliance: 0, network: 0, risk: 0, total: 4 }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function recalc(agent: AgentProfile): void {
|
|
26
|
+
const breakdown = computeTrustScore(agent, getAgent)
|
|
27
|
+
agent.breakdown = breakdown
|
|
28
|
+
agent.trustScore = breakdown.total
|
|
29
|
+
agent.tier = getSpendingLimits(breakdown.total).tier.name
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- Core Store Functions ---
|
|
33
|
+
|
|
34
|
+
export function getOrCreateAgent(address: string): AgentProfile {
|
|
35
|
+
if (agents.has(address)) return agents.get(address)!
|
|
36
|
+
|
|
37
|
+
const today = todayString()
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
|
|
40
|
+
const profile: AgentProfile = {
|
|
41
|
+
address,
|
|
42
|
+
isOWSWallet: false,
|
|
43
|
+
worldIdVerified: false,
|
|
44
|
+
webBotAuthVerified: false,
|
|
45
|
+
trustScore: 4,
|
|
46
|
+
tier: 'Restricted',
|
|
47
|
+
breakdown: defaultBreakdown(),
|
|
48
|
+
totalRequests: 0,
|
|
49
|
+
successfulRequests: 0,
|
|
50
|
+
failedRequests: 0,
|
|
51
|
+
dailySpent: 0,
|
|
52
|
+
dailyDate: today,
|
|
53
|
+
consecutiveApprovals: 0,
|
|
54
|
+
consecutiveDenials: 0,
|
|
55
|
+
totalApproved: 0,
|
|
56
|
+
totalDenied: 0,
|
|
57
|
+
humanOverrides: 0,
|
|
58
|
+
counterparties: [],
|
|
59
|
+
requestTimestamps: [],
|
|
60
|
+
lastActive: now,
|
|
61
|
+
consecutiveCleanDays: 0,
|
|
62
|
+
cleanDayDate: today,
|
|
63
|
+
createdAt: now,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
agents.set(address, profile)
|
|
67
|
+
refreshOnChainData(address).catch(() => {})
|
|
68
|
+
return profile
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getAgent(address: string): AgentProfile | undefined {
|
|
72
|
+
return agents.get(address)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getAllAgents(): AgentProfile[] {
|
|
76
|
+
return Array.from(agents.values())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getTopAgents(n: number): AgentProfile[] {
|
|
80
|
+
return getAllAgents().sort((a, b) => b.trustScore - a.trustScore).slice(0, n)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function recordApproval(address: string, txTo: string, amount: number): AgentProfile {
|
|
84
|
+
const agent = getOrCreateAgent(address)
|
|
85
|
+
const now = Date.now()
|
|
86
|
+
const today = todayString()
|
|
87
|
+
|
|
88
|
+
agent.totalRequests++
|
|
89
|
+
agent.successfulRequests++
|
|
90
|
+
agent.totalApproved++
|
|
91
|
+
agent.consecutiveApprovals++
|
|
92
|
+
agent.consecutiveDenials = 0
|
|
93
|
+
|
|
94
|
+
agent.requestTimestamps.push(now)
|
|
95
|
+
if (agent.requestTimestamps.length > 100) agent.requestTimestamps = agent.requestTimestamps.slice(-100)
|
|
96
|
+
|
|
97
|
+
if (!agent.counterparties.includes(txTo)) agent.counterparties.push(txTo)
|
|
98
|
+
|
|
99
|
+
if (agent.dailyDate !== today) {
|
|
100
|
+
agent.dailySpent = 0
|
|
101
|
+
agent.dailyDate = today
|
|
102
|
+
}
|
|
103
|
+
agent.dailySpent += amount
|
|
104
|
+
agent.lastActive = now
|
|
105
|
+
|
|
106
|
+
if (agent.cleanDayDate === today) {
|
|
107
|
+
// already counted today
|
|
108
|
+
} else if (agent.cleanDayDate === yesterdayString()) {
|
|
109
|
+
agent.consecutiveCleanDays++
|
|
110
|
+
} else {
|
|
111
|
+
agent.consecutiveCleanDays = 1
|
|
112
|
+
}
|
|
113
|
+
agent.cleanDayDate = today
|
|
114
|
+
|
|
115
|
+
recalc(agent)
|
|
116
|
+
refreshOnChainData(agent.address).catch(() => {})
|
|
117
|
+
return agent
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function recordDenial(
|
|
121
|
+
address: string,
|
|
122
|
+
tx?: PolicyContext['transaction'],
|
|
123
|
+
amount?: number
|
|
124
|
+
): AgentProfile {
|
|
125
|
+
const agent = getOrCreateAgent(address)
|
|
126
|
+
const now = Date.now()
|
|
127
|
+
|
|
128
|
+
agent.totalRequests++
|
|
129
|
+
agent.failedRequests++
|
|
130
|
+
agent.totalDenied++
|
|
131
|
+
agent.consecutiveDenials++
|
|
132
|
+
agent.consecutiveApprovals = 0
|
|
133
|
+
|
|
134
|
+
agent.requestTimestamps.push(now)
|
|
135
|
+
if (agent.requestTimestamps.length > 100) agent.requestTimestamps = agent.requestTimestamps.slice(-100)
|
|
136
|
+
|
|
137
|
+
agent.lastActive = now
|
|
138
|
+
|
|
139
|
+
if (tx && amount !== undefined) {
|
|
140
|
+
agent.pendingOverride = { tx, amount, createdAt: Date.now() }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
recalc(agent)
|
|
144
|
+
return agent
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function recordOverride(address: string): AgentProfile | undefined {
|
|
148
|
+
const agent = getAgent(address)
|
|
149
|
+
if (!agent || !agent.pendingOverride) return undefined
|
|
150
|
+
|
|
151
|
+
const OVERRIDE_TTL = 5 * 60 * 1000 // 5 minutes
|
|
152
|
+
if (Date.now() - (agent.pendingOverride.createdAt || 0) > OVERRIDE_TTL) {
|
|
153
|
+
agent.pendingOverride = undefined
|
|
154
|
+
return undefined // expired
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
agent.humanOverrides++
|
|
158
|
+
agent.pendingOverride = undefined
|
|
159
|
+
|
|
160
|
+
agent.totalApproved++
|
|
161
|
+
agent.consecutiveApprovals++
|
|
162
|
+
agent.consecutiveDenials = 0
|
|
163
|
+
|
|
164
|
+
recalc(agent)
|
|
165
|
+
const boost = getConfig().scoring?.overrideBoost ?? 3
|
|
166
|
+
agent.trustScore = Math.min(100, agent.trustScore + boost)
|
|
167
|
+
agent.tier = getSpendingLimits(agent.trustScore).tier.name
|
|
168
|
+
return agent
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function addVerifiedHuman(address: string, nullifierHash: string): AgentProfile {
|
|
172
|
+
const agent = getOrCreateAgent(address)
|
|
173
|
+
agent.worldIdVerified = true
|
|
174
|
+
agent.nullifierHash = nullifierHash
|
|
175
|
+
recalc(agent)
|
|
176
|
+
return agent
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function isVerifiedHuman(address: string): boolean {
|
|
180
|
+
return agents.get(address)?.worldIdVerified ?? false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function setWebBotAuthVerified(address: string): AgentProfile {
|
|
184
|
+
const agent = getOrCreateAgent(address)
|
|
185
|
+
agent.webBotAuthVerified = true
|
|
186
|
+
recalc(agent)
|
|
187
|
+
return agent
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function setOWSWallet(address: string, walletId: string, apiKeyId: string): AgentProfile {
|
|
191
|
+
const agent = getOrCreateAgent(address)
|
|
192
|
+
agent.isOWSWallet = true
|
|
193
|
+
agent.walletId = walletId
|
|
194
|
+
agent.apiKeyId = apiKeyId
|
|
195
|
+
recalc(agent)
|
|
196
|
+
return agent
|
|
197
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// --- OWS Policy Types ---
|
|
2
|
+
|
|
3
|
+
export interface PolicyContext {
|
|
4
|
+
chain_id: string
|
|
5
|
+
wallet_id: string
|
|
6
|
+
api_key_id: string
|
|
7
|
+
transaction: {
|
|
8
|
+
to: string
|
|
9
|
+
value: string
|
|
10
|
+
raw_hex: string
|
|
11
|
+
data: string
|
|
12
|
+
}
|
|
13
|
+
spending: {
|
|
14
|
+
daily_total: string
|
|
15
|
+
date: string
|
|
16
|
+
}
|
|
17
|
+
timestamp: string
|
|
18
|
+
policy_config?: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PolicyResult {
|
|
22
|
+
allow: boolean
|
|
23
|
+
reason?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Scoring Types ---
|
|
27
|
+
|
|
28
|
+
export interface TrustBreakdown {
|
|
29
|
+
identity: number
|
|
30
|
+
onChain: number
|
|
31
|
+
behavior: number
|
|
32
|
+
compliance: number
|
|
33
|
+
network: number
|
|
34
|
+
risk: number
|
|
35
|
+
total: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SpendingTier {
|
|
39
|
+
name: 'Sovereign' | 'Trusted' | 'Building' | 'Cautious' | 'Restricted' | 'Frozen'
|
|
40
|
+
color: string
|
|
41
|
+
dailyLimit: number
|
|
42
|
+
perTxLimit: number
|
|
43
|
+
minScore: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Agent Profile ---
|
|
47
|
+
|
|
48
|
+
export interface AgentProfile {
|
|
49
|
+
address: string
|
|
50
|
+
walletId?: string
|
|
51
|
+
apiKeyId?: string
|
|
52
|
+
chainId?: string
|
|
53
|
+
isOWSWallet: boolean
|
|
54
|
+
worldIdVerified: boolean
|
|
55
|
+
webBotAuthVerified: boolean
|
|
56
|
+
nullifierHash?: string
|
|
57
|
+
trustScore: number
|
|
58
|
+
breakdown: TrustBreakdown
|
|
59
|
+
tier: string
|
|
60
|
+
totalRequests: number
|
|
61
|
+
successfulRequests: number
|
|
62
|
+
failedRequests: number
|
|
63
|
+
dailySpent: number
|
|
64
|
+
dailyDate: string
|
|
65
|
+
consecutiveApprovals: number
|
|
66
|
+
consecutiveDenials: number
|
|
67
|
+
totalApproved: number
|
|
68
|
+
totalDenied: number
|
|
69
|
+
humanOverrides: number
|
|
70
|
+
counterparties: string[]
|
|
71
|
+
pendingOverride?: { tx: PolicyContext['transaction']; amount: number; createdAt: number }
|
|
72
|
+
lastOnChainRefresh?: number
|
|
73
|
+
requestTimestamps: number[]
|
|
74
|
+
lastActive: number
|
|
75
|
+
consecutiveCleanDays: number
|
|
76
|
+
cleanDayDate: string
|
|
77
|
+
createdAt: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- WebSocket Event Types ---
|
|
81
|
+
|
|
82
|
+
export interface PolicyDecisionEvent {
|
|
83
|
+
type: 'POLICY_DECISION'
|
|
84
|
+
agent: string
|
|
85
|
+
amount: number
|
|
86
|
+
trustScore: number
|
|
87
|
+
tier: string
|
|
88
|
+
decision: 'APPROVE' | 'DENY' | 'OVERRIDE'
|
|
89
|
+
reason: string
|
|
90
|
+
dailyLimit: number
|
|
91
|
+
dailySpent: number
|
|
92
|
+
timestamp: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TrustChangeEvent {
|
|
96
|
+
type: 'TRUST_CHANGE'
|
|
97
|
+
agent: string
|
|
98
|
+
oldScore: number
|
|
99
|
+
newScore: number
|
|
100
|
+
oldTier: string
|
|
101
|
+
newTier: string
|
|
102
|
+
reason: string
|
|
103
|
+
timestamp: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface BudgetWarningEvent {
|
|
107
|
+
type: 'BUDGET_WARNING'
|
|
108
|
+
agent: string
|
|
109
|
+
spent: number
|
|
110
|
+
limit: number
|
|
111
|
+
percentage: number
|
|
112
|
+
timestamp: number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type TriageEvent = PolicyDecisionEvent | TrustChangeEvent | BudgetWarningEvent
|
|
116
|
+
|
|
117
|
+
// --- Spending Tiers ---
|
|
118
|
+
|
|
119
|
+
export let SPENDING_TIERS: SpendingTier[] = [
|
|
120
|
+
{ name: 'Sovereign', color: '#fff8e1', dailyLimit: 1000, perTxLimit: 500, minScore: 80 },
|
|
121
|
+
{ name: 'Trusted', color: '#ffd700', dailyLimit: 200, perTxLimit: 100, minScore: 60 },
|
|
122
|
+
{ name: 'Building', color: '#4caf50', dailyLimit: 50, perTxLimit: 25, minScore: 40 },
|
|
123
|
+
{ name: 'Cautious', color: '#00bcd4', dailyLimit: 10, perTxLimit: 5, minScore: 20 },
|
|
124
|
+
{ name: 'Restricted', color: '#2196f3', dailyLimit: 2, perTxLimit: 1, minScore: 1 },
|
|
125
|
+
{ name: 'Frozen', color: '#1a1a4e', dailyLimit: 0, perTxLimit: 0, minScore: 0 },
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
export function updateSpendingTiers(tiers: SpendingTier[]): void {
|
|
129
|
+
SPENDING_TIERS = tiers
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getTierForScore(score: number): SpendingTier {
|
|
133
|
+
for (const tier of SPENDING_TIERS) {
|
|
134
|
+
if (score >= tier.minScore) return tier
|
|
135
|
+
}
|
|
136
|
+
return SPENDING_TIERS[SPENDING_TIERS.length - 1]
|
|
137
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
|
|
3
|
+
export interface SignatureComponents {
|
|
4
|
+
signatureInput: string
|
|
5
|
+
signature: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse RFC 9421 Signature-Input header.
|
|
10
|
+
* Format: sig1=("@method" "@authority" "@path"); keyid="agent-key"; alg="ed25519"; created=1234567890
|
|
11
|
+
*/
|
|
12
|
+
export function parseSignatureInput(input: string): {
|
|
13
|
+
label: string
|
|
14
|
+
components: string[]
|
|
15
|
+
params: Record<string, string>
|
|
16
|
+
} | null {
|
|
17
|
+
try {
|
|
18
|
+
const match = input.match(/^(\w+)=\(([^)]*)\);\s*(.+)$/)
|
|
19
|
+
if (!match) return null
|
|
20
|
+
const [, label, componentStr, paramStr] = match
|
|
21
|
+
const components = componentStr.match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) ?? []
|
|
22
|
+
const params: Record<string, string> = {}
|
|
23
|
+
for (const part of paramStr.split(';')) {
|
|
24
|
+
const [k, v] = part.trim().split('=')
|
|
25
|
+
if (k && v) params[k.trim()] = v.replace(/"/g, '').trim()
|
|
26
|
+
}
|
|
27
|
+
return { label, components, params }
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the RFC 9421 signature base string from request components.
|
|
35
|
+
*/
|
|
36
|
+
export function buildSignatureBase(
|
|
37
|
+
components: string[],
|
|
38
|
+
request: { method: string; url: string; headers: Record<string, string> },
|
|
39
|
+
signatureInputValue: string,
|
|
40
|
+
label: string
|
|
41
|
+
): string {
|
|
42
|
+
const lines: string[] = []
|
|
43
|
+
const url = new URL(request.url, 'http://localhost')
|
|
44
|
+
for (const comp of components) {
|
|
45
|
+
if (comp === '@method') lines.push(`"@method": ${request.method.toUpperCase()}`)
|
|
46
|
+
else if (comp === '@authority') lines.push(`"@authority": ${url.host}`)
|
|
47
|
+
else if (comp === '@path') lines.push(`"@path": ${url.pathname}`)
|
|
48
|
+
else if (comp === '@query') lines.push(`"@query": ?${url.search.slice(1)}`)
|
|
49
|
+
else if (comp.startsWith('@')) lines.push(`"${comp}": `)
|
|
50
|
+
else lines.push(`"${comp}": ${request.headers[comp.toLowerCase()] ?? ''}`)
|
|
51
|
+
}
|
|
52
|
+
lines.push(`"@signature-params": ${signatureInputValue.split('=').slice(1).join('=')}`)
|
|
53
|
+
return lines.join('\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verify an Ed25519 signature.
|
|
58
|
+
* Returns true if the signature is valid for the given public key and data.
|
|
59
|
+
*/
|
|
60
|
+
export function verifyEd25519Signature(
|
|
61
|
+
publicKeyBase64: string,
|
|
62
|
+
signatureBase64: string,
|
|
63
|
+
data: string
|
|
64
|
+
): boolean {
|
|
65
|
+
try {
|
|
66
|
+
const pubKeyBuffer = Buffer.from(publicKeyBase64, 'base64')
|
|
67
|
+
const sigBuffer = Buffer.from(signatureBase64, 'base64')
|
|
68
|
+
const key = crypto.createPublicKey({
|
|
69
|
+
key: Buffer.concat([
|
|
70
|
+
// Ed25519 DER prefix
|
|
71
|
+
Buffer.from('302a300506032b6570032100', 'hex'),
|
|
72
|
+
pubKeyBuffer
|
|
73
|
+
]),
|
|
74
|
+
format: 'der',
|
|
75
|
+
type: 'spki'
|
|
76
|
+
})
|
|
77
|
+
return crypto.verify(null, Buffer.from(data), key, sigBuffer)
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Public key cache: agentKey -> base64 public key
|
|
84
|
+
const keyCache = new Map<string, string>()
|
|
85
|
+
|
|
86
|
+
export function registerAgentPublicKey(agentKey: string, publicKeyBase64: string): void {
|
|
87
|
+
keyCache.set(agentKey, publicKeyBase64)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getAgentPublicKey(agentKey: string): string | undefined {
|
|
91
|
+
return keyCache.get(agentKey)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Full Web Bot Auth verification.
|
|
96
|
+
* Checks Signature-Input + Signature headers against agent's registered public key.
|
|
97
|
+
*/
|
|
98
|
+
export function verifyWebBotAuth(
|
|
99
|
+
headers: Record<string, string>,
|
|
100
|
+
request: { method: string; url: string; headers: Record<string, string> },
|
|
101
|
+
agentKey: string
|
|
102
|
+
): boolean {
|
|
103
|
+
const signatureInput = headers['signature-input']
|
|
104
|
+
const signature = headers['signature']
|
|
105
|
+
if (!signatureInput || !signature) return false
|
|
106
|
+
|
|
107
|
+
const parsed = parseSignatureInput(signatureInput)
|
|
108
|
+
if (!parsed) return false
|
|
109
|
+
|
|
110
|
+
// Check algorithm is ed25519
|
|
111
|
+
if (parsed.params.alg && parsed.params.alg !== 'ed25519') return false
|
|
112
|
+
|
|
113
|
+
// Check expiry
|
|
114
|
+
if (parsed.params.expires && Number(parsed.params.expires) < Date.now() / 1000) return false
|
|
115
|
+
|
|
116
|
+
// Get public key
|
|
117
|
+
const pubKey = getAgentPublicKey(agentKey) || getAgentPublicKey(parsed.params.keyid)
|
|
118
|
+
if (!pubKey) return false
|
|
119
|
+
|
|
120
|
+
// Build signature base and verify
|
|
121
|
+
const base = buildSignatureBase(parsed.components, request, signatureInput, parsed.label)
|
|
122
|
+
|
|
123
|
+
// Extract signature value (format: sig1=:base64:)
|
|
124
|
+
const sigMatch = signature.match(new RegExp(`${parsed.label}=:([^:]+):`))
|
|
125
|
+
if (!sigMatch) return false
|
|
126
|
+
|
|
127
|
+
return verifyEd25519Signature(pubKey, sigMatch[1], base)
|
|
128
|
+
}
|
package/src/xmtp.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Client, IdentifierKind } from '@xmtp/node-sdk'
|
|
2
|
+
import type { Signer, Identifier, Dm } from '@xmtp/node-sdk'
|
|
3
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
4
|
+
import { hexToBytes } from 'viem'
|
|
5
|
+
import { getConfig } from './config'
|
|
6
|
+
|
|
7
|
+
const HUMAN_ADDRESS = process.env.HUMAN_XMTP_ADDRESS || ''
|
|
8
|
+
|
|
9
|
+
// Module-level state
|
|
10
|
+
let xmtpClient: Client | null = null
|
|
11
|
+
let humanDm: Dm | null = null
|
|
12
|
+
let lastDeniedAgent = ''
|
|
13
|
+
|
|
14
|
+
function humanIdentifier(): Identifier {
|
|
15
|
+
return {
|
|
16
|
+
identifier: HUMAN_ADDRESS.toLowerCase(),
|
|
17
|
+
identifierKind: IdentifierKind.Ethereum,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ----------------------------------------------------------------
|
|
22
|
+
// Incoming message listener — detects "override" replies
|
|
23
|
+
// ----------------------------------------------------------------
|
|
24
|
+
async function startMessageListener(): Promise<void> {
|
|
25
|
+
if (!xmtpClient) return
|
|
26
|
+
try {
|
|
27
|
+
const stream = await xmtpClient.conversations.streamAllMessages()
|
|
28
|
+
for await (const message of stream) {
|
|
29
|
+
try {
|
|
30
|
+
const text = typeof message.content === 'string' ? message.content.trim().toLowerCase() : ''
|
|
31
|
+
if (text.startsWith('override') && lastDeniedAgent) {
|
|
32
|
+
const port = process.env.PORT || '4021'
|
|
33
|
+
const agentToOverride = lastDeniedAgent
|
|
34
|
+
lastDeniedAgent = ''
|
|
35
|
+
const res = await fetch(`http://localhost:${port}/api/override/${agentToOverride}`, { method: 'POST' })
|
|
36
|
+
if (res.ok) {
|
|
37
|
+
console.log(`[XMTP] Override accepted for agent: ${agentToOverride}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('[XMTP] Error processing message:', err)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('[XMTP] Message stream error:', err)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ----------------------------------------------------------------
|
|
50
|
+
// Send helper — finds or creates DM with human, caches it
|
|
51
|
+
// ----------------------------------------------------------------
|
|
52
|
+
async function sendToHuman(text: string): Promise<void> {
|
|
53
|
+
if (!xmtpClient) return
|
|
54
|
+
try {
|
|
55
|
+
if (!humanDm) {
|
|
56
|
+
humanDm = await xmtpClient.conversations.createDmWithIdentifier(humanIdentifier())
|
|
57
|
+
}
|
|
58
|
+
await humanDm.sendText(text)
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('[XMTP] Send error:', err)
|
|
61
|
+
humanDm = null // reset so next call retries
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ----------------------------------------------------------------
|
|
66
|
+
// Exported functions
|
|
67
|
+
// ----------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export async function initXMTP(): Promise<void> {
|
|
70
|
+
if (!getConfig().xmtp?.enabled) {
|
|
71
|
+
console.log('[XMTP] Disabled by config')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const privateKey = process.env.XMTP_PRIVATE_KEY
|
|
75
|
+
if (!privateKey || !HUMAN_ADDRESS) {
|
|
76
|
+
console.log('[XMTP] Not configured — notifications disabled')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const account = privateKeyToAccount(privateKey as `0x${string}`)
|
|
82
|
+
|
|
83
|
+
const signer: Signer = {
|
|
84
|
+
type: 'EOA',
|
|
85
|
+
getIdentifier: (): Identifier => ({
|
|
86
|
+
identifier: account.address.toLowerCase(),
|
|
87
|
+
identifierKind: IdentifierKind.Ethereum,
|
|
88
|
+
}),
|
|
89
|
+
signMessage: async (message: string): Promise<Uint8Array> => {
|
|
90
|
+
const sig = await account.signMessage({ message })
|
|
91
|
+
return hexToBytes(sig)
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// `as any` required: ClientOptions is a union type that doesn't survive
|
|
96
|
+
// Omit<> in Client.create's signature — SDK types collapse the union.
|
|
97
|
+
xmtpClient = await Client.create(signer, { env: 'production' } as any)
|
|
98
|
+
|
|
99
|
+
// Warm up the DM channel
|
|
100
|
+
humanDm = await xmtpClient.conversations.createDmWithIdentifier(humanIdentifier())
|
|
101
|
+
|
|
102
|
+
// Start listening for override replies (fire and forget)
|
|
103
|
+
startMessageListener().catch((err) => {
|
|
104
|
+
console.error('[XMTP] Listener startup error:', err)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
console.log('[XMTP] Initialized — notifications enabled')
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('[XMTP] Init failed:', err)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function notifyDenial(
|
|
114
|
+
agent: string,
|
|
115
|
+
amount: number,
|
|
116
|
+
trustScore: number,
|
|
117
|
+
tier: string,
|
|
118
|
+
dailyLimit: number,
|
|
119
|
+
dailySpent: number,
|
|
120
|
+
reason: string,
|
|
121
|
+
txTo: string,
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
if (!xmtpClient) return
|
|
124
|
+
lastDeniedAgent = agent
|
|
125
|
+
const msg = [
|
|
126
|
+
`⚠️ Transaction denied`,
|
|
127
|
+
`Agent: ${agent}`,
|
|
128
|
+
`Amount: $${amount.toFixed(2)} USDC → ${txTo}`,
|
|
129
|
+
`Trust score: ${trustScore} (${tier} tier)`,
|
|
130
|
+
`Daily limit: $${dailyLimit} | Spent today: $${dailySpent.toFixed(2)}`,
|
|
131
|
+
`Reason: ${reason}`,
|
|
132
|
+
``,
|
|
133
|
+
`Reply "override" to approve this transaction.`,
|
|
134
|
+
].join('\n')
|
|
135
|
+
await sendToHuman(msg)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function notifyTrustChange(
|
|
139
|
+
agent: string,
|
|
140
|
+
oldScore: number,
|
|
141
|
+
newScore: number,
|
|
142
|
+
oldTier: string,
|
|
143
|
+
newTier: string,
|
|
144
|
+
reason: string,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
if (!xmtpClient) return
|
|
147
|
+
const msg = [
|
|
148
|
+
`📊 Trust update: ${agent}`,
|
|
149
|
+
`Score: ${oldScore} → ${newScore} | Tier: ${oldTier} → ${newTier}`,
|
|
150
|
+
`Reason: ${reason}`,
|
|
151
|
+
].join('\n')
|
|
152
|
+
await sendToHuman(msg)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function notifyBudgetWarning(
|
|
156
|
+
agent: string,
|
|
157
|
+
spent: number,
|
|
158
|
+
limit: number,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
if (!xmtpClient) return
|
|
161
|
+
const percentage = Math.round((spent / limit) * 100)
|
|
162
|
+
const remaining = limit - spent
|
|
163
|
+
const msg = [
|
|
164
|
+
`💰 Budget alert: ${agent}`,
|
|
165
|
+
`Daily spend: $${spent.toFixed(2)} / $${limit} (${percentage}%)`,
|
|
166
|
+
`Remaining: $${remaining.toFixed(2)}`,
|
|
167
|
+
].join('\n')
|
|
168
|
+
await sendToHuman(msg)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function notifyAnomaly(
|
|
172
|
+
agent: string,
|
|
173
|
+
pattern: string,
|
|
174
|
+
action: string,
|
|
175
|
+
riskPenalty: number,
|
|
176
|
+
oldScore: number,
|
|
177
|
+
newScore: number,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
if (!xmtpClient) return
|
|
180
|
+
const msg = [
|
|
181
|
+
`🚨 Anomaly: ${agent}`,
|
|
182
|
+
`Pattern: ${pattern}`,
|
|
183
|
+
`Action: ${action}`,
|
|
184
|
+
`Risk penalty: -${riskPenalty}`,
|
|
185
|
+
`Trust score: ${oldScore} → ${newScore}`,
|
|
186
|
+
].join('\n')
|
|
187
|
+
await sendToHuman(msg)
|
|
188
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"warningThreshold": 0.8,
|
|
3
|
+
"scoreBands": [
|
|
4
|
+
{ "name": "Sovereign", "min": 80, "dailyLimit": 1000, "perTxLimit": 500 },
|
|
5
|
+
{ "name": "Trusted", "min": 60, "dailyLimit": 200, "perTxLimit": 100 },
|
|
6
|
+
{ "name": "Building", "min": 40, "dailyLimit": 50, "perTxLimit": 25 },
|
|
7
|
+
{ "name": "Cautious", "min": 20, "dailyLimit": 10, "perTxLimit": 5 },
|
|
8
|
+
{ "name": "Restricted", "min": 1, "dailyLimit": 2, "perTxLimit": 1 },
|
|
9
|
+
{ "name": "Frozen", "min": 0, "dailyLimit": 0, "perTxLimit": 0 }
|
|
10
|
+
],
|
|
11
|
+
"xmtp": { "enabled": true },
|
|
12
|
+
"worldId": { "enabled": true },
|
|
13
|
+
"webBotAuth": { "enabled": true },
|
|
14
|
+
"networkScore": { "enabled": true },
|
|
15
|
+
"scoring": {
|
|
16
|
+
"overrideBoost": 3,
|
|
17
|
+
"maxFrequencyPenalty": 10,
|
|
18
|
+
"inactivityDecayRate": 0.5
|
|
19
|
+
},
|
|
20
|
+
"port": 4021,
|
|
21
|
+
"dashboardEnabled": true
|
|
22
|
+
}
|