signet-protocol 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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/anomaly.d.ts +42 -0
  4. package/dist/anomaly.d.ts.map +1 -0
  5. package/dist/anomaly.js +209 -0
  6. package/dist/anomaly.js.map +1 -0
  7. package/dist/badge.d.ts +56 -0
  8. package/dist/badge.d.ts.map +1 -0
  9. package/dist/badge.js +171 -0
  10. package/dist/badge.js.map +1 -0
  11. package/dist/bonds.d.ts +39 -0
  12. package/dist/bonds.d.ts.map +1 -0
  13. package/dist/bonds.js +149 -0
  14. package/dist/bonds.js.map +1 -0
  15. package/dist/challenges.d.ts +18 -0
  16. package/dist/challenges.d.ts.map +1 -0
  17. package/dist/challenges.js +145 -0
  18. package/dist/challenges.js.map +1 -0
  19. package/dist/cold-call.d.ts +74 -0
  20. package/dist/cold-call.d.ts.map +1 -0
  21. package/dist/cold-call.js +176 -0
  22. package/dist/cold-call.js.map +1 -0
  23. package/dist/compliance.d.ts +82 -0
  24. package/dist/compliance.d.ts.map +1 -0
  25. package/dist/compliance.js +478 -0
  26. package/dist/compliance.js.map +1 -0
  27. package/dist/connections.d.ts +63 -0
  28. package/dist/connections.d.ts.map +1 -0
  29. package/dist/connections.js +170 -0
  30. package/dist/connections.js.map +1 -0
  31. package/dist/constants.d.ts +86 -0
  32. package/dist/constants.d.ts.map +1 -0
  33. package/dist/constants.js +124 -0
  34. package/dist/constants.js.map +1 -0
  35. package/dist/credentials.d.ts +190 -0
  36. package/dist/credentials.d.ts.map +1 -0
  37. package/dist/credentials.js +686 -0
  38. package/dist/credentials.js.map +1 -0
  39. package/dist/crypto.d.ts +27 -0
  40. package/dist/crypto.d.ts.map +1 -0
  41. package/dist/crypto.js +75 -0
  42. package/dist/crypto.js.map +1 -0
  43. package/dist/errors.d.ts +17 -0
  44. package/dist/errors.d.ts.map +1 -0
  45. package/dist/errors.js +29 -0
  46. package/dist/errors.js.map +1 -0
  47. package/dist/i18n.d.ts +98 -0
  48. package/dist/i18n.d.ts.map +1 -0
  49. package/dist/i18n.js +1118 -0
  50. package/dist/i18n.js.map +1 -0
  51. package/dist/identity-bridge.d.ts +52 -0
  52. package/dist/identity-bridge.d.ts.map +1 -0
  53. package/dist/identity-bridge.js +228 -0
  54. package/dist/identity-bridge.js.map +1 -0
  55. package/dist/identity-tree.d.ts +47 -0
  56. package/dist/identity-tree.d.ts.map +1 -0
  57. package/dist/identity-tree.js +69 -0
  58. package/dist/identity-tree.js.map +1 -0
  59. package/dist/index.d.ts +55 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +86 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/key-derivation.d.ts +43 -0
  64. package/dist/key-derivation.d.ts.map +1 -0
  65. package/dist/key-derivation.js +212 -0
  66. package/dist/key-derivation.js.map +1 -0
  67. package/dist/lsag.d.ts +23 -0
  68. package/dist/lsag.d.ts.map +1 -0
  69. package/dist/lsag.js +35 -0
  70. package/dist/lsag.js.map +1 -0
  71. package/dist/merkle.d.ts +19 -0
  72. package/dist/merkle.d.ts.map +1 -0
  73. package/dist/merkle.js +155 -0
  74. package/dist/merkle.js.map +1 -0
  75. package/dist/policies.d.ts +22 -0
  76. package/dist/policies.d.ts.map +1 -0
  77. package/dist/policies.js +123 -0
  78. package/dist/policies.js.map +1 -0
  79. package/dist/range-proof.d.ts +6 -0
  80. package/dist/range-proof.d.ts.map +1 -0
  81. package/dist/range-proof.js +45 -0
  82. package/dist/range-proof.js.map +1 -0
  83. package/dist/relay.d.ts +106 -0
  84. package/dist/relay.d.ts.map +1 -0
  85. package/dist/relay.js +336 -0
  86. package/dist/relay.js.map +1 -0
  87. package/dist/ring-signature.d.ts +35 -0
  88. package/dist/ring-signature.d.ts.map +1 -0
  89. package/dist/ring-signature.js +56 -0
  90. package/dist/ring-signature.js.map +1 -0
  91. package/dist/shamir.d.ts +55 -0
  92. package/dist/shamir.d.ts.map +1 -0
  93. package/dist/shamir.js +253 -0
  94. package/dist/shamir.js.map +1 -0
  95. package/dist/signet-words.d.ts +42 -0
  96. package/dist/signet-words.d.ts.map +1 -0
  97. package/dist/signet-words.js +82 -0
  98. package/dist/signet-words.js.map +1 -0
  99. package/dist/store.d.ts +65 -0
  100. package/dist/store.d.ts.map +1 -0
  101. package/dist/store.js +290 -0
  102. package/dist/store.js.map +1 -0
  103. package/dist/trust-score.d.ts +9 -0
  104. package/dist/trust-score.d.ts.map +1 -0
  105. package/dist/trust-score.js +186 -0
  106. package/dist/trust-score.js.map +1 -0
  107. package/dist/types.d.ts +358 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +15 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist/utils.d.ts +11 -0
  112. package/dist/utils.d.ts.map +1 -0
  113. package/dist/utils.js +21 -0
  114. package/dist/utils.js.map +1 -0
  115. package/dist/validation.d.ts +33 -0
  116. package/dist/validation.d.ts.map +1 -0
  117. package/dist/validation.js +312 -0
  118. package/dist/validation.js.map +1 -0
  119. package/dist/verifiers.d.ts +18 -0
  120. package/dist/verifiers.d.ts.map +1 -0
  121. package/dist/verifiers.js +118 -0
  122. package/dist/verifiers.js.map +1 -0
  123. package/dist/vouches.d.ts +14 -0
  124. package/dist/vouches.d.ts.map +1 -0
  125. package/dist/vouches.js +103 -0
  126. package/dist/vouches.js.map +1 -0
  127. package/package.json +76 -0
  128. package/src/anomaly.ts +307 -0
  129. package/src/badge.ts +208 -0
  130. package/src/bonds.ts +203 -0
  131. package/src/challenges.ts +187 -0
  132. package/src/cold-call.ts +238 -0
  133. package/src/compliance.ts +612 -0
  134. package/src/connections.ts +216 -0
  135. package/src/constants.ts +146 -0
  136. package/src/credentials.ts +908 -0
  137. package/src/crypto.ts +85 -0
  138. package/src/errors.ts +31 -0
  139. package/src/i18n.ts +1347 -0
  140. package/src/identity-bridge.ts +262 -0
  141. package/src/identity-tree.ts +90 -0
  142. package/src/index.ts +452 -0
  143. package/src/lsag.ts +53 -0
  144. package/src/merkle.ts +176 -0
  145. package/src/policies.ts +154 -0
  146. package/src/range-proof.ts +66 -0
  147. package/src/relay.ts +433 -0
  148. package/src/ring-signature.ts +76 -0
  149. package/src/signet-words.ts +122 -0
  150. package/src/store.ts +336 -0
  151. package/src/trust-score.ts +208 -0
  152. package/src/types.ts +482 -0
  153. package/src/utils.ts +20 -0
  154. package/src/validation.ts +391 -0
  155. package/src/verifiers.ts +156 -0
  156. package/src/vouches.ts +141 -0
