@c15t/backend 2.0.0-rc.5 → 2.0.0-rc.8

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 (69) hide show
  1. package/dist/302.js +473 -0
  2. package/dist/583.js +540 -0
  3. package/dist/915.js +1742 -0
  4. package/dist/cache.cjs +1 -1
  5. package/dist/cache.js +4 -415
  6. package/dist/core.cjs +484 -33
  7. package/dist/core.js +21 -2571
  8. package/dist/db/adapters/drizzle.cjs +1 -1
  9. package/dist/db/adapters/drizzle.js +1 -2
  10. package/dist/db/adapters/kysely.cjs +1 -1
  11. package/dist/db/adapters/kysely.js +1 -2
  12. package/dist/db/adapters/mongo.cjs +1 -1
  13. package/dist/db/adapters/mongo.js +1 -2
  14. package/dist/db/adapters/prisma.cjs +1 -1
  15. package/dist/db/adapters/prisma.js +1 -2
  16. package/dist/db/adapters/typeorm.cjs +1 -1
  17. package/dist/db/adapters/typeorm.js +1 -2
  18. package/dist/db/adapters.cjs +1 -1
  19. package/dist/db/migrator.cjs +1 -1
  20. package/dist/db/schema.cjs +5 -1
  21. package/dist/db/schema.js +3 -2
  22. package/dist/define-config.cjs +1 -1
  23. package/dist/edge.cjs +9 -9
  24. package/dist/edge.js +6 -885
  25. package/dist/router.cjs +239 -57
  26. package/dist/router.js +1 -2058
  27. package/dist/types/index.cjs +1 -1
  28. package/dist-types/cache/gvl-resolver.d.ts +1 -1
  29. package/dist-types/db/registry/consent-policy.d.ts +57 -1
  30. package/dist-types/db/registry/index.d.ts +43 -1
  31. package/dist-types/db/registry/runtime-policy-decision.d.ts +1 -1
  32. package/dist-types/db/registry/types.d.ts +2 -1
  33. package/dist-types/db/schema/1.0.0/consent.d.ts +2 -2
  34. package/dist-types/db/schema/2.0.0/audit-log.d.ts +2 -2
  35. package/dist-types/db/schema/2.0.0/consent-policy.d.ts +3 -2
  36. package/dist-types/db/schema/2.0.0/consent-purpose.d.ts +2 -2
  37. package/dist-types/db/schema/2.0.0/consent.d.ts +2 -2
  38. package/dist-types/db/schema/2.0.0/domain.d.ts +2 -2
  39. package/dist-types/db/schema/2.0.0/index.d.ts +7 -0
  40. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +2 -2
  41. package/dist-types/db/schema/2.0.0/subject.d.ts +2 -2
  42. package/dist-types/db/schema/index.d.ts +14 -0
  43. package/dist-types/edge/index.d.ts +2 -2
  44. package/dist-types/edge/init-handler.d.ts +5 -3
  45. package/dist-types/edge/resolve-consent.d.ts +6 -6
  46. package/dist-types/edge/types.d.ts +1 -1
  47. package/dist-types/handlers/init/index.d.ts +4 -4
  48. package/dist-types/handlers/init/policy.d.ts +1 -1
  49. package/dist-types/handlers/init/resolve-init.d.ts +2 -2
  50. package/dist-types/handlers/init/translations.d.ts +1 -1
  51. package/dist-types/handlers/legal-document/current.handler.d.ts +11 -0
  52. package/dist-types/handlers/legal-document/snapshot.d.ts +39 -0
  53. package/dist-types/handlers/policy/snapshot.d.ts +1 -1
  54. package/dist-types/handlers/subject/get.handler.d.ts +3 -0
  55. package/dist-types/handlers/subject/list.handler.d.ts +3 -0
  56. package/dist-types/handlers/utils/consent-enrichment.d.ts +3 -0
  57. package/dist-types/middleware/cors/is-origin-trusted.d.ts +1 -1
  58. package/dist-types/policies/defaults.d.ts +2 -2
  59. package/dist-types/policies/matchers.d.ts +2 -2
  60. package/dist-types/routes/index.d.ts +1 -0
  61. package/dist-types/routes/legal-document.d.ts +7 -0
  62. package/dist-types/types/index.d.ts +26 -5
  63. package/dist-types/utils/instrumentation.d.ts +2 -2
  64. package/dist-types/utils/logger.d.ts +1 -1
  65. package/dist-types/version.d.ts +1 -1
  66. package/docs/api/configuration.md +13 -2
  67. package/docs/guides/edge-deployment.md +18 -15
  68. package/docs/guides/policy-packs.md +1 -1
  69. package/package.json +19 -19
