@c15t/backend 2.0.0-rc.6 → 2.0.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 (54) hide show
  1. package/README.md +3 -3
  2. package/dist/302.js +2 -3
  3. package/dist/{364.js → 915.js} +656 -25
  4. package/dist/core.cjs +497 -15
  5. package/dist/core.js +8 -156
  6. package/dist/db/schema.cjs +4 -0
  7. package/dist/db/schema.js +3 -2
  8. package/dist/edge.cjs +8 -8
  9. package/dist/edge.js +3 -3
  10. package/dist/router.cjs +253 -42
  11. package/dist/router.js +1 -1
  12. package/dist-types/cache/gvl-resolver.d.ts +1 -1
  13. package/dist-types/db/registry/consent-policy.d.ts +57 -1
  14. package/dist-types/db/registry/index.d.ts +43 -1
  15. package/dist-types/db/registry/types.d.ts +2 -1
  16. package/dist-types/db/schema/1.0.0/consent.d.ts +1 -1
  17. package/dist-types/db/schema/2.0.0/audit-log.d.ts +1 -1
  18. package/dist-types/db/schema/2.0.0/consent-policy.d.ts +3 -2
  19. package/dist-types/db/schema/2.0.0/consent-purpose.d.ts +1 -1
  20. package/dist-types/db/schema/2.0.0/consent.d.ts +1 -1
  21. package/dist-types/db/schema/2.0.0/domain.d.ts +1 -1
  22. package/dist-types/db/schema/2.0.0/index.d.ts +7 -0
  23. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +1 -1
  24. package/dist-types/db/schema/2.0.0/subject.d.ts +1 -1
  25. package/dist-types/db/schema/index.d.ts +14 -0
  26. package/dist-types/edge/index.d.ts +2 -2
  27. package/dist-types/edge/init-handler.d.ts +5 -3
  28. package/dist-types/edge/resolve-consent.d.ts +6 -6
  29. package/dist-types/edge/types.d.ts +1 -1
  30. package/dist-types/handlers/init/index.d.ts +4 -4
  31. package/dist-types/handlers/init/policy.d.ts +1 -1
  32. package/dist-types/handlers/init/resolve-init.d.ts +2 -2
  33. package/dist-types/handlers/init/translations.d.ts +1 -1
  34. package/dist-types/handlers/legal-document/current.handler.d.ts +11 -0
  35. package/dist-types/handlers/legal-document/snapshot.d.ts +39 -0
  36. package/dist-types/handlers/subject/get.handler.d.ts +3 -0
  37. package/dist-types/handlers/subject/list.handler.d.ts +3 -0
  38. package/dist-types/handlers/utils/consent-enrichment.d.ts +3 -0
  39. package/dist-types/middleware/cors/is-origin-trusted.d.ts +1 -1
  40. package/dist-types/policies/builder.d.ts +7 -7
  41. package/dist-types/policies/defaults.d.ts +2 -2
  42. package/dist-types/policies/matchers.d.ts +2 -2
  43. package/dist-types/routes/index.d.ts +1 -0
  44. package/dist-types/routes/legal-document.d.ts +7 -0
  45. package/dist-types/types/index.d.ts +39 -18
  46. package/dist-types/utils/instrumentation.d.ts +2 -2
  47. package/dist-types/utils/logger.d.ts +1 -1
  48. package/dist-types/version.d.ts +1 -1
  49. package/docs/api/configuration.md +24 -13
  50. package/docs/guides/database-setup.md +4 -4
  51. package/docs/guides/edge-deployment.md +18 -15
  52. package/docs/guides/iab-tcf.md +4 -4
  53. package/docs/quickstart.md +9 -9
  54. package/package.json +8 -8
package/dist/router.cjs CHANGED
@@ -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;
@@ -592,14 +601,14 @@ async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT
592
601
  if (!parsed.vendorListVersion || !parsed.purposes || !parsed.vendors) throw new Error('Invalid GVL response: missing required fields');
