@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.
- package/README.md +3 -3
- package/dist/302.js +2 -3
- package/dist/{364.js → 915.js} +656 -25
- package/dist/core.cjs +497 -15
- package/dist/core.js +8 -156
- package/dist/db/schema.cjs +4 -0
- package/dist/db/schema.js +3 -2
- package/dist/edge.cjs +8 -8
- package/dist/edge.js +3 -3
- package/dist/router.cjs +253 -42
- package/dist/router.js +1 -1
- package/dist-types/cache/gvl-resolver.d.ts +1 -1
- package/dist-types/db/registry/consent-policy.d.ts +57 -1
- package/dist-types/db/registry/index.d.ts +43 -1
- package/dist-types/db/registry/types.d.ts +2 -1
- package/dist-types/db/schema/1.0.0/consent.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/audit-log.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/consent-policy.d.ts +3 -2
- package/dist-types/db/schema/2.0.0/consent-purpose.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/consent.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/domain.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/index.d.ts +7 -0
- package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/subject.d.ts +1 -1
- package/dist-types/db/schema/index.d.ts +14 -0
- package/dist-types/edge/index.d.ts +2 -2
- package/dist-types/edge/init-handler.d.ts +5 -3
- package/dist-types/edge/resolve-consent.d.ts +6 -6
- package/dist-types/edge/types.d.ts +1 -1
- package/dist-types/handlers/init/index.d.ts +4 -4
- package/dist-types/handlers/init/policy.d.ts +1 -1
- package/dist-types/handlers/init/resolve-init.d.ts +2 -2
- package/dist-types/handlers/init/translations.d.ts +1 -1
- package/dist-types/handlers/legal-document/current.handler.d.ts +11 -0
- package/dist-types/handlers/legal-document/snapshot.d.ts +39 -0
- package/dist-types/handlers/subject/get.handler.d.ts +3 -0
- package/dist-types/handlers/subject/list.handler.d.ts +3 -0
- package/dist-types/handlers/utils/consent-enrichment.d.ts +3 -0
- package/dist-types/middleware/cors/is-origin-trusted.d.ts +1 -1
- package/dist-types/policies/builder.d.ts +7 -7
- package/dist-types/policies/defaults.d.ts +2 -2
- package/dist-types/policies/matchers.d.ts +2 -2
- package/dist-types/routes/index.d.ts +1 -0
- package/dist-types/routes/legal-document.d.ts +7 -0
- package/dist-types/types/index.d.ts +39 -18
- package/dist-types/utils/instrumentation.d.ts +2 -2
- package/dist-types/utils/logger.d.ts +1 -1
- package/dist-types/version.d.ts +1 -1
- package/docs/api/configuration.md +24 -13
- package/docs/guides/database-setup.md +4 -4
- package/docs/guides/edge-deployment.md +18 -15
- package/docs/guides/iab-tcf.md +4 -4
- package/docs/quickstart.md +9 -9
- 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: ()=>
|
|
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':
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
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
|
-
|
|
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` –
|
|
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:
|
|
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());
|