@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/router.cjs
CHANGED
|
@@ -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;
|
|
@@ -592,14 +601,14 @@ async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT
|
|
|
592
601
|
if (!parsed.vendorListVersion || !parsed.purposes || !parsed.vendors) throw new Error('Invalid GVL response: missing required fields');
|
|
593
602
|
return parsed;
|
|
594
603
|
});
|
|
595
|
-
|
|
604
|
+
metrics_getMetrics()?.recordGvlFetch({
|
|
596
605
|
language,
|
|
597
606
|
source: 'fetch',
|
|
598
607
|
status: 200
|
|
599
608
|
}, Date.now() - fetchStart);
|
|
600
609
|
return gvl;
|
|
601
610
|
} catch (error) {
|
|
602
|
-
|
|
611
|
+
metrics_getMetrics()?.recordGvlError({
|
|
603
612
|
language,
|
|
604
613
|
errorType: error instanceof Error ? error.name : 'UnknownError'
|
|
605
614
|
});
|
|
@@ -620,18 +629,18 @@ function createGVLResolver(options) {
|
|
|
620
629
|
if (bundled?.[language]) return bundled[language];
|
|
621
630
|
const memoryHit = await withCacheSpan('get', 'memory', ()=>memoryCache.get(cacheKey));
|
|
622
631
|
if (memoryHit) {
|
|
623
|
-
|
|
632
|
+
metrics_getMetrics()?.recordCacheHit('memory');
|
|
624
633
|
return memoryHit;
|
|
625
634
|
}
|
|
626
|
-
|
|
635
|
+
metrics_getMetrics()?.recordCacheMiss('memory');
|
|
627
636
|
if (cacheAdapter) {
|
|
628
637
|
const externalHit = await withCacheSpan('get', 'external', ()=>cacheAdapter.get(cacheKey));
|
|
629
638
|
if (externalHit) {
|
|
630
|
-
|
|
639
|
+
metrics_getMetrics()?.recordCacheHit('external');
|
|
631
640
|
await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, externalHit, 300000));
|
|
632
641
|
return externalHit;
|
|
633
642
|
}
|
|
634
|
-
|
|
643
|
+
metrics_getMetrics()?.recordCacheMiss('external');
|
|
635
644
|
}
|
|
636
645
|
const gvl = await fetchGVLWithLanguage(language, vendorIds, endpoint);
|
|
637
646
|
if (gvl) {
|
|
@@ -1125,7 +1134,7 @@ async function resolveInitPayload(request, options, logger) {
|
|
|
1125
1134
|
proofConfig: policyDecision.policy.proof
|
|
1126
1135
|
}) : void 0;
|
|
1127
1136
|
const gpc = '1' === request.headers.get('sec-gpc');
|
|
1128
|
-
|
|
1137
|
+
metrics_getMetrics()?.recordInit({
|
|
1129
1138
|
jurisdiction,
|
|
1130
1139
|
country: location?.countryCode ?? void 0,
|
|
1131
1140
|
region: location?.regionCode ?? void 0,
|
|
@@ -1194,7 +1203,15 @@ Use for geo-targeted consent banners and regional compliance.`,
|
|
|
1194
1203
|
});
|
|
1195
1204
|
return app;
|
|
1196
1205
|
};
|
|
1197
|
-
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
|
+
}
|
|
1198
1215
|
function getHeaders(headers) {
|
|
1199
1216
|
if (!headers) return {
|
|
1200
1217
|
countryCode: null,
|
|
@@ -1229,7 +1246,7 @@ const statusHandler = async (c)=>{
|
|
|
1229
1246
|
try {
|
|
1230
1247
|
await ctx.db.findFirst('subject', {});
|
|
1231
1248
|
return c.json({
|
|
1232
|
-
version:
|
|
1249
|
+
version: "2.0.0",
|
|
1233
1250
|
timestamp: new Date(),
|
|
1234
1251
|
client: clientInfo
|
|
1235
1252
|
});
|
|
@@ -1322,7 +1339,7 @@ const getSubjectHandler = async (c)=>{
|
|
|
1322
1339
|
});
|
|
1323
1340
|
} catch (error) {
|
|
1324
1341
|
logger.error('Error in GET /subjects/:id handler', {
|
|
1325
|
-
error:
|
|
1342
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1326
1343
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1327
1344
|
});
|
|
1328
1345
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
@@ -1383,7 +1400,7 @@ const listSubjectsHandler = async (c)=>{
|
|
|
1383
1400
|
});
|
|
1384
1401
|
} catch (error) {
|
|
1385
1402
|
logger.error('Error in GET /subjects handler', {
|
|
1386
|
-
error:
|
|
1403
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1387
1404
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1388
1405
|
});
|
|
1389
1406
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
@@ -1395,9 +1412,7 @@ const listSubjectsHandler = async (c)=>{
|
|
|
1395
1412
|
});
|
|
1396
1413
|
}
|
|
1397
1414
|
};
|
|
1398
|
-
const
|
|
1399
|
-
var external_base_x_default = /*#__PURE__*/ __webpack_require__.n(external_base_x_namespaceObject);
|
|
1400
|
-
const prefixes = {
|
|
1415
|
+
const utils_prefixes = {
|
|
1401
1416
|
auditLog: 'log',
|
|
1402
1417
|
consent: 'cns',
|
|
1403
1418
|
consentPolicy: 'pol',
|
|
@@ -1405,10 +1420,10 @@ const prefixes = {
|
|
|
1405
1420
|
domain: 'dom',
|
|
1406
1421
|
subject: 'sub'
|
|
1407
1422
|
};
|
|
1408
|
-
const
|
|
1409
|
-
function
|
|
1423
|
+
const utils_b58 = external_base_x_default()('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
|
1424
|
+
function utils_generateId(model) {
|
|
1410
1425
|
const buf = crypto.getRandomValues(new Uint8Array(20));
|
|
1411
|
-
const prefix =
|
|
1426
|
+
const prefix = utils_prefixes[model];
|
|
1412
1427
|
const EPOCH_TIMESTAMP = 1700000000000;
|
|
1413
1428
|
const t = Date.now() - EPOCH_TIMESTAMP;
|
|
1414
1429
|
const high = Math.floor(t / 0x100000000);
|
|
@@ -1421,9 +1436,9 @@ function generateId(model) {
|
|
|
1421
1436
|
buf[5] = low >>> 16 & 255;
|
|
1422
1437
|
buf[6] = low >>> 8 & 255;
|
|
1423
1438
|
buf[7] = 255 & low;
|
|
1424
|
-
return `${prefix}_${
|
|
1439
|
+
return `${prefix}_${utils_b58.encode(buf)}`;
|
|
1425
1440
|
}
|
|
1426
|
-
async function
|
|
1441
|
+
async function utils_generateUniqueId(db, model, ctx, options = {}) {
|
|
1427
1442
|
const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
|
|
1428
1443
|
if (attempt >= maxRetries) {
|
|
1429
1444
|
const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
|
|
@@ -1433,7 +1448,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
1433
1448
|
});
|
|
1434
1449
|
throw error;
|
|
1435
1450
|
}
|
|
1436
|
-
const id =
|
|
1451
|
+
const id = utils_generateId(model);
|
|
1437
1452
|
try {
|
|
1438
1453
|
const existing = await db.findFirst(model, {
|
|
1439
1454
|
where: (b)=>b('id', '=', id)
|
|
@@ -1447,7 +1462,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
1447
1462
|
});
|
|
1448
1463
|
const delay = Math.min(baseDelay * 2 ** attempt, 1000);
|
|
1449
1464
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
1450
|
-
return
|
|
1465
|
+
return utils_generateUniqueId(db, model, ctx, {
|
|
1451
1466
|
maxRetries,
|
|
1452
1467
|
attempt: attempt + 1,
|
|
1453
1468
|
baseDelay
|
|
@@ -1463,7 +1478,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
1463
1478
|
if (attempt < maxRetries - 1) {
|
|
1464
1479
|
const delay = Math.min(baseDelay * 2 ** attempt, 2000);
|
|
1465
1480
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
1466
|
-
return
|
|
1481
|
+
return utils_generateUniqueId(db, model, ctx, {
|
|
1467
1482
|
maxRetries,
|
|
1468
1483
|
attempt: attempt + 1,
|
|
1469
1484
|
baseDelay
|
|
@@ -1512,7 +1527,7 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1512
1527
|
}
|
|
1513
1528
|
});
|
|
1514
1529
|
await tx.create('auditLog', {
|
|
1515
|
-
id: await
|
|
1530
|
+
id: await utils_generateUniqueId(tx, 'auditLog', ctx),
|
|
1516
1531
|
subjectId,
|
|
1517
1532
|
entityType: 'subject',
|
|
1518
1533
|
entityId: subjectId,
|
|
@@ -1540,7 +1555,7 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1540
1555
|
externalId,
|
|
1541
1556
|
identityProvider
|
|
1542
1557
|
});
|
|
1543
|
-
|
|
1558
|
+
metrics_getMetrics()?.recordSubjectLinked(identityProvider);
|
|
1544
1559
|
return c.json({
|
|
1545
1560
|
success: true,
|
|
1546
1561
|
subject: {
|
|
@@ -1550,7 +1565,7 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1550
1565
|
});
|
|
1551
1566
|
} catch (error) {
|
|
1552
1567
|
logger.error('Error in PATCH /subjects/:id handler', {
|
|
1553
|
-
error:
|
|
1568
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1554
1569
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1555
1570
|
});
|
|
1556
1571
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
@@ -1562,6 +1577,79 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1562
1577
|
});
|
|
1563
1578
|
}
|
|
1564
1579
|
};
|
|
1580
|
+
const DEFAULT_ISSUER = 'c15t';
|
|
1581
|
+
const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
|
|
1582
|
+
function isLegalDocumentPolicyType(type) {
|
|
1583
|
+
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1584
|
+
}
|
|
1585
|
+
function snapshot_resolveSnapshotIssuer(options) {
|
|
1586
|
+
return options?.issuer?.trim() || DEFAULT_ISSUER;
|
|
1587
|
+
}
|
|
1588
|
+
function snapshot_resolveSnapshotAudience(params) {
|
|
1589
|
+
const configuredAudience = params.options?.audience?.trim();
|
|
1590
|
+
if (configuredAudience) return configuredAudience;
|
|
1591
|
+
return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
|
|
1592
|
+
}
|
|
1593
|
+
function snapshot_getSigningKey(secret) {
|
|
1594
|
+
return new TextEncoder().encode(secret);
|
|
1595
|
+
}
|
|
1596
|
+
function isLegalDocumentSnapshotPayload(payload) {
|
|
1597
|
+
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;
|
|
1598
|
+
}
|
|
1599
|
+
async function verifyLegalDocumentSnapshotToken(params) {
|
|
1600
|
+
const { token, options, tenantId } = params;
|
|
1601
|
+
if (!options?.signingKey) return {
|
|
1602
|
+
valid: false,
|
|
1603
|
+
reason: 'missing'
|
|
1604
|
+
};
|
|
1605
|
+
if (!token) return {
|
|
1606
|
+
valid: false,
|
|
1607
|
+
reason: 'missing'
|
|
1608
|
+
};
|
|
1609
|
+
if (3 !== token.split('.').length) return {
|
|
1610
|
+
valid: false,
|
|
1611
|
+
reason: 'malformed'
|
|
1612
|
+
};
|
|
1613
|
+
try {
|
|
1614
|
+
const { payload, protectedHeader } = await (0, external_jose_namespaceObject.jwtVerify)(token, snapshot_getSigningKey(options.signingKey), {
|
|
1615
|
+
issuer: snapshot_resolveSnapshotIssuer(options),
|
|
1616
|
+
audience: snapshot_resolveSnapshotAudience({
|
|
1617
|
+
options,
|
|
1618
|
+
tenantId
|
|
1619
|
+
})
|
|
1620
|
+
});
|
|
1621
|
+
const header = protectedHeader;
|
|
1622
|
+
if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
|
|
1623
|
+
valid: false,
|
|
1624
|
+
reason: 'invalid'
|
|
1625
|
+
};
|
|
1626
|
+
if (!isLegalDocumentSnapshotPayload(payload)) return {
|
|
1627
|
+
valid: false,
|
|
1628
|
+
reason: 'invalid'
|
|
1629
|
+
};
|
|
1630
|
+
if (payload.sub !== payload.hash) return {
|
|
1631
|
+
valid: false,
|
|
1632
|
+
reason: 'invalid'
|
|
1633
|
+
};
|
|
1634
|
+
if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
|
|
1635
|
+
valid: false,
|
|
1636
|
+
reason: 'invalid'
|
|
1637
|
+
};
|
|
1638
|
+
return {
|
|
1639
|
+
valid: true,
|
|
1640
|
+
payload
|
|
1641
|
+
};
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
if (error instanceof external_jose_namespaceObject.errors.JWTExpired) return {
|
|
1644
|
+
valid: false,
|
|
1645
|
+
reason: 'expired'
|
|
1646
|
+
};
|
|
1647
|
+
return {
|
|
1648
|
+
valid: false,
|
|
1649
|
+
reason: 'invalid'
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1565
1653
|
function buildRuntimeDecisionDedupeKey(input) {
|
|
1566
1654
|
return [
|
|
1567
1655
|
input.tenantId ?? 'default',
|
|
@@ -1641,6 +1729,9 @@ function parseLanguageFromHeader(header) {
|
|
|
1641
1729
|
if (!firstLanguage) return;
|
|
1642
1730
|
return firstLanguage.split('-')[0]?.toLowerCase();
|
|
1643
1731
|
}
|
|
1732
|
+
function isLegalDocumentType(type) {
|
|
1733
|
+
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1734
|
+
}
|
|
1644
1735
|
function resolveSnapshotFailureMode(ctx) {
|
|
1645
1736
|
return ctx.policySnapshot?.onValidationFailure ?? 'reject';
|
|
1646
1737
|
}
|
|
@@ -1675,6 +1766,45 @@ function buildSnapshotHttpException(reason) {
|
|
|
1675
1766
|
}
|
|
1676
1767
|
}
|
|
1677
1768
|
}
|
|
1769
|
+
function buildLegalDocumentSnapshotHttpException(reason) {
|
|
1770
|
+
switch(reason){
|
|
1771
|
+
case 'missing':
|
|
1772
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1773
|
+
message: 'Legal document snapshot token is required',
|
|
1774
|
+
cause: {
|
|
1775
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
case 'expired':
|
|
1779
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1780
|
+
message: 'Legal document snapshot token has expired',
|
|
1781
|
+
cause: {
|
|
1782
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
case 'malformed':
|
|
1786
|
+
case 'invalid':
|
|
1787
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1788
|
+
message: 'Legal document snapshot token is invalid',
|
|
1789
|
+
cause: {
|
|
1790
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
default:
|
|
1794
|
+
{
|
|
1795
|
+
const _exhaustive = reason;
|
|
1796
|
+
throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function buildLegalDocumentProofHttpException(message) {
|
|
1801
|
+
return new http_exception_namespaceObject.HTTPException(409, {
|
|
1802
|
+
message,
|
|
1803
|
+
cause: {
|
|
1804
|
+
code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1678
1808
|
const postSubjectHandler = async (c)=>{
|
|
1679
1809
|
const ctx = c.get('c15tContext');
|
|
1680
1810
|
const logger = ctx.logger;
|
|
@@ -1700,16 +1830,30 @@ const postSubjectHandler = async (c)=>{
|
|
|
1700
1830
|
const requestLanguage = parseLanguageFromHeader(acceptLanguage);
|
|
1701
1831
|
const location = await getLocation(request, ctx);
|
|
1702
1832
|
const resolvedJurisdiction = getJurisdiction(location, ctx);
|
|
1703
|
-
const
|
|
1833
|
+
const legalDocumentConsent = isLegalDocumentType(type);
|
|
1834
|
+
const runtimeSnapshotVerification = legalDocumentConsent ? {
|
|
1835
|
+
valid: false,
|
|
1836
|
+
reason: 'missing'
|
|
1837
|
+
} : await verifyPolicySnapshotToken({
|
|
1704
1838
|
token: input.policySnapshotToken,
|
|
1705
1839
|
options: ctx.policySnapshot,
|
|
1706
1840
|
tenantId: ctx.tenantId
|
|
1707
1841
|
});
|
|
1708
|
-
const
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1842
|
+
const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
|
|
1843
|
+
token: input.documentSnapshotToken,
|
|
1844
|
+
options: ctx.legalDocumentSnapshot,
|
|
1845
|
+
tenantId: ctx.tenantId
|
|
1846
|
+
}) : {
|
|
1847
|
+
valid: false,
|
|
1848
|
+
reason: 'missing'
|
|
1849
|
+
};
|
|
1850
|
+
const hasValidSnapshot = runtimeSnapshotVerification.valid;
|
|
1851
|
+
const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
|
|
1852
|
+
const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
|
|
1853
|
+
if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
|
|
1854
|
+
const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
|
|
1855
|
+
if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
|
|
1856
|
+
const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await resolvePolicyDecision({
|
|
1713
1857
|
policies: ctx.policyPacks,
|
|
1714
1858
|
countryCode: location.countryCode,
|
|
1715
1859
|
regionCode: location.regionCode,
|
|
@@ -1766,7 +1910,61 @@ const postSubjectHandler = async (c)=>{
|
|
|
1766
1910
|
let purposeIds = [];
|
|
1767
1911
|
let appliedPreferences;
|
|
1768
1912
|
const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
|
|
1769
|
-
|
|
1913
|
+
const inputPolicyHash = 'policyHash' in input ? input.policyHash : void 0;
|
|
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 && !inputPolicyHash) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId or policyHash when snapshot verification is disabled');
|
|
1927
|
+
if (inputPolicyId) {
|
|
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 (inputPolicyHash) {
|
|
1947
|
+
const policy = await registry.findLegalDocumentPolicyByHash(type, inputPolicyHash);
|
|
1948
|
+
if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
|
|
1949
|
+
message: 'Policy not found',
|
|
1950
|
+
cause: {
|
|
1951
|
+
code: 'POLICY_NOT_FOUND',
|
|
1952
|
+
type,
|
|
1953
|
+
policyHash: inputPolicyHash
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
if (!policy.isActive) throw new http_exception_namespaceObject.HTTPException(400, {
|
|
1957
|
+
message: 'Policy is inactive',
|
|
1958
|
+
cause: {
|
|
1959
|
+
code: 'POLICY_INACTIVE',
|
|
1960
|
+
policyId: policy.id,
|
|
1961
|
+
type,
|
|
1962
|
+
policyHash: inputPolicyHash
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
policyId = policy.id;
|
|
1966
|
+
}
|
|
1967
|
+
} else if (inputPolicyId) {
|
|
1770
1968
|
policyId = inputPolicyId;
|
|
1771
1969
|
const policy = await registry.findConsentPolicyById(inputPolicyId);
|
|
1772
1970
|
if (!policy) throw new http_exception_namespaceObject.HTTPException(404, {
|
|
@@ -1828,6 +2026,13 @@ const postSubjectHandler = async (c)=>{
|
|
|
1828
2026
|
});
|
|
1829
2027
|
purposeIds = purposes;
|
|
1830
2028
|
}
|
|
2029
|
+
if (!policyId) throw new http_exception_namespaceObject.HTTPException(500, {
|
|
2030
|
+
message: 'Failed to resolve policy',
|
|
2031
|
+
cause: {
|
|
2032
|
+
code: 'POLICY_RESOLUTION_FAILED',
|
|
2033
|
+
type
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
1831
2036
|
const expiryDays = effectivePolicy?.consent?.expiryDays;
|
|
1832
2037
|
const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
|
|
1833
2038
|
const proofConfig = effectivePolicy?.proof;
|
|
@@ -1920,7 +2125,7 @@ const postSubjectHandler = async (c)=>{
|
|
|
1920
2125
|
where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
|
|
1921
2126
|
})) : void 0;
|
|
1922
2127
|
const consentRecord = await tx.create('consent', {
|
|
1923
|
-
id: await
|
|
2128
|
+
id: await utils_generateUniqueId(tx, 'consent', ctx),
|
|
1924
2129
|
subjectId: subject.id,
|
|
1925
2130
|
domainId: domainRecord.id,
|
|
1926
2131
|
policyId,
|
|
@@ -1957,7 +2162,7 @@ const postSubjectHandler = async (c)=>{
|
|
|
1957
2162
|
consent: consentRecord
|
|
1958
2163
|
};
|
|
1959
2164
|
});
|
|
1960
|
-
const metrics =
|
|
2165
|
+
const metrics = metrics_getMetrics();
|
|
1961
2166
|
if (metrics) {
|
|
1962
2167
|
const jurisdiction = effectiveJurisdiction;
|
|
1963
2168
|
metrics.recordConsentCreated({
|
|
@@ -1987,10 +2192,16 @@ const postSubjectHandler = async (c)=>{
|
|
|
1987
2192
|
});
|
|
1988
2193
|
} catch (error) {
|
|
1989
2194
|
logger.error('Error in POST /subjects handler', {
|
|
1990
|
-
error:
|
|
2195
|
+
error: extract_error_message_extractErrorMessage(error),
|
|
1991
2196
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1992
2197
|
});
|
|
1993
2198
|
if (error instanceof http_exception_namespaceObject.HTTPException) throw error;
|
|
2199
|
+
if (error instanceof consent_policy_LegalDocumentPolicyConflictError) throw new http_exception_namespaceObject.HTTPException(409, {
|
|
2200
|
+
message: error.message,
|
|
2201
|
+
cause: {
|
|
2202
|
+
code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
1994
2205
|
throw new http_exception_namespaceObject.HTTPException(500, {
|
|
1995
2206
|
message: 'Internal server error',
|
|
1996
2207
|
cause: {
|
|
@@ -2024,7 +2235,7 @@ const createSubjectRoutes = ()=>{
|
|
|
2024
2235
|
}), (0, external_hono_openapi_namespaceObject.validator)('param', schema_namespaceObject.getSubjectInputSchema), getSubjectHandler);
|
|
2025
2236
|
app.post('/', (0, external_hono_openapi_namespaceObject.describeRoute)({
|
|
2026
2237
|
summary: 'Record consent for a subject',
|
|
2027
|
-
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` –
|
|
2238
|
+
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`",
|
|
2028
2239
|
tags: [
|
|
2029
2240
|
'Subject',
|
|
2030
2241
|
'Consent'
|
package/dist/router.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createConsentRoutes, createInitRoute, createStatusRoute, createSubjectRoutes } from "./
|
|
1
|
+
export { createConsentRoutes, createInitRoute, createStatusRoute, createSubjectRoutes } from "./915.js";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @packageDocumentation
|
|
11
11
|
*/
|
|
12
|
-
import type { GlobalVendorList } from '
|
|
12
|
+
import type { GlobalVendorList } from '@c15t/schema/types';
|
|
13
13
|
import type { CacheAdapter } from './types';
|
|
14
14
|
/**
|
|
15
15
|
* Options for creating a GVL resolver.
|
|
@@ -1,19 +1,75 @@
|
|
|
1
|
-
import type { PolicyType } from '../schema';
|
|
1
|
+
import type { LegalDocumentPolicyType, PolicyType } from '../schema';
|
|
2
2
|
import type { Registry } from './types';
|
|
3
|
+
export interface LegalDocumentPolicyInput {
|
|
4
|
+
type: LegalDocumentPolicyType;
|
|
5
|
+
version: string;
|
|
6
|
+
hash: string;
|
|
7
|
+
effectiveDate: Date;
|
|
8
|
+
}
|
|
9
|
+
export declare class LegalDocumentPolicyConflictError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function buildLegalDocumentPolicyId(input: {
|
|
13
|
+
tenantId?: string;
|
|
14
|
+
type: LegalDocumentPolicyType;
|
|
15
|
+
hash: string;
|
|
16
|
+
}): Promise<string>;
|
|
3
17
|
export declare function policyRegistry({ db, ctx }: Registry): {
|
|
4
18
|
findConsentPolicyById: (policyId: string) => Promise<{
|
|
5
19
|
id: string;
|
|
6
20
|
version: string;
|
|
7
21
|
type: string;
|
|
22
|
+
hash: string | null;
|
|
8
23
|
effectiveDate: Date;
|
|
9
24
|
isActive: boolean;
|
|
10
25
|
createdAt: Date;
|
|
11
26
|
tenantId: string | null;
|
|
12
27
|
} | null>;
|
|
28
|
+
findLatestPolicyByType: (type: PolicyType) => Promise<{
|
|
29
|
+
id: string;
|
|
30
|
+
version: string;
|
|
31
|
+
type: string;
|
|
32
|
+
hash: string | null;
|
|
33
|
+
effectiveDate: Date;
|
|
34
|
+
isActive: boolean;
|
|
35
|
+
createdAt: Date;
|
|
36
|
+
tenantId: string | null;
|
|
37
|
+
} | null>;
|
|
38
|
+
findLegalDocumentPolicyByHash: (type: LegalDocumentPolicyType, hash: string) => Promise<{
|
|
39
|
+
id: string;
|
|
40
|
+
version: string;
|
|
41
|
+
type: string;
|
|
42
|
+
hash: string | null;
|
|
43
|
+
effectiveDate: Date;
|
|
44
|
+
isActive: boolean;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
tenantId: string | null;
|
|
47
|
+
} | null>;
|
|
48
|
+
syncCurrentLegalDocumentPolicy: (input: LegalDocumentPolicyInput) => Promise<{
|
|
49
|
+
id: string;
|
|
50
|
+
version: string;
|
|
51
|
+
type: string;
|
|
52
|
+
hash: string | null;
|
|
53
|
+
effectiveDate: Date;
|
|
54
|
+
isActive: boolean;
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
tenantId: string | null;
|
|
57
|
+
}>;
|
|
58
|
+
findOrCreateLegalDocumentPolicy: (input: LegalDocumentPolicyInput) => Promise<{
|
|
59
|
+
id: string;
|
|
60
|
+
version: string;
|
|
61
|
+
type: string;
|
|
62
|
+
hash: string | null;
|
|
63
|
+
effectiveDate: Date;
|
|
64
|
+
isActive: boolean;
|
|
65
|
+
createdAt: Date;
|
|
66
|
+
tenantId: string | null;
|
|
67
|
+
}>;
|
|
13
68
|
findOrCreatePolicy: (type: PolicyType) => Promise<{
|
|
14
69
|
id: string;
|
|
15
70
|
version: string;
|
|
16
71
|
type: string;
|
|
72
|
+
hash: string | null;
|
|
17
73
|
effectiveDate: Date;
|
|
18
74
|
isActive: boolean;
|
|
19
75
|
createdAt: Date;
|