593
602
  return parsed;
594
603
  });
595
- getMetrics()?.recordGvlFetch({
604
+ metrics_getMetrics()?.recordGvlFetch({
596
605
  language,
597
606
  source: 'fetch',
598
607
  status: 200
599
608
  }, Date.now() - fetchStart);
600
609
  return gvl;
601
610
  } catch (error) {
602
- getMetrics()?.recordGvlError({
611
+ metrics_getMetrics()?.recordGvlError({
603
612
  language,
604
613
  errorType: error instanceof Error ? error.name : 'UnknownError'
605
614
  });
@@ -620,18 +629,18 @@ function createGVLResolver(options) {
620
629
  if (bundled?.[language]) return bundled[language];
621
630
  const memoryHit = await withCacheSpan('get', 'memory', ()=>memoryCache.get(cacheKey));
622
631
  if (memoryHit) {
623
- getMetrics()?.recordCacheHit('memory');
632
+ metrics_getMetrics()?.recordCacheHit('memory');
624
633
  return memoryHit;
625
634
  }
626
- getMetrics()?.recordCacheMiss('memory');
635
+ metrics_getMetrics()?.recordCacheMiss('memory');
627
636
  if (cacheAdapter) {
628
637
  const externalHit = await withCacheSpan('get', 'external', ()=>cacheAdapter.get(cacheKey));
629
638
  if (externalHit) {
630
- getMetrics()?.recordCacheHit('external');
639
+ metrics_getMetrics()?.recordCacheHit('external');
631
640
  await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, externalHit, 300000));
632
641
  return externalHit;
633
642
  }
634
- getMetrics()?.recordCacheMiss('external');
643
+ metrics_getMetrics()?.recordCacheMiss('external');
635
644
  }
636
645
  const gvl = await fetchGVLWithLanguage(language, vendorIds, endpoint);
637
646
  if (gvl) {
@@ -1125,7 +1134,7 @@ async function resolveInitPayload(request, options, logger) {
1125
1134
  proofConfig: policyDecision.policy.proof
1126
1135
  }) : void 0;
1127
1136
  const gpc = '1' === request.headers.get('sec-gpc');
1128
- getMetrics()?.recordInit({
1137
+ metrics_getMetrics()?.recordInit({
1129
1138
  jurisdiction,
1130
1139
  country: location?.countryCode ?? void 0,
1131
1140
  region: location?.regionCode ?? void 0,
@@ -1194,7 +1203,15 @@ Use for geo-targeted consent banners and regional compliance.`,
1194
1203
  });
1195
1204
  return app;
1196
1205
  };
1197
- const version_version = '2.0.0-rc.6';
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
+ }
1198
1215
  function getHeaders(headers) {
1199
1216
  if (!headers) return {
1200
1217
  countryCode: null,
@@ -1229,7 +1246,7 @@ const statusHandler = async (c)=>{
1229
1246
  try {
1230
1247
  await ctx.db.findFirst('subject', {});
1231
1248
  return c.json({
1232
- version: version_version,
1249
+ version: "2.0.0",
1233
1250
  timestamp: new Date(),
1234
1251
  client: clientInfo
1235
1252
  });
@@ -1322,7 +1339,7 @@ const getSubjectHandler = async (c)=>{
1322
1339
  });
1323
1340
  } catch (error) {
1324
1341
  logger.error('Error in GET /subjects/:id handler', {
1325
- error: extractErrorMessage(error),
1342
+ error: extract_error_message_extractErrorMessage(error),
1326
1343
  errorType: error instanceof Error ? error.constructor.name : typeof error
1327
1344
  });
1328
1345
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
@@ -1383,7 +1400,7 @@ const listSubjectsHandler = async (c)=>{
1383
1400
  });
1384
1401
  } catch (error) {
1385
1402
  logger.error('Error in GET /subjects handler', {
1386
- error: extractErrorMessage(error),
1403
+ error: extract_error_message_extractErrorMessage(error),
1387
1404
  errorType: error instanceof Error ? error.constructor.name : typeof error
1388
1405
  });
1389
1406
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
@@ -1395,9 +1412,7 @@ const listSubjectsHandler = async (c)=>{
1395
1412
  });
1396
1413
  }
1397
1414
  };
1398
- const external_base_x_namespaceObject = require("base-x");
1399
- var external_base_x_default = /*#__PURE__*/ __webpack_require__.n(external_base_x_namespaceObject);
1400
- const prefixes = {
1415
+ const utils_prefixes = {
1401
1416
  auditLog: 'log',
1402
1417
  consent: 'cns',
1403
1418
  consentPolicy: 'pol',
@@ -1405,10 +1420,10 @@ const prefixes = {
1405
1420
  domain: 'dom',
1406
1421
  subject: 'sub'
1407
1422
  };
1408
- const b58 = external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
1409
- function generateId(model) {
1423
+ const utils_b58 = external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
1424
+ function utils_generateId(model) {
1410
1425
  const buf = crypto.getRandomValues(new Uint8Array(20));
1411
- const prefix = prefixes[model];
1426
+ const prefix = utils_prefixes[model];
1412
1427
  const EPOCH_TIMESTAMP = 1700000000000;
1413
1428
  const t = Date.now() - EPOCH_TIMESTAMP;
1414
1429
  const high = Math.floor(t / 0x100000000);
@@ -1421,9 +1436,9 @@ function generateId(model) {
1421
1436
  buf[5] = low >>> 16 & 255;
1422
1437
  buf[6] = low >>> 8 & 255;
1423
1438
  buf[7] = 255 & low;
1424
- return `${prefix}_${b58.encode(buf)}`;
1439
+ return `${prefix}_${utils_b58.encode(buf)}`;
1425
1440
  }
1426
- async function generateUniqueId(db, model, ctx, options = {}) {
1441
+ async function utils_generateUniqueId(db, model, ctx, options = {}) {
1427
1442
  const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
1428
1443
  if (attempt >= maxRetries) {
1429
1444
  const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
@@ -1433,7 +1448,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
1433
1448
  });
1434
1449
  throw error;
1435
1450
  }
1436
- const id = generateId(model);
1451
+ const id = utils_generateId(model);
1437
1452
  try {
1438
1453
  const existing = await db.findFirst(model, {
1439
1454
  where: (b)=>b('id', '=', id)
@@ -1447,7 +1462,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
1447
1462
  });
1448
1463
  const delay = Math.min(baseDelay * 2 ** attempt, 1000);
1449
1464
  await new Promise((resolve)=>setTimeout(resolve, delay));
1450
- return generateUniqueId(db, model, ctx, {
1465
+ return utils_generateUniqueId(db, model, ctx, {
1451
1466
  maxRetries,
1452
1467
  attempt: attempt + 1,
1453
1468
  baseDelay
@@ -1463,7 +1478,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
1463
1478
  if (attempt < maxRetries - 1) {
1464
1479
  const delay = Math.min(baseDelay * 2 ** attempt, 2000);
1465
1480
  await new Promise((resolve)=>setTimeout(resolve, delay));
1466
- return generateUniqueId(db, model, ctx, {
1481
+ return utils_generateUniqueId(db, model, ctx, {
1467
1482
  maxRetries,
1468
1483
  attempt: attempt + 1,
1469
1484
  baseDelay
@@ -1512,7 +1527,7 @@ const patchSubjectHandler = async (c)=>{
1512
1527
  }
1513
1528
  });
1514
1529
  await tx.create('auditLog', {
1515
- id: await generateUniqueId(tx, 'auditLog', ctx),
1530
+ id: await utils_generateUniqueId(tx, 'auditLog', ctx),
1516
1531
  subjectId,
1517
1532
  entityType: 'subject',
1518
1533
  entityId: subjectId,
@@ -1540,7 +1555,7 @@ const patchSubjectHandler = async (c)=>{
1540
1555
  externalId,
1541
1556
  identityProvider
1542
1557
  });
1543
- getMetrics()?.recordSubjectLinked(identityProvider);
1558
+ metrics_getMetrics()?.recordSubjectLinked(identityProvider);
1544
1559
  return c.json({
1545
1560
  success: true,
1546
1561
  subject: {
@@ -1550,7 +1565,7 @@ const patchSubjectHandler = async (c)=>{
1550
1565
  });
1551
1566
  } catch (error) {
1552
1567
  logger.error('Error in PATCH /subjects/:id handler', {
1553
- error: extractErrorMessage(error),
1568
+ error: extract_error_message_extractErrorMessage(error),
1554
1569
  errorType: error instanceof Error ? error.constructor.name : typeof error
1555
1570
  });
1556
1571
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
@@ -1562,6 +1577,79 @@ const patchSubjectHandler = async (c)=>{
1562
1577
  });
1563
1578
  }
1564
1579
  };
1580
+ const DEFAULT_ISSUER = 'c15t';
1581
+ const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
1582
+ function isLegalDocumentPolicyType(type) {
1583
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1584
+ }
1585
+ function snapshot_resolveSnapshotIssuer(options) {
1586
+ return options?.issuer?.trim() || DEFAULT_ISSUER;
1587
+ }
1588
+ function snapshot_resolveSnapshotAudience(params) {
1589
+ const configuredAudience = params.options?.audience?.trim();
1590
+ if (configuredAudience) return configuredAudience;
1591
+ return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
1592
+ }
1593
+ function snapshot_getSigningKey(secret) {
1594
+ return new TextEncoder().encode(secret);
1595
+ }
1596
+ function isLegalDocumentSnapshotPayload(payload) {
1597
+ 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;
1598
+ }
1599
+ async function verifyLegalDocumentSnapshotToken(params) {
1600
+ const { token, options, tenantId } = params;
1601
+ if (!options?.signingKey) return {
1602
+ valid: false,
1603
+ reason: 'missing'
1604
+ };
1605
+ if (!token) return {
1606
+ valid: false,
1607
+ reason: 'missing'
1608
+ };
1609
+ if (3 !== token.split('.').length) return {
1610
+ valid: false,
1611
+ reason: 'malformed'
1612
+ };
1613
+ try {
1614
+ const { payload, protectedHeader } = await (0, external_jose_namespaceObject.jwtVerify)(token, snapshot_getSigningKey(options.signingKey), {
1615
+ issuer: snapshot_resolveSnapshotIssuer(options),
1616
+ audience: snapshot_resolveSnapshotAudience({
1617
+ options,
1618
+ tenantId
1619
+ })
1620
+ });
1621
+ const header = protectedHeader;
1622
+ if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
1623
+ valid: false,
1624
+ reason: 'invalid'
1625
+ };
1626
+ if (!isLegalDocumentSnapshotPayload(payload)) return {
1627
+ valid: false,
1628
+ reason: 'invalid'
1629
+ };
1630
+ if (payload.sub !== payload.hash) return {
1631
+ valid: false,
1632
+ reason: 'invalid'
1633
+ };
1634
+ if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
1635
+ valid: false,
1636
+ reason: 'invalid'
1637
+ };
1638
+ return {
1639
+ valid: true,
1640
+ payload
1641
+ };
1642
+ } catch (error) {
1643
+ if (error instanceof external_jose_namespaceObject.errors.JWTExpired) return {
1644
+ valid: false,
1645
+ reason: 'expired'
1646
+ };
1647
+ return {
1648
+ valid: false,
1649
+ reason: 'invalid'
1650
+ };
1651
+ }
1652
+ }
1565
1653
  function buildRuntimeDecisionDedupeKey(input) {
1566
1654
  return [
1567
1655
  input.tenantId ?? 'default',
@@ -1641,6 +1729,9 @@ function parseLanguageFromHeader(header) {
1641
1729
  if (!firstLanguage) return;
1642
1730
  return firstLanguage.split('-')[0]?.toLowerCase();
1643
1731
  }
1732
+ function isLegalDocumentType(type) {
1733
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1734
+ }
1644
1735
  function resolveSnapshotFailureMode(ctx) {
1645
1736
  return ctx.policySnapshot?.onValidationFailure ?? 'reject';
1646
1737
  }
@@ -1675,6 +1766,45 @@ function buildSnapshotHttpException(reason) {
1675
1766
  }
1676
1767
  }
1677
1768
  }
1769
+ function buildLegalDocumentSnapshotHttpException(reason) {
1770
+ switch(reason){
1771
+ case 'missing':
1772
+ return new http_exception_namespaceObject.HTTPException(409, {
1773
+ message: 'Legal document snapshot token is required',
1774
+ cause: {
1775
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
1776
+ }
1777
+ });
1778
+ case 'expired':
1779
+ return new http_exception_namespaceObject.HTTPException(409, {
1780
+ message: 'Legal document snapshot token has expired',
1781
+ cause: {
1782
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
1783
+ }
1784
+ });
1785
+ case 'malformed':
1786
+ case 'invalid':
1787
+ return new http_exception_namespaceObject.HTTPException(409, {
1788
+ message: 'Legal document snapshot token is invalid',
1789
+ cause: {
1790
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
1791
+ }
1792
+ });
1793
+ default:
1794
+ {
1795
+ const _exhaustive = reason;
1796
+ throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
1797
+ }
1798
+ }
1799
+ }
1800
+ function buildLegalDocumentProofHttpException(message) {
1801
+ return new http_exception_namespaceObject.HTTPException(409, {
1802
+ message,
1803
+ cause: {
1804
+ code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
1805
+ }
1806
+ });
1807
+ }
1678
1808
  const postSubjectHandler = async (c)=>{
1679
1809
  const ctx = c.get('c15tContext');
1680
1810
  const logger = ctx.logger;
@@ -1700,16 +1830,30 @@ const postSubjectHandler = async (c)=>{
1700
1830
  const requestLanguage = parseLanguageFromHeader(acceptLanguage);
1701
1831
  const location = await getLocation(request, ctx);
1702
1832
  const resolvedJurisdiction = getJurisdiction(location, ctx);
1703
- const snapshotVerification = await verifyPolicySnapshotToken({
1833
+ const legalDocumentConsent = isLegalDocumentType(type);
1834
+ const runtimeSnapshotVerification = legalDocumentConsent ? {
1835
+ valid: false,
1836
+ reason: 'missing'
1837
+ } : await verifyPolicySnapshotToken({
1704
1838
  token: input.policySnapshotToken,
1705
1839
  options: ctx.policySnapshot,
1706
1840
  tenantId: ctx.tenantId
1707
1841
  });
1708
- const hasValidSnapshot = snapshotVerification.valid;
1709
- const snapshotPayload = snapshotVerification.valid ? snapshotVerification.payload : null;
1710
- const shouldRequireSnapshot = !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
1711
- if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(snapshotVerification.reason);
1712
- const resolvedPolicyDecision = hasValidSnapshot ? void 0 : await resolvePolicyDecision({
1842
+ const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
1843
+ token: input.documentSnapshotToken,
1844
+ options: ctx.legalDocumentSnapshot,
1845
+ tenantId: ctx.tenantId
1846
+ }) : {
1847
+ valid: false,
1848
+ reason: 'missing'
1849
+ };
1850
+ const hasValidSnapshot = runtimeSnapshotVerification.valid;
1851
+ const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
1852
+ const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
1853
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
1854
+ const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
1855
+ if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
1856
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await resolvePolicyDecision({
1713
1857
  policies: ctx.policyPacks,
1714
1858
  countryCode: location.countryCode,
1715
1859
  regionCode: location.regionCode,
@@ -1766,7 +1910,61 @@ const postSubjectHandler = async (c)=>{
1766
1910
  let purposeIds = [];
1767
1911
  let appliedPreferences;
1768
1912
  const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
1769
- if (inputPolicyId) {
1913
+ const inputPolicyHash = 'policyHash' in input ? input.policyHash : void 0;
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 && !inputPolicyHash) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId or policyHash when snapshot verification is disabled');
1927
+ if (inputPolicyId) {
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 (inputPolicyHash) {
1947
+ const policy = await registry.findLegalDocumentPolicyByHash(type, inputPolicyHash);
1948
+ if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
1949
+ message: 'Policy not found',
1950
+ cause: {
1951
+ code: 'POLICY_NOT_FOUND',
1952
+ type,
1953
+ policyHash: inputPolicyHash
1954
+ }
1955
+ });
1956
+ if (!policy.isActive) throw new http_exception_namespaceObject.HTTPException(400, {
1957
+ message: 'Policy is inactive',
1958
+ cause: {
1959
+ code: 'POLICY_INACTIVE',
1960
+ policyId: policy.id,
1961
+ type,
1962
+ policyHash: inputPolicyHash
1963
+ }
1964
+ });
1965
+ policyId = policy.id;
1966
+ }
1967
+ } else if (inputPolicyId) {
1770
1968
  policyId = inputPolicyId;
1771
1969
  const policy = await registry.findConsentPolicyById(inputPolicyId);
1772
1970
  if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
@@ -1828,6 +2026,13 @@ const postSubjectHandler = async (c)=>{
1828
2026
  });
1829
2027
  purposeIds = purposes;
1830
2028
  }
2029
+ if (!policyId) throw new http_exception_namespaceObject.HTTPException(500, {
2030
+ message: 'Failed to resolve policy',
2031
+ cause: {
2032
+ code: 'POLICY_RESOLUTION_FAILED',
2033
+ type
2034
+ }
2035
+ });
1831
2036
  const expiryDays = effectivePolicy?.consent?.expiryDays;
1832
2037
  const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
1833
2038
  const proofConfig = effectivePolicy?.proof;
@@ -1920,7 +2125,7 @@ const postSubjectHandler = async (c)=>{
1920
2125
  where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1921
2126
  })) : void 0;
1922
2127
  const consentRecord = await tx.create('consent', {
1923
- id: await generateUniqueId(tx, 'consent', ctx),
2128
+ id: await utils_generateUniqueId(tx, 'consent', ctx),
1924
2129
  subjectId: subject.id,
1925
2130
  domainId: domainRecord.id,
1926
2131
  policyId,
@@ -1957,7 +2162,7 @@ const postSubjectHandler = async (c)=>{
1957
2162
  consent: consentRecord
1958
2163
  };
1959
2164
  });
1960
- const metrics = getMetrics();
2165
+ const metrics = metrics_getMetrics();
1961
2166
  if (metrics) {
1962
2167
  const jurisdiction = effectiveJurisdiction;
1963
2168
  metrics.recordConsentCreated({
@@ -1987,10 +2192,16 @@ const postSubjectHandler = async (c)=>{
1987
2192
  });
1988
2193
  } catch (error) {
1989
2194
  logger.error('Error in POST /subjects handler', {
1990
- error: extractErrorMessage(error),
2195
+ error: extract_error_message_extractErrorMessage(error),
1991
2196
  errorType: error instanceof Error ? error.constructor.name : typeof error
1992
2197
  });
1993
2198
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
2199
+ if (error instanceof consent_policy_LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
2200
+ message: error.message,
2201
+ cause: {
2202
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
2203
+ }
2204
+ });
1994
2205
  throw new http_exception_namespaceObject.HTTPException(500, {
1995
2206
  message: 'Internal server error',
1996
2207
  cause: {
@@ -2024,7 +2235,7 @@ const createSubjectRoutes = ()=>{
2024
2235
  }), (0, external_hono_openapi_namespaceObject.validator)('param', schema_namespaceObject.getSubjectInputSchema), getSubjectHandler);
2025
2236
  app.post('/', (0, external_hono_openapi_namespaceObject.describeRoute)({
2026
2237
  summary: 'Record consent for a subject',
2027
- 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` – Optional `policyId`\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
2238
+ 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` – Prefer a signed `documentSnapshotToken`; otherwise use a release `policyHash`, with `policyId` kept only for compatibility\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
2028
2239
  tags: [
2029
2240
  'Subject',
2030
2241
  'Consent'
package/dist/router.js CHANGED
@@ -1 +1 @@
1
- export { createConsentRoutes, createInitRoute, createStatusRoute, createSubjectRoutes } from "./364.js";
1
+ export { createConsentRoutes, createInitRoute, createStatusRoute, createSubjectRoutes } from "./915.js";
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @packageDocumentation
11
11
  */
12
- import type { GlobalVendorList } from '../../../schema/dist-types/types';
12
+ import type { GlobalVendorList } from '@c15t/schema/types';
13
13
  import type { CacheAdapter } from './types';
14
14
  /**
15
15
  * Options for creating a GVL resolver.
@@ -1,19 +1,75 @@
1
- import type { PolicyType } from '../schema';
1
+ import type { LegalDocumentPolicyType, PolicyType } from '../schema';
2
2
  import type { Registry } from './types';
3
+ export interface LegalDocumentPolicyInput {
4
+ type: LegalDocumentPolicyType;
5
+ version: string;
6
+ hash: string;
7
+ effectiveDate: Date;
8
+ }
9
+ export declare class LegalDocumentPolicyConflictError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ export declare function buildLegalDocumentPolicyId(input: {
13
+ tenantId?: string;
14
+ type: LegalDocumentPolicyType;
15
+ hash: string;
16
+ }): Promise<string>;
3
17
  export declare function policyRegistry({ db, ctx }: Registry): {
4
18
  findConsentPolicyById: (policyId: string) => Promise<{
5
19
  id: string;
6
20
  version: string;
7
21
  type: string;
22
+ hash: string | null;
8
23
  effectiveDate: Date;
9
24
  isActive: boolean;
10
25
  createdAt: Date;
11
26
  tenantId: string | null;
12
27
  } | null>;
28
+ findLatestPolicyByType: (type: PolicyType) => Promise<{
29
+ id: string;
30
+ version: string;
31
+ type: string;
32
+ hash: string | null;
33
+ effectiveDate: Date;
34
+ isActive: boolean;
35
+ createdAt: Date;
36
+ tenantId: string | null;
37
+ } | null>;
38
+ findLegalDocumentPolicyByHash: (type: LegalDocumentPolicyType, hash: string) => Promise<{
39
+ id: string;
40
+ version: string;
41
+ type: string;
42
+ hash: string | null;
43
+ effectiveDate: Date;
44
+ isActive: boolean;
45
+ createdAt: Date;
46
+ tenantId: string | null;
47
+ } | null>;
48
+ syncCurrentLegalDocumentPolicy: (input: LegalDocumentPolicyInput) => Promise<{
49
+ id: string;
50
+ version: string;
51
+ type: string;
52
+ hash: string | null;
53
+ effectiveDate: Date;
54
+ isActive: boolean;
55
+ createdAt: Date;
56
+ tenantId: string | null;
57
+ }>;
58
+ findOrCreateLegalDocumentPolicy: (input: LegalDocumentPolicyInput) => Promise<{
59
+ id: string;
60
+ version: string;
61
+ type: string;
62
+ hash: string | null;
63
+ effectiveDate: Date;
64
+ isActive: boolean;
65
+ createdAt: Date;
66
+ tenantId: string | null;
67
+ }>;
13
68
  findOrCreatePolicy: (type: PolicyType) => Promise<{
14
69
  id: string;
15
70
  version: string;
16
71
  type: string;
72
+ hash: string | null;
17
73
  effectiveDate: Date;
18
74
  isActive: boolean;
19
75
  createdAt: Date;