@consenti/api 0.1.0

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,623 @@
1
+ # @consenti/api
2
+
3
+ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@consenti/api.svg)](https://www.npmjs.com/package/@consenti/api)
6
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../../LICENSE)
7
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
8
+
9
+ ---
10
+
11
+ ## Why this package?
12
+
13
+ - **Three SQLite drivers** — `node:sqlite` (Node 22.5+ built-in, zero install), `node-sqlite3-wasm` (WASM, zero compilation, optional peer dep), `better-sqlite3` (native, optional peer dep)
14
+ - **One function to mount** — `createConsenti()` returns a router you drop into any Node.js HTTP framework
15
+ - **Self-contained admin dashboard** — full SPA served at your `basePath`, no separate deploy
16
+ - **Multi-regulation** — GDPR, CCPA, CPRA, GPC, TCF v2.2, DPDPA, LGPD, PDPA, PIPEDA, APPI, KVKK, POPIA, UK-GDPR
17
+ - **Pluggable storage** — SQLite (zero-config, default) · PostgreSQL · MySQL · MongoDB
18
+
19
+ ---
20
+
21
+ ## Requirements
22
+
23
+ - **Node.js ≥ 20.0.0** — Node 22 or 24 LTS recommended for production
24
+
25
+ ## Which SQLite driver should I use?
26
+
27
+ | Driver | Config | Node requirement | Extra install? |
28
+ |---|---|---|---|
29
+ | `node:sqlite` | `driver: 'node:sqlite'` | ≥ 22.5 (stable in 24) | **No** — Node built-in |
30
+ | `node-sqlite3-wasm` | `driver: 'node-sqlite3-wasm'` | ≥ 20 | Yes — `npm install node-sqlite3-wasm` |
31
+ | `better-sqlite3` | `driver: 'better-sqlite3'` | ≥ 20 | Yes — `npm install better-sqlite3` |
32
+ | `'sqlite'` | `driver: 'sqlite'` | ≥ 20 | Alias for `better-sqlite3` |
33
+
34
+ `node:sqlite` is the only driver that requires no extra install — it ships with Node 22.5+.
35
+ On Node 20/21, choose between `node-sqlite3-wasm` (pure WASM, no compilation) or `better-sqlite3` (native binary, faster but may fail on Alpine/musl/ARM).
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @consenti/api
43
+ ```
44
+
45
+ All SQLite drivers and other database adapters are optional peer dependencies — only install what you need:
46
+
47
+ ```bash
48
+ # SQLite (choose one — node:sqlite needs no install on Node 22.5+)
49
+ npm install node-sqlite3-wasm # WASM SQLite — Node 20+, zero compilation
50
+ npm install better-sqlite3 # native SQLite — Node 20+, faster but needs a C++ toolchain
51
+
52
+ # Other databases
53
+ npm install pg # PostgreSQL
54
+ npm install mysql2 # MySQL
55
+ npm install mongodb # MongoDB
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Quick Start
61
+
62
+ ### Express
63
+
64
+ ```ts
65
+ import express from 'express'
66
+ import { createConsenti } from '@consenti/api'
67
+
68
+ const app = express()
69
+
70
+ const consenti = createConsenti({
71
+ storage: { driver: 'sqlite', path: './consenti.db' },
72
+ auth: {
73
+ mode: 'local',
74
+ adminEmail: 'admin@example.com',
75
+ adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
76
+ },
77
+ dashboard: true,
78
+ })
79
+
80
+ app.use(consenti.router)
81
+
82
+ app.listen(3000, () => {
83
+ console.log('Server → http://localhost:3000')
84
+ console.log('Admin → http://localhost:3000/consenti/')
85
+ console.log('API → http://localhost:3000/consenti/api/v1/')
86
+ })
87
+ ```
88
+
89
+ ### Standalone (no framework)
90
+
91
+ ```ts
92
+ import { createConsenti } from '@consenti/api'
93
+
94
+ const consenti = createConsenti({
95
+ storage: { driver: 'sqlite', path: './consenti.db' },
96
+ auth: {
97
+ mode: 'local',
98
+ adminEmail: 'admin@example.com',
99
+ adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
100
+ },
101
+ dashboard: true,
102
+ })
103
+
104
+ await consenti.start(3001)
105
+ // Admin dashboard → http://localhost:3001/consenti/
106
+ // REST API → http://localhost:3001/consenti/api/v1/
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Framework Adapters
112
+
113
+ ### Express / Connect
114
+
115
+ ```ts
116
+ app.use(consenti.router) // as middleware (calls next() for unmatched routes)
117
+ // — or —
118
+ app.use(consenti.handler) // as a terminal handler (no next())
119
+ ```
120
+
121
+ ### Fastify
122
+
123
+ ```ts
124
+ import Fastify from 'fastify'
125
+ const app = Fastify()
126
+
127
+ const consenti = createConsenti({ /* ... */ })
128
+
129
+ app.all('/consenti/*', async (req, reply) => {
130
+ reply.hijack()
131
+ consenti.handler(req.raw, reply.raw)
132
+ })
133
+ ```
134
+
135
+ ### Next.js App Router
136
+
137
+ ```ts
138
+ // app/consenti/[...path]/route.ts
139
+ import { createConsenti } from '@consenti/api'
140
+ import type { NextRequest } from 'next/server'
141
+
142
+ let consenti: Awaited<ReturnType<typeof createConsenti>>
143
+
144
+ async function getConsenti() {
145
+ if (!consenti) {
146
+ consenti = createConsenti({
147
+ storage: { driver: 'sqlite', path: './consenti.db' },
148
+ auth: {
149
+ mode: 'local',
150
+ adminEmail: process.env.CONSENTI_ADMIN_EMAIL!,
151
+ adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
152
+ },
153
+ })
154
+ }
155
+ return consenti
156
+ }
157
+
158
+ export async function GET(req: NextRequest) {
159
+ const c = await getConsenti()
160
+ return c.handleRequest(req)
161
+ }
162
+
163
+ export { GET as POST, GET as PUT, GET as DELETE, GET as PATCH }
164
+ ```
165
+
166
+ ### Hono / Fetch-based
167
+
168
+ ```ts
169
+ return consenti.honoApp.fetch(request)
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Full Configuration
175
+
176
+ ```ts
177
+ import { createConsenti } from '@consenti/api'
178
+
179
+ const consenti = createConsenti({
180
+ // ── Required ────────────────────────────────────────────────────────────────
181
+ storage: {
182
+ driver: 'node-sqlite3-wasm', // 'node-sqlite3-wasm' | 'node:sqlite' | 'better-sqlite3' | 'pg' | 'mongodb' | 'mysql'
183
+ path: './consenti.db', // SQLite only — file path
184
+
185
+ // PostgreSQL
186
+ // driver: 'pg',
187
+ // url: 'postgresql://user:pass@localhost:5432/consenti',
188
+ // pool: { max: 10, idleTimeoutMillis: 30000 },
189
+
190
+ // MongoDB
191
+ // driver: 'mongodb',
192
+ // url: 'mongodb://localhost:27017',
193
+ // dbName: 'consenti',
194
+
195
+ // MySQL
196
+ // driver: 'mysql',
197
+ // url: 'mysql://user:pass@localhost:3306/consenti',
198
+ },
199
+ auth: {
200
+ mode: 'local', // 'local' | 'oauth' | 'saml' | 'none'
201
+ adminEmail: 'admin@example.com',
202
+ adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
203
+ jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET, // auto-generated if omitted (changes on restart)
204
+ sessionTtl: 86400, // JWT TTL in seconds (default: 24 h)
205
+
206
+ // OAuth / OIDC (e.g. Auth0, Keycloak)
207
+ // mode: 'oauth',
208
+ // oidc: {
209
+ // issuer: 'https://your-idp.example.com',
210
+ // clientId: 'your-client-id',
211
+ // clientSecret: process.env.OIDC_SECRET,
212
+ // redirectUri: 'https://app.example.com/consenti/admin/auth/callback',
213
+ // },
214
+
215
+ // SAML
216
+ // mode: 'saml',
217
+ // saml: {
218
+ // issuer: 'consenti',
219
+ // entryPoint: 'https://idp.example.com/sso/saml',
220
+ // cert: process.env.SAML_CERT,
221
+ // callbackUrl: 'https://app.example.com/consenti/admin/auth/saml/callback',
222
+ // },
223
+
224
+ // Custom — bring your own validator
225
+ // mode: 'custom',
226
+ // validateUser: async (req) => ({ id, email, roles, permissions } | null),
227
+ },
228
+
229
+ // ── Optional ────────────────────────────────────────────────────────────────
230
+ basePath: '/consenti', // URL prefix (default: '/consenti')
231
+ dashboard: true, // serve admin SPA (default: false)
232
+ trustProxy: false, // true when behind nginx / Cloudflare
233
+
234
+ rateLimit: {
235
+ enabled: true,
236
+ windowMs: 60_000, // rolling window (default: 60 s)
237
+ maxRequests: 60, // max requests per IP per window (default: 60)
238
+ },
239
+
240
+ cors: {
241
+ origins: ['https://yoursite.com'], // never use '*' in production
242
+ credentials: true,
243
+ },
244
+
245
+ compliance: {
246
+ gdpr: true, // opt-in enforcement
247
+ ccpa: false, // opt-out model
248
+ gpc: true, // store gpc_detected on records
249
+ tcf: {
250
+ enabled: false, // IAB TCF v2.2
251
+ cmpId: 123, // your registered CMP ID
252
+ },
253
+ },
254
+
255
+ ageGate: {
256
+ enabled: false,
257
+ minimumAge: 13,
258
+ requireParentalConsent: false,
259
+ },
260
+
261
+ dataRetention: {
262
+ purgeAfterDays: 365, // auto-purge old records (0 = disabled)
263
+ },
264
+
265
+ multiTenant: {
266
+ enabled: false, // isolate data per tenant
267
+ },
268
+
269
+ plugins: [], // ConsentiServerPlugin instances
270
+ })
271
+ ```
272
+
273
+ ---
274
+
275
+ ## First Boot
276
+
277
+ On first start with SQLite, Consenti:
278
+
279
+ 1. Creates the database file at the configured path
280
+ 2. Runs all migrations (creates `visitors`, `consent_records`, `consent_history`, `profiles`, `admin_users`, `audit_logs` tables)
281
+ 3. Creates the admin user from `auth.adminEmail` + `auth.adminPassword`
282
+ 4. Seeds the default profile (`profileId: '0'`)
283
+
284
+ Subsequent starts run only pending migrations.
285
+
286
+ ---
287
+
288
+ ## Public REST API
289
+
290
+ Base path: `/consenti/api/v1` (change with `basePath` in config). No authentication required.
291
+
292
+ | Method | Path | Description |
293
+ |------------|-------------------------------|-------------------------------------------------|
294
+ | `GET` | `/profiles/:id` | Get profile resolved in its default locale |
295
+ | `GET` | `/profiles/:id/:locale` | Get profile resolved for a specific locale |
296
+ | `POST` | `/consent` | Submit a consent record |
297
+ | `GET` | `/consent/:visitorId` | Get current consent for a visitor |
298
+ | `PUT` | `/consent/:visitorId` | Update consent for a visitor |
299
+ | `DELETE` | `/consent/:visitorId` | GDPR right-to-erasure — delete all visitor data |
300
+ | `GET` | `/consent/:visitorId/verify` | Check if consent is still valid |
301
+
302
+ ### Submit consent
303
+
304
+ ```http
305
+ POST /consenti/api/v1/consent
306
+ Content-Type: application/json
307
+
308
+ {
309
+ "profileId": "my-profile-id",
310
+ "consentJson": {
311
+ "necessary": "granted",
312
+ "analytics": "granted",
313
+ "marketing": "denied"
314
+ },
315
+ "visitorId": "uuid-v4",
316
+ "profileVersion": 3,
317
+ "locale": "en",
318
+ "gpcDetected": false,
319
+ "source": "banner"
320
+ }
321
+ ```
322
+
323
+ Response `201`:
324
+
325
+ ```json
326
+ {
327
+ "id": "record-uuid",
328
+ "visitorId": "visitor-uuid",
329
+ "profileId": "my-profile-id",
330
+ "profileVersion": 3,
331
+ "locale": "en",
332
+ "consentJson": { "necessary": "granted", "analytics": "granted", "marketing": "denied" },
333
+ "gpcDetected": false,
334
+ "source": "banner",
335
+ "createdAt": "2026-06-26T10:00:00.000Z",
336
+ "updatedAt": "2026-06-26T10:00:00.000Z"
337
+ }
338
+ ```
339
+
340
+ ### Verify consent
341
+
342
+ ```http
343
+ GET /consenti/api/v1/consent/visitor-uuid/verify
344
+ ```
345
+
346
+ ```json
347
+ { "valid": true }
348
+
349
+ // or, when stale:
350
+ { "valid": false, "reason": "profile_version_mismatch" }
351
+ // other reasons: "consent_expired" | "missing_cookies"
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Admin REST API
357
+
358
+ Base path: `/consenti/admin`. Every route requires `Authorization: Bearer <jwt>`.
359
+
360
+ Obtain a token via `POST /consenti/admin/auth/login`.
361
+
362
+ ### Auth
363
+
364
+ | Method | Path | Description |
365
+ |--------|-------------------|-----------------------------------|
366
+ | `POST` | `/auth/login` | Authenticate — returns a JWT |
367
+ | `GET` | `/auth/me` | Get current authenticated user |
368
+ | `POST` | `/auth/logout` | Invalidate session |
369
+
370
+ ```http
371
+ POST /consenti/admin/auth/login
372
+ Content-Type: application/json
373
+
374
+ { "email": "admin@example.com", "password": "your-password" }
375
+ ```
376
+
377
+ ```json
378
+ { "token": "eyJhbGciOiJIUzI1NiJ9..." }
379
+ ```
380
+
381
+ ### Profiles
382
+
383
+ | Method | Path | Description |
384
+ |------------|-------------------|------------------------------------------|
385
+ | `GET` | `/profiles` | List all profiles |
386
+ | `POST` | `/profiles` | Create a profile |
387
+ | `GET` | `/profiles/:id` | Get a profile |
388
+ | `PUT` | `/profiles/:id` | Update a profile (auto-bumps version) |
389
+ | `DELETE` | `/profiles/:id` | Delete a profile |
390
+
391
+ ### Consents
392
+
393
+ | Method | Path | Description |
394
+ |----------|----------------------------------|---------------------------------------------|
395
+ | `GET` | `/consents` | List consent records (paginated, filterable)|
396
+ | `GET` | `/consents/:visitorId` | Get consent record for a visitor |
397
+ | `GET` | `/consents/:visitorId/history` | Get consent change history |
398
+
399
+ Query params for `GET /consents`: `page`, `limit`, `profileId`, `from`, `to`.
400
+
401
+ ### Visitors
402
+
403
+ | Method | Path | Description |
404
+ |--------|----------------|-------------------------------------|
405
+ | `GET` | `/visitors` | List visitor records (paginated) |
406
+
407
+ IPs are never stored raw — only SHA-256 hashes appear in the `ipHash` field.
408
+
409
+ ### Users & Roles
410
+
411
+ | Method | Path | Description |
412
+ |------------|-----------------------------------|----------------------------------|
413
+ | `GET` | `/users` | List admin users |
414
+ | `POST` | `/users` | Create an admin user |
415
+ | `PUT` | `/users/:id` | Update an admin user |
416
+ | `GET` | `/roles` | List roles |
417
+ | `POST` | `/roles` | Create a role |
418
+ | `GET` | `/roles/:id/permissions` | Get permissions for a role |
419
+ | `POST` | `/roles/:id/permissions` | Assign a permission to a role |
420
+ | `DELETE` | `/roles/:id/permissions/:permId` | Revoke a permission from a role |
421
+ | `GET` | `/permissions` | List all available permissions |
422
+
423
+ ### API Keys
424
+
425
+ | Method | Path | Description |
426
+ |------------|-----------------|------------------------------------------------------|
427
+ | `GET` | `/apikeys` | List API keys |
428
+ | `POST` | `/apikeys` | Create an API key (raw key returned once — save it!) |
429
+ | `DELETE` | `/apikeys/:id` | Revoke an API key |
430
+
431
+ ### Stats & Reporting
432
+
433
+ | Method | Path | Description |
434
+ |--------|---------------------------|------------------------------------------|
435
+ | `GET` | `/stats/overview` | Total consents, visitors, GPC count |
436
+ | `GET` | `/stats/timeline` | Daily consent counts (`?days=30`) |
437
+ | `GET` | `/stats/categories` | Per-category grant/deny breakdown |
438
+ | `GET` | `/stats/countries` | Consent count by country |
439
+ | `GET` | `/stats/gpc` | GPC detection statistics |
440
+ | `GET` | `/export/consents` | Export consent records (CSV or JSON) |
441
+ | `GET` | `/export/consents/xlsx` | Export as XLSX |
442
+ | `GET` | `/export/audit` | Export audit log (CSV or JSON) |
443
+
444
+ ### Audit Log
445
+
446
+ | Method | Path | Description |
447
+ |--------|-----------|---------------------------------------------------------|
448
+ | `GET` | `/audit` | Paginated audit log; filter by `action`, `resourceType`, date range |
449
+
450
+ ### Multi-Tenant
451
+
452
+ | Method | Path | Description |
453
+ |------------|------------------|----------------------|
454
+ | `GET` | `/tenants` | List tenants |
455
+ | `POST` | `/tenants` | Create a tenant |
456
+ | `PUT` | `/tenants/:id` | Update a tenant |
457
+ | `DELETE` | `/tenants/:id` | Delete a tenant |
458
+
459
+ Only active when `multiTenant.enabled: true`.
460
+
461
+ ### IAB TCF
462
+
463
+ | Method | Path | Description |
464
+ |--------|-------------------|---------------------------|
465
+ | `GET` | `/tcf/vendors` | List IAB TCF vendors |
466
+ | `GET` | `/tcf/purposes` | List IAB TCF purposes |
467
+
468
+ Only active when `tcf.enabled: true`.
469
+
470
+ ---
471
+
472
+ ## Admin Dashboard
473
+
474
+ Served at `{basePath}/` when `dashboard: true`.
475
+
476
+ | Section | Description |
477
+ |-------------------|--------------------------------------------------------------------------|
478
+ | Dashboard | Consent overview, timeline chart, country breakdown, GPC stats |
479
+ | Profiles | Create / edit / copy / delete consent profiles via 3-step wizard |
480
+ | Cookie Templates | Reusable cookie/purpose definitions |
481
+ | UI Templates | Reusable banner + modal layout settings |
482
+ | Consents | Browse, filter, export consent records; per-visitor history |
483
+ | Visitors | Visitor list with geographic data |
484
+ | Users | Admin user management |
485
+ | Roles | RBAC roles and fine-grained permission assignment |
486
+ | Sites | Multi-tenant site management |
487
+ | TCF Vendors | IAB Global Vendor List |
488
+ | Audit Log | Immutable log of all admin actions |
489
+ | Settings | API keys, branding |
490
+
491
+ ### Profile Creation Wizard
492
+
493
+ **Step 1 — Cookie Template:** Define cookie IDs and their properties:
494
+
495
+ | Field | Type | Description |
496
+ |---------------------|-----------------------------------|---------------------------------------------------|
497
+ | `id` | `string` | Unique identifier referenced in button arrays |
498
+ | `type` | `'consent' \| 'legitimate_interest'` | Legal basis |
499
+ | `mandatory` | `boolean` | Always granted; toggle disabled in modal |
500
+ | `listenGpc` | `boolean` | Auto-denied when GPC signal is active |
501
+ | `expiry` | `number` | Days until re-consent is required (0 = session) |
502
+
503
+ **Step 2 — UI Template:** Configure banner and modal layout:
504
+
505
+ - **Main Banner / GPC Banner:** `position`, `overlayOpacity`, `showClose`, `headingTag`, `buttons`
506
+ - **Preference Modal:** `position`, `overlayOpacity`, `showClose`, `persistent`, `buttons`, `categories`
507
+ - Button `type`: `primary | secondary | text | reject | submit | manage | close`
508
+ - Button `cookies`: `*` (grant all) · `!` (deny all) · comma-separated IDs
509
+
510
+ **Step 3 — Content:** Enter localised copy with live preview:
511
+
512
+ - Main Banner: heading, body HTML
513
+ - GPC Banner: heading, body HTML
514
+ - Preference Modal: heading, subheading, intro HTML, per-category heading + description
515
+
516
+ ---
517
+
518
+ ## Plugins (Server-side)
519
+
520
+ ```ts
521
+ import { ConsentiServerPlugin } from '@consenti/api'
522
+ import type { PluginContext, CreateConsentInput, ConsentDbRecord } from '@consenti/api'
523
+
524
+ class MyPlugin extends ConsentiServerPlugin {
525
+ name = 'my-plugin'
526
+
527
+ async initialize(ctx: PluginContext) {
528
+ // ctx.storage — access storage adapter directly
529
+ console.log('Plugin initialised')
530
+ }
531
+
532
+ async destroy() {}
533
+
534
+ // All hooks are optional:
535
+ async beforeConsentSave(data: CreateConsentInput) {
536
+ return data // modify or validate; return modified data
537
+ }
538
+
539
+ async afterConsentSave(record: ConsentDbRecord) {
540
+ // send to analytics, trigger webhooks, etc.
541
+ await fetch('/my-webhook', { method: 'POST', body: JSON.stringify(record) })
542
+ }
543
+
544
+ async beforeConsentUpdate(data: unknown) { return data }
545
+ async afterConsentUpdate(record: unknown) {}
546
+ async beforeProfileFetch(id: string) { return id }
547
+ async afterProfileFetch(profile: unknown) { return profile }
548
+ async beforeUserCreate(data: unknown) { return data }
549
+ async afterUserCreate(user: unknown) {}
550
+ }
551
+
552
+ const consenti = createConsenti({
553
+ // ...
554
+ plugins: [new MyPlugin()],
555
+ })
556
+ ```
557
+
558
+ ---
559
+
560
+ ## Programmatic Service Access
561
+
562
+ ```ts
563
+ const consenti = createConsenti({ /* ... */ })
564
+
565
+ // Access services directly — bypass HTTP layer
566
+ const profile = await consenti.services.profile.get('profile-id')
567
+ const consent = await consenti.services.consent.submit({ /* ... */ })
568
+ const visitors = await consenti.services.visitor.list({ tenantId: 'default' })
569
+
570
+ // Access storage adapter
571
+ await consenti.storage.getConsents({ tenantId: 'default', from: '2026-01-01' })
572
+
573
+ // Event bus
574
+ consenti.eventBus.on('ready', () => console.log('Backend ready'))
575
+
576
+ // Graceful shutdown
577
+ await consenti.destroy()
578
+ ```
579
+
580
+ ---
581
+
582
+ ## Security
583
+
584
+ | Concern | Approach |
585
+ |---------------------------|------------------------------------------------------------------------------|
586
+ | IP addresses | Never stored raw — SHA-256 hashed: `createHash('sha256').update(ip).digest('hex')` |
587
+ | Passwords | `scrypt` via `node:crypto` — no external bcrypt |
588
+ | JWTs | `createHmac` via `node:crypto` — no jsonwebtoken package |
589
+ | Admin routes | Always require `Authorization: Bearer <jwt>` |
590
+ | Stack traces | Suppressed in `NODE_ENV=production` responses |
591
+ | CORS | Explicit allowlist — never use `origins: '*'` in production |
592
+ | Rate limiting | Applied to all public API routes by default |
593
+
594
+ ---
595
+
596
+ ## Environment Variables
597
+
598
+ | Variable | Description |
599
+ |---------------------------|-----------------------------------------------------------------------------|
600
+ | `NODE_ENV` | Set to `'production'` to suppress stack traces in error responses |
601
+ | `CONSENTI_ADMIN_PASSWORD` | Override `auth.adminPassword` from config |
602
+ | `CONSENTI_ADMIN_JWT_SECRET` | Override `auth.jwtSecret` from config |
603
+ | `CONSENTI_DB_PATH` | Override `storage.path` (SQLite) |
604
+ | `CONSENTI_BASE_PATH` | Override `basePath` |
605
+
606
+ ---
607
+
608
+ ## Interactive API Docs
609
+
610
+ When running, Swagger UI is available at `{basePath}/api/docs`.
611
+ OpenAPI JSON at `{basePath}/api/openapi.json`.
612
+
613
+ ---
614
+
615
+ ## Contributing
616
+
617
+ See [CONTRIBUTING.md](../../CONTRIBUTING.md) in the monorepo root.
618
+
619
+ ---
620
+
621
+ ## License
622
+
623
+ Apache 2.0 — see [LICENSE](../../LICENSE).