@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/core.cjs CHANGED
@@ -180,6 +180,7 @@ var __webpack_modules__ = {
180
180
  id: (0, schema_namespaceObject.idColumn)('id', 'varchar(255)'),
181
181
  version: (0, schema_namespaceObject.column)('version', 'string'),
182
182
  type: (0, schema_namespaceObject.column)('type', 'string'),
183
+ hash: (0, schema_namespaceObject.column)('hash', 'string').nullable(),
183
184
  effectiveDate: (0, schema_namespaceObject.column)('effectiveDate', 'timestamp'),
184
185
  isActive: (0, schema_namespaceObject.column)('isActive', 'bool').defaultTo$(()=>true),
185
186
  createdAt: (0, schema_namespaceObject.column)('createdAt', 'timestamp').defaultTo$('now'),
@@ -348,7 +349,7 @@ var __webpack_exports__ = {};
348
349
  (()=>{
349
350
  __webpack_require__.r(__webpack_exports__);
350
351
  __webpack_require__.d(__webpack_exports__, {
351
- version: ()=>version_version,
352
+ version: ()=>"2.0.0",
352
353
  c15tInstance: ()=>c15tInstance,
353
354
  EEA_COUNTRY_CODES: ()=>types_namespaceObject.EEA_COUNTRY_CODES,
354
355
  EU_COUNTRY_CODES: ()=>types_namespaceObject.EU_COUNTRY_CODES,
@@ -737,7 +738,6 @@ var __webpack_exports__ = {};
737
738
  language
738
739
  };
739
740
  }
740
- const version_version = '2.0.0-rc.6';
741
741
  function extractErrorMessage(error) {
742
742
  if (error instanceof AggregateError && error.errors?.length > 0) {
743
743
  const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
@@ -752,7 +752,7 @@ var __webpack_exports__ = {};
752
752
  const defaultAttributes = {
753
753
  ...telemetryConfig?.defaultAttributes || {},
754
754
  'service.name': String(appName),
755
- 'service.version': version_version
755
+ 'service.version': "2.0.0"
756
756
  };
757
757
  if (tenantId) defaultAttributes['tenant.id'] = tenantId;
758
758
  const config = {
@@ -1132,6 +1132,23 @@ var __webpack_exports__ = {};
1132
1132
  throw error;
1133
1133
  }
1134
1134
  }
1135
+ class LegalDocumentPolicyConflictError extends Error {
1136
+ constructor(message){
1137
+ super(message);
1138
+ this.name = 'LegalDocumentPolicyConflictError';
1139
+ }
1140
+ }
1141
+ async function buildLegalDocumentPolicyId(input) {
1142
+ const digest = await (0, types_namespaceObject.hashSha256Hex)([
1143
+ input.tenantId ?? 'default',
1144
+ input.type,
1145
+ input.hash
1146
+ ].join('|'));
1147
+ return `pol_${digest}`;
1148
+ }
1149
+ function hasLegalDocumentPolicyConflict(policy, input) {
1150
+ return policy.version !== input.version || policy.hash !== input.hash || policy.effectiveDate.getTime() !== input.effectiveDate.getTime();
1151
+ }
1135
1152
  function policyRegistry({ db, ctx }) {
1136
1153
  const { logger } = ctx;
1137
1154
  return {
@@ -1160,6 +1177,176 @@ var __webpack_exports__ = {};
1160
1177
  throw error;
1161
1178
  }
1162
1179
  },
1180
+ findLatestPolicyByType: async (type)=>{
1181
+ const start = Date.now();
1182
+ try {
1183
+ const result = await withDatabaseSpan({
1184
+ operation: 'findLatest',
1185
+ entity: 'consentPolicy'
1186
+ }, async ()=>db.findFirst('consentPolicy', {
1187
+ where: (b)=>b.and(b('isActive', '=', true), b('type', '=', type)),
1188
+ orderBy: [
1189
+ 'effectiveDate',
1190
+ 'desc'
1191
+ ]
1192
+ }));
1193
+ getMetrics()?.recordDbQuery({
1194
+ operation: 'findLatest',
1195
+ entity: 'consentPolicy'
1196
+ }, Date.now() - start);
1197
+ return result;
1198
+ } catch (error) {
1199
+ getMetrics()?.recordDbError({
1200
+ operation: 'findLatest',
1201
+ entity: 'consentPolicy'
1202
+ });
1203
+ throw error;
1204
+ }
1205
+ },
1206
+ findLegalDocumentPolicyByHash: async (type, hash)=>{
1207
+ const start = Date.now();
1208
+ try {
1209
+ const policyId = await buildLegalDocumentPolicyId({
1210
+ tenantId: ctx.tenantId,
1211
+ type,
1212
+ hash
1213
+ });
1214
+ const result = await withDatabaseSpan({
1215
+ operation: 'findByHash',
1216
+ entity: 'consentPolicy'
1217
+ }, async ()=>db.findFirst('consentPolicy', {
1218
+ where: (b)=>b('id', '=', policyId)
1219
+ }));
1220
+ getMetrics()?.recordDbQuery({
1221
+ operation: 'findByHash',
1222
+ entity: 'consentPolicy'
1223
+ }, Date.now() - start);
1224
+ return result;
1225
+ } catch (error) {
1226
+ getMetrics()?.recordDbError({
1227
+ operation: 'findByHash',
1228
+ entity: 'consentPolicy'
1229
+ });
1230
+ throw error;
1231
+ }
1232
+ },
1233
+ syncCurrentLegalDocumentPolicy: async (input)=>{
1234
+ const start = Date.now();
1235
+ try {
1236
+ const result = await withDatabaseSpan({
1237
+ operation: 'syncCurrent',
1238
+ entity: 'consentPolicy'
1239
+ }, async ()=>{
1240
+ const policyId = await buildLegalDocumentPolicyId({
1241
+ tenantId: ctx.tenantId,
1242
+ type: input.type,
1243
+ hash: input.hash
1244
+ });
1245
+ return db.transaction(async (tx)=>{
1246
+ const existing = await tx.findFirst('consentPolicy', {
1247
+ where: (b)=>b('id', '=', policyId)
1248
+ });
1249
+ if (existing) {
1250
+ if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
1251
+ await tx.updateMany('consentPolicy', {
1252
+ where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true), b('id', '!=', existing.id)),
1253
+ set: {
1254
+ isActive: false
1255
+ }
1256
+ });
1257
+ if (!existing.isActive) {
1258
+ await tx.updateMany('consentPolicy', {
1259
+ where: (b)=>b('id', '=', existing.id),
1260
+ set: {
1261
+ isActive: true
1262
+ }
1263
+ });
1264
+ return {
1265
+ ...existing,
1266
+ isActive: true
1267
+ };
1268
+ }
1269
+ return existing;
1270
+ }
1271
+ await tx.updateMany('consentPolicy', {
1272
+ where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true)),
1273
+ set: {
1274
+ isActive: false
1275
+ }
1276
+ });
1277
+ const policy = await tx.create('consentPolicy', {
1278
+ id: policyId,
1279
+ version: input.version,
1280
+ type: input.type,
1281
+ hash: input.hash,
1282
+ effectiveDate: input.effectiveDate,
1283
+ isActive: true
1284
+ });
1285
+ return policy;
1286
+ });
1287
+ });
1288
+ getMetrics()?.recordDbQuery({
1289
+ operation: 'syncCurrent',
1290
+ entity: 'consentPolicy'
1291
+ }, Date.now() - start);
1292
+ return result;
1293
+ } catch (error) {
1294
+ getMetrics()?.recordDbError({
1295
+ operation: 'syncCurrent',
1296
+ entity: 'consentPolicy'
1297
+ });
1298
+ throw error;
1299
+ }
1300
+ },
1301
+ findOrCreateLegalDocumentPolicy: async (input)=>{
1302
+ const start = Date.now();
1303
+ try {
1304
+ const result = await withDatabaseSpan({
1305
+ operation: 'findOrCreateLegalDocument',
1306
+ entity: 'consentPolicy'
1307
+ }, async ()=>{
1308
+ const policyId = await buildLegalDocumentPolicyId({
1309
+ tenantId: ctx.tenantId,
1310
+ type: input.type,
1311
+ hash: input.hash
1312
+ });
1313
+ const existing = await db.findFirst('consentPolicy', {
1314
+ where: (b)=>b('id', '=', policyId)
1315
+ });
1316
+ if (existing) {
1317
+ if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
1318
+ return existing;
1319
+ }
1320
+ const policy = await db.create('consentPolicy', {
1321
+ id: policyId,
1322
+ version: input.version,
1323
+ type: input.type,
1324
+ hash: input.hash,
1325
+ effectiveDate: input.effectiveDate,
1326
+ isActive: false
1327
+ }).catch(async ()=>{
1328
+ const concurrent = await db.findFirst('consentPolicy', {
1329
+ where: (b)=>b('id', '=', policyId)
1330
+ });
1331
+ if (!concurrent) throw new LegalDocumentPolicyConflictError('Failed to create legal document consent policy');
1332
+ if (hasLegalDocumentPolicyConflict(concurrent, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
1333
+ return concurrent;
1334
+ });
1335
+ return policy;
1336
+ });
1337
+ getMetrics()?.recordDbQuery({
1338
+ operation: 'findOrCreateLegalDocument',
1339
+ entity: 'consentPolicy'
1340
+ }, Date.now() - start);
1341
+ return result;
1342
+ } catch (error) {
1343
+ getMetrics()?.recordDbError({
1344
+ operation: 'findOrCreateLegalDocument',
1345
+ entity: 'consentPolicy'
1346
+ });
1347
+ throw error;
1348
+ }
1349
+ },
1163
1350
  findOrCreatePolicy: async (type)=>{
1164
1351
  const start = Date.now();
1165
1352
  try {
@@ -1584,7 +1771,8 @@ var __webpack_exports__ = {};
1584
1771
  registry: createRegistry({
1585
1772
  db: orm,
1586
1773
  ctx: {
1587
- logger
1774
+ logger,
1775
+ tenantId: options.tenantId
1588
1776
  }
1589
1777
  })
1590
1778
  };
@@ -1611,7 +1799,7 @@ var __webpack_exports__ = {};
1611
1799
  for (const p of policyMap.values())uniqueTypes.add(p.type);
1612
1800
  const latestPolicyByType = new Map();
1613
1801
  for (const type of uniqueTypes){
1614
- const latest = await registry.findOrCreatePolicy(type);
1802
+ const latest = await registry.findLatestPolicyByType(type);
1615
1803
  if (latest) latestPolicyByType.set(type, latest.id);
1616
1804
  }
1617
1805
  return {
@@ -1637,11 +1825,17 @@ var __webpack_exports__ = {};
1637
1825
  }
1638
1826
  return consents.map((consent)=>{
1639
1827
  let policyType = 'unknown';
1828
+ let policyVersion;
1829
+ let policyHash;
1830
+ let policyEffectiveDate;
1640
1831
  let isLatestPolicy = false;
1641
1832
  if (consent.policyId) {
1642
1833
  const policy = policyMap.get(consent.policyId);
1643
1834
  if (policy) {
1644
1835
  policyType = policy.type;
1836
+ policyVersion = policy.version;
1837
+ policyHash = policy.hash ?? void 0;
1838
+ policyEffectiveDate = policy.effectiveDate;
1645
1839
  isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
1646
1840
  }
1647
1841
  }
@@ -1658,6 +1852,9 @@ var __webpack_exports__ = {};
1658
1852
  id: consent.id,
1659
1853
  type: policyType,
1660
1854
  policyId: consent.policyId ?? void 0,
1855
+ policyVersion,
1856
+ policyHash,
1857
+ policyEffectiveDate,
1661
1858
  isLatestPolicy,
1662
1859
  preferences,
1663
1860
  givenAt: consent.givenAt
@@ -2352,6 +2549,94 @@ Use for geo-targeted consent banners and regional compliance.`,
2352
2549
  });
2353
2550
  return app;
2354
2551
  };
2552
+ const syncCurrentLegalDocumentHandler = async (c)=>{
2553
+ const ctx = c.get('c15tContext');
2554
+ const logger = ctx.logger;
2555
+ logger.info('Handling PUT /legal-documents/:type/current request');
2556
+ if (!ctx.apiKeyAuthenticated) throw new http_exception_namespaceObject.HTTPException(401, {
2557
+ message: 'API key required. Use Authorization: Bearer <api_key>',
2558
+ cause: {
2559
+ code: 'UNAUTHORIZED'
2560
+ }
2561
+ });
2562
+ const type = c.req.param('type');
2563
+ const body = await c.req.json();
2564
+ const effectiveDate = new Date(body.effectiveDate);
2565
+ if (Number.isNaN(effectiveDate.getTime())) throw new http_exception_namespaceObject.HTTPException(422, {
2566
+ message: 'effectiveDate must be a valid ISO-8601 string',
2567
+ cause: {
2568
+ code: 'INPUT_VALIDATION_FAILED'
2569
+ }
2570
+ });
2571
+ try {
2572
+ const policy = await ctx.registry.syncCurrentLegalDocumentPolicy({
2573
+ type,
2574
+ version: body.version,
2575
+ hash: body.hash,
2576
+ effectiveDate
2577
+ });
2578
+ return c.json({
2579
+ policy: {
2580
+ id: policy.id,
2581
+ type: policy.type,
2582
+ version: policy.version,
2583
+ hash: policy.hash,
2584
+ effectiveDate: policy.effectiveDate,
2585
+ isActive: policy.isActive
2586
+ }
2587
+ });
2588
+ } catch (error) {
2589
+ logger.error('Error in PUT /legal-documents/:type/current handler', {
2590
+ error: extractErrorMessage(error),
2591
+ errorType: error instanceof Error ? error.constructor.name : typeof error
2592
+ });
2593
+ if (error instanceof LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
2594
+ message: error.message,
2595
+ cause: {
2596
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
2597
+ }
2598
+ });
2599
+ if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
2600
+ throw new http_exception_namespaceObject.HTTPException(500, {
2601
+ message: 'Internal server error',
2602
+ cause: {
2603
+ code: 'INTERNAL_SERVER_ERROR'
2604
+ }
2605
+ });
2606
+ }
2607
+ };
2608
+ const createLegalDocumentRoutes = ()=>{
2609
+ const app = new external_hono_namespaceObject.Hono();
2610
+ app.put('/:type/current', (0, external_hono_openapi_namespaceObject.describeRoute)({
2611
+ summary: 'Sync the current legal document release (API key required)',
2612
+ description: 'Marks a legal document release as the latest known version for its type. Requires a Bearer API key.',
2613
+ tags: [
2614
+ 'LegalDocument'
2615
+ ],
2616
+ security: [
2617
+ {
2618
+ bearerAuth: []
2619
+ }
2620
+ ],
2621
+ responses: {
2622
+ 200: {
2623
+ description: 'Current legal document release synced successfully',
2624
+ content: {
2625
+ 'application/json': {
2626
+ schema: (0, external_hono_openapi_namespaceObject.resolver)(schema_.legalDocumentCurrentOutputSchema)
2627
+ }
2628
+ }
2629
+ },
2630
+ 401: {
2631
+ description: 'Missing or invalid API key'
2632
+ },
2633
+ 409: {
2634
+ description: 'Release metadata conflicts with an existing release'
2635
+ }
2636
+ }
2637
+ }), (0, external_hono_openapi_namespaceObject.validator)('param', schema_.legalDocumentCurrentParamsSchema), (0, external_hono_openapi_namespaceObject.validator)('json', schema_.legalDocumentCurrentInputSchema), syncCurrentLegalDocumentHandler);
2638
+ return app;
2639
+ };
2355
2640
  function getHeaders(headers) {
2356
2641
  if (!headers) return {
2357
2642
  countryCode: null,
@@ -2386,7 +2671,7 @@ Use for geo-targeted consent banners and regional compliance.`,
2386
2671
  try {
2387
2672
  await ctx.db.findFirst('subject', {});
2388
2673
  return c.json({
2389
- version: version_version,
2674
+ version: "2.0.0",
2390
2675
  timestamp: new Date(),
2391
2676
  client: clientInfo
2392
2677
  });
@@ -2717,6 +3002,79 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2717
3002
  });
2718
3003
  }
2719
3004
  };
3005
+ const DEFAULT_ISSUER = 'c15t';
3006
+ const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
3007
+ function isLegalDocumentPolicyType(type) {
3008
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
3009
+ }
3010
+ function snapshot_resolveSnapshotIssuer(options) {
3011
+ return options?.issuer?.trim() || DEFAULT_ISSUER;
3012
+ }
3013
+ function snapshot_resolveSnapshotAudience(params) {
3014
+ const configuredAudience = params.options?.audience?.trim();
3015
+ if (configuredAudience) return configuredAudience;
3016
+ return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
3017
+ }
3018
+ function snapshot_getSigningKey(secret) {
3019
+ return new TextEncoder().encode(secret);
3020
+ }
3021
+ function isLegalDocumentSnapshotPayload(payload) {
3022
+ 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;
3023
+ }
3024
+ async function verifyLegalDocumentSnapshotToken(params) {
3025
+ const { token, options, tenantId } = params;
3026
+ if (!options?.signingKey) return {
3027
+ valid: false,
3028
+ reason: 'missing'
3029
+ };
3030
+ if (!token) return {
3031
+ valid: false,
3032
+ reason: 'missing'
3033
+ };
3034
+ if (3 !== token.split('.').length) return {
3035
+ valid: false,
3036
+ reason: 'malformed'
3037
+ };
3038
+ try {
3039
+ const { payload, protectedHeader } = await (0, external_jose_namespaceObject.jwtVerify)(token, snapshot_getSigningKey(options.signingKey), {
3040
+ issuer: snapshot_resolveSnapshotIssuer(options),
3041
+ audience: snapshot_resolveSnapshotAudience({
3042
+ options,
3043
+ tenantId
3044
+ })
3045
+ });
3046
+ const header = protectedHeader;
3047
+ if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
3048
+ valid: false,
3049
+ reason: 'invalid'
3050
+ };
3051
+ if (!isLegalDocumentSnapshotPayload(payload)) return {
3052
+ valid: false,
3053
+ reason: 'invalid'
3054
+ };
3055
+ if (payload.sub !== payload.hash) return {
3056
+ valid: false,
3057
+ reason: 'invalid'
3058
+ };
3059
+ if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
3060
+ valid: false,
3061
+ reason: 'invalid'
3062
+ };
3063
+ return {
3064
+ valid: true,
3065
+ payload
3066
+ };
3067
+ } catch (error) {
3068
+ if (error instanceof external_jose_namespaceObject.errors.JWTExpired) return {
3069
+ valid: false,
3070
+ reason: 'expired'
3071
+ };
3072
+ return {
3073
+ valid: false,
3074
+ reason: 'invalid'
3075
+ };
3076
+ }
3077
+ }
2720
3078
  function buildRuntimeDecisionDedupeKey(input) {
2721
3079
  return [
2722
3080
  input.tenantId ?? 'default',
@@ -2796,6 +3154,9 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2796
3154
  if (!firstLanguage) return;
2797
3155
  return firstLanguage.split('-')[0]?.toLowerCase();
2798
3156
  }
3157
+ function isLegalDocumentType(type) {
3158
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
3159
+ }
2799
3160
  function resolveSnapshotFailureMode(ctx) {
2800
3161
  return ctx.policySnapshot?.onValidationFailure ?? 'reject';
2801
3162
  }
@@ -2830,6 +3191,45 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2830
3191
  }
