@tantainnovative/create-ndpr 0.1.0 → 0.4.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.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Express — Lawful Basis Router
3
+ * Generated by create-ndpr for: {{ORG_NAME}}
4
+ * DPO: {{DPO_EMAIL}}
5
+ *
6
+ * NDPA §25 — data processing must have a lawful basis. This router
7
+ * manages a register of processing activities and their lawful bases.
8
+ *
9
+ * Routes:
10
+ * GET /lawful-basis — List records (optional ?lawfulBasis= filter)
11
+ * GET /lawful-basis/:id — Get a single record
12
+ * POST /lawful-basis — Create a new lawful basis record
13
+ * PUT /lawful-basis/:id — Update an existing record
14
+ * DELETE /lawful-basis/:id — Delete a record
15
+ *
16
+ * Usage:
17
+ * import { lawfulBasisRouter } from './routes/lawful-basis';
18
+ * app.use('/api/lawful-basis', lawfulBasisRouter);
19
+ */
20
+
21
+ import { Router } from 'express';
22
+ // {{#if ORM=prisma}}
23
+ import { PrismaClient } from '@prisma/client';
24
+
25
+ const prisma = new PrismaClient();
26
+ // {{/if}}
27
+ // {{#if ORM=drizzle}}
28
+ import { db } from '@/drizzle';
29
+ import { lawfulBasisRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
30
+ import { eq, desc } from 'drizzle-orm';
31
+ // {{/if}}
32
+ // {{#if ORM=none}}
33
+ // TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
34
+ // in-memory map below is enough to develop against locally but DOES NOT
35
+ // satisfy NDPA Section 44 (record-keeping). Replace `lawfulBasisStore`/
36
+ // `auditLog` with your DB / KV / API.
37
+ interface LawfulBasisRecord {
38
+ id: string;
39
+ activityName: string;
40
+ lawfulBasis: string;
41
+ justification: string;
42
+ dataCategories: string[];
43
+ purposes: string[];
44
+ assessedBy: string;
45
+ reviewDate: Date | null;
46
+ createdAt: Date;
47
+ updatedAt: Date;
48
+ }
49
+ const lawfulBasisStore = new Map<string, LawfulBasisRecord>();
50
+ const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
51
+ function newId() { return crypto.randomUUID(); }
52
+ // {{/if}}
53
+
54
+ export const lawfulBasisRouter = Router();
55
+
56
+ // GET /lawful-basis?lawfulBasis=consent
57
+ lawfulBasisRouter.get('/', async (req, res) => {
58
+ const { lawfulBasis } = req.query;
59
+ const basisFilter = typeof lawfulBasis === 'string' ? lawfulBasis : undefined;
60
+
61
+ // {{#if ORM=prisma}}
62
+ const records = await prisma.lawfulBasisRecord.findMany({
63
+ where: basisFilter ? { lawfulBasis: basisFilter } : undefined,
64
+ orderBy: { createdAt: 'desc' },
65
+ });
66
+ // {{/if}}
67
+ // {{#if ORM=drizzle}}
68
+ const records = basisFilter
69
+ ? await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.lawfulBasis, basisFilter)).orderBy(desc(lawfulBasisRecords.createdAt))
70
+ : await db.select().from(lawfulBasisRecords).orderBy(desc(lawfulBasisRecords.createdAt));
71
+ // {{/if}}
72
+ // {{#if ORM=none}}
73
+ const records = [...lawfulBasisStore.values()]
74
+ .filter((r) => (basisFilter ? r.lawfulBasis === basisFilter : true))
75
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
76
+ // {{/if}}
77
+
78
+ return res.json(records);
79
+ });
80
+
81
+ // GET /lawful-basis/:id
82
+ lawfulBasisRouter.get('/:id', async (req, res) => {
83
+ // {{#if ORM=prisma}}
84
+ const record = await prisma.lawfulBasisRecord.findUnique({ where: { id: req.params.id } });
85
+ // {{/if}}
86
+ // {{#if ORM=drizzle}}
87
+ const [record] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, req.params.id)).limit(1);
88
+ // {{/if}}
89
+ // {{#if ORM=none}}
90
+ const record = lawfulBasisStore.get(req.params.id) ?? null;
91
+ // {{/if}}
92
+
93
+ if (!record) {
94
+ return res.status(404).json({ error: 'Lawful basis record not found' });
95
+ }
96
+
97
+ return res.json(record);
98
+ });
99
+
100
+ // POST /lawful-basis
101
+ lawfulBasisRouter.post('/', async (req, res) => {
102
+ const {
103
+ activityName,
104
+ lawfulBasis,
105
+ justification,
106
+ dataCategories,
107
+ purposes,
108
+ assessedBy,
109
+ reviewDate,
110
+ } = req.body;
111
+
112
+ if (!activityName || !lawfulBasis || !justification || !dataCategories || !purposes || !assessedBy) {
113
+ return res.status(400).json({
114
+ error: 'activityName, lawfulBasis, justification, dataCategories, purposes, and assessedBy are required',
115
+ });
116
+ }
117
+
118
+ if (!Array.isArray(dataCategories) || !Array.isArray(purposes)) {
119
+ return res.status(400).json({ error: 'dataCategories and purposes must be arrays' });
120
+ }
121
+
122
+ // {{#if ORM=prisma}}
123
+ const record = await prisma.lawfulBasisRecord.create({
124
+ data: {
125
+ activityName,
126
+ lawfulBasis,
127
+ justification,
128
+ dataCategories,
129
+ purposes,
130
+ assessedBy,
131
+ reviewDate: reviewDate ? new Date(reviewDate) : null,
132
+ },
133
+ });
134
+
135
+ await prisma.complianceAuditLog.create({
136
+ data: {
137
+ module: 'lawful-basis',
138
+ action: 'created',
139
+ entityId: record.id,
140
+ entityType: 'LawfulBasisRecord',
141
+ changes: { activityName, lawfulBasis, assessedBy },
142
+ },
143
+ });
144
+ // {{/if}}
145
+ // {{#if ORM=drizzle}}
146
+ const [record] = await db.insert(lawfulBasisRecords).values({
147
+ activityName,
148
+ lawfulBasis,
149
+ justification,
150
+ dataCategories,
151
+ purposes,
152
+ assessedBy,
153
+ reviewDate: reviewDate ? new Date(reviewDate) : null,
154
+ }).returning();
155
+
156
+ await db.insert(complianceAuditLog).values({
157
+ module: 'lawful-basis',
158
+ action: 'created',
159
+ entityId: record.id,
160
+ entityType: 'LawfulBasisRecord',
161
+ changes: { activityName, lawfulBasis, assessedBy },
162
+ });
163
+ // {{/if}}
164
+ // {{#if ORM=none}}
165
+ const now = new Date();
166
+ const record: LawfulBasisRecord = {
167
+ id: newId(),
168
+ activityName,
169
+ lawfulBasis,
170
+ justification,
171
+ dataCategories,
172
+ purposes,
173
+ assessedBy,
174
+ reviewDate: reviewDate ? new Date(reviewDate) : null,
175
+ createdAt: now,
176
+ updatedAt: now,
177
+ };
178
+ lawfulBasisStore.set(record.id, record);
179
+ auditLog.push({ id: newId(), module: 'lawful-basis', action: 'created', entityId: record.id, at: new Date() });
180
+ // {{/if}}
181
+
182
+ return res.status(201).json(record);
183
+ });
184
+
185
+ // PUT /lawful-basis/:id
186
+ lawfulBasisRouter.put('/:id', async (req, res) => {
187
+ const { id } = req.params;
188
+
189
+ const data = { ...req.body };
190
+ if (data.reviewDate) {
191
+ data.reviewDate = new Date(data.reviewDate);
192
+ }
193
+
194
+ // {{#if ORM=prisma}}
195
+ const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
196
+ if (!existing) {
197
+ return res.status(404).json({ error: 'Lawful basis record not found' });
198
+ }
199
+
200
+ const record = await prisma.lawfulBasisRecord.update({
201
+ where: { id },
202
+ data,
203
+ });
204
+
205
+ await prisma.complianceAuditLog.create({
206
+ data: {
207
+ module: 'lawful-basis',
208
+ action: 'updated',
209
+ entityId: record.id,
210
+ entityType: 'LawfulBasisRecord',
211
+ changes: data,
212
+ },
213
+ });
214
+ // {{/if}}
215
+ // {{#if ORM=drizzle}}
216
+ const [existing] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
217
+ if (!existing) {
218
+ return res.status(404).json({ error: 'Lawful basis record not found' });
219
+ }
220
+
221
+ const [record] = await db.update(lawfulBasisRecords).set(data).where(eq(lawfulBasisRecords.id, id)).returning();
222
+
223
+ await db.insert(complianceAuditLog).values({
224
+ module: 'lawful-basis',
225
+ action: 'updated',
226
+ entityId: record.id,
227
+ entityType: 'LawfulBasisRecord',
228
+ changes: data,
229
+ });
230
+ // {{/if}}
231
+ // {{#if ORM=none}}
232
+ const existing = lawfulBasisStore.get(id);
233
+ if (!existing) {
234
+ return res.status(404).json({ error: 'Lawful basis record not found' });
235
+ }
236
+ const record: LawfulBasisRecord = { ...existing, ...data, id, updatedAt: new Date() };
237
+ lawfulBasisStore.set(id, record);
238
+ auditLog.push({ id: newId(), module: 'lawful-basis', action: 'updated', entityId: record.id, at: new Date() });
239
+ // {{/if}}
240
+
241
+ return res.json(record);
242
+ });
243
+
244
+ // DELETE /lawful-basis/:id
245
+ lawfulBasisRouter.delete('/:id', async (req, res) => {
246
+ const { id } = req.params;
247
+
248
+ // {{#if ORM=prisma}}
249
+ const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
250
+ if (!existing) {
251
+ return res.status(404).json({ error: 'Lawful basis record not found' });
252
+ }
253
+
254
+ await prisma.lawfulBasisRecord.delete({ where: { id } });
255
+
256
+ await prisma.complianceAuditLog.create({
257
+ data: {
258
+ module: 'lawful-basis',
259
+ action: 'deleted',
260
+ entityId: id,
261
+ entityType: 'LawfulBasisRecord',
262
+ changes: { activityName: existing.activityName },
263
+ },
264
+ });
265
+ // {{/if}}
266
+ // {{#if ORM=drizzle}}
267
+ const [existing] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
268
+ if (!existing) {
269
+ return res.status(404).json({ error: 'Lawful basis record not found' });
270
+ }
271
+
272
+ await db.delete(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id));
273
+
274
+ await db.insert(complianceAuditLog).values({
275
+ module: 'lawful-basis',
276
+ action: 'deleted',
277
+ entityId: id,
278
+ entityType: 'LawfulBasisRecord',
279
+ changes: { activityName: existing.activityName },
280
+ });
281
+ // {{/if}}
282
+ // {{#if ORM=none}}
283
+ const existing = lawfulBasisStore.get(id);
284
+ if (!existing) {
285
+ return res.status(404).json({ error: 'Lawful basis record not found' });
286
+ }
287
+ lawfulBasisStore.delete(id);
288
+ auditLog.push({ id: newId(), module: 'lawful-basis', action: 'deleted', entityId: id, at: new Date() });
289
+ // {{/if}}
290
+
291
+ return res.json({ success: true });
292
+ });
@@ -15,9 +15,12 @@
15
15
  * app.use('/api/ndpr', createNDPRRouter());
