@c15t/backend 2.1.0 → 2.2.0-canary-20260727202135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +20 -0
  2. package/README.md +25 -12
  3. package/dist/{583.js → 261.js} +1 -1
  4. package/dist/{915.js → 49.js} +202 -123
  5. package/dist/{302.js → 744.js} +4 -3
  6. package/dist/cache.cjs +17 -13
  7. package/dist/cache.js +2 -2
  8. package/dist/core.cjs +223 -138
  9. package/dist/core.js +9 -8
  10. package/dist/db/adapters/drizzle.cjs +9 -5
  11. package/dist/db/adapters/kysely.cjs +9 -5
  12. package/dist/db/adapters/mongo.cjs +9 -5
  13. package/dist/db/adapters/prisma.cjs +9 -5
  14. package/dist/db/adapters/typeorm.cjs +9 -5
  15. package/dist/db/adapters.js +1 -0
  16. package/dist/db/migrator.cjs +12 -8
  17. package/dist/db/schema.cjs +24 -17
  18. package/dist/db/schema.js +2 -2
  19. package/dist/define-config.cjs +12 -8
  20. package/dist/edge.cjs +59 -18
  21. package/dist/edge.js +49 -12
  22. package/dist/router.cjs +229 -124
  23. package/dist/router.js +1 -1
  24. package/dist/types/index.cjs +9 -5
  25. package/dist-types/db/registry/index.d.ts +35 -35
  26. package/dist-types/db/registry/utils/generate-id.d.ts +32 -0
  27. package/dist-types/db/schema/1.0.0/index.d.ts +36 -1096
  28. package/dist-types/db/schema/2.0.0/consent-policy.d.ts +2 -2
  29. package/dist-types/db/schema/2.0.0/index.d.ts +16 -599
  30. package/dist-types/db/schema/index.d.ts +253 -2479
  31. package/dist-types/handlers/init/index.d.ts +1 -1
  32. package/dist-types/handlers/subject/consent-idempotency.d.ts +87 -0
  33. package/dist-types/handlers/subject/consent-time.d.ts +12 -0
  34. package/dist-types/handlers/subject/get.handler.d.ts +1 -3
  35. package/dist-types/handlers/subject/list.handler.d.ts +1 -3
  36. package/dist-types/handlers/subject/post.handler.d.ts +5 -1
  37. package/dist-types/middleware/cors/is-origin-trusted.d.ts +0 -11
  38. package/dist-types/middleware/openapi/config.d.ts +6 -6
  39. package/dist-types/version.d.ts +1 -1
  40. package/docs/README.md +15 -44
  41. package/docs/self-host/api/configuration.md +98 -0
  42. package/docs/{api → self-host/api}/endpoints.md +3 -2
  43. package/docs/{guides → self-host/guides}/caching.md +3 -1
  44. package/docs/{guides → self-host/guides}/database-setup.md +2 -6
  45. package/docs/{guides → self-host/guides}/edge-deployment.md +3 -2
  46. package/docs/{guides → self-host/guides}/framework-integration.md +1 -0
  47. package/docs/{guides → self-host/guides}/iab-tcf.md +6 -4
  48. package/docs/{guides → self-host/guides}/observability.md +1 -0
  49. package/docs/{guides → self-host/guides}/policy-packs.md +3 -1
  50. package/docs/{quickstart.md → self-host/quickstart.md} +10 -21
  51. package/package.json +24 -19
  52. package/readme.json +13 -7
  53. package/docs/api/configuration.md +0 -208
  54. /package/dist/{942.js → 292.js} +0 -0
package/AGENTS.md ADDED
@@ -0,0 +1,20 @@
1
+ # @c15t/backend
2
+
3
+ > Self-hosted c15t backend docs for configuration, APIs, storage, policy packs, and operations.
4
+
5
+ These docs ship inside the package so coding agents can read them offline. Open the topic file you need from the list below — paths are relative to this file.
6
+
7
+ ## Self Host
8
+
9
+ Run the c15t backend, configure storage, and operate consent infrastructure.
10
+
11
+ - [Configuration Reference](./docs/self-host/api/configuration.md): Complete reference for all c15t backend configuration options.
12
+ - [API Endpoints](./docs/self-host/api/endpoints.md): Full reference for every c15t consent backend endpoint.
13
+ - [Caching](./docs/self-host/guides/caching.md): Add a caching layer to your self-hosted c15t backend for production performance.
14
+ - [Database Setup](./docs/self-host/guides/database-setup.md): Configure a database adapter for your self-hosted c15t backend.
15
+ - [Edge Deployment](./docs/self-host/guides/edge-deployment.md): Run consent policy resolution at the edge for faster initial banner loads.
16
+ - [Framework Integration](./docs/self-host/guides/framework-integration.md): Mount the c15t consent backend in any JavaScript framework or runtime.
17
+ - [IAB TCF Support](./docs/self-host/guides/iab-tcf.md): Enable IAB Transparency and Consent Framework (TCF) support in your self-hosted backend.
18
+ - [Observability](./docs/self-host/guides/observability.md): Add logging, metrics, and tracing to your self-hosted c15t backend.
19
+ - [Policy Packs](./docs/self-host/guides/policy-packs.md): Configure regional policy packs on the backend and return deterministic runtime policy decisions from /init.
20
+ - [Quickstart](./docs/self-host/quickstart.md): Self-host the c15t consent management backend in your own infrastructure.
package/README.md CHANGED
@@ -9,25 +9,27 @@
9
9
 
