@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/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