@tantainnovative/create-ndpr 0.1.0 → 0.5.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,137 @@
1
+ import type { NextRequest } from 'next/server';
2
+
3
+ export interface NDPRVerifiedActor {
4
+ /** Stable identity and profile fields from a verified server session. */
5
+ id: string;
6
+ displayName: string;
7
+ email: string;
8
+ department?: string;
9
+ /** Stable data-subject identifier for this account, when applicable. */
10
+ subjectId?: string;
11
+ /** Map application roles to ndpr:staff or ndpr:admin server-side. */
12
+ roles: readonly string[];
13
+ }
14
+
15
+ export interface NDPRRequestContext {
16
+ /** Verified tenant boundary. Never take this value from request input. */
17
+ tenantId: string;
18
+ /** Normalized verified actor; null for public/anonymous requests. */
19
+ actor: NDPRVerifiedActor | null;
20
+ actorId: string | null;
21
+ /** Verified account subject ID or high-entropy anonymous browser ID. */
22
+ subjectId: string | null;
23
+ subjectSource: 'verified-account-subject' | 'anonymous-uuid-capability' | null;
24
+ /** Verified application roles used by your authorization policy. */
25
+ roles: readonly string[];
26
+ }
27
+
28
+ export type NDPRContextRequirement = 'tenant' | 'subject' | 'staff';
29
+ export interface NDPRContextProblem { status: 401 | 403 | 503; error: string }
30
+ export type NDPRVerifiedActorResolver = (
31
+ request: NextRequest,
32
+ ) => Promise<NDPRVerifiedActor | null>;
33
+
34
+ const ANONYMOUS_SUBJECT_PATTERN =
35
+ /^anon_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36
+ const STAFF_ROLES = new Set(['ndpr:staff', 'ndpr:admin']);
37
+
38
+ /**
39
+ * Authentication integration seam. Replace this safe default with your
40
+ * verified server session provider. Never trust actor, account subject,
41
+ * profile, role, or tenant fields from request-controlled input.
42
+ */
43
+ export async function resolveVerifiedNDPRActor(
44
+ _request: NextRequest,
45
+ ): Promise<NDPRVerifiedActor | null> {
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * The tenant comes only from server configuration. The only client-provided
51
+ * identity accepted by default is a random anon_<UUID> subject capability;
52
+ * it never grants staff access.
53
+ */
54
+ export async function resolveNDPRRequestContext(
55
+ request: NextRequest,
56
+ actorResolver: NDPRVerifiedActorResolver = resolveVerifiedNDPRActor,
57
+ ): Promise<NDPRRequestContext> {
58
+ const tenantId = process.env.NDPR_TENANT_ID?.trim() ?? '';
59
+ const candidate = request.headers.get('x-ndpr-subject-id')?.trim() ?? '';
60
+ const anonymousSubjectId = ANONYMOUS_SUBJECT_PATTERN.test(candidate)
61
+ ? candidate
62
+ : null;
63
+ const actor = normalizeVerifiedActor(await actorResolver(request));
64
+ const subjectId = actor?.subjectId ?? anonymousSubjectId;
65
+ const subjectSource = actor?.subjectId
66
+ ? 'verified-account-subject' as const
67
+ : anonymousSubjectId
68
+ ? 'anonymous-uuid-capability' as const
69
+ : null;
70
+
71
+ return {
72
+ tenantId,
73
+ actor,
74
+ actorId: actor?.id ?? null,
75
+ subjectId,
76
+ subjectSource,
77
+ roles: actor?.roles ?? [],
78
+ };
79
+ }
80
+
81
+ export function getNDPRContextProblem(
82
+ context: NDPRRequestContext,
83
+ requirement: NDPRContextRequirement,
84
+ ): NDPRContextProblem | null {
85
+ if (!context.tenantId) {
86
+ return {
87
+ status: 503,
88
+ error: 'NDPR_TENANT_ID is not configured on the server',
89
+ };
90
+ }
91
+ if (requirement === 'subject' && !context.subjectId) {
92
+ return { status: 401, error: 'A verified data-subject identity is required' };
93
+ }
94
+ if (requirement === 'staff' && !context.actorId) {
95
+ return {
96
+ status: 401,
97
+ error: 'Connect resolveVerifiedNDPRActor to verified staff authentication',
98
+ };
99
+ }
100
+ if (requirement === 'staff'
101
+ && !context.roles.some((role) => STAFF_ROLES.has(role))) {
102
+ return { status: 403, error: 'NDPR staff or administrator role required' };
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function normalizeVerifiedActor(
108
+ actor: NDPRVerifiedActor | null,
109
+ ): NDPRVerifiedActor | null {
110
+ if (!actor
111
+ || typeof actor.id !== 'string'
112
+ || typeof actor.displayName !== 'string'
113
+ || typeof actor.email !== 'string'
114
+ || !Array.isArray(actor.roles)) {
115
+ return null;
116
+ }
117
+ const id = actor.id.trim();
118
+ const displayName = actor.displayName.trim();
119
+ const email = actor.email.trim();
120
+ if (!id || !displayName || !email) return null;
121
+
122
+ return {
123
+ id,
124
+ displayName,
125
+ email,
126
+ department: typeof actor.department === 'string'
127
+ ? actor.department.trim() || undefined
128
+ : undefined,
129
+ subjectId: typeof actor.subjectId === 'string'
130
+ ? actor.subjectId.trim() || undefined
131
+ : undefined,
132
+ roles: actor.roles
133
+ .filter((role): role is string => typeof role === 'string')
134
+ .map((role) => role.trim())
135
+ .filter(Boolean),
136
+ };
137
+ }
@@ -1,15 +1,5 @@
1
- // NDPA Compliance Schema — generated by create-ndpr
2
- // Organisation: {{ORG_NAME}}
3
- // DPO: {{DPO_EMAIL}}
4
- //
5
- // Covers:
6
- // - ConsentRecord — NDPA §25–26 (consent & withdrawal)
7
- // - DSRRequest — NDPA Part IV §34–38 (data subject rights)
8
- // - BreachReport — NDPA §40 (72-hour breach notification)
9
- // - ProcessingRecord — ROPA (accountability principle)
10
- // - ComplianceAuditLog — §44 audit trail
11
- //
12
- // Run `prisma migrate dev --name ndpr-init` to apply.
1
+ // NDPA compliance schema — generated by create-ndpr for {{ORG_NAME_COMMENT}}
2
+ // Every row is tenant-scoped. Derive tenantId only from verified server context.
13
3
 
14
4
  generator client {
15
5
  provider = "prisma-client-js"
@@ -21,25 +11,32 @@ datasource db {
21
11
  }
22
12
 
23
13
  model ConsentRecord {
24
- id String @id @default(cuid())
25
- subjectId String
26
- consents Json
27
- version String
28
- method String
29
- lawfulBasis String?
30
- ipAddress String?
31
- userAgent String?
32
- createdAt DateTime @default(now())
33
- revokedAt DateTime?
34
-
35
- @@index([subjectId])
14
+ id String @id @default(cuid())
15
+ tenantId String
16
+ subjectId String
17
+ activeSubjectKey String? @unique
18
+ consents Json
19
+ version String
20
+ method String
21
+ hasInteracted Boolean
22
+ lawfulBasis String?
23
+ ipAddress String?
24
+ userAgent String?
25
+ clientTimestamp DateTime
26
+ createdAt DateTime @default(now())
27
+ revokedAt DateTime?
28
+
29
+ @@index([tenantId, subjectId, revokedAt])
30
+ @@index([tenantId, subjectId, clientTimestamp, version, method])
36
31
  @@map("ndpr_consent_records")
37
32
  }
38
33
 
39
34
  model DSRRequest {
40
- id String @id @default(cuid())
35
+ id String @id @default(cuid())
36
+ tenantId String
37
+ subjectId String
41
38
  type String
42
- status String @default("pending")
39
+ status String @default("pending")
43
40
  subjectName String
44
41
  subjectEmail String
45
42
  subjectPhone String?
@@ -48,43 +45,53 @@ model DSRRequest {
48
45
  description String?
49
46
  internalNotes String?
50
47
  assignedTo String?
51
- submittedAt DateTime @default(now())
48
+ submittedAt DateTime @default(now())
52
49
  acknowledgedAt DateTime?
53
50
  completedAt DateTime?
54
51
  dueAt DateTime
55
52
 
56
- @@index([status])
57
- @@index([subjectEmail])
53
+ @@index([tenantId, status])
54
+ @@index([tenantId, subjectId])
55
+ @@index([tenantId, subjectEmail])
58
56
  @@map("ndpr_dsr_requests")
59
57
  }
60
58
 
61
59
  model BreachReport {
62
- id String @id @default(cuid())
63
- title String
64
- description String
65
- category String
66
- severity String
67
- status String @default("ongoing")
68
- discoveredAt DateTime
69
- occurredAt DateTime?
70
- reportedAt DateTime @default(now())
71
- ndpcNotifiedAt DateTime?
72
- reporterName String
73
- reporterEmail String
74
- reporterDepartment String?
75
- affectedSystems Json
76
- dataTypes Json
77
- estimatedAffected Int?
78
- initialActions String?
79
- ndpcNotificationSent Boolean @default(false)
80
-
81
- @@index([status])
82
- @@index([severity])
60
+ id String @id @default(cuid())
61
+ tenantId String
62
+ title String
63
+ description String
64
+ category String
65
+ severity String
66
+ status String @default("ongoing")
67
+ discoveredAt DateTime
68
+ occurredAt DateTime?
69
+ reportedAt DateTime @default(now())
70
+ ndpcNotifiedAt DateTime?
71
+ reporterName String
72
+ reporterEmail String
73
+ reporterDepartment String?
74
+ affectedSystems Json
75
+ dataTypes Json
76
+ involvesSensitiveData Boolean @default(false)
77
+ estimatedAffected Int?
78
+ approximateRecordCount Int?
79
+ dataSubjectCategories Json
80
+ likelyConsequences String?
81
+ mitigationMeasures String?
82
+ isPhasedReport Boolean @default(false)
83
+ supplementsReportId String?
84
+ initialActions String?
85
+ ndpcNotificationSent Boolean @default(false)
86
+
87
+ @@index([tenantId, status])
88
+ @@index([tenantId, severity])
83
89
  @@map("ndpr_breach_reports")
84
90
  }
85
91
 
86
92
  model ProcessingRecord {
87
93
  id String @id @default(cuid())
94
+ tenantId String
88
95
  purpose String
89
96
  lawfulBasis String
90
97
  dataCategories Json
@@ -99,11 +106,74 @@ model ProcessingRecord {
99
106
  createdAt DateTime @default(now())
100
107
  updatedAt DateTime @updatedAt
101
108
 
109
+ @@index([tenantId, status])
102
110
  @@map("ndpr_processing_records")
103
111
  }
104
112
 
113
+ model DPIARecord {
114
+ id String @id @default(cuid())
115
+ tenantId String
116
+ projectName String
117
+ description String
118
+ dpiaData Json
119
+ overallRisk String
120
+ score Int
121
+ status String @default("draft")
122
+ conductedBy String
123
+ approvedBy String?
124
+ createdAt DateTime @default(now())
125
+ updatedAt DateTime @updatedAt
126
+ removedAt DateTime?
127
+
128
+ @@index([tenantId, removedAt, status])
129
+ @@map("ndpr_dpia_records")
130
+ }
131
+
132
+ model LawfulBasisRecord {
133
+ id String @id @default(cuid())
134
+ tenantId String
135
+ activityName String
136
+ lawfulBasis String
137
+ justification String
138
+ dataCategories Json
139
+ purposes Json
140
+ assessedBy String
141
+ assessedAt DateTime @default(now())
142
+ reviewDate DateTime?
143
+ createdAt DateTime @default(now())
144
+ updatedAt DateTime @updatedAt
145
+ removedAt DateTime?
146
+
147
+ @@index([tenantId, removedAt, lawfulBasis])
148
+ @@map("ndpr_lawful_basis_records")
149
+ }
150
+
151
+ model CrossBorderTransferRecord {
152
+ id String @id @default(cuid())
153
+ tenantId String
154
+ destinationCountry String
155
+ recipientName String
156
+ transferMechanism String
157
+ safeguards String
158
+ dataCategories Json
159
+ adequacyStatus String
160
+ ndpcApprovalRequired Boolean @default(false)
161
+ ndpcApprovalReference String?
162
+ riskLevel String
163
+ status String @default("active")
164
+ createdAt DateTime @default(now())
165
+ updatedAt DateTime @updatedAt
166
+ removedAt DateTime?
167
+
168
+ @@index([tenantId, removedAt, destinationCountry])
169
+ @@index([tenantId, removedAt, riskLevel])
170
+ @@index([tenantId, removedAt, status])
171
+ @@map("ndpr_cross_border_transfer_records")
172
+ }
173
+
105
174
  model ComplianceAuditLog {
106
175
  id String @id @default(cuid())
176
+ tenantId String
107
177
  module String
108
178
  action String
109
179
  entityId String
@@ -112,6 +182,6 @@ model ComplianceAuditLog {
112
182
  performedBy String?
113
183
  createdAt DateTime @default(now())
114
184
 
115
- @@index([module, entityId])
185
+ @@index([tenantId, module, entityId])
116
186
  @@map("ndpr_audit_log")
117
187
  }
@@ -1,250 +0,0 @@
1
- /**
2
- * Express — NDPR Compliance Router
3
- * Generated by create-ndpr for: {{ORG_NAME}}
4
- * DPO: {{DPO_EMAIL}}
5
- *
6
- * Mount this router in your Express app:
7
- *
8
- * import express from 'express';
9
- * import cookieParser from 'cookie-parser';
10
- * import { createNDPRRouter } from './src/ndpr';
11
- *
12
- * const app = express();
13
- * app.use(express.json());
14
- * app.use(cookieParser());
15
- * app.use('/api/ndpr', createNDPRRouter());
16
- *
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
21
- */
22
-
23
- import { Router } from 'express';
24
- import { PrismaClient } from '@prisma/client';
25
-
26
- const prisma = new PrismaClient();
27
-
28
- // ---------------------------------------------------------------------------
29
- // Consent router — NDPA §25–26
30
- // ---------------------------------------------------------------------------
31
-
32
- function consentRouter(): Router {
33
- const router = Router();
34
-
35
- // GET /consent?subjectId=xxx
36
- router.get('/', async (req, res) => {
37
- const { subjectId } = req.query;
38
- if (!subjectId || typeof subjectId !== 'string') {
39
- return res.status(400).json({ error: 'subjectId required' });
40
- }
41
- const record = await prisma.consentRecord.findFirst({
42
- where: { subjectId, revokedAt: null },
43
- orderBy: { createdAt: 'desc' },
44
- });
45
- return res.json(record);
46
- });
47
-
48
- // POST /consent
49
- router.post('/', async (req, res) => {
50
- const { subjectId, consents, version, method, lawfulBasis } = req.body;
51
- if (!subjectId || !consents || !version) {
52
- return res.status(400).json({ error: 'subjectId, consents, and version are required' });
53
- }
54
- await prisma.consentRecord.updateMany({
55
- where: { subjectId, revokedAt: null },
56
- data: { revokedAt: new Date() },
57
- });
58
- const record = await prisma.consentRecord.create({
59
- data: {
60
- subjectId,
61
- consents,
62
- version,
63
- method: method ?? 'api',
64
- lawfulBasis: lawfulBasis ?? null,
65
- ipAddress:
66
- (req.headers['x-forwarded-for'] as string)?.split(',')[0].trim() ??
67
- req.socket.remoteAddress ??
68
- null,
69
- userAgent: req.headers['user-agent'] ?? null,
70
- },
71
- });
72
- await prisma.complianceAuditLog.create({
73
- data: {
74
- module: 'consent',
75
- action: 'created',
76
- entityId: record.id,
77
- entityType: 'ConsentRecord',
78
- changes: { subjectId, version, consents },
79
- },
80
- });
81
- return res.status(201).json(record);
82
- });
83
-
84
- // DELETE /consent?subjectId=xxx
85
- router.delete('/', async (req, res) => {
86
- const { subjectId } = req.query;
87
- if (!subjectId || typeof subjectId !== 'string') {
88
- return res.status(400).json({ error: 'subjectId required' });
89
- }
90
- await prisma.consentRecord.updateMany({
91
- where: { subjectId, revokedAt: null },
92
- data: { revokedAt: new Date() },
93
- });
94
- await prisma.complianceAuditLog.create({
95
- data: {
96
- module: 'consent',
97
- action: 'revoked',
98
- entityId: subjectId,
99
- entityType: 'ConsentRecord',
100
- },
101
- });
102
- return res.json({ success: true });
103
- });
104
-
105
- return router;
106
- }
107
-
108
- // ---------------------------------------------------------------------------
109
- // DSR router — NDPA §34–38
110
- // ---------------------------------------------------------------------------
111
-
112
- function dsrRouter(): Router {
113
- const router = Router();
114
-
115
- // GET /dsr?status=pending
116
- router.get('/', async (req, res) => {
117
- const { status } = req.query;
118
- const requests = await prisma.dSRRequest.findMany({
119
- where: status && typeof status === 'string' ? { status } : undefined,
120
- orderBy: { submittedAt: 'desc' },
121
- });
122
- return res.json(requests);
123
- });
124
-
125
- // POST /dsr
126
- router.post('/', async (req, res) => {
127
- const { type, subjectName, subjectEmail, subjectPhone, identifierType, identifierValue, description } =
128
- req.body;
129
- if (!type || !subjectName || !subjectEmail || !identifierType || !identifierValue) {
130
- return res
131
- .status(400)
132
- .json({ error: 'type, subjectName, subjectEmail, identifierType, and identifierValue are required' });
133
- }
134
- const dueAt = new Date();
135
- dueAt.setDate(dueAt.getDate() + 30);
136
- const request = await prisma.dSRRequest.create({
137
- data: {
138
- type,
139
- subjectName,
140
- subjectEmail,
141
- subjectPhone: subjectPhone ?? null,
142
- identifierType,
143
- identifierValue,
144
- description: description ?? null,
145
- status: 'pending',
146
- dueAt,
147
- },
148
- });
149
- await prisma.complianceAuditLog.create({
150
- data: {
151
- module: 'dsr',
152
- action: 'submitted',
153
- entityId: request.id,
154
- entityType: 'DSRRequest',
155
- changes: { type, subjectEmail, status: 'pending' },
156
- },
157
- });
158
- return res.status(201).json(request);
159
- });
160
-
161
- return router;
162
- }
163
-
164
- // ---------------------------------------------------------------------------
165
- // Breach router — NDPA §40
166
- // ---------------------------------------------------------------------------
167
-
168
- function breachRouter(): Router {
169
- const router = Router();
170
-
171
- function calculateSeverity(category: string, estimatedAffected?: number) {
172
- const highRisk = ['unauthorized_access', 'ransomware', 'data_exfiltration', 'identity_theft'];
173
- if (highRisk.includes(category)) return (estimatedAffected ?? 0) > 1000 ? 'critical' : 'high';
174
- if ((estimatedAffected ?? 0) > 500) return 'high';
175
- if ((estimatedAffected ?? 0) > 50) return 'medium';
176
- return 'low';
177
- }
178
-
179
- // GET /breach?status=ongoing
180
- router.get('/', async (req, res) => {
181
- const { status } = req.query;
182
- const reports = await prisma.breachReport.findMany({
183
- where: status && typeof status === 'string' ? { status } : undefined,
184
- orderBy: { reportedAt: 'desc' },
185
- });
186
- return res.json(reports);
187
- });
188
-
189
- // POST /breach
190
- router.post('/', async (req, res) => {
191
- const {
192
- title, description, category, discoveredAt, occurredAt,
193
- reporterName, reporterEmail, reporterDepartment,
194
- affectedSystems, dataTypes, estimatedAffected, initialActions,
195
- } = req.body;
196
-
197
- if (!title || !description || !category || !discoveredAt || !reporterName || !reporterEmail) {
198
- return res.status(400).json({
199
- error: 'title, description, category, discoveredAt, reporterName, and reporterEmail are required',
200
- });
201
- }
202
- if (!Array.isArray(affectedSystems) || !Array.isArray(dataTypes)) {
203
- return res.status(400).json({ error: 'affectedSystems and dataTypes must be arrays' });
204
- }
205
-
206
- const severity = calculateSeverity(category, estimatedAffected);
207
- const report = await prisma.breachReport.create({
208
- data: {
209
- title, description, category, severity, status: 'ongoing',
210
- discoveredAt: new Date(discoveredAt),
211
- occurredAt: occurredAt ? new Date(occurredAt) : null,
212
- reporterName, reporterEmail,
213
- reporterDepartment: reporterDepartment ?? null,
214
- affectedSystems, dataTypes,
215
- estimatedAffected: estimatedAffected ?? null,
216
- initialActions: initialActions ?? null,
217
- },
218
- });
219
- await prisma.complianceAuditLog.create({
220
- data: {
221
- module: 'breach',
222
- action: 'reported',
223
- entityId: report.id,
224
- entityType: 'BreachReport',
225
- changes: { title, category, severity, status: 'ongoing' },
226
- },
227
- });
228
- return res.status(201).json(report);
229
- });
230
-
231
- return router;
232
- }
233
-
234
- // ---------------------------------------------------------------------------
235
- // Main factory
236
- // ---------------------------------------------------------------------------
237
-
238
- /**
239
- * Returns a fully configured NDPR compliance Express Router.
240
- *
241
- * Mount with:
242
- * app.use('/api/ndpr', createNDPRRouter());
243
- */
244
- export function createNDPRRouter(): Router {
245
- const router = Router();
246
- router.use('/consent', consentRouter());
247
- router.use('/dsr', dsrRouter());
248
- router.use('/breach', breachRouter());
249
- return router;
250
- }