2831
3192
  }
2832
3193
  }
3194
+ function buildLegalDocumentSnapshotHttpException(reason) {
3195
+ switch(reason){
3196
+ case 'missing':
3197
+ return new http_exception_namespaceObject.HTTPException(409, {
3198
+ message: 'Legal document snapshot token is required',
3199
+ cause: {
3200
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
3201
+ }
3202
+ });
3203
+ case 'expired':
3204
+ return new http_exception_namespaceObject.HTTPException(409, {
3205
+ message: 'Legal document snapshot token has expired',
3206
+ cause: {
3207
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
3208
+ }
3209
+ });
3210
+ case 'malformed':
3211
+ case 'invalid':
3212
+ return new http_exception_namespaceObject.HTTPException(409, {
3213
+ message: 'Legal document snapshot token is invalid',
3214
+ cause: {
3215
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
3216
+ }
3217
+ });
3218
+ default:
3219
+ {
3220
+ const _exhaustive = reason;
3221
+ throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
3222
+ }
3223
+ }
3224
+ }
3225
+ function buildLegalDocumentProofHttpException(message) {
3226
+ return new http_exception_namespaceObject.HTTPException(409, {
3227
+ message,
3228
+ cause: {
3229
+ code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
3230
+ }
3231
+ });
3232
+ }
2833
3233
  const postSubjectHandler = async (c)=>{
2834
3234
  const ctx = c.get('c15tContext');
2835
3235
  const logger = ctx.logger;
@@ -2855,16 +3255,30 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2855
3255
  const requestLanguage = parseLanguageFromHeader(acceptLanguage);
2856
3256
  const location = await getLocation(request, ctx);
2857
3257
  const resolvedJurisdiction = getJurisdiction(location, ctx);
2858
- const snapshotVerification = await verifyPolicySnapshotToken({
3258
+ const legalDocumentConsent = isLegalDocumentType(type);
3259
+ const runtimeSnapshotVerification = legalDocumentConsent ? {
3260
+ valid: false,
3261
+ reason: 'missing'
3262
+ } : await verifyPolicySnapshotToken({
2859
3263
  token: input.policySnapshotToken,
2860
3264
  options: ctx.policySnapshot,
2861
3265
  tenantId: ctx.tenantId
2862
3266
  });
2863
- const hasValidSnapshot = snapshotVerification.valid;
2864
- const snapshotPayload = snapshotVerification.valid ? snapshotVerification.payload : null;
2865
- const shouldRequireSnapshot = !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
2866
- if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(snapshotVerification.reason);
2867
- const resolvedPolicyDecision = hasValidSnapshot ? void 0 : await resolvePolicyDecision({
3267
+ const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
3268
+ token: input.documentSnapshotToken,
3269
+ options: ctx.legalDocumentSnapshot,
3270
+ tenantId: ctx.tenantId
3271
+ }) : {
3272
+ valid: false,
3273
+ reason: 'missing'
3274
+ };
3275
+ const hasValidSnapshot = runtimeSnapshotVerification.valid;
3276
+ const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
3277
+ const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
3278
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
3279
+ const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
3280
+ if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
3281
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await resolvePolicyDecision({
2868
3282
  policies: ctx.policyPacks,
2869
3283
  countryCode: location.countryCode,
2870
3284
  regionCode: location.regionCode,
@@ -2921,7 +3335,61 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2921
3335
  let purposeIds = [];
2922
3336
  let appliedPreferences;
2923
3337
  const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
2924
- if (inputPolicyId) {
3338
+ const inputPolicyHash = 'policyHash' in input ? input.policyHash : void 0;
3339
+ if (legalDocumentConsent && legalDocumentSnapshotVerification.valid) {
3340
+ if (legalDocumentSnapshotVerification.payload.type !== type) throw buildLegalDocumentSnapshotHttpException('invalid');
3341
+ const effectiveDate = new Date(legalDocumentSnapshotVerification.payload.effectiveDate);
3342
+ if (Number.isNaN(effectiveDate.getTime())) throw buildLegalDocumentSnapshotHttpException('invalid');
3343
+ const documentPolicy = await registry.findOrCreateLegalDocumentPolicy({
3344
+ type,
3345
+ version: legalDocumentSnapshotVerification.payload.version,
3346
+ hash: legalDocumentSnapshotVerification.payload.hash,
3347
+ effectiveDate
3348
+ });
3349
+ policyId = documentPolicy.id;
3350
+ } else if (legalDocumentConsent) {
3351
+ if (!ctx.legalDocumentSnapshot?.signingKey && !inputPolicyId && !inputPolicyHash) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId or policyHash when snapshot verification is disabled');
3352
+ if (inputPolicyId) {
3353
+ policyId = inputPolicyId;
3354
+ const policy = await registry.findConsentPolicyById(inputPolicyId);
3355
+ if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
3356
+ message: 'Policy not found',
3357
+ cause: {
3358
+ code: 'POLICY_NOT_FOUND',
3359
+ policyId,
3360
+ type
3361
+ }
3362
+ });
3363
+ if (!policy.isActive) throw new http_exception_namespaceObject.HTTPException(400, {
3364
+ message: 'Policy is inactive',
3365
+ cause: {
3366
+ code: 'POLICY_INACTIVE',
3367
+ policyId,
3368
+ type
3369
+ }
3370
+ });
3371
+ } else if (inputPolicyHash) {
3372
+ const policy = await registry.findLegalDocumentPolicyByHash(type, inputPolicyHash);
3373
+ if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
3374
+ message: 'Policy not found',
3375
+ cause: {
3376
+ code: 'POLICY_NOT_FOUND',
3377
+ type,
3378
+ policyHash: inputPolicyHash
3379
+ }
3380
+ });
3381
+ if (!policy.isActive) throw new http_exception_namespaceObject.HTTPException(400, {
3382
+ message: 'Policy is inactive',
3383
+ cause: {
3384
+ code: 'POLICY_INACTIVE',
3385
+ policyId: policy.id,
3386
+ type,
3387
+ policyHash: inputPolicyHash
3388
+ }
3389
+ });
3390
+ policyId = policy.id;
3391
+ }
3392
+ } else if (inputPolicyId) {
2925
3393
  policyId = inputPolicyId;
2926
3394
  const policy = await registry.findConsentPolicyById(inputPolicyId);
2927
3395
  if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
@@ -2983,6 +3451,13 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2983
3451
  });
2984
3452
  purposeIds = purposes;
2985
3453
  }
