@c15t/backend 2.0.0-rc.4 → 2.0.0-rc.5

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 (308) hide show
  1. package/dist/core.cjs +830 -74
  2. package/dist/core.js +807 -75
  3. package/dist/db/schema.cjs +37 -0
  4. package/dist/db/schema.js +33 -2
  5. package/dist/edge.cjs +1106 -0
  6. package/dist/edge.js +1069 -0
  7. package/dist/router.cjs +613 -64
  8. package/dist/router.js +613 -64
  9. package/{dist → dist-types}/cache/adapters/cloudflare-kv.d.ts +0 -1
  10. package/{dist → dist-types}/cache/adapters/index.d.ts +0 -1
  11. package/{dist → dist-types}/cache/adapters/memory.d.ts +0 -1
  12. package/{dist → dist-types}/cache/adapters/upstash-redis.d.ts +0 -1
  13. package/{dist → dist-types}/cache/gvl-resolver.d.ts +1 -2
  14. package/{dist → dist-types}/cache/index.d.ts +0 -1
  15. package/{dist → dist-types}/cache/keys.d.ts +0 -1
  16. package/{dist → dist-types}/cache/types.d.ts +0 -1
  17. package/{dist → dist-types}/core.d.ts +8 -1
  18. package/{dist → dist-types}/db/migrator/index.d.ts +0 -1
  19. package/{dist → dist-types}/db/registry/consent-policy.d.ts +0 -1
  20. package/{dist → dist-types}/db/registry/consent-purpose.d.ts +0 -1
  21. package/{dist → dist-types}/db/registry/domain.d.ts +0 -1
  22. package/{dist → dist-types}/db/registry/index.d.ts +22 -2
  23. package/dist-types/db/registry/runtime-policy-decision.d.ts +60 -0
  24. package/{dist → dist-types}/db/registry/subject.d.ts +0 -1
  25. package/{dist → dist-types}/db/registry/types.d.ts +1 -2
  26. package/{dist → dist-types}/db/registry/utils/generate-id.d.ts +0 -1
  27. package/{dist → dist-types}/db/registry/utils.d.ts +0 -1
  28. package/{dist → dist-types}/db/schema/1.0.0/audit-log.d.ts +0 -1
  29. package/{dist → dist-types}/db/schema/1.0.0/consent-policy.d.ts +0 -1
  30. package/{dist → dist-types}/db/schema/1.0.0/consent-purpose.d.ts +0 -1
  31. package/{dist → dist-types}/db/schema/1.0.0/consent-record.d.ts +0 -1
  32. package/{dist → dist-types}/db/schema/1.0.0/consent.d.ts +1 -2
  33. package/{dist → dist-types}/db/schema/1.0.0/domain.d.ts +0 -1
  34. package/{dist → dist-types}/db/schema/1.0.0/index.d.ts +0 -1
  35. package/{dist → dist-types}/db/schema/1.0.0/subject.d.ts +0 -1
  36. package/{dist → dist-types}/db/schema/2.0.0/audit-log.d.ts +1 -2
  37. package/{dist → dist-types}/db/schema/2.0.0/consent-policy.d.ts +1 -2
  38. package/{dist → dist-types}/db/schema/2.0.0/consent-purpose.d.ts +1 -2
  39. package/{dist → dist-types}/db/schema/2.0.0/consent.d.ts +5 -2
  40. package/{dist → dist-types}/db/schema/2.0.0/domain.d.ts +1 -2
  41. package/{dist → dist-types}/db/schema/2.0.0/index.d.ts +432 -17
  42. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +23 -0
  43. package/{dist → dist-types}/db/schema/2.0.0/subject.d.ts +1 -2
  44. package/{dist → dist-types}/db/schema/index.d.ts +862 -33
  45. package/{dist → dist-types}/db/tenant-scope.d.ts +0 -1
  46. package/{dist → dist-types}/define-config.d.ts +0 -1
  47. package/dist-types/edge/index.d.ts +5 -0
  48. package/dist-types/edge/init-handler.d.ts +38 -0
  49. package/dist-types/edge/resolve-consent.d.ts +80 -0
  50. package/dist-types/edge/types.d.ts +13 -0
  51. package/{dist → dist-types}/handlers/consent/check.handler.d.ts +0 -1
  52. package/{src/handlers/consent/index.ts → dist-types/handlers/consent/index.d.ts} +0 -1
  53. package/{dist → dist-types}/handlers/init/geo.d.ts +2 -3
  54. package/{dist → dist-types}/handlers/init/index.d.ts +4 -5
  55. package/dist-types/handlers/init/policy.d.ts +26 -0
  56. package/dist-types/handlers/init/resolve-init.d.ts +44 -0
  57. package/dist-types/handlers/init/translations.d.ts +48 -0
  58. package/dist-types/handlers/policy/snapshot.d.ts +99 -0
  59. package/{src/handlers/status/index.ts → dist-types/handlers/status/index.d.ts} +0 -1
  60. package/{dist → dist-types}/handlers/status/status.handler.d.ts +0 -1
  61. package/{dist → dist-types}/handlers/subject/get.handler.d.ts +0 -1
  62. package/{src/handlers/subject/index.ts → dist-types/handlers/subject/index.d.ts} +0 -1
  63. package/{dist → dist-types}/handlers/subject/list.handler.d.ts +0 -1
  64. package/{dist → dist-types}/handlers/subject/patch.handler.d.ts +0 -1
  65. package/{dist → dist-types}/handlers/subject/post.handler.d.ts +12 -1
  66. package/{dist → dist-types}/handlers/utils/consent-enrichment.d.ts +0 -1
  67. package/{dist → dist-types}/init.d.ts +0 -1
  68. package/{dist → dist-types}/middleware/auth/index.d.ts +0 -1
  69. package/{dist → dist-types}/middleware/auth/validate-api-key.d.ts +0 -1
  70. package/{dist → dist-types}/middleware/cors/cors.d.ts +0 -1
  71. package/{src/middleware/cors/index.ts → dist-types/middleware/cors/index.d.ts} +0 -1
  72. package/{dist → dist-types}/middleware/cors/is-origin-trusted.d.ts +1 -2
  73. package/{dist → dist-types}/middleware/cors/process-cors.d.ts +0 -1
  74. package/{dist → dist-types}/middleware/openapi/config.d.ts +0 -1
  75. package/{dist → dist-types}/middleware/openapi/handlers.d.ts +0 -1
  76. package/{src/middleware/openapi/index.ts → dist-types/middleware/openapi/index.d.ts} +0 -1
  77. package/{dist → dist-types}/middleware/process-ip/index.d.ts +0 -1
  78. package/dist-types/policies/builder.d.ts +127 -0
  79. package/dist-types/policies/defaults.d.ts +2 -0
  80. package/dist-types/policies/matchers.d.ts +3 -0
  81. package/{dist → dist-types}/router.d.ts +0 -1
  82. package/{dist → dist-types}/routes/consent.d.ts +0 -1
  83. package/{src/routes/index.ts → dist-types/routes/index.d.ts} +0 -1
  84. package/{dist → dist-types}/routes/init.d.ts +0 -1
  85. package/{dist → dist-types}/routes/status.d.ts +0 -1
  86. package/{dist → dist-types}/routes/subject.d.ts +0 -1
  87. package/{dist → dist-types}/types/api.d.ts +0 -1
  88. package/{dist → dist-types}/types/index.d.ts +110 -6
  89. package/dist-types/utils/background.d.ts +6 -0
  90. package/{dist → dist-types}/utils/create-telemetry-options.d.ts +0 -1
  91. package/{dist → dist-types}/utils/env.d.ts +0 -1
  92. package/{dist → dist-types}/utils/extract-error-message.d.ts +0 -1
  93. package/{dist → dist-types}/utils/instrumentation.d.ts +0 -1
  94. package/{dist → dist-types}/utils/logger.d.ts +1 -2
  95. package/{dist → dist-types}/utils/metrics.d.ts +0 -1
  96. package/dist-types/version.d.ts +1 -0
  97. package/docs/README.md +49 -0
  98. package/docs/api/configuration.md +197 -0
  99. package/docs/api/endpoints.md +211 -0
  100. package/docs/guides/caching.md +85 -0
  101. package/docs/guides/database-setup.md +128 -0
  102. package/docs/guides/edge-deployment.md +248 -0
  103. package/docs/guides/framework-integration.md +142 -0
  104. package/docs/guides/iab-tcf.md +89 -0
  105. package/docs/guides/observability.md +96 -0
  106. package/docs/guides/policy-packs.md +396 -0
  107. package/docs/quickstart.md +129 -0
  108. package/package.json +33 -19
  109. package/.turbo/turbo-build.log +0 -49
  110. package/CHANGELOG.md +0 -123
  111. package/dist/cache/adapters/cloudflare-kv.d.ts.map +0 -1
  112. package/dist/cache/adapters/index.d.ts.map +0 -1
  113. package/dist/cache/adapters/memory.d.ts.map +0 -1
  114. package/dist/cache/adapters/upstash-redis.d.ts.map +0 -1
  115. package/dist/cache/gvl-resolver.d.ts.map +0 -1
  116. package/dist/cache/index.d.ts.map +0 -1
  117. package/dist/cache/keys.d.ts.map +0 -1
  118. package/dist/cache/types.d.ts.map +0 -1
  119. package/dist/core.d.ts.map +0 -1
  120. package/dist/db/adapters/drizzle.d.ts +0 -2
  121. package/dist/db/adapters/drizzle.d.ts.map +0 -1
  122. package/dist/db/adapters/index.d.ts +0 -2
  123. package/dist/db/adapters/index.d.ts.map +0 -1
  124. package/dist/db/adapters/kysely.d.ts +0 -2
  125. package/dist/db/adapters/kysely.d.ts.map +0 -1
  126. package/dist/db/adapters/mongo.d.ts +0 -2
  127. package/dist/db/adapters/mongo.d.ts.map +0 -1
  128. package/dist/db/adapters/prisma.d.ts +0 -2
  129. package/dist/db/adapters/prisma.d.ts.map +0 -1
  130. package/dist/db/adapters/typeorm.d.ts +0 -2
  131. package/dist/db/adapters/typeorm.d.ts.map +0 -1
  132. package/dist/db/migrator/index.d.ts.map +0 -1
  133. package/dist/db/registry/consent-policy.d.ts.map +0 -1
  134. package/dist/db/registry/consent-purpose.d.ts.map +0 -1
  135. package/dist/db/registry/domain.d.ts.map +0 -1
  136. package/dist/db/registry/index.d.ts.map +0 -1
  137. package/dist/db/registry/subject.d.ts.map +0 -1
  138. package/dist/db/registry/types.d.ts.map +0 -1
  139. package/dist/db/registry/utils/generate-id.d.ts.map +0 -1
  140. package/dist/db/registry/utils.d.ts.map +0 -1
  141. package/dist/db/schema/1.0.0/audit-log.d.ts.map +0 -1
  142. package/dist/db/schema/1.0.0/consent-policy.d.ts.map +0 -1
  143. package/dist/db/schema/1.0.0/consent-purpose.d.ts.map +0 -1
  144. package/dist/db/schema/1.0.0/consent-record.d.ts.map +0 -1
  145. package/dist/db/schema/1.0.0/consent.d.ts.map +0 -1
  146. package/dist/db/schema/1.0.0/domain.d.ts.map +0 -1
  147. package/dist/db/schema/1.0.0/index.d.ts.map +0 -1
  148. package/dist/db/schema/1.0.0/subject.d.ts.map +0 -1
  149. package/dist/db/schema/2.0.0/audit-log.d.ts.map +0 -1
  150. package/dist/db/schema/2.0.0/consent-policy.d.ts.map +0 -1
  151. package/dist/db/schema/2.0.0/consent-purpose.d.ts.map +0 -1
  152. package/dist/db/schema/2.0.0/consent.d.ts.map +0 -1
  153. package/dist/db/schema/2.0.0/domain.d.ts.map +0 -1
  154. package/dist/db/schema/2.0.0/index.d.ts.map +0 -1
  155. package/dist/db/schema/2.0.0/subject.d.ts.map +0 -1
  156. package/dist/db/schema/index.d.ts.map +0 -1
  157. package/dist/db/tenant-scope.d.ts.map +0 -1
  158. package/dist/define-config.d.ts.map +0 -1
  159. package/dist/handlers/consent/check.handler.d.ts.map +0 -1
  160. package/dist/handlers/consent/index.d.ts +0 -12
  161. package/dist/handlers/consent/index.d.ts.map +0 -1
  162. package/dist/handlers/init/geo.d.ts.map +0 -1
  163. package/dist/handlers/init/index.d.ts.map +0 -1
  164. package/dist/handlers/init/translations.d.ts +0 -26
  165. package/dist/handlers/init/translations.d.ts.map +0 -1
  166. package/dist/handlers/status/index.d.ts +0 -7
  167. package/dist/handlers/status/index.d.ts.map +0 -1
  168. package/dist/handlers/status/status.handler.d.ts.map +0 -1
  169. package/dist/handlers/subject/get.handler.d.ts.map +0 -1
  170. package/dist/handlers/subject/index.d.ts +0 -10
  171. package/dist/handlers/subject/index.d.ts.map +0 -1
  172. package/dist/handlers/subject/list.handler.d.ts.map +0 -1
  173. package/dist/handlers/subject/patch.handler.d.ts.map +0 -1
  174. package/dist/handlers/subject/post.handler.d.ts.map +0 -1
  175. package/dist/handlers/utils/consent-enrichment.d.ts.map +0 -1
  176. package/dist/init.d.ts.map +0 -1
  177. package/dist/middleware/auth/index.d.ts.map +0 -1
  178. package/dist/middleware/auth/validate-api-key.d.ts.map +0 -1
  179. package/dist/middleware/cors/cors.d.ts.map +0 -1
  180. package/dist/middleware/cors/index.d.ts +0 -30
  181. package/dist/middleware/cors/index.d.ts.map +0 -1
  182. package/dist/middleware/cors/is-origin-trusted.d.ts.map +0 -1
  183. package/dist/middleware/cors/process-cors.d.ts.map +0 -1
  184. package/dist/middleware/openapi/config.d.ts.map +0 -1
  185. package/dist/middleware/openapi/handlers.d.ts.map +0 -1
  186. package/dist/middleware/openapi/index.d.ts +0 -12
  187. package/dist/middleware/openapi/index.d.ts.map +0 -1
  188. package/dist/middleware/process-ip/index.d.ts.map +0 -1
  189. package/dist/router.d.ts.map +0 -1
  190. package/dist/routes/consent.d.ts.map +0 -1
  191. package/dist/routes/index.d.ts +0 -10
  192. package/dist/routes/index.d.ts.map +0 -1
  193. package/dist/routes/init.d.ts.map +0 -1
  194. package/dist/routes/status.d.ts.map +0 -1
  195. package/dist/routes/subject.d.ts.map +0 -1
  196. package/dist/types/api.d.ts.map +0 -1
  197. package/dist/types/index.d.ts.map +0 -1
  198. package/dist/utils/create-telemetry-options.d.ts.map +0 -1
  199. package/dist/utils/env.d.ts.map +0 -1
  200. package/dist/utils/extract-error-message.d.ts.map +0 -1
  201. package/dist/utils/index.d.ts +0 -4
  202. package/dist/utils/index.d.ts.map +0 -1
  203. package/dist/utils/instrumentation.d.ts.map +0 -1
  204. package/dist/utils/logger.d.ts.map +0 -1
  205. package/dist/utils/metrics.d.ts.map +0 -1
  206. package/dist/version.d.ts +0 -2
  207. package/dist/version.d.ts.map +0 -1
  208. package/knip.json +0 -31
  209. package/rslib.config.ts +0 -93
  210. package/src/cache/adapters/cloudflare-kv.ts +0 -71
  211. package/src/cache/adapters/index.ts +0 -22
  212. package/src/cache/adapters/memory.ts +0 -111
  213. package/src/cache/adapters/upstash-redis.ts +0 -113
  214. package/src/cache/gvl-resolver.ts +0 -289
  215. package/src/cache/index.ts +0 -34
  216. package/src/cache/keys.ts +0 -68
  217. package/src/cache/types.ts +0 -66
  218. package/src/core.ts +0 -369
  219. package/src/db/migrator/index.ts +0 -80
  220. package/src/db/registry/consent-policy.test.ts +0 -451
  221. package/src/db/registry/consent-policy.ts +0 -82
  222. package/src/db/registry/consent-purpose.test.ts +0 -428
  223. package/src/db/registry/consent-purpose.ts +0 -61
  224. package/src/db/registry/domain.test.ts +0 -445
  225. package/src/db/registry/domain.ts +0 -91
  226. package/src/db/registry/index.ts +0 -14
  227. package/src/db/registry/subject.test.ts +0 -371
  228. package/src/db/registry/subject.ts +0 -126
  229. package/src/db/registry/types.ts +0 -10
  230. package/src/db/registry/utils/generate-id.test.ts +0 -216
  231. package/src/db/registry/utils/generate-id.ts +0 -133
  232. package/src/db/registry/utils.ts +0 -133
  233. package/src/db/schema/1.0.0/audit-log.ts +0 -15
  234. package/src/db/schema/1.0.0/consent-policy.ts +0 -14
  235. package/src/db/schema/1.0.0/consent-purpose.ts +0 -14
  236. package/src/db/schema/1.0.0/consent-record.ts +0 -10
  237. package/src/db/schema/1.0.0/consent.ts +0 -20
  238. package/src/db/schema/1.0.0/domain.ts +0 -12
  239. package/src/db/schema/1.0.0/index.ts +0 -48
  240. package/src/db/schema/1.0.0/subject.ts +0 -11
  241. package/src/db/schema/2.0.0/audit-log.ts +0 -18
  242. package/src/db/schema/2.0.0/consent-policy.ts +0 -28
  243. package/src/db/schema/2.0.0/consent-purpose.ts +0 -12
  244. package/src/db/schema/2.0.0/consent.ts +0 -28
  245. package/src/db/schema/2.0.0/domain.ts +0 -12
  246. package/src/db/schema/2.0.0/index.ts +0 -47
  247. package/src/db/schema/2.0.0/subject.ts +0 -13
  248. package/src/db/schema/index.ts +0 -15
  249. package/src/db/tenant-scope.test.ts +0 -747
  250. package/src/db/tenant-scope.ts +0 -103
  251. package/src/define-config.ts +0 -19
  252. package/src/handlers/consent/check.handler.ts +0 -126
  253. package/src/handlers/init/geo.test.ts +0 -317
  254. package/src/handlers/init/geo.ts +0 -195
  255. package/src/handlers/init/index.test.ts +0 -205
  256. package/src/handlers/init/index.ts +0 -114
  257. package/src/handlers/init/translations.test.ts +0 -121
  258. package/src/handlers/init/translations.ts +0 -69
  259. package/src/handlers/status/status.handler.test.ts +0 -155
  260. package/src/handlers/status/status.handler.ts +0 -51
  261. package/src/handlers/subject/get.handler.ts +0 -92
  262. package/src/handlers/subject/list.handler.ts +0 -92
  263. package/src/handlers/subject/patch.handler.ts +0 -119
  264. package/src/handlers/subject/post.handler.test.ts +0 -294
  265. package/src/handlers/subject/post.handler.ts +0 -268
  266. package/src/handlers/utils/consent-enrichment.test.ts +0 -380
  267. package/src/handlers/utils/consent-enrichment.ts +0 -218
  268. package/src/init.test.ts +0 -122
  269. package/src/init.ts +0 -88
  270. package/src/middleware/auth/index.ts +0 -11
  271. package/src/middleware/auth/validate-api-key.test.ts +0 -86
  272. package/src/middleware/auth/validate-api-key.ts +0 -107
  273. package/src/middleware/cors/cors.test.ts +0 -135
  274. package/src/middleware/cors/cors.ts +0 -186
  275. package/src/middleware/cors/is-origin-trusted.test.ts +0 -164
  276. package/src/middleware/cors/is-origin-trusted.ts +0 -130
  277. package/src/middleware/cors/process-cors.ts +0 -91
  278. package/src/middleware/openapi/config.ts +0 -29
  279. package/src/middleware/openapi/handlers.ts +0 -34
  280. package/src/middleware/process-ip/index.test.ts +0 -193
  281. package/src/middleware/process-ip/index.ts +0 -199
  282. package/src/router.ts +0 -15
  283. package/src/routes/consent.ts +0 -52
  284. package/src/routes/init.ts +0 -105
  285. package/src/routes/status.ts +0 -46
  286. package/src/routes/subject.ts +0 -152
  287. package/src/types/api.ts +0 -48
  288. package/src/types/index.ts +0 -391
  289. package/src/utils/create-telemetry-options.test.ts +0 -286
  290. package/src/utils/create-telemetry-options.ts +0 -229
  291. package/src/utils/env.ts +0 -84
  292. package/src/utils/extract-error-message.ts +0 -21
  293. package/src/utils/instrumentation.test.ts +0 -183
  294. package/src/utils/instrumentation.ts +0 -194
  295. package/src/utils/logger.ts +0 -41
  296. package/src/utils/metrics.test.ts +0 -311
  297. package/src/utils/metrics.ts +0 -402
  298. package/src/utils/telemetry-pii.test.ts +0 -323
  299. package/src/version.ts +0 -2
  300. package/tsconfig.json +0 -11
  301. package/vitest.config.ts +0 -28
  302. /package/{src/db/adapters/drizzle.ts → dist-types/db/adapters/drizzle.d.ts} +0 -0
  303. /package/{src/db/adapters/index.ts → dist-types/db/adapters/index.d.ts} +0 -0
  304. /package/{src/db/adapters/kysely.ts → dist-types/db/adapters/kysely.d.ts} +0 -0
  305. /package/{src/db/adapters/mongo.ts → dist-types/db/adapters/mongo.d.ts} +0 -0
  306. /package/{src/db/adapters/prisma.ts → dist-types/db/adapters/prisma.d.ts} +0 -0
  307. /package/{src/db/adapters/typeorm.ts → dist-types/db/adapters/typeorm.d.ts} +0 -0
  308. /package/{src/utils/index.ts → dist-types/utils/index.d.ts} +0 -0
