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

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