3454
+ if (!policyId) throw new http_exception_namespaceObject.HTTPException(500, {
3455
+ message: 'Failed to resolve policy',
3456
+ cause: {
3457
+ code: 'POLICY_RESOLUTION_FAILED',
3458
+ type
3459
+ }
3460
+ });
2986
3461
  const expiryDays = effectivePolicy?.consent?.expiryDays;
2987
3462
  const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
2988
3463
  const proofConfig = effectivePolicy?.proof;
@@ -3146,6 +3621,12 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3146
3621
  errorType: error instanceof Error ? error.constructor.name : typeof error
3147
3622
  });
3148
3623
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
3624
+ if (error instanceof LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
3625
+ message: error.message,
3626
+ cause: {
3627
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
3628
+ }
3629
+ });
3149
3630
  throw new http_exception_namespaceObject.HTTPException(500, {
3150
3631
  message: 'Internal server error',
3151
3632
  cause: {
@@ -3179,7 +3660,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3179
3660
  }), (0, external_hono_openapi_namespaceObject.validator)('param', schema_.getSubjectInputSchema), getSubjectHandler);
3180
3661
  app.post('/', (0, external_hono_openapi_namespaceObject.describeRoute)({
3181
3662
  summary: 'Record consent for a subject',
3182
- 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`",
3663
+ 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`",
3183
3664
  tags: [
3184
3665
  'Subject',
3185
3666
  'Consent'
@@ -3392,7 +3873,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3392
3873
  openapi: '3.1.0',
3393
3874
  info: {
3394
3875
  title: options.appName || 'c15t API',
3395
- version: version_version,
3876
+ version: "2.0.0",
3396
3877
  description: 'API for consent management'
3397
3878
  },
3398
3879
  servers: [
@@ -3417,6 +3898,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3417
3898
  }));
3418
3899
  }
3419
3900
  app.route('/init', createInitRoute(options));
3901
+ app.route('/legal-documents', createLegalDocumentRoutes());
3420
3902
  app.route('/subjects', createSubjectRoutes());
3421
3903
  app.route('/consents', createConsentRoutes());
3422
3904
  app.route('/status', createStatusRoute());