@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 ADDED
@@ -0,0 +1,407 @@
1
+ # @parmanasystems/server
2
+
3
+ Fastify REST API server for the parmanasystems deterministic governance runtime.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@parmanasystems/server)](https://www.npmjs.com/package/@parmanasystems/server)
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ `@parmanasystems/server` is an HTTP wrapper over `@parmanasystems/execution`. It exposes the governance execution pipeline over a REST API so that any language or platform can execute and independently verify governance decisions without embedding the TypeScript SDK directly.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @parmanasystems/server
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Quick start
24
+
25
+ ```bash
26
+ # Start with default ephemeral Ed25519 keypair (development)
27
+ npx Parmana-server
28
+
29
+ # Start with persistent keys from environment variables
30
+ Parmana_PRIVATE_KEY="$(cat private.pem)" Parmana_PUBLIC_KEY="$(cat public.pem)" npx Parmana-server
31
+
32
+ # Enable API key authentication
33
+ Parmana_API_KEY=my-secret-key npx Parmana-server
34
+ ```
35
+
36
+ The server listens on `http://0.0.0.0:3000` by default.
37
+
38
+ ---
39
+
40
+ ## Environment variables
41
+
42
+ | Variable | Required | Description |
43
+ |---|---|---|
44
+ | `PORT` | No | HTTP port (default: `3000`) |
45
+ | `HOST` | No | Bind address (default: `0.0.0.0`) |
46
+ | `Parmana_API_KEY` | No | When set, all routes require `Authorization: Bearer <key>` |
47
+ | `Parmana_PRIVATE_KEY` | No | PEM-encoded Ed25519 private key for signing |
48
+ | `Parmana_PUBLIC_KEY` | No | PEM-encoded Ed25519 public key for verification |
49
+
50
+ When `Parmana_PRIVATE_KEY` and `Parmana_PUBLIC_KEY` are absent, the server generates an ephemeral Ed25519 keypair on startup. This is suitable for development but means attestations cannot be verified after a restart.
51
+
52
+ ---
53
+
54
+ ## API routes
55
+
56
+ ### `GET /health`
57
+
58
+ Returns runtime status, version, and per-subsystem health checks.
59
+
60
+ ```bash
61
+ curl http://localhost:3000/health
62
+ ```
63
+
64
+ ```json
65
+ {
66
+ "status": "ok",
67
+ "version": "1.2.3",
68
+ "timestamp": "2026-05-03T10:00:00.000Z",
69
+ "checks": {
70
+ "runtime_manifest": "ok",
71
+ "signing_key": "ok",
72
+ "audit_db": "unconfigured"
73
+ }
74
+ }
75
+ ```
76
+
77
+ **`status`** is `"ok"` when all checks are `"ok"` or `"unconfigured"`. It is `"degraded"` if any check is `"error"` or `"unavailable"`. The HTTP status code is always `200` — callers decide how to act on a degraded response.
78
+
79
+ **Check values:**
80
+
81
+ | Check | Values | Meaning |
82
+ |---|---|---|
83
+ | `runtime_manifest` | `ok` / `error` | Whether `getRuntimeManifest()` succeeds |
84
+ | `signing_key` | `ok` / `unconfigured` | `Parmana_PRIVATE_KEY` env var set, or dev key on disk |
85
+ | `audit_db` | `ok` / `unavailable` / `unconfigured` | DB reachable / unreachable / `AUDIT_DATABASE_URL` not set |
86
+
87
+ ---
88
+
89
+ ### `POST /execute`
90
+
91
+ Runs the deterministic governance runtime and returns a signed `ExecutionAttestation`.
92
+
93
+ **Request body validation:**
94
+
95
+ | Field | Type | minLength | maxLength | Pattern |
96
+ |---|---|---|---|---|
97
+ | `policy_id` | string | 1 | 128 | `^[a-zA-Z0-9_-]+$` |
98
+ | `policy_version` | string | 1 | 32 | `^v?\d+(\.\d+){0,2}([-+][\w.-]+)?$` |
99
+ | `decision_type` | string | 1 | 64 | `^[a-zA-Z0-9_-]+$` |
100
+ | `signals_hash` | string | 64 | 64 | `^[0-9a-f]{64}$` |
101
+
102
+ Extra fields not listed above are rejected with `400 Bad Request` (`additionalProperties: false`).
103
+
104
+ ```bash
105
+ curl -X POST http://localhost:3000/execute \
106
+ -H "Content-Type: application/json" \
107
+ -d '{
108
+ "policy_id": "loan-approval",
109
+ "policy_version": "v1",
110
+ "decision_type": "approve",
111
+ "signals_hash": "a3f1e2d4b5c6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
112
+ }'
113
+ ```
114
+
115
+ ```json
116
+ {
117
+ "result": {
118
+ "execution_id": "550e8400-e29b-41d4-a716-446655440000",
119
+ "policy_id": "loan-approval",
120
+ "policy_version": "v1",
121
+ "schema_version": "1.0.0",
122
+ "runtime_version": "1.0.0",
123
+ "runtime_hash": "a1b2c3...",
124
+ "decision": "approve",
125
+ "signals_hash": "a3f1e2d4b5c6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
126
+ "executed_at": "2025-05-02T10:00:00.000Z"
127
+ },
128
+ "signature": "base64-encoded-Ed25519-signature"
129
+ }
130
+ ```
131
+
132
+ **Error responses:**
133
+
134
+ | Status | Meaning |
135
+ |---|---|
136
+ | `400` | Missing required fields, failed pattern/length validation, or unexpected fields |
137
+ | `422` | Execution failed (token expired, replay detected, etc.) |
138
+
139
+ ---
140
+
141
+ ### `POST /verify`
142
+
143
+ Independently verifies an `ExecutionAttestation`. Pass the response from `POST /execute` directly.
144
+
145
+ ```bash
146
+ curl -X POST http://localhost:3000/verify \
147
+ -H "Content-Type: application/json" \
148
+ -d '{ "result": { ... }, "signature": "..." }'
149
+ ```
150
+
151
+ ```json
152
+ {
153
+ "valid": true,
154
+ "checks": {
155
+ "signature_verified": true,
156
+ "runtime_verified": true,
157
+ "schema_compatible": true
158
+ }
159
+ }
160
+ ```
161
+
162
+ **Error responses:**
163
+
164
+ | Status | Meaning |
165
+ |---|---|
166
+ | `400` | Malformed attestation body |
167
+ | `422` | Verification threw an unexpected error |
168
+
169
+ ---
170
+
171
+ ### Audit routes (requires `AUDIT_DATABASE_URL`)
172
+
173
+ When `AUDIT_DATABASE_URL` is configured, five read-only audit query routes are registered:
174
+
175
+ | Route | Description |
176
+ |---|---|
177
+ | `GET /audit/decisions` | Decision timeline. Query params: `limit` (default 100), `offset`, `policy_id`, `decision`, `from`, `to` |
178
+ | `GET /audit/decisions/:executionId` | Single decision row with full `ExecutionAttestation` JSONB |
179
+ | `GET /audit/security` | Security event dashboard. Query params: `from`, `to`, `limit` |
180
+ | `GET /audit/stats` | Aggregate counts: total decisions, decisions today, verifications, security events, API calls |
181
+ | `GET /audit/verifications/:executionId` | All verification attempts for an execution, newest first |
182
+
183
+ All audit routes return `404` if the database is not configured.
184
+
185
+ ---
186
+
187
+ ### Stub endpoints (501 Not Implemented)
188
+
189
+ These endpoints are defined in the OpenAPI spec and will be implemented in future releases:
190
+
191
+ | Endpoint | Description |
192
+ |---|---|
193
+ | `GET /runtime/manifest` | Returns the signed bundle manifest for the active runtime |
194
+ | `GET /runtime/capabilities` | Lists runtime capabilities |
195
+ | `POST /evaluate` | Dry-run policy evaluation without attestation |
196
+ | `POST /simulate` | Full simulation mode — no side effects |
197
+
198
+ ---
199
+
200
+ ## Rate limits
201
+
202
+ All routes are rate-limited per API key (when `Parmana_API_KEY` is set) or per client IP (in dev mode).
203
+
204
+ | Route | Limit |
205
+ |---|---|
206
+ | `POST /execute` | 100 req/min |
207
+ | `POST /verify` | 200 req/min |
208
+ | `GET /audit/*` | 60 req/min |
209
+ | `GET /health` | 300 req/min |
210
+ | `GET /runtime/*` | 60 req/min |
211
+ | `POST /evaluate` | 60 req/min |
212
+ | `POST /simulate` | 60 req/min |
213
+
214
+ When authenticated, the rate limit key is `sha256(Parmana_API_KEY)`. In dev mode (no `Parmana_API_KEY`), the key falls back to `X-Forwarded-For` → `X-Real-IP` → socket IP.
215
+
216
+ Every response includes rate limit headers:
217
+
218
+ ```
219
+ X-RateLimit-Limit: 100
220
+ X-RateLimit-Remaining: 99
221
+ X-RateLimit-Reset: 1714640460
222
+ ```
223
+
224
+ **Payload size limits** (enforced independently of rate limits):
225
+
226
+ | Route | Max body |
227
+ |---|---|
228
+ | `POST /execute` | 64 KB |
229
+ | `POST /verify` | 64 KB |
230
+ | `POST /evaluate` | 64 KB |
231
+ | `POST /simulate` | 64 KB |
232
+ | All other routes | 1 MB (global default) |
233
+
234
+ Requests exceeding the limit receive `413 Payload Too Large`. GET routes carry no body so no limit applies.
235
+
236
+ When a rate limit is exceeded the server responds with `429 Too Many Requests`:
237
+
238
+ ```json
239
+ {
240
+ "error": "Rate limit exceeded",
241
+ "limit": 100,
242
+ "remaining": 0,
243
+ "reset": 1714640460
244
+ }
245
+ ```
246
+
247
+ ---
248
+
249
+ ## CORS
250
+
251
+ Cross-origin requests are controlled via the `CORS_ORIGIN` environment variable.
252
+
253
+ | `CORS_ORIGIN` value | Behaviour |
254
+ |---|---|
255
+ | _(not set)_ | Allow `http://localhost:5173` and `http://localhost:8080` (dev default) |
256
+ | `http://localhost:5173,https://app.example.com` | Allow those two origins only |
257
+ | `*` | Reflect any request origin (allows all — use with care in production) |
258
+
259
+ ```bash
260
+ # Single origin
261
+ CORS_ORIGIN=https://app.example.com
262
+
263
+ # Multiple origins (comma-separated, no spaces)
264
+ CORS_ORIGIN=https://app.example.com,https://admin.example.com
265
+ ```
266
+
267
+ **`credentials: true`** is set on all CORS responses. This means browsers will include cookies and `Authorization` headers on cross-origin requests. When using `CORS_ORIGIN=*`, the server reflects the specific request origin rather than sending a literal `*` so that credentials continue to work.
268
+
269
+ Allowed methods: `GET`, `POST`.
270
+ Allowed request headers: `Content-Type`, `Authorization`, `X-Request-ID`.
271
+ Exposed response headers: `X-Request-ID`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.
272
+ Preflight cache (`Access-Control-Max-Age`): **86400 s** (24 h).
273
+
274
+ ---
275
+
276
+ ## Security headers
277
+
278
+ All responses include the following HTTP security headers, set via [`@fastify/helmet`](https://github.com/fastify/fastify-helmet):
279
+
280
+ | Header | Value | Purpose |
281
+ |---|---|---|
282
+ | `Content-Security-Policy` | `default-src 'none'; frame-ancestors 'none'` | Blocks all resource loading and framing — this is a pure API, not a browser app |
283
+ | `Cross-Origin-Resource-Policy` | `cross-origin` | Allows cross-origin reads (needed for browser clients consuming the API) |
284
+ | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` | Enforces HTTPS for 1 year across all subdomains; eligible for HSTS preload list |
285
+ | `X-Content-Type-Options` | `nosniff` | Prevents MIME-type sniffing |
286
+ | `X-Frame-Options` | `DENY` | Blocks the response from being embedded in a frame |
287
+ | `X-DNS-Prefetch-Control` | `off` | Disables DNS prefetching |
288
+ | `Referrer-Policy` | `no-referrer` | Suppresses the `Referer` header on all requests |
289
+ | `X-Download-Options` | `noopen` | Prevents IE from executing downloaded files in the browser context |
290
+ | `X-XSS-Protection` | `0` | Disables the legacy XSS auditor (modern browsers ignore it; CSP is the correct control) |
291
+ | `X-Powered-By` | _(removed)_ | Suppressed to avoid leaking server technology |
292
+
293
+ `Cross-Origin-Embedder-Policy` is intentionally disabled — it would block cross-origin API requests from browser clients that have not opted in to COEP.
294
+
295
+ ---
296
+
297
+ ## Graceful shutdown
298
+
299
+ The server handles `SIGTERM` and `SIGINT` (Ctrl-C) with a clean, ordered shutdown:
300
+
301
+ 1. Stops accepting new connections (`app.close()`).
302
+ 2. Waits for in-flight requests to complete.
303
+ 3. Closes the PostgreSQL audit pool (`auditDb.disconnect()`), if configured.
304
+ 4. Logs `"Server closed cleanly"` and exits `0`.
305
+
306
+ If shutdown takes longer than **10 seconds** the process force-exits with code `1` and logs `"Graceful shutdown timed out, forcing exit"`. The timeout is `unref()`-ed so it does not extend the process lifetime on its own.
307
+
308
+ ```
309
+ SIGTERM/SIGINT
310
+ └─ app.close() — drain in-flight HTTP requests
311
+ └─ auditDb.disconnect() — close postgres pool (if configured)
312
+ └─ process.exit(0)
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Logging
318
+
319
+ The server uses [pino](https://getpino.io/) structured JSON logging via Fastify.
320
+
321
+ ### Log level
322
+
323
+ | `LOG_LEVEL` | Default |
324
+ |---|---|
325
+ | `trace` / `debug` / `info` / `warn` / `error` / `fatal` | `debug` in development, `info` in production |
326
+
327
+ Set `NODE_ENV=production` or `LOG_LEVEL=info` to suppress debug output. `LOG_LEVEL` takes priority over `NODE_ENV`.
328
+
329
+ ### Request ID (`X-Request-ID`)
330
+
331
+ Every request gets a unique ID. The server:
332
+
333
+ - Reuses `X-Request-ID` from the incoming request if present.
334
+ - Generates a `crypto.randomUUID()` otherwise.
335
+ - Echoes the ID back in the `X-Request-ID` response header.
336
+ - Includes `reqId` in every structured log line for that request.
337
+
338
+ ```bash
339
+ curl -H "X-Request-ID: my-trace-id" http://localhost:3000/health
340
+ # Response header: X-Request-ID: my-trace-id
341
+ ```
342
+
343
+ ### Redacted fields
344
+
345
+ The following fields are replaced with `[REDACTED]` in all log output:
346
+
347
+ | Field | Why |
348
+ |---|---|
349
+ | `req.headers.authorization` | Bearer token must not appear in logs |
350
+ | `req.body.signature` | Ed25519 signature is key material |
351
+ | `req.body.attestation.signature` | Nested signature in verify requests |
352
+
353
+ ### Structured log events
354
+
355
+ | Event | Level | Fields |
356
+ |---|---|---|
357
+ | Governance decision executed | `info` | `reqId`, `policy_id`, `policy_version`, `decision_type` |
358
+ | Governance decision failed | `warn` | `reqId`, `error` |
359
+ | Attestation verified | `info` | `reqId`, `valid`, `checks` |
360
+ | Authentication failure | `warn` | `reqId`, `reason: "auth_failure"` |
361
+
362
+ ---
363
+
364
+ ## Authentication
365
+
366
+ When `Parmana_API_KEY` is set, all requests must include:
367
+
368
+ ```
369
+ Authorization: Bearer <your-api-key>
370
+ ```
371
+
372
+ Requests without a valid bearer token receive `401 Unauthorized`.
373
+
374
+ When `Parmana_API_KEY` is unset, the auth hook is disabled. This is the default development mode.
375
+
376
+ ---
377
+
378
+ ## OpenAPI specification
379
+
380
+ The full OpenAPI 3.0.3 specification is available at [`openapi.json`](../../openapi.json) in the repository root.
381
+
382
+ To regenerate it:
383
+
384
+ ```bash
385
+ npx tsx scripts/export-openapi.ts
386
+ ```
387
+
388
+ ---
389
+
390
+ ## Programmatic usage
391
+
392
+ ```ts
393
+ import { createServer } from "@parmanasystems/server";
394
+
395
+ const { app, auditDb } = await createServer();
396
+ await app.listen({ port: 3000, host: "0.0.0.0" });
397
+
398
+ // On shutdown:
399
+ // await auditDb?.disconnect();
400
+ // await app.close();
401
+ ```
402
+
403
+ ---
404
+
405
+ ## License
406
+
407
+ Apache-2.0
@@ -0,0 +1,7 @@
1
+ import * as node_http from 'node:http';
2
+ import * as fastify from 'fastify';
3
+
4
+ declare const app: fastify.FastifyInstance<fastify.RawServerDefault, node_http.IncomingMessage, node_http.ServerResponse<node_http.IncomingMessage>, fastify.FastifyBaseLogger, fastify.FastifyTypeProviderDefault>;
5
+ declare function startServer(): Promise<void>;
6
+
7
+ export { app, startServer };