@reinconsole/graph 0.1.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/dist/index.js ADDED
@@ -0,0 +1,910 @@
1
+ // ../../packages/core/dist/index.js
2
+ import { z } from "zod";
3
+ var Chain = z.enum(["base", "solana", "polygon", "bnb"]);
4
+ var Asset = z.enum(["USDC", "USDT", "EURC"]);
5
+ var DecimalString = z.string().regex(/^\d+(\.\d+)?$/, 'must be a non-negative decimal string, e.g. "5.00"');
6
+ var DECIMAL_RE = /^\d+(\.\d+)?$/;
7
+ function isValidDecimal(value) {
8
+ return DECIMAL_RE.test(value);
9
+ }
10
+ function assertDecimal(value) {
11
+ if (!isValidDecimal(value)) {
12
+ throw new TypeError(`invalid decimal string: ${JSON.stringify(value)}`);
13
+ }
14
+ }
15
+ function fractionLength(value) {
16
+ const dot = value.indexOf(".");
17
+ return dot === -1 ? 0 : value.length - dot - 1;
18
+ }
19
+ function toScaled(value, scale) {
20
+ const dot = value.indexOf(".");
21
+ const intPart = dot === -1 ? value : value.slice(0, dot);
22
+ const fracPart = dot === -1 ? "" : value.slice(dot + 1);
23
+ const paddedFrac = (fracPart + "0".repeat(scale)).slice(0, scale);
24
+ return BigInt(intPart + paddedFrac);
25
+ }
26
+ function fromScaled(scaled, scale) {
27
+ if (scale === 0) return scaled.toString();
28
+ const digits = scaled.toString().padStart(scale + 1, "0");
29
+ const intPart = digits.slice(0, digits.length - scale);
30
+ const fracPart = digits.slice(digits.length - scale).replace(/0+$/, "");
31
+ return fracPart.length > 0 ? `${intPart}.${fracPart}` : intPart;
32
+ }
33
+ function sumDecimal(values) {
34
+ if (values.length === 0) return "0";
35
+ let scale = 0;
36
+ for (const v of values) {
37
+ assertDecimal(v);
38
+ scale = Math.max(scale, fractionLength(v));
39
+ }
40
+ let total = 0n;
41
+ for (const v of values) total += toScaled(v, scale);
42
+ return fromScaled(total, scale);
43
+ }
44
+ var ULID_BODY = "[0-9A-HJKMNP-TV-Z]{26}";
45
+ function prefixedId(prefix) {
46
+ return z.string().regex(new RegExp(`^${prefix}_${ULID_BODY}$`), `expected a "${prefix}_" prefixed id`);
47
+ }
48
+ var OrgId = prefixedId("org");
49
+ var AgentId = prefixedId("agt");
50
+ var PolicyId = prefixedId("pol");
51
+ var IntentId = prefixedId("int");
52
+ var DecisionId = prefixedId("dec");
53
+ var ReceiptId = prefixedId("rcp");
54
+ var SessionId = prefixedId("ses");
55
+ var GateReceiptId = prefixedId("grc");
56
+ var EnforcementMode = z.enum(["observed", "sdk", "session-key"]);
57
+ var AgentWallet = z.object({
58
+ chain: Chain,
59
+ address: z.string().min(1),
60
+ mode: EnforcementMode
61
+ });
62
+ var AgentStatus = z.enum(["active", "frozen"]);
63
+ var Agent = z.object({
64
+ id: AgentId,
65
+ orgId: OrgId,
66
+ name: z.string().min(1).max(200),
67
+ /** On-chain ERC-8004 identity, if the agent is registered. */
68
+ erc8004Id: z.string().optional(),
69
+ wallets: z.array(AgentWallet).default([]),
70
+ status: AgentStatus.default("active"),
71
+ createdAt: z.coerce.date()
72
+ });
73
+ var REGISTRY_PATTERN = /^0x[0-9a-fA-F]{40}$/;
74
+ var ID_PATTERN = /^eip155:(\d+):(0x[0-9a-fA-F]{40})\/(\d+)$/;
75
+ function formatErc8004Id(ref) {
76
+ if (!Number.isSafeInteger(ref.chainId) || ref.chainId < 1) {
77
+ throw new RangeError(`erc8004: bad chainId ${ref.chainId}`);
78
+ }
79
+ if (!REGISTRY_PATTERN.test(ref.registry)) {
80
+ throw new RangeError(`erc8004: bad registry address ${ref.registry}`);
81
+ }
82
+ if (typeof ref.tokenId !== "bigint" || ref.tokenId < 0n) {
83
+ throw new RangeError(`erc8004: bad tokenId ${ref.tokenId}`);
84
+ }
85
+ return `eip155:${ref.chainId}:${ref.registry.toLowerCase()}/${ref.tokenId}`;
86
+ }
87
+ function parseErc8004Id(value) {
88
+ const match = ID_PATTERN.exec(value);
89
+ if (!match) return void 0;
90
+ const chainId = Number(match[1]);
91
+ if (!Number.isSafeInteger(chainId) || chainId < 1) return void 0;
92
+ return {
93
+ chainId,
94
+ registry: match[2].toLowerCase(),
95
+ tokenId: BigInt(match[3])
96
+ };
97
+ }
98
+ var Erc8004Id = z.string().refine((value) => parseErc8004Id(value) !== void 0, {
99
+ message: 'expected "eip155:{chainId}:{registry}/{tokenId}"'
100
+ });
101
+ var Vendor = z.object({
102
+ host: z.string().min(1),
103
+ address: z.string().min(1),
104
+ erc8004Id: z.string().optional()
105
+ });
106
+ var TaskContext = z.object({
107
+ taskId: z.string().optional(),
108
+ parentRunId: z.string().optional(),
109
+ purpose: z.string().max(500).optional()
110
+ });
111
+ var PaymentIntent = z.object({
112
+ id: IntentId,
113
+ agentId: AgentId,
114
+ vendor: Vendor,
115
+ resource: z.string(),
116
+ amount: DecimalString,
117
+ asset: Asset,
118
+ chain: Chain,
119
+ taskContext: TaskContext.default({}),
120
+ nonce: z.string().min(1),
121
+ createdAt: z.coerce.date()
122
+ });
123
+ var DecisionOutcome = z.enum(["allow", "deny", "escalate"]);
124
+ var Decision = z.object({
125
+ id: DecisionId,
126
+ intentId: IntentId,
127
+ /**
128
+ * sha256 of the intent's canonical content (see canonical.ts). Binds the
129
+ * decision to the exact transfer it judged — amount, recipient, asset,
130
+ * chain — so a signer can verify an {intent, decision} pair offline as a
131
+ * self-contained spend voucher, not just a reference by id.
132
+ */
133
+ intentHash: z.string(),
134
+ outcome: DecisionOutcome,
135
+ /** Ids of the rules that fired, for explainability. */
136
+ matchedRules: z.array(z.string()).default([]),
137
+ /** Human-readable explanation surfaced in the dashboard. */
138
+ reason: z.string().optional(),
139
+ policyId: z.string(),
140
+ policyVersion: z.string(),
141
+ /** Hash of the previous decision in the chain (tamper-evident log). */
142
+ prevHash: z.string(),
143
+ /** Hash of this decision's canonical content. */
144
+ hash: z.string(),
145
+ /** Service signing-key signature over `hash`. */
146
+ signature: z.string(),
147
+ latencyMs: z.number().nonnegative(),
148
+ decidedAt: z.coerce.date()
149
+ });
150
+ var Session = z.object({
151
+ id: SessionId,
152
+ agentId: AgentId,
153
+ /** sha256 hex of the bearer token. */
154
+ tokenHash: z.string(),
155
+ /** Cumulative ceiling across the session's lifetime. Absent = uncapped. */
156
+ capAmount: DecimalString.optional(),
157
+ /** Ceiling per individual signature. Absent = uncapped. */
158
+ maxPerPayment: DecimalString.optional(),
159
+ expiresAt: z.coerce.date(),
160
+ createdAt: z.coerce.date(),
161
+ revokedAt: z.coerce.date().optional()
162
+ });
163
+ var SettledPayment = z.object({
164
+ intentId: IntentId,
165
+ txHash: z.string(),
166
+ chain: Chain,
167
+ blockNumber: z.coerce.bigint(),
168
+ facilitator: z.string().optional(),
169
+ feePaid: DecimalString.optional(),
170
+ confirmedAt: z.coerce.date()
171
+ });
172
+ var ReceiptSettlement = z.object({
173
+ txHash: z.string().optional(),
174
+ networkId: z.string().optional(),
175
+ /** Raw header payload, kept for forensics until the indexer confirms. */
176
+ raw: z.string().optional()
177
+ });
178
+ var Receipt = z.object({
179
+ id: ReceiptId,
180
+ agentId: AgentId,
181
+ intentId: IntentId,
182
+ decisionId: DecisionId,
183
+ outcome: DecisionOutcome,
184
+ /** The URL the agent actually fetched (the paywalled resource). */
185
+ url: z.string(),
186
+ method: z.string().default("GET"),
187
+ vendorHost: z.string(),
188
+ amount: DecimalString,
189
+ asset: Asset,
190
+ chain: Chain,
191
+ taskContext: TaskContext.default({}),
192
+ /** Human-readable explanation copied from the decision. */
193
+ reason: z.string().optional(),
194
+ /** Present only once a payment was made and the vendor confirmed it. */
195
+ settlement: ReceiptSettlement.optional(),
196
+ createdAt: z.coerce.date()
197
+ });
198
+ var GateReceipt = z.object({
199
+ id: GateReceiptId,
200
+ at: z.coerce.date(),
201
+ /** The route pattern that priced this request, e.g. "/api/reports/*". */
202
+ route: z.string().min(1),
203
+ /** The concrete resource paid for (URL path actually requested). */
204
+ resource: z.string().min(1),
205
+ method: z.string().min(1),
206
+ /** The paying wallet address, as asserted by the settled payment. */
207
+ payer: z.string().min(1),
208
+ payTo: z.string().min(1),
209
+ /** Human-unit decimal amount, e.g. "0.05". */
210
+ amount: DecimalString,
211
+ /** The same amount in the asset's atomic units, e.g. "50000". */
212
+ amountAtomic: z.string().regex(/^\d+$/, "atomic amount must be an integer string"),
213
+ /** Token symbol or contract address, exactly as quoted in the requirement. */
214
+ asset: z.string().min(1),
215
+ network: z.string().min(1),
216
+ /** Settlement transaction hash (mock ledger or on-chain). */
217
+ transaction: z.string().min(1)
218
+ });
219
+ var Window = z.string().regex(/^\d+[smhd]$/, 'window must be like "30s", "15m", "1h", or "7d"');
220
+ var Multiplier = z.string().regex(/^\d+(\.\d+)?x$/, 'multiplier must be like "3x"');
221
+ var Condition = z.object({
222
+ /** Per-transaction amount exceeds this value. */
223
+ amountGt: DecimalString.optional(),
224
+ /** Sum of spend in a rolling window exceeds `gt`. */
225
+ rollingSum: z.object({ window: Window, gt: DecimalString }).optional(),
226
+ /** Transaction count in a rolling window exceeds `gt`. */
227
+ txCount: z.object({ window: Window, gt: z.number().int().nonnegative() }).optional(),
228
+ /** Vendor host matches one of these patterns (supports `*` globs). */
229
+ vendorHostIn: z.array(z.string()).optional(),
230
+ /** First time Rein has seen this vendor for the agent. */
231
+ vendorFirstSeen: z.boolean().optional(),
232
+ /** Vendor reputation score is below this threshold (Phase 3 hook). */
233
+ vendorReputationLt: z.number().min(0).max(100).optional(),
234
+ /** Amount is more than `gt`-times the observed median for this resource. */
235
+ amountVsResourceMedian: z.object({ gt: Multiplier }).optional()
236
+ }).refine((c) => Object.values(c).some((v) => v !== void 0), {
237
+ message: "a condition must specify at least one predicate"
238
+ });
239
+ var Rule = z.object({
240
+ id: z.string().min(1),
241
+ allow: Condition.optional(),
242
+ deny: Condition.optional(),
243
+ escalate: Condition.optional()
244
+ }).refine((r) => [r.allow, r.deny, r.escalate].filter((v) => v !== void 0).length === 1, {
245
+ message: "a rule must specify exactly one of allow / deny / escalate"
246
+ });
247
+ var PolicyDefault = z.enum(["allow", "deny"]);
248
+ var AppliesTo = z.object({
249
+ /** Agent id patterns (supports `*` globs, e.g. "agt_research_*"). */
250
+ agents: z.array(z.string()).optional(),
251
+ chains: z.array(Chain).optional()
252
+ });
253
+ var Escalation = z.object({
254
+ approvers: z.array(z.string()).default([]),
255
+ timeoutAction: PolicyDefault.default("deny"),
256
+ timeoutMin: z.number().int().positive().default(15)
257
+ });
258
+ var Policy = z.object({
259
+ policyId: z.string().min(1),
260
+ version: z.string().default("1"),
261
+ appliesTo: AppliesTo.default({}),
262
+ rules: z.array(Rule).default([]),
263
+ default: PolicyDefault.default("deny"),
264
+ /** Below this amount, fail-open is permitted during a policy-service outage. */
265
+ denyFloor: DecimalString.default("0.05"),
266
+ escalation: Escalation.optional()
267
+ });
268
+ var ReputationSubject = z.object({
269
+ kind: z.enum(["agent", "vendor"]),
270
+ id: z.string()
271
+ });
272
+ var ReputationComponents = z.object({
273
+ volume: z.number(),
274
+ longevity: z.number(),
275
+ disputeRate: z.number(),
276
+ counterpartyQuality: z.number(),
277
+ settlementReliability: z.number()
278
+ });
279
+ var ReputationScore = z.object({
280
+ subject: ReputationSubject,
281
+ score: z.number().min(0).max(100),
282
+ components: ReputationComponents,
283
+ confidence: z.number().min(0).max(1),
284
+ asOf: z.coerce.date(),
285
+ /** Hash-anchored attestation, optionally published on-chain (ERC-8004). */
286
+ evidenceUri: z.string().optional()
287
+ });
288
+ var ReinEvent = z.discriminatedUnion("type", [
289
+ z.object({ type: z.literal("intent.created"), at: z.coerce.date(), intent: PaymentIntent }),
290
+ z.object({ type: z.literal("decision.made"), at: z.coerce.date(), decision: Decision }),
291
+ z.object({ type: z.literal("payment.settled"), at: z.coerce.date(), payment: SettledPayment }),
292
+ z.object({
293
+ type: z.literal("shadow.spend"),
294
+ at: z.coerce.date(),
295
+ agentId: AgentId,
296
+ txHash: z.string(),
297
+ chain: Chain,
298
+ amount: DecimalString
299
+ }),
300
+ z.object({
301
+ type: z.literal("signature.released"),
302
+ at: z.coerce.date(),
303
+ sessionId: SessionId,
304
+ agentId: AgentId,
305
+ intentId: IntentId,
306
+ decisionId: DecisionId,
307
+ amount: DecimalString
308
+ }),
309
+ z.object({
310
+ type: z.literal("signature.refused"),
311
+ at: z.coerce.date(),
312
+ /** Refusal code, e.g. "decision_replayed" (see @reinconsole/signer). */
313
+ code: z.string(),
314
+ reason: z.string(),
315
+ sessionId: SessionId.optional(),
316
+ agentId: AgentId.optional(),
317
+ intentId: IntentId.optional()
318
+ }),
319
+ z.object({
320
+ type: z.literal("gate.quoted"),
321
+ at: z.coerce.date(),
322
+ resource: z.string(),
323
+ method: z.string(),
324
+ amount: DecimalString,
325
+ asset: z.string(),
326
+ network: z.string()
327
+ }),
328
+ z.object({ type: z.literal("gate.settled"), at: z.coerce.date(), receipt: GateReceipt }),
329
+ z.object({
330
+ type: z.literal("gate.refused"),
331
+ at: z.coerce.date(),
332
+ /** Refusal code, e.g. "payment_replayed" (see @reinconsole/gate). */
333
+ code: z.string(),
334
+ reason: z.string(),
335
+ resource: z.string(),
336
+ /** Known only when the payment header decoded far enough to name a payer. */
337
+ payer: z.string().optional()
338
+ })
339
+ ]);
340
+
341
+ // src/evidence.ts
342
+ function normalizeSubject(subject) {
343
+ if (subject.id.startsWith("eip155:")) {
344
+ const ref = parseErc8004Id(subject.id);
345
+ if (ref) return { kind: subject.kind, id: formatErc8004Id(ref) };
346
+ }
347
+ const id = subject.kind === "vendor" || subject.id.startsWith("0x") || subject.id.startsWith("0X") ? subject.id.toLowerCase() : subject.id;
348
+ return { kind: subject.kind, id };
349
+ }
350
+ function subjectKey(subject) {
351
+ const normal = normalizeSubject(subject);
352
+ return `${normal.kind}:${normal.id}`;
353
+ }
354
+ var EvidenceLedger = class {
355
+ records = /* @__PURE__ */ new Map();
356
+ recordAttempt(subject, atMs) {
357
+ this.touch(subject, atMs).attempts += 1;
358
+ }
359
+ recordSettlement(a, b, amount, atMs) {
360
+ for (const [subject, other] of [
361
+ [a, b],
362
+ [b, a]
363
+ ]) {
364
+ const ev = this.touch(subject, atMs);
365
+ ev.settled += 1;
366
+ ev.volume = sumDecimal([ev.volume, amount]);
367
+ const key = subjectKey(other);
368
+ const line = ev.counterparties.get(key) ?? { settled: 0, volume: "0" };
369
+ line.settled += 1;
370
+ line.volume = sumDecimal([line.volume, amount]);
371
+ ev.counterparties.set(key, line);
372
+ }
373
+ }
374
+ recordRefusal(subject, code, atMs) {
375
+ const ev = this.touch(subject, atMs);
376
+ ev.refusals[code] = (ev.refusals[code] ?? 0) + 1;
377
+ }
378
+ recordShadowSpend(subject, atMs) {
379
+ this.touch(subject, atMs).shadowSpends += 1;
380
+ }
381
+ recordDispute(subject, atMs) {
382
+ this.touch(subject, atMs).disputes += 1;
383
+ }
384
+ recordEndorsement(subject, atMs) {
385
+ this.touch(subject, atMs).endorsements += 1;
386
+ }
387
+ merge(canonical, alias) {
388
+ const aliasKey = subjectKey(alias);
389
+ const canonicalKey = subjectKey(canonical);
390
+ const from = this.records.get(aliasKey);
391
+ if (!from || aliasKey === canonicalKey) return;
392
+ const into = this.records.get(canonicalKey) ?? this.touch(canonical, from.firstSeenMs);
393
+ into.firstSeenMs = Math.min(into.firstSeenMs, from.firstSeenMs);
394
+ into.lastSeenMs = Math.max(into.lastSeenMs, from.lastSeenMs);
395
+ into.attempts += from.attempts;
396
+ into.settled += from.settled;
397
+ into.volume = sumDecimal([into.volume, from.volume]);
398
+ for (const [code, count] of Object.entries(from.refusals)) {
399
+ into.refusals[code] = (into.refusals[code] ?? 0) + count;
400
+ }
401
+ into.shadowSpends += from.shadowSpends;
402
+ into.disputes += from.disputes;
403
+ into.endorsements += from.endorsements;
404
+ for (const [peerKey, line] of from.counterparties) {
405
+ if (peerKey === canonicalKey) continue;
406
+ const existing = into.counterparties.get(peerKey) ?? { settled: 0, volume: "0" };
407
+ existing.settled += line.settled;
408
+ existing.volume = sumDecimal([existing.volume, line.volume]);
409
+ into.counterparties.set(peerKey, existing);
410
+ const peer = this.records.get(peerKey);
411
+ const held = peer?.counterparties.get(aliasKey);
412
+ if (peer && held) {
413
+ peer.counterparties.delete(aliasKey);
414
+ const reverse = peer.counterparties.get(canonicalKey) ?? { settled: 0, volume: "0" };
415
+ reverse.settled += held.settled;
416
+ reverse.volume = sumDecimal([reverse.volume, held.volume]);
417
+ peer.counterparties.set(canonicalKey, reverse);
418
+ }
419
+ }
420
+ into.counterparties.delete(aliasKey);
421
+ this.records.delete(aliasKey);
422
+ }
423
+ get(subject) {
424
+ return this.records.get(subjectKey(subject));
425
+ }
426
+ getByKey(key) {
427
+ return this.records.get(key);
428
+ }
429
+ all() {
430
+ return this.records.values();
431
+ }
432
+ get size() {
433
+ return this.records.size;
434
+ }
435
+ /**
436
+ * Insert a fully-formed record verbatim — the hydration primitive a durable
437
+ * store uses to rebuild the working set on open. Bypasses the live counters
438
+ * (the record already holds its accumulated state and timestamps).
439
+ */
440
+ load(record) {
441
+ this.records.set(subjectKey(record.subject), record);
442
+ }
443
+ /** Get-or-create the record for a subject and stamp first/last seen. */
444
+ touch(subject, atMs) {
445
+ const key = subjectKey(subject);
446
+ let record = this.records.get(key);
447
+ if (!record) {
448
+ record = {
449
+ subject: normalizeSubject(subject),
450
+ firstSeenMs: atMs,
451
+ lastSeenMs: atMs,
452
+ attempts: 0,
453
+ settled: 0,
454
+ volume: "0",
455
+ refusals: {},
456
+ shadowSpends: 0,
457
+ disputes: 0,
458
+ endorsements: 0,
459
+ counterparties: /* @__PURE__ */ new Map()
460
+ };
461
+ this.records.set(key, record);
462
+ }
463
+ if (atMs < record.firstSeenMs) record.firstSeenMs = atMs;
464
+ if (atMs > record.lastSeenMs) record.lastSeenMs = atMs;
465
+ return record;
466
+ }
467
+ };
468
+
469
+ // src/intents.ts
470
+ var DEFAULT_CORRELATION_LIMIT = 1e4;
471
+ var InMemoryIntentStore = class {
472
+ constructor(limit = DEFAULT_CORRELATION_LIMIT) {
473
+ this.limit = limit;
474
+ }
475
+ limit;
476
+ intents = /* @__PURE__ */ new Map();
477
+ remember(intentId, facts) {
478
+ if (!this.intents.has(intentId) && this.intents.size >= this.limit) {
479
+ const oldest = this.intents.keys().next().value;
480
+ if (oldest !== void 0) this.intents.delete(oldest);
481
+ }
482
+ this.intents.set(intentId, facts);
483
+ }
484
+ peek(intentId) {
485
+ return this.intents.get(intentId);
486
+ }
487
+ take(intentId) {
488
+ const facts = this.intents.get(intentId);
489
+ if (facts) this.intents.delete(intentId);
490
+ return facts;
491
+ }
492
+ get size() {
493
+ return this.intents.size;
494
+ }
495
+ };
496
+
497
+ // src/scoring.ts
498
+ var DEFAULT_WEIGHTS = {
499
+ settlementReliability: 0.3,
500
+ disputeRate: 0.3,
501
+ volume: 0.15,
502
+ counterpartyQuality: 0.15,
503
+ longevity: 0.1
504
+ };
505
+ var DAY_MS = 864e5;
506
+ var BADNESS = { dispute: 4, shadowSpend: 4, replay: 3, refusal: 1, endorsementCredit: 2 };
507
+ var REPLAY_CODES = /* @__PURE__ */ new Set(["payment_replayed", "decision_replayed"]);
508
+ function clamp(value, lo, hi) {
509
+ return Math.min(hi, Math.max(lo, value));
510
+ }
511
+ function refusalCounts(ev) {
512
+ let replays = 0;
513
+ let others = 0;
514
+ for (const [code, count] of Object.entries(ev.refusals)) {
515
+ if (REPLAY_CODES.has(code)) replays += count;
516
+ else others += count;
517
+ }
518
+ return { replays, others };
519
+ }
520
+ function volumeComponent(ev) {
521
+ return 100 * (1 - Math.exp(-ev.settled / 20));
522
+ }
523
+ function longevityComponent(ev, nowMs) {
524
+ const knownDays = Math.max(0, nowMs - ev.firstSeenMs) / DAY_MS;
525
+ return 100 * Math.min(1, knownDays / 30);
526
+ }
527
+ function disputeComponent(ev) {
528
+ const { replays, others } = refusalCounts(ev);
529
+ const badness = Math.max(
530
+ 0,
531
+ ev.disputes * BADNESS.dispute + ev.shadowSpends * BADNESS.shadowSpend + replays * BADNESS.replay + others * BADNESS.refusal - ev.endorsements * BADNESS.endorsementCredit
532
+ );
533
+ const interactions = Math.max(1, ev.attempts + ev.disputes + ev.endorsements);
534
+ return 100 * Math.max(0, 1 - badness / interactions);
535
+ }
536
+ function reliabilityComponent(ev) {
537
+ if (ev.attempts === 0) return 50;
538
+ return 100 * Math.min(1, ev.settled / ev.attempts);
539
+ }
540
+ function confidence(ev, nowMs) {
541
+ const { replays, others } = refusalCounts(ev);
542
+ const observations = ev.attempts + ev.disputes + ev.endorsements + ev.shadowSpends + replays + others;
543
+ const depth = 1 - Math.exp(-observations / 10);
544
+ const knownDays = Math.max(0, nowMs - ev.firstSeenMs) / DAY_MS;
545
+ const age = 0.4 + 0.6 * Math.min(1, knownDays / 7);
546
+ return clamp(depth * age, 0, 1);
547
+ }
548
+ function blend(components, weights) {
549
+ const total = components.volume * weights.volume + components.longevity * weights.longevity + components.disputeRate * weights.disputeRate + components.counterpartyQuality * weights.counterpartyQuality + components.settlementReliability * weights.settlementReliability;
550
+ return clamp(Math.round(total), 0, 100);
551
+ }
552
+ function blendBase(ev, nowMs, weights) {
553
+ const remaining = weights.volume + weights.longevity + weights.disputeRate + weights.settlementReliability;
554
+ if (remaining <= 0) return 50;
555
+ const total = volumeComponent(ev) * weights.volume + longevityComponent(ev, nowMs) * weights.longevity + disputeComponent(ev) * weights.disputeRate + reliabilityComponent(ev) * weights.settlementReliability;
556
+ return clamp(total / remaining, 0, 100);
557
+ }
558
+
559
+ // src/graph.ts
560
+ var NO_FAULT_GATE_CODES = /* @__PURE__ */ new Set([
561
+ "rate_limited",
562
+ "velocity_exceeded",
563
+ "rails_unavailable",
564
+ "settle_unknown"
565
+ ]);
566
+ var ReputationGraph = class {
567
+ ledger;
568
+ intents;
569
+ weights;
570
+ now;
571
+ /** aliasKey -> canonical subject. Links are derived state (an agent registry
572
+ * or ERC-8004 lookup knows them) — re-assert at boot; merges are idempotent. */
573
+ aliases = /* @__PURE__ */ new Map();
574
+ constructor(options = {}) {
575
+ this.weights = { ...DEFAULT_WEIGHTS, ...options.weights };
576
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
577
+ this.ledger = options.ledger ?? new EvidenceLedger();
578
+ this.intents = options.intents ?? new InMemoryIntentStore(options.correlationLimit ?? DEFAULT_CORRELATION_LIMIT);
579
+ }
580
+ /** Subscribe to a bus. Returns `this` so construction chains. */
581
+ observe(source) {
582
+ source.onEvent((event) => this.ingest(event));
583
+ return this;
584
+ }
585
+ /**
586
+ * Fire-and-forget a port write: bus handlers cannot await, and a rejecting
587
+ * durable write must never become an unhandled rejection (Node kills the
588
+ * process). Failures are the port's to surface — flush() throws them.
589
+ */
590
+ fire(write) {
591
+ try {
592
+ void Promise.resolve(write()).catch(() => void 0);
593
+ } catch {
594
+ }
595
+ }
596
+ /**
597
+ * Declare that `alias` is the same real-world party as `canonical` (the
598
+ * ERC-8004 identity story: one identity, many addresses/ids — an engine
599
+ * agent's ULID and its paying wallet, or a vendor's host and its payTo
600
+ * address). Everything already known about the alias is folded into the
601
+ * canonical subject, and all future evidence and lookups for the alias
602
+ * resolve to it. Idempotent — re-asserting known links at boot is free.
603
+ */
604
+ link(canonical, alias) {
605
+ const target = normalizeSubject(this.resolve(canonical));
606
+ const aliasKey = subjectKey(alias);
607
+ if (subjectKey(target) === aliasKey) return;
608
+ this.aliases.set(aliasKey, target);
609
+ for (const [key, resolved] of this.aliases) {
610
+ if (subjectKey(resolved) === aliasKey) this.aliases.set(key, target);
611
+ }
612
+ this.fire(() => this.ledger.merge(target, alias));
613
+ }
614
+ /** The canonical subject for a possibly-aliased one. */
615
+ resolve(subject) {
616
+ return this.aliases.get(subjectKey(subject)) ?? subject;
617
+ }
618
+ ingest(event) {
619
+ const atMs = event.at.getTime();
620
+ switch (event.type) {
621
+ case "intent.created": {
622
+ this.fire(
623
+ () => this.intents.remember(event.intent.id, {
624
+ agentId: event.intent.agentId,
625
+ host: event.intent.vendor.host,
626
+ amount: event.intent.amount
627
+ })
628
+ );
629
+ return;
630
+ }
631
+ case "decision.made": {
632
+ if (event.decision.outcome !== "allow") return;
633
+ const facts = this.intents.peek(event.decision.intentId);
634
+ if (!facts) return;
635
+ this.fire(() => this.ledger.recordAttempt(this.resolve({ kind: "agent", id: facts.agentId }), atMs));
636
+ this.fire(() => this.ledger.recordAttempt(this.resolve({ kind: "vendor", id: facts.host }), atMs));
637
+ return;
638
+ }
639
+ case "payment.settled": {
640
+ const facts = this.intents.take(event.payment.intentId);
641
+ if (!facts) return;
642
+ const agent = this.resolve({ kind: "agent", id: facts.agentId });
643
+ const vendor = this.resolve({ kind: "vendor", id: facts.host });
644
+ this.fire(() => this.ledger.recordSettlement(agent, vendor, facts.amount, atMs));
645
+ return;
646
+ }
647
+ case "shadow.spend": {
648
+ this.fire(() => this.ledger.recordShadowSpend(this.resolve({ kind: "agent", id: event.agentId }), atMs));
649
+ return;
650
+ }
651
+ case "signature.refused": {
652
+ const agentId = event.agentId;
653
+ if (!agentId) return;
654
+ this.fire(
655
+ () => this.ledger.recordRefusal(this.resolve({ kind: "agent", id: agentId }), event.code, atMs)
656
+ );
657
+ return;
658
+ }
659
+ case "gate.settled": {
660
+ const payer = this.resolve({ kind: "agent", id: event.receipt.payer });
661
+ const recipient = this.resolve({ kind: "vendor", id: event.receipt.payTo });
662
+ this.fire(() => this.ledger.recordAttempt(payer, atMs));
663
+ this.fire(() => this.ledger.recordAttempt(recipient, atMs));
664
+ this.fire(() => this.ledger.recordSettlement(payer, recipient, event.receipt.amount, atMs));
665
+ return;
666
+ }
667
+ case "gate.refused": {
668
+ if (!event.payer || NO_FAULT_GATE_CODES.has(event.code)) return;
669
+ const payer = this.resolve({ kind: "agent", id: event.payer });
670
+ this.fire(() => this.ledger.recordAttempt(payer, atMs));
671
+ this.fire(() => this.ledger.recordRefusal(payer, event.code, atMs));
672
+ return;
673
+ }
674
+ // signature.released and gate.quoted carry no evidence the engine-side
675
+ // events don't already: released duplicates the allow that preceded it,
676
+ // and a quote names no payer.
677
+ case "signature.released":
678
+ case "gate.quoted":
679
+ return;
680
+ }
681
+ }
682
+ /**
683
+ * Agent-side alternative to observing the engine bus: feed the SDK guard's
684
+ * onReceipt straight in. Connect events OR receipts per side, not both —
685
+ * a receipt restates the decision/settlement the bus already carried.
686
+ */
687
+ ingestReceipt(receipt) {
688
+ if (receipt.outcome !== "allow") return;
689
+ const atMs = receipt.createdAt.getTime();
690
+ const agent = this.resolve({ kind: "agent", id: receipt.agentId });
691
+ const vendor = this.resolve({ kind: "vendor", id: receipt.vendorHost });
692
+ this.fire(() => this.ledger.recordAttempt(agent, atMs));
693
+ this.fire(() => this.ledger.recordAttempt(vendor, atMs));
694
+ if (receipt.settlement?.txHash) {
695
+ this.fire(() => this.ledger.recordSettlement(agent, vendor, receipt.amount, atMs));
696
+ }
697
+ }
698
+ /** Record an out-of-band dispute or endorsement against a subject. */
699
+ report(input) {
700
+ const atMs = (input.at ?? this.now()).getTime();
701
+ const subject = this.resolve(input.subject);
702
+ if (input.kind === "dispute") this.fire(() => this.ledger.recordDispute(subject, atMs));
703
+ else this.fire(() => this.ledger.recordEndorsement(subject, atMs));
704
+ }
705
+ /** Await any pending durable writes (no-op for in-memory stores). */
706
+ async flush() {
707
+ await this.ledger.flush?.();
708
+ await this.intents.flush?.();
709
+ }
710
+ /** The score for one subject, or undefined when nothing is known (fairness).
711
+ * Aliased subjects resolve to their canonical identity — a linked wallet
712
+ * answers with the merged history, on both sides of the wire. */
713
+ score(subject) {
714
+ const ev = this.ledger.get(this.resolve(subject));
715
+ return ev && this.scoreOf(ev);
716
+ }
717
+ /** All known scores, best first. */
718
+ scores(kind) {
719
+ const out = [];
720
+ for (const ev of this.ledger.all()) {
721
+ if (kind && ev.subject.kind !== kind) continue;
722
+ out.push(this.scoreOf(ev));
723
+ }
724
+ return out.sort((a, b) => b.score - a.score);
725
+ }
726
+ /** The score plus the raw evidence behind it. */
727
+ explain(subject) {
728
+ const ev = this.ledger.get(this.resolve(subject));
729
+ if (!ev) return void 0;
730
+ return {
731
+ score: this.scoreOf(ev),
732
+ weights: this.weights,
733
+ evidence: {
734
+ attempts: ev.attempts,
735
+ settled: ev.settled,
736
+ volume: ev.volume,
737
+ refusals: { ...ev.refusals },
738
+ shadowSpends: ev.shadowSpends,
739
+ disputes: ev.disputes,
740
+ endorsements: ev.endorsements,
741
+ firstSeen: new Date(ev.firstSeenMs),
742
+ lastSeen: new Date(ev.lastSeenMs),
743
+ counterparties: [...ev.counterparties.entries()].map(([key, line]) => {
744
+ const [kind, ...id] = key.split(":");
745
+ return {
746
+ subject: { kind, id: id.join(":") },
747
+ settled: line.settled,
748
+ volume: line.volume
749
+ };
750
+ })
751
+ }
752
+ };
753
+ }
754
+ /**
755
+ * Push every vendor score that clears the confidence floor into the engine's
756
+ * spend store (or any sink). Returns what was pushed. Low-confidence scores
757
+ * are withheld so `vendorReputationLt` keeps treating thin histories as
758
+ * unknown — the evaluator's no-data-no-trigger rule stays intact end to end.
759
+ */
760
+ async syncVendors(sink, options = {}) {
761
+ const minConfidence = options.minConfidence ?? 0.3;
762
+ const pushed = [];
763
+ for (const ev of this.ledger.all()) {
764
+ if (ev.subject.kind !== "vendor") continue;
765
+ const score = this.scoreOf(ev);
766
+ if (score.confidence < minConfidence) continue;
767
+ await sink.setVendorReputation(ev.subject.id, score.score);
768
+ pushed.push({ host: ev.subject.id, score: score.score, confidence: score.confidence });
769
+ }
770
+ return pushed;
771
+ }
772
+ /** Number of subjects with evidence. */
773
+ subjects() {
774
+ return this.ledger.size;
775
+ }
776
+ scoreOf(ev) {
777
+ const nowMs = this.now().getTime();
778
+ const components = {
779
+ volume: volumeComponent(ev),
780
+ longevity: longevityComponent(ev, nowMs),
781
+ disputeRate: disputeComponent(ev),
782
+ counterpartyQuality: this.counterpartyQuality(ev, nowMs),
783
+ settlementReliability: reliabilityComponent(ev)
784
+ };
785
+ return ReputationScore.parse({
786
+ subject: ev.subject,
787
+ score: blend(components, this.weights),
788
+ components,
789
+ confidence: confidence(ev, nowMs),
790
+ asOf: new Date(nowMs)
791
+ });
792
+ }
793
+ /** One-hop mean of the counterparties' base scores; neutral 50 when alone. */
794
+ counterpartyQuality(ev, nowMs) {
795
+ if (ev.counterparties.size === 0) return 50;
796
+ let total = 0;
797
+ for (const key of ev.counterparties.keys()) {
798
+ const other = this.ledger.getByKey(key);
799
+ total += other ? blendBase(other, nowMs, this.weights) : 50;
800
+ }
801
+ return total / ev.counterparties.size;
802
+ }
803
+ };
804
+ function payerCheck(graph, options = {}) {
805
+ const denyBelow = options.denyBelow ?? 40;
806
+ const minConfidence = options.minConfidence ?? 0.3;
807
+ return (payer) => {
808
+ const score = graph.score({ kind: "agent", id: payer });
809
+ if (!score || score.confidence < minConfidence) return void 0;
810
+ if (score.score >= denyBelow) return void 0;
811
+ return `payer reputation ${score.score} is below this gate's floor of ${denyBelow}`;
812
+ };
813
+ }
814
+
815
+ // src/server.ts
816
+ import { fileURLToPath } from "url";
817
+ import { realpathSync } from "fs";
818
+ import Fastify from "fastify";
819
+ import { z as z2 } from "zod";
820
+ var ReportInput = z2.object({
821
+ subject: ReputationSubject,
822
+ kind: z2.enum(["dispute", "endorsement"]),
823
+ at: z2.coerce.date().optional(),
824
+ note: z2.string().max(500).optional()
825
+ });
826
+ var ScoresQuery = z2.object({ kind: z2.enum(["agent", "vendor"]).optional() });
827
+ var LinkInput = z2.object({
828
+ canonical: ReputationSubject,
829
+ alias: ReputationSubject
830
+ }).refine((l) => l.canonical.kind === l.alias.kind, {
831
+ message: "canonical and alias must be the same kind"
832
+ });
833
+ function buildGraphServer(graph = new ReputationGraph()) {
834
+ const app = Fastify({ logger: false });
835
+ app.setErrorHandler((err, _req, reply) => {
836
+ if (err instanceof z2.ZodError) {
837
+ return reply.status(400).send({ error: "validation_error", issues: err.issues });
838
+ }
839
+ const message = err instanceof Error ? err.message : String(err);
840
+ return reply.status(500).send({ error: "internal_error", message });
841
+ });
842
+ app.get("/health", () => ({ status: "ok", subjects: graph.subjects() }));
843
+ app.post("/v1/events", async (req) => {
844
+ const events = z2.union([ReinEvent.transform((e) => [e]), z2.array(ReinEvent)]).parse(req.body);
845
+ for (const event of events) graph.ingest(event);
846
+ await graph.flush();
847
+ return { ingested: events.length };
848
+ });
849
+ app.post("/v1/reports", async (req) => {
850
+ const input = ReportInput.parse(req.body);
851
+ graph.report(input);
852
+ await graph.flush();
853
+ return graph.score(input.subject);
854
+ });
855
+ app.post("/v1/links", async (req) => {
856
+ const links = z2.union([LinkInput.transform((l) => [l]), z2.array(LinkInput)]).parse(req.body);
857
+ for (const link of links) graph.link(link.canonical, link.alias);
858
+ await graph.flush();
859
+ return { linked: links.length };
860
+ });
861
+ app.get("/v1/scores", (req) => {
862
+ const { kind } = ScoresQuery.parse(req.query);
863
+ return graph.scores(kind);
864
+ });
865
+ app.get("/v1/scores/:kind/:id", (req, reply) => {
866
+ const subject = ReputationSubject.parse(req.params);
867
+ const explanation = graph.explain(subject);
868
+ if (!explanation) {
869
+ return reply.status(404).send({ error: "not_found", message: `no evidence for ${subject.kind} ${subject.id}` });
870
+ }
871
+ return explanation;
872
+ });
873
+ return app;
874
+ }
875
+ function isMainModule() {
876
+ if (!process.argv[1]) return false;
877
+ try {
878
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
879
+ } catch {
880
+ return false;
881
+ }
882
+ }
883
+ if (isMainModule()) {
884
+ const port = Number(process.env.PORT ?? 8788);
885
+ const host = process.env.HOST ?? "0.0.0.0";
886
+ const app = buildGraphServer();
887
+ app.listen({ port, host }).then(() => console.log(`[rein] graph listening on http://${host}:${port}`)).catch((err) => {
888
+ console.error(err);
889
+ process.exit(1);
890
+ });
891
+ }
892
+ export {
893
+ DEFAULT_CORRELATION_LIMIT,
894
+ DEFAULT_WEIGHTS,
895
+ EvidenceLedger,
896
+ InMemoryIntentStore,
897
+ ReputationGraph,
898
+ blend,
899
+ blendBase,
900
+ buildGraphServer,
901
+ confidence,
902
+ disputeComponent,
903
+ longevityComponent,
904
+ normalizeSubject,
905
+ payerCheck,
906
+ reliabilityComponent,
907
+ subjectKey,
908
+ volumeComponent
909
+ };
910
+ //# sourceMappingURL=index.js.map