@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/core.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  var __webpack_modules__ = {
3
- "./src/db/schema/index.ts" (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
3
+ "./src/db/schema/index.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4
4
  __webpack_require__.d(__webpack_exports__, {
5
5
  DB: ()=>DB
6
6
  });
@@ -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'),
@@ -294,7 +295,7 @@ var __webpack_modules__ = {
294
295
  ]
295
296
  });
296
297
  },
297
- "./src/define-config.ts" (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
298
+ "./src/define-config.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
298
299
  __webpack_require__.d(__webpack_exports__, {
299
300
  defineConfig: ()=>defineConfig
300
301
  });
@@ -336,7 +337,7 @@ function __webpack_require__(moduleId) {
336
337
  })();
337
338
  (()=>{
338
339
  __webpack_require__.r = (exports1)=>{
339
- if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
340
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
340
341
  value: 'Module'
341
342
  });
342
343
  Object.defineProperty(exports1, '__esModule', {
@@ -737,7 +738,7 @@ var __webpack_exports__ = {};
737
738
  language
738
739
  };
739
740
  }
740
- const version_version = '2.0.0-rc.5';
741
+ const version_version = '2.0.0-rc.8';
741
742
  function extractErrorMessage(error) {
742
743
  if (error instanceof AggregateError && error.errors?.length > 0) {
743
744
  const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
@@ -1132,6 +1133,23 @@ var __webpack_exports__ = {};
1132
1133
  throw error;
1133
1134
  }
1134
1135
  }
1136
+ class LegalDocumentPolicyConflictError extends Error {
1137
+ constructor(message){
1138
+ super(message);
1139
+ this.name = 'LegalDocumentPolicyConflictError';
1140
+ }
1141
+ }
1142
+ async function buildLegalDocumentPolicyId(input) {
1143
+ const digest = await (0, types_namespaceObject.hashSha256Hex)([
1144
+ input.tenantId ?? 'default',
1145
+ input.type,
1146
+ input.hash
1147
+ ].join('|'));
1148
+ return `pol_${digest}`;
1149
+ }
1150
+ function hasLegalDocumentPolicyConflict(policy, input) {
1151
+ return policy.version !== input.version || policy.hash !== input.hash || policy.effectiveDate.getTime() !== input.effectiveDate.getTime();
1152
+ }
1135
1153
  function policyRegistry({ db, ctx }) {
1136
1154
  const { logger } = ctx;
1137
1155
  return {
@@ -1160,6 +1178,176 @@ var __webpack_exports__ = {};
1160
1178
  throw error;
1161
1179
  }
1162
1180
  },
1181
+ findLatestPolicyByType: async (type)=>{
1182
+ const start = Date.now();
1183
+ try {
1184
+ const result = await withDatabaseSpan({
1185
+ operation: 'findLatest',
1186
+ entity: 'consentPolicy'
1187
+ }, async ()=>db.findFirst('consentPolicy', {
1188
+ where: (b)=>b.and(b('isActive', '=', true), b('type', '=', type)),
1189
+ orderBy: [
1190
+ 'effectiveDate',
1191
+ 'desc'
1192
+ ]
1193
+ }));
1194
+ getMetrics()?.recordDbQuery({
1195
+ operation: 'findLatest',
1196
+ entity: 'consentPolicy'
1197
+ }, Date.now() - start);
1198
+ return result;
1199
+ } catch (error) {
1200
+ getMetrics()?.recordDbError({
1201
+ operation: 'findLatest',
1202
+ entity: 'consentPolicy'
1203
+ });
1204
+ throw error;
1205
+ }
1206
+ },
1207
+ findLegalDocumentPolicyByHash: async (type, hash)=>{
1208
+ const start = Date.now();
1209
+ try {
1210
+ const policyId = await buildLegalDocumentPolicyId({
1211
+ tenantId: ctx.tenantId,
1212
+ type,
1213
+ hash
1214
+ });
1215
+ const result = await withDatabaseSpan({
1216
+ operation: 'findByHash',
1217
+ entity: 'consentPolicy'
1218
+ }, async ()=>db.findFirst('consentPolicy', {
1219
+ where: (b)=>b('id', '=', policyId)
1220
+ }));
1221
+ getMetrics()?.recordDbQuery({
1222
+ operation: 'findByHash',
1223
+ entity: 'consentPolicy'
1224
+ }, Date.now() - start);
1225
+ return result;
1226
+ } catch (error) {
1227
+ getMetrics()?.recordDbError({
1228
+ operation: 'findByHash',
1229
+ entity: 'consentPolicy'
1230
+ });
1231
+ throw error;
1232
+ }
1233
+ },
1234
+ syncCurrentLegalDocumentPolicy: async (input)=>{
1235
+ const start = Date.now();
1236
+ try {
1237
+ const result = await withDatabaseSpan({
1238
+ operation: 'syncCurrent',
1239
+ entity: 'consentPolicy'
1240
+ }, async ()=>{
1241
+ const policyId = await buildLegalDocumentPolicyId({
1242
+ tenantId: ctx.tenantId,
1243
+ type: input.type,
1244
+ hash: input.hash
1245
+ });
1246
+ return db.transaction(async (tx)=>{
1247
+ const existing = await tx.findFirst('consentPolicy', {
1248
+ where: (b)=>b('id', '=', policyId)
1249
+ });
1250
+ if (existing) {
1251
+ if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
1252
+ await tx.updateMany('consentPolicy', {
1253
+ where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true), b('id', '!=', existing.id)),
1254
+ set: {
1255
+ isActive: false
1256
+ }
1257
+ });
1258
+ if (!existing.isActive) {
1259
+ await tx.updateMany('consentPolicy', {
1260
+ where: (b)=>b('id', '=', existing.id),
1261
+ set: {
1262
+ isActive: true
1263
+ }
1264
+ });
1265
+ return {
1266
+ ...existing,
1267
+ isActive: true
1268
+ };
1269
+ }
1270
+ return existing;
1271
+ }
1272
+ await tx.updateMany('consentPolicy', {
1273
+ where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true)),
1274
+ set: {
1275
+ isActive: false
1276
+ }
1277
+ });
1278
+ const policy = await tx.create('consentPolicy', {
1279
+ id: policyId,
1280
+ version: input.version,
1281
+ type: input.type,
1282
+ hash: input.hash,
1283
+ effectiveDate: input.effectiveDate,
1284
+ isActive: true
1285
+ });
1286
+ return policy;
1287
+ });
1288
+ });
1289
+ getMetrics()?.recordDbQuery({
1290
+ operation: 'syncCurrent',
1291
+ entity: 'consentPolicy'
1292
+ }, Date.now() - start);
1293
+ return result;
1294
+ } catch (error) {
1295
+ getMetrics()?.recordDbError({
1296
+ operation: 'syncCurrent',
1297
+ entity: 'consentPolicy'
1298
+ });
1299
+ throw error;
1300
+ }
1301
+ },
1302
+ findOrCreateLegalDocumentPolicy: async (input)=>{
1303
+ const start = Date.now();
1304
+ try {
1305
+ const result = await withDatabaseSpan({
1306
+ operation: 'findOrCreateLegalDocument',
1307
+ entity: 'consentPolicy'
1308
+ }, async ()=>{
1309
+ const policyId = await buildLegalDocumentPolicyId({
1310
+ tenantId: ctx.tenantId,
1311
+ type: input.type,
1312
+ hash: input.hash
1313
+ });
1314
+ const existing = await db.findFirst('consentPolicy', {
1315
+ where: (b)=>b('id', '=', policyId)
1316
+ });
1317
+ if (existing) {
1318
+ if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
1319
+ return existing;
1320
+ }
1321
+ const policy = await db.create('consentPolicy', {
1322
+ id: policyId,
1323
+ version: input.version,
1324
+ type: input.type,
1325
+ hash: input.hash,
1326
+ effectiveDate: input.effectiveDate,
1327
+ isActive: false
1328
+ }).catch(async ()=>{
1329
+ const concurrent = await db.findFirst('consentPolicy', {
1330
+ where: (b)=>b('id', '=', policyId)
1331
+ });
1332
+ if (!concurrent) throw new LegalDocumentPolicyConflictError('Failed to create legal document consent policy');
1333
+ if (hasLegalDocumentPolicyConflict(concurrent, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
1334
+ return concurrent;
1335
+ });
1336
+ return policy;
1337
+ });
1338
+ getMetrics()?.recordDbQuery({
1339
+ operation: 'findOrCreateLegalDocument',
1340
+ entity: 'consentPolicy'
1341
+ }, Date.now() - start);
1342
+ return result;
1343
+ } catch (error) {
1344
+ getMetrics()?.recordDbError({
1345
+ operation: 'findOrCreateLegalDocument',
1346
+ entity: 'consentPolicy'
1347
+ });
1348
+ throw error;
1349
+ }
1350
+ },
1163
1351
  findOrCreatePolicy: async (type)=>{
1164
1352
  const start = Date.now();
1165
1353
  try {
@@ -1584,7 +1772,8 @@ var __webpack_exports__ = {};
1584
1772
  registry: createRegistry({
1585
1773
  db: orm,
1586
1774
  ctx: {
1587
- logger
1775
+ logger,
1776
+ tenantId: options.tenantId
1588
1777
  }
1589
1778
  })
1590
1779
  };
@@ -1611,7 +1800,7 @@ var __webpack_exports__ = {};
1611
1800
  for (const p of policyMap.values())uniqueTypes.add(p.type);
1612
1801
  const latestPolicyByType = new Map();
1613
1802
  for (const type of uniqueTypes){
1614
- const latest = await registry.findOrCreatePolicy(type);
1803
+ const latest = await registry.findLatestPolicyByType(type);
1615
1804
  if (latest) latestPolicyByType.set(type, latest.id);
1616
1805
  }
1617
1806
  return {
@@ -1637,11 +1826,17 @@ var __webpack_exports__ = {};
1637
1826
  }
1638
1827
  return consents.map((consent)=>{
1639
1828
  let policyType = 'unknown';
1829
+ let policyVersion;
1830
+ let policyHash;
1831
+ let policyEffectiveDate;
1640
1832
  let isLatestPolicy = false;
1641
1833
  if (consent.policyId) {
1642
1834
  const policy = policyMap.get(consent.policyId);
1643
1835
  if (policy) {
1644
1836
  policyType = policy.type;
1837
+ policyVersion = policy.version;
1838
+ policyHash = policy.hash ?? void 0;
1839
+ policyEffectiveDate = policy.effectiveDate;
1645
1840
  isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
1646
1841
  }
1647
1842
  }
@@ -1658,6 +1853,9 @@ var __webpack_exports__ = {};
1658
1853
  id: consent.id,
1659
1854
  type: policyType,
1660
1855
  policyId: consent.policyId ?? void 0,
1856
+ policyVersion,
1857
+ policyHash,
1858
+ policyEffectiveDate,
1661
1859
  isLatestPolicy,
1662
1860
  preferences,
1663
1861
  givenAt: consent.givenAt
@@ -1772,11 +1970,7 @@ var __webpack_exports__ = {};
1772
1970
  const app = new external_hono_namespaceObject.Hono();
1773
1971
  app.get('/check', (0, external_hono_openapi_namespaceObject.describeRoute)({
1774
1972
  summary: 'Check consent by external user ID',
1775
- description: `Pre-banner cross-device consent check. Use to avoid showing the banner when the user has already consented on another device.
1776
-
1777
- **Query parameters:**
1778
- - \`externalId\` – External user ID to check
1779
- - \`type\` – Consent type(s) to check (comma-separated)`,
1973
+ 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)",
1780
1974
  tags: [
1781
1975
  'Consent'
1782
1976
  ],
@@ -2356,6 +2550,94 @@ Use for geo-targeted consent banners and regional compliance.`,
2356
2550
  });
2357
2551
  return app;
2358
2552
  };
2553
+ const syncCurrentLegalDocumentHandler = async (c)=>{
2554
+ const ctx = c.get('c15tContext');
2555
+ const logger = ctx.logger;
2556
+ logger.info('Handling PUT /legal-documents/:type/current request');
2557
+ if (!ctx.apiKeyAuthenticated) throw new http_exception_namespaceObject.HTTPException(401, {
2558
+ message: 'API key required. Use Authorization: Bearer <api_key>',
2559
+ cause: {
2560
+ code: 'UNAUTHORIZED'
2561
+ }
2562
+ });
2563
+ const type = c.req.param('type');
2564
+ const body = await c.req.json();
2565
+ const effectiveDate = new Date(body.effectiveDate);
2566
+ if (Number.isNaN(effectiveDate.getTime())) throw new http_exception_namespaceObject.HTTPException(422, {
2567
+ message: 'effectiveDate must be a valid ISO-8601 string',
2568
+ cause: {
2569
+ code: 'INPUT_VALIDATION_FAILED'
2570
+ }
2571
+ });
2572
+ try {
2573
+ const policy = await ctx.registry.syncCurrentLegalDocumentPolicy({
2574
+ type,
2575
+ version: body.version,
2576
+ hash: body.hash,
2577
+ effectiveDate
2578
+ });
2579
+ return c.json({
2580
+ policy: {
2581
+ id: policy.id,
2582
+ type: policy.type,
2583
+ version: policy.version,
2584
+ hash: policy.hash,
2585
+ effectiveDate: policy.effectiveDate,
2586
+ isActive: policy.isActive
2587
+ }
2588
+ });
2589
+ } catch (error) {
2590
+ logger.error('Error in PUT /legal-documents/:type/current handler', {
2591
+ error: extractErrorMessage(error),
2592
+ errorType: error instanceof Error ? error.constructor.name : typeof error
2593
+ });
2594
+ if (error instanceof LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
2595
+ message: error.message,
2596
+ cause: {
2597
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
2598
+ }
2599
+ });
2600
+ if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
2601
+ throw new http_exception_namespaceObject.HTTPException(500, {
2602
+ message: 'Internal server error',
2603
+ cause: {
2604
+ code: 'INTERNAL_SERVER_ERROR'
2605
+ }
2606
+ });
2607
+ }
2608
+ };
2609
+ const createLegalDocumentRoutes = ()=>{
2610
+ const app = new external_hono_namespaceObject.Hono();
2611
+ app.put('/:type/current', (0, external_hono_openapi_namespaceObject.describeRoute)({
2612
+ summary: 'Sync the current legal document release (API key required)',
2613
+ description: 'Marks a legal document release as the latest known version for its type. Requires a Bearer API key.',
2614
+ tags: [
2615
+ 'LegalDocument'
2616
+ ],
2617
+ security: [
2618
+ {
2619
+ bearerAuth: []
2620
+ }
2621
+ ],
2622
+ responses: {
2623
+ 200: {
2624
+ description: 'Current legal document release synced successfully',
2625
+ content: {
2626
+ 'application/json': {
2627
+ schema: (0, external_hono_openapi_namespaceObject.resolver)(schema_.legalDocumentCurrentOutputSchema)
2628
+ }
2629
+ }
2630
+ },
2631
+ 401: {
2632
+ description: 'Missing or invalid API key'
2633
+ },
2634
+ 409: {
2635
+ description: 'Release metadata conflicts with an existing release'
2636
+ }
2637
+ }
2638
+ }), (0, external_hono_openapi_namespaceObject.validator)('param', schema_.legalDocumentCurrentParamsSchema), (0, external_hono_openapi_namespaceObject.validator)('json', schema_.legalDocumentCurrentInputSchema), syncCurrentLegalDocumentHandler);
2639
+ return app;
2640
+ };
2359
2641
  function getHeaders(headers) {
2360
2642
  if (!headers) return {
2361
2643
  countryCode: null,
@@ -2442,6 +2724,12 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2442
2724
  const subjectId = c.req.param('id');
2443
2725
  const type = c.req.query('type');
2444
2726
  const typeFilter = type?.split(',').map((t)=>t.trim()) || [];
2727
+ if (!subjectId) throw new http_exception_namespaceObject.HTTPException(400, {
2728
+ message: 'Subject ID is required',
2729
+ cause: {
2730
+ code: 'SUBJECT_ID_REQUIRED'
2731
+ }
2732
+ });
2445
2733
  logger.debug('Request parameters', {
2446
2734
  subjectId,
2447
2735
  typeFilter
@@ -2633,6 +2921,12 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2633
2921
  const subjectId = c.req.param('id');
2634
2922
  const body = await c.req.json();
2635
2923
  const { externalId, identityProvider = 'external' } = body;
2924
+ if (!subjectId) throw new http_exception_namespaceObject.HTTPException(400, {
2925
+ message: 'Subject ID is required',
2926
+ cause: {
2927
+ code: 'SUBJECT_ID_REQUIRED'
2928
+ }
2929
+ });
2636
2930
  logger.debug('Request parameters', {
2637
2931
  subjectId,
2638
2932
  externalId,
@@ -2709,6 +3003,79 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2709
3003
  });
2710
3004
  }
2711
3005
  };
3006
+ const DEFAULT_ISSUER = 'c15t';
3007
+ const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
3008
+ function isLegalDocumentPolicyType(type) {
3009
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
3010
+ }
3011
+ function snapshot_resolveSnapshotIssuer(options) {
3012
+ return options?.issuer?.trim() || DEFAULT_ISSUER;
3013
+ }
3014
+ function snapshot_resolveSnapshotAudience(params) {
3015
+ const configuredAudience = params.options?.audience?.trim();
3016
+ if (configuredAudience) return configuredAudience;
3017
+ return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
3018
+ }
3019
+ function snapshot_getSigningKey(secret) {
3020
+ return new TextEncoder().encode(secret);
3021
+ }
3022
+ function isLegalDocumentSnapshotPayload(payload) {
3023
+ 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;
3024
+ }
3025
+ async function verifyLegalDocumentSnapshotToken(params) {
3026
+ const { token, options, tenantId } = params;
3027
+ if (!options?.signingKey) return {
3028
+ valid: false,
3029
+ reason: 'missing'
3030
+ };
3031
+ if (!token) return {
3032
+ valid: false,
3033
+ reason: 'missing'
3034
+ };
3035
+ if (3 !== token.split('.').length) return {
3036
+ valid: false,
3037
+ reason: 'malformed'
3038
+ };
3039
+ try {
3040
+ const { payload, protectedHeader } = await (0, external_jose_namespaceObject.jwtVerify)(token, snapshot_getSigningKey(options.signingKey), {
3041
+ issuer: snapshot_resolveSnapshotIssuer(options),
3042
+ audience: snapshot_resolveSnapshotAudience({
3043
+ options,
3044
+ tenantId
3045
+ })
3046
+ });
3047
+ const header = protectedHeader;
3048
+ if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
3049
+ valid: false,
3050
+ reason: 'invalid'
3051
+ };
3052
+ if (!isLegalDocumentSnapshotPayload(payload)) return {
3053
+ valid: false,
3054
+ reason: 'invalid'
3055
+ };
3056
+ if (payload.sub !== payload.hash) return {
3057
+ valid: false,
3058
+ reason: 'invalid'
3059
+ };
3060
+ if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
3061
+ valid: false,
3062
+ reason: 'invalid'
3063
+ };
3064
+ return {
3065
+ valid: true,
3066
+ payload
3067
+ };
3068
+ } catch (error) {
3069
+ if (error instanceof external_jose_namespaceObject.errors.JWTExpired) return {
3070
+ valid: false,
3071
+ reason: 'expired'
3072
+ };
3073
+ return {
3074
+ valid: false,
3075
+ reason: 'invalid'
3076
+ };
3077
+ }
3078
+ }
2712
3079
  function buildRuntimeDecisionDedupeKey(input) {
2713
3080
  return [
2714
3081
  input.tenantId ?? 'default',
@@ -2788,6 +3155,9 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2788
3155
  if (!firstLanguage) return;
2789
3156
  return firstLanguage.split('-')[0]?.toLowerCase();
2790
3157
  }
3158
+ function isLegalDocumentType(type) {
3159
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
3160
+ }
2791
3161
  function resolveSnapshotFailureMode(ctx) {
2792
3162
  return ctx.policySnapshot?.onValidationFailure ?? 'reject';
2793
3163
  }
@@ -2822,6 +3192,45 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2822
3192
  }
2823
3193
  }
2824
3194
  }
3195
+ function buildLegalDocumentSnapshotHttpException(reason) {
3196
+ switch(reason){
3197
+ case 'missing':
3198
+ return new http_exception_namespaceObject.HTTPException(409, {
3199
+ message: 'Legal document snapshot token is required',
3200
+ cause: {
3201
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
3202
+ }
3203
+ });
3204
+ case 'expired':
3205
+ return new http_exception_namespaceObject.HTTPException(409, {
3206
+ message: 'Legal document snapshot token has expired',
3207
+ cause: {
3208
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
3209
+ }
3210
+ });
3211
+ case 'malformed':
3212
+ case 'invalid':
3213
+ return new http_exception_namespaceObject.HTTPException(409, {
3214
+ message: 'Legal document snapshot token is invalid',
3215
+ cause: {
3216
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
3217
+ }
3218
+ });
3219
+ default:
3220
+ {
3221
+ const _exhaustive = reason;
3222
+ throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
3223
+ }
3224
+ }
3225
+ }
3226
+ function buildLegalDocumentProofHttpException(message) {
3227
+ return new http_exception_namespaceObject.HTTPException(409, {
3228
+ message,
3229
+ cause: {
3230
+ code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
3231
+ }
3232
+ });
3233
+ }
2825
3234
  const postSubjectHandler = async (c)=>{
2826
3235
  const ctx = c.get('c15tContext');
2827
3236
  const logger = ctx.logger;
@@ -2847,16 +3256,30 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2847
3256
  const requestLanguage = parseLanguageFromHeader(acceptLanguage);
2848
3257
  const location = await getLocation(request, ctx);
2849
3258
  const resolvedJurisdiction = getJurisdiction(location, ctx);
2850
- const snapshotVerification = await verifyPolicySnapshotToken({
3259
+ const legalDocumentConsent = isLegalDocumentType(type);
3260
+ const runtimeSnapshotVerification = legalDocumentConsent ? {
3261
+ valid: false,
3262
+ reason: 'missing'
3263
+ } : await verifyPolicySnapshotToken({
2851
3264
  token: input.policySnapshotToken,
2852
3265
  options: ctx.policySnapshot,
2853
3266
  tenantId: ctx.tenantId
2854
3267
  });
2855
- const hasValidSnapshot = snapshotVerification.valid;
2856
- const snapshotPayload = snapshotVerification.valid ? snapshotVerification.payload : null;
2857
- const shouldRequireSnapshot = !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
2858
- if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(snapshotVerification.reason);
2859
- const resolvedPolicyDecision = hasValidSnapshot ? void 0 : await resolvePolicyDecision({
3268
+ const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
3269
+ token: input.documentSnapshotToken,
3270
+ options: ctx.legalDocumentSnapshot,
3271
+ tenantId: ctx.tenantId
3272
+ }) : {
3273
+ valid: false,
3274
+ reason: 'missing'
3275
+ };
3276
+ const hasValidSnapshot = runtimeSnapshotVerification.valid;
3277
+ const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
3278
+ const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
3279
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
3280
+ const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
3281
+ if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
3282
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await resolvePolicyDecision({
2860
3283
  policies: ctx.policyPacks,
2861
3284
  countryCode: location.countryCode,
2862
3285
  regionCode: location.regionCode,
@@ -2913,7 +3336,39 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2913
3336
  let purposeIds = [];
2914
3337
  let appliedPreferences;
2915
3338
  const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
2916
- if (inputPolicyId) {
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) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId when snapshot verification is disabled');
3352
+ if (!inputPolicyId) throw buildLegalDocumentProofHttpException('Legal document consent requires a valid document snapshot token or policyId');
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 (inputPolicyId) {
2917
3372
  policyId = inputPolicyId;
2918
3373
  const policy = await registry.findConsentPolicyById(inputPolicyId);
2919
3374
  if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
@@ -3138,6 +3593,12 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3138
3593
  errorType: error instanceof Error ? error.constructor.name : typeof error
3139
3594
  });
3140
3595
  if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
3596
+ if (error instanceof LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
3597
+ message: error.message,
3598
+ cause: {
3599
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
3600
+ }
3601
+ });
3141
3602
  throw new http_exception_namespaceObject.HTTPException(500, {
3142
3603
  message: 'Internal server error',
3143
3604
  cause: {
@@ -3150,11 +3611,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3150
3611
  const app = new external_hono_namespaceObject.Hono();
3151
3612
  app.get('/:id', (0, external_hono_openapi_namespaceObject.describeRoute)({
3152
3613
  summary: 'Get subject consent status',
3153
- description: `Returns the subject's consent status for this device. Use to check if the subject has valid consent for given policy types.
3154
-
3155
- **Query:** \`type\` – Filter by consent type(s), comma-separated (e.g. \`privacy_policy,cookie_banner\`).
3156
-
3157
- **Response:** \`subject\`, \`consents\` (matching filter), \`isValid\` (valid consent for requested type(s)).`,
3614
+ 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)).",
3158
3615
  tags: [
3159
3616
  'Subject',
3160
3617
  'Consent'
@@ -3175,12 +3632,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3175
3632
  }), (0, external_hono_openapi_namespaceObject.validator)('param', schema_.getSubjectInputSchema), getSubjectHandler);
3176
3633
  app.post('/', (0, external_hono_openapi_namespaceObject.describeRoute)({
3177
3634
  summary: 'Record consent for a subject',
3178
- description: `Creates a new consent record (append-only). Creates the subject if it does not exist.
3179
-
3180
- **Request body by \`type\`:**
3181
- - \`cookie_banner\` – Requires \`preferences\` object
3182
- - \`privacy_policy\`, \`dpa\`, \`terms_and_conditions\` – Optional \`policyId\`
3183
- - \`marketing_communications\`, \`age_verification\`, \`other\` – Optional \`preferences\``,
3635
+ 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`",
3184
3636
  tags: [
3185
3637
  'Subject',
3186
3638
  'Consent'
@@ -3265,7 +3717,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3265
3717
  if (!value) return;
3266
3718
  return (0, schema_.compactDefined)({
3267
3719
  allowedActions: (0, schema_.dedupeTrimmedStrings)(value.allowedActions),
3268
- primaryAction: value.primaryAction,
3720
+ primaryActions: value.primaryActions,
3269
3721
  layout: value.layout,
3270
3722
  direction: value.direction,
3271
3723
  uiProfile: value.uiProfile,
@@ -3413,13 +3865,12 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3413
3865
  }));
3414
3866
  const publicSpecUrl = `${basePath}${openApiConfig.specPath}`.replace(/\/+/g, '/');
3415
3867
  app.get(openApiConfig.docsPath, (0, hono_api_reference_namespaceObject.apiReference)({
3416
- spec: {
3417
- url: publicSpecUrl
3418
- },
3868
+ url: publicSpecUrl,
3419
3869
  pageTitle: `${options.appName || 'c15t API'} Documentation`
3420
3870
  }));
3421
3871
  }
3422
3872
  app.route('/init', createInitRoute(options));
3873
+ app.route('/legal-documents', createLegalDocumentRoutes());
3423
3874
  app.route('/subjects', createSubjectRoutes());
3424
3875
  app.route('/consents', createConsentRoutes());
3425
3876
  app.route('/status', createStatusRoute());