@@ -0,0 +1,612 @@
1
+ // Signet Compliance Module
2
+ // Jurisdiction-aware compliance checking for credentials, data transfers,
3
+ // child protection, and professional regulation.
4
+
5
+ import {
6
+ getJurisdiction,
7
+ canTransferData,
8
+ isProfessionRegulated,
9
+ type Jurisdiction,
10
+ type ProfessionType,
11
+ } from 'jurisdiction-kit';
12
+ import { getTagValue } from './validation.js';
13
+ import { SignetValidationError } from './errors.js';
14
+ import type { NostrEvent } from './types.js';
15
+
16
+ // --- Types ---
17
+
18
+ export type ComplianceSeverity = 'error' | 'warning' | 'info';
19
+
20
+ export interface ComplianceIssue {
21
+ code: string;
22
+ severity: ComplianceSeverity;
23
+ jurisdiction: string;
24
+ message: string;
25
+ regulation: string;
26
+ remediation?: string;
27
+ }
28
+
29
+ export interface ComplianceResult {
30
+ compliant: boolean;
31
+ issues: ComplianceIssue[];
32
+ jurisdiction: string;
33
+ checkedAt: number;
34
+ }
35
+
36
+ export interface CrossBorderResult {
37
+ allowed: boolean;
38
+ mechanism?: string;
39
+ issues: ComplianceIssue[];
40
+ fromJurisdiction: string;
41
+ toJurisdiction: string;
42
+ }
43
+
44
+ export interface ChildComplianceResult {
45
+ compliant: boolean;
46
+ issues: ComplianceIssue[];
47
+ jurisdiction: string;
48
+ minConsentAge: number;
49
+ ageOfMajority: number;
50
+ requiresParentalConsent: boolean;
51
+ }
52
+
53
+ export interface ConsentRequirement {
54
+ jurisdiction: string;
55
+ requiresExplicitConsent: boolean;
56
+ consentAge: number;
57
+ parentalConsentRequired: boolean;
58
+ dataCategories: string[];
59
+ specialCategories: string[];
60
+ notes?: string;
61
+ }
62
+
63
+ const GDPR_JURISDICTIONS = ['GB', 'FR', 'DE', 'ES', 'IT', 'NL', 'IE'];
64
+
65
+ // --- Credential Compliance ---
66
+
67
+ /**
68
+ * Check if a credential complies with a jurisdiction's regulations.
69
+ */
70
+ export function checkCredentialCompliance(
71
+ credential: NostrEvent,
72
+ jurisdictionCode: string
73
+ ): ComplianceResult {
74
+ const issues: ComplianceIssue[] = [];
75
+ const j = getJurisdiction(jurisdictionCode);
76
+
77
+ if (!j) {
78
+ return {
79
+ compliant: false,
80
+ issues: [{
81
+ code: 'UNKNOWN_JURISDICTION',
82
+ severity: 'error',
83
+ jurisdiction: jurisdictionCode,
84
+ message: `Jurisdiction '${jurisdictionCode}' is not recognised in the Signet registry.`,
85
+ regulation: 'Signet Protocol',
86
+ }],
87
+ jurisdiction: jurisdictionCode,
88
+ checkedAt: Math.floor(Date.now() / 1000),
89
+ };
90
+ }
91
+
92
+ // Check profession regulation
93
+ const profession = getTagValue(credential, 'profession');
94
+ if (profession) {
95
+ const profType = mapProfessionString(profession);
96
+ if (profType && !isProfessionRegulated(jurisdictionCode, profType)) {
97
+ issues.push({
98
+ code: 'PROFESSION_NOT_REGULATED',
99
+ severity: 'warning',
100
+ jurisdiction: jurisdictionCode,
101
+ message: `Profession '${profession}' is not registered as regulated in ${j.name}.`,
102
+ regulation: 'Professional Regulation',
103
+ remediation: 'Verify the profession is regulated in this jurisdiction before issuing credentials.',
104
+ });
105
+ }
106
+ }
107
+
108
+ // Check credential expiry
109
+ const expiresStr = getTagValue(credential, 'expiration');
110
+ if (expiresStr) {
111
+ const expires = parseInt(expiresStr, 10);
112
+ const now = Math.floor(Date.now() / 1000);
113
+ if (isNaN(expires) || expires < now) {
114
+ issues.push({
115
+ code: 'CREDENTIAL_EXPIRED',
116
+ severity: 'error',
117
+ jurisdiction: jurisdictionCode,
118
+ message: 'Credential has expired.',
119
+ regulation: 'Credential Validity',
120
+ remediation: 'Renew the credential before use.',
121
+ });
122
+ }
123
+ }
124
+
125
+ // Check electronic signature recognition
126
+ if (!j.eSignatureRecognised) {
127
+ issues.push({
128
+ code: 'ESIGNATURE_NOT_RECOGNISED',
129
+ severity: 'warning',
130
+ jurisdiction: jurisdictionCode,
131
+ message: `Electronic signatures may not be fully recognised in ${j.name}.`,
132
+ regulation: 'Electronic Signatures',
133
+ remediation: 'Additional attestation or wet signature may be required.',
134
+ });
135
+ }
136
+
137
+ // Check scope for child-related credentials
138
+ const scope = getTagValue(credential, 'scope');
139
+ if (scope === 'adult+child') {
140
+ const childIssues = checkChildDataRequirements(credential, j);
141
+ issues.push(...childIssues);
142
+ }
143
+
144
+ // Check data protection consent requirements
145
+ if (j.dataProtection.requiresExplicitConsent) {
146
+ issues.push({
147
+ code: 'CONSENT_REQUIRED',
148
+ severity: 'info',
149
+ jurisdiction: jurisdictionCode,
150
+ message: `${j.dataProtection.name} requires explicit consent for processing personal data.`,
151
+ regulation: j.dataProtection.fullName,
152
+ remediation: 'Ensure explicit consent is obtained and recorded before credential issuance.',
153
+ });
154
+ }
155
+
156
+ // Check breach notification requirements
157
+ if (j.dataProtection.breachNotificationHours > 0) {
158
+ issues.push({
159
+ code: 'BREACH_NOTIFICATION',
160
+ severity: 'info',
161
+ jurisdiction: jurisdictionCode,
162
+ message: `Data breaches must be reported to ${j.dataProtection.supervisoryAuthority} within ${j.dataProtection.breachNotificationHours} hours.`,
163
+ regulation: j.dataProtection.fullName,
164
+ });
165
+ }
166
+
167
+ return {
168
+ compliant: !issues.some((i) => i.severity === 'error'),
169
+ issues,
170
+ jurisdiction: jurisdictionCode,
171
+ checkedAt: Math.floor(Date.now() / 1000),
172
+ };
173
+ }
174
+
175
+ // --- Cross-Border Compliance ---
176
+
177
+ /**
178
+ * Check if a credential can be used across borders.
179
+ */
180
+ export function checkCrossBorderCompliance(
181
+ fromJurisdiction: string,
182
+ toJurisdiction: string
183
+ ): CrossBorderResult {
184
+ const issues: ComplianceIssue[] = [];
185
+ const transfer = canTransferData(fromJurisdiction, toJurisdiction);
186
+
187
+ const fromJ = getJurisdiction(fromJurisdiction);
188
+ const toJ = getJurisdiction(toJurisdiction);
189
+
190
+ if (!fromJ || !toJ) {
191
+ return {
192
+ allowed: false,
193
+ issues: [{
194
+ code: 'UNKNOWN_JURISDICTION',
195
+ severity: 'error',
196
+ jurisdiction: fromJurisdiction,
197
+ message: 'One or both jurisdictions are not recognised.',
198
+ regulation: 'Signet Protocol',
199
+ }],
200
+ fromJurisdiction,
201
+ toJurisdiction,
202
+ };
203
+ }
204
+
205
+ if (transfer.mechanism === 'safeguards-required') {
206
+ issues.push({
207
+ code: 'SAFEGUARDS_REQUIRED',
208
+ severity: 'warning',
209
+ jurisdiction: fromJurisdiction,
210
+ message: `Transfer from ${fromJ.name} to ${toJ.name} requires Standard Contractual Clauses or equivalent safeguards.`,
211
+ regulation: fromJ.dataProtection.fullName,
212
+ remediation: 'Implement SCCs, BCRs, or obtain an adequacy determination.',
213
+ });
214
+ }
215
+
216
+ // Special cases
217
+ if (fromJurisdiction === 'CN') {
218
+ issues.push({
219
+ code: 'CN_DATA_LOCALIZATION',
220
+ severity: 'warning',
221
+ jurisdiction: 'CN',
222
+ message: 'China\'s PIPL requires data localization. Cross-border transfers require security assessment by CAC.',
223
+ regulation: 'Personal Information Protection Law (PIPL)',
224
+ remediation: 'Complete a data export security assessment or obtain PPC certification.',
225
+ });
226
+ }
227
+
228
+ if (fromJurisdiction === 'IN') {
229
+ issues.push({
230
+ code: 'IN_DATA_LOCALIZATION',
231
+ severity: 'info',
232
+ jurisdiction: 'IN',
233
+ message: 'India\'s DPDPA 2023 may restrict transfers to certain jurisdictions via government notification.',
234
+ regulation: 'Digital Personal Data Protection Act 2023',
235
+ remediation: 'Check the list of restricted jurisdictions published by the Indian government.',
236
+ });
237
+ }
238
+
239
+ if (fromJurisdiction === 'SA') {
240
+ issues.push({
241
+ code: 'SA_CROSS_BORDER',
242
+ severity: 'warning',
243
+ jurisdiction: 'SA',
244
+ message: 'Saudi Arabia requires data to remain within the Kingdom unless specific conditions are met.',
245
+ regulation: 'Personal Data Protection Law (Royal Decree M/19)',
246
+ remediation: 'Ensure the transfer meets SDAIA cross-border transfer requirements.',
247
+ });
248
+ }
249
+
250
+ return {
251
+ allowed: transfer.allowed,
252
+ mechanism: transfer.mechanism,
253
+ issues,
254
+ fromJurisdiction,
255
+ toJurisdiction,
256
+ };
257
+ }
258
+
259
+ // --- Child Protection Compliance ---
260
+
261
+ /**
262
+ * Check child data protection compliance for a jurisdiction.
263
+ */
264
+ export function checkChildCompliance(
265
+ childAge: number,
266
+ jurisdictionCode: string
267
+ ): ChildComplianceResult {
268
+ if (!Number.isFinite(childAge) || childAge < 0 || childAge > 150) {
269
+ throw new SignetValidationError(`Invalid childAge: ${childAge} (must be 0-150)`);
270
+ }
271
+ const issues: ComplianceIssue[] = [];
272
+ const j = getJurisdiction(jurisdictionCode);
273
+
274
+ if (!j) {
275
+ return {
276
+ compliant: false,
277
+ issues: [{
278
+ code: 'UNKNOWN_JURISDICTION',
279
+ severity: 'error',
280
+ jurisdiction: jurisdictionCode,
281
+ message: `Jurisdiction '${jurisdictionCode}' is not recognised.`,
282
+ regulation: 'Signet Protocol',
283
+ }],
284
+ jurisdiction: jurisdictionCode,
285
+ minConsentAge: 16,
286
+ ageOfMajority: 18,
287
+ requiresParentalConsent: true,
288
+ };
289
+ }
290
+
291
+ const consentAge = j.childProtection.minAgeDigitalConsent;
292
+ const majority = j.childProtection.ageOfMajority;
293
+ const needsParental = childAge < consentAge;
294
+
295
+ if (needsParental) {
296
+ issues.push({
297
+ code: 'PARENTAL_CONSENT_REQUIRED',
298
+ severity: 'error',
299
+ jurisdiction: jurisdictionCode,
300
+ message: `Child age ${childAge} is below the digital consent age of ${consentAge} in ${j.name}. Verifiable parental consent is required.`,
301
+ regulation: j.childProtection.name,
302
+ remediation: 'Obtain and record verifiable parental consent before processing child data.',
303
+ });
304
+ }
305
+
306
+ if (j.childProtection.enhancedProtections) {
307
+ issues.push({
308
+ code: 'ENHANCED_CHILD_PROTECTIONS',
309
+ severity: 'info',
310
+ jurisdiction: jurisdictionCode,
311
+ message: `${j.name} requires enhanced protections for children's data under ${j.childProtection.name}.`,
312
+ regulation: j.childProtection.name,
313
+ remediation: 'Apply data minimisation, purpose limitation, and enhanced security for child data.',
314
+ });
315
+ }
316
+
317
+ if (j.childProtection.profilingRestrictions) {
318
+ issues.push({
319
+ code: 'PROFILING_RESTRICTED',
320
+ severity: 'warning',
321
+ jurisdiction: jurisdictionCode,
322
+ message: `Automated profiling of children is restricted in ${j.name}.`,
323
+ regulation: j.childProtection.name,
324
+ remediation: 'Do not use child data for automated profiling or targeted services.',
325
+ });
326
+ }
327
+
328
+ // Jurisdiction-specific child protection rules
329
+ if (jurisdictionCode === 'US') {
330
+ issues.push({
331
+ code: 'COPPA_COMPLIANCE',
332
+ severity: 'warning',
333
+ jurisdiction: 'US',
334
+ message: 'COPPA requires verifiable parental consent before collecting data from children under 13.',
335
+ regulation: "Children's Online Privacy Protection Act (COPPA)",
336
+ remediation: 'Implement COPPA-compliant consent mechanisms (signed form, credit card verification, etc.).',
337
+ });
338
+ }
339
+
340
+ if (jurisdictionCode === 'GB') {
341
+ issues.push({
342
+ code: 'AADC_COMPLIANCE',
343
+ severity: 'info',
344
+ jurisdiction: 'GB',
345
+ message: 'The Age Appropriate Design Code (Children\'s Code) applies to services likely to be accessed by children.',
346
+ regulation: 'Age Appropriate Design Code (ICO)',
347
+ remediation: 'Conduct a Data Protection Impact Assessment (DPIA) for child-facing features.',
348
+ });
349
+ }
350
+
351
+ return {
352
+ compliant: !issues.some((i) => i.severity === 'error'),
353
+ issues,
354
+ jurisdiction: jurisdictionCode,
355
+ minConsentAge: consentAge,
356
+ ageOfMajority: majority,
357
+ requiresParentalConsent: needsParental,
358
+ };
359
+ }
360
+
361
+ // --- Consent Requirements ---
362
+
363
+ /**
364
+ * Get consent requirements for a jurisdiction.
365
+ */
366
+ export function getConsentRequirements(jurisdictionCode: string): ConsentRequirement {
367
+ const j = getJurisdiction(jurisdictionCode);
368
+
369
+ if (!j) {
370
+ return {
371
+ jurisdiction: jurisdictionCode,
372
+ requiresExplicitConsent: true,
373
+ consentAge: 16,
374
+ parentalConsentRequired: true,
375
+ dataCategories: ['identity', 'professional-status'],
376
+ specialCategories: ['biometric-hash', 'child-age-range'],
377
+ };
378
+ }
379
+
380
+ const base: ConsentRequirement = {
381
+ jurisdiction: jurisdictionCode,
382
+ requiresExplicitConsent: j.dataProtection.requiresExplicitConsent,
383
+ consentAge: j.childProtection.minAgeDigitalConsent,
384
+ parentalConsentRequired: j.childProtection.requiresParentalConsent,
385
+ dataCategories: ['identity', 'professional-status', 'jurisdiction'],
386
+ specialCategories: [],
387
+ };
388
+
389
+ // Add special category data based on credential types
390
+ if (j.childProtection.enhancedProtections) {
391
+ base.specialCategories.push('child-age-range');
392
+ }
393
+
394
+ // GDPR special categories
395
+ if (GDPR_JURISDICTIONS.includes(jurisdictionCode)) {
396
+ base.specialCategories.push('biometric-hash');
397
+ base.notes = 'GDPR Article 9 applies — biometric data used for identification is a special category.';
398
+ }
399
+
400
+ // Brazil LGPD sensitive data
401
+ if (jurisdictionCode === 'BR') {
402
+ base.specialCategories.push('biometric-hash');
403
+ base.notes = 'LGPD treats biometric data as sensitive personal data requiring specific legal basis.';
404
+ }
405
+
406
+ return base;
407
+ }
408
+
409
+ // --- Data Retention ---
410
+
411
+ /**
412
+ * Get data retention guidance for a jurisdiction.
413
+ */
414
+ export function getRetentionGuidance(jurisdictionCode: string): {
415
+ maxDays: number;
416
+ guidance: string;
417
+ regulation: string;
418
+ } {
419
+ const j = getJurisdiction(jurisdictionCode);
420
+ if (!j) {
421
+ return {
422
+ maxDays: 365,
423
+ guidance: 'Unknown jurisdiction — apply a conservative 1-year retention period.',
424
+ regulation: 'Best Practice',
425
+ };
426
+ }
427
+
428
+ if (j.dataProtection.maxRetentionDays > 0) {
429
+ return {
430
+ maxDays: j.dataProtection.maxRetentionDays,
431
+ guidance: `${j.name} mandates a maximum retention of ${j.dataProtection.maxRetentionDays} days.`,
432
+ regulation: j.dataProtection.fullName,
433
+ };
434
+ }
435
+
436
+ // Default guidance based on jurisdiction type
437
+ if (GDPR_JURISDICTIONS.includes(jurisdictionCode)) {
438
+ return {
439
+ maxDays: 0,
440
+ guidance: 'GDPR requires data to be kept no longer than necessary for the purpose. Apply data minimisation. Signet credentials are inherently time-limited via the expires tag.',
441
+ regulation: j.dataProtection.fullName,
442
+ };
443
+ }
444
+
445
+ return {
446
+ maxDays: 0,
447
+ guidance: `${j.dataProtection.name} does not specify a fixed retention period. Data should be retained only as long as necessary for the processing purpose.`,
448
+ regulation: j.dataProtection.fullName,
449
+ };
450
+ }
451
+
452
+ // --- Multi-Jurisdiction Compliance ---
453
+
454
+ /**
455
+ * Check compliance across multiple jurisdictions simultaneously.
456
+ * Useful for credentials that may be used internationally.
457
+ */
458
+ export function checkMultiJurisdictionCompliance(
459
+ credential: NostrEvent,
460
+ jurisdictions: string[]
461
+ ): Map<string, ComplianceResult> {
462
+ const results = new Map<string, ComplianceResult>();
463
+ for (const code of jurisdictions) {
464
+ results.set(code, checkCredentialCompliance(credential, code));
465
+ }
466
+ return results;
467
+ }
468
+
469
+ /**
470
+ * Find the most restrictive requirements across jurisdictions.
471
+ * Useful for setting defaults that satisfy all target jurisdictions.
472
+ */
473
+ export function getMostRestrictiveRequirements(jurisdictions: string[]): {
474
+ highestConsentAge: number;
475
+ highestAgeOfMajority: number;
476
+ requiresExplicitConsent: boolean;
477
+ shortestBreachNotification: number;
478
+ allRequireErasure: boolean;
479
+ jurisdictions: string[];
480
+ } {
481
+ let highestConsentAge = 0;
482
+ let highestAgeOfMajority = 0;
483
+ let requiresExplicitConsent = false;
484
+ let shortestBreachNotification = Infinity;
485
+ let allRequireErasure = true;
486
+
487
+ for (const code of jurisdictions) {
488
+ const j = getJurisdiction(code);
489
+ if (!j) continue;
490
+
491
+ highestConsentAge = Math.max(highestConsentAge, j.childProtection.minAgeDigitalConsent);
492
+ highestAgeOfMajority = Math.max(highestAgeOfMajority, j.childProtection.ageOfMajority);
493
+ if (j.dataProtection.requiresExplicitConsent) requiresExplicitConsent = true;
494
+ if (j.dataProtection.breachNotificationHours > 0) {
495
+ shortestBreachNotification = Math.min(shortestBreachNotification, j.dataProtection.breachNotificationHours);
496
+ }
497
+ if (!j.dataProtection.rightToErasure) allRequireErasure = false;
498
+ }
499
+
500
+ return {
501
+ highestConsentAge,
502
+ highestAgeOfMajority,
503
+ requiresExplicitConsent,
504
+ shortestBreachNotification: shortestBreachNotification === Infinity ? 0 : shortestBreachNotification,
505
+ allRequireErasure,
506
+ jurisdictions,
507
+ };
508
+ }
509
+
510
+ // --- Internal Helpers ---
511
+
512
+ function mapProfessionString(profession: string): ProfessionType | undefined {
513
+ const map: Record<string, ProfessionType> = {
514
+ solicitor: 'legal', lawyer: 'legal', attorney: 'legal', advocate: 'legal',
515
+ barrister: 'legal', avocat: 'legal', abogado: 'legal', advogado: 'legal',
516
+ rechtsanwalt: 'legal', avvocato: 'legal', advocaat: 'legal',
517
+ doctor: 'medical', physician: 'medical', surgeon: 'medical',
518
+ médecin: 'medical', arzt: 'medical', medico: 'medical',
519
+ notary: 'notary', notaire: 'notary', notar: 'notary', notaio: 'notary',
520
+ accountant: 'accounting', cpa: 'accounting', 'chartered-accountant': 'accounting',
521
+ 'expert-comptable': 'accounting', wirtschaftsprüfer: 'accounting',
522
+ engineer: 'engineering', ingénieur: 'engineering', ingenieur: 'engineering',
523
+ ingeniero: 'engineering', engenheiro: 'engineering',
524
+ teacher: 'teaching', enseignant: 'teaching', lehrer: 'teaching',
525
+ profesor: 'teaching', professor: 'teaching',
526
+ veterinarian: 'veterinary', vet: 'veterinary',
527
+ veterinario: 'veterinary', vétérinaire: 'veterinary', tierarzt: 'veterinary',
528
+ pharmacist: 'pharmacy', 'pharmacy-technician': 'pharmacy',
529
+ farmacéutico: 'pharmacy', pharmacien: 'pharmacy', apotheker: 'pharmacy',
530
+ architect: 'architecture',
531
+ arquitecto: 'architecture', architecte: 'architecture', architekt: 'architecture',
532
+ 'social-worker': 'social-work', 'social worker': 'social-work',
533
+ nurse: 'medical', midwife: 'medical',
534
+ dentist: 'medical', 'dental-hygienist': 'medical',
535
+ optometrist: 'medical', optician: 'medical',
536
+ osteopath: 'medical', chiropractor: 'medical',
537
+ paramedic: 'medical', physiotherapist: 'medical',
538
+ radiographer: 'medical', dietitian: 'medical',
539
+ 'speech-therapist': 'medical', 'occupational-therapist': 'medical',
540
+ };
541
+ return map[profession.toLowerCase()];
542
+ }
543
+
544
+ function checkChildDataRequirements(
545
+ credential: NostrEvent,
546
+ j: Jurisdiction
547
+ ): ComplianceIssue[] {
548
+ const issues: ComplianceIssue[] = [];
549
+
550
+ // Check age range tag
551
+ const ageRange = getTagValue(credential, 'age-range');
552
+ if (!ageRange) {
553
+ issues.push({
554
+ code: 'MISSING_AGE_RANGE',
555
+ severity: 'error',
556
+ jurisdiction: j.code,
557
+ message: 'Child credential is missing the age-range tag.',
558
+ regulation: j.childProtection.name,
559
+ remediation: 'Include an age-range tag (e.g., "8-12") in the credential.',
560
+ });
561
+ return issues;
562
+ }
563
+
564
+ // Handle "18+" format (adults, no upper bound)
565
+ let minAge: number;
566
+ let maxAge: number;
567
+ if (ageRange.endsWith('+')) {
568
+ minAge = parseInt(ageRange.slice(0, -1), 10);
569
+ maxAge = 150;
570
+ } else {
571
+ const [minStr, maxStr] = ageRange.split('-');
572
+ minAge = parseInt(minStr, 10);
573
+ maxAge = parseInt(maxStr, 10);
574
+ }
575
+
576
+ if (isNaN(minAge) || isNaN(maxAge)) {
577
+ issues.push({
578
+ code: 'INVALID_AGE_RANGE',
579
+ severity: 'error',
580
+ jurisdiction: j.code,
581
+ message: `Malformed age-range tag "${ageRange}" — cannot determine age bounds.`,
582
+ regulation: j.childProtection.name,
583
+ remediation: 'Use a valid age-range format: "0-3", "4-7", "8-12", "13-17", or "18+".',
584
+ });
585
+ return issues;
586
+ }
587
+
588
+ // Check if age range includes ages below digital consent age
589
+ if (minAge < j.childProtection.minAgeDigitalConsent) {
590
+ issues.push({
591
+ code: 'BELOW_CONSENT_AGE',
592
+ severity: 'warning',
593
+ jurisdiction: j.code,
594
+ message: `Age range ${ageRange} includes ages below the digital consent age of ${j.childProtection.minAgeDigitalConsent} in ${j.name}. Parental consent is required.`,
595
+ regulation: j.childProtection.name,
596
+ remediation: 'Ensure verifiable parental consent has been obtained.',
597
+ });
598
+ }
599
+
600
+ // Check that the age range is within expected bounds (skip upper check for open-ended "18+" ranges)
601
+ if (minAge < 0 || (!ageRange.endsWith('+') && maxAge > j.childProtection.ageOfMajority)) {
602
+ issues.push({
603
+ code: 'INVALID_AGE_RANGE',
604
+ severity: 'warning',
605
+ jurisdiction: j.code,
606
+ message: `Age range ${ageRange} extends beyond expected bounds for ${j.name} (0-${j.childProtection.ageOfMajority}).`,
607
+ regulation: j.childProtection.name,
608
+ });
609
+ }
610
+
611
+ return issues;
612
+ }