@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.
- package/dist/302.js +1 -1
- package/dist/{364.js → 915.js} +626 -24
- package/dist/core.cjs +465 -11
- package/dist/core.js +4 -152
- 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 +224 -41
- 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/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 +26 -5
- 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 +13 -2
- package/docs/guides/edge-deployment.md +18 -15
- 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.
|
|
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.
|
|
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
|
|
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
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
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 (
|
|
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` –
|
|
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());
|