@parmanasystems/server 1.0.19
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 +407 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +943 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import { createHash, randomUUID } from "crypto";
|
|
4
|
+
import rateLimit from "@fastify/rate-limit";
|
|
5
|
+
import cors from "@fastify/cors";
|
|
6
|
+
import helmet from "@fastify/helmet";
|
|
7
|
+
|
|
8
|
+
// src/runtime.ts
|
|
9
|
+
import crypto from "crypto";
|
|
10
|
+
import {
|
|
11
|
+
LocalSigner,
|
|
12
|
+
LocalVerifier,
|
|
13
|
+
getRuntimeManifest
|
|
14
|
+
} from "@parmanasystems/execution";
|
|
15
|
+
import {
|
|
16
|
+
loadPrivateKey,
|
|
17
|
+
loadPublicKey
|
|
18
|
+
} from "@parmanasystems/crypto";
|
|
19
|
+
function resolveKeyPair() {
|
|
20
|
+
if (process.env.Parmana_PRIVATE_KEY && process.env.Parmana_PUBLIC_KEY) {
|
|
21
|
+
return {
|
|
22
|
+
privateKey: process.env.Parmana_PRIVATE_KEY,
|
|
23
|
+
publicKey: process.env.Parmana_PUBLIC_KEY,
|
|
24
|
+
source: "env"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return {
|
|
29
|
+
privateKey: loadPrivateKey(),
|
|
30
|
+
publicKey: loadPublicKey(),
|
|
31
|
+
source: "disk"
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519", {
|
|
36
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
37
|
+
publicKeyEncoding: { type: "spki", format: "pem" }
|
|
38
|
+
});
|
|
39
|
+
return { privateKey, publicKey, source: "ephemeral" };
|
|
40
|
+
}
|
|
41
|
+
var keys = resolveKeyPair();
|
|
42
|
+
var signingKeySource = keys.source;
|
|
43
|
+
var signer = new LocalSigner(keys.privateKey);
|
|
44
|
+
var verifier = new LocalVerifier(keys.publicKey);
|
|
45
|
+
var runtimeManifest = getRuntimeManifest();
|
|
46
|
+
|
|
47
|
+
// src/auth.ts
|
|
48
|
+
async function authHook(req, reply) {
|
|
49
|
+
const apiKey = process.env.Parmana_API_KEY;
|
|
50
|
+
if (!apiKey) return;
|
|
51
|
+
const auth = req.headers.authorization;
|
|
52
|
+
if (auth !== `Bearer ${apiKey}`) {
|
|
53
|
+
req.log.warn({ reqId: req.id, reason: "auth_failure" }, "auth_failure");
|
|
54
|
+
reply.code(401).send({ error: "Unauthorized" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/routes.ts
|
|
59
|
+
import { getRuntimeManifest as getRuntimeManifest2 } from "@parmanasystems/execution";
|
|
60
|
+
import { existsSync } from "fs";
|
|
61
|
+
|
|
62
|
+
// src/routes/execute.ts
|
|
63
|
+
import {
|
|
64
|
+
executeFromSignals,
|
|
65
|
+
RedisReplayStore
|
|
66
|
+
} from "@parmanasystems/execution";
|
|
67
|
+
var S_EXECUTION_OUTPUT = {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
status: {
|
|
71
|
+
type: "string"
|
|
72
|
+
},
|
|
73
|
+
execution_id: {
|
|
74
|
+
type: "string"
|
|
75
|
+
},
|
|
76
|
+
decision: {
|
|
77
|
+
type: "object"
|
|
78
|
+
},
|
|
79
|
+
execution_state: {
|
|
80
|
+
type: "string"
|
|
81
|
+
},
|
|
82
|
+
requires_override: {
|
|
83
|
+
type: "boolean"
|
|
84
|
+
},
|
|
85
|
+
signature: {
|
|
86
|
+
type: "string"
|
|
87
|
+
},
|
|
88
|
+
error: {
|
|
89
|
+
type: "string"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var S_ERROR = {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
error: {
|
|
97
|
+
type: "string"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
required: [
|
|
101
|
+
"error"
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
var S_RATE_LIMIT_ERROR = {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
error: {
|
|
108
|
+
type: "string"
|
|
109
|
+
},
|
|
110
|
+
limit: {
|
|
111
|
+
type: "integer"
|
|
112
|
+
},
|
|
113
|
+
remaining: {
|
|
114
|
+
type: "integer"
|
|
115
|
+
},
|
|
116
|
+
reset: {
|
|
117
|
+
type: "integer",
|
|
118
|
+
description: "Unix timestamp when the rate limit resets"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
required: [
|
|
122
|
+
"error",
|
|
123
|
+
"limit",
|
|
124
|
+
"remaining",
|
|
125
|
+
"reset"
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
function registerExecuteRoute(app2, deps) {
|
|
129
|
+
const {
|
|
130
|
+
signer: signer2,
|
|
131
|
+
verifier: verifier2,
|
|
132
|
+
auditDb: auditDb2
|
|
133
|
+
} = deps;
|
|
134
|
+
const replayStore = new RedisReplayStore(
|
|
135
|
+
"redis://127.0.0.1:6379"
|
|
136
|
+
);
|
|
137
|
+
app2.post(
|
|
138
|
+
"/execute",
|
|
139
|
+
{
|
|
140
|
+
bodyLimit: 65536,
|
|
141
|
+
config: {
|
|
142
|
+
rateLimit: {
|
|
143
|
+
max: 100,
|
|
144
|
+
timeWindow: "1 minute"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
schema: {
|
|
148
|
+
tags: [
|
|
149
|
+
"Execution"
|
|
150
|
+
],
|
|
151
|
+
summary: "Execute deterministic governance decision",
|
|
152
|
+
description: "Executes deterministic governance evaluation using governed signals and replay-safe execution semantics.",
|
|
153
|
+
security: [
|
|
154
|
+
{ bearerAuth: [] }
|
|
155
|
+
],
|
|
156
|
+
body: {
|
|
157
|
+
type: "object",
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
properties: {
|
|
160
|
+
policyId: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Policy identifier"
|
|
163
|
+
},
|
|
164
|
+
policyVersion: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Policy version"
|
|
167
|
+
},
|
|
168
|
+
signals: {
|
|
169
|
+
type: "object",
|
|
170
|
+
description: "Governed deterministic input signals"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
required: [
|
|
174
|
+
"policyId",
|
|
175
|
+
"policyVersion",
|
|
176
|
+
"signals"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
response: {
|
|
180
|
+
200: {
|
|
181
|
+
description: "Deterministic execution result",
|
|
182
|
+
...S_EXECUTION_OUTPUT
|
|
183
|
+
},
|
|
184
|
+
400: {
|
|
185
|
+
description: "Invalid request body",
|
|
186
|
+
...S_ERROR
|
|
187
|
+
},
|
|
188
|
+
413: {
|
|
189
|
+
description: "Request body too large",
|
|
190
|
+
...S_ERROR
|
|
191
|
+
},
|
|
192
|
+
422: {
|
|
193
|
+
description: "Execution failure",
|
|
194
|
+
...S_ERROR
|
|
195
|
+
},
|
|
196
|
+
429: {
|
|
197
|
+
description: "Rate limit exceeded",
|
|
198
|
+
...S_RATE_LIMIT_ERROR
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
async (req, reply) => {
|
|
204
|
+
const {
|
|
205
|
+
policyId,
|
|
206
|
+
policyVersion,
|
|
207
|
+
signals
|
|
208
|
+
} = req.body;
|
|
209
|
+
try {
|
|
210
|
+
const execution = await executeFromSignals(
|
|
211
|
+
{
|
|
212
|
+
policyId,
|
|
213
|
+
policyVersion,
|
|
214
|
+
signals
|
|
215
|
+
},
|
|
216
|
+
signer2,
|
|
217
|
+
verifier2,
|
|
218
|
+
replayStore
|
|
219
|
+
);
|
|
220
|
+
auditDb2?.recordDecision(
|
|
221
|
+
execution
|
|
222
|
+
);
|
|
223
|
+
req.log.info(
|
|
224
|
+
{
|
|
225
|
+
reqId: req.id,
|
|
226
|
+
policyId,
|
|
227
|
+
policyVersion
|
|
228
|
+
},
|
|
229
|
+
"execute:success"
|
|
230
|
+
);
|
|
231
|
+
reply.send(
|
|
232
|
+
execution
|
|
233
|
+
);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
req.log.warn(
|
|
236
|
+
{
|
|
237
|
+
reqId: req.id,
|
|
238
|
+
error: err.message
|
|
239
|
+
},
|
|
240
|
+
"execute:failure"
|
|
241
|
+
);
|
|
242
|
+
reply.code(422).send({
|
|
243
|
+
error: err.message
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/routes/verify.ts
|
|
251
|
+
import {
|
|
252
|
+
verifyAttestation
|
|
253
|
+
} from "@parmanasystems/verifier";
|
|
254
|
+
var S_ATTESTATION = {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
execution_id: {
|
|
258
|
+
type: "string"
|
|
259
|
+
},
|
|
260
|
+
decision: {
|
|
261
|
+
type: "object"
|
|
262
|
+
},
|
|
263
|
+
execution_state: {
|
|
264
|
+
type: "string"
|
|
265
|
+
},
|
|
266
|
+
runtime_hash: {
|
|
267
|
+
type: "string"
|
|
268
|
+
},
|
|
269
|
+
signature: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Base64 Ed25519 signature over the canonical attestation payload"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
required: [
|
|
275
|
+
"execution_id",
|
|
276
|
+
"decision",
|
|
277
|
+
"execution_state",
|
|
278
|
+
"runtime_hash",
|
|
279
|
+
"signature"
|
|
280
|
+
]
|
|
281
|
+
};
|
|
282
|
+
var S_VERIFICATION_RESULT = {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
valid: {
|
|
286
|
+
type: "boolean"
|
|
287
|
+
},
|
|
288
|
+
checks: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
signature_verified: {
|
|
292
|
+
type: "boolean"
|
|
293
|
+
},
|
|
294
|
+
runtime_verified: {
|
|
295
|
+
type: "boolean"
|
|
296
|
+
},
|
|
297
|
+
schema_compatible: {
|
|
298
|
+
type: "boolean"
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
required: [
|
|
302
|
+
"signature_verified",
|
|
303
|
+
"runtime_verified",
|
|
304
|
+
"schema_compatible"
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
required: [
|
|
309
|
+
"valid",
|
|
310
|
+
"checks"
|
|
311
|
+
]
|
|
312
|
+
};
|
|
313
|
+
var S_ERROR2 = {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
error: {
|
|
317
|
+
type: "string"
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
required: [
|
|
321
|
+
"error"
|
|
322
|
+
]
|
|
323
|
+
};
|
|
324
|
+
var S_RATE_LIMIT_ERROR2 = {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {
|
|
327
|
+
error: {
|
|
328
|
+
type: "string"
|
|
329
|
+
},
|
|
330
|
+
limit: {
|
|
331
|
+
type: "integer"
|
|
332
|
+
},
|
|
333
|
+
remaining: {
|
|
334
|
+
type: "integer"
|
|
335
|
+
},
|
|
336
|
+
reset: {
|
|
337
|
+
type: "integer",
|
|
338
|
+
description: "Unix timestamp when the rate limit resets"
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
required: [
|
|
342
|
+
"error",
|
|
343
|
+
"limit",
|
|
344
|
+
"remaining",
|
|
345
|
+
"reset"
|
|
346
|
+
]
|
|
347
|
+
};
|
|
348
|
+
function registerVerifyRoute(app2, deps) {
|
|
349
|
+
const {
|
|
350
|
+
verifier: verifier2,
|
|
351
|
+
runtimeManifest: runtimeManifest2,
|
|
352
|
+
auditDb: auditDb2
|
|
353
|
+
} = deps;
|
|
354
|
+
app2.post(
|
|
355
|
+
"/verify",
|
|
356
|
+
{
|
|
357
|
+
bodyLimit: 65536,
|
|
358
|
+
config: {
|
|
359
|
+
rateLimit: {
|
|
360
|
+
max: 200,
|
|
361
|
+
timeWindow: "1 minute"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
schema: {
|
|
365
|
+
tags: [
|
|
366
|
+
"Verification"
|
|
367
|
+
],
|
|
368
|
+
summary: "Verify an execution attestation",
|
|
369
|
+
description: "Checks the cryptographic signature and runtime provenance of a deterministic execution attestation.",
|
|
370
|
+
security: [
|
|
371
|
+
{ bearerAuth: [] }
|
|
372
|
+
],
|
|
373
|
+
body: {
|
|
374
|
+
...S_ATTESTATION,
|
|
375
|
+
additionalProperties: false,
|
|
376
|
+
description: "Canonical flattened ExecutionAttestation"
|
|
377
|
+
},
|
|
378
|
+
response: {
|
|
379
|
+
200: {
|
|
380
|
+
description: "Verification result with per-check breakdown",
|
|
381
|
+
...S_VERIFICATION_RESULT
|
|
382
|
+
},
|
|
383
|
+
400: {
|
|
384
|
+
description: "Malformed attestation body",
|
|
385
|
+
...S_ERROR2
|
|
386
|
+
},
|
|
387
|
+
413: {
|
|
388
|
+
description: "Request body too large",
|
|
389
|
+
...S_ERROR2
|
|
390
|
+
},
|
|
391
|
+
422: {
|
|
392
|
+
description: "Verification threw an unexpected error",
|
|
393
|
+
...S_ERROR2
|
|
394
|
+
},
|
|
395
|
+
429: {
|
|
396
|
+
description: "Rate limit exceeded",
|
|
397
|
+
...S_RATE_LIMIT_ERROR2
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
async (req, reply) => {
|
|
403
|
+
const body = req.body;
|
|
404
|
+
if (typeof body?.execution_id !== "string" || typeof body?.signature !== "string") {
|
|
405
|
+
reply.code(400).send({
|
|
406
|
+
error: "Body must be a valid ExecutionAttestation"
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const result = verifyAttestation(
|
|
412
|
+
body,
|
|
413
|
+
verifier2,
|
|
414
|
+
runtimeManifest2
|
|
415
|
+
);
|
|
416
|
+
auditDb2?.recordVerification(
|
|
417
|
+
body.execution_id,
|
|
418
|
+
result
|
|
419
|
+
);
|
|
420
|
+
req.log.info(
|
|
421
|
+
{
|
|
422
|
+
reqId: req.id,
|
|
423
|
+
valid: result.valid,
|
|
424
|
+
checks: result.checks
|
|
425
|
+
},
|
|
426
|
+
"verify:success"
|
|
427
|
+
);
|
|
428
|
+
reply.send(result);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
reply.code(422).send({
|
|
431
|
+
error: err.message
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/routes/audit.ts
|
|
439
|
+
var S_ERROR3 = {
|
|
440
|
+
type: "object",
|
|
441
|
+
properties: { error: { type: "string" } },
|
|
442
|
+
required: ["error"]
|
|
443
|
+
};
|
|
444
|
+
function registerAuditRoutes(app2, auditDb2) {
|
|
445
|
+
app2.get("/audit/decisions", {
|
|
446
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
447
|
+
schema: {
|
|
448
|
+
tags: ["Audit"],
|
|
449
|
+
summary: "Decision timeline",
|
|
450
|
+
description: "Returns a paginated, filtered list of governance decisions joined with their latest verification status. Ordered by executed_at descending.",
|
|
451
|
+
security: [{ bearerAuth: [] }],
|
|
452
|
+
querystring: {
|
|
453
|
+
type: "object",
|
|
454
|
+
properties: {
|
|
455
|
+
limit: {
|
|
456
|
+
type: "integer",
|
|
457
|
+
minimum: 1,
|
|
458
|
+
maximum: 1e3,
|
|
459
|
+
default: 50,
|
|
460
|
+
description: "Max rows (default 50, max 1000)"
|
|
461
|
+
},
|
|
462
|
+
offset: {
|
|
463
|
+
type: "integer",
|
|
464
|
+
minimum: 0,
|
|
465
|
+
default: 0,
|
|
466
|
+
description: "Row offset for pagination (default 0)"
|
|
467
|
+
},
|
|
468
|
+
policy_id: {
|
|
469
|
+
type: "string",
|
|
470
|
+
maxLength: 200,
|
|
471
|
+
description: "Filter by exact policy_id"
|
|
472
|
+
},
|
|
473
|
+
decision: {
|
|
474
|
+
type: "string",
|
|
475
|
+
enum: ["approve", "deny", "any"],
|
|
476
|
+
description: "Filter by decision value; 'any' returns all"
|
|
477
|
+
},
|
|
478
|
+
from: {
|
|
479
|
+
type: "string",
|
|
480
|
+
format: "date-time",
|
|
481
|
+
description: "ISO 8601 lower bound on executed_at (inclusive)"
|
|
482
|
+
},
|
|
483
|
+
to: {
|
|
484
|
+
type: "string",
|
|
485
|
+
format: "date-time",
|
|
486
|
+
description: "ISO 8601 upper bound on executed_at (inclusive)"
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
response: {
|
|
491
|
+
200: { description: "Decision timeline rows", type: "array", items: { type: "object" } },
|
|
492
|
+
400: { description: "Invalid query parameters", ...S_ERROR3 },
|
|
493
|
+
500: S_ERROR3
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}, async (req, reply) => {
|
|
497
|
+
const { limit, offset: _offset, policy_id, decision, from, to } = req.query;
|
|
498
|
+
const parsedLimit = parseInt(String(limit ?? 50), 10);
|
|
499
|
+
const normalizedDecision = decision === "any" ? void 0 : decision;
|
|
500
|
+
try {
|
|
501
|
+
reply.send(await auditDb2.getDecisionTimeline(parsedLimit, {
|
|
502
|
+
policy_id: policy_id || void 0,
|
|
503
|
+
decision: normalizedDecision,
|
|
504
|
+
from_date: from || void 0,
|
|
505
|
+
to_date: to || void 0
|
|
506
|
+
}));
|
|
507
|
+
} catch (err) {
|
|
508
|
+
reply.code(500).send({ error: err.message });
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
app2.get("/audit/decisions/:executionId", {
|
|
512
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
513
|
+
schema: {
|
|
514
|
+
tags: ["Audit"],
|
|
515
|
+
summary: "Decision detail",
|
|
516
|
+
description: "Returns the full audit record for a single governance decision, including the stored attestation JSONB.",
|
|
517
|
+
security: [{ bearerAuth: [] }],
|
|
518
|
+
params: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: { executionId: { type: "string", minLength: 1, maxLength: 200 } },
|
|
521
|
+
required: ["executionId"]
|
|
522
|
+
},
|
|
523
|
+
response: {
|
|
524
|
+
200: { description: "Audit decision record", type: "object" },
|
|
525
|
+
400: { description: "Invalid path parameters", ...S_ERROR3 },
|
|
526
|
+
404: S_ERROR3,
|
|
527
|
+
500: S_ERROR3
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}, async (req, reply) => {
|
|
531
|
+
try {
|
|
532
|
+
const decision = await auditDb2.getDecisionById(req.params.executionId);
|
|
533
|
+
if (!decision) {
|
|
534
|
+
reply.code(404).send({ error: "Decision not found" });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
reply.send(decision);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
reply.code(500).send({ error: err.message });
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
app2.get("/audit/security", {
|
|
543
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
544
|
+
schema: {
|
|
545
|
+
tags: ["Audit"],
|
|
546
|
+
summary: "Security dashboard",
|
|
547
|
+
description: "Returns aggregated security event counts grouped by event_type and severity, ordered by event_count descending.",
|
|
548
|
+
security: [{ bearerAuth: [] }],
|
|
549
|
+
querystring: {
|
|
550
|
+
type: "object",
|
|
551
|
+
properties: {
|
|
552
|
+
from: {
|
|
553
|
+
type: "string",
|
|
554
|
+
format: "date-time",
|
|
555
|
+
description: "ISO 8601 lower bound on occurred_at (inclusive)"
|
|
556
|
+
},
|
|
557
|
+
to: {
|
|
558
|
+
type: "string",
|
|
559
|
+
format: "date-time",
|
|
560
|
+
description: "ISO 8601 upper bound on occurred_at (inclusive)"
|
|
561
|
+
},
|
|
562
|
+
limit: {
|
|
563
|
+
type: "integer",
|
|
564
|
+
minimum: 1,
|
|
565
|
+
maximum: 1e3,
|
|
566
|
+
default: 50,
|
|
567
|
+
description: "Max rows (default 50, max 1000)"
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
response: {
|
|
572
|
+
200: { description: "Security event summary", type: "array", items: { type: "object" } },
|
|
573
|
+
400: { description: "Invalid query parameters", ...S_ERROR3 },
|
|
574
|
+
500: S_ERROR3
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}, async (_req, reply) => {
|
|
578
|
+
try {
|
|
579
|
+
reply.send(await auditDb2.getSecurityDashboard());
|
|
580
|
+
} catch (err) {
|
|
581
|
+
reply.code(500).send({ error: err.message });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
app2.get("/audit/stats", {
|
|
585
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
586
|
+
schema: {
|
|
587
|
+
tags: ["Audit"],
|
|
588
|
+
summary: "Audit statistics",
|
|
589
|
+
description: "Returns aggregate counts across all audit tables: total decisions, decisions today, verifications (valid/invalid), security events, and API access calls.",
|
|
590
|
+
security: [{ bearerAuth: [] }],
|
|
591
|
+
response: {
|
|
592
|
+
200: { description: "Aggregate audit statistics", type: "object" },
|
|
593
|
+
500: S_ERROR3
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}, async (_req, reply) => {
|
|
597
|
+
try {
|
|
598
|
+
reply.send(await auditDb2.getStats());
|
|
599
|
+
} catch (err) {
|
|
600
|
+
reply.code(500).send({ error: err.message });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
app2.get("/audit/verifications/:executionId", {
|
|
604
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
605
|
+
schema: {
|
|
606
|
+
tags: ["Audit"],
|
|
607
|
+
summary: "Verification history",
|
|
608
|
+
description: "Returns all verification attempts for a given execution ID, newest first.",
|
|
609
|
+
security: [{ bearerAuth: [] }],
|
|
610
|
+
params: {
|
|
611
|
+
type: "object",
|
|
612
|
+
properties: { executionId: { type: "string", format: "uuid" } },
|
|
613
|
+
required: ["executionId"]
|
|
614
|
+
},
|
|
615
|
+
response: {
|
|
616
|
+
200: { description: "Verification records", type: "array", items: { type: "object" } },
|
|
617
|
+
500: S_ERROR3
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}, async (req, reply) => {
|
|
621
|
+
try {
|
|
622
|
+
reply.send(await auditDb2.getVerificationsByExecution(req.params.executionId));
|
|
623
|
+
} catch (err) {
|
|
624
|
+
reply.code(500).send({ error: err.message });
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/routes.ts
|
|
630
|
+
var S_ERROR4 = {
|
|
631
|
+
type: "object",
|
|
632
|
+
properties: { error: { type: "string" } },
|
|
633
|
+
required: ["error"]
|
|
634
|
+
};
|
|
635
|
+
var S_NOT_IMPLEMENTED = {
|
|
636
|
+
...S_ERROR4,
|
|
637
|
+
properties: { error: { type: "string", enum: ["Not implemented"] } }
|
|
638
|
+
};
|
|
639
|
+
function checkRuntime() {
|
|
640
|
+
try {
|
|
641
|
+
getRuntimeManifest2();
|
|
642
|
+
return "ok";
|
|
643
|
+
} catch {
|
|
644
|
+
return "error";
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function checkSigningKey() {
|
|
648
|
+
if (process.env.Parmana_PRIVATE_KEY) return "ok";
|
|
649
|
+
if (existsSync("./dev-keys/bundle_signing_key")) return "ok";
|
|
650
|
+
return "unconfigured";
|
|
651
|
+
}
|
|
652
|
+
async function checkAuditDb(db) {
|
|
653
|
+
if (!db) return "unconfigured";
|
|
654
|
+
try {
|
|
655
|
+
await db.ping();
|
|
656
|
+
return "ok";
|
|
657
|
+
} catch {
|
|
658
|
+
return "unavailable";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function registerRoutes(app2, deps) {
|
|
662
|
+
const { signer: signer2, verifier: verifier2, runtimeManifest: runtimeManifest2, auditDb: auditDb2 } = deps;
|
|
663
|
+
app2.get("/health", {
|
|
664
|
+
config: { rateLimit: { max: 300, timeWindow: "1 minute" } },
|
|
665
|
+
schema: {
|
|
666
|
+
tags: ["Runtime"],
|
|
667
|
+
summary: "Health check",
|
|
668
|
+
response: {
|
|
669
|
+
200: {
|
|
670
|
+
description: "Server health status with per-subsystem checks",
|
|
671
|
+
type: "object",
|
|
672
|
+
properties: {
|
|
673
|
+
status: { type: "string", enum: ["ok", "degraded"] },
|
|
674
|
+
version: { type: "string" },
|
|
675
|
+
timestamp: { type: "string", format: "date-time" },
|
|
676
|
+
checks: {
|
|
677
|
+
type: "object",
|
|
678
|
+
properties: {
|
|
679
|
+
runtime_manifest: { type: "string", enum: ["ok", "error"] },
|
|
680
|
+
signing_key: { type: "string", enum: ["ok", "unconfigured"] },
|
|
681
|
+
audit_db: { type: "string", enum: ["ok", "unavailable", "unconfigured"] }
|
|
682
|
+
},
|
|
683
|
+
required: ["runtime_manifest", "signing_key", "audit_db"]
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
required: ["status", "version", "timestamp", "checks"]
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}, async () => {
|
|
691
|
+
const checks = {
|
|
692
|
+
runtime_manifest: checkRuntime(),
|
|
693
|
+
signing_key: checkSigningKey(),
|
|
694
|
+
audit_db: await checkAuditDb(auditDb2)
|
|
695
|
+
};
|
|
696
|
+
const degraded = Object.values(checks).some((v) => v === "error" || v === "unavailable");
|
|
697
|
+
return {
|
|
698
|
+
status: degraded ? "degraded" : "ok",
|
|
699
|
+
version: "1.2.3",
|
|
700
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
701
|
+
checks
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
registerExecuteRoute(app2, { signer: signer2, verifier: verifier2, auditDb: auditDb2 });
|
|
705
|
+
registerVerifyRoute(app2, { verifier: verifier2, runtimeManifest: runtimeManifest2, auditDb: auditDb2 });
|
|
706
|
+
if (auditDb2) {
|
|
707
|
+
registerAuditRoutes(app2, auditDb2);
|
|
708
|
+
}
|
|
709
|
+
const stub = async (_req, reply) => {
|
|
710
|
+
reply.code(501).send({ error: "Not implemented" });
|
|
711
|
+
};
|
|
712
|
+
app2.get("/runtime/manifest", {
|
|
713
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
714
|
+
schema: {
|
|
715
|
+
tags: ["Runtime"],
|
|
716
|
+
summary: "Runtime bundle manifest",
|
|
717
|
+
description: "Returns the signed bundle manifest for the active governance runtime.",
|
|
718
|
+
security: [{ bearerAuth: [] }],
|
|
719
|
+
response: { 501: { description: "Not yet implemented", ...S_NOT_IMPLEMENTED } }
|
|
720
|
+
}
|
|
721
|
+
}, stub);
|
|
722
|
+
app2.get("/runtime/capabilities", {
|
|
723
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
724
|
+
schema: {
|
|
725
|
+
tags: ["Runtime"],
|
|
726
|
+
summary: "Runtime capability declarations",
|
|
727
|
+
description: "Lists the capabilities supported by this runtime instance.",
|
|
728
|
+
security: [{ bearerAuth: [] }],
|
|
729
|
+
response: { 501: { description: "Not yet implemented", ...S_NOT_IMPLEMENTED } }
|
|
730
|
+
}
|
|
731
|
+
}, stub);
|
|
732
|
+
app2.post("/evaluate", {
|
|
733
|
+
bodyLimit: 65536,
|
|
734
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
735
|
+
schema: {
|
|
736
|
+
tags: ["Execution"],
|
|
737
|
+
summary: "Evaluate a policy without executing",
|
|
738
|
+
description: "Dry-run policy evaluation \u2014 computes a decision without issuing an attestation or consuming a replay slot.",
|
|
739
|
+
security: [{ bearerAuth: [] }],
|
|
740
|
+
body: { type: "object" },
|
|
741
|
+
response: { 501: { description: "Not yet implemented", ...S_NOT_IMPLEMENTED } }
|
|
742
|
+
}
|
|
743
|
+
}, stub);
|
|
744
|
+
app2.post("/simulate", {
|
|
745
|
+
bodyLimit: 65536,
|
|
746
|
+
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
747
|
+
schema: {
|
|
748
|
+
tags: ["Execution"],
|
|
749
|
+
summary: "Simulate a governance decision dry-run",
|
|
750
|
+
description: "Runs the full execution pipeline in simulation mode \u2014 no side effects, no attestation produced.",
|
|
751
|
+
security: [{ bearerAuth: [] }],
|
|
752
|
+
body: { type: "object" },
|
|
753
|
+
response: { 501: { description: "Not yet implemented", ...S_NOT_IMPLEMENTED } }
|
|
754
|
+
}
|
|
755
|
+
}, stub);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/server.ts
|
|
759
|
+
import { AuditDb } from "@parmanasystems/audit-db";
|
|
760
|
+
|
|
761
|
+
// src/middleware/audit.ts
|
|
762
|
+
function createAuditMiddleware(auditDb2) {
|
|
763
|
+
return async function onResponse(req, reply) {
|
|
764
|
+
auditDb2.recordApiAccess({
|
|
765
|
+
method: req.method,
|
|
766
|
+
path: req.url,
|
|
767
|
+
status_code: reply.statusCode,
|
|
768
|
+
response_time_ms: Number.isFinite(reply.elapsedTime) ? Math.round(reply.elapsedTime) : void 0,
|
|
769
|
+
ip_address: req.ip,
|
|
770
|
+
user_agent: req.headers["user-agent"]
|
|
771
|
+
});
|
|
772
|
+
if (reply.statusCode === 401) {
|
|
773
|
+
auditDb2.recordSecurityEvent({
|
|
774
|
+
event_type: "auth_failure",
|
|
775
|
+
severity: "medium",
|
|
776
|
+
ip_address: req.ip,
|
|
777
|
+
path: req.url,
|
|
778
|
+
method: req.method,
|
|
779
|
+
user_agent: req.headers["user-agent"],
|
|
780
|
+
details: { status_code: 401 }
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/server.ts
|
|
787
|
+
function createServer() {
|
|
788
|
+
const app2 = Fastify({
|
|
789
|
+
bodyLimit: 1048576,
|
|
790
|
+
// 1 MB global default
|
|
791
|
+
logger: {
|
|
792
|
+
level: process.env.LOG_LEVEL ?? (process.env.NODE_ENV === "production" ? "info" : "debug"),
|
|
793
|
+
redact: {
|
|
794
|
+
paths: [
|
|
795
|
+
"req.headers.authorization",
|
|
796
|
+
"req.body.signature",
|
|
797
|
+
"req.body.attestation.signature"
|
|
798
|
+
],
|
|
799
|
+
censor: "[REDACTED]"
|
|
800
|
+
},
|
|
801
|
+
serializers: {
|
|
802
|
+
req(req) {
|
|
803
|
+
const fwd = req.headers["x-forwarded-for"];
|
|
804
|
+
return {
|
|
805
|
+
method: req.method,
|
|
806
|
+
url: req.url,
|
|
807
|
+
reqId: req.id,
|
|
808
|
+
remoteAddress: (Array.isArray(fwd) ? fwd[0] : fwd) ?? req.socket?.remoteAddress
|
|
809
|
+
};
|
|
810
|
+
},
|
|
811
|
+
res(res) {
|
|
812
|
+
return {
|
|
813
|
+
statusCode: res.statusCode
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
genReqId: (req) => req.headers["x-request-id"] ?? randomUUID()
|
|
819
|
+
});
|
|
820
|
+
const corsOriginEnv = process.env.CORS_ORIGIN;
|
|
821
|
+
const corsOrigin = corsOriginEnv === "*" ? true : corsOriginEnv ? corsOriginEnv.split(",").map((o) => o.trim()) : ["http://localhost:5173", "http://localhost:8080"];
|
|
822
|
+
app2.register(cors, {
|
|
823
|
+
origin: corsOrigin,
|
|
824
|
+
methods: ["GET", "POST"],
|
|
825
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
|
|
826
|
+
exposedHeaders: ["X-Request-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"],
|
|
827
|
+
credentials: true,
|
|
828
|
+
maxAge: 86400
|
|
829
|
+
});
|
|
830
|
+
app2.register(helmet, {
|
|
831
|
+
contentSecurityPolicy: {
|
|
832
|
+
directives: {
|
|
833
|
+
defaultSrc: ["'none'"],
|
|
834
|
+
frameAncestors: ["'none'"]
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
crossOriginEmbedderPolicy: false,
|
|
838
|
+
crossOriginResourcePolicy: { policy: "cross-origin" },
|
|
839
|
+
dnsPrefetchControl: { allow: false },
|
|
840
|
+
frameguard: { action: "deny" },
|
|
841
|
+
hsts: {
|
|
842
|
+
maxAge: 31536e3,
|
|
843
|
+
includeSubDomains: true,
|
|
844
|
+
preload: true
|
|
845
|
+
},
|
|
846
|
+
ieNoOpen: true,
|
|
847
|
+
noSniff: true,
|
|
848
|
+
referrerPolicy: { policy: "no-referrer" },
|
|
849
|
+
xssFilter: true
|
|
850
|
+
});
|
|
851
|
+
const auditDb2 = process.env.AUDIT_DATABASE_URL ? new AuditDb(process.env.AUDIT_DATABASE_URL) : void 0;
|
|
852
|
+
if (auditDb2) {
|
|
853
|
+
auditDb2.migrate().catch(
|
|
854
|
+
(err) => app2.log.error({ err }, "audit-db migration failed \u2014 continuing without audit persistence")
|
|
855
|
+
);
|
|
856
|
+
app2.addHook("onResponse", createAuditMiddleware(auditDb2));
|
|
857
|
+
}
|
|
858
|
+
app2.addHook("onSend", async (req, reply) => {
|
|
859
|
+
reply.header("X-Request-ID", req.id);
|
|
860
|
+
});
|
|
861
|
+
app2.addHook("onSend", async (_req, reply) => {
|
|
862
|
+
reply.removeHeader("X-Powered-By");
|
|
863
|
+
});
|
|
864
|
+
app2.addHook("preHandler", authHook);
|
|
865
|
+
const apiKeyHash = process.env.Parmana_API_KEY ? createHash("sha256").update(process.env.Parmana_API_KEY).digest("hex") : null;
|
|
866
|
+
app2.register(rateLimit, {
|
|
867
|
+
keyGenerator(req) {
|
|
868
|
+
if (apiKeyHash) return apiKeyHash;
|
|
869
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
870
|
+
const forwardedStr = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
871
|
+
const forwardedIp = forwardedStr?.split(",")[0]?.trim();
|
|
872
|
+
const realIp = req.headers["x-real-ip"];
|
|
873
|
+
const realIpStr = Array.isArray(realIp) ? realIp[0] : realIp;
|
|
874
|
+
return forwardedIp ?? realIpStr ?? req.ip;
|
|
875
|
+
},
|
|
876
|
+
errorResponseBuilder(_req, context) {
|
|
877
|
+
return {
|
|
878
|
+
error: "Rate limit exceeded",
|
|
879
|
+
limit: context.max,
|
|
880
|
+
remaining: 0,
|
|
881
|
+
reset: Math.floor(Date.now() / 1e3) + Math.ceil(context.ttl / 1e3)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
registerRoutes(app2, { signer, verifier, runtimeManifest, auditDb: auditDb2 });
|
|
886
|
+
return { app: app2, auditDb: auditDb2 };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/index.ts
|
|
890
|
+
var port = parseInt(process.env.PORT ?? "3000", 10);
|
|
891
|
+
var host = process.env.HOST ?? "0.0.0.0";
|
|
892
|
+
var { app, auditDb } = createServer();
|
|
893
|
+
async function shutdown(signal) {
|
|
894
|
+
app.log.info(`Shutdown signal received (${signal}), closing server...`);
|
|
895
|
+
const shutdownTimeout = setTimeout(() => {
|
|
896
|
+
app.log.error("Graceful shutdown timed out, forcing exit");
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}, 1e4);
|
|
899
|
+
shutdownTimeout.unref();
|
|
900
|
+
try {
|
|
901
|
+
await app.close();
|
|
902
|
+
if (auditDb) {
|
|
903
|
+
await auditDb.disconnect();
|
|
904
|
+
}
|
|
905
|
+
clearTimeout(shutdownTimeout);
|
|
906
|
+
app.log.info("Server closed cleanly");
|
|
907
|
+
process.exit(0);
|
|
908
|
+
} catch (err) {
|
|
909
|
+
app.log.error({ err }, "Error during shutdown");
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
process.on("SIGTERM", () => {
|
|
914
|
+
void shutdown("SIGTERM");
|
|
915
|
+
});
|
|
916
|
+
process.on("SIGINT", () => {
|
|
917
|
+
void shutdown("SIGINT");
|
|
918
|
+
});
|
|
919
|
+
async function startServer() {
|
|
920
|
+
try {
|
|
921
|
+
await app.listen({ port, host });
|
|
922
|
+
app.log.info(`Server listening on http://${host}:${port}`);
|
|
923
|
+
if (auditDb) {
|
|
924
|
+
app.log.info("Audit DB connected");
|
|
925
|
+
} else {
|
|
926
|
+
app.log.info("Audit DB not configured");
|
|
927
|
+
}
|
|
928
|
+
const keyMsg = {
|
|
929
|
+
env: "Signing key loaded from env",
|
|
930
|
+
disk: "Signing key loaded from disk",
|
|
931
|
+
ephemeral: "Using ephemeral signing key"
|
|
932
|
+
};
|
|
933
|
+
app.log.info(keyMsg[signingKeySource]);
|
|
934
|
+
} catch (err) {
|
|
935
|
+
app.log.error(err);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
export {
|
|
940
|
+
app,
|
|
941
|
+
startServer
|
|
942
|
+
};
|
|
943
|
+
//# sourceMappingURL=index.js.map
|