@reinconsole/graph 0.2.0 → 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 +1 -1
- package/dist/index.d.ts +42 -2
- package/dist/index.js +588 -246
- 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,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
|
|
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 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:
|
|
140
|
+
address: z2.string().min(1),
|
|
62
141
|
mode: EnforcementMode
|
|
63
142
|
});
|
|
64
|
-
var AgentStatus =
|
|
65
|
-
var AgentLabel =
|
|
66
|
-
var Agent =
|
|
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:
|
|
148
|
+
name: z2.string().min(1).max(200),
|
|
70
149
|
/** On-chain ERC-8004 identity, if the agent is registered. */
|
|
71
|
-
erc8004Id:
|
|
150
|
+
erc8004Id: z2.string().optional(),
|
|
72
151
|
/** Semantic grouping labels — the targets of policy `appliesTo.labels`. */
|
|
73
|
-
labels:
|
|
74
|
-
wallets:
|
|
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:
|
|
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 =
|
|
182
|
+
var Erc8004Id = z2.string().refine((value) => parseErc8004Id(value) !== void 0, {
|
|
104
183
|
message: 'expected "eip155:{chainId}:{registry}/{tokenId}"'
|
|
105
184
|
});
|
|
106
|
-
var Vendor =
|
|
107
|
-
host:
|
|
108
|
-
address:
|
|
109
|
-
erc8004Id:
|
|
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 =
|
|
112
|
-
taskId:
|
|
113
|
-
parentRunId:
|
|
114
|
-
purpose:
|
|
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 =
|
|
195
|
+
var PaymentIntent = z2.object({
|
|
117
196
|
id: IntentId,
|
|
118
197
|
agentId: AgentId,
|
|
119
198
|
vendor: Vendor,
|
|
120
|
-
resource:
|
|
199
|
+
resource: z2.string(),
|
|
121
200
|
amount: DecimalString,
|
|
122
201
|
asset: Asset,
|
|
123
202
|
chain: Chain,
|
|
124
203
|
taskContext: TaskContext.default({}),
|
|
125
|
-
nonce:
|
|
126
|
-
createdAt:
|
|
204
|
+
nonce: z2.string().min(1),
|
|
205
|
+
createdAt: z2.coerce.date()
|
|
127
206
|
});
|
|
128
|
-
var DecisionOutcome =
|
|
129
|
-
var Decision =
|
|
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:
|
|
217
|
+
intentHash: z2.string(),
|
|
139
218
|
outcome: DecisionOutcome,
|
|
140
219
|
/** Ids of the rules that fired, for explainability. */
|
|
141
|
-
matchedRules:
|
|
220
|
+
matchedRules: z2.array(z2.string()).default([]),
|
|
142
221
|
/** Human-readable explanation surfaced in the dashboard. */
|
|
143
|
-
reason:
|
|
144
|
-
policyId:
|
|
145
|
-
policyVersion:
|
|
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:
|
|
226
|
+
prevHash: z2.string(),
|
|
148
227
|
/** Hash of this decision's canonical content. */
|
|
149
|
-
hash:
|
|
228
|
+
hash: z2.string(),
|
|
150
229
|
/** Service signing-key signature over `hash`. */
|
|
151
|
-
signature:
|
|
152
|
-
latencyMs:
|
|
153
|
-
decidedAt:
|
|
230
|
+
signature: z2.string(),
|
|
231
|
+
latencyMs: z2.number().nonnegative(),
|
|
232
|
+
decidedAt: z2.coerce.date()
|
|
154
233
|
});
|
|
155
|
-
var
|
|
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:
|
|
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:
|
|
165
|
-
createdAt:
|
|
166
|
-
revokedAt:
|
|
244
|
+
expiresAt: z2.coerce.date(),
|
|
245
|
+
createdAt: z2.coerce.date(),
|
|
246
|
+
revokedAt: z2.coerce.date().optional()
|
|
167
247
|
});
|
|
168
|
-
var SettledPayment =
|
|
248
|
+
var SettledPayment = z2.object({
|
|
169
249
|
intentId: IntentId,
|
|
170
|
-
txHash:
|
|
250
|
+
txHash: z2.string(),
|
|
171
251
|
chain: Chain,
|
|
172
|
-
blockNumber:
|
|
173
|
-
facilitator:
|
|
252
|
+
blockNumber: z2.coerce.bigint(),
|
|
253
|
+
facilitator: z2.string().optional(),
|
|
174
254
|
feePaid: DecimalString.optional(),
|
|
175
|
-
confirmedAt:
|
|
255
|
+
confirmedAt: z2.coerce.date()
|
|
176
256
|
});
|
|
177
|
-
var SettlementReport =
|
|
257
|
+
var SettlementReport = z2.object({
|
|
178
258
|
intentId: IntentId,
|
|
179
259
|
/** The settling transaction, when the observer saw one. */
|
|
180
|
-
txHash:
|
|
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:
|
|
186
|
-
confirmedAt:
|
|
265
|
+
source: z2.string().min(1).max(64).optional(),
|
|
266
|
+
confirmedAt: z2.coerce.date()
|
|
187
267
|
});
|
|
188
|
-
var ReceiptSettlement =
|
|
189
|
-
txHash:
|
|
190
|
-
networkId:
|
|
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:
|
|
272
|
+
raw: z2.string().optional()
|
|
193
273
|
});
|
|
194
|
-
var Receipt =
|
|
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:
|
|
202
|
-
method:
|
|
203
|
-
vendorHost:
|
|
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:
|
|
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:
|
|
292
|
+
createdAt: z2.coerce.date()
|
|
213
293
|
});
|
|
214
|
-
var GateReceipt =
|
|
294
|
+
var GateReceipt = z2.object({
|
|
215
295
|
id: GateReceiptId,
|
|
216
|
-
at:
|
|
296
|
+
at: z2.coerce.date(),
|
|
217
297
|
/** The route pattern that priced this request, e.g. "/api/reports/*". */
|
|
218
|
-
route:
|
|
298
|
+
route: z2.string().min(1),
|
|
219
299
|
/** The concrete resource paid for (URL path actually requested). */
|
|
220
|
-
resource:
|
|
221
|
-
method:
|
|
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:
|
|
224
|
-
payTo:
|
|
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:
|
|
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:
|
|
231
|
-
network:
|
|
310
|
+
asset: z2.string().min(1),
|
|
311
|
+
network: z2.string().min(1),
|
|
232
312
|
/** Settlement transaction hash (mock ledger or on-chain). */
|
|
233
|
-
transaction:
|
|
313
|
+
transaction: z2.string().min(1)
|
|
234
314
|
});
|
|
235
|
-
var Window =
|
|
236
|
-
var Multiplier =
|
|
237
|
-
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({
|
|
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:
|
|
321
|
+
rollingSum: z2.object({ window: Window, gt: DecimalString }).optional(),
|
|
242
322
|
/** Transaction count in a rolling window exceeds `gt`. */
|
|
243
|
-
txCount:
|
|
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:
|
|
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:
|
|
333
|
+
resourceIn: z2.array(z2.string()).optional(),
|
|
254
334
|
/** First time Rein has seen this vendor for the agent. */
|
|
255
|
-
vendorFirstSeen:
|
|
335
|
+
vendorFirstSeen: z2.boolean().optional(),
|
|
256
336
|
/** Vendor reputation score is below this threshold (Phase 3 hook). */
|
|
257
|
-
vendorReputationLt:
|
|
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:
|
|
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:
|
|
352
|
+
taskBudget: z2.object({ gt: DecimalString }).optional(),
|
|
273
353
|
/** The intent carries no `taskContext.taskId` (or an empty one). */
|
|
274
|
-
taskIdMissing:
|
|
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 =
|
|
279
|
-
id:
|
|
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:
|
|
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 =
|
|
299
|
-
id:
|
|
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 =
|
|
307
|
-
var AppliesTo =
|
|
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:
|
|
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:
|
|
317
|
-
chains:
|
|
396
|
+
labels: z2.array(z2.string()).optional(),
|
|
397
|
+
chains: z2.array(Chain).optional()
|
|
318
398
|
});
|
|
319
|
-
var Escalation =
|
|
320
|
-
approvers:
|
|
399
|
+
var Escalation = z2.object({
|
|
400
|
+
approvers: z2.array(z2.string()).default([]),
|
|
321
401
|
timeoutAction: PolicyDefault.default("deny"),
|
|
322
|
-
timeoutMin:
|
|
402
|
+
timeoutMin: z2.number().int().positive().default(15)
|
|
323
403
|
});
|
|
324
|
-
var Policy =
|
|
325
|
-
policyId:
|
|
326
|
-
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(),
|
|
327
419
|
appliesTo: AppliesTo.default({}),
|
|
328
|
-
rules:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
450
|
+
since: z2.coerce.date(),
|
|
359
451
|
/** What this agent is supposed to be doing, shown beside the alarm. */
|
|
360
|
-
note:
|
|
452
|
+
note: z2.string().max(200).optional()
|
|
361
453
|
});
|
|
362
454
|
var LivenessWatchInput = LivenessExpectation.omit({ since: true }).extend({
|
|
363
|
-
graceMs:
|
|
455
|
+
graceMs: z2.number().int().nonnegative().optional()
|
|
364
456
|
});
|
|
365
|
-
var Heartbeat =
|
|
457
|
+
var Heartbeat = z2.object({
|
|
366
458
|
agentId: AgentId,
|
|
367
459
|
/** When the agent was alive. Defaults to arrival time. */
|
|
368
|
-
at:
|
|
460
|
+
at: z2.coerce.date().optional(),
|
|
369
461
|
/** Free-form note from the reporter, e.g. "poll cycle 41 ok". */
|
|
370
|
-
note:
|
|
462
|
+
note: z2.string().max(200).optional()
|
|
371
463
|
});
|
|
372
|
-
var LivenessStatus =
|
|
373
|
-
var
|
|
374
|
-
var
|
|
375
|
-
|
|
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:
|
|
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:
|
|
399
|
-
algorithm:
|
|
400
|
-
createdAt:
|
|
401
|
-
revokedAt:
|
|
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 =
|
|
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:
|
|
482
|
+
intentHash: z2.string(),
|
|
409
483
|
agentId: AgentId,
|
|
410
|
-
|
|
411
|
-
|
|
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:
|
|
503
|
+
taskId: z2.string().optional(),
|
|
422
504
|
/** Why policy escalated, verbatim from the decision. */
|
|
423
|
-
reason:
|
|
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:
|
|
513
|
+
breakers: z2.array(z2.string()).default([]),
|
|
432
514
|
status: ApprovalStatus,
|
|
433
|
-
createdAt:
|
|
515
|
+
createdAt: z2.coerce.date(),
|
|
434
516
|
/** Past this instant the request denies, whatever arrives afterwards. */
|
|
435
|
-
expiresAt:
|
|
436
|
-
resolvedAt:
|
|
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 =
|
|
447
|
-
approve:
|
|
448
|
-
reject:
|
|
528
|
+
var ApprovalChallenges = z2.object({
|
|
529
|
+
approve: z2.string(),
|
|
530
|
+
reject: z2.string()
|
|
449
531
|
});
|
|
450
|
-
var ApprovalGrant =
|
|
532
|
+
var ApprovalGrant = z2.object({
|
|
451
533
|
decisionId: DecisionId,
|
|
452
|
-
intentHash:
|
|
534
|
+
intentHash: z2.string(),
|
|
453
535
|
verdict: ApprovalVerdict,
|
|
454
536
|
approverKeyId: ApproverKeyId,
|
|
455
|
-
signature:
|
|
537
|
+
signature: z2.string().min(1)
|
|
456
538
|
});
|
|
457
|
-
var ReputationSubject =
|
|
458
|
-
kind:
|
|
459
|
-
id:
|
|
539
|
+
var ReputationSubject = z2.object({
|
|
540
|
+
kind: z2.enum(["agent", "vendor"]),
|
|
541
|
+
id: z2.string()
|
|
460
542
|
});
|
|
461
|
-
var ReputationComponents =
|
|
462
|
-
volume:
|
|
463
|
-
longevity:
|
|
464
|
-
disputeRate:
|
|
465
|
-
counterpartyQuality:
|
|
466
|
-
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()
|
|
467
549
|
});
|
|
468
|
-
var ReputationScore =
|
|
550
|
+
var ReputationScore = z2.object({
|
|
469
551
|
subject: ReputationSubject,
|
|
470
|
-
score:
|
|
552
|
+
score: z2.number().min(0).max(100),
|
|
471
553
|
components: ReputationComponents,
|
|
472
|
-
confidence:
|
|
473
|
-
asOf:
|
|
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:
|
|
557
|
+
evidenceUri: z2.string().optional()
|
|
476
558
|
});
|
|
477
|
-
var ReinEvent =
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
type:
|
|
483
|
-
at:
|
|
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:
|
|
567
|
+
txHash: z2.string(),
|
|
486
568
|
chain: Chain,
|
|
487
569
|
amount: DecimalString
|
|
488
570
|
}),
|
|
489
|
-
|
|
490
|
-
type:
|
|
491
|
-
at:
|
|
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
|
-
|
|
499
|
-
type:
|
|
500
|
-
at:
|
|
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:
|
|
503
|
-
reason:
|
|
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
|
-
|
|
509
|
-
type:
|
|
510
|
-
at:
|
|
511
|
-
resource:
|
|
512
|
-
method:
|
|
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:
|
|
515
|
-
network:
|
|
596
|
+
asset: z2.string(),
|
|
597
|
+
network: z2.string()
|
|
516
598
|
}),
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
type:
|
|
520
|
-
at:
|
|
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
|
-
|
|
524
|
-
type:
|
|
525
|
-
at:
|
|
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
|
-
|
|
531
|
-
type:
|
|
532
|
-
at:
|
|
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:
|
|
618
|
+
silentMs: z2.number().int().nonnegative(),
|
|
537
619
|
/** Last sighting, absent when the agent was never seen at all. */
|
|
538
|
-
lastSeenAt:
|
|
620
|
+
lastSeenAt: z2.coerce.date().optional()
|
|
539
621
|
}),
|
|
540
|
-
|
|
541
|
-
type:
|
|
542
|
-
at:
|
|
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:
|
|
627
|
+
silentMs: z2.number().int().nonnegative(),
|
|
546
628
|
source: LivenessSource
|
|
547
629
|
}),
|
|
548
|
-
|
|
549
|
-
type:
|
|
550
|
-
at:
|
|
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:
|
|
553
|
-
reason:
|
|
554
|
-
resource:
|
|
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:
|
|
638
|
+
payer: z2.string().optional()
|
|
557
639
|
})
|
|
558
640
|
]);
|
|
559
641
|
|
|
@@ -1035,32 +1117,253 @@ function payerCheck(graph, options = {}) {
|
|
|
1035
1117
|
import { fileURLToPath } from "url";
|
|
1036
1118
|
import { realpathSync } from "fs";
|
|
1037
1119
|
import Fastify from "fastify";
|
|
1038
|
-
import { z as
|
|
1039
|
-
|
|
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({
|
|
1040
1315
|
subject: ReputationSubject,
|
|
1041
|
-
kind:
|
|
1042
|
-
at:
|
|
1043
|
-
note:
|
|
1316
|
+
kind: z3.enum(["dispute", "endorsement"]),
|
|
1317
|
+
at: z3.coerce.date().optional(),
|
|
1318
|
+
note: z3.string().max(500).optional()
|
|
1044
1319
|
});
|
|
1045
|
-
var ScoresQuery =
|
|
1046
|
-
var LinkInput =
|
|
1320
|
+
var ScoresQuery = z3.object({ kind: z3.enum(["agent", "vendor"]).optional() });
|
|
1321
|
+
var LinkInput = z3.object({
|
|
1047
1322
|
canonical: ReputationSubject,
|
|
1048
1323
|
alias: ReputationSubject
|
|
1049
1324
|
}).refine((l) => l.canonical.kind === l.alias.kind, {
|
|
1050
1325
|
message: "canonical and alias must be the same kind"
|
|
1051
1326
|
});
|
|
1052
|
-
function
|
|
1327
|
+
function isWrite(method) {
|
|
1328
|
+
return method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
|
|
1329
|
+
}
|
|
1330
|
+
function buildGraphServer(graph = new ReputationGraph(), options = {}) {
|
|
1053
1331
|
const app = Fastify({ logger: false });
|
|
1332
|
+
const auth = options.auth;
|
|
1054
1333
|
app.setErrorHandler((err, _req, reply) => {
|
|
1055
|
-
if (err instanceof
|
|
1334
|
+
if (err instanceof z3.ZodError) {
|
|
1056
1335
|
return reply.status(400).send({ error: "validation_error", issues: err.issues });
|
|
1057
1336
|
}
|
|
1337
|
+
if (AuthError.is(err)) {
|
|
1338
|
+
return reply.status(err.status).send({ error: err.code, message: err.message });
|
|
1339
|
+
}
|
|
1058
1340
|
const message = err instanceof Error ? err.message : String(err);
|
|
1059
1341
|
return reply.status(500).send({ error: "internal_error", message });
|
|
1060
1342
|
});
|
|
1061
|
-
|
|
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
|
+
}));
|
|
1062
1365
|
app.post("/v1/events", async (req) => {
|
|
1063
|
-
const events =
|
|
1366
|
+
const events = z3.union([ReinEvent.transform((e) => [e]), z3.array(ReinEvent)]).parse(req.body);
|
|
1064
1367
|
for (const event of events) graph.ingest(event);
|
|
1065
1368
|
await graph.flush();
|
|
1066
1369
|
return { ingested: events.length };
|
|
@@ -1072,7 +1375,7 @@ function buildGraphServer(graph = new ReputationGraph()) {
|
|
|
1072
1375
|
return graph.score(input.subject);
|
|
1073
1376
|
});
|
|
1074
1377
|
app.post("/v1/links", async (req) => {
|
|
1075
|
-
const links =
|
|
1378
|
+
const links = z3.union([LinkInput.transform((l) => [l]), z3.array(LinkInput)]).parse(req.body);
|
|
1076
1379
|
for (const link of links) graph.link(link.canonical, link.alias);
|
|
1077
1380
|
await graph.flush();
|
|
1078
1381
|
return { linked: links.length };
|
|
@@ -1091,6 +1394,36 @@ function buildGraphServer(graph = new ReputationGraph()) {
|
|
|
1091
1394
|
});
|
|
1092
1395
|
return app;
|
|
1093
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
|
+
}
|
|
1094
1427
|
function isMainModule() {
|
|
1095
1428
|
if (!process.argv[1]) return false;
|
|
1096
1429
|
try {
|
|
@@ -1101,9 +1434,16 @@ function isMainModule() {
|
|
|
1101
1434
|
}
|
|
1102
1435
|
if (isMainModule()) {
|
|
1103
1436
|
const port = Number(process.env.PORT ?? 8788);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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) => {
|
|
1107
1447
|
console.error(err);
|
|
1108
1448
|
process.exit(1);
|
|
1109
1449
|
});
|
|
@@ -1119,10 +1459,12 @@ export {
|
|
|
1119
1459
|
buildGraphServer,
|
|
1120
1460
|
confidence,
|
|
1121
1461
|
disputeComponent,
|
|
1462
|
+
graphAuthFromEnv,
|
|
1122
1463
|
longevityComponent,
|
|
1123
1464
|
normalizeSubject,
|
|
1124
1465
|
payerCheck,
|
|
1125
1466
|
reliabilityComponent,
|
|
1467
|
+
resolveGraphHost,
|
|
1126
1468
|
subjectKey,
|
|
1127
1469
|
volumeComponent
|
|
1128
1470
|
};
|