@reinconsole/graph 0.1.1 → 0.3.0-rc.1

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 CHANGED
@@ -1,8 +1,101 @@
1
- // ../../packages/core/dist/index.js
1
+ // ../../packages/core/dist/chunk-6H2LDEB7.js
2
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"');
3
+ var ULID_BODY = "[0-9A-HJKMNP-TV-Z]{26}";
4
+ function prefixedId(prefix) {
5
+ return z.string().regex(new RegExp(`^${prefix}_${ULID_BODY}$`), `expected a "${prefix}_" prefixed id`);
6
+ }
7
+ var OrgId = prefixedId("org");
8
+ var AgentId = prefixedId("agt");
9
+ var PolicyId = prefixedId("pol");
10
+ var IntentId = prefixedId("int");
11
+ var DecisionId = prefixedId("dec");
12
+ var ReceiptId = prefixedId("rcp");
13
+ var SessionId = prefixedId("ses");
14
+ var GateReceiptId = prefixedId("grc");
15
+ var ApiKeyId = prefixedId("key");
16
+ var ApproverKeyId = prefixedId("apk");
17
+ var ApiKeyScope = z.enum(["read", "evaluate", "approve", "report", "admin"]);
18
+ var ApiKey = z.object({
19
+ id: ApiKeyId,
20
+ name: z.string().min(1).max(200),
21
+ scopes: z.array(ApiKeyScope).min(1),
22
+ /**
23
+ * The org this key acts inside. ABSENT means unscoped — an operator key that
24
+ * reads and writes across every org, which is what a self-hosted single-org
25
+ * deployment and every env-seeded boot secret have always been. That is the
26
+ * compatibility rule the whole tenancy design rests on: adding tenancy
27
+ * changed no existing key's behaviour, because no existing key has this
28
+ * field.
29
+ *
30
+ * Present, it is a hard confinement, not a default: the key cannot read,
31
+ * spend, approve or govern outside the org, and a write it submits is
32
+ * stamped with this org rather than the one the body asked for.
33
+ */
34
+ orgId: OrgId.optional(),
35
+ /**
36
+ * Narrow an org-scoped key further, to specific agents — what an agent
37
+ * runtime should carry, so a stolen key spends one agent's budget rather
38
+ * than the org's. Only meaningful alongside {@link ApiKey.orgId}: agent
39
+ * narrowing without an org is refused at issuance, because the scope it
40
+ * would describe has no org to resolve policies and approvers in.
41
+ *
42
+ * Capped at 64: a list long enough to need more is a fleet, and a fleet is
43
+ * what the org scope is for.
44
+ */
45
+ agentIds: z.array(AgentId).max(64).optional(),
46
+ createdAt: z.coerce.date(),
47
+ /** Last successful authentication, for spotting keys that can be retired. */
48
+ lastUsedAt: z.coerce.date().optional(),
49
+ /** Set by revoke(); a revoked key never authenticates again. */
50
+ revokedAt: z.coerce.date().optional(),
51
+ /** Set by rotate(); with `previousSecretExpiresAt` it dates the overlap. */
52
+ rotatedAt: z.coerce.date().optional()
53
+ });
54
+ var ApiKeyRecord = ApiKey.extend({
55
+ secretHash: z.string(),
56
+ previousSecretHash: z.string().optional(),
57
+ previousSecretExpiresAt: z.coerce.date().optional()
58
+ });
59
+ function toPublicApiKey(record) {
60
+ const { secretHash, previousSecretHash, previousSecretExpiresAt, ...pub } = record;
61
+ return pub;
62
+ }
63
+ var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
64
+ var TIME_LEN = 10;
65
+ var RANDOM_LEN = 16;
66
+ function randomBytes(length) {
67
+ const bytes = new Uint8Array(length);
68
+ globalThis.crypto.getRandomValues(bytes);
69
+ return bytes;
70
+ }
71
+ function encodeTime(now) {
72
+ let time = now;
73
+ let out = "";
74
+ for (let i = TIME_LEN - 1; i >= 0; i--) {
75
+ out = ENCODING.charAt(time % 32) + out;
76
+ time = Math.floor(time / 32);
77
+ }
78
+ return out;
79
+ }
80
+ function encodeRandom() {
81
+ let out = "";
82
+ for (const byte of randomBytes(RANDOM_LEN)) {
83
+ out += ENCODING.charAt(byte % 32);
84
+ }
85
+ return out;
86
+ }
87
+ function ulid(seedTime = Date.now()) {
88
+ return encodeTime(seedTime) + encodeRandom();
89
+ }
90
+ function newId(prefix) {
91
+ return `${prefix}_${ulid()}`;
92
+ }
93
+
94
+ // ../../packages/core/dist/index.js
95
+ import { z as z2 } from "zod";
96
+ var Chain = z2.enum(["base", "solana", "polygon", "bnb"]);
97
+ var Asset = z2.enum(["USDC", "USDT", "EURC"]);
98
+ var DecimalString = z2.string().regex(/^\d+(\.\d+)?$/, 'must be a non-negative decimal string, e.g. "5.00"');
6
99
  var DECIMAL_RE = /^\d+(\.\d+)?$/;
