@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/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# @parmanasystems/server
|
|
2
|
+
|
|
3
|
+
Fastify REST API server for the parmanasystems deterministic governance runtime.
|
|
4
|
+
|
|
5
|
+
[](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
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|