@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/README.md +2 -2
- package/dist/index.d.ts +57 -4
- package/dist/index.js +726 -165
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,8 +1,101 @@
|
|
|
1
|
-
// ../../packages/core/dist/
|
|
1
|
+
// ../../packages/core/dist/chunk-6H2LDEB7.js
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
45
|
-
|
|
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:
|
|
140
|
+
address: z2.string().min(1),
|
|
60
141
|
mode: EnforcementMode
|
|
61
142
|
});
|
|
62
|
-
var AgentStatus =
|
|
63
|
-
var
|
|
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:
|
|
148
|
+
name: z2.string().min(1).max(200),
|
|
67
149
|
/** On-chain ERC-8004 identity, if the agent is registered. */
|
|
68
|
-
erc8004Id:
|
|
69
|
-
|
|
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:
|
|
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 =
|
|
182
|
+
var Erc8004Id = z2.string().refine((value) => parseErc8004Id(value) !== void 0, {
|
|
99
183
|
message: 'expected "eip155:{chainId}:{registry}/{tokenId}"'
|
|
100
184
|
});
|
|
101
|
-
var Vendor =
|
|
102
|
-
host:
|
|
103
|
-
address:
|
|
104
|
-
erc8004Id:
|
|
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 =
|
|
107
|
-
taskId:
|
|
108
|
-
parentRunId:
|
|
109
|
-
purpose:
|
|
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 =
|
|
195
|
+
var PaymentIntent = z2.object({
|
|
112
196
|
id: IntentId,
|
|
113
197
|
agentId: AgentId,
|
|
114
198
|
vendor: Vendor,
|
|
115
|
-
resource:
|
|
199
|
+
resource: z2.string(),
|
|
116
200
|
amount: DecimalString,
|
|
117
201
|
asset: Asset,
|
|
118
202
|
chain: Chain,
|
|
119
203
|
taskContext: TaskContext.default({}),
|
|
120
|
-
nonce:
|
|
121
|
-
createdAt:
|
|
204
|
+
nonce: z2.string().min(1),
|
|
205
|
+
createdAt: z2.coerce.date()
|
|
122
206
|
});
|
|
123
|
-
var DecisionOutcome =
|
|
124
|
-
var Decision =
|
|
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:
|
|
217
|
+
intentHash: z2.string(),
|
|
134
218
|
outcome: DecisionOutcome,
|
|
135
219
|
/** Ids of the rules that fired, for explainability. */
|
|
136
|
-
matchedRules:
|
|
220
|
+
matchedRules: z2.array(z2.string()).default([]),
|
|
137
221
|
/** Human-readable explanation surfaced in the dashboard. */
|
|
138
|
-
reason:
|
|
139
|
-
policyId:
|
|
140
|
-
policyVersion:
|
|
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:
|
|
226
|
+
prevHash: z2.string(),
|
|
143
227
|
/** Hash of this decision's canonical content. */
|
|
144
|
-
hash:
|
|
228
|
+
hash: z2.string(),
|
|
145
229
|
/** Service signing-key signature over `hash`. */
|
|
146
|
-
signature:
|
|
147
|
-
latencyMs:
|
|
148
|
-
decidedAt:
|
|
230
|
+
signature: z2.string(),
|
|
231
|
+
latencyMs: z2.number().nonnegative(),
|
|
232
|
+
decidedAt: z2.coerce.date()
|
|
149
233
|
});
|
|
150
|
-
var
|
|
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:
|
|
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:
|
|
160
|
-
createdAt:
|
|
161
|
-
revokedAt:
|
|
244
|
+
expiresAt: z2.coerce.date(),
|
|
245
|
+
createdAt: z2.coerce.date(),
|
|
246
|
+
revokedAt: z2.coerce.date().optional()
|
|
162
247
|
});
|
|
163
|
-
var SettledPayment =
|
|
248
|
+
var SettledPayment = z2.object({
|
|
164
249
|
intentId: IntentId,
|
|
165
|
-
txHash:
|
|
250
|
+
txHash: z2.string(),
|
|
166
251
|
chain: Chain,
|
|
167
|
-
blockNumber:
|
|
168
|
-
facilitator:
|
|
252
|
+
blockNumber: z2.coerce.bigint(),
|
|
253
|
+
facilitator: z2.string().optional(),
|
|
169
254
|
feePaid: DecimalString.optional(),
|
|
170
|
-
confirmedAt:
|
|
255
|
+
confirmedAt: z2.coerce.date()
|
|
171
256
|
});
|
|
172
|
-
var
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
272
|
+
raw: z2.string().optional()
|
|
177
273
|
});
|
|
178
|
-
var Receipt =
|
|
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:
|
|
186
|
-
method:
|
|
187
|
-
vendorHost:
|
|
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:
|
|
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:
|
|
292
|
+
createdAt: z2.coerce.date()
|
|
197
293
|
});
|
|
198
|
-
var GateReceipt =
|
|
294
|
+
var GateReceipt = z2.object({
|
|
199
295
|
id: GateReceiptId,
|
|
200
|
-
at:
|
|
296
|
+
at: z2.coerce.date(),
|
|
201
297
|
/** The route pattern that priced this request, e.g. "/api/reports/*". */
|
|
202
|
-
route:
|
|
298
|
+
route: z2.string().min(1),
|
|
203
299
|
/** The concrete resource paid for (URL path actually requested). */
|
|
204
|
-
resource:
|
|
205
|
-
method:
|
|
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:
|
|
208
|
-
payTo:
|
|
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:
|
|
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:
|
|
215
|
-
network:
|
|
310
|
+
asset: z2.string().min(1),
|
|
311
|
+
network: z2.string().min(1),
|
|
216
312
|
/** Settlement transaction hash (mock ledger or on-chain). */
|
|
217
|
-
transaction:
|
|
313
|
+
transaction: z2.string().min(1)
|
|
218
314
|
});
|
|
219
|
-
var Window =
|
|
220
|
-
var Multiplier =
|
|
221
|
-
var Condition =
|
|
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:
|
|
321
|
+
rollingSum: z2.object({ window: Window, gt: DecimalString }).optional(),
|
|
226
322
|
/** Transaction count in a rolling window exceeds `gt`. */
|
|
227
|
-
txCount:
|
|
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:
|
|
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:
|
|
335
|
+
vendorFirstSeen: z2.boolean().optional(),
|
|
232
336
|
/** Vendor reputation score is below this threshold (Phase 3 hook). */
|
|
233
|
-
vendorReputationLt:
|
|
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:
|
|
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
|
|
240
|
-
id:
|
|
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 =
|
|
248
|
-
var AppliesTo =
|
|
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:
|
|
251
|
-
|
|
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 =
|
|
254
|
-
approvers:
|
|
399
|
+
var Escalation = z2.object({
|
|
400
|
+
approvers: z2.array(z2.string()).default([]),
|
|
255
401
|
timeoutAction: PolicyDefault.default("deny"),
|
|
256
|
-
timeoutMin:
|
|
402
|
+
timeoutMin: z2.number().int().positive().default(15)
|
|
257
403
|
});
|
|
258
|
-
var Policy =
|
|
259
|
-
policyId:
|
|
260
|
-
version:
|
|
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:
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
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 =
|
|
273
|
-
volume:
|
|
274
|
-
longevity:
|
|
275
|
-
disputeRate:
|
|
276
|
-
counterpartyQuality:
|
|
277
|
-
settlementReliability:
|
|
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 =
|
|
550
|
+
var ReputationScore = z2.object({
|
|
280
551
|
subject: ReputationSubject,
|
|
281
|
-
score:
|
|
552
|
+
score: z2.number().min(0).max(100),
|
|
282
553
|
components: ReputationComponents,
|
|
283
|
-
confidence:
|
|
284
|
-
asOf:
|
|
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:
|
|
287
|
-
});
|
|
288
|
-
var ReinEvent =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
type:
|
|
294
|
-
at:
|
|
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:
|
|
567
|
+
txHash: z2.string(),
|
|
297
568
|
chain: Chain,
|
|
298
569
|
amount: DecimalString
|
|
299
570
|
}),
|
|
300
|
-
|
|
301
|
-
type:
|
|
302
|
-
at:
|
|
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
|
-
|
|
310
|
-
type:
|
|
311
|
-
at:
|
|
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:
|
|
314
|
-
reason:
|
|
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
|
-
|
|
320
|
-
type:
|
|
321
|
-
at:
|
|
322
|
-
resource:
|
|
323
|
-
method:
|
|
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:
|
|
326
|
-
network:
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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:
|
|
334
|
-
reason:
|
|
335
|
-
resource:
|
|
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:
|
|
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.
|
|
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
|
|
820
|
-
|
|
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:
|
|
823
|
-
at:
|
|
824
|
-
note:
|
|
1316
|
+
kind: z3.enum(["dispute", "endorsement"]),
|
|
1317
|
+
at: z3.coerce.date().optional(),
|
|
1318
|
+
note: z3.string().max(500).optional()
|
|
825
1319
|
});
|
|
826
|
-
var ScoresQuery =
|
|
827
|
-
var LinkInput =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
};
|