@c15t/backend 2.0.0-rc.6 → 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 (49) hide show
  1. package/dist/302.js +1 -1
  2. package/dist/{364.js → 915.js} +626 -24
  3. package/dist/core.cjs +465 -11
  4. package/dist/core.js +4 -152
  5. package/dist/db/schema.cjs +4 -0
  6. package/dist/db/schema.js +3 -2
  7. package/dist/edge.cjs +8 -8
  8. package/dist/edge.js +3 -3
  9. package/dist/router.cjs +224 -41
  10. package/dist/router.js +1 -1
  11. package/dist-types/cache/gvl-resolver.d.ts +1 -1
  12. package/dist-types/db/registry/consent-policy.d.ts +57 -1
  13. package/dist-types/db/registry/index.d.ts +43 -1
  14. package/dist-types/db/registry/types.d.ts +2 -1
  15. package/dist-types/db/schema/1.0.0/consent.d.ts +1 -1
  16. package/dist-types/db/schema/2.0.0/audit-log.d.ts +1 -1
  17. package/dist-types/db/schema/2.0.0/consent-policy.d.ts +3 -2
  18. package/dist-types/db/schema/2.0.0/consent-purpose.d.ts +1 -1
  19. package/dist-types/db/schema/2.0.0/consent.d.ts +1 -1
  20. package/dist-types/db/schema/2.0.0/domain.d.ts +1 -1
  21. package/dist-types/db/schema/2.0.0/index.d.ts +7 -0
  22. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +1 -1
  23. package/dist-types/db/schema/2.0.0/subject.d.ts +1 -1
  24. package/dist-types/db/schema/index.d.ts +14 -0
  25. package/dist-types/edge/index.d.ts +2 -2
  26. package/dist-types/edge/init-handler.d.ts +5 -3
  27. package/dist-types/edge/resolve-consent.d.ts +6 -6
  28. package/dist-types/edge/types.d.ts +1 -1
  29. package/dist-types/handlers/init/index.d.ts +4 -4
  30. package/dist-types/handlers/init/policy.d.ts +1 -1
  31. package/dist-types/handlers/init/resolve-init.d.ts +2 -2
  32. package/dist-types/handlers/init/translations.d.ts +1 -1
  33. package/dist-types/handlers/legal-document/current.handler.d.ts +11 -0
  34. package/dist-types/handlers/legal-document/snapshot.d.ts +39 -0
  35. package/dist-types/handlers/subject/get.handler.d.ts +3 -0
  36. package/dist-types/handlers/subject/list.handler.d.ts +3 -0
  37. package/dist-types/handlers/utils/consent-enrichment.d.ts +3 -0
  38. package/dist-types/middleware/cors/is-origin-trusted.d.ts +1 -1
  39. package/dist-types/policies/defaults.d.ts +2 -2
  40. package/dist-types/policies/matchers.d.ts +2 -2
  41. package/dist-types/routes/index.d.ts +1 -0
  42. package/dist-types/routes/legal-document.d.ts +7 -0
  43. package/dist-types/types/index.d.ts +26 -5
  44. package/dist-types/utils/instrumentation.d.ts +2 -2
  45. package/dist-types/utils/logger.d.ts +1 -1
  46. package/dist-types/version.d.ts +1 -1
  47. package/docs/api/configuration.md +13 -2
  48. package/docs/guides/edge-deployment.md +18 -15
  49. package/package.json +6 -6
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'),
@@ -737,7 +738,7 @@ var __webpack_exports__ = {};
737
738
  language
738
739
  };
739
740
  }
740
- const version_version = '2.0.0-rc.6';
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
@@ -2352,6 +2550,94 @@ Use for geo-targeted consent banners and regional compliance.`,
2352
2550
  });
2353
2551
  return app;
2354
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
+ };
2355
2641
  function getHeaders(headers) {
2356
2642
  if (!headers) return {
2357
2643
  countryCode: null,
@@ -2717,6 +3003,79 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2717
3003
  });
2718
3004
  }
2719
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
+ }
2720
3079
  function buildRuntimeDecisionDedupeKey(input) {
2721
3080
  return [
2722
3081
  input.tenantId ?? 'default',
@@ -2796,6 +3155,9 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2796
3155
  if (!firstLanguage) return;
2797
3156
  return firstLanguage.split('-')[0]?.toLowerCase();
2798
3157
  }
3158
+ function isLegalDocumentType(type) {
3159
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
3160
+ }
2799
3161
  function resolveSnapshotFailureMode(ctx) {
2800
3162
  return ctx.policySnapshot?.onValidationFailure ?? 'reject';
2801
3163
  }
@@ -2830,6 +3192,45 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2830
3192
  }
2831
3193
  }
2832
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
+ }
2833
3234
  const postSubjectHandler = async (c)=>{
2834
3235
  const ctx = c.get('c15tContext');
2835
3236
  const logger = ctx.logger;
@@ -2855,16 +3256,30 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2855
3256
  const requestLanguage = parseLanguageFromHeader(acceptLanguage);
2856
3257
  const location = await getLocation(request, ctx);
2857
3258
  const resolvedJurisdiction = getJurisdiction(location, ctx);
2858
- const snapshotVerification = await verifyPolicySnapshotToken({
3259
+ const legalDocumentConsent = isLegalDocumentType(type);
3260
+ const runtimeSnapshotVerification = legalDocumentConsent ? {
3261
+ valid: false,
3262
+ reason: 'missing'
3263
+ } : await verifyPolicySnapshotToken({
2859
3264
  token: input.policySnapshotToken,
2860
3265
  options: ctx.policySnapshot,
2861
3266
  tenantId: ctx.tenantId
2862
3267
  });
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({
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({
2868
3283
  policies: ctx.policyPacks,
2869
3284
  countryCode: location.countryCode,
2870
3285
  regionCode: location.regionCode,
@@ -2921,7 +3336,39 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
2921
3336
  let purposeIds = [];
2922
3337
  let appliedPreferences;
2923
3338
  const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
2924
- 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) {
2925
3372
  policyId = inputPolicyId;
2926
3373
  const policy = await registry.findConsentPolicyById(inputPolicyId);
2927
3374
  if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
@@ -3146,6 +3593,12 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3146
3593
  errorType: error instanceof Error ? error.constructor.name : typeof error
3147
3594
  });
3148
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
+ });
3149
3602
  throw new http_exception_namespaceObject.HTTPException(500, {
3150
3603
  message: 'Internal server error',
3151
3604
  cause: {
@@ -3179,7 +3632,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3179
3632
  }), (0, external_hono_openapi_namespaceObject.validator)('param', schema_.getSubjectInputSchema), getSubjectHandler);
3180
3633
  app.post('/', (0, external_hono_openapi_namespaceObject.describeRoute)({
3181
3634
  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`",
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`",
3183
3636
  tags: [
3184
3637
  'Subject',
3185
3638
  'Consent'
@@ -3417,6 +3870,7 @@ Use for health checks, load balancer probes, and debugging. Performs a lightweig
3417
3870
  }));
3418
3871
  }
3419
3872
  app.route('/init', createInitRoute(options));
3873
+ app.route('/legal-documents', createLegalDocumentRoutes());
3420
3874
  app.route('/subjects', createSubjectRoutes());
3421
3875
  app.route('/consents', createConsentRoutes());
3422
3876
  app.route('/status', createStatusRoute());