package/dist/router.cjs CHANGED
@@ -646,6 +646,124 @@ function createGVLResolver(options) {
646
646
  }
647
647
  };
648
648
  }
649
+ const external_jose_namespaceObject = require("jose");
650
+ const POLICY_SNAPSHOT_JWT_HEADER = {
651
+ alg: 'HS256',
652
+ typ: 'JWT'
653
+ };
654
+ const DEFAULT_POLICY_SNAPSHOT_ISSUER = 'c15t';
655
+ const DEFAULT_POLICY_SNAPSHOT_AUDIENCE = 'c15t-policy-snapshot';
656
+ function resolveSnapshotIssuer(options) {
657
+ return options?.issuer?.trim() || DEFAULT_POLICY_SNAPSHOT_ISSUER;
658
+ }
659
+ function resolveSnapshotAudience(params) {
660
+ const configuredAudience = params.options?.audience?.trim();
661
+ if (configuredAudience) return configuredAudience;
662
+ return params.tenantId ? `${DEFAULT_POLICY_SNAPSHOT_AUDIENCE}:${params.tenantId}` : DEFAULT_POLICY_SNAPSHOT_AUDIENCE;
663
+ }
664
+ function getSigningKey(secret) {
665
+ return new TextEncoder().encode(secret);
666
+ }
667
+ function isPolicySnapshotPayload(payload) {
668
+ return 'string' == typeof payload.policyId && 'string' == typeof payload.fingerprint && 'string' == typeof payload.matchedBy && 'string' == typeof payload.jurisdiction && 'string' == typeof payload.model && 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub && 'number' == typeof payload.iat && 'number' == typeof payload.exp;
669
+ }
670
+ async function createPolicySnapshotToken(params) {
671
+ const { options } = params;
672
+ if (!options?.signingKey) return;
673
+ const iat = Math.floor(Date.now() / 1000);
674
+ const ttlSeconds = options.ttlSeconds ?? 1800;
675
+ const exp = iat + ttlSeconds;
676
+ const iss = resolveSnapshotIssuer(options);
677
+ const aud = resolveSnapshotAudience({
678
+ options,
679
+ tenantId: params.tenantId
680
+ });
681
+ const payload = {
682
+ iss,
683
+ aud,
684
+ sub: params.policyId,
685
+ tenantId: params.tenantId,
686
+ policyId: params.policyId,
687
+ fingerprint: params.fingerprint,
688
+ matchedBy: params.matchedBy,
689
+ country: params.country,
690
+ region: params.region,
691
+ jurisdiction: params.jurisdiction,
692
+ language: params.language,
693
+ model: params.model,
694
+ policyI18n: params.policyI18n,
695
+ expiryDays: params.expiryDays,
696
+ scopeMode: params.scopeMode,
697
+ uiMode: params.uiMode,
698
+ bannerUi: params.bannerUi,
699
+ dialogUi: params.dialogUi,
700
+ categories: params.categories,
701
+ preselectedCategories: params.preselectedCategories,
702
+ gpc: params.gpc,
703
+ proofConfig: params.proofConfig,
704
+ iat,
705
+ exp
706
+ };
707
+ const token = await new external_jose_namespaceObject.SignJWT(payload).setProtectedHeader(POLICY_SNAPSHOT_JWT_HEADER).setIssuedAt(iat).setExpirationTime(exp).sign(getSigningKey(options.signingKey));
708
+ return {
709
+ token,
710
+ payload
711
+ };
712
+ }
713
+ async function verifyPolicySnapshotToken(params) {
714
+ const { token, options, tenantId } = params;
715
+ if (!options?.signingKey) return {
716
+ valid: false,
717
+ reason: 'missing'
718
+ };
719
+ if (!token) return {
720
+ valid: false,
721
+ reason: 'missing'
722
+ };
723
+ if (3 !== token.split('.').length) return {
724
+ valid: false,
725
+ reason: 'malformed'
726
+ };
727
+ try {
728
+ const { payload, protectedHeader } = await (0, external_jose_namespaceObject.jwtVerify)(token, getSigningKey(options.signingKey), {
729
+ issuer: resolveSnapshotIssuer(options),
730
+ audience: resolveSnapshotAudience({
731
+ options,
732
+ tenantId
733
+ })
734
+ });
735
+ const header = protectedHeader;
736
+ if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
737
+ valid: false,
738
+ reason: 'invalid'
739
+ };
740
+ if (!isPolicySnapshotPayload(payload)) return {
741
+ valid: false,
742
+ reason: 'invalid'
743
+ };
744
+ if (payload.sub !== payload.policyId) return {
745
+ valid: false,
746
+ reason: 'invalid'
747
+ };
748
+ if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
749
+ valid: false,
750
+ reason: 'invalid'
751
+ };
752
+ return {
753
+ valid: true,
754
+ payload
755
+ };
756
+ } catch (error) {
757
+ if (error instanceof external_jose_namespaceObject.errors.JWTExpired) return {
758
+ valid: false,
759
+ reason: 'expired'
760
+ };
761
+ return {
762
+ valid: false,
763
+ reason: 'invalid'
764
+ };
765
+ }
766
+ }
649
767
  function geo_normalizeHeader(value) {
650
768
  if (!value) return null;
651
769
  return Array.isArray(value) ? value[0] ?? null : value;
@@ -801,28 +919,250 @@ function getJurisdiction(location, options) {
801
919
  if (options.disableGeoLocation) return 'GDPR';
802
920
  return checkJurisdiction(location.countryCode, location.regionCode);
803
921
  }
922
+ const schema_types_namespaceObject = require("@c15t/schema/types");
923
+ async function resolvePolicyDecision(params) {
924
+ return (0, schema_types_namespaceObject.resolvePolicyDecision)({
925
+ policies: params.policies,
926
+ countryCode: params.countryCode,
927
+ regionCode: params.regionCode,
928
+ jurisdiction: params.jurisdiction,
929
+ iabEnabled: params.iabEnabled
930
+ });
931
+ }
804
932
  const translations_namespaceObject = require("@c15t/translations");
805
933
  const all_namespaceObject = require("@c15t/translations/all");
934
+ const DEFAULT_PROFILE = 'default';
935
+ const warnedKeys = new Set();
806
936
  function isSupportedBaseLanguage(lang) {
807
937
  return lang in all_namespaceObject.baseTranslations;
808
938
  }
809
- function translations_getTranslationsData(acceptLanguage, customTranslations) {
810
- const supportedDefaultLanguages = Object.keys(all_namespaceObject.baseTranslations);
811
- const supportedCustomLanguages = Object.keys(customTranslations || {});
812
- const supportedLanguages = [
813
- ...supportedDefaultLanguages,
814
- ...supportedCustomLanguages
939
+ function warnOnce(logger, key, message, metadata) {
940
+ if (!logger || warnedKeys.has(key)) return;
941
+ warnedKeys.add(key);
942
+ logger.warn(message, metadata);
943
+ }
944
+ function normalizeLanguage(value) {
945
+ if (!value) return;
946
+ const normalized = value.split(',')[0]?.split(';')[0]?.trim().toLowerCase();
947
+ if (!normalized) return;
948
+ return normalized.split('-')[0] ?? void 0;
949
+ }
950
+ function normalizeProfiles(params) {
951
+ const profiles = params.i18n?.messages;
952
+ const legacy = params.customTranslations;
953
+ if (profiles && Object.keys(profiles).length > 0) {
954
+ if (legacy && Object.keys(legacy).length > 0) warnOnce(params.logger, 'i18n.customTranslations.ignored', '`customTranslations` is deprecated and ignored when `i18n.messages` is configured.');
955
+ return profiles;
956
+ }
957
+ if (legacy && Object.keys(legacy).length > 0) {
958
+ warnOnce(params.logger, 'i18n.customTranslations.deprecated', '`customTranslations` is deprecated. Use `i18n.messages` instead.');
959
+ return {
960
+ [DEFAULT_PROFILE]: {
961
+ translations: legacy
962
+ }
963
+ };
964
+ }
965
+ return {};
966
+ }
967
+ function buildCandidates(input) {
968
+ const raw = [
969
+ {
970
+ language: input.language,
971
+ reason: 'profile_language'
972
+ },
973
+ {
974
+ language: input.fallbackLanguage,
975
+ reason: 'profile_fallback'
976
+ }
815
977
  ];
816
- const preferredLanguage = (0, translations_namespaceObject.selectLanguage)(supportedLanguages, {
978
+ const dedupe = new Set();
979
+ return raw.filter((candidate)=>{
980
+ const key = candidate.language;
981
+ if (dedupe.has(key)) return false;
982
+ dedupe.add(key);
983
+ return true;
984
+ });
985
+ }
986
+ function getProfileLanguages(profiles, profile) {
987
+ return Object.keys(profiles[profile]?.translations ?? {}).sort();
988
+ }
989
+ function getSelectableLanguages(input) {
990
+ return getProfileLanguages(input.profiles, input.profile);
991
+ }
992
+ function resolveFallbackLanguage(input) {
993
+ const configuredFallbackLanguage = normalizeLanguage(input.profile?.fallbackLanguage) ?? 'en';
994
+ const profileLanguages = Object.keys(input.profile?.translations ?? {}).sort();
995
+ if (profileLanguages.includes(configuredFallbackLanguage)) return configuredFallbackLanguage;
996
+ if (profileLanguages.includes('en')) return 'en';
997
+ return profileLanguages[0] ?? configuredFallbackLanguage;
998
+ }
999
+ function resolveActiveProfile(input) {
1000
+ const requestedProfile = input.policyProfile ?? input.defaultProfile;
1001
+ if (input.profiles[requestedProfile]) return requestedProfile;
1002
+ if (input.policyProfile) warnOnce(input.logger, `i18n.profile.missing:${requestedProfile}`, `Policy i18n profile '${requestedProfile}' does not exist. Falling back to default profile '${input.defaultProfile}'.`);
1003
+ return input.defaultProfile;
1004
+ }
1005
+ function translations_getTranslationsData(acceptLanguage, customTranslations, options) {
1006
+ const profiles = normalizeProfiles({
1007
+ customTranslations,
1008
+ i18n: options?.i18n,
1009
+ logger: options?.logger
1010
+ });
1011
+ const defaultProfile = options?.i18n?.defaultProfile ?? DEFAULT_PROFILE;
1012
+ const profile = resolveActiveProfile({
1013
+ profiles,
1014
+ defaultProfile,
1015
+ policyProfile: options?.policyI18n?.messageProfile,
1016
+ logger: options?.logger
1017
+ });
1018
+ const configuredLanguages = Object.keys(profiles).length > 0 ? getSelectableLanguages({
1019
+ profiles,
1020
+ profile
1021
+ }) : Object.keys(all_namespaceObject.baseTranslations);
1022
+ const fallbackLanguage = Object.keys(profiles).length > 0 ? resolveFallbackLanguage({
1023
+ profile: profiles[profile]
1024
+ }) : 'en';
1025
+ const policyLanguage = normalizeLanguage(options?.policyI18n?.language);
1026
+ const requestedLanguage = policyLanguage ?? (0, translations_namespaceObject.selectLanguage)(configuredLanguages, {
817
1027
  header: acceptLanguage,
818
- fallback: 'en'
1028
+ fallback: fallbackLanguage
1029
+ });
1030
+ const candidates = buildCandidates({
1031
+ language: requestedLanguage,
1032
+ fallbackLanguage
1033
+ });
1034
+ const selectedCandidate = candidates.find((candidate)=>!!profiles[profile]?.translations[candidate.language]);
1035
+ if (selectedCandidate && 'profile_language' !== selectedCandidate.reason) warnOnce(options?.logger, `i18n.fallback:${profile}:${requestedLanguage}:${selectedCandidate.language}`, `Policy translation fallback used (${selectedCandidate.reason}).`, {
1036
+ requestedProfile: profile,
1037
+ requestedLanguage,
1038
+ resolvedProfile: profile,
1039
+ resolvedLanguage: selectedCandidate.language
819
1040
  });
820
- const base = isSupportedBaseLanguage(preferredLanguage) ? all_namespaceObject.baseTranslations[preferredLanguage] : all_namespaceObject.baseTranslations.en;
821
- const custom = supportedCustomLanguages.includes(preferredLanguage) ? customTranslations?.[preferredLanguage] : {};
1041
+ let language = selectedCandidate?.language ?? requestedLanguage;
1042
+ if (!selectedCandidate && !isSupportedBaseLanguage(language)) {
1043
+ warnOnce(options?.logger, `i18n.base-fallback:${language}`, `No translation found for '${language}'. Falling back to base English translations.`);
1044
+ language = 'en';
1045
+ }
1046
+ const base = isSupportedBaseLanguage(language) ? all_namespaceObject.baseTranslations[language] : all_namespaceObject.baseTranslations.en;
1047
+ const custom = selectedCandidate ? profiles[profile]?.translations[selectedCandidate.language] : void 0;
822
1048
  const translations = custom ? (0, translations_namespaceObject.deepMergeTranslations)(base, custom) : base;
823
1049
  return {
824
1050
  translations: translations,
825
- language: preferredLanguage
1051
+ language
1052
+ };
1053
+ }
1054
+ function stripIabTranslations(translations) {
1055
+ const { iab: _iab, ...rest } = translations;
1056
+ return rest;
1057
+ }
1058
+ function resolveNoPolicyFallback() {
1059
+ return {
1060
+ id: 'no_banner',
1061
+ model: 'none',
1062
+ ui: {
1063
+ mode: 'none'
1064
+ }
1065
+ };
1066
+ }
1067
+ async function resolveInitPayload(request, options, logger) {
1068
+ const acceptLanguage = request.headers.get('accept-language') || 'en';
1069
+ const location = await getLocation(request, options);
1070
+ const jurisdiction = getJurisdiction(location, options);
1071
+ const hasExplicitPolicyPack = void 0 !== options.policyPacks;
1072
+ const isExplicitEmptyPolicyPack = hasExplicitPolicyPack && (options.policyPacks?.length ?? 0) === 0;
1073
+ const policyDecision = isExplicitEmptyPolicyPack ? void 0 : await resolvePolicyDecision({
1074
+ policies: options.policyPacks,
1075
+ countryCode: location.countryCode,
1076
+ regionCode: location.regionCode,
1077
+ jurisdiction,
1078
+ iabEnabled: options.iab?.enabled === true
1079
+ });
1080
+ if (hasExplicitPolicyPack && !isExplicitEmptyPolicyPack && !policyDecision) logger?.warn('Policy packs configured but no policy matched', {
1081
+ country: location.countryCode,
1082
+ region: location.regionCode
1083
+ });
1084
+ const resolvedPolicy = hasExplicitPolicyPack ? policyDecision?.policy ?? resolveNoPolicyFallback() : void 0;
1085
+ const iabOptions = options.iab;
1086
+ const shouldIncludeIabPayload = iabOptions?.enabled === true && (!hasExplicitPolicyPack || resolvedPolicy?.model === 'iab');
1087
+ const translationsResult = translations_getTranslationsData(acceptLanguage, options.customTranslations, {
1088
+ i18n: options.i18n,
1089
+ policyI18n: resolvedPolicy?.i18n,
1090
+ logger
1091
+ });
1092
+ const responseTranslations = shouldIncludeIabPayload ? translationsResult : {
1093
+ ...translationsResult,
1094
+ translations: stripIabTranslations(translationsResult.translations)
1095
+ };
1096
+ let gvl = null;
1097
+ if (shouldIncludeIabPayload && iabOptions) {
1098
+ const language = translationsResult.language.split('-')[0] || 'en';
1099
+ const gvlResolver = createGVLResolver({
1100
+ appName: options.appName || 'c15t',
1101
+ bundled: iabOptions.bundled,
1102
+ cacheAdapter: options.cache?.adapter,
1103
+ vendorIds: iabOptions.vendorIds,
1104
+ endpoint: iabOptions.endpoint
1105
+ });
1106
+ gvl = await gvlResolver.get(language);
1107
+ }
1108
+ const customVendors = shouldIncludeIabPayload ? iabOptions?.customVendors : void 0;
1109
+ const snapshot = policyDecision ? await createPolicySnapshotToken({
1110
+ options: options.policySnapshot,
1111
+ tenantId: options.tenantId,
1112
+ policyId: policyDecision.policy.id,
1113
+ fingerprint: policyDecision.fingerprint,
1114
+ matchedBy: policyDecision.matchedBy,
1115
+ country: location?.countryCode ?? null,
1116
+ region: location?.regionCode ?? null,
1117
+ jurisdiction,
1118
+ language: translationsResult.language,
1119
+ model: policyDecision.policy.model,
1120
+ policyI18n: policyDecision.policy.i18n,
1121
+ expiryDays: policyDecision.policy.consent?.expiryDays,
1122
+ scopeMode: policyDecision.policy.consent?.scopeMode,
1123
+ uiMode: policyDecision.policy.ui?.mode,
1124
+ bannerUi: policyDecision.policy.ui?.banner,
1125
+ dialogUi: policyDecision.policy.ui?.dialog,
1126
+ categories: policyDecision.policy.consent?.categories,
1127
+ preselectedCategories: policyDecision.policy.consent?.preselectedCategories,
1128
+ gpc: policyDecision.policy.consent?.gpc,
1129
+ proofConfig: policyDecision.policy.proof
1130
+ }) : void 0;
1131
+ const gpc = '1' === request.headers.get('sec-gpc');
1132
+ getMetrics()?.recordInit({
1133
+ jurisdiction,
1134
+ country: location?.countryCode ?? void 0,
1135
+ region: location?.regionCode ?? void 0,
1136
+ gpc
1137
+ });
1138
+ return {
1139
+ jurisdiction,
1140
+ location,
1141
+ translations: responseTranslations,
1142
+ branding: options.branding || 'c15t',
1143
+ ...shouldIncludeIabPayload && {
1144
+ gvl,
1145
+ customVendors
1146
+ },
1147
+ ...resolvedPolicy && {
1148
+ policy: resolvedPolicy
1149
+ },
1150
+ ...policyDecision && {
1151
+ policyDecision: {
1152
+ policyId: policyDecision.policy.id,
1153
+ fingerprint: policyDecision.fingerprint,
1154
+ matchedBy: policyDecision.matchedBy,
1155
+ country: location.countryCode,
1156
+ region: location.regionCode,
1157
+ jurisdiction
1158
+ }
1159
+ },
1160
+ ...snapshot?.token && {
1161
+ policySnapshotToken: snapshot.token
1162
+ },
1163
+ ...shouldIncludeIabPayload && iabOptions?.cmpId != null && {
1164
+ cmpId: iabOptions.cmpId
1165
+ }
826
1166
  };
827
1167
  }
828
1168
  const createInitRoute = (options)=>{
@@ -835,7 +1175,7 @@ const createInitRoute = (options)=>{
835
1175
  - **Location** – User's location (null if geo-location is disabled)
836
1176
  - **Translations** – Consent manager copy (from \`Accept-Language\` header)
837
1177
  - **Branding** – Configured branding key
838
- - **GVL** – Global Vendor List when enabled
1178
+ - **GVL** – Global Vendor List when IAB is active for the request
839
1179
 
840
1180
  Use for geo-targeted consent banners and regional compliance.`,
841
1181
  tags: [
@@ -852,46 +1192,13 @@ Use for geo-targeted consent banners and regional compliance.`,
852
1192
  }
853
1193
  }
854
1194
  }), async (c)=>{
855
- const request = c.req.raw;
856
- const acceptLanguage = request.headers.get('accept-language') || 'en';
857
- const location = await getLocation(request, options);
858
- const jurisdiction = getJurisdiction(location, options);
859
- const translationsResult = translations_getTranslationsData(acceptLanguage, options.customTranslations);
860
- let gvl = null;
861
- if (options.iab?.enabled) {
862
- const language = translationsResult.language.split('-')[0] || 'en';
863
- const gvlResolver = createGVLResolver({
864
- appName: options.appName || 'c15t',
865
- bundled: options.iab.bundled,
866
- cacheAdapter: options.cache?.adapter,
867
- vendorIds: options.iab.vendorIds,
868
- endpoint: options.iab.endpoint
869
- });
870
- gvl = await gvlResolver.get(language);
871
- }
872
- const customVendors = options.iab?.customVendors;
873
- const gpc = '1' === request.headers.get('sec-gpc');
874
- getMetrics()?.recordInit({
875
- jurisdiction,
876
- country: location?.countryCode ?? void 0,
877
- region: location?.regionCode ?? void 0,
878
- gpc
879
- });
880
- return c.json({
881
- jurisdiction,
882
- location,
883
- translations: translationsResult,
884
- branding: options.branding || 'c15t',
885
- gvl,
886
- customVendors,
887
- ...options.iab?.cmpId != null && {
888
- cmpId: options.iab.cmpId
889
- }
890
- });
1195
+ const ctx = c.get('c15tContext');
1196
+ const payload = await resolveInitPayload(c.req.raw, options, ctx?.logger);
1197
+ return c.json(payload);
891
1198
  });
892
1199
  return app;
893
1200
  };
894
- const version_version = '2.0.0-rc.4';
1201
+ const version_version = '2.0.0-rc.5';
895
1202
  function getHeaders(headers) {
896
1203
  if (!headers) return {
897
1204
  countryCode: null,
@@ -1247,6 +1554,119 @@ const patchSubjectHandler = async (c)=>{
1247
1554
  });
1248
1555
  }
1249
1556
  };
1557
+ function buildRuntimeDecisionDedupeKey(input) {
1558
+ return [
1559
+ input.tenantId ?? 'default',
1560
+ input.fingerprint,
1561
+ input.matchedBy,
1562
+ input.countryCode ?? 'none',
1563
+ input.regionCode ?? 'none',
1564
+ input.jurisdiction,
1565
+ input.language ?? 'none'
1566
+ ].join('|');
1567
+ }
1568
+ function buildDecisionPayload(params) {
1569
+ const { tenantId, snapshot, decision, location, jurisdiction, language, proofConfig } = params;
1570
+ if (snapshot?.valid && snapshot.payload) {
1571
+ const sp = snapshot.payload;
1572
+ return {
1573
+ tenantId,
1574
+ policyId: sp.policyId,
1575
+ fingerprint: sp.fingerprint,
1576
+ matchedBy: sp.matchedBy,
1577
+ countryCode: sp.country,
1578
+ regionCode: sp.region,
1579
+ jurisdiction: sp.jurisdiction,
1580
+ language: sp.language,
1581
+ model: sp.model,
1582
+ policyI18n: sp.policyI18n,
1583
+ uiMode: sp.uiMode,
1584
+ bannerUi: sp.bannerUi,
1585
+ dialogUi: sp.dialogUi,
1586
+ categories: sp.categories,
1587
+ preselectedCategories: sp.preselectedCategories,
1588
+ proofConfig: sp.proofConfig,
1589
+ dedupeKey: buildRuntimeDecisionDedupeKey({
1590
+ tenantId,
1591
+ fingerprint: sp.fingerprint,
1592
+ matchedBy: sp.matchedBy,
1593
+ countryCode: sp.country,
1594
+ regionCode: sp.region,
1595
+ jurisdiction: sp.jurisdiction,
1596
+ language: sp.language
1597
+ }),
1598
+ source: 'snapshot_token'
1599
+ };
1600
+ }
1601
+ if (decision) return {
1602
+ tenantId,
1603
+ policyId: decision.policy.id,
1604
+ fingerprint: decision.fingerprint,
1605
+ matchedBy: decision.matchedBy,
1606
+ countryCode: location.countryCode,
1607
+ regionCode: location.regionCode,
1608
+ jurisdiction,
1609
+ language,
1610
+ model: decision.policy.model,
1611
+ policyI18n: decision.policy.i18n,
1612
+ uiMode: decision.policy.ui?.mode,
1613
+ bannerUi: decision.policy.ui?.banner,
1614
+ dialogUi: decision.policy.ui?.dialog,
1615
+ categories: decision.policy.consent?.categories,
1616
+ preselectedCategories: decision.policy.consent?.preselectedCategories,
1617
+ proofConfig,
1618
+ dedupeKey: buildRuntimeDecisionDedupeKey({
1619
+ tenantId,
1620
+ fingerprint: decision.fingerprint,
1621
+ matchedBy: decision.matchedBy,
1622
+ countryCode: location.countryCode,
1623
+ regionCode: location.regionCode,
1624
+ jurisdiction,
1625
+ language
1626
+ }),
1627
+ source: 'write_time_fallback'
1628
+ };
1629
+ }
1630
+ function parseLanguageFromHeader(header) {
1631
+ if (!header) return;
1632
+ const firstLanguage = header.split(',')[0]?.split(';')[0]?.trim();
1633
+ if (!firstLanguage) return;
1634
+ return firstLanguage.split('-')[0]?.toLowerCase();
1635
+ }
1636
+ function resolveSnapshotFailureMode(ctx) {
1637
+ return ctx.policySnapshot?.onValidationFailure ?? 'reject';
1638
+ }
1639
+ function buildSnapshotHttpException(reason) {
1640
+ switch(reason){
1641
+ case 'missing':
1642
+ return new http_exception_namespaceObject.HTTPException(409, {
1643
+ message: 'Policy snapshot token is required',
1644
+ cause: {
1645
+ code: 'POLICY_SNAPSHOT_REQUIRED'
1646
+ }
1647
+ });
1648
+ case 'expired':
1649
+ return new http_exception_namespaceObject.HTTPException(409, {
1650
+ message: 'Policy snapshot token has expired',
1651
+ cause: {
1652
+ code: 'POLICY_SNAPSHOT_EXPIRED'
1653
+ }
1654
+ });
1655
+ case 'malformed':
1656
+ case 'invalid':
1657
+ return new http_exception_namespaceObject.HTTPException(409, {
1658
+ message: 'Policy snapshot token is invalid',
1659
+ cause: {
1660
+ code: 'POLICY_SNAPSHOT_INVALID'
1661
+ }
1662
+ });
1663
+ default:
1664
+ {
1665
+ const _exhaustive = reason;
1666
+ throw new Error(`Unhandled policy snapshot verification failure reason: ${_exhaustive}`);
1667
+ }
1668
+ }
1669
+ }
1250
1670
  const postSubjectHandler = async (c)=>{
1251
1671
  const ctx = c.get('c15tContext');
1252
1672
  const logger = ctx.logger;
@@ -1258,9 +1678,6 @@ const postSubjectHandler = async (c)=>{
1258
1678
  const givenAt = new Date(givenAtEpoch);
1259
1679
  const rawConsentAction = 'consentAction' in input ? input.consentAction : void 0;
1260
1680
  let derivedConsentAction;
1261
- if ('all' === rawConsentAction) derivedConsentAction = 'accept_all';
1262
- else if ('necessary' === rawConsentAction) derivedConsentAction = 'opt-out' === input.jurisdictionModel ? 'opt_out' : 'reject_all';
1263
- else if ('custom' === rawConsentAction) derivedConsentAction = 'custom';
1264
1681
  logger.debug('Request parameters', {
1265
1682
  type,
1266
1683
  subjectId,
@@ -1269,6 +1686,50 @@ const postSubjectHandler = async (c)=>{
1269
1686
  domain
1270
1687
  });
1271
1688
  try {
1689
+ if ('cookie_banner' === type) logger.warn('`cookie_banner` policy type is deprecated in 2.0 RC and will be removed in 2.0 GA. Use backend runtime `policyPacks` for banner behavior.');
1690
+ const request = c.req.raw ?? new Request('https://c15t.local/subjects');
1691
+ const acceptLanguage = request.headers.get('accept-language');
1692
+ const requestLanguage = parseLanguageFromHeader(acceptLanguage);
1693
+ const location = await getLocation(request, ctx);
1694
+ const resolvedJurisdiction = getJurisdiction(location, ctx);
1695
+ const snapshotVerification = await verifyPolicySnapshotToken({
1696
+ token: input.policySnapshotToken,
1697
+ options: ctx.policySnapshot,
1698
+ tenantId: ctx.tenantId
1699
+ });
1700
+ const hasValidSnapshot = snapshotVerification.valid;
1701
+ const snapshotPayload = snapshotVerification.valid ? snapshotVerification.payload : null;
1702
+ const shouldRequireSnapshot = !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
1703
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(snapshotVerification.reason);
1704
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : await resolvePolicyDecision({
1705
+ policies: ctx.policyPacks,
1706
+ countryCode: location.countryCode,
1707
+ regionCode: location.regionCode,
1708
+ jurisdiction: resolvedJurisdiction,
1709
+ iabEnabled: ctx.iab?.enabled === true
1710
+ });
1711
+ const effectivePolicy = hasValidSnapshot && snapshotPayload ? {
1712
+ id: snapshotPayload.policyId,
1713
+ model: snapshotPayload.model,
1714
+ i18n: snapshotPayload.policyI18n,
1715
+ consent: {
1716
+ expiryDays: snapshotPayload.expiryDays,
1717
+ scopeMode: snapshotPayload.scopeMode,
1718
+ categories: snapshotPayload.categories,
1719
+ preselectedCategories: snapshotPayload.preselectedCategories,
1720
+ gpc: snapshotPayload.gpc
1721
+ },
1722
+ ui: {
1723
+ mode: snapshotPayload.uiMode,
1724
+ banner: snapshotPayload.bannerUi,
1725
+ dialog: snapshotPayload.dialogUi
1726
+ },
1727
+ proof: snapshotPayload.proofConfig
1728
+ } : resolvedPolicyDecision?.policy;
1729
+ const effectiveModel = effectivePolicy?.model ?? ('opt-in' === input.jurisdictionModel || 'opt-out' === input.jurisdictionModel || 'iab' === input.jurisdictionModel ? input.jurisdictionModel : void 0);
1730
+ if ('all' === rawConsentAction) derivedConsentAction = 'accept_all';
1731
+ else if ('necessary' === rawConsentAction) derivedConsentAction = 'opt-out' === effectiveModel ? 'opt_out' : 'reject_all';
1732
+ else if ('custom' === rawConsentAction) derivedConsentAction = 'custom';
1272
1733
  const subject = await registry.findOrCreateSubject({
1273
1734
  subjectId,
1274
1735
  externalSubjectId,
@@ -1295,6 +1756,7 @@ const postSubjectHandler = async (c)=>{
1295
1756
  });
1296
1757
  let policyId;
1297
1758
  let purposeIds = [];
1759
+ let appliedPreferences;
1298
1760
  const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
1299
1761
  if (inputPolicyId) {
1300
1762
  policyId = inputPolicyId;
@@ -1327,20 +1789,66 @@ const postSubjectHandler = async (c)=>{
1327
1789
  policyId = policy.id;
1328
1790
  }
1329
1791
  if (preferences) {
1330
- const consentedPurposes = Object.entries(preferences).filter(([_, isConsented])=>isConsented).map(([purposeCode])=>purposeCode);
1792
+ const allowedCategories = effectivePolicy?.consent?.categories;
1793
+ const effectiveScopeMode = effectivePolicy?.consent?.scopeMode ?? 'permissive';
1794
+ const hasWildcardCategoryScope = allowedCategories?.includes('*') === true;
1795
+ const appliedPreferenceEntries = Object.entries(preferences);
1796
+ let filteredAppliedPreferenceEntries = appliedPreferenceEntries;
1797
+ if (allowedCategories && allowedCategories.length > 0 && !hasWildcardCategoryScope) {
1798
+ const disallowed = appliedPreferenceEntries.map(([purpose])=>purpose).filter((purpose)=>!allowedCategories.includes(purpose));
1799
+ filteredAppliedPreferenceEntries = appliedPreferenceEntries.filter(([purpose])=>allowedCategories.includes(purpose));
1800
+ if (disallowed.length > 0 && 'strict' === effectiveScopeMode) throw new http_exception_namespaceObject.HTTPException(400, {
1801
+ message: 'Preferences include categories not allowed by policy',
1802
+ cause: {
1803
+ code: 'PURPOSE_NOT_ALLOWED',
1804
+ disallowed
1805
+ }
1806
+ });
1807
+ }
1808
+ appliedPreferences = Object.fromEntries(filteredAppliedPreferenceEntries);
1809
+ const filteredConsentedPurposeCodes = filteredAppliedPreferenceEntries.filter(([_, isConsented])=>isConsented).map(([purposeCode])=>purposeCode);
1331
1810
  logger.debug('Consented purposes', {
1332
- consentedPurposes
1811
+ consentedPurposes: filteredConsentedPurposeCodes
1333
1812
  });
1334
- const purposesRaw = await Promise.all(consentedPurposes.map((purposeCode)=>registry.findOrCreateConsentPurposeByCode(purposeCode)));
1813
+ const purposesRaw = await Promise.all(filteredConsentedPurposeCodes.map((purposeCode)=>registry.findOrCreateConsentPurposeByCode(purposeCode)));
1335
1814
  const purposes = purposesRaw.map((purpose)=>purpose?.id ?? null).filter((id)=>Boolean(id));
1336
1815
  logger.debug('Filtered purposes', {
1337
1816
  purposes
1338
1817
  });
1339
1818
  if (0 === purposes.length) logger.warn('No valid purpose IDs found after filtering. Using empty list.', {
1340
- consentedPurposes
1819
+ consentedPurposes: filteredConsentedPurposeCodes
1341
1820
  });
1342
1821
  purposeIds = purposes;
1343
1822
  }
1823
+ const expiryDays = effectivePolicy?.consent?.expiryDays;
1824
+ const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
1825
+ const proofConfig = effectivePolicy?.proof;
1826
+ const shouldStoreIp = proofConfig?.storeIp ?? true;
1827
+ const shouldStoreUserAgent = proofConfig?.storeUserAgent ?? true;
1828
+ const shouldStoreLanguage = proofConfig?.storeLanguage ?? false;
1829
+ const effectiveLanguage = (snapshotPayload?.language && hasValidSnapshot ? snapshotPayload.language : requestLanguage) ?? void 0;
1830
+ const metadataWithPolicy = {
1831
+ ...metadata ?? {},
1832
+ ...shouldStoreLanguage && effectiveLanguage ? {
1833
+ policyLanguage: effectiveLanguage
1834
+ } : {}
1835
+ };
1836
+ const effectiveJurisdiction = hasValidSnapshot && snapshotPayload ? snapshotPayload.jurisdiction : resolvedJurisdiction;
1837
+ const decisionPayload = buildDecisionPayload({
1838
+ tenantId: ctx.tenantId,
1839
+ snapshot: hasValidSnapshot && snapshotPayload ? {
1840
+ valid: true,
1841
+ payload: snapshotPayload
1842
+ } : null,
1843
+ decision: resolvedPolicyDecision,
1844
+ location: {
1845
+ countryCode: location.countryCode,
1846
+ regionCode: location.regionCode
1847
+ },
1848
+ jurisdiction: resolvedJurisdiction,
1849
+ language: effectiveLanguage,
1850
+ proofConfig
1851
+ });
1344
1852
  const existingConsent = await db.findFirst('consent', {
1345
1853
  where: (b)=>b.and(b('subjectId', '=', subject.id), b('domainId', '=', domainRecord.id), b('policyId', '=', policyId), b('givenAt', '=', givenAt))
1346
1854
  });
@@ -1355,6 +1863,7 @@ const postSubjectHandler = async (c)=>{
1355
1863
  domain: domainRecord.name,
1356
1864
  type,
1357
1865
  metadata,
1866
+ appliedPreferences,
1358
1867
  uiSource: input.uiSource,
1359
1868
  givenAt: existingConsent.givenAt
1360
1869
  });
@@ -1366,6 +1875,42 @@ const postSubjectHandler = async (c)=>{
1366
1875
  policyId,
1367
1876
  purposeIds
1368
1877
  });
1878
+ const runtimePolicyDecision = decisionPayload ? await tx.findFirst('runtimePolicyDecision', {
1879
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1880
+ }) ?? await tx.create('runtimePolicyDecision', {
1881
+ id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
1882
+ tenantId: decisionPayload.tenantId,
1883
+ policyId: decisionPayload.policyId,
1884
+ fingerprint: decisionPayload.fingerprint,
1885
+ matchedBy: decisionPayload.matchedBy,
1886
+ countryCode: decisionPayload.countryCode,
1887
+ regionCode: decisionPayload.regionCode,
1888
+ jurisdiction: decisionPayload.jurisdiction,
1889
+ language: decisionPayload.language,
1890
+ model: decisionPayload.model,
1891
+ policyI18n: decisionPayload.policyI18n ? {
1892
+ json: decisionPayload.policyI18n
1893
+ } : void 0,
1894
+ uiMode: decisionPayload.uiMode,
1895
+ bannerUi: decisionPayload.bannerUi ? {
1896
+ json: decisionPayload.bannerUi
1897
+ } : void 0,
1898
+ dialogUi: decisionPayload.dialogUi ? {
1899
+ json: decisionPayload.dialogUi
1900
+ } : void 0,
1901
+ categories: decisionPayload.categories ? {
1902
+ json: decisionPayload.categories
1903
+ } : void 0,
1904
+ preselectedCategories: decisionPayload.preselectedCategories ? {
1905
+ json: decisionPayload.preselectedCategories
1906
+ } : void 0,
1907
+ proofConfig: decisionPayload.proofConfig ? {
1908
+ json: decisionPayload.proofConfig
1909
+ } : void 0,
1910
+ dedupeKey: decisionPayload.dedupeKey
1911
+ }).catch(async ()=>tx.findFirst('runtimePolicyDecision', {
1912
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1913
+ })) : void 0;
1369
1914
  const consentRecord = await tx.create('consent', {
1370
1915
  id: await generateUniqueId(tx, 'consent', ctx),
1371
1916
  subjectId: subject.id,
@@ -1374,17 +1919,20 @@ const postSubjectHandler = async (c)=>{
1374
1919
  purposeIds: {
1375
1920
  json: purposeIds
1376
1921
  },
1377
- metadata: metadata ? {
1378
- json: metadata
1922
+ metadata: Object.keys(metadataWithPolicy).length > 0 ? {
1923
+ json: metadataWithPolicy
1379
1924
  } : void 0,
1380
- ipAddress: ctx.ipAddress,
1381
- userAgent: ctx.userAgent,
1382
- jurisdiction: input.jurisdiction,
1383
- jurisdictionModel: input.jurisdictionModel,
1925
+ ipAddress: shouldStoreIp ? ctx.ipAddress : null,
1926
+ userAgent: shouldStoreUserAgent ? ctx.userAgent : null,
1927
+ jurisdiction: effectiveJurisdiction,
1928
+ jurisdictionModel: effectiveModel,
1384
1929
  tcString: input.tcString,
1385
1930
  uiSource: input.uiSource,
1386
1931
  consentAction: derivedConsentAction,
1387
- givenAt
1932
+ givenAt,
1933
+ validUntil,
1934
+ runtimePolicyDecisionId: runtimePolicyDecision?.id,
1935
+ runtimePolicySource: decisionPayload?.source
1388
1936
  });
1389
1937
  logger.debug('Created consent', {
1390
1938
  consentRecord: consentRecord.id
@@ -1403,7 +1951,7 @@ const postSubjectHandler = async (c)=>{
1403
1951
  });
1404
1952
  const metrics = getMetrics();
1405
1953
  if (metrics) {
1406
- const jurisdiction = input.jurisdiction;
1954
+ const jurisdiction = effectiveJurisdiction;
1407
1955
  metrics.recordConsentCreated({
1408
1956
  type,
1409
1957
  jurisdiction
@@ -1425,6 +1973,7 @@ const postSubjectHandler = async (c)=>{
1425
1973
  domain: domainRecord.name,
1426
1974
  type,
1427
1975
  metadata,
1976
+ appliedPreferences,
1428
1977
  uiSource: input.uiSource,
1429
1978
  givenAt: result.consent.givenAt
1430
1979
  });