16
16
  *
17
17
  * Routes exposed:
18
- * GET/POST/DELETE /api/ndpr/consent — NDPA §25–26
19
- * GET/POST /api/ndpr/dsr — NDPA §34–38
20
- * GET/POST /api/ndpr/breach — NDPA §40
18
+ * GET/POST/DELETE /api/ndpr/consent — NDPA §25–26
19
+ * GET/POST /api/ndpr/dsr — NDPA §34–38
20
+ * GET/POST /api/ndpr/breach — NDPA §40
21
+ * CRUD /api/ndpr/dpia — NDPA §34(3)
22
+ * CRUD /api/ndpr/lawful-basis — NDPA §25
23
+ * CRUD /api/ndpr/cross-border — NDPA §41–42
21
24
  */
22
25
 
23
26
  import { Router } from 'express';
@@ -231,6 +234,259 @@ function breachRouter(): Router {
231
234
  return router;
232
235
  }
233
236
 
237
+ // ---------------------------------------------------------------------------
238
+ // DPIA router — NDPA §34(3)
239
+ // ---------------------------------------------------------------------------
240
+
241
+ function dpiaRouter(): Router {
242
+ const router = Router();
243
+
244
+ // GET /dpia?status=draft
245
+ router.get('/', async (req, res) => {
246
+ const { status } = req.query;
247
+ const records = await prisma.dPIARecord.findMany({
248
+ where: status && typeof status === 'string' ? { status } : undefined,
249
+ orderBy: { createdAt: 'desc' },
250
+ });
251
+ return res.json(records);
252
+ });
253
+
254
+ // GET /dpia/:id
255
+ router.get('/:id', async (req, res) => {
256
+ const record = await prisma.dPIARecord.findUnique({ where: { id: req.params.id } });
257
+ if (!record) return res.status(404).json({ error: 'DPIA record not found' });
258
+ return res.json(record);
259
+ });
260
+
261
+ // POST /dpia
262
+ router.post('/', async (req, res) => {
263
+ const { projectName, description, dpiaData, overallRisk, score, conductedBy, approvedBy } = req.body;
264
+ if (!projectName || !description || !dpiaData || !overallRisk || score == null || !conductedBy) {
265
+ return res.status(400).json({
266
+ error: 'projectName, description, dpiaData, overallRisk, score, and conductedBy are required',
267
+ });
268
+ }
269
+ const record = await prisma.dPIARecord.create({
270
+ data: {
271
+ projectName, description, dpiaData, overallRisk, score,
272
+ status: 'draft', conductedBy, approvedBy: approvedBy ?? null,
273
+ },
274
+ });
275
+ await prisma.complianceAuditLog.create({
276
+ data: {
277
+ module: 'dpia', action: 'created', entityId: record.id,
278
+ entityType: 'DPIARecord', changes: { projectName, overallRisk, score, status: 'draft' },
279
+ },
280
+ });
281
+ return res.status(201).json(record);
282
+ });
283
+
284
+ // PUT /dpia/:id
285
+ router.put('/:id', async (req, res) => {
286
+ const { id } = req.params;
287
+ const existing = await prisma.dPIARecord.findUnique({ where: { id } });
288
+ if (!existing) return res.status(404).json({ error: 'DPIA record not found' });
289
+ const record = await prisma.dPIARecord.update({ where: { id }, data: req.body });
290
+ await prisma.complianceAuditLog.create({
291
+ data: { module: 'dpia', action: 'updated', entityId: record.id, entityType: 'DPIARecord', changes: req.body },
292
+ });
293
+ return res.json(record);
294
+ });
295
+
296
+ // DELETE /dpia/:id
297
+ router.delete('/:id', async (req, res) => {
298
+ const { id } = req.params;
299
+ const existing = await prisma.dPIARecord.findUnique({ where: { id } });
300
+ if (!existing) return res.status(404).json({ error: 'DPIA record not found' });
301
+ await prisma.dPIARecord.delete({ where: { id } });
302
+ await prisma.complianceAuditLog.create({
303
+ data: {
304
+ module: 'dpia', action: 'deleted', entityId: id,
305
+ entityType: 'DPIARecord', changes: { projectName: existing.projectName },
306
+ },
307
+ });
308
+ return res.json({ success: true });
309
+ });
310
+
311
+ return router;
312
+ }
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // Lawful Basis router — NDPA §25
316
+ // ---------------------------------------------------------------------------
317
+
318
+ function lawfulBasisRouter(): Router {
319
+ const router = Router();
320
+
321
+ // GET /lawful-basis?lawfulBasis=consent
322
+ router.get('/', async (req, res) => {
323
+ const { lawfulBasis } = req.query;
324
+ const records = await prisma.lawfulBasisRecord.findMany({
325
+ where: lawfulBasis && typeof lawfulBasis === 'string' ? { lawfulBasis } : undefined,
326
+ orderBy: { createdAt: 'desc' },
327
+ });
328
+ return res.json(records);
329
+ });
330
+
331
+ // GET /lawful-basis/:id
332
+ router.get('/:id', async (req, res) => {
333
+ const record = await prisma.lawfulBasisRecord.findUnique({ where: { id: req.params.id } });
334
+ if (!record) return res.status(404).json({ error: 'Lawful basis record not found' });
335
+ return res.json(record);
336
+ });
337
+
338
+ // POST /lawful-basis
339
+ router.post('/', async (req, res) => {
340
+ const { activityName, lawfulBasis, justification, dataCategories, purposes, assessedBy, reviewDate } = req.body;
341
+ if (!activityName || !lawfulBasis || !justification || !dataCategories || !purposes || !assessedBy) {
342
+ return res.status(400).json({
343
+ error: 'activityName, lawfulBasis, justification, dataCategories, purposes, and assessedBy are required',
344
+ });
345
+ }
346
+ if (!Array.isArray(dataCategories) || !Array.isArray(purposes)) {
347
+ return res.status(400).json({ error: 'dataCategories and purposes must be arrays' });
348
+ }
349
+ const record = await prisma.lawfulBasisRecord.create({
350
+ data: {
351
+ activityName, lawfulBasis, justification, dataCategories, purposes,
352
+ assessedBy, reviewDate: reviewDate ? new Date(reviewDate) : null,
353
+ },
354
+ });
355
+ await prisma.complianceAuditLog.create({
356
+ data: {
357
+ module: 'lawful-basis', action: 'created', entityId: record.id,
358
+ entityType: 'LawfulBasisRecord', changes: { activityName, lawfulBasis, assessedBy },
359
+ },
360
+ });
361
+ return res.status(201).json(record);
362
+ });
363
+
364
+ // PUT /lawful-basis/:id
365
+ router.put('/:id', async (req, res) => {
366
+ const { id } = req.params;
367
+ const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
368
+ if (!existing) return res.status(404).json({ error: 'Lawful basis record not found' });
369
+ const data = { ...req.body };
370
+ if (data.reviewDate) data.reviewDate = new Date(data.reviewDate);
371
+ const record = await prisma.lawfulBasisRecord.update({ where: { id }, data });
372
+ await prisma.complianceAuditLog.create({
373
+ data: {
374
+ module: 'lawful-basis', action: 'updated', entityId: record.id,
375
+ entityType: 'LawfulBasisRecord', changes: data,
376
+ },
377
+ });
378
+ return res.json(record);
379
+ });
380
+
381
+ // DELETE /lawful-basis/:id
382
+ router.delete('/:id', async (req, res) => {
383
+ const { id } = req.params;
384
+ const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
385
+ if (!existing) return res.status(404).json({ error: 'Lawful basis record not found' });
386
+ await prisma.lawfulBasisRecord.delete({ where: { id } });
387
+ await prisma.complianceAuditLog.create({
388
+ data: {
389
+ module: 'lawful-basis', action: 'deleted', entityId: id,
390
+ entityType: 'LawfulBasisRecord', changes: { activityName: existing.activityName },
391
+ },
392
+ });
393
+ return res.json({ success: true });
394
+ });
395
+
396
+ return router;
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Cross-Border Transfer router — NDPA §41–42
401
+ // ---------------------------------------------------------------------------
402
+
403
+ function crossBorderRouter(): Router {
404
+ const router = Router();
405
+
406
+ // GET /cross-border?riskLevel=high&destinationCountry=US
407
+ router.get('/', async (req, res) => {
408
+ const { riskLevel, destinationCountry } = req.query;
409
+ const where: Record<string, string> = {};
410
+ if (riskLevel && typeof riskLevel === 'string') where.riskLevel = riskLevel;
411
+ if (destinationCountry && typeof destinationCountry === 'string') where.destinationCountry = destinationCountry;
412
+ const records = await prisma.crossBorderTransferRecord.findMany({
413
+ where: Object.keys(where).length > 0 ? where : undefined,
414
+ orderBy: { createdAt: 'desc' },
415
+ });
416
+ return res.json(records);
417
+ });
418
+
419
+ // GET /cross-border/:id
420
+ router.get('/:id', async (req, res) => {
421
+ const record = await prisma.crossBorderTransferRecord.findUnique({ where: { id: req.params.id } });
422
+ if (!record) return res.status(404).json({ error: 'Cross-border transfer record not found' });
423
+ return res.json(record);
424
+ });
425
+
426
+ // POST /cross-border
427
+ router.post('/', async (req, res) => {
428
+ const {
429
+ destinationCountry, recipientName, transferMechanism, safeguards,
430
+ dataCategories, adequacyStatus, ndpcApprovalRequired, ndpcApprovalReference, riskLevel,
431
+ } = req.body;
432
+ if (!destinationCountry || !recipientName || !transferMechanism || !safeguards || !dataCategories || !adequacyStatus || !riskLevel) {
433
+ return res.status(400).json({
434
+ error: 'destinationCountry, recipientName, transferMechanism, safeguards, dataCategories, adequacyStatus, and riskLevel are required',
435
+ });
436
+ }
437
+ if (!Array.isArray(dataCategories)) {
438
+ return res.status(400).json({ error: 'dataCategories must be an array' });
439
+ }
440
+ const record = await prisma.crossBorderTransferRecord.create({
441
+ data: {
442
+ destinationCountry, recipientName, transferMechanism, safeguards, dataCategories,
443
+ adequacyStatus, ndpcApprovalRequired: ndpcApprovalRequired ?? false,
444
+ ndpcApprovalReference: ndpcApprovalReference ?? null, riskLevel,
445
+ },
446
+ });
447
+ await prisma.complianceAuditLog.create({
448
+ data: {
449
+ module: 'cross-border', action: 'created', entityId: record.id,
450
+ entityType: 'CrossBorderTransferRecord', changes: { destinationCountry, recipientName, riskLevel },
451
+ },
452
+ });
453
+ return res.status(201).json(record);
454
+ });
455
+
456
+ // PUT /cross-border/:id
457
+ router.put('/:id', async (req, res) => {
458
+ const { id } = req.params;
459
+ const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
460
+ if (!existing) return res.status(404).json({ error: 'Cross-border transfer record not found' });
461
+ const record = await prisma.crossBorderTransferRecord.update({ where: { id }, data: req.body });
462
+ await prisma.complianceAuditLog.create({
463
+ data: {
464
+ module: 'cross-border', action: 'updated', entityId: record.id,
465
+ entityType: 'CrossBorderTransferRecord', changes: req.body,
466
+ },
467
+ });
468
+ return res.json(record);
469
+ });
470
+
471
+ // DELETE /cross-border/:id
472
+ router.delete('/:id', async (req, res) => {
473
+ const { id } = req.params;
474
+ const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
475
+ if (!existing) return res.status(404).json({ error: 'Cross-border transfer record not found' });
476
+ await prisma.crossBorderTransferRecord.delete({ where: { id } });
477
+ await prisma.complianceAuditLog.create({
478
+ data: {
479
+ module: 'cross-border', action: 'deleted', entityId: id,
480
+ entityType: 'CrossBorderTransferRecord',
481
+ changes: { destinationCountry: existing.destinationCountry, recipientName: existing.recipientName },
482
+ },
483
+ });
484
+ return res.json({ success: true });
485
+ });
486
+
487
+ return router;
488
+ }
489
+
234
490
  // ---------------------------------------------------------------------------
235
491
  // Main factory
236
492
  // ---------------------------------------------------------------------------
@@ -246,5 +502,8 @@ export function createNDPRRouter(): Router {
246
502
  router.use('/consent', consentRouter());
247
503
  router.use('/dsr', dsrRouter());
248
504
  router.use('/breach', breachRouter());
505
+ router.use('/dpia', dpiaRouter());
506
+ router.use('/lawful-basis', lawfulBasisRouter());
507
+ router.use('/cross-border', crossBorderRouter());
249
508
  return router;
250
509
  }
@@ -0,0 +1,31 @@
1
+ # NDPA 2023 / NDPC GAID 2025 compliance gate — generated by create-ndpr for {{ORG_NAME}}.
2
+ #
3
+ # Runs `ndpr audit` on every push and pull request. The audit scores your
4
+ # compliance config (consent, DSR, DPIA, breach, policy, lawful basis,
5
+ # cross-border, RoPA) plus your GAID 2025 DCPMI registration tier, and exits
6
+ # non-zero when the overall score drops below the threshold — so a regression
7
+ # fails CI the same way a broken test would.
8
+ #
9
+ # Edit ndpr.audit.json to reflect your real posture, then raise --min-score
10
+ # over time as you close gaps. Docs: https://ndprtoolkit.com.ng/docs/guides/audit-cli
11
+ name: NDPA compliance audit
12
+
13
+ on:
14
+ push:
15
+ branches: [main]
16
+ pull_request:
17
+
18
+ jobs:
19
+ ndpr-audit:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 20
26
+ # The `ndpr` CLI ships with the toolkit. --no-save keeps it out of your
27
+ # package.json if you don't already depend on it directly.
28
+ - name: Install the NDPR toolkit (provides the `ndpr` CLI)
29
+ run: npm install --no-save @tantainnovative/ndpr-toolkit@^5.5.1
30
+ - name: Run the NDPA compliance audit
31
+ run: npx ndpr audit --min-score 70
@@ -0,0 +1,14 @@
1
+ {
2
+ "minScore": 70,
3
+ "compliance": {
4
+ "consent": { "hasConsentMechanism": true, "hasPurposeSpecification": true, "hasWithdrawalMechanism": true, "hasMinorProtection": false, "consentRecordsRetained": true },
5
+ "dsr": { "hasRequestMechanism": true, "supportsAccess": true, "supportsRectification": true, "supportsErasure": false, "supportsPortability": false, "supportsObjection": false, "responseTimelineDays": 30 },
6
+ "dpia": { "conductedForHighRisk": true, "documentedRisks": true, "mitigationMeasures": true },
7
+ "breach": { "hasNotificationProcess": true, "notifiesWithin72Hours": true, "hasRiskAssessment": true, "hasRecordKeeping": true },
8
+ "policy": { "hasPrivacyPolicy": true, "isPubliclyAccessible": true, "lastUpdated": "2026-01-01", "coversAllSections": true },
9
+ "lawfulBasis": { "documentedForAllProcessing": true, "hasLegitimateInterestAssessment": false },
10
+ "crossBorder": { "hasTransferMechanisms": true, "adequacyAssessed": true, "ndpcApprovalObtained": false },
11
+ "ropa": { "maintained": true, "includesAllProcessing": true, "lastReviewed": "2026-01-01" }
12
+ },
13
+ "dcpmi": { "dataSubjectsInSixMonths": 0 }
14
+ }