7
100
  function isValidDecimal(value) {
8
101
  return DECIMAL_RE.test(value);
@@ -41,34 +134,25 @@ function sumDecimal(values) {
41
134
  for (const v of values) total += toScaled(v, scale);
42
135
  return fromScaled(total, scale);
43
136
  }
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({
137
+ var EnforcementMode = z2.enum(["observed", "sdk", "session-key"]);
138
+ var AgentWallet = z2.object({
58
139
  chain: Chain,
59
- address: z.string().min(1),
140
+ address: z2.string().min(1),
60
141
  mode: EnforcementMode
61
142
  });
62
- var AgentStatus = z.enum(["active", "frozen"]);
63
- var Agent = z.object({
143
+ var AgentStatus = z2.enum(["active", "frozen"]);
144
+ var AgentLabel = z2.string().max(40).regex(/^[a-z0-9][a-z0-9-]*$/, 'label must be a lowercase slug like "research" or "prod-trading"');
145
+ var Agent = z2.object({
64
146
  id: AgentId,
65
147
  orgId: OrgId,
66
- name: z.string().min(1).max(200),
148
+ name: z2.string().min(1).max(200),
67
149
  /** On-chain ERC-8004 identity, if the agent is registered. */
68
- erc8004Id: z.string().optional(),
69
- wallets: z.array(AgentWallet).default([]),
150
+ erc8004Id: z2.string().optional(),
151
+ /** Semantic grouping labels — the targets of policy `appliesTo.labels`. */
152
+ labels: z2.array(AgentLabel).max(16).refine((ls) => new Set(ls).size === ls.length, { message: "labels must be unique" }).default([]),
153
+ wallets: z2.array(AgentWallet).default([]),
70
154
  status: AgentStatus.default("active"),
71
- createdAt: z.coerce.date()
155
+ createdAt: z2.coerce.date()
72
156
  });
73
157
  var REGISTRY_PATTERN = /^0x[0-9a-fA-F]{40}$/;
74
158
  var ID_PATTERN = /^eip155:(\d+):(0x[0-9a-fA-F]{40})\/(\d+)$/;
@@ -95,33 +179,33 @@ function parseErc8004Id(value) {
95
179
  tokenId: BigInt(match[3])
96
180
  };
97
181
  }
98
- var Erc8004Id = z.string().refine((value) => parseErc8004Id(value) !== void 0, {
182
+ var Erc8004Id = z2.string().refine((value) => parseErc8004Id(value) !== void 0, {
99
183
  message: 'expected "eip155:{chainId}:{registry}/{tokenId}"'
100
184
  });
101
- var Vendor = z.object({
102
- host: z.string().min(1),
103
- address: z.string().min(1),
104
- erc8004Id: z.string().optional()
185
+ var Vendor = z2.object({
186
+ host: z2.string().min(1),
187
+ address: z2.string().min(1),
188
+ erc8004Id: z2.string().optional()
105
189
  });
106
- var TaskContext = z.object({
107
- taskId: z.string().optional(),
108
- parentRunId: z.string().optional(),
109
- purpose: z.string().max(500).optional()
190
+ var TaskContext = z2.object({
191
+ taskId: z2.string().optional(),
192
+ parentRunId: z2.string().optional(),
193
+ purpose: z2.string().max(500).optional()
110
194
  });
111
- var PaymentIntent = z.object({
195
+ var PaymentIntent = z2.object({
112
196
  id: IntentId,
113
197
  agentId: AgentId,
114
198
  vendor: Vendor,
115
- resource: z.string(),
199
+ resource: z2.string(),
116
200
  amount: DecimalString,
117
201
  asset: Asset,
118
202
  chain: Chain,
119
203
  taskContext: TaskContext.default({}),
120
- nonce: z.string().min(1),
121
- createdAt: z.coerce.date()
204
+ nonce: z2.string().min(1),
205
+ createdAt: z2.coerce.date()
122
206
  });
123
- var DecisionOutcome = z.enum(["allow", "deny", "escalate"]);
124
- var Decision = z.object({
207
+ var DecisionOutcome = z2.enum(["allow", "deny", "escalate"]);
208
+ var Decision = z2.object({
125
209
  id: DecisionId,
126
210
  intentId: IntentId,
127
211
  /**
@@ -130,211 +214,428 @@ var Decision = z.object({
130
214
  * chain — so a signer can verify an {intent, decision} pair offline as a
131
215
  * self-contained spend voucher, not just a reference by id.
132
216
  */
133
- intentHash: z.string(),
217
+ intentHash: z2.string(),
134
218
  outcome: DecisionOutcome,
135
219
  /** Ids of the rules that fired, for explainability. */
136
- matchedRules: z.array(z.string()).default([]),
220
+ matchedRules: z2.array(z2.string()).default([]),
137
221
  /** Human-readable explanation surfaced in the dashboard. */
138
- reason: z.string().optional(),
139
- policyId: z.string(),
140
- policyVersion: z.string(),
222
+ reason: z2.string().optional(),
223
+ policyId: z2.string(),
224
+ policyVersion: z2.string(),
141
225
  /** Hash of the previous decision in the chain (tamper-evident log). */
142
- prevHash: z.string(),
226
+ prevHash: z2.string(),
143
227
  /** Hash of this decision's canonical content. */
144
- hash: z.string(),
228
+ hash: z2.string(),
145
229
  /** Service signing-key signature over `hash`. */
146
- signature: z.string(),
147
- latencyMs: z.number().nonnegative(),
148
- decidedAt: z.coerce.date()
230
+ signature: z2.string(),
231
+ latencyMs: z2.number().nonnegative(),
232
+ decidedAt: z2.coerce.date()
149
233
  });
150
- var Session = z.object({
234
+ var AttributedDecision = Decision.extend({ agentId: AgentId.optional() });
235
+ var Session = z2.object({
151
236
  id: SessionId,
152
237
  agentId: AgentId,
153
238
  /** sha256 hex of the bearer token. */
154
- tokenHash: z.string(),
239
+ tokenHash: z2.string(),
155
240
  /** Cumulative ceiling across the session's lifetime. Absent = uncapped. */
156
241
  capAmount: DecimalString.optional(),
157
242
  /** Ceiling per individual signature. Absent = uncapped. */
158
243
  maxPerPayment: DecimalString.optional(),
159
- expiresAt: z.coerce.date(),
160
- createdAt: z.coerce.date(),
161
- revokedAt: z.coerce.date().optional()
244
+ expiresAt: z2.coerce.date(),
245
+ createdAt: z2.coerce.date(),
246
+ revokedAt: z2.coerce.date().optional()
162
247
  });
163
- var SettledPayment = z.object({
248
+ var SettledPayment = z2.object({
164
249
  intentId: IntentId,
165
- txHash: z.string(),
250
+ txHash: z2.string(),
166
251
  chain: Chain,
167
- blockNumber: z.coerce.bigint(),
168
- facilitator: z.string().optional(),
252
+ blockNumber: z2.coerce.bigint(),
253
+ facilitator: z2.string().optional(),
169
254
  feePaid: DecimalString.optional(),
170
- confirmedAt: z.coerce.date()
255
+ confirmedAt: z2.coerce.date()
171
256
  });
172
- var ReceiptSettlement = z.object({
173
- txHash: z.string().optional(),
174
- networkId: z.string().optional(),
257
+ var SettlementReport = z2.object({
258
+ intentId: IntentId,
259
+ /** The settling transaction, when the observer saw one. */
260
+ txHash: z2.string().optional(),
261
+ chain: Chain.optional(),
262
+ /** The amount as SETTLED, when known — may differ from the amount allowed. */
263
+ amount: DecimalString.optional(),
264
+ /** Free-form observer tag, e.g. "indexer", "guard", "facilitator". */
265
+ source: z2.string().min(1).max(64).optional(),
266
+ confirmedAt: z2.coerce.date()
267
+ });
268
+ var ReceiptSettlement = z2.object({
269
+ txHash: z2.string().optional(),
270
+ networkId: z2.string().optional(),
175
271
  /** Raw header payload, kept for forensics until the indexer confirms. */
176
- raw: z.string().optional()
272
+ raw: z2.string().optional()
177
273
  });
178
- var Receipt = z.object({
274
+ var Receipt = z2.object({
179
275
  id: ReceiptId,
180
276
  agentId: AgentId,
181
277
  intentId: IntentId,
182
278
  decisionId: DecisionId,
183
279
  outcome: DecisionOutcome,
184
280
  /** The URL the agent actually fetched (the paywalled resource). */
185
- url: z.string(),
186
- method: z.string().default("GET"),
187
- vendorHost: z.string(),
281
+ url: z2.string(),
282
+ method: z2.string().default("GET"),
283
+ vendorHost: z2.string(),
188
284
  amount: DecimalString,
189
285
  asset: Asset,
190
286
  chain: Chain,
191
287
  taskContext: TaskContext.default({}),
192
288
  /** Human-readable explanation copied from the decision. */
193
- reason: z.string().optional(),
289
+ reason: z2.string().optional(),
194
290
  /** Present only once a payment was made and the vendor confirmed it. */
195
291
  settlement: ReceiptSettlement.optional(),
196
- createdAt: z.coerce.date()
292
+ createdAt: z2.coerce.date()
197
293
  });
198
- var GateReceipt = z.object({
294
+ var GateReceipt = z2.object({
199
295
  id: GateReceiptId,
200
- at: z.coerce.date(),
296
+ at: z2.coerce.date(),
201
297
  /** The route pattern that priced this request, e.g. "/api/reports/*". */
202
- route: z.string().min(1),
298
+ route: z2.string().min(1),
203
299
  /** The concrete resource paid for (URL path actually requested). */
204
- resource: z.string().min(1),
205
- method: z.string().min(1),
300
+ resource: z2.string().min(1),
301
+ method: z2.string().min(1),
206
302
  /** The paying wallet address, as asserted by the settled payment. */
207
- payer: z.string().min(1),
208
- payTo: z.string().min(1),
303
+ payer: z2.string().min(1),
304
+ payTo: z2.string().min(1),
209
305
  /** Human-unit decimal amount, e.g. "0.05". */
210
306
  amount: DecimalString,
211
307
  /** 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"),
308
+ amountAtomic: z2.string().regex(/^\d+$/, "atomic amount must be an integer string"),
213
309
  /** Token symbol or contract address, exactly as quoted in the requirement. */
214
- asset: z.string().min(1),
215
- network: z.string().min(1),
310
+ asset: z2.string().min(1),
311
+ network: z2.string().min(1),
216
312
  /** Settlement transaction hash (mock ledger or on-chain). */
217
- transaction: z.string().min(1)
313
+ transaction: z2.string().min(1)
218
314
  });
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({
315
+ var Window = z2.string().regex(/^\d+[smhd]$/, 'window must be like "30s", "15m", "1h", or "7d"');
316
+ var Multiplier = z2.string().regex(/^\d+(\.\d+)?x$/, 'multiplier must be like "3x"');
317
+ var Condition = z2.object({
222
318
  /** Per-transaction amount exceeds this value. */
223
319
  amountGt: DecimalString.optional(),
224
320
  /** Sum of spend in a rolling window exceeds `gt`. */
225
- rollingSum: z.object({ window: Window, gt: DecimalString }).optional(),
321
+ rollingSum: z2.object({ window: Window, gt: DecimalString }).optional(),
226
322
  /** Transaction count in a rolling window exceeds `gt`. */
227
- txCount: z.object({ window: Window, gt: z.number().int().nonnegative() }).optional(),
323
+ txCount: z2.object({ window: Window, gt: z2.number().int().nonnegative() }).optional(),
228
324
  /** Vendor host matches one of these patterns (supports `*` globs). */
229
- vendorHostIn: z.array(z.string()).optional(),
325
+ vendorHostIn: z2.array(z2.string()).optional(),
326
+ /**
327
+ * Resource PATH matches one of these patterns (supports `*` globs, e.g.
328
+ * "/v1/reports/*"). A full-URL resource is reduced to its pathname before
329
+ * matching (see `resourcePathOf`), so the same pattern works whether the
330
+ * vendor declares "https://api.x.com/v1/answer" or "/v1/answer". Pair
331
+ * with `vendorHostIn` to scope a path rule to specific vendors.
332
+ */
333
+ resourceIn: z2.array(z2.string()).optional(),
230
334
  /** First time Rein has seen this vendor for the agent. */
231
- vendorFirstSeen: z.boolean().optional(),
335
+ vendorFirstSeen: z2.boolean().optional(),
232
336
  /** Vendor reputation score is below this threshold (Phase 3 hook). */
233
- vendorReputationLt: z.number().min(0).max(100).optional(),
337
+ vendorReputationLt: z2.number().min(0).max(100).optional(),
234
338
  /** Amount is more than `gt`-times the observed median for this resource. */
235
- amountVsResourceMedian: z.object({ gt: Multiplier }).optional()
339
+ amountVsResourceMedian: z2.object({ gt: Multiplier }).optional(),
340
+ /**
341
+ * Cumulative spend attributed to THIS intent's `taskContext.taskId`
342
+ * (prior spend on the task plus this payment) exceeds `gt`. Scopes a
343
+ * budget to one unit of work rather than to a window: a research run that
344
+ * is meant to cost a dollar cannot quietly cost fifty, however slowly.
345
+ *
346
+ * An intent carrying no `taskId` cannot be attributed to a task, so the
347
+ * predicate never triggers for one. Requiring attribution is a separate,
348
+ * deliberate rule (deny on `taskIdMissing`), not a side effect of setting
349
+ * a budget — otherwise every untagged probe payment would trip every
350
+ * task budget in the policy.
351
+ */
352
+ taskBudget: z2.object({ gt: DecimalString }).optional(),
353
+ /** The intent carries no `taskContext.taskId` (or an empty one). */
354
+ taskIdMissing: z2.boolean().optional()
236
355
  }).refine((c) => Object.values(c).some((v) => v !== void 0), {
237
356
  message: "a condition must specify at least one predicate"
238
357
  });
239
- var Rule = z.object({
240
- id: z.string().min(1),
358
+ var Breaker = z2.object({
359
+ id: z2.string().min(1),
360
+ /** The trailing span the tripwires measure over. */
361
+ window: Window,
362
+ /**
363
+ * Trip when the window would hold MORE than this many transactions.
364
+ * `10` permits ten in the window and escalates the eleventh.
365
+ */
366
+ txCount: z2.number().int().positive().optional(),
367
+ /**
368
+ * Trip when the window's spend would exceed this total. `"50.00"` permits
369
+ * a window totalling exactly 50.00 and escalates the payment that would
370
+ * carry it past. Prospective, like every other spend predicate: the
371
+ * payment that would breach the cap is the one that escalates, rather
372
+ * than the innocent one after it.
373
+ */
374
+ valueCap: DecimalString.optional()
375
+ }).refine((b) => b.txCount !== void 0 || b.valueCap !== void 0, {
376
+ message: "a breaker must specify at least one of txCount / valueCap"
377
+ });
378
+ var Rule = z2.object({
379
+ id: z2.string().min(1),
241
380
  allow: Condition.optional(),
242
381
  deny: Condition.optional(),
243
382
  escalate: Condition.optional()
244
383
  }).refine((r) => [r.allow, r.deny, r.escalate].filter((v) => v !== void 0).length === 1, {
245
384
  message: "a rule must specify exactly one of allow / deny / escalate"
246
385
  });
247
- var PolicyDefault = z.enum(["allow", "deny"]);
248
- var AppliesTo = z.object({
386
+ var PolicyDefault = z2.enum(["allow", "deny"]);
387
+ var AppliesTo = z2.object({
249
388
  /** Agent id patterns (supports `*` globs, e.g. "agt_research_*"). */
250
- agents: z.array(z.string()).optional(),
251
- chains: z.array(Chain).optional()
389
+ agents: z2.array(z2.string()).optional(),
390
+ /**
391
+ * Agent label patterns (supports `*` globs) — semantic targeting. Matches
392
+ * when the agent carries at least one label matching any pattern. Requires
393
+ * the agent DOCUMENT: an intent from an unregistered agent never matches a
394
+ * labels-targeted policy (and so fails closed when no other policy applies).
395
+ */
396
+ labels: z2.array(z2.string()).optional(),
397
+ chains: z2.array(Chain).optional()
252
398
  });
253
- var Escalation = z.object({
254
- approvers: z.array(z.string()).default([]),
399
+ var Escalation = z2.object({
400
+ approvers: z2.array(z2.string()).default([]),
255
401
  timeoutAction: PolicyDefault.default("deny"),
256
- timeoutMin: z.number().int().positive().default(15)
402
+ timeoutMin: z2.number().int().positive().default(15)
257
403
  });
258
- var Policy = z.object({
259
- policyId: z.string().min(1),
260
- version: z.string().default("1"),
404
+ var Policy = z2.object({
405
+ policyId: z2.string().min(1),
406
+ version: z2.string().default("1"),
407
+ /**
408
+ * The org this policy governs. ABSENT means GLOBAL — it is considered for
409
+ * every agent, which is what every policy written before tenancy existed is
410
+ * and stays.
411
+ *
412
+ * This is separate from {@link AppliesTo} on purpose, and it has to be:
413
+ * `appliesTo: {}` matches every agent, so a tenant's policy carrying the
414
+ * default targeting would otherwise govern other tenants' agents the moment
415
+ * it was written. Org scoping is applied BEFORE targeting — a policy whose
416
+ * `orgId` is not the agent's org is not a candidate at all.
417
+ */
418
+ orgId: OrgId.optional(),
261
419
  appliesTo: AppliesTo.default({}),
262
- rules: z.array(Rule).default([]),
420
+ rules: z2.array(Rule).default([]),
421
+ /**
422
+ * Behavioral breakers evaluated alongside the rules. A tripped breaker
423
+ * escalates at the same precedence as an `escalate` rule — so an explicit
424
+ * DENY still wins, and an ALLOW rule cannot wave past a tripped breaker.
425
+ */
426
+ breakers: z2.array(Breaker).default([]),
263
427
  default: PolicyDefault.default("deny"),
264
428
  /** Below this amount, fail-open is permitted during a policy-service outage. */
265
429
  denyFloor: DecimalString.default("0.05"),
266
430
  escalation: Escalation.optional()
267
431
  });
268
- var ReputationSubject = z.object({
269
- kind: z.enum(["agent", "vendor"]),
270
- id: z.string()
432
+ var LivenessSource = z2.enum([
433
+ /** It submitted an intent — ANY outcome, including a denial. */
434
+ "intent",
435
+ /** It reported in explicitly, having nothing to buy. */
436
+ "heartbeat"
437
+ ]);
438
+ var DEFAULT_LIVENESS_GRACE_MS = 6e4;
439
+ var LivenessExpectation = z2.object({
440
+ agentId: AgentId,
441
+ /** Longest silence still considered normal, e.g. '15m'. */
442
+ interval: Window,
443
+ /** Extra slack past `interval` before the silence is an alarm. */
444
+ graceMs: z2.number().int().nonnegative().default(DEFAULT_LIVENESS_GRACE_MS),
445
+ /**
446
+ * When watching began. An agent that has NEVER been seen is measured from
447
+ * here, so a watch on an agent that never starts does raise an alarm — a
448
+ * detector that never came up is exactly as dead as one that stopped.
449
+ */
450
+ since: z2.coerce.date(),
451
+ /** What this agent is supposed to be doing, shown beside the alarm. */
452
+ note: z2.string().max(200).optional()
453
+ });
454
+ var LivenessWatchInput = LivenessExpectation.omit({ since: true }).extend({
455
+ graceMs: z2.number().int().nonnegative().optional()
456
+ });
457
+ var Heartbeat = z2.object({
458
+ agentId: AgentId,
459
+ /** When the agent was alive. Defaults to arrival time. */
460
+ at: z2.coerce.date().optional(),
461
+ /** Free-form note from the reporter, e.g. "poll cycle 41 ok". */
462
+ note: z2.string().max(200).optional()
463
+ });
464
+ var LivenessStatus = z2.enum(["alive", "late", "missing", "unknown"]);
465
+ var ApprovalVerdict = z2.enum(["approve", "reject"]);
466
+ var ApprovalStatus = z2.enum(["pending", "approved", "rejected", "expired"]);
467
+ var ApproverKey = z2.object({
468
+ id: ApproverKeyId,
469
+ orgId: OrgId,
470
+ name: z2.string().min(1).max(200),
471
+ /** ed25519 public key, SPKI PEM — the same encoding as the decision log's. */
472
+ publicKey: z2.string().min(1),
473
+ algorithm: z2.literal("ed25519").default("ed25519"),
474
+ createdAt: z2.coerce.date(),
475
+ revokedAt: z2.coerce.date().optional()
476
+ });
477
+ var ApprovalRequest = z2.object({
478
+ /** The `escalate` decision this request answers (primary key). */
479
+ decisionId: DecisionId,
480
+ intentId: IntentId,
481
+ /** Binds the approval to the exact transfer — the signed bytes commit to it. */
482
+ intentHash: z2.string(),
483
+ agentId: AgentId,
484
+ /**
485
+ * The org the escalating agent belongs to, stamped when the request is
486
+ * parked. ABSENT means the agent was never registered (or the request
487
+ * predates tenancy), and such a request is answerable only by an unscoped
488
+ * operator — there is no org to check an approver against, and guessing one
489
+ * is how a cross-tenant approval happens.
490
+ */
491
+ orgId: OrgId.optional(),
492
+ vendorHost: z2.string(),
493
+ resource: z2.string(),
494
+ amount: DecimalString,
495
+ asset: Asset,
496
+ chain: Chain,
497
+ /**
498
+ * Task attribution copied from the intent. Carried so an approved payment
499
+ * still counts against its `taskBudget` — an escalation that lost its task
500
+ * id on the way through would make the budget silently under-count exactly
501
+ * the payments a human had to look at.
502
+ */
503
+ taskId: z2.string().optional(),
504
+ /** Why policy escalated, verbatim from the decision. */
505
+ reason: z2.string(),
506
+ /**
507
+ * Breaker ids whose trip contributed to this escalation. An approval of
508
+ * this request resets exactly these — moving their counting floor to the
509
+ * approval instant — so a human waving one payment through also clears the
510
+ * behavior that stopped it, rather than being asked again immediately.
511
+ * Empty when ordinary rules did the escalating.
512
+ */
513
+ breakers: z2.array(z2.string()).default([]),
514
+ status: ApprovalStatus,
515
+ createdAt: z2.coerce.date(),
516
+ /** Past this instant the request denies, whatever arrives afterwards. */
517
+ expiresAt: z2.coerce.date(),
518
+ resolvedAt: z2.coerce.date().optional(),
519
+ /** Which registered key signed — absent when the TTL lapsed instead. */
520
+ approverKeyId: ApproverKeyId.optional(),
521
+ /**
522
+ * The follow-up decision appended to the chain when this resolved. That
523
+ * decision, not this record, is the voucher a signer verifies: the original
524
+ * `escalate` decision is never rewritten.
525
+ */
526
+ finalDecisionId: DecisionId.optional()
527
+ });
528
+ var ApprovalChallenges = z2.object({
529
+ approve: z2.string(),
530
+ reject: z2.string()
531
+ });
532
+ var ApprovalGrant = z2.object({
533
+ decisionId: DecisionId,
534
+ intentHash: z2.string(),
535
+ verdict: ApprovalVerdict,
536
+ approverKeyId: ApproverKeyId,
537
+ signature: z2.string().min(1)
538
+ });
539
+ var ReputationSubject = z2.object({
540
+ kind: z2.enum(["agent", "vendor"]),
541
+ id: z2.string()
271
542
  });
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()
543
+ var ReputationComponents = z2.object({
544
+ volume: z2.number(),
545
+ longevity: z2.number(),
546
+ disputeRate: z2.number(),
547
+ counterpartyQuality: z2.number(),
548
+ settlementReliability: z2.number()
278
549
  });
279
- var ReputationScore = z.object({
550
+ var ReputationScore = z2.object({
280
551
  subject: ReputationSubject,
281
- score: z.number().min(0).max(100),
552
+ score: z2.number().min(0).max(100),
282
553
  components: ReputationComponents,
283
- confidence: z.number().min(0).max(1),
284
- asOf: z.coerce.date(),
554
+ confidence: z2.number().min(0).max(1),
555
+ asOf: z2.coerce.date(),
285
556
  /** 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(),
557
+ evidenceUri: z2.string().optional()
558
+ });
559
+ var ReinEvent = z2.discriminatedUnion("type", [
560
+ z2.object({ type: z2.literal("intent.created"), at: z2.coerce.date(), intent: PaymentIntent }),
561
+ z2.object({ type: z2.literal("decision.made"), at: z2.coerce.date(), decision: Decision }),
562
+ z2.object({ type: z2.literal("payment.settled"), at: z2.coerce.date(), payment: SettledPayment }),
563
+ z2.object({
564
+ type: z2.literal("shadow.spend"),
565
+ at: z2.coerce.date(),
295
566
  agentId: AgentId,
296
- txHash: z.string(),
567
+ txHash: z2.string(),
297
568
  chain: Chain,
298
569
  amount: DecimalString
299
570
  }),
300
- z.object({
301
- type: z.literal("signature.released"),
302
- at: z.coerce.date(),
571
+ z2.object({
572
+ type: z2.literal("signature.released"),
573
+ at: z2.coerce.date(),
303
574
  sessionId: SessionId,
304
575
  agentId: AgentId,
305
576
  intentId: IntentId,
306
577
  decisionId: DecisionId,
307
578
  amount: DecimalString
308
579
  }),
309
- z.object({
310
- type: z.literal("signature.refused"),
311
- at: z.coerce.date(),
580
+ z2.object({
581
+ type: z2.literal("signature.refused"),
582
+ at: z2.coerce.date(),
312
583
  /** Refusal code, e.g. "decision_replayed" (see @reinconsole/signer). */
313
- code: z.string(),
314
- reason: z.string(),
584
+ code: z2.string(),
585
+ reason: z2.string(),
315
586
  sessionId: SessionId.optional(),
316
587
  agentId: AgentId.optional(),
317
588
  intentId: IntentId.optional()
318
589
  }),
319
- z.object({
320
- type: z.literal("gate.quoted"),
321
- at: z.coerce.date(),
322
- resource: z.string(),
323
- method: z.string(),
590
+ z2.object({
591
+ type: z2.literal("gate.quoted"),
592
+ at: z2.coerce.date(),
593
+ resource: z2.string(),
594
+ method: z2.string(),
324
595
  amount: DecimalString,
325
- asset: z.string(),
326
- network: z.string()
596
+ asset: z2.string(),
597
+ network: z2.string()
598
+ }),
599
+ z2.object({ type: z2.literal("gate.settled"), at: z2.coerce.date(), receipt: GateReceipt }),
600
+ z2.object({
601
+ type: z2.literal("approval.requested"),
602
+ at: z2.coerce.date(),
603
+ request: ApprovalRequest
604
+ }),
605
+ z2.object({
606
+ type: z2.literal("approval.resolved"),
607
+ at: z2.coerce.date(),
608
+ request: ApprovalRequest,
609
+ /** The follow-up allow/deny decision this resolution appended. */
610
+ decision: Decision
611
+ }),
612
+ z2.object({
613
+ type: z2.literal("liveness.missing"),
614
+ at: z2.coerce.date(),
615
+ agentId: AgentId,
616
+ expectation: LivenessExpectation,
617
+ /** How long the agent has been silent when the alarm was raised. */
618
+ silentMs: z2.number().int().nonnegative(),
619
+ /** Last sighting, absent when the agent was never seen at all. */
620
+ lastSeenAt: z2.coerce.date().optional()
327
621
  }),
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(),
622
+ z2.object({
623
+ type: z2.literal("liveness.recovered"),
624
+ at: z2.coerce.date(),
625
+ agentId: AgentId,
626
+ /** How long the silence lasted, measured to this sighting. */
627
+ silentMs: z2.number().int().nonnegative(),
628
+ source: LivenessSource
629
+ }),
630
+ z2.object({
631
+ type: z2.literal("gate.refused"),
632
+ at: z2.coerce.date(),
332
633
  /** Refusal code, e.g. "payment_replayed" (see @reinconsole/gate). */
333
- code: z.string(),
334
- reason: z.string(),
335
- resource: z.string(),
634
+ code: z2.string(),
635
+ reason: z2.string(),
636
+ resource: z2.string(),
336
637
  /** Known only when the payment header decoded far enough to name a payer. */
337
- payer: z.string().optional()
638
+ payer: z2.string().optional()
338
639
  })
339
640
  ]);
340
641
 
@@ -542,7 +843,7 @@ function confidence(ev, nowMs) {
542
843
  const observations = ev.attempts + ev.disputes + ev.endorsements + ev.shadowSpends + replays + others;
543
844
  const depth = 1 - Math.exp(-observations / 10);
544
845
  const knownDays = Math.max(0, nowMs - ev.firstSeenMs) / DAY_MS;
545
- const age = 0.4 + 0.6 * Math.min(1, knownDays / 7);
846
+ const age = 0.1 + 0.9 * Math.min(1, knownDays / 7);
546
847
  return clamp(depth * age, 0, 1);
547
848
  }
548
849
  function blend(components, weights) {
@@ -816,32 +1117,253 @@ function payerCheck(graph, options = {}) {
816
1117
  import { fileURLToPath } from "url";
817
1118
  import { realpathSync } from "fs";
818
1119
  import Fastify from "fastify";
819
- import { z as z2 } from "zod";
820
- var ReportInput = z2.object({
1120
+ import { z as z3 } from "zod";
1121
+
1122
+ // ../../packages/core/dist/auth.js
1123
+ import { randomBytes as randomBytes2, createHash, timingSafeEqual } from "crypto";
1124
+ var SECRET_PREFIX = "rk_";
1125
+ var TOUCH_INTERVAL_MS = 6e4;
1126
+ var DEFAULT_ROTATION_GRACE_MS = 36e5;
1127
+ function mintSecret() {
1128
+ return SECRET_PREFIX + randomBytes2(32).toString("base64url");
1129
+ }
1130
+ function hashSecret(secret) {
1131
+ return createHash("sha256").update(secret).digest("hex");
1132
+ }
1133
+ function digestsEqual(a, b) {
1134
+ const left = Buffer.from(a, "hex");
1135
+ const right = Buffer.from(b, "hex");
1136
+ return left.length === right.length && timingSafeEqual(left, right);
1137
+ }
1138
+ var InMemoryApiKeyStore = class {
1139
+ byId = /* @__PURE__ */ new Map();
1140
+ hashes = /* @__PURE__ */ new Map();
1141
+ // secretHash -> keyId
1142
+ put(record) {
1143
+ const existing = this.byId.get(record.id);
1144
+ if (existing) {
1145
+ this.hashes.delete(existing.secretHash);
1146
+ if (existing.previousSecretHash) this.hashes.delete(existing.previousSecretHash);
1147
+ }
1148
+ this.byId.set(record.id, record);
1149
+ this.hashes.set(record.secretHash, record.id);
1150
+ if (record.previousSecretHash) this.hashes.set(record.previousSecretHash, record.id);
1151
+ }
1152
+ get(id) {
1153
+ return this.byId.get(id);
1154
+ }
1155
+ byHash(hash) {
1156
+ const id = this.hashes.get(hash);
1157
+ return id === void 0 ? void 0 : this.byId.get(id);
1158
+ }
1159
+ list() {
1160
+ return [...this.byId.values()];
1161
+ }
1162
+ };
1163
+ var AuthError = class _AuthError extends Error {
1164
+ constructor(status, code, message) {
1165
+ super(message);
1166
+ this.status = status;
1167
+ this.code = code;
1168
+ this.name = "AuthError";
1169
+ }
1170
+ status;
1171
+ code;
1172
+ /**
1173
+ * A brand, because `instanceof` is not enough here. Every service bundles
1174
+ * `@reinconsole/core` into its own output (`noExternal`, for self-contained
1175
+ * deploys), so an `ApiKeyAuth` built from one package and handed to a server
1176
+ * in another throws an AuthError from a DIFFERENT copy of this class. An
1177
+ * `instanceof` check in the catching service then misses it and a 401 turns
1178
+ * into a 500 — which is what happened the first time the graph was handed
1179
+ * the engine's auth object.
1180
+ */
1181
+ reinAuthError = true;
1182
+ /** Use this, never a bare `instanceof`, when catching across packages. */
1183
+ static is(err) {
1184
+ return err instanceof _AuthError || typeof err === "object" && err !== null && err.reinAuthError === true;
1185
+ }
1186
+ };
1187
+ var ApiKeyAuth = class {
1188
+ store;
1189
+ now;
1190
+ constructor(options = {}) {
1191
+ this.store = options.store ?? new InMemoryApiKeyStore();
1192
+ this.now = options.now ?? Date.now;
1193
+ }
1194
+ /**
1195
+ * Mint a key. The secret in the result is the only copy that will exist.
1196
+ *
1197
+ * `orgId` confines the key to one tenant; omitting it mints the unscoped
1198
+ * operator key that every deployment had before tenancy. `agentIds` narrows
1199
+ * an org-scoped key to named agents and is refused without an org, because a
1200
+ * scope with no org cannot say which policies or approvers the key may
1201
+ * reach — and a half-defined scope is the kind that fails open.
1202
+ */
1203
+ async issue(input) {
1204
+ if (input.scopes.length === 0) throw new TypeError("an API key needs at least one scope");
1205
+ if (input.agentIds?.length && input.orgId === void 0) {
1206
+ throw new TypeError("agentIds narrows an org-scoped key; pass orgId as well");
1207
+ }
1208
+ const secret = input.secret ?? mintSecret();
1209
+ const record = ApiKeyRecord.parse({
1210
+ id: newId("key"),
1211
+ name: input.name,
1212
+ scopes: input.scopes,
1213
+ ...input.orgId !== void 0 ? { orgId: input.orgId } : {},
1214
+ ...input.agentIds?.length ? { agentIds: input.agentIds } : {},
1215
+ createdAt: new Date(this.now()),
1216
+ secretHash: hashSecret(secret)
1217
+ });
1218
+ await this.store.put(record);
1219
+ return { key: toPublicApiKey(record), secret };
1220
+ }
1221
+ /**
1222
+ * Mint a replacement secret for an existing key. The outgoing secret keeps
1223
+ * working for `graceMs` so callers can be redeployed one at a time; pass 0
1224
+ * to cut it off immediately (what a compromised secret needs).
1225
+ */
1226
+ async rotate(keyId, options = {}) {
1227
+ const record = this.store.get(keyId);
1228
+ if (!record) throw new AuthError(404, "unknown_key_id", `no such key: ${keyId}`);
1229
+ const graceMs = options.graceMs ?? DEFAULT_ROTATION_GRACE_MS;
1230
+ const secret = mintSecret();
1231
+ const rotated = {
1232
+ ...record,
1233
+ secretHash: hashSecret(secret),
1234
+ rotatedAt: new Date(this.now())
1235
+ };
1236
+ delete rotated.previousSecretHash;
1237
+ delete rotated.previousSecretExpiresAt;
1238
+ if (graceMs > 0) {
1239
+ rotated.previousSecretHash = record.secretHash;
1240
+ rotated.previousSecretExpiresAt = new Date(this.now() + graceMs);
1241
+ }
1242
+ await this.store.put(rotated);
1243
+ return { key: toPublicApiKey(rotated), secret };
1244
+ }
1245
+ async revoke(keyId) {
1246
+ const record = this.store.get(keyId);
1247
+ if (!record) return void 0;
1248
+ const revoked = { ...record, revokedAt: new Date(this.now()) };
1249
+ await this.store.put(revoked);
1250
+ return toPublicApiKey(revoked);
1251
+ }
1252
+ list() {
1253
+ return this.store.list().map(toPublicApiKey);
1254
+ }
1255
+ /** One key's public record, for an ownership check before rotate/revoke. */
1256
+ get(keyId) {
1257
+ const record = this.store.get(keyId);
1258
+ return record === void 0 ? void 0 : toPublicApiKey(record);
1259
+ }
1260
+ /** True once at least one key exists — what "auth is configured" means. */
1261
+ hasKeys() {
1262
+ return this.store.list().length > 0;
1263
+ }
1264
+ /**
1265
+ * Resolve a request's credentials, or throw. `scope` is what the route
1266
+ * needs; an `admin` key satisfies every scope, any other key must hold the
1267
+ * exact one.
1268
+ */
1269
+ authenticate(headers, scope) {
1270
+ const presented = readCredential(headers);
1271
+ if (presented === void 0) {
1272
+ throw new AuthError(401, "missing_credentials", "missing API key (Authorization: Bearer ...)");
1273
+ }
1274
+ const hash = hashSecret(presented);
1275
+ const record = this.store.byHash(hash);
1276
+ if (!record) throw new AuthError(401, "invalid_key", "unknown API key");
1277
+ if (!digestsEqual(record.secretHash, hash)) {
1278
+ const expiry = record.previousSecretExpiresAt;
1279
+ if (record.previousSecretHash === void 0 || !digestsEqual(record.previousSecretHash, hash) || expiry === void 0 || this.now() >= expiry.getTime()) {
1280
+ throw new AuthError(401, "secret_expired", "rotated API key secret is no longer accepted");
1281
+ }
1282
+ }
1283
+ if (record.revokedAt) throw new AuthError(401, "key_revoked", "API key has been revoked");
1284
+ if (!record.scopes.includes("admin") && !record.scopes.includes(scope)) {
1285
+ throw new AuthError(403, "insufficient_scope", `API key lacks the "${scope}" scope`);
1286
+ }
1287
+ void this.touch(record);
1288
+ return toPublicApiKey(record);
1289
+ }
1290
+ /** Throttled write-through of `lastUsedAt` — never on the critical path. */
1291
+ async touch(record) {
1292
+ const at = this.now();
1293
+ if (record.lastUsedAt && at - record.lastUsedAt.getTime() < TOUCH_INTERVAL_MS) return;
1294
+ try {
1295
+ await this.store.put({ ...record, lastUsedAt: new Date(at) });
1296
+ } catch {
1297
+ }
1298
+ }
1299
+ };
1300
+ function first(value) {
1301
+ return Array.isArray(value) ? value[0] : value;
1302
+ }
1303
+ function readCredential(headers) {
1304
+ const auth = first(headers.authorization)?.trim();
1305
+ if (auth) {
1306
+ const match = /^Bearer\s+(.+)$/i.exec(auth);
1307
+ if (match?.[1]) return match[1].trim();
1308
+ }
1309
+ const apiKey = first(headers["x-api-key"])?.trim();
1310
+ return apiKey === void 0 || apiKey === "" ? void 0 : apiKey;
1311
+ }
1312
+
1313
+ // src/server.ts
1314
+ var ReportInput = z3.object({
821
1315
  subject: ReputationSubject,
822
- kind: z2.enum(["dispute", "endorsement"]),
823
- at: z2.coerce.date().optional(),
824
- note: z2.string().max(500).optional()
1316
+ kind: z3.enum(["dispute", "endorsement"]),
1317
+ at: z3.coerce.date().optional(),
1318
+ note: z3.string().max(500).optional()
825
1319
  });
826
- var ScoresQuery = z2.object({ kind: z2.enum(["agent", "vendor"]).optional() });
827
- var LinkInput = z2.object({
1320
+ var ScoresQuery = z3.object({ kind: z3.enum(["agent", "vendor"]).optional() });
1321
+ var LinkInput = z3.object({
828
1322
  canonical: ReputationSubject,
829
1323
  alias: ReputationSubject
830
1324
  }).refine((l) => l.canonical.kind === l.alias.kind, {
831
1325
  message: "canonical and alias must be the same kind"
832
1326
  });
833
- function buildGraphServer(graph = new ReputationGraph()) {
1327
+ function isWrite(method) {
1328
+ return method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
1329
+ }
1330
+ function buildGraphServer(graph = new ReputationGraph(), options = {}) {
834
1331
  const app = Fastify({ logger: false });
1332
+ const auth = options.auth;
835
1333
  app.setErrorHandler((err, _req, reply) => {
836
- if (err instanceof z2.ZodError) {
1334
+ if (err instanceof z3.ZodError) {
837
1335
  return reply.status(400).send({ error: "validation_error", issues: err.issues });
838
1336
  }
1337
+ if (AuthError.is(err)) {
1338
+ return reply.status(err.status).send({ error: err.code, message: err.message });
1339
+ }
839
1340
  const message = err instanceof Error ? err.message : String(err);
840
1341
  return reply.status(500).send({ error: "internal_error", message });
841
1342
  });
842
- app.get("/health", () => ({ status: "ok", subjects: graph.subjects() }));
1343
+ if (auth) {
1344
+ app.addHook("onRequest", async (req, reply) => {
1345
+ if (!isWrite(req.method)) return;
1346
+ try {
1347
+ auth.authenticate(req.headers, "report");
1348
+ } catch (err) {
1349
+ if (AuthError.is(err)) {
1350
+ if (err.status === 401) reply.header("WWW-Authenticate", 'Bearer realm="rein-graph"');
1351
+ return reply.status(err.status).send({ error: err.code, message: err.message });
1352
+ }
1353
+ throw err;
1354
+ }
1355
+ return;
1356
+ });
1357
+ }
1358
+ app.get("/health", () => ({
1359
+ status: "ok",
1360
+ subjects: graph.subjects(),
1361
+ // Advertised so a producer can tell "this graph wants a key" apart from
1362
+ // "my key is wrong" without guessing from a 401.
1363
+ auth: auth ? "api-key" : "none"
1364
+ }));
843
1365
  app.post("/v1/events", async (req) => {
844
- const events = z2.union([ReinEvent.transform((e) => [e]), z2.array(ReinEvent)]).parse(req.body);
1366
+ const events = z3.union([ReinEvent.transform((e) => [e]), z3.array(ReinEvent)]).parse(req.body);
845
1367
  for (const event of events) graph.ingest(event);
846
1368
  await graph.flush();
847
1369
  return { ingested: events.length };
@@ -853,7 +1375,7 @@ function buildGraphServer(graph = new ReputationGraph()) {
853
1375
  return graph.score(input.subject);
854
1376
  });
855
1377
  app.post("/v1/links", async (req) => {
856
- const links = z2.union([LinkInput.transform((l) => [l]), z2.array(LinkInput)]).parse(req.body);
1378
+ const links = z3.union([LinkInput.transform((l) => [l]), z3.array(LinkInput)]).parse(req.body);
857
1379
  for (const link of links) graph.link(link.canonical, link.alias);
858
1380
  await graph.flush();
859
1381
  return { linked: links.length };
@@ -872,6 +1394,36 @@ function buildGraphServer(graph = new ReputationGraph()) {
872
1394
  });
873
1395
  return app;
874
1396
  }
1397
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
1398
+ async function graphAuthFromEnv(env) {
1399
+ const raw = env["REIN_GRAPH_API_KEY"]?.trim();
1400
+ if (!raw) return void 0;
1401
+ const auth = new ApiKeyAuth();
1402
+ const secrets = raw.split(",").map((secret) => secret.trim()).filter(Boolean);
1403
+ for (const [i, secret] of secrets.entries()) {
1404
+ await auth.issue({ name: `env-key-${i + 1}`, scopes: ["report"], secret });
1405
+ }
1406
+ return auth.hasKeys() ? auth : void 0;
1407
+ }
1408
+ function resolveGraphHost(env, hasAuth = false) {
1409
+ const requested = env["HOST"]?.trim();
1410
+ const optedIn = env["REIN_GRAPH_PUBLIC"]?.trim() === "1";
1411
+ if (hasAuth) return { host: requested || "0.0.0.0" };
1412
+ if (optedIn) {
1413
+ return {
1414
+ host: requested || "0.0.0.0",
1415
+ warning: "REIN_GRAPH_PUBLIC=1 \u2014 this reputation graph is answering unauthenticated requests. Anyone who can reach it can write evidence that moves scores."
1416
+ };
1417
+ }
1418
+ if (requested && !LOOPBACK.has(requested)) {
1419
+ throw new Error(
1420
+ `refusing to bind ${requested}: the reputation graph has no authentication.
1421
+ Set REIN_GRAPH_API_KEY=<secret> to protect its writes, or
1422
+ set REIN_GRAPH_PUBLIC=1 to expose an open graph deliberately.`
1423
+ );
1424
+ }
1425
+ return { host: requested || "127.0.0.1" };
1426
+ }
875
1427
  function isMainModule() {
876
1428
  if (!process.argv[1]) return false;
877
1429
  try {
@@ -882,9 +1434,16 @@ function isMainModule() {
882
1434
  }
883
1435
  if (isMainModule()) {
884
1436
  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) => {
1437
+ graphAuthFromEnv(process.env).then((auth) => {
1438
+ const { host, warning } = resolveGraphHost(process.env, auth !== void 0);
1439
+ if (warning) console.warn(`[rein] ${warning}`);
1440
+ const app = buildGraphServer(new ReputationGraph(), { ...auth ? { auth } : {} });
1441
+ return app.listen({ port, host }).then(
1442
+ () => console.log(
1443
+ `[rein] graph listening on http://${host}:${port} (auth: ${auth ? "api-key" : "none"})`
1444
+ )
1445
+ );
1446
+ }).catch((err) => {
888
1447
  console.error(err);
889
1448
  process.exit(1);
890
1449
  });
@@ -900,10 +1459,12 @@ export {
900
1459
  buildGraphServer,
901
1460
  confidence,
902
1461
  disputeComponent,
1462
+ graphAuthFromEnv,
903
1463
  longevityComponent,
904
1464
  normalizeSubject,
905
1465
  payerCheck,
906
1466
  reliabilityComponent,
1467
+ resolveGraphHost,
907
1468
  subjectKey,
908
1469
  volumeComponent
909
1470
  };