10
10
  # @c15t/backend: Consent Management Backend
11
11
 
12
- [![GitHub stars](https://img.shields.io/github/stars/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t)
13
- [![CI](https://img.shields.io/github/actions/workflow/status/c15t/c15t/ci.yml?style=flat-square)](https://github.com/c15t/c15t/actions/workflows/ci.yml)
14
- [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](https://github.com/c15t/c15t/blob/main/LICENSE.md)
15
- [![Discord](https://img.shields.io/discord/1312171102268690493?style=flat-square)](https://c15t.link/discord)
16
- [![npm version](https://img.shields.io/npm/v/%40c15t%2Fbackend?style=flat-square)](https://www.npmjs.com/package/@c15t/backend)
17
- [![Top Language](https://img.shields.io/github/languages/top/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t)
18
- [![Last Commit](https://img.shields.io/github/last-commit/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t/commits/main)
19
- [![Open Issues](https://img.shields.io/github/issues/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t/issues)
12
+ <p>
13
+ <a href="https://www.npmjs.com/package/@c15t/backend"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/npm/%40c15t%2Fbackend.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/npm/%40c15t%2Fbackend.svg?variant=outline&mode=light" alt="Latest NPM Version"></picture></a>
14
+ <a href="https://github.com/c15t/c15t"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/c15t/c15t/stars.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/github/c15t/c15t/stars.svg?variant=outline&mode=light" alt="Stars"></picture></a>
15
+ <a href="https://github.com/c15t/c15t/blob/main/LICENSE.md"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/c15t/c15t/license.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/github/c15t/c15t/license.svg?variant=outline&mode=light" alt="License"></picture></a>
16
+ <a href="https://c15t.link/discord"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/discord/1312171102268690493.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/discord/1312171102268690493.svg?variant=outline&mode=light" alt="Discord"></picture></a>
17
+ <a href="https://skills.sh/c15t/skills/c15t"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/skills/c15t/skills/c15t.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/skills/c15t/skills/c15t.svg?variant=outline&mode=light" alt="Skills"></picture></a>
18
+ <a href="https://inth.com?utm_source=npm&utm_medium=readme&utm_campaign=oss_readme&utm_content=%40c15t%2Fbackend"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/badge/Made%20By-Inth-ffc803.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAzOTMgNDAwIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTgyLjY2MiAwdjM2Ljg5NWgtNTkuMDMxdjgyLjczM2g1OS4wMzF2MzYuODkzSDI3LjQ4MnYtMzYuODkzaDU5LjAzVjM2Ljg5NWgtNTkuMDNWMHpNMzIxLjk0MSA4OS44NVYwaDM1LjM1NXYxNTYuNTIxaC0yNS43MTNsLTg2LjEzNy05MC4zNjR2OTAuMzY0aC0zNS4zNTVWMGgyNi4zNTV6Ii8%2BPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzE4LjU3MSAxODUuNzE0aDc0LjI4NlY0MDBIMFYxODUuNzE0aDI3Mi44NTd2LTQ3LjE0M3ptLTI5MS4wOSAyOC45Njl2MzcuMTE4aDU4LjEzN3YxMTkuNjI4aDM2Ljg5NVYyNTEuODAxaDU4LjU4NHYtMzcuMTE4em0xODIuNjEuMjI0djE1Ni41MjJoMzYuODk0VjMxMy41OWg3My4zNDF2NTcuODM5aDM3LjExOFYyMTQuOTA3aC0zNy4xMTh2NjEuNzg4aC03My4zNDF2LTYxLjc4OHoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg%3D%3D&color=ffc803&labelTextColor=000000&valueColor=000000&mode=dark"><img src="https://shieldcn.dev/badge/Made%20By-Inth-ffc803.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAzOTMgNDAwIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTgyLjY2MiAwdjM2Ljg5NWgtNTkuMDMxdjgyLjczM2g1OS4wMzF2MzYuODkzSDI3LjQ4MnYtMzYuODkzaDU5LjAzVjM2Ljg5NWgtNTkuMDNWMHpNMzIxLjk0MSA4OS44NVYwaDM1LjM1NXYxNTYuNTIxaC0yNS43MTNsLTg2LjEzNy05MC4zNjR2OTAuMzY0aC0zNS4zNTVWMGgyNi4zNTV6Ii8%2BPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzE4LjU3MSAxODUuNzE0aDc0LjI4NlY0MDBIMFYxODUuNzE0aDI3Mi44NTd2LTQ3LjE0M3ptLTI5MS4wOSAyOC45Njl2MzcuMTE4aDU4LjEzN3YxMTkuNjI4aDM2Ljg5NVYyNTEuODAxaDU4LjU4NHYtMzcuMTE4em0xODIuNjEuMjI0djE1Ni41MjJoMzYuODk0VjMxMy41OWg3My4zNDF2NTcuODM5aDM3LjExOFYyMTQuOTA3aC0zNy4xMTh2NjEuNzg4aC03My4zNDF2LTYxLjc4OHoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg%3D%3D&color=ffc803&labelTextColor=000000&valueColor=000000&mode=light" alt="Made by Inth"></picture></a>
19
+ </p>
20
20
 
21
- Consent policy engine and API for c15t. Powers the cookie banner, consent manager, and preference center. Webhooks, audit logs, storage adapters. Self-host or use inth.com
21
+ Self-hostable consent management backend for c15t. Powers cookie banners, consent managers, preference centers, policy resolution, audit logs, and consent records.
22
22
 
23
23
  ## Key Features
24
24
 
25
- - Consent Management: Track and manage user consent preferences
26
- - Geo-Location: Identify user's location to show relevant consent preferences
25
+ - Self-hosted CMP backend: Run c15t on your own infrastructure with your own database
26
+ - Consent Management: Track and manage user consent preferences and consent records
27
+ - Geo-Location: Identify a user's location to show relevant consent preferences
27
28
  - Server-Side Translation: Reduce banner bundle size by translating consent preferences on the server
28
29
  - Audit Logging: Comprehensive logging of all consent-related actions
29
30
  - Domain Management: Handle multiple domains and subdomains
30
31
  - Policy Management: Version and manage consent policies
32
+ - IAB TCF 2.3 support: Configure GVL, CMP registration, and custom vendors
31
33
 
32
34
  ## Prerequisites
33
35
 
@@ -48,7 +50,18 @@ Configure your database adapter (supports Drizzle, Kysely, Prisma, TypeORM, and
48
50
 
49
51
  ## Documentation
50
52
 
51
- For further information, guides, and examples visit the [reference documentation](https://c15t.com/docs/self-host/v2).
53
+ For further information, guides, and examples visit the [reference documentation](https://c15t.com/docs/self-host/quickstart).
54
+
55
+ ## Hosted or Self-Hosted
56
+
57
+ Use [inth.com](https://inth.com) when you want a hosted c15t backend with no infrastructure to maintain. Use @c15t/backend when you need full control over hosting, database, CORS origins, policy configuration, audit logs, and deployment topology.
58
+
59
+ ## Related Packages
60
+
61
+ - [c15t](https://www.npmjs.com/package/c15t): Headless JavaScript consent engine
62
+ - [@c15t/react](https://www.npmjs.com/package/@c15t/react): React consent UI and headless hooks
63
+ - [@c15t/nextjs](https://www.npmjs.com/package/@c15t/nextjs): Next.js App Router and Pages Router integration
64
+ - [@c15t/node-sdk](https://www.npmjs.com/package/@c15t/node-sdk): Type-safe Node.js API client
52
65
 
53
66
  ## Support
54
67
 
@@ -2,7 +2,7 @@ import { inspectPolicies, resolvePolicyDecision, resolvePolicySync, validatePoli
2
2
  import { deepMergeTranslations, selectLanguage } from "@c15t/translations";
3
3
  import { baseTranslations } from "@c15t/translations/all";
4
4
  import { SignJWT, errors, jwtVerify } from "jose";
5
- import { createGVLResolver, getMetrics } from "./302.js";
5
+ import { createGVLResolver, getMetrics } from "./744.js";
6
6
  const DEFAULT_PROFILE = 'default';
7
7
  const warnedKeys = new Set();
8
8
  function isSupportedBaseLanguage(lang) {
@@ -1,13 +1,12 @@
1
1
  import { hashSha256Hex } from "@c15t/schema/types";
2
2
  import base_x from "base-x";
3
- import { checkConsentOutputSchema, checkConsentQuerySchema, getSubjectInputSchema, getSubjectOutputSchema, initOutputSchema, legalDocumentCurrentInputSchema, legalDocumentCurrentOutputSchema, legalDocumentCurrentParamsSchema, listSubjectsOutputSchema, listSubjectsQuerySchema, patchSubjectOutputSchema, postSubjectInputSchema, postSubjectOutputSchema, statusOutputSchema, subjectIdSchema } from "@c15t/schema";
3
+ import { checkConsentOutputSchema, checkConsentQuerySchema, getSubjectOutputSchema, getSubjectParamsSchema, getSubjectQuerySchema, initOutputSchema, isLegalDocumentType, legalDocumentCurrentInputSchema, legalDocumentCurrentOutputSchema, legalDocumentCurrentParamsSchema, listSubjectsOutputSchema, listSubjectsQuerySchema, patchSubjectInputSchema, patchSubjectOutputSchema, patchSubjectParamsSchema, postSubjectInputSchema, postSubjectOutputSchema, statusOutputSchema } from "@c15t/schema";
4
4
  import { Hono } from "hono";
5
5
  import { describeRoute, resolver, validator } from "hono-openapi";
6
6
  import { HTTPException } from "hono/http-exception";
7
7
  import { errors, jwtVerify } from "jose";
8
- import { extractErrorMessage, getMetrics, withDatabaseSpan } from "./302.js";
9
- import { getLocation, resolveInitPayload, policy_resolvePolicyDecision, verifyPolicySnapshotToken, getJurisdiction } from "./583.js";
10
- import * as __rspack_external_valibot from "valibot";
8
+ import { version, getMetrics, withDatabaseSpan, extractErrorMessage } from "./744.js";
9
+ import { getLocation, resolveInitPayload, policy_resolvePolicyDecision, verifyPolicySnapshotToken, getJurisdiction } from "./261.js";
11
10
  const prefixes = {
12
11
  auditLog: 'log',
13
12
  consent: 'cns',
@@ -18,11 +17,12 @@ const prefixes = {
18
17
  subject: 'sub'
19
18
  };
20
19
  const b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
21
- function generateId(model) {
22
- const buf = crypto.getRandomValues(new Uint8Array(20));
23
- const prefix = prefixes[model];
24
- const EPOCH_TIMESTAMP = 1700000000000;
25
- const t = Date.now() - EPOCH_TIMESTAMP;
20
+ const generate_id_EPOCH_TIMESTAMP = 1700000000000;
21
+ const ID_BYTE_LENGTH = 20;
22
+ const TIMESTAMP_BYTE_LENGTH = 8;
23
+ function writeTimestamp(buf, timestamp) {
24
+ const offset = timestamp - generate_id_EPOCH_TIMESTAMP;
25
+ const t = Number.isFinite(offset) ? Math.max(0, offset) : 0;
26
26
  const high = Math.floor(t / 0x100000000);
27
27
  const low = t >>> 0;
28
28
  buf[0] = high >>> 24 & 255;
@@ -33,7 +33,21 @@ function generateId(model) {
33
33
  buf[5] = low >>> 16 & 255;
34
34
  buf[6] = low >>> 8 & 255;
35
35
  buf[7] = 255 & low;
36
- return `${prefix}_${b58.encode(buf)}`;
36
+ }
37
+ function generateId(model) {
38
+ const buf = crypto.getRandomValues(new Uint8Array(ID_BYTE_LENGTH));
39
+ writeTimestamp(buf, Date.now());
40
+ return `${prefixes[model]}_${b58.encode(buf)}`;
41
+ }
42
+ async function generateDeterministicId(model, timestamp, identity) {
43
+ const digest = await hashSha256Hex(JSON.stringify(identity));
44
+ const buf = new Uint8Array(ID_BYTE_LENGTH);
45
+ writeTimestamp(buf, timestamp);
46
+ for(let i = TIMESTAMP_BYTE_LENGTH; i < ID_BYTE_LENGTH; i++){
47
+ const offset = (i - TIMESTAMP_BYTE_LENGTH) * 2;
48
+ buf[i] = Number.parseInt(digest.slice(offset, offset + 2), 16);
49
+ }
50
+ return `${prefixes[model]}_${b58.encode(buf)}`;
37
51
  }
38
52
  async function generateUniqueId(db, model, ctx, options = {}) {
39
53
  const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
@@ -709,7 +723,7 @@ const statusHandler = async (c)=>{
709
723
  try {
710
724
  await ctx.db.findFirst('subject', {});
711
725
  return c.json({
712
- version: "2.1.0",
726
+ version: version,
713
727
  timestamp: new Date(),
714
728
  client: clientInfo
715
729
  });
@@ -1041,9 +1055,6 @@ const patchSubjectHandler = async (c)=>{
1041
1055
  };
1042
1056
  const DEFAULT_ISSUER = 'c15t';
1043
1057
  const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
1044
- function isLegalDocumentPolicyType(type) {
1045
- return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1046
- }
1047
1058
  function resolveSnapshotIssuer(options) {
1048
1059
  return options?.issuer?.trim() || DEFAULT_ISSUER;
1049
1060
  }
@@ -1056,7 +1067,7 @@ function getSigningKey(secret) {
1056
1067
  return new TextEncoder().encode(secret);
1057
1068
  }
1058
1069
  function isLegalDocumentSnapshotPayload(payload) {
1059
- 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;
1070
+ return 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub && isLegalDocumentType(payload.type) && 'string' == typeof payload.version && 'string' == typeof payload.hash && 'string' == typeof payload.effectiveDate && 'number' == typeof payload.iat && 'number' == typeof payload.exp;
1060
1071
  }
1061
1072
  async function verifyLegalDocumentSnapshotToken(params) {
1062
1073
  const { token, options, tenantId } = params;
@@ -1112,6 +1123,38 @@ async function verifyLegalDocumentSnapshotToken(params) {
1112
1123
  };
1113
1124
  }
1114
1125
  }
1126
+ function buildConsentId(input) {
1127
+ return generateDeterministicId('consent', input.givenAt.getTime(), [
1128
+ input.tenantId ?? null,
1129
+ input.subjectId,
1130
+ input.domainId,
1131
+ input.policyId ?? null,
1132
+ input.givenAt.toISOString()
1133
+ ]);
1134
+ }
1135
+ function findConsentById(database, consentId) {
1136
+ return database.findFirst('consent', {
1137
+ where: (builder)=>builder('id', '=', consentId)
1138
+ });
1139
+ }
1140
+ async function findExistingConsentSubmission(database, consentId, identity) {
1141
+ const deterministicConsent = await findConsentById(database, consentId);
1142
+ if (deterministicConsent) return deterministicConsent;
1143
+ return database.findFirst('consent', {
1144
+ where: (builder)=>builder.and(builder('subjectId', '=', identity.subjectId), builder('domainId', '=', identity.domainId), builder('policyId', '=', identity.policyId ?? null), builder('givenAt', '=', identity.givenAt), void 0 === identity.tenantId ? builder.isNull('tenantId') : builder('tenantId', '=', identity.tenantId))
1145
+ });
1146
+ }
1147
+ function isUniqueConstraintViolationError(error) {
1148
+ const code = 'object' == typeof error && null !== error && 'code' in error ? String(error.code) : void 0;
1149
+ if ('23505' === code || 'ER_DUP_ENTRY' === code || '1062' === code || 'P2002' === code || code?.startsWith('SQLITE_CONSTRAINT')) return true;
1150
+ const message = extractErrorMessage(error).toLowerCase();
1151
+ return message.includes('unique constraint') || message.includes('unique violation') || message.includes('duplicate key') || message.includes('duplicate entry') || message.includes('unique conflict');
1152
+ }
1153
+ const MAX_FUTURE_CONSENT_TIME_DRIFT_MS = 300000;
1154
+ function clampConsentGivenAt(givenAt, now = Date.now()) {
1155
+ if (givenAt.getTime() > now + MAX_FUTURE_CONSENT_TIME_DRIFT_MS) return new Date(now);
1156
+ return givenAt;
1157
+ }
1115
1158
  function buildRuntimeDecisionDedupeKey(input) {
1116
1159
  return [
1117
1160
  input.tenantId ?? 'default',
@@ -1191,9 +1234,6 @@ function parseLanguageFromHeader(header) {
1191
1234
  if (!firstLanguage) return;
1192
1235
  return firstLanguage.split('-')[0]?.toLowerCase();
1193
1236
  }
1194
- function isLegalDocumentType(type) {
1195
- return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1196
- }
1197
1237
  function resolveSnapshotFailureMode(ctx) {
1198
1238
  return ctx.policySnapshot?.onValidationFailure ?? 'reject';
1199
1239
  }
@@ -1275,7 +1315,13 @@ const postSubjectHandler = async (c)=>{
1275
1315
  const input = await c.req.json();
1276
1316
  const { type, subjectId, identityProvider, externalSubjectId, domain, metadata, givenAt: givenAtEpoch } = input;
1277
1317
  const preferences = 'preferences' in input ? input.preferences : void 0;
1278
- const givenAt = new Date(givenAtEpoch);
1318
+ const requestedGivenAt = new Date(givenAtEpoch);
1319
+ const givenAt = clampConsentGivenAt(requestedGivenAt);
1320
+ const wasGivenAtClamped = givenAt.getTime() !== requestedGivenAt.getTime();
1321
+ if (wasGivenAtClamped) logger.warn('Consent givenAt was too far in the future and was clamped to server time', {
1322
+ requestedGivenAt: requestedGivenAt.toISOString(),
1323
+ clampedGivenAt: givenAt.toISOString()
1324
+ });
1279
1325
  const rawConsentAction = 'consentAction' in input ? input.consentAction : void 0;
1280
1326
  let derivedConsentAction;
1281
1327
  logger.debug('Request parameters', {
@@ -1510,6 +1556,9 @@ const postSubjectHandler = async (c)=>{
1510
1556
  ...metadata ?? {},
1511
1557
  ...shouldStoreLanguage && effectiveLanguage ? {
1512
1558
  policyLanguage: effectiveLanguage
1559
+ } : {},
1560
+ ...wasGivenAtClamped ? {
1561
+ clientGivenAt: requestedGivenAt.toISOString()
1513
1562
  } : {}
1514
1563
  };
1515
1564
  const effectiveJurisdiction = hasValidSnapshot && snapshotPayload ? snapshotPayload.jurisdiction : resolvedJurisdiction;
@@ -1528,9 +1577,15 @@ const postSubjectHandler = async (c)=>{
1528
1577
  language: effectiveLanguage,
1529
1578
  proofConfig
1530
1579
  });
1531
- const existingConsent = await db.findFirst('consent', {
1532
- where: (b)=>b.and(b('subjectId', '=', subject.id), b('domainId', '=', domainRecord.id), b('policyId', '=', policyId), b('givenAt', '=', givenAt))
1533
- });
1580
+ const consentIdentity = {
1581
+ tenantId: ctx.tenantId,
1582
+ subjectId: subject.id,
1583
+ domainId: domainRecord.id,
1584
+ policyId,
1585
+ givenAt: requestedGivenAt
1586
+ };
1587
+ const consentId = await buildConsentId(consentIdentity);
1588
+ const existingConsent = await findExistingConsentSubmission(db, consentId, consentIdentity);
1534
1589
  if (existingConsent) {
1535
1590
  logger.debug('Duplicate consent detected, returning existing record', {
1536
1591
  consentId: existingConsent.id
@@ -1547,103 +1602,132 @@ const postSubjectHandler = async (c)=>{
1547
1602
  givenAt: existingConsent.givenAt
1548
1603
  });
1549
1604
  }
1550
- const result = await db.transaction(async (tx)=>{
1551
- logger.debug('Creating consent record', {
1552
- subjectId: subject.id,
1553
- domainId: domainRecord.id,
1554
- policyId,
1555
- purposeIds
1556
- });
1557
- const runtimePolicyDecision = decisionPayload ? await tx.findFirst('runtimePolicyDecision', {
1558
- where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1559
- }) ?? await tx.create('runtimePolicyDecision', {
1560
- id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
1561
- tenantId: decisionPayload.tenantId,
1562
- policyId: decisionPayload.policyId,
1563
- fingerprint: decisionPayload.fingerprint,
1564
- matchedBy: decisionPayload.matchedBy,
1565
- countryCode: decisionPayload.countryCode,
1566
- regionCode: decisionPayload.regionCode,
1567
- jurisdiction: decisionPayload.jurisdiction,
1568
- language: decisionPayload.language,
1569
- model: decisionPayload.model,
1570
- policyI18n: decisionPayload.policyI18n ? {
1571
- json: decisionPayload.policyI18n
1572
- } : void 0,
1573
- uiMode: decisionPayload.uiMode,
1574
- bannerUi: decisionPayload.bannerUi ? {
1575
- json: decisionPayload.bannerUi
1576
- } : void 0,
1577
- dialogUi: decisionPayload.dialogUi ? {
1578
- json: decisionPayload.dialogUi
1579
- } : void 0,
1580
- categories: decisionPayload.categories ? {
1581
- json: decisionPayload.categories
1582
- } : void 0,
1583
- preselectedCategories: decisionPayload.preselectedCategories ? {
1584
- json: decisionPayload.preselectedCategories
1585
- } : void 0,
1586
- proofConfig: decisionPayload.proofConfig ? {
1587
- json: decisionPayload.proofConfig
1588
- } : void 0,
1589
- dedupeKey: decisionPayload.dedupeKey
1590
- }).catch(async ()=>tx.findFirst('runtimePolicyDecision', {
1605
+ const runConsentTransaction = ()=>db.transaction(async (tx)=>{
1606
+ logger.debug('Creating consent record', {
1607
+ subjectId: subject.id,
1608
+ domainId: domainRecord.id,
1609
+ policyId,
1610
+ purposeIds
1611
+ });
1612
+ const runtimePolicyDecision = decisionPayload ? await tx.findFirst('runtimePolicyDecision', {
1591
1613
  where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1592
- })) : void 0;
1593
- const consentRecord = await tx.create('consent', {
1594
- id: await utils_generateUniqueId(tx, 'consent', ctx),
1595
- subjectId: subject.id,
1596
- domainId: domainRecord.id,
1597
- policyId,
1598
- purposeIds: {
1599
- json: purposeIds
1600
- },
1601
- metadata: Object.keys(metadataWithPolicy).length > 0 ? {
1602
- json: metadataWithPolicy
1603
- } : void 0,
1604
- ipAddress: shouldStoreIp ? ctx.ipAddress : null,
1605
- userAgent: shouldStoreUserAgent ? ctx.userAgent : null,
1606
- jurisdiction: effectiveJurisdiction,
1607
- jurisdictionModel: effectiveModel,
1608
- tcString: input.tcString,
1609
- uiSource: input.uiSource,
1610
- consentAction: derivedConsentAction,
1611
- givenAt,
1612
- validUntil,
1613
- runtimePolicyDecisionId: runtimePolicyDecision?.id,
1614
- runtimePolicySource: decisionPayload?.source
1615
- });
1616
- logger.debug('Created consent', {
1617
- consentRecord: consentRecord.id
1618
- });
1619
- if (!consentRecord) throw new HTTPException(500, {
1620
- message: 'Failed to create consent',
1621
- cause: {
1622
- code: 'CONSENT_CREATION_FAILED',
1614
+ }) ?? await tx.create('runtimePolicyDecision', {
1615
+ id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
1616
+ tenantId: decisionPayload.tenantId,
1617
+ policyId: decisionPayload.policyId,
1618
+ fingerprint: decisionPayload.fingerprint,
1619
+ matchedBy: decisionPayload.matchedBy,
1620
+ countryCode: decisionPayload.countryCode,
1621
+ regionCode: decisionPayload.regionCode,
1622
+ jurisdiction: decisionPayload.jurisdiction,
1623
+ language: decisionPayload.language,
1624
+ model: decisionPayload.model,
1625
+ policyI18n: decisionPayload.policyI18n ? {
1626
+ json: decisionPayload.policyI18n
1627
+ } : void 0,
1628
+ uiMode: decisionPayload.uiMode,
1629
+ bannerUi: decisionPayload.bannerUi ? {
1630
+ json: decisionPayload.bannerUi
1631
+ } : void 0,
1632
+ dialogUi: decisionPayload.dialogUi ? {
1633
+ json: decisionPayload.dialogUi
1634
+ } : void 0,
1635
+ categories: decisionPayload.categories ? {
1636
+ json: decisionPayload.categories
1637
+ } : void 0,
1638
+ preselectedCategories: decisionPayload.preselectedCategories ? {
1639
+ json: decisionPayload.preselectedCategories
1640
+ } : void 0,
1641
+ proofConfig: decisionPayload.proofConfig ? {
1642
+ json: decisionPayload.proofConfig
1643
+ } : void 0,
1644
+ dedupeKey: decisionPayload.dedupeKey
1645
+ }) : void 0;
1646
+ const consentRecord = await tx.create('consent', {
1647
+ id: consentId,
1623
1648
  subjectId: subject.id,
1624
- domain
1625
- }
1649
+ domainId: domainRecord.id,
1650
+ policyId,
1651
+ purposeIds: {
1652
+ json: purposeIds
1653
+ },
1654
+ metadata: Object.keys(metadataWithPolicy).length > 0 ? {
1655
+ json: metadataWithPolicy
1656
+ } : void 0,
1657
+ ipAddress: shouldStoreIp ? ctx.ipAddress : null,
1658
+ userAgent: shouldStoreUserAgent ? ctx.userAgent : null,
1659
+ jurisdiction: effectiveJurisdiction,
1660
+ jurisdictionModel: effectiveModel,
1661
+ tcString: input.tcString,
1662
+ uiSource: input.uiSource,
1663
+ consentAction: derivedConsentAction,
1664
+ givenAt,
1665
+ validUntil,
1666
+ runtimePolicyDecisionId: runtimePolicyDecision?.id,
1667
+ runtimePolicySource: decisionPayload?.source
1668
+ });
1669
+ if (!consentRecord) throw new HTTPException(500, {
1670
+ message: 'Failed to create consent',
1671
+ cause: {
1672
+ code: 'CONSENT_CREATION_FAILED',
1673
+ subjectId: subject.id,
1674
+ domain
1675
+ }
1676
+ });
1677
+ logger.debug('Created consent', {
1678
+ consentRecord: consentRecord.id
1679
+ });
1680
+ return {
1681
+ consent: consentRecord,
1682
+ created: true
1683
+ };
1626
1684
  });
1627
- return {
1628
- consent: consentRecord
1629
- };
1685
+ const maxTransactionAttempts = 3;
1686
+ let result;
1687
+ for(let attempt = 1; attempt <= maxTransactionAttempts; attempt++)try {
1688
+ result = await runConsentTransaction();
1689
+ break;
1690
+ } catch (error) {
1691
+ if (!isUniqueConstraintViolationError(error)) throw error;
1692
+ const concurrentConsent = await findConsentById(db, consentId);
1693
+ if (concurrentConsent) {
1694
+ logger.debug('Consent insert conflicted, returning existing record', {
1695
+ consentId: concurrentConsent.id
1696
+ });
1697
+ result = {
1698
+ consent: concurrentConsent,
1699
+ created: false
1700
+ };
1701
+ break;
1702
+ }
1703
+ if (attempt === maxTransactionAttempts) throw error;
1704
+ }
1705
+ if (!result) throw new HTTPException(500, {
1706
+ message: 'Failed to create consent',
1707
+ cause: {
1708
+ code: 'CONSENT_CREATION_FAILED',
1709
+ subjectId: subject.id,
1710
+ domain
1711
+ }
1630
1712
  });
1631
- const metrics = getMetrics();
1632
- if (metrics) {
1633
- const jurisdiction = effectiveJurisdiction;
1634
- metrics.recordConsentCreated({
1635
- type,
1636
- jurisdiction
1637
- });
1638
- const hasAccepted = preferences && Object.values(preferences).some(Boolean);
1639
- if (hasAccepted) metrics.recordConsentAccepted({
1640
- type,
1641
- jurisdiction
1642
- });
1643
- else metrics.recordConsentRejected({
1644
- type,
1645
- jurisdiction
1646
- });
1713
+ if (result.created) {
1714
+ const metrics = getMetrics();
1715
+ if (metrics) {
1716
+ const jurisdiction = effectiveJurisdiction;
1717
+ metrics.recordConsentCreated({
1718
+ type,
1719
+ jurisdiction
1720
+ });
1721
+ const hasAccepted = preferences && Object.values(preferences).some(Boolean);
1722
+ if (hasAccepted) metrics.recordConsentAccepted({
1723
+ type,
1724
+ jurisdiction
1725
+ });
1726
+ else metrics.recordConsentRejected({
1727
+ type,
1728
+ jurisdiction
1729
+ });
1730
+ }
1647
1731
  }
1648
1732
  return c.json({
1649
1733
  subjectId: subject.id,
@@ -1698,10 +1782,10 @@ const createSubjectRoutes = ()=>{
1698
1782
  description: 'Subject not found for the given ID'
1699
1783
  }
1700
1784
  }
1701
- }), validator('param', getSubjectInputSchema), getSubjectHandler);
1785
+ }), validator('param', getSubjectParamsSchema), validator('query', getSubjectQuerySchema), getSubjectHandler);
1702
1786
  app.post('/', describeRoute({
1703
1787
  summary: 'Record consent for a subject',
1704
- 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`",
1788
+ 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`, and suffixed legal-document variants such as `terms_and_conditions_b2b` – Prefer a signed `documentSnapshotToken`; otherwise use a release `policyHash`, with `policyId` kept only for compatibility\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
1705
1789
  tags: [
1706
1790
  'Subject',
1707
1791
  'Consent'
@@ -1739,12 +1823,7 @@ const createSubjectRoutes = ()=>{
1739
1823
  description: 'Subject not found for the given ID'
1740
1824
  }
1741
1825
  }
1742
- }), validator('param', __rspack_external_valibot.object({
1743
- id: subjectIdSchema
1744
- })), validator('json', __rspack_external_valibot.object({
1745
- externalId: __rspack_external_valibot.string(),
1746
- identityProvider: __rspack_external_valibot.optional(__rspack_external_valibot.string())
1747
- })), patchSubjectHandler);
1826
+ }), validator('param', patchSubjectParamsSchema), validator('json', patchSubjectInputSchema), patchSubjectHandler);
1748
1827
  app.get('/', describeRoute({
1749
1828
  summary: 'List subjects by external ID (API key required)',
1750
1829
  description: 'Returns all subjects linked to the given external ID. Requires Bearer token (API key). Use for server-side consent lookups.',
@@ -1,4 +1,5 @@
1
1
  import { SpanKind, SpanStatusCode as api_SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
2
+ const version = '2.2.0-canary-20260727202135';
2
3
  function extractErrorMessage(error) {
3
4
  if (error instanceof AggregateError && error.errors?.length > 0) {
4
5
  const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
@@ -13,7 +14,7 @@ function createTelemetryOptions(appName = 'c15t', telemetryConfig, tenantId) {
13
14
  const defaultAttributes = {
14
15
  ...telemetryConfig?.defaultAttributes || {},
15
16
  'service.name': String(appName),
16
- 'service.version': "2.1.0"
17
+ 'service.version': version
17
18
  };
18
19
  if (tenantId) defaultAttributes['tenant.id'] = tenantId;
19
20
  const config = {
@@ -315,7 +316,6 @@ function getMetrics(options) {
315
316
  metricsInstance = createMetrics(getMeter(options));
316
317
  return metricsInstance;
317
318
  }
318
- const GVL_TTL_MS = 259200000;
319
319
  const memory_memoryCache = new Map();
320
320
  function createMemoryCacheAdapter() {
321
321
  return {
@@ -368,6 +368,7 @@ function createCacheKey(appName, namespace, ...parts) {
368
368
  ];
369
369
  return allParts.join(':');
370
370
  }
371
+ const GVL_TTL_MS = 259200000;
371
372
  const GVL_ENDPOINT = 'https://gvl.inth.app';
372
373
  const inflightRequests = new Map();
373
374
  async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT) {
@@ -469,4 +470,4 @@ function createGVLResolver(options) {
469
470
  }
470
471
  };
471
472
  }
472
- export { GVL_TTL_MS, clearMemoryCache, createCacheKey, createGVLCacheKey, createGVLResolver, createMemoryCacheAdapter, createRequestSpan, createTelemetryOptions, extractErrorMessage, getMemoryCacheSize, getMetrics, getTraceContext, handleSpanError, isTelemetryEnabled, withDatabaseSpan, withSpanContext };
473
+ export { GVL_TTL_MS, clearMemoryCache, createCacheKey, createGVLCacheKey, createGVLResolver, createMemoryCacheAdapter, createRequestSpan, createTelemetryOptions, extractErrorMessage, getMemoryCacheSize, getMetrics, getTraceContext, handleSpanError, isTelemetryEnabled, version, withDatabaseSpan, withSpanContext };
package/dist/cache.cjs CHANGED
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
3
  (()=>{
4
- __webpack_require__.d = (exports1, definition)=>{
5
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
- enumerable: true,
7
- get: definition[key]
8
- });
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
9
13
  };
10
14
  })();
11
15
  (()=>{
@@ -24,17 +28,17 @@ var __webpack_require__ = {};
24
28
  var __webpack_exports__ = {};
25
29
  __webpack_require__.r(__webpack_exports__);
26
30
  __webpack_require__.d(__webpack_exports__, {
27
- createGVLResolver: ()=>createGVLResolver,
28
- createUpstashRedisAdapterFromClient: ()=>createUpstashRedisAdapterFromClient,
29
- createCacheKey: ()=>createCacheKey,
30
- clearMemoryCache: ()=>clearMemoryCache,
31
- MEMORY_TTL_MS: ()=>300000,
32
- getMemoryCacheSize: ()=>getMemoryCacheSize,
33
- createUpstashRedisAdapter: ()=>createUpstashRedisAdapter,
34
31
  GVL_TTL_MS: ()=>GVL_TTL_MS,
32
+ MEMORY_TTL_MS: ()=>300000,
33
+ clearMemoryCache: ()=>clearMemoryCache,
34
+ createCacheKey: ()=>createCacheKey,
35
35
  createCloudflareKVAdapter: ()=>createCloudflareKVAdapter,
36
36
  createGVLCacheKey: ()=>createGVLCacheKey,
37
- createMemoryCacheAdapter: ()=>createMemoryCacheAdapter
37
+ createGVLResolver: ()=>createGVLResolver,
38
+ createMemoryCacheAdapter: ()=>createMemoryCacheAdapter,
39
+ createUpstashRedisAdapter: ()=>createUpstashRedisAdapter,
40
+ createUpstashRedisAdapterFromClient: ()=>createUpstashRedisAdapterFromClient,
41
+ getMemoryCacheSize: ()=>getMemoryCacheSize
38
42
  });
39
43
  const GVL_TTL_MS = 259200000;
40
44
  function createCloudflareKVAdapter(kv) {
package/dist/cache.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Redis } from "@upstash/redis";
2
- import { GVL_TTL_MS } from "./302.js";
2
+ import { GVL_TTL_MS } from "./744.js";
3
3
  function createCloudflareKVAdapter(kv) {
4
4
  return {
5
5
  async get (key) {
@@ -52,5 +52,5 @@ function createUpstashRedisAdapterFromClient(client) {
52
52
  };
53
53
  }
54
54
  var cache_MEMORY_TTL_MS = 300000;
55
- export { GVL_TTL_MS, clearMemoryCache, createCacheKey, createGVLCacheKey, createGVLResolver, createMemoryCacheAdapter, getMemoryCacheSize } from "./302.js";
55
+ export { GVL_TTL_MS, clearMemoryCache, createCacheKey, createGVLCacheKey, createGVLResolver, createMemoryCacheAdapter, getMemoryCacheSize } from "./744.js";
56
56
  export { cache_MEMORY_TTL_MS as MEMORY_TTL_MS, createCloudflareKVAdapter, createUpstashRedisAdapter, createUpstashRedisAdapterFromClient };