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