@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.
- package/dist/302.js +473 -0
- package/dist/583.js +540 -0
- package/dist/915.js +1742 -0
- package/dist/cache.cjs +1 -1
- package/dist/cache.js +4 -415
- package/dist/core.cjs +484 -33
- package/dist/core.js +21 -2571
- package/dist/db/adapters/drizzle.cjs +1 -1
- package/dist/db/adapters/drizzle.js +1 -2
- package/dist/db/adapters/kysely.cjs +1 -1
- package/dist/db/adapters/kysely.js +1 -2
- package/dist/db/adapters/mongo.cjs +1 -1
- package/dist/db/adapters/mongo.js +1 -2
- package/dist/db/adapters/prisma.cjs +1 -1
- package/dist/db/adapters/prisma.js +1 -2
- package/dist/db/adapters/typeorm.cjs +1 -1
- package/dist/db/adapters/typeorm.js +1 -2
- package/dist/db/adapters.cjs +1 -1
- package/dist/db/migrator.cjs +1 -1
- package/dist/db/schema.cjs +5 -1
- package/dist/db/schema.js +3 -2
- package/dist/define-config.cjs +1 -1
- package/dist/edge.cjs +9 -9
- package/dist/edge.js +6 -885
- package/dist/router.cjs +239 -57
- package/dist/router.js +1 -2058
- package/dist/types/index.cjs +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/runtime-policy-decision.d.ts +1 -1
- package/dist-types/db/registry/types.d.ts +2 -1
- package/dist-types/db/schema/1.0.0/consent.d.ts +2 -2
- package/dist-types/db/schema/2.0.0/audit-log.d.ts +2 -2
- 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 +2 -2
- package/dist-types/db/schema/2.0.0/consent.d.ts +2 -2
- package/dist-types/db/schema/2.0.0/domain.d.ts +2 -2
- 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 +2 -2
- package/dist-types/db/schema/2.0.0/subject.d.ts +2 -2
- 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/policy/snapshot.d.ts +1 -1
- 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/docs/guides/policy-packs.md +1 -1
- package/package.json +19 -19
package/dist/router.cjs
CHANGED
|
@@ -22,7 +22,7 @@ var __webpack_require__ = {};
|
|
|
22
22
|
})();
|
|
23
23
|
(()=>{
|
|
24
24
|
__webpack_require__.r = (exports1)=>{
|
|
25
|
-
if (
|
|
25
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
26
26
|
value: 'Module'
|
|
27
27
|
});
|
|
28
28
|
Object.defineProperty(exports1, '__esModule', {
|
|
@@ -42,7 +42,7 @@ const schema_namespaceObject = require("@c15t/schema");
|
|
|
42
42
|
const external_hono_namespaceObject = require("hono");
|
|
43
43
|
const external_hono_openapi_namespaceObject = require("hono-openapi");
|
|
44
44
|
const http_exception_namespaceObject = require("hono/http-exception");
|
|
45
|
-
function
|
|
45
|
+
function extract_error_message_extractErrorMessage(error) {
|
|
46
46
|
if (error instanceof AggregateError && error.errors?.length > 0) {
|
|
47
47
|
const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
|
|
48
48
|
return `AggregateError: ${inner}`;
|
|
@@ -75,7 +75,7 @@ function getDefaultAttributes() {
|
|
|
75
75
|
const handleSpanError = (span, error)=>{
|
|
76
76
|
span.setStatus({
|
|
77
77
|
code: api_namespaceObject.SpanStatusCode.ERROR,
|
|
78
|
-
message:
|
|
78
|
+
message: extract_error_message_extractErrorMessage(error)
|
|
79
79
|
});
|
|
80
80
|
if (error instanceof Error) span.setAttribute('error.type', error.name);
|
|
81
81
|
};
|
|
@@ -244,7 +244,7 @@ function createMetrics(meter) {
|
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
let metricsInstance = null;
|
|
247
|
-
function
|
|
247
|
+
function metrics_getMetrics(options) {
|
|
248
248
|
if (metricsInstance) return metricsInstance;
|
|
249
249
|
if (!create_telemetry_options_isTelemetryEnabled(options)) return null;
|
|
250
250
|
metricsInstance = createMetrics(getMeter(options));
|
|
@@ -270,7 +270,7 @@ async function batchLoadPolicies(policyIds, ctx) {
|
|
|
270
270
|
for (const p of policyMap.values())uniqueTypes.add(p.type);
|
|
271
271
|
const latestPolicyByType = new Map();
|
|
272
272
|
for (const type of uniqueTypes){
|
|
273
|
-
const latest = await registry.
|
|
273
|
+
const latest = await registry.findLatestPolicyByType(type);
|
|
274
274
|
if (latest) latestPolicyByType.set(type, latest.id);
|
|
275
275
|
}
|
|
276
276
|
return {
|
|
@@ -296,11 +296,17 @@ async function enrichConsents(consents, ctx) {
|
|
|
296
296
|
}
|
|
297
297
|
return consents.map((consent)=>{
|
|
298
298
|
let policyType = 'unknown';
|
|
299
|
+
let policyVersion;
|
|
300
|
+
let policyHash;
|
|
301
|
+
let policyEffectiveDate;
|
|
299
302
|
let isLatestPolicy = false;
|
|
300
303
|
if (consent.policyId) {
|
|
301
304
|
const policy = policyMap.get(consent.policyId);
|
|
302
305
|
if (policy) {
|
|
303
306
|
policyType = policy.type;
|
|
307
|
+
policyVersion = policy.version;
|
|
308
|
+
policyHash = policy.hash ?? void 0;
|
|
309
|
+
policyEffectiveDate = policy.effectiveDate;
|
|
304
310
|
isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
|
|
305
311
|
}
|
|
306
312
|
}
|
|
@@ -317,6 +323,9 @@ async function enrichConsents(consents, ctx) {
|
|
|
317
323
|
id: consent.id,
|
|
318
324
|
type: policyType,
|
|
319
325
|
policyId: consent.policyId ?? void 0,
|
|
326
|
+
policyVersion,
|
|
327
|
+
policyHash,
|
|
328
|
+
policyEffectiveDate,
|
|
320
329
|
isLatestPolicy,
|
|
321
330
|
preferences,
|
|
322
331
|
givenAt: consent.givenAt
|
|
@@ -408,14 +417,14 @@ const checkConsentHandler = async (c)=>{
|
|
|
408
417
|
externalId,
|
|
409
418
|
results
|
|
410
419
|
});
|
|
411
|
-
const metrics =
|
|
420
|
+
const metrics = metrics_getMetrics();
|
|
412
421
|
if (metrics) for (const [type, result] of Object.entries(results))metrics.recordConsentCheck(type, result.hasConsent);
|
|
413
422
|
return c.json({
|
|
414
423
|
results
|
|
415
424
|
});
|
|
416
425
|
} catch (error) {
|
|
417
426
|
logger.error('Error in GET /consents/check handler', {
|
|
418
|
-
error:
|
|
427
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
419
428
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
420
429
|
});
|
|
421
430
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
@@ -431,11 +440,7 @@ const createConsentRoutes = ()=>{
|
|
|
431
440
|
const app = new external_hono_namespaceObject.Hono();
|
|
432
441
|
app.get('/check', (0, external_hono_openapi_namespaceObject.describeRoute)({
|
|
433
442
|
summary: 'Check consent by external user ID',
|
|
434
|
-
description:
|
|
435
|
-
|
|
436
|
-
**Query parameters:**
|
|
437
|
-
- \`externalId\` – External user ID to check
|
|
438
|
-
- \`type\` – Consent type(s) to check (comma-separated)`,
|
|
443
|
+
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)",
|
|
439
444
|
tags: [
|
|
440
445
|
'Consent'
|
|
441
446
|
],
|
|
@@ -596,14 +601,14 @@ async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT
|
|
|
596
601
|
if (!parsed.vendorListVersion || !parsed.purposes || !parsed.vendors) throw new Error('Invalid GVL response: missing required fields');
|
|
597
602
|
return parsed;
|
|
598
603
|
});
|
|
599
|
-
|
|
604
|
+
metrics_getMetrics()?.recordGvlFetch({
|
|
600
605
|
language,
|
|
601
606
|
source: 'fetch',
|
|
602
607
|
status: 200
|
|
603
608
|
}, Date.now() - fetchStart);
|
|
604
609
|
return gvl;
|
|
605
610
|
} catch (error) {
|
|
606
|
-
|
|
611
|
+
metrics_getMetrics()?.recordGvlError({
|
|
607
612
|
language,
|
|
608
613
|
errorType: error instanceof Error ? error.name : 'UnknownError'
|
|
609
614
|
});
|
|
@@ -624,18 +629,18 @@ function createGVLResolver(options) {
|
|
|
624
629
|
if (bundled?.[language]) return bundled[language];
|
|
625
630
|
const memoryHit = await withCacheSpan('get', 'memory', ()=>memoryCache.get(cacheKey));
|
|
626
631
|
if (memoryHit) {
|
|
627
|
-
|
|
632
|
+
metrics_getMetrics()?.recordCacheHit('memory');
|
|
628
633
|
return memoryHit;
|
|
629
634
|
}
|
|
630
|
-
|
|
635
|
+
metrics_getMetrics()?.recordCacheMiss('memory');
|
|
631
636
|
if (cacheAdapter) {
|
|
632
637
|
const externalHit = await withCacheSpan('get', 'external', ()=>cacheAdapter.get(cacheKey));
|
|
633
638
|
if (externalHit) {
|
|
634
|
-
|
|
639
|
+
metrics_getMetrics()?.recordCacheHit('external');
|
|
635
640
|
await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, externalHit, 300000));
|
|
636
641
|
return externalHit;
|
|
637
642
|
}
|
|
638
|
-
|
|
643
|
+
metrics_getMetrics()?.recordCacheMiss('external');
|
|
639
644
|
}
|
|
640
645
|
const gvl = await fetchGVLWithLanguage(language, vendorIds, endpoint);
|
|
641
646
|
if (gvl) {
|
|
@@ -1129,7 +1134,7 @@ async function resolveInitPayload(request, options, logger) {
|
|
|
1129
1134
|
proofConfig: policyDecision.policy.proof
|
|
1130
1135
|
}) : void 0;
|
|
1131
1136
|
const gpc = '1' === request.headers.get('sec-gpc');
|
|
1132
|
-
|
|
1137
|
+
metrics_getMetrics()?.recordInit({
|
|
1133
1138
|
jurisdiction,
|
|
1134
1139
|
country: location?.countryCode ?? void 0,
|
|
1135
1140
|
region: location?.regionCode ?? void 0,
|
|
@@ -1198,7 +1203,16 @@ Use for geo-targeted consent banners and regional compliance.`,
|
|
|
1198
1203
|
});
|
|
1199
1204
|
return app;
|
|
1200
1205
|
};
|
|
1201
|
-
const
|
|
1206
|
+
const external_base_x_namespaceObject = require("base-x");
|
|
1207
|
+
var external_base_x_default = /*#__PURE__*/ __webpack_require__.n(external_base_x_namespaceObject);
|
|
1208
|
+
external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
|
1209
|
+
class consent_policy_LegalDocumentPolicyConflictError extends Error {
|
|
1210
|
+
constructor(message){
|
|
1211
|
+
super(message);
|
|
1212
|
+
this.name = 'LegalDocumentPolicyConflictError';
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const version_version = '2.0.0-rc.8';
|
|
1202
1216
|
function getHeaders(headers) {
|
|
1203
1217
|
if (!headers) return {
|
|
1204
1218
|
countryCode: null,
|
|
@@ -1285,6 +1299,12 @@ const getSubjectHandler = async (c)=>{
|
|
|
1285
1299
|
const subjectId = c.req.param('id');
|
|
1286
1300
|
const type = c.req.query('type');
|
|
1287
1301
|
const typeFilter = type?.split(',').map((t)=>t.trim()) || [];
|
|
1302
|
+
if (!subjectId) throw new http_exception_namespaceObject.HTTPException(400, {
|
|
1303
|
+
message: 'Subject ID is required',
|
|
1304
|
+
cause: {
|
|
1305
|
+
code: 'SUBJECT_ID_REQUIRED'
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1288
1308
|
logger.debug('Request parameters', {
|
|
1289
1309
|
subjectId,
|
|
1290
1310
|
typeFilter
|
|
@@ -1320,7 +1340,7 @@ const getSubjectHandler = async (c)=>{
|
|
|
1320
1340
|
});
|
|
1321
1341
|
} catch (error) {
|
|
1322
1342
|
logger.error('Error in GET /subjects/:id handler', {
|
|
1323
|
-
error:
|
|
1343
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1324
1344
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1325
1345
|
});
|
|
1326
1346
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
@@ -1381,7 +1401,7 @@ const listSubjectsHandler = async (c)=>{
|
|
|
1381
1401
|
});
|
|
1382
1402
|
} catch (error) {
|
|
1383
1403
|
logger.error('Error in GET /subjects handler', {
|
|
1384
|
-
error:
|
|
1404
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1385
1405
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1386
1406
|
});
|
|
1387
1407
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
@@ -1393,9 +1413,7 @@ const listSubjectsHandler = async (c)=>{
|
|
|
1393
1413
|
});
|
|
1394
1414
|
}
|
|
1395
1415
|
};
|
|
1396
|
-
const
|
|
1397
|
-
var external_base_x_default = /*#__PURE__*/ __webpack_require__.n(external_base_x_namespaceObject);
|
|
1398
|
-
const prefixes = {
|
|
1416
|
+
const utils_prefixes = {
|
|
1399
1417
|
auditLog: 'log',
|
|
1400
1418
|
consent: 'cns',
|
|
1401
1419
|
consentPolicy: 'pol',
|
|
@@ -1403,10 +1421,10 @@ const prefixes = {
|
|
|
1403
1421
|
domain: 'dom',
|
|
1404
1422
|
subject: 'sub'
|
|
1405
1423
|
};
|
|
1406
|
-
const
|
|
1407
|
-
function
|
|
1424
|
+
const utils_b58 = external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
|
1425
|
+
function utils_generateId(model) {
|
|
1408
1426
|
const buf = crypto.getRandomValues(new Uint8Array(20));
|
|
1409
|
-
const prefix =
|
|
1427
|
+
const prefix = utils_prefixes[model];
|
|
1410
1428
|
const EPOCH_TIMESTAMP = 1700000000000;
|
|
1411
1429
|
const t = Date.now() - EPOCH_TIMESTAMP;
|
|
1412
1430
|
const high = Math.floor(t / 0x100000000);
|
|
@@ -1419,9 +1437,9 @@ function generateId(model) {
|
|
|
1419
1437
|
buf[5] = low >>> 16 & 255;
|
|
1420
1438
|
buf[6] = low >>> 8 & 255;
|
|
1421
1439
|
buf[7] = 255 & low;
|
|
1422
|
-
return `${prefix}_${
|
|
1440
|
+
return `${prefix}_${utils_b58.encode(buf)}`;
|
|
1423
1441
|
}
|
|
1424
|
-
async function
|
|
1442
|
+
async function utils_generateUniqueId(db, model, ctx, options = {}) {
|
|
1425
1443
|
const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
|
|
1426
1444
|
if (attempt >= maxRetries) {
|
|
1427
1445
|
const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
|
|
@@ -1431,7 +1449,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
1431
1449
|
});
|
|
1432
1450
|
throw error;
|
|
1433
1451
|
}
|
|
1434
|
-
const id =
|
|
1452
|
+
const id = utils_generateId(model);
|
|
1435
1453
|
try {
|
|
1436
1454
|
const existing = await db.findFirst(model, {
|
|
1437
1455
|
where: (b)=>b('id', '=', id)
|
|
@@ -1445,7 +1463,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
1445
1463
|
});
|
|
1446
1464
|
const delay = Math.min(baseDelay * 2 ** attempt, 1000);
|
|
1447
1465
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
1448
|
-
return
|
|
1466
|
+
return utils_generateUniqueId(db, model, ctx, {
|
|
1449
1467
|
maxRetries,
|
|
1450
1468
|
attempt: attempt + 1,
|
|
1451
1469
|
baseDelay
|
|
@@ -1461,7 +1479,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
1461
1479
|
if (attempt < maxRetries - 1) {
|
|
1462
1480
|
const delay = Math.min(baseDelay * 2 ** attempt, 2000);
|
|
1463
1481
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
1464
|
-
return
|
|
1482
|
+
return utils_generateUniqueId(db, model, ctx, {
|
|
1465
1483
|
maxRetries,
|
|
1466
1484
|
attempt: attempt + 1,
|
|
1467
1485
|
baseDelay
|
|
@@ -1478,6 +1496,12 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1478
1496
|
const subjectId = c.req.param('id');
|
|
1479
1497
|
const body = await c.req.json();
|
|
1480
1498
|
const { externalId, identityProvider = 'external' } = body;
|
|
1499
|
+
if (!subjectId) throw new http_exception_namespaceObject.HTTPException(400, {
|
|
1500
|
+
message: 'Subject ID is required',
|
|
1501
|
+
cause: {
|
|
1502
|
+
code: 'SUBJECT_ID_REQUIRED'
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1481
1505
|
logger.debug('Request parameters', {
|
|
1482
1506
|
subjectId,
|
|
1483
1507
|
externalId,
|
|
@@ -1504,7 +1528,7 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1504
1528
|
}
|
|
1505
1529
|
});
|
|
1506
1530
|
await tx.create('auditLog', {
|
|
1507
|
-
id: await
|
|
1531
|
+
id: await utils_generateUniqueId(tx, 'auditLog', ctx),
|
|
1508
1532
|
subjectId,
|
|
1509
1533
|
entityType: 'subject',
|
|
1510
1534
|
entityId: subjectId,
|
|
@@ -1532,7 +1556,7 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1532
1556
|
externalId,
|
|
1533
1557
|
identityProvider
|
|
1534
1558
|
});
|
|
1535
|
-
|
|
1559
|
+
metrics_getMetrics()?.recordSubjectLinked(identityProvider);
|
|
1536
1560
|
return c.json({
|
|
1537
1561
|
success: true,
|
|
1538
1562
|
subject: {
|
|
@@ -1542,7 +1566,7 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1542
1566
|
});
|
|
1543
1567
|
} catch (error) {
|
|
1544
1568
|
logger.error('Error in PATCH /subjects/:id handler', {
|
|
1545
|
-
error:
|
|
1569
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1546
1570
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1547
1571
|
});
|
|
1548
1572
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
@@ -1554,6 +1578,79 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1554
1578
|
});
|
|
1555
1579
|
}
|
|
1556
1580
|
};
|
|
1581
|
+
const DEFAULT_ISSUER = 'c15t';
|
|
1582
|
+
const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
|
|
1583
|
+
function isLegalDocumentPolicyType(type) {
|
|
1584
|
+
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1585
|
+
}
|
|
1586
|
+
function snapshot_resolveSnapshotIssuer(options) {
|
|
1587
|
+
return options?.issuer?.trim() || DEFAULT_ISSUER;
|
|
1588
|
+
}
|
|
1589
|
+
function snapshot_resolveSnapshotAudience(params) {
|
|
1590
|
+
const configuredAudience = params.options?.audience?.trim();
|
|
1591
|
+
if (configuredAudience) return configuredAudience;
|
|
1592
|
+
return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
|
|
1593
|
+
}
|
|
1594
|
+
function snapshot_getSigningKey(secret) {
|
|
1595
|
+
return new TextEncoder().encode(secret);
|
|
1596
|
+
}
|
|
1597
|
+
function isLegalDocumentSnapshotPayload(payload) {
|
|
1598
|
+
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;
|
|
1599
|
+
}
|
|
1600
|
+
async function verifyLegalDocumentSnapshotToken(params) {
|
|
1601
|
+
const { token, options, tenantId } = params;
|
|
1602
|
+
if (!options?.signingKey) return {
|
|
1603
|
+
valid: false,
|
|
1604
|
+
reason: 'missing'
|
|
1605
|
+
};
|
|
1606
|
+
if (!token) return {
|
|
1607
|
+
valid: false,
|
|
1608
|
+
reason: 'missing'
|
|
1609
|
+
};
|
|
1610
|
+
if (3 !== token.split('.').length) return {
|
|
1611
|
+
valid: false,
|
|
1612
|
+
reason: 'malformed'
|
|
1613
|
+
};
|
|
1614
|
+
try {
|
|
1615
|
+
const { payload, protectedHeader } = await (0, external_jose_namespaceObject.jwtVerify)(token, snapshot_getSigningKey(options.signingKey), {
|
|
1616
|
+
issuer: snapshot_resolveSnapshotIssuer(options),
|
|
1617
|
+
audience: snapshot_resolveSnapshotAudience({
|
|
1618
|
+
options,
|
|
1619
|
+
tenantId
|
|
1620
|
+
})
|
|
1621
|
+
});
|
|
1622
|
+
const header = protectedHeader;
|
|
1623
|
+
if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
|
|
1624
|
+
valid: false,
|
|
1625
|
+
reason: 'invalid'
|
|
1626
|
+
};
|
|
1627
|
+
if (!isLegalDocumentSnapshotPayload(payload)) return {
|
|
1628
|
+
valid: false,
|
|
1629
|
+
reason: 'invalid'
|
|
1630
|
+
};
|
|
1631
|
+
if (payload.sub !== payload.hash) return {
|
|
1632
|
+
valid: false,
|
|
1633
|
+
reason: 'invalid'
|
|
1634
|
+
};
|
|
1635
|
+
if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
|
|
1636
|
+
valid: false,
|
|
1637
|
+
reason: 'invalid'
|
|
1638
|
+
};
|
|
1639
|
+
return {
|
|
1640
|
+
valid: true,
|
|
1641
|
+
payload
|
|
1642
|
+
};
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (error instanceof external_jose_namespaceObject.errors.JWTExpired) return {
|
|
1645
|
+
valid: false,
|
|
1646
|
+
reason: 'expired'
|
|
1647
|
+
};
|
|
1648
|
+
return {
|
|
1649
|
+
valid: false,
|
|
1650
|
+
reason: 'invalid'
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1557
1654
|
function buildRuntimeDecisionDedupeKey(input) {
|
|
1558
1655
|
return [
|
|
1559
1656
|
input.tenantId ?? 'default',
|
|
@@ -1633,6 +1730,9 @@ function parseLanguageFromHeader(header) {
|
|
|
1633
1730
|
if (!firstLanguage) return;
|
|
1634
1731
|
return firstLanguage.split('-')[0]?.toLowerCase();
|
|
1635
1732
|
}
|
|
1733
|
+
function isLegalDocumentType(type) {
|
|
1734
|
+
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1735
|
+
}
|
|
1636
1736
|
function resolveSnapshotFailureMode(ctx) {
|
|
1637
1737
|
return ctx.policySnapshot?.onValidationFailure ?? 'reject';
|
|
1638
1738
|
}
|
|
@@ -1667,6 +1767,45 @@ function buildSnapshotHttpException(reason) {
|
|
|
1667
1767
|
}
|
|
1668
1768
|
}
|
|
1669
1769
|
}
|
|
1770
|
+
function buildLegalDocumentSnapshotHttpException(reason) {
|
|
1771
|
+
switch(reason){
|
|
1772
|
+
case 'missing':
|
|
1773
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1774
|
+
message: 'Legal document snapshot token is required',
|
|
1775
|
+
cause: {
|
|
1776
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
case 'expired':
|
|
1780
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1781
|
+
message: 'Legal document snapshot token has expired',
|
|
1782
|
+
cause: {
|
|
1783
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
case 'malformed':
|
|
1787
|
+
case 'invalid':
|
|
1788
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1789
|
+
message: 'Legal document snapshot token is invalid',
|
|
1790
|
+
cause: {
|
|
1791
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
default:
|
|
1795
|
+
{
|
|
1796
|
+
const _exhaustive = reason;
|
|
1797
|
+
throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function buildLegalDocumentProofHttpException(message) {
|
|
1802
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1803
|
+
message,
|
|
1804
|
+
cause: {
|
|
1805
|
+
code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1670
1809
|
const postSubjectHandler = async (c)=>{
|
|
1671
1810
|
const ctx = c.get('c15tContext');
|
|
1672
1811
|
const logger = ctx.logger;
|
|
@@ -1692,16 +1831,30 @@ const postSubjectHandler = async (c)=>{
|
|
|
1692
1831
|
const requestLanguage = parseLanguageFromHeader(acceptLanguage);
|
|
1693
1832
|
const location = await getLocation(request, ctx);
|
|
1694
1833
|
const resolvedJurisdiction = getJurisdiction(location, ctx);
|
|
1695
|
-
const
|
|
1834
|
+
const legalDocumentConsent = isLegalDocumentType(type);
|
|
1835
|
+
const runtimeSnapshotVerification = legalDocumentConsent ? {
|
|
1836
|
+
valid: false,
|
|
1837
|
+
reason: 'missing'
|
|
1838
|
+
} : await verifyPolicySnapshotToken({
|
|
1696
1839
|
token: input.policySnapshotToken,
|
|
1697
1840
|
options: ctx.policySnapshot,
|
|
1698
1841
|
tenantId: ctx.tenantId
|
|
1699
1842
|
});
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1843
|
+
const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
|
|
1844
|
+
token: input.documentSnapshotToken,
|
|
1845
|
+
options: ctx.legalDocumentSnapshot,
|
|
1846
|
+
tenantId: ctx.tenantId
|
|
1847
|
+
}) : {
|
|
1848
|
+
valid: false,
|
|
1849
|
+
reason: 'missing'
|
|
1850
|
+
};
|
|
1851
|
+
const hasValidSnapshot = runtimeSnapshotVerification.valid;
|
|
1852
|
+
const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
|
|
1853
|
+
const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
|
|
1854
|
+
if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
|
|
1855
|
+
const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
|
|
1856
|
+
if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
|
|
1857
|
+
const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await resolvePolicyDecision({
|
|
1705
1858
|
policies: ctx.policyPacks,
|
|
1706
1859
|
countryCode: location.countryCode,
|
|
1707
1860
|
regionCode: location.regionCode,
|
|
@@ -1758,7 +1911,39 @@ const postSubjectHandler = async (c)=>{
|
|
|
1758
1911
|
let purposeIds = [];
|
|
1759
1912
|
let appliedPreferences;
|
|
1760
1913
|
const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
|
|
1761
|
-
if (
|
|
1914
|
+
if (legalDocumentConsent && legalDocumentSnapshotVerification.valid) {
|
|
1915
|
+
if (legalDocumentSnapshotVerification.payload.type !== type) throw buildLegalDocumentSnapshotHttpException('invalid');
|
|
1916
|
+
const effectiveDate = new Date(legalDocumentSnapshotVerification.payload.effectiveDate);
|
|
1917
|
+
if (Number.isNaN(effectiveDate.getTime())) throw buildLegalDocumentSnapshotHttpException('invalid');
|
|
1918
|
+
const documentPolicy = await registry.findOrCreateLegalDocumentPolicy({
|
|
1919
|
+
type,
|
|
1920
|
+
version: legalDocumentSnapshotVerification.payload.version,
|
|
1921
|
+
hash: legalDocumentSnapshotVerification.payload.hash,
|
|
1922
|
+
effectiveDate
|
|
1923
|
+
});
|
|
1924
|
+
policyId = documentPolicy.id;
|
|
1925
|
+
} else if (legalDocumentConsent) {
|
|
1926
|
+
if (!ctx.legalDocumentSnapshot?.signingKey && !inputPolicyId) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId when snapshot verification is disabled');
|
|
1927
|
+
if (!inputPolicyId) throw buildLegalDocumentProofHttpException('Legal document consent requires a valid document snapshot token or policyId');
|
|
1928
|
+
policyId = inputPolicyId;
|
|
1929
|
+
const policy = await registry.findConsentPolicyById(inputPolicyId);
|
|
1930
|
+
if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
|
|
1931
|
+
message: 'Policy not found',
|
|
1932
|
+
cause: {
|
|
1933
|
+
code: 'POLICY_NOT_FOUND',
|
|
1934
|
+
policyId,
|
|
1935
|
+
type
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
if (!policy.isActive) throw new http_exception_namespaceObject.HTTPException(400, {
|
|
1939
|
+
message: 'Policy is inactive',
|
|
1940
|
+
cause: {
|
|
1941
|
+
code: 'POLICY_INACTIVE',
|
|
1942
|
+
policyId,
|
|
1943
|
+
type
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
} else if (inputPolicyId) {
|
|
1762
1947
|
policyId = inputPolicyId;
|
|
1763
1948
|
const policy = await registry.findConsentPolicyById(inputPolicyId);
|
|
1764
1949
|
if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
|
|
@@ -1912,7 +2097,7 @@ const postSubjectHandler = async (c)=>{
|
|
|
1912
2097
|
where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
|
|
1913
2098
|
})) : void 0;
|
|
1914
2099
|
const consentRecord = await tx.create('consent', {
|
|
1915
|
-
id: await
|
|
2100
|
+
id: await utils_generateUniqueId(tx, 'consent', ctx),
|
|
1916
2101
|
subjectId: subject.id,
|
|
1917
2102
|
domainId: domainRecord.id,
|
|
1918
2103
|
policyId,
|
|
@@ -1949,7 +2134,7 @@ const postSubjectHandler = async (c)=>{
|
|
|
1949
2134
|
consent: consentRecord
|
|
1950
2135
|
};
|
|
1951
2136
|
});
|
|
1952
|
-
const metrics =
|
|
2137
|
+
const metrics = metrics_getMetrics();
|
|
1953
2138
|
if (metrics) {
|
|
1954
2139
|
const jurisdiction = effectiveJurisdiction;
|
|
1955
2140
|
metrics.recordConsentCreated({
|
|
@@ -1979,10 +2164,16 @@ const postSubjectHandler = async (c)=>{
|
|
|
1979
2164
|
});
|
|
1980
2165
|
} catch (error) {
|
|
1981
2166
|
logger.error('Error in POST /subjects handler', {
|
|
1982
|
-
error:
|
|
2167
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1983
2168
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1984
2169
|
});
|
|
1985
2170
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
2171
|
+
if (error instanceof consent_policy_LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
|
|
2172
|
+
message: error.message,
|
|
2173
|
+
cause: {
|
|
2174
|
+
code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
1986
2177
|
throw new http_exception_namespaceObject.HTTPException(500, {
|
|
1987
2178
|
message: 'Internal server error',
|
|
1988
2179
|
cause: {
|
|
@@ -1995,11 +2186,7 @@ const createSubjectRoutes = ()=>{
|
|
|
1995
2186
|
const app = new external_hono_namespaceObject.Hono();
|
|
1996
2187
|
app.get('/:id', (0, external_hono_openapi_namespaceObject.describeRoute)({
|
|
1997
2188
|
summary: 'Get subject consent status',
|
|
1998
|
-
description:
|
|
1999
|
-
|
|
2000
|
-
**Query:** \`type\` – Filter by consent type(s), comma-separated (e.g. \`privacy_policy,cookie_banner\`).
|
|
2001
|
-
|
|
2002
|
-
**Response:** \`subject\`, \`consents\` (matching filter), \`isValid\` (valid consent for requested type(s)).`,
|
|
2189
|
+
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)).",
|
|
2003
2190
|
tags: [
|
|
2004
2191
|
'Subject',
|
|
2005
2192
|
'Consent'
|
|
@@ -2020,12 +2207,7 @@ const createSubjectRoutes = ()=>{
|
|
|
2020
2207
|
}), (0, external_hono_openapi_namespaceObject.validator)('param', schema_namespaceObject.getSubjectInputSchema), getSubjectHandler);
|
|
2021
2208
|
app.post('/', (0, external_hono_openapi_namespaceObject.describeRoute)({
|
|
2022
2209
|
summary: 'Record consent for a subject',
|
|
2023
|
-
description:
|
|
2024
|
-
|
|
2025
|
-
**Request body by \`type\`:**
|
|
2026
|
-
- \`cookie_banner\` – Requires \`preferences\` object
|
|
2027
|
-
- \`privacy_policy\`, \`dpa\`, \`terms_and_conditions\` – Optional \`policyId\`
|
|
2028
|
-
- \`marketing_communications\`, \`age_verification\`, \`other\` – Optional \`preferences\``,
|
|
2210
|
+
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`",
|
|
2029
2211
|
tags: [
|
|
2030
2212
|
'Subject',
|
|
2031
2213
|
'Consent'
|