package/dist/router.cjs CHANGED
@@ -22,7 +22,7 @@ var __webpack_require__ = {};
22
22
  })();
23
23
  (()=>{
24
24
  __webpack_require__.r = (exports1)=>{
25
- if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
25
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
26
26
  value: 'Module'
27
27
  });
28
28
  Object.defineProperty(exports1, '__esModule', {
@@ -42,7 +42,7 @@ const schema_namespaceObject = require("@c15t/schema");
42
42
  const external_hono_namespaceObject = require("hono");
43
43
  const external_hono_openapi_namespaceObject = require("hono-openapi");
44
44
  const http_exception_namespaceObject = require("hono/http-exception");
45
- function extractErrorMessage(error) {
45
+ function extract_error_message_extractErrorMessage(error) {
46
46
  if (error instanceof AggregateError && error.errors?.length > 0) {
47
47
  const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
48
48
  return `AggregateError: ${inner}`;
@@ -75,7 +75,7 @@ function getDefaultAttributes() {
75
75
  const handleSpanError = (span, error)=>{
76
76
  span.setStatus({
77
77
  code: api_namespaceObject.SpanStatusCode.ERROR,
78
- message: extractErrorMessage(error)
78
+ message: extract_error_message_extractErrorMessage(error)
79
79
  });
80
80
  if (error instanceof Error) span.setAttribute('error.type', error.name);
81
81
  };
@@ -244,7 +244,7 @@ function createMetrics(meter) {
244
244
  };
245
245
  }
246
246
  let metricsInstance = null;
247
- function getMetrics(options) {
247
+ function metrics_getMetrics(options) {
248
248
  if (metricsInstance) return metricsInstance;
249
249
  if (!create_telemetry_options_isTelemetryEnabled(options)) return null;
250
250
  metricsInstance = createMetrics(getMeter(options));
@@ -270,7 +270,7 @@ async function batchLoadPolicies(policyIds, ctx) {
270
270
  for (const p of policyMap.values())uniqueTypes.add(p.type);
271
271
  const latestPolicyByType = new Map();
272
272
  for (const type of uniqueTypes){
273
- const latest = await registry.findOrCreatePolicy(type);
273
+ const latest = await registry.findLatestPolicyByType(type);
274
274
  if (latest) latestPolicyByType.set(type, latest.id);
275
275
  }
276
276
  return {
@@ -296,11 +296,17 @@ async function enrichConsents(consents, ctx) {
296
296
  }
297
297
  return consents.map((consent)=>{
298
298
  let policyType = 'unknown';
299
+ let policyVersion;
300
+ let policyHash;
301
+ let policyEffectiveDate;
299
302
  let isLatestPolicy = false;
300
303
  if (consent.policyId) {
301
304
  const policy = policyMap.get(consent.policyId);
302
305
  if (policy) {
303
306
  policyType = policy.type;
307
+ policyVersion = policy.version;
308
+ policyHash = policy.hash ?? void 0;
309
+ policyEffectiveDate = policy.effectiveDate;
304
310
  isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
305
311
  }
306
312
  }
@@ -317,6 +323,9 @@ async function enrichConsents(consents, ctx) {
317
323
  id: consent.id,
318
324
  type: policyType,
319
325
  policyId: consent.policyId ?? void 0,
326
+ policyVersion,
327
+ policyHash,
328
+ policyEffectiveDate,
320
329
  isLatestPolicy,
321
330
  preferences,
322
331
  givenAt: consent.givenAt
@@ -408,14 +417,14 @@ const checkConsentHandler = async (c)=>{
408
417
  externalId,
409
418
  results
410
419
  });
411
- const metrics = getMetrics();
420
+ const metrics = metrics_getMetrics();
412
421
  if (metrics) for (const [type, result] of Object.entries(results))metrics.recordConsentCheck(type, result.hasConsent);
413
422
  return c.json({
414
423
  results
415
424
  });
416
425
  } catch (error) {
417
426
  logger.error('Error in GET /consents/check handler', {
418
- error: extractErrorMessage(error),
427
+ error: extract_error_message_extractErrorMessage(error),
419
428
  errorType: error instanceof Error ? error.constructor.name : typeof error
420
429
  });
421
430
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
@@ -431,11 +440,7 @@ const createConsentRoutes = ()=>{
431
440
  const app = new external_hono_namespaceObject.Hono();
432
441
  app.get('/check', (0, external_hono_openapi_namespaceObject.describeRoute)({
433
442
  summary: 'Check consent by external user ID',
434
- description: `Pre-banner cross-device consent check. Use to avoid showing the banner when the user has already consented on another device.
435
-
436
- **Query parameters:**
437
- - \`externalId\` – External user ID to check
438
- - \`type\` – Consent type(s) to check (comma-separated)`,
443
+ description: "Pre-banner cross-device consent check. Use to avoid showing the banner when the user has already consented on another device.\n\n**Query parameters:**\n- `externalId` – External user ID to check\n- `type` – Consent type(s) to check (comma-separated)",
439
444
  tags: [
440
445
  'Consent'
441
446
  ],
@@ -596,14 +601,14 @@ async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT
596
601
  if (!parsed.vendorListVersion || !parsed.purposes || !parsed.vendors) throw new Error('Invalid GVL response: missing required fields');
597
602
  return parsed;
598
603
  });
599
- getMetrics()?.recordGvlFetch({
604
+ metrics_getMetrics()?.recordGvlFetch({
600
605
  language,
601
606
  source: 'fetch',
602
607
  status: 200
603
608
  }, Date.now() - fetchStart);
604
609
  return gvl;
605
610
  } catch (error) {
606
- getMetrics()?.recordGvlError({
611
+ metrics_getMetrics()?.recordGvlError({
607
612
  language,
608
613
  errorType: error instanceof Error ? error.name : 'UnknownError'
609
614
  });
@@ -624,18 +629,18 @@ function createGVLResolver(options) {
624
629
  if (bundled?.[language]) return bundled[language];
625
630
  const memoryHit = await withCacheSpan('get', 'memory', ()=>memoryCache.get(cacheKey));
626
631
  if (memoryHit) {
627
- getMetrics()?.recordCacheHit('memory');
632
+ metrics_getMetrics()?.recordCacheHit('memory');
628
633
  return memoryHit;
629
634
  }
630
- getMetrics()?.recordCacheMiss('memory');
635
+ metrics_getMetrics()?.recordCacheMiss('memory');
631
636
  if (cacheAdapter) {
632
637
  const externalHit = await withCacheSpan('get', 'external', ()=>cacheAdapter.get(cacheKey));
633
638
  if (externalHit) {
634
- getMetrics()?.recordCacheHit('external');
639
+ metrics_getMetrics()?.recordCacheHit('external');
635
640
  await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, externalHit, 300000));
636
641
  return externalHit;
637
642
  }
638
- getMetrics()?.recordCacheMiss('external');
643
+ metrics_getMetrics()?.recordCacheMiss('external');
639
644
  }
640
645
  const gvl = await fetchGVLWithLanguage(language, vendorIds, endpoint);
641
646
  if (gvl) {
@@ -1129,7 +1134,7 @@ async function resolveInitPayload(request, options, logger) {
1129
1134
  proofConfig: policyDecision.policy.proof
1130
1135
  }) : void 0;
1131
1136
  const gpc = '1' === request.headers.get('sec-gpc');
1132
- getMetrics()?.recordInit({
1137
+ metrics_getMetrics()?.recordInit({
1133
1138
  jurisdiction,
1134
1139
  country: location?.countryCode ?? void 0,
1135
1140
  region: location?.regionCode ?? void 0,
@@ -1198,7 +1203,16 @@ Use for geo-targeted consent banners and regional compliance.`,
1198
1203
  });
1199
1204
  return app;
1200
1205
  };
1201
- const version_version = '2.0.0-rc.5';
1206
+ const external_base_x_namespaceObject = require("base-x");
1207
+ var external_base_x_default = /*#__PURE__*/ __webpack_require__.n(external_base_x_namespaceObject);
1208
+ external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
1209
+ class consent_policy_LegalDocumentPolicyConflictError extends Error {
1210
+ constructor(message){
1211
+ super(message);
1212
+ this.name = 'LegalDocumentPolicyConflictError';
1213
+ }
1214
+ }
1215
+ const version_version = '2.0.0-rc.8';
1202
1216
  function getHeaders(headers) {
1203
1217
  if (!headers) return {
1204
1218
  countryCode: null,
@@ -1285,6 +1299,12 @@ const getSubjectHandler = async (c)=>{
1285
1299
  const subjectId = c.req.param('id');
1286
1300
  const type = c.req.query('type');
1287
1301
  const typeFilter = type?.split(',').map((t)=>t.trim()) || [];
1302
+ if (!subjectId) throw new http_exception_namespaceObject.HTTPException(400, {
1303
+ message: 'Subject ID is required',
1304
+ cause: {
1305
+ code: 'SUBJECT_ID_REQUIRED'
1306
+ }
1307
+ });
1288
1308
  logger.debug('Request parameters', {
1289
1309
  subjectId,
1290
1310
  typeFilter
@@ -1320,7 +1340,7 @@ const getSubjectHandler = async (c)=>{
1320
1340
  });
1321
1341
  } catch (error) {
1322
1342
  logger.error('Error in GET /subjects/:id handler', {
1323
- error: extractErrorMessage(error),
1343
+ error: extract_error_message_extractErrorMessage(error),
1324
1344
  errorType: error instanceof Error ? error.constructor.name : typeof error
1325
1345
  });
1326
1346
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
@@ -1381,7 +1401,7 @@ const listSubjectsHandler = async (c)=>{
1381
1401
  });
1382
1402
  } catch (error) {
1383
1403
  logger.error('Error in GET /subjects handler', {
1384
- error: extractErrorMessage(error),
1404
+ error: extract_error_message_extractErrorMessage(error),
1385
1405
  errorType: error instanceof Error ? error.constructor.name : typeof error
1386
1406
  });
1387
1407
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
@@ -1393,9 +1413,7 @@ const listSubjectsHandler = async (c)=>{
1393
1413
  });
1394
1414
  }
1395
1415
  };
1396
- const external_base_x_namespaceObject = require("base-x");
1397
- var external_base_x_default = /*#__PURE__*/ __webpack_require__.n(external_base_x_namespaceObject);
1398
- const prefixes = {
1416
+ const utils_prefixes = {
1399
1417
  auditLog: 'log',
1400
1418
  consent: 'cns',
1401
1419
  consentPolicy: 'pol',
@@ -1403,10 +1421,10 @@ const prefixes = {
1403
1421
  domain: 'dom',
1404
1422
  subject: 'sub'
1405
1423
  };
1406
- const b58 = external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
1407
- function generateId(model) {
1424
+ const utils_b58 = external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
1425
+ function utils_generateId(model) {
1408
1426
  const buf = crypto.getRandomValues(new Uint8Array(20));
1409
- const prefix = prefixes[model];
1427
+ const prefix = utils_prefixes[model];
1410
1428
  const EPOCH_TIMESTAMP = 1700000000000;
1411
1429
  const t = Date.now() - EPOCH_TIMESTAMP;
1412
1430
  const high = Math.floor(t / 0x100000000);
@@ -1419,9 +1437,9 @@ function generateId(model) {
1419
1437
  buf[5] = low >>> 16 & 255;
1420
1438
  buf[6] = low >>> 8 & 255;
1421
1439
  buf[7] = 255 & low;
1422
- return `${prefix}_${b58.encode(buf)}`;
1440
+ return `${prefix}_${utils_b58.encode(buf)}`;
1423
1441
  }
1424
- async function generateUniqueId(db, model, ctx, options = {}) {
1442
+ async function utils_generateUniqueId(db, model, ctx, options = {}) {
1425
1443
  const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
1426
1444
  if (attempt >= maxRetries) {
1427
1445
  const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
@@ -1431,7 +1449,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
1431
1449
  });
1432
1450
  throw error;
1433
1451
  }
1434
- const id = generateId(model);
1452
+ const id = utils_generateId(model);
1435
1453
  try {
1436
1454
  const existing = await db.findFirst(model, {
1437
1455
  where: (b)=>b('id', '=', id)
@@ -1445,7 +1463,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
1445
1463
  });
1446
1464
  const delay = Math.min(baseDelay * 2 ** attempt, 1000);
1447
1465
  await new Promise((resolve)=>setTimeout(resolve, delay));
1448
- return generateUniqueId(db, model, ctx, {
1466
+ return utils_generateUniqueId(db, model, ctx, {
1449
1467
  maxRetries,
1450
1468
  attempt: attempt + 1,
1451
1469
  baseDelay
@@ -1461,7 +1479,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
1461
1479
  if (attempt < maxRetries - 1) {
1462
1480
  const delay = Math.min(baseDelay * 2 ** attempt, 2000);
1463
1481
  await new Promise((resolve)=>setTimeout(resolve, delay));
1464
- return generateUniqueId(db, model, ctx, {
1482
+ return utils_generateUniqueId(db, model, ctx, {
1465
1483
  maxRetries,
1466
1484
  attempt: attempt + 1,
1467
1485
  baseDelay
@@ -1478,6 +1496,12 @@ const patchSubjectHandler = async (c)=>{
1478
1496
  const subjectId = c.req.param('id');
1479
1497
  const body = await c.req.json();
1480
1498
  const { externalId, identityProvider = 'external' } = body;
1499
+ if (!subjectId) throw new http_exception_namespaceObject.HTTPException(400, {
1500
+ message: 'Subject ID is required',
1501
+ cause: {
1502
+ code: 'SUBJECT_ID_REQUIRED'
1503
+ }
1504
+ });
1481
1505
  logger.debug('Request parameters', {
1482
1506
  subjectId,
1483
1507
  externalId,
@@ -1504,7 +1528,7 @@ const patchSubjectHandler = async (c)=>{
1504
1528
  }
1505
1529
  });
1506
1530
  await tx.create('auditLog', {
1507
- id: await generateUniqueId(tx, 'auditLog', ctx),
1531
+ id: await utils_generateUniqueId(tx, 'auditLog', ctx),
1508
1532
  subjectId,
1509
1533
  entityType: 'subject',
1510
1534
  entityId: subjectId,
@@ -1532,7 +1556,7 @@ const patchSubjectHandler = async (c)=>{
1532
1556
  externalId,
1533
1557
  identityProvider
1534
1558
  });
1535
- getMetrics()?.recordSubjectLinked(identityProvider);
1559
+ metrics_getMetrics()?.recordSubjectLinked(identityProvider);
1536
1560
  return c.json({
1537
1561
  success: true,
1538
1562
  subject: {
@@ -1542,7 +1566,7 @@ const patchSubjectHandler = async (c)=>{
1542
1566
  });
1543
1567
  } catch (error) {
1544
1568
  logger.error('Error in PATCH /subjects/:id handler', {
1545
- error: extractErrorMessage(error),
1569
+ error: extract_error_message_extractErrorMessage(error),
1546
1570
  errorType: error instanceof Error ? error.constructor.name : typeof error
1547
1571
  });
1548
1572
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
@@ -1554,6 +1578,79 @@ const patchSubjectHandler = async (c)=>{
1554
1578
  });
1555
1579
  }
1556
1580
  };
1581
+ const DEFAULT_ISSUER = 'c15t';
1582
+ const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
1583
+ function isLegalDocumentPolicyType(type) {
1584
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1585
+ }
1586
+ function snapshot_resolveSnapshotIssuer(options) {
1587
+ return options?.issuer?.trim() || DEFAULT_ISSUER;
1588
+ }
1589
+ function snapshot_resolveSnapshotAudience(params) {
1590
+ const configuredAudience = params.options?.audience?.trim();
1591
+ if (configuredAudience) return configuredAudience;
1592
+ return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
1593
+ }
1594
+ function snapshot_getSigningKey(secret) {
1595
+ return new TextEncoder().encode(secret);
1596
+ }
1597
+ function isLegalDocumentSnapshotPayload(payload) {
1598
+ return 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub && isLegalDocumentPolicyType(payload.type) && 'string' == typeof payload.version && 'string' == typeof payload.hash && 'string' == typeof payload.effectiveDate && 'number' == typeof payload.iat && 'number' == typeof payload.exp;
1599
+ }
1600
+ async function verifyLegalDocumentSnapshotToken(params) {
1601
+ const { token, options, tenantId } = params;
1602
+ if (!options?.signingKey) return {
1603
+ valid: false,
1604
+ reason: 'missing'
1605
+ };
1606
+ if (!token) return {
1607
+ valid: false,
1608
+ reason: 'missing'
1609
+ };
1610
+ if (3 !== token.split('.').length) return {
1611
+ valid: false,
1612
+ reason: 'malformed'
1613
+ };
1614
+ try {
1615
+ const { payload, protectedHeader } = await (0, external_jose_namespaceObject.jwtVerify)(token, snapshot_getSigningKey(options.signingKey), {
1616
+ issuer: snapshot_resolveSnapshotIssuer(options),
1617
+ audience: snapshot_resolveSnapshotAudience({
1618
+ options,
1619
+ tenantId
1620
+ })
1621
+ });
1622
+ const header = protectedHeader;
1623
+ if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
1624
+ valid: false,
1625
+ reason: 'invalid'
1626
+ };
1627
+ if (!isLegalDocumentSnapshotPayload(payload)) return {
1628
+ valid: false,
1629
+ reason: 'invalid'
1630
+ };
1631
+ if (payload.sub !== payload.hash) return {
1632
+ valid: false,
1633
+ reason: 'invalid'
1634
+ };
1635
+ if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
1636
+ valid: false,
1637
+ reason: 'invalid'
1638
+ };
1639
+ return {
1640
+ valid: true,
1641
+ payload
1642
+ };
1643
+ } catch (error) {
1644
+ if (error instanceof external_jose_namespaceObject.errors.JWTExpired) return {
1645
+ valid: false,
1646
+ reason: 'expired'
1647
+ };
1648
+ return {
1649
+ valid: false,
1650
+ reason: 'invalid'
1651
+ };
1652
+ }
1653
+ }
1557
1654
  function buildRuntimeDecisionDedupeKey(input) {
1558
1655
  return [
1559
1656
  input.tenantId ?? 'default',
@@ -1633,6 +1730,9 @@ function parseLanguageFromHeader(header) {
1633
1730
  if (!firstLanguage) return;
1634
1731
  return firstLanguage.split('-')[0]?.toLowerCase();
1635
1732
  }
1733
+ function isLegalDocumentType(type) {
1734
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1735
+ }
1636
1736
  function resolveSnapshotFailureMode(ctx) {
1637
1737
  return ctx.policySnapshot?.onValidationFailure ?? 'reject';
1638
1738
  }
@@ -1667,6 +1767,45 @@ function buildSnapshotHttpException(reason) {
1667
1767
  }
1668
1768
  }
1669
1769
  }
1770
+ function buildLegalDocumentSnapshotHttpException(reason) {
1771
+ switch(reason){
1772
+ case 'missing':
1773
+ return new http_exception_namespaceObject.HTTPException(409, {
1774
+ message: 'Legal document snapshot token is required',
1775
+ cause: {
1776
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
1777
+ }
1778
+ });
1779
+ case 'expired':
1780
+ return new http_exception_namespaceObject.HTTPException(409, {
1781
+ message: 'Legal document snapshot token has expired',
1782
+ cause: {
1783
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
1784
+ }
1785
+ });
1786
+ case 'malformed':
1787
+ case 'invalid':
1788
+ return new http_exception_namespaceObject.HTTPException(409, {
1789
+ message: 'Legal document snapshot token is invalid',
1790
+ cause: {
1791
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
1792
+ }
1793
+ });
1794
+ default:
1795
+ {
1796
+ const _exhaustive = reason;
1797
+ throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
1798
+ }
1799
+ }
1800
+ }
1801
+ function buildLegalDocumentProofHttpException(message) {
1802
+ return new http_exception_namespaceObject.HTTPException(409, {
1803
+ message,
1804
+ cause: {
1805
+ code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
1806
+ }
1807
+ });
1808
+ }
1670
1809
  const postSubjectHandler = async (c)=>{
1671
1810
  const ctx = c.get('c15tContext');
1672
1811
  const logger = ctx.logger;
@@ -1692,16 +1831,30 @@ const postSubjectHandler = async (c)=>{
1692
1831
  const requestLanguage = parseLanguageFromHeader(acceptLanguage);
1693
1832
  const location = await getLocation(request, ctx);
1694
1833
  const resolvedJurisdiction = getJurisdiction(location, ctx);
1695
- const snapshotVerification = await verifyPolicySnapshotToken({
1834
+ const legalDocumentConsent = isLegalDocumentType(type);
1835
+ const runtimeSnapshotVerification = legalDocumentConsent ? {
1836
+ valid: false,
1837
+ reason: 'missing'
1838
+ } : await verifyPolicySnapshotToken({
1696
1839
  token: input.policySnapshotToken,
1697
1840
  options: ctx.policySnapshot,
1698
1841
  tenantId: ctx.tenantId
1699
1842
  });
1700
- const hasValidSnapshot = snapshotVerification.valid;
1701
- const snapshotPayload = snapshotVerification.valid ? snapshotVerification.payload : null;
1702
- const shouldRequireSnapshot = !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
1703
- if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(snapshotVerification.reason);
1704
- const resolvedPolicyDecision = hasValidSnapshot ? void 0 : await resolvePolicyDecision({
1843
+ const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
1844
+ token: input.documentSnapshotToken,
1845
+ options: ctx.legalDocumentSnapshot,
1846
+ tenantId: ctx.tenantId
1847
+ }) : {
1848
+ valid: false,
1849
+ reason: 'missing'
1850
+ };
1851
+ const hasValidSnapshot = runtimeSnapshotVerification.valid;
1852
+ const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
1853
+ const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
1854
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
1855
+ const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
1856
+ if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
1857
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await resolvePolicyDecision({
1705
1858
  policies: ctx.policyPacks,
1706
1859
  countryCode: location.countryCode,
1707
1860
  regionCode: location.regionCode,
@@ -1758,7 +1911,39 @@ const postSubjectHandler = async (c)=>{
1758
1911
  let purposeIds = [];
1759
1912
  let appliedPreferences;
1760
1913
  const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
1761
- if (inputPolicyId) {
1914
+ if (legalDocumentConsent && legalDocumentSnapshotVerification.valid) {
1915
+ if (legalDocumentSnapshotVerification.payload.type !== type) throw buildLegalDocumentSnapshotHttpException('invalid');
1916
+ const effectiveDate = new Date(legalDocumentSnapshotVerification.payload.effectiveDate);
1917
+ if (Number.isNaN(effectiveDate.getTime())) throw buildLegalDocumentSnapshotHttpException('invalid');
1918
+ const documentPolicy = await registry.findOrCreateLegalDocumentPolicy({
1919
+ type,
1920
+ version: legalDocumentSnapshotVerification.payload.version,
1921
+ hash: legalDocumentSnapshotVerification.payload.hash,
1922
+ effectiveDate
1923
+ });
1924
+ policyId = documentPolicy.id;
1925
+ } else if (legalDocumentConsent) {
1926
+ if (!ctx.legalDocumentSnapshot?.signingKey && !inputPolicyId) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId when snapshot verification is disabled');
1927
+ if (!inputPolicyId) throw buildLegalDocumentProofHttpException('Legal document consent requires a valid document snapshot token or policyId');
1928
+ policyId = inputPolicyId;
1929
+ const policy = await registry.findConsentPolicyById(inputPolicyId);
1930
+ if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
1931
+ message: 'Policy not found',
1932
+ cause: {
1933
+ code: 'POLICY_NOT_FOUND',
1934
+ policyId,
1935
+ type
1936
+ }
1937
+ });
1938
+ if (!policy.isActive) throw new http_exception_namespaceObject.HTTPException(400, {
1939
+ message: 'Policy is inactive',
1940
+ cause: {
1941
+ code: 'POLICY_INACTIVE',
1942
+ policyId,
1943
+ type
1944
+ }
1945
+ });
1946
+ } else if (inputPolicyId) {
1762
1947
  policyId = inputPolicyId;
1763
1948
  const policy = await registry.findConsentPolicyById(inputPolicyId);
1764
1949
  if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
@@ -1912,7 +2097,7 @@ const postSubjectHandler = async (c)=>{
1912
2097
  where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1913
2098
  })) : void 0;
1914
2099
  const consentRecord = await tx.create('consent', {
1915
- id: await generateUniqueId(tx, 'consent', ctx),
2100
+ id: await utils_generateUniqueId(tx, 'consent', ctx),
1916
2101
  subjectId: subject.id,
1917
2102
  domainId: domainRecord.id,
1918
2103
  policyId,
@@ -1949,7 +2134,7 @@ const postSubjectHandler = async (c)=>{
1949
2134
  consent: consentRecord
1950
2135
  };
1951
2136
  });
1952
- const metrics = getMetrics();
2137
+ const metrics = metrics_getMetrics();
1953
2138
  if (metrics) {
1954
2139
  const jurisdiction = effectiveJurisdiction;
1955
2140
  metrics.recordConsentCreated({
@@ -1979,10 +2164,16 @@ const postSubjectHandler = async (c)=>{
1979
2164
  });
1980
2165
  } catch (error) {
1981
2166
  logger.error('Error in POST /subjects handler', {
1982
- error: extractErrorMessage(error),
2167
+ error: extract_error_message_extractErrorMessage(error),
1983
2168
  errorType: error instanceof Error ? error.constructor.name : typeof error
1984
2169
  });
1985
2170
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
2171
+ if (error instanceof consent_policy_LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
2172
+ message: error.message,
2173
+ cause: {
2174
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
2175
+ }
2176
+ });
1986
2177
  throw new http_exception_namespaceObject.HTTPException(500, {
1987
2178
  message: 'Internal server error',
1988
2179
  cause: {
@@ -1995,11 +2186,7 @@ const createSubjectRoutes = ()=>{
1995
2186
  const app = new external_hono_namespaceObject.Hono();
1996
2187
  app.get('/:id', (0, external_hono_openapi_namespaceObject.describeRoute)({
1997
2188
  summary: 'Get subject consent status',
1998
- description: `Returns the subject's consent status for this device. Use to check if the subject has valid consent for given policy types.
1999
-
2000
- **Query:** \`type\` – Filter by consent type(s), comma-separated (e.g. \`privacy_policy,cookie_banner\`).
2001
-
2002
- **Response:** \`subject\`, \`consents\` (matching filter), \`isValid\` (valid consent for requested type(s)).`,
2189
+ description: "Returns the subject's consent status for this device. Use to check if the subject has valid consent for given policy types.\n\n**Query:** `type` – Filter by consent type(s), comma-separated (e.g. `privacy_policy,cookie_banner`).\n\n**Response:** `subject`, `consents` (matching filter), `isValid` (valid consent for requested type(s)).",
2003
2190
  tags: [
2004
2191
  'Subject',
2005
2192
  'Consent'
@@ -2020,12 +2207,7 @@ const createSubjectRoutes = ()=>{
2020
2207
  }), (0, external_hono_openapi_namespaceObject.validator)('param', schema_namespaceObject.getSubjectInputSchema), getSubjectHandler);
2021
2208
  app.post('/', (0, external_hono_openapi_namespaceObject.describeRoute)({
2022
2209
  summary: 'Record consent for a subject',
2023
- description: `Creates a new consent record (append-only). Creates the subject if it does not exist.
2024
-
2025
- **Request body by \`type\`:**
2026
- - \`cookie_banner\` – Requires \`preferences\` object
2027
- - \`privacy_policy\`, \`dpa\`, \`terms_and_conditions\` – Optional \`policyId\`
2028
- - \`marketing_communications\`, \`age_verification\`, \`other\` – Optional \`preferences\``,
2210
+ description: "Creates a new consent record (append-only). Creates the subject if it does not exist.\n\n**Request body by `type`:**\n- `cookie_banner` – Requires `preferences` object\n- `privacy_policy`, `dpa`, `terms_and_conditions` – Requires a valid `documentSnapshotToken` when legal-document verification is enabled, otherwise an explicit `policyId`\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
2029
2211
  tags: [
2030
2212
  'Subject',
2031
2213
  'Consent'