@c15t/backend 2.0.0-rc.0 → 2.0.0-rc.10

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 (336) hide show
  1. package/README.md +3 -3
  2. package/dist/302.js +473 -0
  3. package/dist/583.js +540 -0
  4. package/dist/915.js +1771 -0
  5. package/dist/cache.cjs +5 -5
  6. package/dist/cache.js +4 -415
  7. package/dist/core.cjs +1356 -120
  8. package/dist/core.js +163 -1981
  9. package/dist/db/adapters/drizzle.cjs +1 -1
  10. package/dist/db/adapters/drizzle.js +1 -2
  11. package/dist/db/adapters/kysely.cjs +1 -1
  12. package/dist/db/adapters/kysely.js +1 -2
  13. package/dist/db/adapters/mongo.cjs +1 -1
  14. package/dist/db/adapters/mongo.js +1 -2
  15. package/dist/db/adapters/prisma.cjs +1 -1
  16. package/dist/db/adapters/prisma.js +1 -2
  17. package/dist/db/adapters/typeorm.cjs +1 -1
  18. package/dist/db/adapters/typeorm.js +1 -2
  19. package/dist/db/adapters.cjs +1 -1
  20. package/dist/db/migrator.cjs +1 -1
  21. package/dist/db/schema.cjs +43 -3
  22. package/dist/db/schema.js +35 -4
  23. package/dist/define-config.cjs +1 -1
  24. package/dist/edge.cjs +1106 -0
  25. package/dist/edge.js +190 -0
  26. package/dist/router.cjs +885 -123
  27. package/dist/router.js +1 -1507
  28. package/dist/{types.cjs → types/index.cjs} +1 -1
  29. package/{dist → dist-types}/cache/adapters/cloudflare-kv.d.ts +0 -1
  30. package/{dist → dist-types}/cache/adapters/index.d.ts +0 -1
  31. package/{dist → dist-types}/cache/adapters/memory.d.ts +0 -1
  32. package/{dist → dist-types}/cache/adapters/upstash-redis.d.ts +0 -1
  33. package/{dist → dist-types}/cache/gvl-resolver.d.ts +0 -1
  34. package/{dist → dist-types}/cache/index.d.ts +0 -1
  35. package/{dist → dist-types}/cache/keys.d.ts +0 -1
  36. package/{dist → dist-types}/cache/types.d.ts +0 -1
  37. package/{dist → dist-types}/core.d.ts +8 -1
  38. package/{dist → dist-types}/db/migrator/index.d.ts +0 -1
  39. package/dist-types/db/registry/consent-policy.d.ts +78 -0
  40. package/{dist → dist-types}/db/registry/consent-purpose.d.ts +0 -1
  41. package/{dist → dist-types}/db/registry/domain.d.ts +0 -1
  42. package/dist-types/db/registry/index.d.ts +118 -0
  43. package/dist-types/db/registry/runtime-policy-decision.d.ts +60 -0
  44. package/{dist → dist-types}/db/registry/subject.d.ts +0 -2
  45. package/{dist → dist-types}/db/registry/types.d.ts +1 -1
  46. package/{dist → dist-types}/db/registry/utils/generate-id.d.ts +0 -1
  47. package/{dist → dist-types}/db/registry/utils.d.ts +0 -1
  48. package/{dist → dist-types}/db/schema/1.0.0/audit-log.d.ts +0 -1
  49. package/{dist → dist-types}/db/schema/1.0.0/consent-policy.d.ts +0 -1
  50. package/{dist → dist-types}/db/schema/1.0.0/consent-purpose.d.ts +0 -1
  51. package/{dist → dist-types}/db/schema/1.0.0/consent-record.d.ts +0 -1
  52. package/{dist → dist-types}/db/schema/1.0.0/consent.d.ts +1 -2
  53. package/{dist → dist-types}/db/schema/1.0.0/domain.d.ts +0 -1
  54. package/{dist → dist-types}/db/schema/1.0.0/index.d.ts +0 -32
  55. package/{dist → dist-types}/db/schema/1.0.0/subject.d.ts +0 -2
  56. package/{dist → dist-types}/db/schema/2.0.0/audit-log.d.ts +1 -2
  57. package/{dist → dist-types}/db/schema/2.0.0/consent-policy.d.ts +3 -3
  58. package/{dist → dist-types}/db/schema/2.0.0/consent-purpose.d.ts +1 -2
  59. package/{dist → dist-types}/db/schema/2.0.0/consent.d.ts +7 -2
  60. package/{dist → dist-types}/db/schema/2.0.0/domain.d.ts +1 -2
  61. package/{dist → dist-types}/db/schema/2.0.0/index.d.ts +455 -28
  62. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +23 -0
  63. package/{dist → dist-types}/db/schema/2.0.0/subject.d.ts +1 -3
  64. package/{dist → dist-types}/db/schema/index.d.ts +908 -86
  65. package/{dist → dist-types}/db/tenant-scope.d.ts +0 -1
  66. package/dist-types/define-config.d.ts +17 -0
  67. package/dist-types/edge/index.d.ts +5 -0
  68. package/dist-types/edge/init-handler.d.ts +40 -0
  69. package/dist-types/edge/resolve-consent.d.ts +80 -0
  70. package/dist-types/edge/types.d.ts +13 -0
  71. package/{dist → dist-types}/handlers/consent/check.handler.d.ts +0 -1
  72. package/{src/handlers/consent/index.ts → dist-types/handlers/consent/index.d.ts} +0 -1
  73. package/{dist → dist-types}/handlers/init/geo.d.ts +2 -3
  74. package/{dist → dist-types}/handlers/init/index.d.ts +2 -3
  75. package/dist-types/handlers/init/policy.d.ts +26 -0
  76. package/dist-types/handlers/init/resolve-init.d.ts +44 -0
  77. package/dist-types/handlers/init/translations.d.ts +48 -0
  78. package/dist-types/handlers/legal-document/current.handler.d.ts +11 -0
  79. package/dist-types/handlers/legal-document/snapshot.d.ts +39 -0
  80. package/dist-types/handlers/policy/snapshot.d.ts +99 -0
  81. package/{src/handlers/status/index.ts → dist-types/handlers/status/index.d.ts} +0 -1
  82. package/{dist → dist-types}/handlers/status/status.handler.d.ts +0 -1
  83. package/{dist → dist-types}/handlers/subject/get.handler.d.ts +3 -2
  84. package/{src/handlers/subject/index.ts → dist-types/handlers/subject/index.d.ts} +0 -1
  85. package/{dist → dist-types}/handlers/subject/list.handler.d.ts +3 -2
  86. package/{dist → dist-types}/handlers/subject/patch.handler.d.ts +0 -2
  87. package/{dist → dist-types}/handlers/subject/post.handler.d.ts +12 -1
  88. package/{dist → dist-types}/handlers/utils/consent-enrichment.d.ts +3 -1
  89. package/{dist → dist-types}/init.d.ts +4 -7
  90. package/{dist → dist-types}/middleware/auth/index.d.ts +0 -1
  91. package/{dist → dist-types}/middleware/auth/validate-api-key.d.ts +0 -1
  92. package/{dist → dist-types}/middleware/cors/cors.d.ts +0 -1
  93. package/{src/middleware/cors/index.ts → dist-types/middleware/cors/index.d.ts} +0 -1
  94. package/{dist → dist-types}/middleware/cors/is-origin-trusted.d.ts +0 -1
  95. package/{dist → dist-types}/middleware/cors/process-cors.d.ts +0 -1
  96. package/{dist → dist-types}/middleware/openapi/config.d.ts +0 -1
  97. package/{dist → dist-types}/middleware/openapi/handlers.d.ts +0 -1
  98. package/{src/middleware/openapi/index.ts → dist-types/middleware/openapi/index.d.ts} +0 -1
  99. package/{dist → dist-types}/middleware/process-ip/index.d.ts +0 -1
  100. package/dist-types/policies/builder.d.ts +127 -0
  101. package/dist-types/policies/defaults.d.ts +2 -0
  102. package/dist-types/policies/matchers.d.ts +3 -0
  103. package/{dist → dist-types}/router.d.ts +0 -1
  104. package/{dist → dist-types}/routes/consent.d.ts +0 -1
  105. package/{dist → dist-types}/routes/index.d.ts +1 -1
  106. package/{dist → dist-types}/routes/init.d.ts +0 -1
  107. package/dist-types/routes/legal-document.d.ts +7 -0
  108. package/{dist → dist-types}/routes/status.d.ts +0 -1
  109. package/{dist → dist-types}/routes/subject.d.ts +0 -1
  110. package/{dist → dist-types}/types/api.d.ts +0 -1
  111. package/dist-types/types/index.d.ts +464 -0
  112. package/dist-types/utils/background.d.ts +6 -0
  113. package/{dist → dist-types}/utils/create-telemetry-options.d.ts +1 -2
  114. package/{dist → dist-types}/utils/env.d.ts +0 -1
  115. package/{dist → dist-types}/utils/extract-error-message.d.ts +0 -1
  116. package/{dist → dist-types}/utils/instrumentation.d.ts +2 -3
  117. package/{dist → dist-types}/utils/logger.d.ts +0 -1
  118. package/{dist → dist-types}/utils/metrics.d.ts +0 -1
  119. package/dist-types/version.d.ts +1 -0
  120. package/docs/README.md +49 -0
  121. package/docs/api/configuration.md +208 -0
  122. package/docs/api/endpoints.md +211 -0
  123. package/docs/guides/caching.md +85 -0
  124. package/docs/guides/database-setup.md +128 -0
  125. package/docs/guides/edge-deployment.md +251 -0
  126. package/docs/guides/framework-integration.md +142 -0
  127. package/docs/guides/iab-tcf.md +89 -0
  128. package/docs/guides/observability.md +96 -0
  129. package/docs/guides/policy-packs.md +396 -0
  130. package/docs/quickstart.md +129 -0
  131. package/package.json +53 -39
  132. package/.turbo/turbo-build.log +0 -49
  133. package/CHANGELOG.md +0 -89
  134. package/dist/cache/adapters/cloudflare-kv.d.ts.map +0 -1
  135. package/dist/cache/adapters/index.d.ts.map +0 -1
  136. package/dist/cache/adapters/memory.d.ts.map +0 -1
  137. package/dist/cache/adapters/upstash-redis.d.ts.map +0 -1
  138. package/dist/cache/gvl-resolver.d.ts.map +0 -1
  139. package/dist/cache/index.d.ts.map +0 -1
  140. package/dist/cache/keys.d.ts.map +0 -1
  141. package/dist/cache/types.d.ts.map +0 -1
  142. package/dist/core.d.ts.map +0 -1
  143. package/dist/db/adapters/drizzle.d.ts +0 -2
  144. package/dist/db/adapters/drizzle.d.ts.map +0 -1
  145. package/dist/db/adapters/index.d.ts +0 -2
  146. package/dist/db/adapters/index.d.ts.map +0 -1
  147. package/dist/db/adapters/kysely.d.ts +0 -2
  148. package/dist/db/adapters/kysely.d.ts.map +0 -1
  149. package/dist/db/adapters/mongo.d.ts +0 -2
  150. package/dist/db/adapters/mongo.d.ts.map +0 -1
  151. package/dist/db/adapters/prisma.d.ts +0 -2
  152. package/dist/db/adapters/prisma.d.ts.map +0 -1
  153. package/dist/db/adapters/typeorm.d.ts +0 -2
  154. package/dist/db/adapters/typeorm.d.ts.map +0 -1
  155. package/dist/db/migrator/index.d.ts.map +0 -1
  156. package/dist/db/registry/consent-policy.d.ts +0 -23
  157. package/dist/db/registry/consent-policy.d.ts.map +0 -1
  158. package/dist/db/registry/consent-purpose.d.ts.map +0 -1
  159. package/dist/db/registry/domain.d.ts.map +0 -1
  160. package/dist/db/registry/index.d.ts +0 -57
  161. package/dist/db/registry/index.d.ts.map +0 -1
  162. package/dist/db/registry/subject.d.ts.map +0 -1
  163. package/dist/db/registry/types.d.ts.map +0 -1
  164. package/dist/db/registry/utils/generate-id.d.ts.map +0 -1
  165. package/dist/db/registry/utils.d.ts.map +0 -1
  166. package/dist/db/schema/1.0.0/audit-log.d.ts.map +0 -1
  167. package/dist/db/schema/1.0.0/consent-policy.d.ts.map +0 -1
  168. package/dist/db/schema/1.0.0/consent-purpose.d.ts.map +0 -1
  169. package/dist/db/schema/1.0.0/consent-record.d.ts.map +0 -1
  170. package/dist/db/schema/1.0.0/consent.d.ts.map +0 -1
  171. package/dist/db/schema/1.0.0/domain.d.ts.map +0 -1
  172. package/dist/db/schema/1.0.0/index.d.ts.map +0 -1
  173. package/dist/db/schema/1.0.0/subject.d.ts.map +0 -1
  174. package/dist/db/schema/2.0.0/audit-log.d.ts.map +0 -1
  175. package/dist/db/schema/2.0.0/consent-policy.d.ts.map +0 -1
  176. package/dist/db/schema/2.0.0/consent-purpose.d.ts.map +0 -1
  177. package/dist/db/schema/2.0.0/consent.d.ts.map +0 -1
  178. package/dist/db/schema/2.0.0/domain.d.ts.map +0 -1
  179. package/dist/db/schema/2.0.0/index.d.ts.map +0 -1
  180. package/dist/db/schema/2.0.0/subject.d.ts.map +0 -1
  181. package/dist/db/schema/index.d.ts.map +0 -1
  182. package/dist/db/tenant-scope.d.ts.map +0 -1
  183. package/dist/define-config.d.ts +0 -5
  184. package/dist/define-config.d.ts.map +0 -1
  185. package/dist/handlers/consent/check.handler.d.ts.map +0 -1
  186. package/dist/handlers/consent/index.d.ts +0 -12
  187. package/dist/handlers/consent/index.d.ts.map +0 -1
  188. package/dist/handlers/init/geo.d.ts.map +0 -1
  189. package/dist/handlers/init/index.d.ts.map +0 -1
  190. package/dist/handlers/init/translations.d.ts +0 -28
  191. package/dist/handlers/init/translations.d.ts.map +0 -1
  192. package/dist/handlers/status/index.d.ts +0 -7
  193. package/dist/handlers/status/index.d.ts.map +0 -1
  194. package/dist/handlers/status/status.handler.d.ts.map +0 -1
  195. package/dist/handlers/subject/get.handler.d.ts.map +0 -1
  196. package/dist/handlers/subject/index.d.ts +0 -10
  197. package/dist/handlers/subject/index.d.ts.map +0 -1
  198. package/dist/handlers/subject/list.handler.d.ts.map +0 -1
  199. package/dist/handlers/subject/patch.handler.d.ts.map +0 -1
  200. package/dist/handlers/subject/post.handler.d.ts.map +0 -1
  201. package/dist/handlers/utils/consent-enrichment.d.ts.map +0 -1
  202. package/dist/init.d.ts.map +0 -1
  203. package/dist/middleware/auth/index.d.ts.map +0 -1
  204. package/dist/middleware/auth/validate-api-key.d.ts.map +0 -1
  205. package/dist/middleware/cors/cors.d.ts.map +0 -1
  206. package/dist/middleware/cors/index.d.ts +0 -30
  207. package/dist/middleware/cors/index.d.ts.map +0 -1
  208. package/dist/middleware/cors/is-origin-trusted.d.ts.map +0 -1
  209. package/dist/middleware/cors/process-cors.d.ts.map +0 -1
  210. package/dist/middleware/openapi/config.d.ts.map +0 -1
  211. package/dist/middleware/openapi/handlers.d.ts.map +0 -1
  212. package/dist/middleware/openapi/index.d.ts +0 -12
  213. package/dist/middleware/openapi/index.d.ts.map +0 -1
  214. package/dist/middleware/process-ip/index.d.ts.map +0 -1
  215. package/dist/router.d.ts.map +0 -1
  216. package/dist/routes/consent.d.ts.map +0 -1
  217. package/dist/routes/index.d.ts.map +0 -1
  218. package/dist/routes/init.d.ts.map +0 -1
  219. package/dist/routes/status.d.ts.map +0 -1
  220. package/dist/routes/subject.d.ts.map +0 -1
  221. package/dist/types/api.d.ts.map +0 -1
  222. package/dist/types/index.d.ts +0 -255
  223. package/dist/types/index.d.ts.map +0 -1
  224. package/dist/utils/create-telemetry-options.d.ts.map +0 -1
  225. package/dist/utils/env.d.ts.map +0 -1
  226. package/dist/utils/extract-error-message.d.ts.map +0 -1
  227. package/dist/utils/index.d.ts +0 -4
  228. package/dist/utils/index.d.ts.map +0 -1
  229. package/dist/utils/instrumentation.d.ts.map +0 -1
  230. package/dist/utils/logger.d.ts.map +0 -1
  231. package/dist/utils/metrics.d.ts.map +0 -1
  232. package/dist/version.d.ts +0 -2
  233. package/dist/version.d.ts.map +0 -1
  234. package/knip.json +0 -31
  235. package/rslib.config.ts +0 -93
  236. package/src/cache/adapters/cloudflare-kv.ts +0 -71
  237. package/src/cache/adapters/index.ts +0 -22
  238. package/src/cache/adapters/memory.ts +0 -111
  239. package/src/cache/adapters/upstash-redis.ts +0 -113
  240. package/src/cache/gvl-resolver.ts +0 -289
  241. package/src/cache/index.ts +0 -34
  242. package/src/cache/keys.ts +0 -68
  243. package/src/cache/types.ts +0 -66
  244. package/src/core.ts +0 -368
  245. package/src/db/migrator/index.ts +0 -80
  246. package/src/db/registry/consent-policy.test.ts +0 -451
  247. package/src/db/registry/consent-policy.ts +0 -82
  248. package/src/db/registry/consent-purpose.test.ts +0 -428
  249. package/src/db/registry/consent-purpose.ts +0 -61
  250. package/src/db/registry/domain.test.ts +0 -445
  251. package/src/db/registry/domain.ts +0 -91
  252. package/src/db/registry/index.ts +0 -14
  253. package/src/db/registry/subject.test.ts +0 -388
  254. package/src/db/registry/subject.ts +0 -129
  255. package/src/db/registry/types.ts +0 -10
  256. package/src/db/registry/utils/generate-id.test.ts +0 -216
  257. package/src/db/registry/utils/generate-id.ts +0 -133
  258. package/src/db/registry/utils.ts +0 -133
  259. package/src/db/schema/1.0.0/audit-log.ts +0 -15
  260. package/src/db/schema/1.0.0/consent-policy.ts +0 -14
  261. package/src/db/schema/1.0.0/consent-purpose.ts +0 -14
  262. package/src/db/schema/1.0.0/consent-record.ts +0 -10
  263. package/src/db/schema/1.0.0/consent.ts +0 -20
  264. package/src/db/schema/1.0.0/domain.ts +0 -12
  265. package/src/db/schema/1.0.0/index.ts +0 -48
  266. package/src/db/schema/1.0.0/subject.ts +0 -12
  267. package/src/db/schema/2.0.0/audit-log.ts +0 -18
  268. package/src/db/schema/2.0.0/consent-policy.ts +0 -28
  269. package/src/db/schema/2.0.0/consent-purpose.ts +0 -12
  270. package/src/db/schema/2.0.0/consent.ts +0 -26
  271. package/src/db/schema/2.0.0/domain.ts +0 -12
  272. package/src/db/schema/2.0.0/index.ts +0 -47
  273. package/src/db/schema/2.0.0/subject.ts +0 -14
  274. package/src/db/schema/index.ts +0 -15
  275. package/src/db/tenant-scope.test.ts +0 -750
  276. package/src/db/tenant-scope.ts +0 -103
  277. package/src/define-config.ts +0 -5
  278. package/src/handlers/consent/check.handler.ts +0 -126
  279. package/src/handlers/init/geo.test.ts +0 -317
  280. package/src/handlers/init/geo.ts +0 -195
  281. package/src/handlers/init/index.test.ts +0 -205
  282. package/src/handlers/init/index.ts +0 -114
  283. package/src/handlers/init/translations.test.ts +0 -121
  284. package/src/handlers/init/translations.ts +0 -72
  285. package/src/handlers/status/status.handler.test.ts +0 -155
  286. package/src/handlers/status/status.handler.ts +0 -51
  287. package/src/handlers/subject/get.handler.ts +0 -93
  288. package/src/handlers/subject/list.handler.ts +0 -93
  289. package/src/handlers/subject/patch.handler.ts +0 -122
  290. package/src/handlers/subject/post.handler.test.ts +0 -294
  291. package/src/handlers/subject/post.handler.ts +0 -254
  292. package/src/handlers/utils/consent-enrichment.test.ts +0 -380
  293. package/src/handlers/utils/consent-enrichment.ts +0 -218
  294. package/src/init.test.ts +0 -126
  295. package/src/init.ts +0 -87
  296. package/src/middleware/auth/index.ts +0 -11
  297. package/src/middleware/auth/validate-api-key.test.ts +0 -86
  298. package/src/middleware/auth/validate-api-key.ts +0 -107
  299. package/src/middleware/cors/cors.test.ts +0 -135
  300. package/src/middleware/cors/cors.ts +0 -186
  301. package/src/middleware/cors/is-origin-trusted.test.ts +0 -164
  302. package/src/middleware/cors/is-origin-trusted.ts +0 -130
  303. package/src/middleware/cors/process-cors.ts +0 -91
  304. package/src/middleware/openapi/config.ts +0 -29
  305. package/src/middleware/openapi/handlers.ts +0 -34
  306. package/src/middleware/process-ip/index.test.ts +0 -195
  307. package/src/middleware/process-ip/index.ts +0 -199
  308. package/src/router.ts +0 -15
  309. package/src/routes/consent.ts +0 -52
  310. package/src/routes/index.ts +0 -10
  311. package/src/routes/init.ts +0 -102
  312. package/src/routes/status.ts +0 -46
  313. package/src/routes/subject.ts +0 -152
  314. package/src/types/api.ts +0 -48
  315. package/src/types/index.ts +0 -288
  316. package/src/utils/create-telemetry-options.test.ts +0 -302
  317. package/src/utils/create-telemetry-options.ts +0 -229
  318. package/src/utils/env.ts +0 -84
  319. package/src/utils/extract-error-message.ts +0 -21
  320. package/src/utils/instrumentation.test.ts +0 -185
  321. package/src/utils/instrumentation.ts +0 -196
  322. package/src/utils/logger.ts +0 -41
  323. package/src/utils/metrics.test.ts +0 -323
  324. package/src/utils/metrics.ts +0 -402
  325. package/src/utils/telemetry-pii.test.ts +0 -325
  326. package/src/version.ts +0 -2
  327. package/tsconfig.json +0 -11
  328. package/vitest.config.ts +0 -28
  329. /package/dist/{types.js → types/index.js} +0 -0
  330. /package/{src/db/adapters/drizzle.ts → dist-types/db/adapters/drizzle.d.ts} +0 -0
  331. /package/{src/db/adapters/index.ts → dist-types/db/adapters/index.d.ts} +0 -0
  332. /package/{src/db/adapters/kysely.ts → dist-types/db/adapters/kysely.d.ts} +0 -0
  333. /package/{src/db/adapters/mongo.ts → dist-types/db/adapters/mongo.d.ts} +0 -0
  334. /package/{src/db/adapters/prisma.ts → dist-types/db/adapters/prisma.d.ts} +0 -0
  335. /package/{src/db/adapters/typeorm.ts → dist-types/db/adapters/typeorm.d.ts} +0 -0
  336. /package/{src/utils/index.ts → dist-types/utils/index.d.ts} +0 -0
package/dist/core.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { createLogger as logger_createLogger } from "@c15t/logger";
2
- import { SpanKind, SpanStatusCode as api_SpanStatusCode, context as api_context, metrics as api_metrics, trace } from "@opentelemetry/api";
2
+ import { SpanStatusCode } from "@opentelemetry/api";
3
3
  import { apiReference } from "@scalar/hono-api-reference";
4
4
  import { Hono } from "hono";
5
5
  import { cors } from "hono/cors";
6
6
  import { HTTPException } from "hono/http-exception";
7
- import { describeRoute, openAPIRouteHandler, resolver, validator } from "hono-openapi";
8
- import base_x from "base-x";
9
- import { fumadb } from "fumadb";
10
- import { column, idColumn, schema, table as schema_table } from "fumadb/schema";
11
- import { checkConsentOutputSchema, checkConsentQuerySchema, getSubjectInputSchema, getSubjectOutputSchema, initOutputSchema, listSubjectsOutputSchema, listSubjectsQuerySchema, patchSubjectOutputSchema, postSubjectInputSchema, postSubjectOutputSchema, statusOutputSchema, subjectIdSchema } from "@c15t/schema";
12
- import { baseTranslations, deepMergeTranslations, selectLanguage } from "@c15t/translations";
13
- import { object, optional, string } from "valibot";
7
+ import { openAPIRouteHandler } from "hono-openapi";
8
+ import { compactDefined, dedupeTrimmedStrings, policyPackPresets } from "@c15t/schema";
9
+ import { EEA_COUNTRY_CODES, EU_COUNTRY_CODES, POLICY_MATCH_DATASET_VERSION, UK_COUNTRY_CODES, policyMatchers } from "@c15t/schema/types";
10
+ import { version as version_version, extractErrorMessage, withDatabaseSpan, getTraceContext as create_telemetry_options_getTraceContext, createRequestSpan, handleSpanError, createTelemetryOptions, getMetrics, isTelemetryEnabled, withSpanContext } from "./302.js";
11
+ import { createLegalDocumentRoutes, createSubjectRoutes, generateUniqueId, createStatusRoute, createInitRoute, createConsentRoutes, policyRegistry } from "./915.js";
12
+ import { validateMessages, inspectPolicies as policy_inspectPolicies } from "./583.js";
13
+ import { DB } from "./db/schema.js";
14
14
  function extractBearerToken(authHeader) {
15
15
  if (!authHeader) return null;
16
16
  const parts = authHeader.split(' ');
@@ -136,12 +136,11 @@ function createCORSOptions(trustedOrigins) {
136
136
  ]
137
137
  };
138
138
  }
139
- const version_version = '2.0.0-rc.0';
140
- const config_createOpenAPIConfig = (options)=>({
141
- enabled: options.advanced?.openapi?.enabled !== false,
139
+ const createOpenAPIConfig = (options)=>({
140
+ enabled: options.openapi?.enabled !== false,
142
141
  specPath: '/spec.json',
143
142
  docsPath: '/docs',
144
- ...options.advanced?.openapi || {}
143
+ ...options.openapi || {}
145
144
  });
146
145
  const DEFAULT_IP_HEADERS = [
147
146
  'x-client-ip',
@@ -236,7 +235,7 @@ function maskIpAddress(ip) {
236
235
  return ip;
237
236
  }
238
237
  function getIpAddress(req, options) {
239
- const ipAddressConfig = options.advanced?.ipAddress;
238
+ const ipAddressConfig = options.ipAddress;
240
239
  if (ipAddressConfig?.tracking === false) return null;
241
240
  const ipHeaders = ipAddressConfig?.ipAddressHeaders || DEFAULT_IP_HEADERS;
242
241
  const headers = req instanceof Request ? req.headers : req;
@@ -252,470 +251,6 @@ function getIpAddress(req, options) {
252
251
  }
253
252
  return null;
254
253
  }
255
- function extractErrorMessage(error) {
256
- if (error instanceof AggregateError && error.errors?.length > 0) {
257
- const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
258
- return `AggregateError: ${inner}`;
259
- }
260
- if (error instanceof Error) return error.message || error.name;
261
- return String(error);
262
- }
263
- let cachedConfig = null;
264
- let cachedDefaultAttributes = {};
265
- function createTelemetryOptions(appName = 'c15t', telemetryConfig, tenantId) {
266
- const defaultAttributes = {
267
- ...telemetryConfig?.defaultAttributes || {},
268
- 'service.name': String(appName),
269
- 'service.version': version_version
270
- };
271
- if (tenantId) defaultAttributes['tenant.id'] = tenantId;
272
- const config = {
273
- enabled: telemetryConfig?.enabled ?? false,
274
- tracer: telemetryConfig?.tracer,
275
- meter: telemetryConfig?.meter,
276
- defaultAttributes
277
- };
278
- cachedConfig = config;
279
- cachedDefaultAttributes = defaultAttributes;
280
- return config;
281
- }
282
- function isTelemetryEnabled(options) {
283
- if (options) return options.advanced?.telemetry?.enabled === true;
284
- return cachedConfig?.enabled === true;
285
- }
286
- const getTracer = (options)=>{
287
- if (!isTelemetryEnabled(options)) return trace.getTracer('c15t-noop');
288
- const tracer = options?.advanced?.telemetry?.tracer ?? cachedConfig?.tracer;
289
- if (tracer) return tracer;
290
- return trace.getTracer(options?.appName ?? 'c15t');
291
- };
292
- const getMeter = (options)=>{
293
- if (!isTelemetryEnabled(options)) return api_metrics.getMeter('c15t-noop');
294
- const meter = options?.advanced?.telemetry?.meter ?? cachedConfig?.meter;
295
- if (meter) return meter;
296
- return api_metrics.getMeter(options?.appName ?? 'c15t');
297
- };
298
- function getDefaultAttributes() {
299
- return cachedDefaultAttributes;
300
- }
301
- const createRequestSpan = (method, path, options)=>{
302
- if (!isTelemetryEnabled(options)) return null;
303
- const tracer = getTracer(options);
304
- const defaultAttrs = options?.advanced?.telemetry?.defaultAttributes || getDefaultAttributes();
305
- const span = tracer.startSpan(`${method} ${path}`, {
306
- attributes: {
307
- 'http.method': method,
308
- ...defaultAttrs
309
- }
310
- });
311
- return span;
312
- };
313
- const handleSpanError = (span, error)=>{
314
- span.setStatus({
315
- code: api_SpanStatusCode.ERROR,
316
- message: extractErrorMessage(error)
317
- });
318
- if (error instanceof Error) span.setAttribute('error.type', error.name);
319
- };
320
- function create_telemetry_options_getTraceContext() {
321
- const activeSpan = trace.getActiveSpan();
322
- if (!activeSpan) return null;
323
- const spanContext = activeSpan.spanContext();
324
- if (!spanContext) return null;
325
- return {
326
- traceId: spanContext.traceId,
327
- spanId: spanContext.spanId
328
- };
329
- }
330
- const withSpanContext = async (span, operation)=>api_context["with"](trace.setSpan(api_context.active(), span), operation);
331
- async function executeWithSpan(span, operation) {
332
- try {
333
- const result = await withSpanContext(span, operation);
334
- span.setStatus({
335
- code: api_SpanStatusCode.OK
336
- });
337
- return result;
338
- } catch (error) {
339
- handleSpanError(span, error);
340
- throw error;
341
- } finally{
342
- span.end();
343
- }
344
- }
345
- function resolveDefaultAttributes(options) {
346
- return options?.advanced?.telemetry?.defaultAttributes || getDefaultAttributes();
347
- }
348
- async function withDatabaseSpan(attributes, operation, options) {
349
- if (!isTelemetryEnabled(options)) return operation();
350
- const tracer = getTracer(options);
351
- const spanName = `db.${attributes.entity}.${attributes.operation}`;
352
- const span = tracer.startSpan(spanName, {
353
- kind: SpanKind.CLIENT,
354
- attributes: {
355
- 'db.system': 'c15t',
356
- 'db.operation': attributes.operation,
357
- 'db.entity': attributes.entity,
358
- ...resolveDefaultAttributes(options),
359
- ...Object.fromEntries(Object.entries(attributes).filter(([key])=>![
360
- 'operation',
361
- 'entity'
362
- ].includes(key)))
363
- }
364
- });
365
- return executeWithSpan(span, operation);
366
- }
367
- async function withExternalSpan(attributes, operation, options) {
368
- if (!isTelemetryEnabled(options)) return operation();
369
- const tracer = getTracer(options);
370
- const url = new URL(attributes.url);
371
- const spanName = `HTTP ${attributes.method} ${url.hostname}`;
372
- const span = tracer.startSpan(spanName, {
373
- kind: SpanKind.CLIENT,
374
- attributes: {
375
- 'http.method': attributes.method,
376
- 'http.url': `${url.origin}${url.pathname}`,
377
- 'http.host': url.hostname,
378
- ...resolveDefaultAttributes(options),
379
- ...Object.fromEntries(Object.entries(attributes).filter(([key])=>![
380
- 'url',
381
- 'method'
382
- ].includes(key)))
383
- }
384
- });
385
- return executeWithSpan(span, operation);
386
- }
387
- async function withCacheSpan(operation, layer, fn, options) {
388
- if (!isTelemetryEnabled(options)) return fn();
389
- const tracer = getTracer(options);
390
- const spanName = `cache.${layer}.${operation}`;
391
- const span = tracer.startSpan(spanName, {
392
- kind: SpanKind.CLIENT,
393
- attributes: {
394
- 'cache.operation': operation,
395
- 'cache.layer': layer,
396
- ...resolveDefaultAttributes(options)
397
- }
398
- });
399
- return executeWithSpan(span, fn);
400
- }
401
- function sanitizeAttributes(attrs) {
402
- return Object.fromEntries(Object.entries(attrs).filter(([_, v])=>null != v));
403
- }
404
- function createMetrics(meter) {
405
- const consentCreated = meter.createCounter('c15t.consent.created', {
406
- description: 'Number of consent submissions',
407
- unit: '1'
408
- });
409
- const consentAccepted = meter.createCounter('c15t.consent.accepted', {
410
- description: 'Number of consents accepted',
411
- unit: '1'
412
- });
413
- const consentRejected = meter.createCounter('c15t.consent.rejected', {
414
- description: 'Number of consents rejected',
415
- unit: '1'
416
- });
417
- const subjectCreated = meter.createCounter('c15t.subject.created', {
418
- description: 'Number of new subjects created',
419
- unit: '1'
420
- });
421
- const subjectLinked = meter.createCounter('c15t.subject.linked', {
422
- description: 'Number of subjects linked to external ID',
423
- unit: '1'
424
- });
425
- const consentCheckCount = meter.createCounter('c15t.consent_check.count', {
426
- description: 'Number of cross-device consent checks',
427
- unit: '1'
428
- });
429
- const initCount = meter.createCounter('c15t.init.count', {
430
- description: 'Number of init endpoint calls',
431
- unit: '1'
432
- });
433
- const httpRequestDuration = meter.createHistogram('c15t.http.request.duration', {
434
- description: 'HTTP request latency',
435
- unit: 'ms'
436
- });
437
- const httpRequestCount = meter.createCounter('c15t.http.request.count', {
438
- description: 'Number of HTTP requests',
439
- unit: '1'
440
- });
441
- const httpErrorCount = meter.createCounter('c15t.http.error.count', {
442
- description: 'Number of HTTP errors',
443
- unit: '1'
444
- });
445
- const dbQueryDuration = meter.createHistogram('c15t.db.query.duration', {
446
- description: 'Database query latency',
447
- unit: 'ms'
448
- });
449
- const dbQueryCount = meter.createCounter('c15t.db.query.count', {
450
- description: 'Number of database queries',
451
- unit: '1'
452
- });
453
- const dbErrorCount = meter.createCounter('c15t.db.error.count', {
454
- description: 'Number of database errors',
455
- unit: '1'
456
- });
457
- const cacheHit = meter.createCounter('c15t.cache.hit', {
458
- description: 'Number of cache hits',
459
- unit: '1'
460
- });
461
- const cacheMiss = meter.createCounter('c15t.cache.miss', {
462
- description: 'Number of cache misses',
463
- unit: '1'
464
- });
465
- const cacheLatency = meter.createHistogram('c15t.cache.latency', {
466
- description: 'Cache operation latency',
467
- unit: 'ms'
468
- });
469
- const gvlFetchDuration = meter.createHistogram('c15t.gvl.fetch.duration', {
470
- description: 'GVL fetch latency',
471
- unit: 'ms'
472
- });
473
- const gvlFetchCount = meter.createCounter('c15t.gvl.fetch.count', {
474
- description: 'Number of GVL fetches',
475
- unit: '1'
476
- });
477
- const gvlFetchError = meter.createCounter('c15t.gvl.fetch.error', {
478
- description: 'Number of GVL fetch errors',
479
- unit: '1'
480
- });
481
- return {
482
- consentCreated,
483
- consentAccepted,
484
- consentRejected,
485
- subjectCreated,
486
- subjectLinked,
487
- consentCheckCount,
488
- initCount,
489
- httpRequestDuration,
490
- httpRequestCount,
491
- httpErrorCount,
492
- dbQueryDuration,
493
- dbQueryCount,
494
- dbErrorCount,
495
- cacheHit,
496
- cacheMiss,
497
- cacheLatency,
498
- gvlFetchDuration,
499
- gvlFetchCount,
500
- gvlFetchError,
501
- recordConsentCreated (attributes) {
502
- consentCreated.add(1, sanitizeAttributes(attributes));
503
- },
504
- recordConsentAccepted (attributes) {
505
- consentAccepted.add(1, sanitizeAttributes(attributes));
506
- },
507
- recordConsentRejected (attributes) {
508
- consentRejected.add(1, sanitizeAttributes(attributes));
509
- },
510
- recordSubjectCreated (attributes) {
511
- subjectCreated.add(1, sanitizeAttributes(attributes));
512
- },
513
- recordSubjectLinked (identityProvider) {
514
- subjectLinked.add(1, {
515
- identityProvider: identityProvider || 'unknown'
516
- });
517
- },
518
- recordConsentCheck (type, found) {
519
- consentCheckCount.add(1, {
520
- type,
521
- found: String(found)
522
- });
523
- },
524
- recordInit (attributes) {
525
- initCount.add(1, sanitizeAttributes(attributes));
526
- },
527
- recordHttpRequest (attributes, durationMs) {
528
- const attrs = sanitizeAttributes(attributes);
529
- httpRequestCount.add(1, attrs);
530
- httpRequestDuration.record(durationMs, attrs);
531
- if (attributes.status >= 400) httpErrorCount.add(1, attrs);
532
- },
533
- recordDbQuery (attributes, durationMs) {
534
- const attrs = sanitizeAttributes(attributes);
535
- dbQueryCount.add(1, attrs);
536
- dbQueryDuration.record(durationMs, attrs);
537
- },
538
- recordDbError (attributes) {
539
- dbErrorCount.add(1, sanitizeAttributes(attributes));
540
- },
541
- recordCacheHit (layer) {
542
- cacheHit.add(1, {
543
- layer
544
- });
545
- },
546
- recordCacheMiss (layer) {
547
- cacheMiss.add(1, {
548
- layer
549
- });
550
- },
551
- recordCacheLatency (attributes, durationMs) {
552
- cacheLatency.record(durationMs, sanitizeAttributes(attributes));
553
- },
554
- recordGvlFetch (attributes, durationMs) {
555
- const attrs = sanitizeAttributes(attributes);
556
- gvlFetchCount.add(1, attrs);
557
- gvlFetchDuration.record(durationMs, attrs);
558
- },
559
- recordGvlError (attributes) {
560
- gvlFetchError.add(1, sanitizeAttributes(attributes));
561
- }
562
- };
563
- }
564
- let metricsInstance = null;
565
- function getMetrics(options) {
566
- if (metricsInstance) return metricsInstance;
567
- if (!isTelemetryEnabled(options)) return null;
568
- metricsInstance = createMetrics(getMeter(options));
569
- return metricsInstance;
570
- }
571
- const prefixes = {
572
- auditLog: 'log',
573
- consent: 'cns',
574
- consentPolicy: 'pol',
575
- consentPurpose: 'pur',
576
- domain: 'dom',
577
- subject: 'sub'
578
- };
579
- const b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
580
- function generateId(model) {
581
- const buf = crypto.getRandomValues(new Uint8Array(20));
582
- const prefix = prefixes[model];
583
- const EPOCH_TIMESTAMP = 1700000000000;
584
- const t = Date.now() - EPOCH_TIMESTAMP;
585
- const high = Math.floor(t / 0x100000000);
586
- const low = t >>> 0;
587
- buf[0] = high >>> 24 & 255;
588
- buf[1] = high >>> 16 & 255;
589
- buf[2] = high >>> 8 & 255;
590
- buf[3] = 255 & high;
591
- buf[4] = low >>> 24 & 255;
592
- buf[5] = low >>> 16 & 255;
593
- buf[6] = low >>> 8 & 255;
594
- buf[7] = 255 & low;
595
- return `${prefix}_${b58.encode(buf)}`;
596
- }
597
- async function generateUniqueId(db, model, ctx, options = {}) {
598
- const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
599
- if (attempt >= maxRetries) {
600
- const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
601
- ctx?.logger?.error?.('ID generation failed', {
602
- model,
603
- maxRetries
604
- });
605
- throw error;
606
- }
607
- const id = generateId(model);
608
- try {
609
- const existing = await db.findFirst(model, {
610
- where: (b)=>b('id', '=', id)
611
- });
612
- if (existing) {
613
- ctx?.logger?.debug?.('ID conflict detected', {
614
- id,
615
- model,
616
- attempt: attempt + 1,
617
- maxRetries
618
- });
619
- const delay = Math.min(baseDelay * 2 ** attempt, 1000);
620
- await new Promise((resolve)=>setTimeout(resolve, delay));
621
- return generateUniqueId(db, model, ctx, {
622
- maxRetries,
623
- attempt: attempt + 1,
624
- baseDelay
625
- });
626
- }
627
- return id;
628
- } catch (error) {
629
- ctx?.logger?.error?.('Error checking ID uniqueness', {
630
- error: error.message,
631
- model,
632
- attempt
633
- });
634
- if (attempt < maxRetries - 1) {
635
- const delay = Math.min(baseDelay * 2 ** attempt, 2000);
636
- await new Promise((resolve)=>setTimeout(resolve, delay));
637
- return generateUniqueId(db, model, ctx, {
638
- maxRetries,
639
- attempt: attempt + 1,
640
- baseDelay
641
- });
642
- }
643
- throw error;
644
- }
645
- }
646
- function policyRegistry({ db, ctx }) {
647
- const { logger } = ctx;
648
- return {
649
- findConsentPolicyById: async (policyId)=>{
650
- const start = Date.now();
651
- try {
652
- const result = await withDatabaseSpan({
653
- operation: 'find',
654
- entity: 'consentPolicy'
655
- }, async ()=>{
656
- const policy = await db.findFirst('consentPolicy', {
657
- where: (b)=>b('id', '=', policyId)
658
- });
659
- return policy;
660
- });
661
- getMetrics()?.recordDbQuery({
662
- operation: 'find',
663
- entity: 'consentPolicy'
664
- }, Date.now() - start);
665
- return result;
666
- } catch (error) {
667
- getMetrics()?.recordDbError({
668
- operation: 'find',
669
- entity: 'consentPolicy'
670
- });
671
- throw error;
672
- }
673
- },
674
- findOrCreatePolicy: async (type)=>{
675
- const start = Date.now();
676
- try {
677
- const result = await withDatabaseSpan({
678
- operation: 'findOrCreate',
679
- entity: 'consentPolicy'
680
- }, async ()=>{
681
- const existingPolicy = await db.findFirst('consentPolicy', {
682
- where: (b)=>b.and(b('isActive', '=', true), b('type', '=', type)),
683
- orderBy: [
684
- 'effectiveDate',
685
- 'desc'
686
- ]
687
- });
688
- if (existingPolicy) {
689
- logger.debug('Found existing policy', {
690
- type,
691
- policyId: existingPolicy.id
692
- });
693
- return existingPolicy;
694
- }
695
- const policy = await db.create('consentPolicy', {
696
- id: await generateUniqueId(db, 'consentPolicy', ctx),
697
- version: '1.0.0',
698
- type,
699
- effectiveDate: new Date(),
700
- isActive: true
701
- });
702
- return policy;
703
- });
704
- getMetrics()?.recordDbQuery({
705
- operation: 'findOrCreate',
706
- entity: 'consentPolicy'
707
- }, Date.now() - start);
708
- return result;
709
- } catch (error) {
710
- getMetrics()?.recordDbError({
711
- operation: 'findOrCreate',
712
- entity: 'consentPolicy'
713
- });
714
- throw error;
715
- }
716
- }
717
- };
718
- }
719
254
  function consentPurposeRegistry({ db, ctx }) {
720
255
  const { logger } = ctx;
721
256
  return {
@@ -845,6 +380,54 @@ function domainRegistry({ db, ctx }) {
845
380
  }
846
381
  };
847
382
  }
383
+ function runtimePolicyDecisionRegistry({ db, ctx }) {
384
+ const { logger } = ctx;
385
+ return {
386
+ findOrCreateRuntimePolicyDecision: async (input)=>{
387
+ const existing = await db.findFirst('runtimePolicyDecision', {
388
+ where: (b)=>b('dedupeKey', '=', input.dedupeKey)
389
+ });
390
+ if (existing) return existing;
391
+ logger.debug('Creating runtime policy decision', {
392
+ policyId: input.policyId,
393
+ fingerprint: input.fingerprint,
394
+ matchedBy: input.matchedBy
395
+ });
396
+ return db.create('runtimePolicyDecision', {
397
+ id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
398
+ tenantId: input.tenantId,
399
+ policyId: input.policyId,
400
+ fingerprint: input.fingerprint,
401
+ matchedBy: input.matchedBy,
402
+ countryCode: input.countryCode,
403
+ regionCode: input.regionCode,
404
+ jurisdiction: input.jurisdiction,
405
+ language: input.language,
406
+ model: input.model,
407
+ policyI18n: input.policyI18n ? {
408
+ json: input.policyI18n
409
+ } : void 0,
410
+ uiMode: input.uiMode,
411
+ bannerUi: input.bannerUi ? {
412
+ json: input.bannerUi
413
+ } : void 0,
414
+ dialogUi: input.dialogUi ? {
415
+ json: input.dialogUi
416
+ } : void 0,
417
+ categories: input.categories ? {
418
+ json: input.categories
419
+ } : void 0,
420
+ preselectedCategories: input.preselectedCategories ? {
421
+ json: input.preselectedCategories
422
+ } : void 0,
423
+ proofConfig: input.proofConfig ? {
424
+ json: input.proofConfig
425
+ } : void 0,
426
+ dedupeKey: input.dedupeKey
427
+ });
428
+ }
429
+ };
430
+ }
848
431
  function subjectRegistry({ db, ctx }) {
849
432
  const { logger } = ctx;
850
433
  return {
@@ -874,8 +457,7 @@ function subjectRegistry({ db, ctx }) {
874
457
  const newSubject = await db.create('subject', {
875
458
  id: subjectId,
876
459
  externalId: externalSubjectId ?? null,
877
- identityProvider: externalSubjectId ? identityProvider ?? 'external' : 'anonymous',
878
- isIdentified: !!externalSubjectId
460
+ identityProvider: externalSubjectId ? identityProvider ?? 'external' : 'anonymous'
879
461
  });
880
462
  logger.debug('Created new subject', {
881
463
  subject: newSubject
@@ -889,8 +471,7 @@ function subjectRegistry({ db, ctx }) {
889
471
  const subject = await db.create('subject', {
890
472
  id: await generateUniqueId(db, 'subject', ctx),
891
473
  externalId: externalSubjectId,
892
- identityProvider: identityProvider ?? 'external',
893
- isIdentified: true
474
+ identityProvider: identityProvider ?? 'external'
894
475
  });
895
476
  return subject;
896
477
  }
@@ -898,8 +479,7 @@ function subjectRegistry({ db, ctx }) {
898
479
  const subject = await db.create('subject', {
899
480
  id: await generateUniqueId(db, 'subject', ctx),
900
481
  externalId: null,
901
- identityProvider: 'anonymous',
902
- isIdentified: false
482
+ identityProvider: 'anonymous'
903
483
  });
904
484
  logger.debug('Created new anonymous subject', {
905
485
  subject
@@ -925,264 +505,9 @@ const createRegistry = (ctx)=>({
925
505
  ...subjectRegistry(ctx),
926
506
  ...consentPurposeRegistry(ctx),
927
507
  ...policyRegistry(ctx),
928
- ...domainRegistry(ctx)
508
+ ...domainRegistry(ctx),
509
+ ...runtimePolicyDecisionRegistry(ctx)
929
510
  });
930
- const auditLogTable = schema_table('auditLog', {
931
- id: idColumn('id', 'varchar(255)'),
932
- entityType: column('entityType', 'string'),
933
- entityId: column('entityId', 'string'),
934
- actionType: column('actionType', 'string'),
935
- subjectId: column('subjectId', 'string').nullable(),
936
- ipAddress: column('ipAddress', 'string').nullable(),
937
- userAgent: column('userAgent', 'string').nullable(),
938
- changes: column('changes', 'json').nullable(),
939
- metadata: column('metadata', 'json').nullable(),
940
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
941
- eventTimezone: column('eventTimezone', 'string').defaultTo$(()=>'UTC')
942
- });
943
- const consentTable = schema_table('consent', {
944
- id: idColumn('id', 'varchar(255)'),
945
- subjectId: column('subjectId', 'string'),
946
- domainId: column('domainId', 'string'),
947
- policyId: column('policyId', 'string').nullable(),
948
- purposeIds: column('purposeIds', 'json'),
949
- metadata: column('metadata', 'json').nullable(),
950
- ipAddress: column('ipAddress', 'string').nullable(),
951
- userAgent: column('userAgent', 'string').nullable(),
952
- status: column('status', 'string').defaultTo$(()=>'active'),
953
- withdrawalReason: column('withdrawalReason', 'string').nullable(),
954
- givenAt: column('givenAt', 'timestamp').defaultTo$('now'),
955
- validUntil: column('validUntil', 'timestamp').nullable(),
956
- isActive: column('isActive', 'bool').defaultTo$(()=>true)
957
- });
958
- const consentPolicyTable = schema_table('consentPolicy', {
959
- id: idColumn('id', 'varchar(255)'),
960
- version: column('version', 'string'),
961
- type: column('type', 'string'),
962
- name: column('name', 'string'),
963
- effectiveDate: column('effectiveDate', 'timestamp'),
964
- expirationDate: column('expirationDate', 'timestamp').nullable(),
965
- content: column('content', 'string'),
966
- contentHash: column('contentHash', 'string'),
967
- isActive: column('isActive', 'bool').defaultTo$(()=>true),
968
- createdAt: column('createdAt', 'timestamp').defaultTo$('now')
969
- });
970
- const consentPurposeTable = schema_table('consentPurpose', {
971
- id: idColumn('id', 'varchar(255)'),
972
- code: column('code', 'string'),
973
- name: column('name', 'string'),
974
- description: column("description", 'string'),
975
- isEssential: column('isEssential', 'bool'),
976
- dataCategory: column('dataCategory', 'string').nullable(),
977
- legalBasis: column('legalBasis', 'string').nullable(),
978
- isActive: column('isActive', 'bool').defaultTo$(()=>true),
979
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
980
- updatedAt: column('updatedAt', 'timestamp').defaultTo$('now')
981
- });
982
- const consentRecordTable = schema_table('consentRecord', {
983
- id: idColumn('id', 'varchar(255)'),
984
- subjectId: column('subjectId', 'string'),
985
- consentId: column('consentId', 'string').nullable(),
986
- actionType: column('actionType', 'string'),
987
- details: column('details', 'json').nullable(),
988
- createdAt: column('createdAt', 'timestamp').defaultTo$('now')
989
- });
990
- const domainTable = schema_table('domain', {
991
- id: idColumn('id', 'varchar(255)'),
992
- name: column('name', 'string').unique(),
993
- description: column("description", 'string').nullable(),
994
- allowedOrigins: column('allowedOrigins', 'json').nullable(),
995
- isVerified: column('isVerified', 'bool').defaultTo$(()=>true),
996
- isActive: column('isActive', 'bool').defaultTo$(()=>true),
997
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
998
- updatedAt: column('updatedAt', 'timestamp').defaultTo$('now')
999
- });
1000
- const subjectTable = schema_table('subject', {
1001
- id: idColumn('id', 'varchar(255)'),
1002
- isIdentified: column('isIdentified', 'bool').defaultTo$(()=>false),
1003
- externalId: column('externalId', 'string').nullable(),
1004
- identityProvider: column('identityProvider', 'string').nullable(),
1005
- lastIpAddress: column('lastIpAddress', 'string').nullable(),
1006
- subjectTimezone: column('subjectTimezone', 'string').nullable(),
1007
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
1008
- updatedAt: column('updatedAt', 'timestamp').defaultTo$('now')
1009
- });
1010
- const v1 = schema({
1011
- version: '1.0.0',
1012
- tables: {
1013
- subject: subjectTable,
1014
- domain: domainTable,
1015
- consentPolicy: consentPolicyTable,
1016
- consentPurpose: consentPurposeTable,
1017
- consent: consentTable,
1018
- auditLog: auditLogTable,
1019
- consentRecord: consentRecordTable
1020
- },
1021
- relations: {
1022
- subject: ({ many })=>({
1023
- consents: many('consent'),
1024
- consentRecords: many('consentRecord'),
1025
- auditLogs: many('auditLog')
1026
- }),
1027
- domain: ({ many })=>({
1028
- consents: many('consent')
1029
- }),
1030
- consentPolicy: ({ many })=>({
1031
- consents: many('consent')
1032
- }),
1033
- consentPurpose: ()=>({}),
1034
- consent: ({ one, many })=>({
1035
- subject: one('subject', [
1036
- 'subjectId',
1037
- 'id'
1038
- ]).foreignKey(),
1039
- domain: one('domain', [
1040
- 'domainId',
1041
- 'id'
1042
- ]).foreignKey(),
1043
- policy: one('consentPolicy', [
1044
- 'policyId',
1045
- 'id'
1046
- ]).foreignKey(),
1047
- consentRecords: many('consentRecord')
1048
- }),
1049
- consentRecord: ({ one })=>({
1050
- subject: one('subject', [
1051
- 'subjectId',
1052
- 'id'
1053
- ]).foreignKey(),
1054
- consent: one('consent', [
1055
- 'consentId',
1056
- 'id'
1057
- ]).foreignKey()
1058
- }),
1059
- auditLog: ({ one })=>({
1060
- subject: one('subject', [
1061
- 'subjectId',
1062
- 'id'
1063
- ]).foreignKey()
1064
- })
1065
- }
1066
- });
1067
- const audit_log_auditLogTable = schema_table('auditLog', {
1068
- id: idColumn('id', 'varchar(255)'),
1069
- entityType: column('entityType', 'string'),
1070
- entityId: column('entityId', 'string'),
1071
- actionType: column('actionType', 'string'),
1072
- subjectId: column('subjectId', 'string').nullable(),
1073
- ipAddress: column('ipAddress', 'string').nullable(),
1074
- userAgent: column('userAgent', 'string').nullable(),
1075
- changes: column('changes', 'json').nullable(),
1076
- metadata: column('metadata', 'json').nullable(),
1077
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
1078
- tenantId: column('tenantId', 'string').nullable()
1079
- });
1080
- const consent_consentTable = schema_table('consent', {
1081
- id: idColumn('id', 'varchar(255)'),
1082
- subjectId: column('subjectId', 'string'),
1083
- domainId: column('domainId', 'string'),
1084
- policyId: column('policyId', 'string').nullable(),
1085
- purposeIds: column('purposeIds', 'json'),
1086
- metadata: column('metadata', 'json').nullable(),
1087
- ipAddress: column('ipAddress', 'string').nullable(),
1088
- userAgent: column('userAgent', 'string').nullable(),
1089
- givenAt: column('givenAt', 'timestamp').defaultTo$('now'),
1090
- validUntil: column('validUntil', 'timestamp').nullable(),
1091
- jurisdiction: column('jurisdiction', 'string').nullable(),
1092
- jurisdictionModel: column('jurisdictionModel', 'string').nullable(),
1093
- tcString: column('tcString', 'string').nullable(),
1094
- uiSource: column('uiSource', 'string').nullable(),
1095
- tenantId: column('tenantId', 'string').nullable()
1096
- });
1097
- const consent_policy_consentPolicyTable = schema_table('consentPolicy', {
1098
- id: idColumn('id', 'varchar(255)'),
1099
- version: column('version', 'string'),
1100
- type: column('type', 'string'),
1101
- effectiveDate: column('effectiveDate', 'timestamp'),
1102
- isActive: column('isActive', 'bool').defaultTo$(()=>true),
1103
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
1104
- tenantId: column('tenantId', 'string').nullable()
1105
- });
1106
- const consent_purpose_consentPurposeTable = schema_table('consentPurpose', {
1107
- id: idColumn('id', 'varchar(255)'),
1108
- code: column('code', 'string'),
1109
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
1110
- updatedAt: column('updatedAt', 'timestamp').defaultTo$('now'),
1111
- tenantId: column('tenantId', 'string').nullable()
1112
- });
1113
- const domain_domainTable = schema_table('domain', {
1114
- id: idColumn('id', 'varchar(255)'),
1115
- name: column('name', 'string'),
1116
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
1117
- updatedAt: column('updatedAt', 'timestamp').defaultTo$('now'),
1118
- tenantId: column('tenantId', 'string').nullable()
1119
- });
1120
- const subject_subjectTable = schema_table('subject', {
1121
- id: idColumn('id', 'varchar(255)'),
1122
- isIdentified: column('isIdentified', 'bool').defaultTo$(()=>false),
1123
- externalId: column('externalId', 'string').nullable(),
1124
- identityProvider: column('identityProvider', 'string').nullable(),
1125
- createdAt: column('createdAt', 'timestamp').defaultTo$('now'),
1126
- updatedAt: column('updatedAt', 'timestamp').defaultTo$('now'),
1127
- tenantId: column('tenantId', 'string').nullable()
1128
- });
1129
- const v2 = schema({
1130
- version: '2.0.0',
1131
- tables: {
1132
- subject: subject_subjectTable,
1133
- domain: domain_domainTable,
1134
- consentPolicy: consent_policy_consentPolicyTable,
1135
- consentPurpose: consent_purpose_consentPurposeTable,
1136
- consent: consent_consentTable,
1137
- auditLog: audit_log_auditLogTable
1138
- },
1139
- relations: {
1140
- subject: ({ many })=>({
1141
- consents: many('consent'),
1142
- auditLogs: many('auditLog')
1143
- }),
1144
- domain: ({ many })=>({
1145
- consents: many('consent')
1146
- }),
1147
- consentPolicy: ({ many })=>({
1148
- consents: many('consent')
1149
- }),
1150
- consentPurpose: ()=>({}),
1151
- consent: ({ one })=>({
1152
- subject: one('subject', [
1153
- 'subjectId',
1154
- 'id'
1155
- ]).foreignKey(),
1156
- domain: one('domain', [
1157
- 'domainId',
1158
- 'id'
1159
- ]).foreignKey(),
1160
- policy: one('consentPolicy', [
1161
- 'policyId',
1162
- 'id'
1163
- ]).foreignKey()
1164
- }),
1165
- auditLog: ({ one })=>({
1166
- subject: one('subject', [
1167
- 'subjectId',
1168
- 'id'
1169
- ]).foreignKey()
1170
- })
1171
- }
1172
- });
1173
- const DB = fumadb({
1174
- namespace: 'c15t',
1175
- schemas: [
1176
- v1,
1177
- v2
1178
- ]
1179
- });
1180
- fumadb({
1181
- namespace: 'c15t',
1182
- schemas: [
1183
- v2
1184
- ]
1185
- });
1186
511
  const SCOPED_METHODS = new Set([
1187
512
  'create',
1188
513
  'createMany',
@@ -1260,7 +585,7 @@ const init = (options)=>{
1260
585
  ...options.logger,
1261
586
  appName: String(appName)
1262
587
  });
1263
- const telemetryOptions = createTelemetryOptions(String(appName), options.advanced?.telemetry, options.tenantId);
588
+ const telemetryOptions = createTelemetryOptions(String(appName), options.telemetry, options.tenantId);
1264
589
  if (isTelemetryEnabled(options)) logger.debug('Telemetry is enabled', {
1265
590
  hasTracer: !!telemetryOptions?.tracer,
1266
591
  hasMeter: !!telemetryOptions?.meter,
@@ -1271,1265 +596,119 @@ const init = (options)=>{
1271
596
  const client = db.client(options.adapter);
1272
597
  const rawOrm = client.orm('2.0.0');
1273
598
  const orm = options.tenantId ? withTenantScope(rawOrm, options.tenantId) : rawOrm;
599
+ const { ipAddress: _ipAddressConfig, ...baseOptions } = options;
600
+ const i18nValidation = validateMessages({
601
+ i18n: options.i18n,
602
+ customTranslations: options.customTranslations,
603
+ policies: options.policyPacks
604
+ });
605
+ for (const warning of i18nValidation.warnings)logger.warn(`i18n: ${warning}`);
606
+ if (i18nValidation.errors.length > 0) throw new Error(`Invalid i18n configuration:\n${i18nValidation.errors.map((error)=>`- ${error}`).join('\n')}`);
607
+ const policyValidation = policy_inspectPolicies(options.policyPacks ?? [], {
608
+ iabEnabled: options.iab?.enabled === true
609
+ });
610
+ for (const warning of policyValidation.warnings)logger.warn(`policyPacks: ${warning}`);
611
+ if (policyValidation.errors.length > 0) throw new Error(policyValidation.errors[0]);
1274
612
  const context = {
1275
- ...options,
613
+ ...baseOptions,
1276
614
  appName,
1277
615
  logger,
1278
616
  db: orm,
1279
617
  registry: createRegistry({
1280
618
  db: orm,
1281
619
  ctx: {
1282
- logger
620
+ logger,
621
+ tenantId: options.tenantId
1283
622
  }
1284
623
  })
1285
624
  };
1286
625
  return context;
1287
626
  };
1288
- function parsePurposeIds(purposeIds) {
1289
- if (null == purposeIds) return [];
1290
- const ids = 'object' == typeof purposeIds && 'json' in purposeIds ? purposeIds.json : purposeIds;
1291
- return Array.isArray(ids) ? ids : [];
1292
- }
1293
- async function batchLoadPolicies(policyIds, ctx) {
1294
- const { db, registry } = ctx;
1295
- const policyMap = new Map();
1296
- if (policyIds.size > 0) {
1297
- const policies = await db.findMany('consentPolicy', {
1298
- where: (b)=>b('id', 'in', [
1299
- ...policyIds
1300
- ])
1301
- });
1302
- for (const p of policies)policyMap.set(p.id, p);
1303
- }
1304
- const uniqueTypes = new Set();
1305
- for (const p of policyMap.values())uniqueTypes.add(p.type);
1306
- const latestPolicyByType = new Map();
1307
- for (const type of uniqueTypes){
1308
- const latest = await registry.findOrCreatePolicy(type);
1309
- if (latest) latestPolicyByType.set(type, latest.id);
1310
- }
1311
- return {
1312
- policyMap,
1313
- latestPolicyByType
1314
- };
1315
- }
1316
- async function enrichConsents(consents, ctx) {
1317
- if (0 === consents.length) return [];
1318
- const policyIds = new Set();
1319
- for (const c of consents)if (c.policyId) policyIds.add(c.policyId);
1320
- const { policyMap, latestPolicyByType } = await batchLoadPolicies(policyIds, ctx);
1321
- const allPurposeIds = new Set();
1322
- for (const c of consents)for (const id of parsePurposeIds(c.purposeIds))allPurposeIds.add(id);
1323
- const purposeMap = new Map();
1324
- if (allPurposeIds.size > 0) {
1325
- const purposes = await ctx.db.findMany('consentPurpose', {
1326
- where: (b)=>b('id', 'in', [
1327
- ...allPurposeIds
1328
- ])
1329
- });
1330
- for (const p of purposes)purposeMap.set(p.id, p.code);
1331
- }
1332
- return consents.map((consent)=>{
1333
- let policyType = 'unknown';
1334
- let isLatestPolicy = false;
1335
- if (consent.policyId) {
1336
- const policy = policyMap.get(consent.policyId);
1337
- if (policy) {
1338
- policyType = policy.type;
1339
- isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
1340
- }
1341
- }
1342
- let preferences;
1343
- const ids = parsePurposeIds(consent.purposeIds);
1344
- if (ids.length > 0) {
1345
- preferences = {};
1346
- for (const purposeId of ids){
1347
- const code = purposeMap.get(purposeId);
1348
- if (code) preferences[code] = true;
1349
- }
1350
- }
1351
- return {
1352
- id: consent.id,
1353
- type: policyType,
1354
- policyId: consent.policyId ?? void 0,
1355
- isLatestPolicy,
1356
- preferences,
1357
- givenAt: consent.givenAt
1358
- };
1359
- });
1360
- }
1361
- async function resolveConsentPolicies(consents, ctx) {
1362
- if (0 === consents.length) return [];
1363
- const policyIds = new Set();
1364
- for (const c of consents)if (c.policyId) policyIds.add(c.policyId);
1365
- const { policyMap, latestPolicyByType } = await batchLoadPolicies(policyIds, ctx);
1366
- return consents.map((consent)=>{
1367
- let policyType = 'unknown';
1368
- let isLatestPolicy = false;
1369
- if (consent.policyId) {
1370
- const policy = policyMap.get(consent.policyId);
1371
- if (policy) {
1372
- policyType = policy.type;
1373
- isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
1374
- }
1375
- }
1376
- return {
1377
- consentId: consent.id,
1378
- policyType,
1379
- policyId: consent.policyId ?? void 0,
1380
- isLatestPolicy
1381
- };
1382
- });
1383
- }
1384
- const checkConsentHandler = async (c)=>{
1385
- const ctx = c.get('c15tContext');
1386
- const logger = ctx.logger;
1387
- logger.info('Handling GET /consents/check request');
1388
- const { db, registry } = ctx;
1389
- const externalId = c.req.query('externalId');
1390
- const type = c.req.query('type');
1391
- if (!externalId) throw new HTTPException(422, {
1392
- message: 'externalId query parameter is required',
1393
- cause: {
1394
- code: 'EXTERNAL_ID_REQUIRED'
1395
- }
1396
- });
1397
- if (!type) throw new HTTPException(422, {
1398
- message: 'type query parameter is required',
1399
- cause: {
1400
- code: 'TYPE_REQUIRED'
1401
- }
1402
- });
1403
- const types = type.split(',').map((t)=>t.trim());
1404
- logger.debug('Request parameters', {
1405
- externalId,
1406
- types
1407
- });
1408
- try {
1409
- const subjects = await db.findMany('subject', {
1410
- where: (b)=>b('externalId', '=', externalId)
1411
- });
1412
- const subjectIds = subjects.map((s)=>s.id);
1413
- const results = {};
1414
- for (const t of types)results[t] = {
1415
- hasConsent: false,
1416
- isLatestPolicy: false
1417
- };
1418
- if (0 === subjectIds.length) {
1419
- logger.debug('No subjects found for externalId', {
1420
- externalId
1421
- });
1422
- return c.json({
1423
- results
1424
- });
1425
- }
1426
- const allConsents = await Promise.all(subjectIds.map((subjectId)=>db.findMany('consent', {
1427
- where: (b)=>b('subjectId', '=', subjectId)
1428
- })));
1429
- const consents = allConsents.flat();
1430
- const policyInfos = await resolveConsentPolicies(consents, {
1431
- db,
1432
- registry
1433
- });
1434
- for (const info of policyInfos){
1435
- if (!types.includes(info.policyType)) continue;
1436
- const entry = results[info.policyType];
1437
- if (entry) {
1438
- entry.hasConsent = true;
1439
- if (info.isLatestPolicy) entry.isLatestPolicy = true;
1440
- }
1441
- }
1442
- logger.debug('Consent check results', {
1443
- externalId,
1444
- results
1445
- });
1446
- const metrics = getMetrics();
1447
- if (metrics) for (const [type, result] of Object.entries(results))metrics.recordConsentCheck(type, result.hasConsent);
1448
- return c.json({
1449
- results
1450
- });
1451
- } catch (error) {
1452
- logger.error('Error in GET /consents/check handler', {
1453
- error: extractErrorMessage(error),
1454
- errorType: error instanceof Error ? error.constructor.name : typeof error
1455
- });
1456
- if (error instanceof HTTPException) throw error;
1457
- throw new HTTPException(500, {
1458
- message: 'Internal server error',
1459
- cause: {
1460
- code: 'INTERNAL_SERVER_ERROR'
1461
- }
1462
- });
1463
- }
1464
- };
1465
- const createConsentRoutes = ()=>{
1466
- const app = new Hono();
1467
- app.get('/check', describeRoute({
1468
- summary: 'Check consent by external user ID',
1469
- description: `Pre-banner cross-device consent check. Use to avoid showing the banner when the user has already consented on another device.
1470
-
1471
- **Query parameters:**
1472
- - \`externalId\` – External user ID to check
1473
- - \`type\` – Consent type(s) to check (comma-separated)`,
1474
- tags: [
1475
- 'Consent'
1476
- ],
1477
- responses: {
1478
- 200: {
1479
- description: 'Consent check result per requested type(s)',
1480
- content: {
1481
- 'application/json': {
1482
- schema: resolver(checkConsentOutputSchema)
1483
- }
1484
- }
1485
- },
1486
- 422: {
1487
- description: 'Invalid or missing query parameters'
1488
- }
1489
- }
1490
- }), validator('query', checkConsentQuerySchema), checkConsentHandler);
1491
- return app;
627
+ const DEFAULT_FALLBACK_POLICY_INPUT = {
628
+ id: 'world_no_banner',
629
+ isDefault: true,
630
+ model: 'none',
631
+ uiMode: 'none'
1492
632
  };
1493
- const GVL_TTL_MS = 259200000;
1494
- const memory_memoryCache = new Map();
1495
- function createMemoryCacheAdapter() {
1496
- return {
1497
- async get (key) {
1498
- const entry = memory_memoryCache.get(key);
1499
- if (!entry) return null;
1500
- if (Date.now() > entry.expiresAt) {
1501
- memory_memoryCache.delete(key);
1502
- return null;
1503
- }
1504
- return entry.value;
1505
- },
1506
- async set (key, value, ttlMs = 300000) {
1507
- memory_memoryCache.set(key, {
1508
- value,
1509
- expiresAt: Date.now() + ttlMs
1510
- });
1511
- },
1512
- async delete (key) {
1513
- memory_memoryCache.delete(key);
1514
- },
1515
- async has (key) {
1516
- const entry = memory_memoryCache.get(key);
1517
- if (!entry) return false;
1518
- if (Date.now() > entry.expiresAt) {
1519
- memory_memoryCache.delete(key);
1520
- return false;
1521
- }
1522
- return true;
1523
- }
1524
- };
1525
- }
1526
- function createGVLCacheKey(appName, language, vendorIds) {
1527
- const sortedIds = vendorIds ? [
1528
- ...vendorIds
1529
- ].sort((a, b)=>a - b).join(',') : 'all';
1530
- return `${appName}:gvl:${language}:${sortedIds}`;
1531
- }
1532
- const GVL_ENDPOINT = 'https://gvl.consent.io';
1533
- const inflightRequests = new Map();
1534
- async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT) {
1535
- const sortedVendorIds = vendorIds ? [
1536
- ...vendorIds
1537
- ].sort((a, b)=>a - b) : [];
1538
- const dedupeKey = `${endpoint}|${language}|${sortedVendorIds.join(',')}`;
1539
- const existingRequest = inflightRequests.get(dedupeKey);
1540
- if (existingRequest) return existingRequest;
1541
- const url = new URL(endpoint);
1542
- if (sortedVendorIds.length > 0) url.searchParams.set('vendorIds', sortedVendorIds.join(','));
1543
- const promise = (async ()=>{
1544
- const fetchStart = Date.now();
1545
- try {
1546
- const gvl = await withExternalSpan({
1547
- url: url.toString(),
1548
- method: 'GET'
1549
- }, async ()=>{
1550
- const response = await fetch(url.toString(), {
1551
- headers: {
1552
- 'Accept-Language': language
1553
- }
1554
- });
1555
- if (204 === response.status) return null;
1556
- if (!response.ok) throw new Error(`Failed to fetch GVL: ${response.status} ${response.statusText}`);
1557
- const text = await response.text();
1558
- const trimmed = text.trim().replace(/^\uFEFF/, '');
1559
- let parsed;
1560
- try {
1561
- parsed = JSON.parse(trimmed);
1562
- } catch {
1563
- let depth = 0;
1564
- let end = -1;
1565
- const start = trimmed.indexOf('{');
1566
- if (start >= 0) for(let i = start; i < trimmed.length; i++){
1567
- const c = trimmed[i];
1568
- if ('{' === c) depth++;
1569
- else if ('}' === c) {
1570
- depth--;
1571
- if (0 === depth) {
1572
- end = i + 1;
1573
- break;
1574
- }
1575
- }
1576
- }
1577
- if (end > 0) parsed = JSON.parse(trimmed.slice(0, end));
1578
- else throw new SyntaxError('Invalid GVL response: not valid JSON');
1579
- }
1580
- if (!parsed.vendorListVersion || !parsed.purposes || !parsed.vendors) throw new Error('Invalid GVL response: missing required fields');
1581
- return parsed;
1582
- });
1583
- getMetrics()?.recordGvlFetch({
1584
- language,
1585
- source: 'fetch',
1586
- status: 200
1587
- }, Date.now() - fetchStart);
1588
- return gvl;
1589
- } catch (error) {
1590
- getMetrics()?.recordGvlError({
1591
- language,
1592
- errorType: error instanceof Error ? error.name : 'UnknownError'
1593
- });
1594
- throw error;
1595
- } finally{
1596
- inflightRequests.delete(dedupeKey);
1597
- }
1598
- })();
1599
- inflightRequests.set(dedupeKey, promise);
1600
- return promise;
1601
- }
1602
- function createGVLResolver(options) {
1603
- const { appName, bundled, cacheAdapter, vendorIds, endpoint } = options;
1604
- const memoryCache = createMemoryCacheAdapter();
1605
- return {
1606
- async get (language) {
1607
- const cacheKey = createGVLCacheKey(appName, language, vendorIds);
1608
- if (bundled?.[language]) return bundled[language];
1609
- const memoryHit = await withCacheSpan('get', 'memory', ()=>memoryCache.get(cacheKey));
1610
- if (memoryHit) {
1611
- getMetrics()?.recordCacheHit('memory');
1612
- return memoryHit;
1613
- }
1614
- getMetrics()?.recordCacheMiss('memory');
1615
- if (cacheAdapter) {
1616
- const externalHit = await withCacheSpan('get', 'external', ()=>cacheAdapter.get(cacheKey));
1617
- if (externalHit) {
1618
- getMetrics()?.recordCacheHit('external');
1619
- await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, externalHit, 300000));
1620
- return externalHit;
1621
- }
1622
- getMetrics()?.recordCacheMiss('external');
1623
- }
1624
- const gvl = await fetchGVLWithLanguage(language, vendorIds, endpoint);
1625
- if (gvl) {
1626
- await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, gvl, 300000));
1627
- if (cacheAdapter) await withCacheSpan('set', 'external', ()=>cacheAdapter.set(cacheKey, gvl, GVL_TTL_MS));
1628
- }
1629
- return gvl;
1630
- }
1631
- };
1632
- }
1633
- function geo_normalizeHeader(value) {
1634
- if (!value) return null;
1635
- return Array.isArray(value) ? value[0] ?? null : value;
1636
- }
1637
- function getGeoHeaders(headers) {
1638
- const countryCode = geo_normalizeHeader(headers.get('x-c15t-country')) ?? geo_normalizeHeader(headers.get('cf-ipcountry')) ?? geo_normalizeHeader(headers.get('x-vercel-ip-country')) ?? geo_normalizeHeader(headers.get('x-amz-cf-ipcountry')) ?? geo_normalizeHeader(headers.get('x-country-code'));
1639
- const regionCode = geo_normalizeHeader(headers.get('x-c15t-region')) ?? geo_normalizeHeader(headers.get('x-vercel-ip-country-region')) ?? geo_normalizeHeader(headers.get('x-region-code'));
633
+ function mergeMatch(input) {
634
+ return policyMatchers.merge(input.countries?.length ? policyMatchers.countries(input.countries) : {}, input.regions?.length ? policyMatchers.regions(input.regions) : {}, input.isDefault ? policyMatchers["default"]() : {});
635
+ }
636
+ function compactUiSurface(value) {
637
+ if (!value) return;
638
+ return compactDefined({
639
+ allowedActions: dedupeTrimmedStrings(value.allowedActions),
640
+ primaryActions: value.primaryActions,
641
+ layout: value.layout,
642
+ direction: value.direction,
643
+ uiProfile: value.uiProfile,
644
+ scrollLock: value.scrollLock
645
+ });
646
+ }
647
+ function buildPolicyConfig(input) {
648
+ const categories = dedupeTrimmedStrings(input.categories);
649
+ const preselectedCategories = dedupeTrimmedStrings(input.preselectedCategories);
1640
650
  return {
1641
- countryCode,
1642
- regionCode
1643
- };
1644
- }
1645
- function checkJurisdiction(countryCode, regionCode) {
1646
- const jurisdictions = {
1647
- EU: new Set([
1648
- 'AT',
1649
- 'BE',
1650
- 'BG',
1651
- 'HR',
1652
- 'CY',
1653
- 'CZ',
1654
- 'DK',
1655
- 'EE',
1656
- 'FI',
1657
- 'FR',
1658
- 'DE',
1659
- 'GR',
1660
- 'HU',
1661
- 'IE',
1662
- 'IT',
1663
- 'LV',
1664
- 'LT',
1665
- 'LU',
1666
- 'MT',
1667
- 'NL',
1668
- 'PL',
1669
- 'PT',
1670
- 'RO',
1671
- 'SK',
1672
- 'SI',
1673
- 'ES',
1674
- 'SE'
1675
- ]),
1676
- EEA: new Set([
1677
- 'IS',
1678
- 'NO',
1679
- 'LI'
1680
- ]),
1681
- UK: new Set([
1682
- 'GB'
1683
- ]),
1684
- CH: new Set([
1685
- 'CH'
1686
- ]),
1687
- BR: new Set([
1688
- 'BR'
1689
- ]),
1690
- CA: new Set([
1691
- 'CA'
1692
- ]),
1693
- AU: new Set([
1694
- 'AU'
1695
- ]),
1696
- JP: new Set([
1697
- 'JP'
1698
- ]),
1699
- KR: new Set([
1700
- 'KR'
1701
- ]),
1702
- US_CCPA_REGIONS: new Set([
1703
- 'CA'
1704
- ]),
1705
- CA_QC_REGIONS: new Set([
1706
- 'QC'
1707
- ])
1708
- };
1709
- let jurisdiction = 'NONE';
1710
- if (countryCode) {
1711
- const normalizedCountryCode = countryCode.toUpperCase();
1712
- const normalizedRegionCode = regionCode && 'string' == typeof regionCode ? (regionCode.includes('-') ? regionCode.split('-').pop() : regionCode).toUpperCase() : null;
1713
- if ('US' === normalizedCountryCode && normalizedRegionCode && jurisdictions.US_CCPA_REGIONS.has(normalizedRegionCode)) return 'CCPA';
1714
- if ('CA' === normalizedCountryCode && normalizedRegionCode && jurisdictions.CA_QC_REGIONS.has(normalizedRegionCode)) return 'QC_LAW25';
1715
- const jurisdictionMap = [
1716
- {
1717
- sets: [
1718
- jurisdictions.UK
1719
- ],
1720
- code: 'UK_GDPR'
1721
- },
1722
- {
1723
- sets: [
1724
- jurisdictions.EU,
1725
- jurisdictions.EEA
1726
- ],
1727
- code: 'GDPR'
1728
- },
1729
- {
1730
- sets: [
1731
- jurisdictions.CH
1732
- ],
1733
- code: 'CH'
1734
- },
1735
- {
1736
- sets: [
1737
- jurisdictions.BR
1738
- ],
1739
- code: 'BR'
1740
- },
1741
- {
1742
- sets: [
1743
- jurisdictions.CA
1744
- ],
1745
- code: 'PIPEDA'
1746
- },
1747
- {
1748
- sets: [
1749
- jurisdictions.AU
1750
- ],
1751
- code: 'AU'
1752
- },
1753
- {
1754
- sets: [
1755
- jurisdictions.JP
1756
- ],
1757
- code: 'APPI'
1758
- },
1759
- {
1760
- sets: [
1761
- jurisdictions.KR
1762
- ],
1763
- code: 'PIPA'
1764
- }
1765
- ];
1766
- for (const { sets, code } of jurisdictionMap)if (sets.some((set)=>set.has(normalizedCountryCode))) {
1767
- jurisdiction = code;
1768
- break;
1769
- }
1770
- }
1771
- return jurisdiction;
1772
- }
1773
- async function getLocation(request, options) {
1774
- if (options.advanced?.disableGeoLocation) return {
1775
- countryCode: null,
1776
- regionCode: null
1777
- };
1778
- const { countryCode, regionCode } = getGeoHeaders(request.headers);
1779
- return {
1780
- countryCode,
1781
- regionCode
651
+ id: input.id,
652
+ match: mergeMatch(input),
653
+ i18n: input.i18n,
654
+ consent: compactDefined({
655
+ model: input.model,
656
+ expiryDays: input.expiryDays,
657
+ scopeMode: input.scopeMode,
658
+ categories,
659
+ preselectedCategories,
660
+ gpc: input.gpc
661
+ }),
662
+ ui: compactDefined({
663
+ mode: input.uiMode,
664
+ banner: compactUiSurface(input.banner),
665
+ dialog: compactUiSurface(input.dialog)
666
+ }),
667
+ proof: compactDefined({
668
+ storeIp: input.proof?.storeIp,
669
+ storeUserAgent: input.proof?.storeUserAgent,
670
+ storeLanguage: input.proof?.storeLanguage
671
+ })
1782
672
  };
1783
673
  }
1784
- function getJurisdiction(location, options) {
1785
- if (options.advanced?.disableGeoLocation) return 'GDPR';
1786
- return checkJurisdiction(location.countryCode, location.regionCode);
1787
- }
1788
- function isSupportedBaseLanguage(lang) {
1789
- return lang in baseTranslations;
1790
- }
1791
- function translations_getTranslationsData(acceptLanguage, customTranslations) {
1792
- const supportedDefaultLanguages = Object.keys(baseTranslations);
1793
- const supportedCustomLanguages = Object.keys(customTranslations || {});
1794
- const supportedLanguages = [
1795
- ...supportedDefaultLanguages,
1796
- ...supportedCustomLanguages
674
+ function buildPolicyPack(inputs) {
675
+ return inputs.map((input)=>buildPolicyConfig(input));
676
+ }
677
+ function buildPolicyPackWithDefault(inputs, defaultPolicy) {
678
+ const pack = buildPolicyPack(inputs);
679
+ const hasDefault = pack.some((policy)=>policy.match.isDefault);
680
+ if (hasDefault) return pack;
681
+ const fallbackInput = defaultPolicy ? {
682
+ ...defaultPolicy,
683
+ isDefault: true,
684
+ countries: void 0,
685
+ regions: void 0
686
+ } : DEFAULT_FALLBACK_POLICY_INPUT;
687
+ return [
688
+ ...pack,
689
+ buildPolicyConfig(fallbackInput)
1797
690
  ];
1798
- const preferredLanguage = selectLanguage(supportedLanguages, {
1799
- header: acceptLanguage,
1800
- fallback: 'en'
1801
- });
1802
- const base = isSupportedBaseLanguage(preferredLanguage) ? baseTranslations[preferredLanguage] : baseTranslations.en;
1803
- const custom = supportedCustomLanguages.includes(preferredLanguage) ? customTranslations?.[preferredLanguage] : {};
1804
- const translations = custom ? deepMergeTranslations(base, custom) : base;
1805
- return {
1806
- translations: translations,
1807
- language: preferredLanguage
1808
- };
1809
691
  }
1810
- const createInitRoute = (options)=>{
1811
- const app = new Hono();
1812
- app.get('/', describeRoute({
1813
- summary: 'Get initial consent manager state',
1814
- description: `Returns the initial state required to render the consent manager.
1815
-
1816
- - **Jurisdiction** – User's jurisdiction (defaults to GDPR if geo-location is disabled)
1817
- - **Location** – User's location (null if geo-location is disabled)
1818
- - **Translations** – Consent manager copy (from \`Accept-Language\` header)
1819
- - **Branding** – Configured branding key
1820
- - **GVL** – Global Vendor List when enabled
1821
-
1822
- Use for geo-targeted consent banners and regional compliance.`,
1823
- tags: [
1824
- 'Init'
1825
- ],
1826
- responses: {
1827
- 200: {
1828
- description: 'Initialization payload (jurisdiction, location, translations, branding, GVL)',
1829
- content: {
1830
- 'application/json': {
1831
- schema: resolver(initOutputSchema)
1832
- }
1833
- }
1834
- }
1835
- }
1836
- }), async (c)=>{
1837
- const request = c.req.raw;
1838
- const acceptLanguage = request.headers.get('accept-language') || 'en';
1839
- const location = await getLocation(request, options);
1840
- const jurisdiction = getJurisdiction(location, options);
1841
- const translationsResult = translations_getTranslationsData(acceptLanguage, options.advanced?.customTranslations);
1842
- let gvl = null;
1843
- if (options.advanced?.gvl?.enabled) {
1844
- const language = translationsResult.language.split('-')[0] || 'en';
1845
- const gvlResolver = createGVLResolver({
1846
- appName: options.appName || 'c15t',
1847
- bundled: options.advanced.gvl.bundled,
1848
- cacheAdapter: options.advanced.cache?.adapter,
1849
- vendorIds: options.advanced.gvl.vendorIds,
1850
- endpoint: options.advanced.gvl.endpoint
1851
- });
1852
- gvl = await gvlResolver.get(language);
1853
- }
1854
- const customVendors = options.advanced?.gvl?.customVendors;
1855
- const gpc = '1' === request.headers.get('sec-gpc');
1856
- getMetrics()?.recordInit({
1857
- jurisdiction,
1858
- country: location?.countryCode ?? void 0,
1859
- region: location?.regionCode ?? void 0,
1860
- gpc
1861
- });
1862
- return c.json({
1863
- jurisdiction,
1864
- location,
1865
- translations: translationsResult,
1866
- branding: options.advanced?.branding || 'c15t',
1867
- gvl,
1868
- customVendors
1869
- });
1870
- });
1871
- return app;
1872
- };
1873
- function getHeaders(headers) {
1874
- if (!headers) return {
1875
- countryCode: null,
1876
- regionCode: null,
1877
- acceptLanguage: null
1878
- };
1879
- const normalizeHeader = (value)=>{
1880
- if (!value) return null;
1881
- return Array.isArray(value) ? value[0] ?? null : value;
1882
- };
1883
- const countryCode = normalizeHeader(headers.get('x-c15t-country')) ?? normalizeHeader(headers.get('cf-ipcountry')) ?? normalizeHeader(headers.get('x-vercel-ip-country')) ?? normalizeHeader(headers.get('x-amz-cf-ipcountry')) ?? normalizeHeader(headers.get('x-country-code'));
1884
- const regionCode = normalizeHeader(headers.get('x-c15t-region')) ?? normalizeHeader(headers.get('x-vercel-ip-country-region')) ?? normalizeHeader(headers.get('x-region-code'));
1885
- const acceptLanguage = normalizeHeader(headers.get('accept-language'));
1886
- return {
1887
- countryCode,
1888
- regionCode,
1889
- acceptLanguage
1890
- };
1891
- }
1892
- const statusHandler = async (c)=>{
1893
- const ctx = c.get('c15tContext');
1894
- const { countryCode, regionCode, acceptLanguage } = getHeaders(ctx.headers);
1895
- const clientInfo = {
1896
- ip: ctx.ipAddress ?? null,
1897
- acceptLanguage,
1898
- userAgent: ctx.userAgent ?? null,
1899
- region: {
1900
- countryCode,
1901
- regionCode
1902
- }
1903
- };
1904
- try {
1905
- await ctx.db.findFirst('subject', {});
1906
- return c.json({
1907
- version: version_version,
1908
- timestamp: new Date(),
1909
- client: clientInfo
1910
- });
1911
- } catch (error) {
1912
- ctx.logger.error('Database health check failed', {
1913
- error
1914
- });
1915
- throw new HTTPException(503, {
1916
- message: 'Database health check failed',
1917
- cause: {
1918
- code: 'SERVICE_UNAVAILABLE',
1919
- error
1920
- }
1921
- });
1922
- }
1923
- };
1924
- const createStatusRoute = ()=>{
1925
- const app = new Hono();
1926
- app.get('/', describeRoute({
1927
- summary: 'Health check and API status',
1928
- description: `Returns API version, timestamp, and client info (IP, region, user agent).
1929
-
1930
- Use for health checks, load balancer probes, and debugging. Performs a lightweight DB check; returns 503 if the database is unreachable.`,
1931
- tags: [
1932
- 'Status'
1933
- ],
1934
- responses: {
1935
- 200: {
1936
- description: 'API is healthy (version, timestamp, client info)',
1937
- content: {
1938
- 'application/json': {
1939
- schema: resolver(statusOutputSchema)
1940
- }
1941
- }
1942
- },
1943
- 503: {
1944
- description: 'Service unavailable (e.g. database unreachable)'
1945
- }
1946
- }
1947
- }), statusHandler);
1948
- return app;
1949
- };
1950
- const getSubjectHandler = async (c)=>{
1951
- const ctx = c.get('c15tContext');
1952
- const logger = ctx.logger;
1953
- logger.info('Handling GET /subjects/:id request');
1954
- const { db, registry } = ctx;
1955
- const subjectId = c.req.param('id');
1956
- const type = c.req.query('type');
1957
- const typeFilter = type?.split(',').map((t)=>t.trim()) || [];
1958
- logger.debug('Request parameters', {
1959
- subjectId,
1960
- typeFilter
1961
- });
1962
- try {
1963
- const subject = await db.findFirst('subject', {
1964
- where: (b)=>b('id', '=', subjectId)
1965
- });
1966
- if (!subject) throw new HTTPException(404, {
1967
- message: 'Subject not found',
1968
- cause: {
1969
- code: 'SUBJECT_NOT_FOUND',
1970
- subjectId
1971
- }
1972
- });
1973
- const consents = await db.findMany('consent', {
1974
- where: (b)=>b('subjectId', '=', subjectId)
1975
- });
1976
- const consentItems = await enrichConsents(consents, {
1977
- db,
1978
- registry
1979
- });
1980
- const filteredConsents = typeFilter.length > 0 ? consentItems.filter((consent)=>typeFilter.includes(consent.type)) : consentItems;
1981
- const isValid = 0 === typeFilter.length || typeFilter.every((t)=>filteredConsents.some((consent)=>consent.type === t && consent.isLatestPolicy));
1982
- return c.json({
1983
- subject: {
1984
- id: subject.id,
1985
- externalId: subject.externalId ?? void 0,
1986
- isIdentified: subject.isIdentified,
1987
- createdAt: subject.createdAt
1988
- },
1989
- consents: filteredConsents,
1990
- isValid
1991
- });
1992
- } catch (error) {
1993
- logger.error('Error in GET /subjects/:id handler', {
1994
- error: extractErrorMessage(error),
1995
- errorType: error instanceof Error ? error.constructor.name : typeof error
1996
- });
1997
- if (error instanceof HTTPException) throw error;
1998
- throw new HTTPException(500, {
1999
- message: 'Internal server error',
2000
- cause: {
2001
- code: 'INTERNAL_SERVER_ERROR'
2002
- }
2003
- });
692
+ function composePacks(...packs) {
693
+ const seen = new Set();
694
+ const result = [];
695
+ for (const pack of packs)for (const policy of pack)if (!seen.has(policy.id)) {
696
+ seen.add(policy.id);
697
+ result.push(policy);
2004
698
  }
2005
- };
2006
- const listSubjectsHandler = async (c)=>{
2007
- const ctx = c.get('c15tContext');
2008
- const logger = ctx.logger;
2009
- logger.info('Handling GET /subjects request');
2010
- const { db, registry } = ctx;
2011
- if (!ctx.apiKeyAuthenticated) throw new HTTPException(401, {
2012
- message: 'API key required. Use Authorization: Bearer <api_key>',
2013
- cause: {
2014
- code: 'UNAUTHORIZED'
2015
- }
2016
- });
2017
- const externalId = c.req.query('externalId');
2018
- if (!externalId) throw new HTTPException(422, {
2019
- message: 'externalId query parameter is required',
2020
- cause: {
2021
- code: 'EXTERNAL_ID_REQUIRED'
2022
- }
2023
- });
2024
- logger.debug('Request parameters', {
2025
- externalId
2026
- });
2027
- try {
2028
- const subjects = await db.findMany('subject', {
2029
- where: (b)=>b('externalId', '=', externalId)
2030
- });
2031
- const subjectItems = await Promise.all(subjects.map(async (subject)=>{
2032
- const consents = await db.findMany('consent', {
2033
- where: (b)=>b('subjectId', '=', subject.id)
2034
- });
2035
- const consentItems = await enrichConsents(consents, {
2036
- db,
2037
- registry
2038
- });
2039
- return {
2040
- id: subject.id,
2041
- externalId: subject.externalId ?? externalId,
2042
- isIdentified: subject.isIdentified,
2043
- createdAt: subject.createdAt,
2044
- consents: consentItems
2045
- };
2046
- }));
2047
- logger.info('Found subjects for externalId', {
2048
- externalId,
2049
- count: subjectItems.length
2050
- });
2051
- return c.json({
2052
- subjects: subjectItems
2053
- });
2054
- } catch (error) {
2055
- logger.error('Error in GET /subjects handler', {
2056
- error: extractErrorMessage(error),
2057
- errorType: error instanceof Error ? error.constructor.name : typeof error
2058
- });
2059
- if (error instanceof HTTPException) throw error;
2060
- throw new HTTPException(500, {
2061
- message: 'Internal server error',
2062
- cause: {
2063
- code: 'INTERNAL_SERVER_ERROR'
2064
- }
2065
- });
2066
- }
2067
- };
2068
- const utils_prefixes = {
2069
- auditLog: 'log',
2070
- consent: 'cns',
2071
- consentPolicy: 'pol',
2072
- consentPurpose: 'pur',
2073
- domain: 'dom',
2074
- subject: 'sub'
2075
- };
2076
- const utils_b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
2077
- function utils_generateId(model) {
2078
- const buf = crypto.getRandomValues(new Uint8Array(20));
2079
- const prefix = utils_prefixes[model];
2080
- const EPOCH_TIMESTAMP = 1700000000000;
2081
- const t = Date.now() - EPOCH_TIMESTAMP;
2082
- const high = Math.floor(t / 0x100000000);
2083
- const low = t >>> 0;
2084
- buf[0] = high >>> 24 & 255;
2085
- buf[1] = high >>> 16 & 255;
2086
- buf[2] = high >>> 8 & 255;
2087
- buf[3] = 255 & high;
2088
- buf[4] = low >>> 24 & 255;
2089
- buf[5] = low >>> 16 & 255;
2090
- buf[6] = low >>> 8 & 255;
2091
- buf[7] = 255 & low;
2092
- return `${prefix}_${utils_b58.encode(buf)}`;
699
+ return result;
2093
700
  }
2094
- async function utils_generateUniqueId(db, model, ctx, options = {}) {
2095
- const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
2096
- if (attempt >= maxRetries) {
2097
- const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
2098
- ctx?.logger?.error?.('ID generation failed', {
2099
- model,
2100
- maxRetries
2101
- });
2102
- throw error;
2103
- }
2104
- const id = utils_generateId(model);
2105
- try {
2106
- const existing = await db.findFirst(model, {
2107
- where: (b)=>b('id', '=', id)
2108
- });
2109
- if (existing) {
2110
- ctx?.logger?.debug?.('ID conflict detected', {
2111
- id,
2112
- model,
2113
- attempt: attempt + 1,
2114
- maxRetries
2115
- });
2116
- const delay = Math.min(baseDelay * 2 ** attempt, 1000);
2117
- await new Promise((resolve)=>setTimeout(resolve, delay));
2118
- return utils_generateUniqueId(db, model, ctx, {
2119
- maxRetries,
2120
- attempt: attempt + 1,
2121
- baseDelay
2122
- });
2123
- }
2124
- return id;
2125
- } catch (error) {
2126
- ctx?.logger?.error?.('Error checking ID uniqueness', {
2127
- error: error.message,
2128
- model,
2129
- attempt
2130
- });
2131
- if (attempt < maxRetries - 1) {
2132
- const delay = Math.min(baseDelay * 2 ** attempt, 2000);
2133
- await new Promise((resolve)=>setTimeout(resolve, delay));
2134
- return utils_generateUniqueId(db, model, ctx, {
2135
- maxRetries,
2136
- attempt: attempt + 1,
2137
- baseDelay
2138
- });
2139
- }
2140
- throw error;
2141
- }
2142
- }
2143
- const patchSubjectHandler = async (c)=>{
2144
- const ctx = c.get('c15tContext');
2145
- const logger = ctx.logger;
2146
- logger.info('Handling PATCH /subjects/:id request');
2147
- const { db } = ctx;
2148
- const subjectId = c.req.param('id');
2149
- const body = await c.req.json();
2150
- const { externalId, identityProvider = 'external' } = body;
2151
- logger.debug('Request parameters', {
2152
- subjectId,
2153
- externalId,
2154
- identityProvider
2155
- });
2156
- try {
2157
- const subject = await db.findFirst('subject', {
2158
- where: (b)=>b('id', '=', subjectId)
2159
- });
2160
- if (!subject) throw new HTTPException(404, {
2161
- message: 'Subject not found',
2162
- cause: {
2163
- code: 'SUBJECT_NOT_FOUND',
2164
- subjectId
2165
- }
2166
- });
2167
- await db.transaction(async (tx)=>{
2168
- await tx.updateMany('subject', {
2169
- where: (b)=>b('id', '=', subjectId),
2170
- set: {
2171
- externalId,
2172
- identityProvider,
2173
- isIdentified: true,
2174
- updatedAt: new Date()
2175
- }
2176
- });
2177
- await tx.create('auditLog', {
2178
- id: await utils_generateUniqueId(tx, 'auditLog', ctx),
2179
- subjectId,
2180
- entityType: 'subject',
2181
- entityId: subjectId,
2182
- actionType: 'identify_user',
2183
- ipAddress: ctx.ipAddress || null,
2184
- userAgent: ctx.userAgent || null,
2185
- changes: {
2186
- externalId: {
2187
- from: subject.externalId,
2188
- to: externalId
2189
- },
2190
- identityProvider: {
2191
- from: subject.identityProvider,
2192
- to: identityProvider
2193
- },
2194
- isIdentified: {
2195
- from: subject.isIdentified,
2196
- to: true
2197
- }
2198
- },
2199
- metadata: {
2200
- externalId,
2201
- identityProvider
2202
- }
2203
- });
2204
- });
2205
- logger.info('Subject linked to external ID', {
2206
- subjectId,
2207
- externalId,
2208
- identityProvider
2209
- });
2210
- getMetrics()?.recordSubjectLinked(identityProvider);
2211
- return c.json({
2212
- success: true,
2213
- subject: {
2214
- id: subjectId,
2215
- externalId,
2216
- isIdentified: true
2217
- }
2218
- });
2219
- } catch (error) {
2220
- logger.error('Error in PATCH /subjects/:id handler', {
2221
- error: extractErrorMessage(error),
2222
- errorType: error instanceof Error ? error.constructor.name : typeof error
2223
- });
2224
- if (error instanceof HTTPException) throw error;
2225
- throw new HTTPException(500, {
2226
- message: 'Internal server error',
2227
- cause: {
2228
- code: 'INTERNAL_SERVER_ERROR'
2229
- }
2230
- });
2231
- }
2232
- };
2233
- const postSubjectHandler = async (c)=>{
2234
- const ctx = c.get('c15tContext');
2235
- const logger = ctx.logger;
2236
- logger.info('Handling POST /subjects request');
2237
- const { db, registry } = ctx;
2238
- const input = await c.req.json();
2239
- const { type, subjectId, identityProvider, externalSubjectId, domain, metadata, givenAt: givenAtEpoch } = input;
2240
- const preferences = 'preferences' in input ? input.preferences : void 0;
2241
- const givenAt = new Date(givenAtEpoch);
2242
- logger.debug('Request parameters', {
2243
- type,
2244
- subjectId,
2245
- identityProvider,
2246
- externalSubjectId,
2247
- domain
2248
- });
2249
- try {
2250
- const subject = await registry.findOrCreateSubject({
2251
- subjectId,
2252
- externalSubjectId,
2253
- identityProvider,
2254
- ipAddress: ctx.ipAddress
2255
- });
2256
- if (!subject) throw new HTTPException(500, {
2257
- message: 'Failed to create subject',
2258
- cause: {
2259
- code: 'SUBJECT_CREATION_FAILED',
2260
- subjectId
2261
- }
2262
- });
2263
- logger.debug('Subject found/created', {
2264
- subjectId: subject.id
2265
- });
2266
- const domainRecord = await registry.findOrCreateDomain(domain);
2267
- if (!domainRecord) throw new HTTPException(500, {
2268
- message: 'Failed to create domain',
2269
- cause: {
2270
- code: 'DOMAIN_CREATION_FAILED',
2271
- domain
2272
- }
2273
- });
2274
- let policyId;
2275
- let purposeIds = [];
2276
- const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
2277
- if (inputPolicyId) {
2278
- policyId = inputPolicyId;
2279
- const policy = await registry.findConsentPolicyById(inputPolicyId);
2280
- if (!policy) throw new HTTPException(404, {
2281
- message: 'Policy not found',
2282
- cause: {
2283
- code: 'POLICY_NOT_FOUND',
2284
- policyId,
2285
- type
2286
- }
2287
- });
2288
- if (!policy.isActive) throw new HTTPException(400, {
2289
- message: 'Policy is inactive',
2290
- cause: {
2291
- code: 'POLICY_INACTIVE',
2292
- policyId,
2293
- type
2294
- }
2295
- });
2296
- } else {
2297
- const policy = await registry.findOrCreatePolicy(type);
2298
- if (!policy) throw new HTTPException(500, {
2299
- message: 'Failed to create policy',
2300
- cause: {
2301
- code: 'POLICY_CREATION_FAILED',
2302
- type
2303
- }
2304
- });
2305
- policyId = policy.id;
2306
- }
2307
- if (preferences) {
2308
- const consentedPurposes = Object.entries(preferences).filter(([_, isConsented])=>isConsented).map(([purposeCode])=>purposeCode);
2309
- logger.debug('Consented purposes', {
2310
- consentedPurposes
2311
- });
2312
- const purposesRaw = await Promise.all(consentedPurposes.map((purposeCode)=>registry.findOrCreateConsentPurposeByCode(purposeCode)));
2313
- const purposes = purposesRaw.map((purpose)=>purpose?.id ?? null).filter((id)=>Boolean(id));
2314
- logger.debug('Filtered purposes', {
2315
- purposes
2316
- });
2317
- if (0 === purposes.length) logger.warn('No valid purpose IDs found after filtering. Using empty list.', {
2318
- consentedPurposes
2319
- });
2320
- purposeIds = purposes;
2321
- }
2322
- const existingConsent = await db.findFirst('consent', {
2323
- where: (b)=>b.and(b('subjectId', '=', subject.id), b('domainId', '=', domainRecord.id), b('policyId', '=', policyId), b('givenAt', '=', givenAt))
2324
- });
2325
- if (existingConsent) {
2326
- logger.debug('Duplicate consent detected, returning existing record', {
2327
- consentId: existingConsent.id
2328
- });
2329
- return c.json({
2330
- subjectId: subject.id,
2331
- consentId: existingConsent.id,
2332
- domainId: domainRecord.id,
2333
- domain: domainRecord.name,
2334
- type,
2335
- metadata,
2336
- uiSource: input.uiSource,
2337
- givenAt: existingConsent.givenAt
2338
- });
2339
- }
2340
- const result = await db.transaction(async (tx)=>{
2341
- logger.debug('Creating consent record', {
2342
- subjectId: subject.id,
2343
- domainId: domainRecord.id,
2344
- policyId,
2345
- purposeIds
2346
- });
2347
- const consentRecord = await tx.create('consent', {
2348
- id: await utils_generateUniqueId(tx, 'consent', ctx),
2349
- subjectId: subject.id,
2350
- domainId: domainRecord.id,
2351
- policyId,
2352
- purposeIds: {
2353
- json: purposeIds
2354
- },
2355
- metadata: metadata ? {
2356
- json: metadata
2357
- } : void 0,
2358
- ipAddress: ctx.ipAddress,
2359
- userAgent: ctx.userAgent,
2360
- jurisdiction: input.jurisdiction,
2361
- jurisdictionModel: input.jurisdictionModel,
2362
- tcString: input.tcString,
2363
- uiSource: input.uiSource,
2364
- givenAt
2365
- });
2366
- logger.debug('Created consent', {
2367
- consentRecord: consentRecord.id
2368
- });
2369
- if (!consentRecord) throw new HTTPException(500, {
2370
- message: 'Failed to create consent',
2371
- cause: {
2372
- code: 'CONSENT_CREATION_FAILED',
2373
- subjectId: subject.id,
2374
- domain
2375
- }
2376
- });
2377
- return {
2378
- consent: consentRecord
2379
- };
2380
- });
2381
- const metrics = getMetrics();
2382
- if (metrics) {
2383
- const jurisdiction = input.jurisdiction;
2384
- metrics.recordConsentCreated({
2385
- type,
2386
- jurisdiction
2387
- });
2388
- const hasAccepted = preferences && Object.values(preferences).some(Boolean);
2389
- if (hasAccepted) metrics.recordConsentAccepted({
2390
- type,
2391
- jurisdiction
2392
- });
2393
- else metrics.recordConsentRejected({
2394
- type,
2395
- jurisdiction
2396
- });
2397
- }
2398
- return c.json({
2399
- subjectId: subject.id,
2400
- consentId: result.consent.id,
2401
- domainId: domainRecord.id,
2402
- domain: domainRecord.name,
2403
- type,
2404
- metadata,
2405
- uiSource: input.uiSource,
2406
- givenAt: result.consent.givenAt
2407
- });
2408
- } catch (error) {
2409
- logger.error('Error in POST /subjects handler', {
2410
- error: extractErrorMessage(error),
2411
- errorType: error instanceof Error ? error.constructor.name : typeof error
2412
- });
2413
- if (error instanceof HTTPException) throw error;
2414
- throw new HTTPException(500, {
2415
- message: 'Internal server error',
2416
- cause: {
2417
- code: 'INTERNAL_SERVER_ERROR'
2418
- }
2419
- });
2420
- }
2421
- };
2422
- const createSubjectRoutes = ()=>{
2423
- const app = new Hono();
2424
- app.get('/:id', describeRoute({
2425
- summary: 'Get subject consent status',
2426
- description: `Returns the subject's consent status for this device. Use to check if the subject has valid consent for given policy types.
2427
-
2428
- **Query:** \`type\` – Filter by consent type(s), comma-separated (e.g. \`privacy_policy,cookie_banner\`).
2429
-
2430
- **Response:** \`subject\`, \`consents\` (matching filter), \`isValid\` (valid consent for requested type(s)).`,
2431
- tags: [
2432
- 'Subject',
2433
- 'Consent'
2434
- ],
2435
- responses: {
2436
- 200: {
2437
- description: 'Subject and consent records for the requested type(s)',
2438
- content: {
2439
- 'application/json': {
2440
- schema: resolver(getSubjectOutputSchema)
2441
- }
2442
- }
2443
- },
2444
- 404: {
2445
- description: 'Subject not found for the given ID'
2446
- }
2447
- }
2448
- }), validator('param', getSubjectInputSchema), getSubjectHandler);
2449
- app.post('/', describeRoute({
2450
- summary: 'Record consent for a subject',
2451
- description: `Creates a new consent record (append-only). Creates the subject if it does not exist.
2452
-
2453
- **Request body by \`type\`:**
2454
- - \`cookie_banner\` – Requires \`preferences\` object
2455
- - \`privacy_policy\`, \`dpa\`, \`terms_and_conditions\` – Optional \`policyId\`
2456
- - \`marketing_communications\`, \`age_verification\`, \`other\` – Optional \`preferences\``,
2457
- tags: [
2458
- 'Subject',
2459
- 'Consent'
2460
- ],
2461
- responses: {
2462
- 200: {
2463
- description: 'Consent recorded; subject and consent in response',
2464
- content: {
2465
- 'application/json': {
2466
- schema: resolver(postSubjectOutputSchema)
2467
- }
2468
- }
2469
- },
2470
- 422: {
2471
- description: 'Invalid request body (schema or validation failed)'
2472
- }
2473
- }
2474
- }), validator('json', postSubjectInputSchema), postSubjectHandler);
2475
- app.patch('/:id', describeRoute({
2476
- summary: 'Link external ID to subject',
2477
- description: 'Associates an external user ID with an existing subject (e.g. after login). Enables cross-device consent sync.',
2478
- tags: [
2479
- 'Subject'
2480
- ],
2481
- responses: {
2482
- 200: {
2483
- description: 'Subject updated with external ID',
2484
- content: {
2485
- 'application/json': {
2486
- schema: resolver(patchSubjectOutputSchema)
2487
- }
2488
- }
2489
- },
2490
- 404: {
2491
- description: 'Subject not found for the given ID'
2492
- }
2493
- }
2494
- }), validator('param', object({
2495
- id: subjectIdSchema
2496
- })), validator('json', object({
2497
- externalId: string(),
2498
- identityProvider: optional(string())
2499
- })), patchSubjectHandler);
2500
- app.get('/', describeRoute({
2501
- summary: 'List subjects by external ID (API key required)',
2502
- description: 'Returns all subjects linked to the given external ID. Requires Bearer token (API key). Use for server-side consent lookups.',
2503
- tags: [
2504
- 'Subject'
2505
- ],
2506
- security: [
2507
- {
2508
- bearerAuth: []
2509
- }
2510
- ],
2511
- responses: {
2512
- 200: {
2513
- description: 'List of subjects for the external ID',
2514
- content: {
2515
- 'application/json': {
2516
- schema: resolver(listSubjectsOutputSchema)
2517
- }
2518
- }
2519
- },
2520
- 401: {
2521
- description: 'Missing or invalid API key'
2522
- }
2523
- }
2524
- }), validator('query', listSubjectsQuerySchema), listSubjectsHandler);
2525
- return app;
701
+ const policyBuilder = {
702
+ create: buildPolicyConfig,
703
+ createPack: buildPolicyPack,
704
+ createPackWithDefault: buildPolicyPackWithDefault,
705
+ composePacks: composePacks
2526
706
  };
2527
- const defineConfig = (config)=>config;
2528
707
  const c15tInstance = (options)=>{
2529
708
  const context = init(options);
2530
709
  const logger = logger_createLogger(options.logger);
2531
710
  const app = new Hono();
2532
- const openApiConfig = config_createOpenAPIConfig(options);
711
+ const openApiConfig = createOpenAPIConfig(options);
2533
712
  const basePath = options.basePath || '/';
2534
713
  const corsOptions = createCORSOptions(options.trustedOrigins);
2535
714
  app.use('*', cors(corsOptions));
@@ -2537,7 +716,7 @@ const c15tInstance = (options)=>{
2537
716
  app.use('*', async (c, next)=>{
2538
717
  const request = c.req.raw;
2539
718
  const startTime = Date.now();
2540
- const apiKeyAuthenticated = validateRequestAuth(request.headers, options.advanced?.apiKeys);
719
+ const apiKeyAuthenticated = validateRequestAuth(request.headers, options.apiKeys);
2541
720
  const enrichedContext = {
2542
721
  ...context,
2543
722
  ipAddress: getIpAddress(request, options),
@@ -2560,7 +739,7 @@ const c15tInstance = (options)=>{
2560
739
  span.updateName(`${c.req.method} ${routePattern}`);
2561
740
  span.setAttribute('http.route', routePattern);
2562
741
  span.setStatus({
2563
- code: api_SpanStatusCode.OK
742
+ code: SpanStatusCode.OK
2564
743
  });
2565
744
  } else await runNext();
2566
745
  } catch (error) {
@@ -2606,16 +785,16 @@ const c15tInstance = (options)=>{
2606
785
  }));
2607
786
  const publicSpecUrl = `${basePath}${openApiConfig.specPath}`.replace(/\/+/g, '/');
2608
787
  app.get(openApiConfig.docsPath, apiReference({
2609
- spec: {
2610
- url: publicSpecUrl
2611
- },
788
+ url: publicSpecUrl,
2612
789
  pageTitle: `${options.appName || 'c15t API'} Documentation`
2613
790
  }));
2614
791
  }
2615
792
  app.route('/init', createInitRoute(options));
793
+ app.route('/legal-documents', createLegalDocumentRoutes());
2616
794
  app.route('/subjects', createSubjectRoutes());
2617
795
  app.route('/consents', createConsentRoutes());
2618
796
  app.route('/status', createStatusRoute());
797
+ app.route('/', createStatusRoute());
2619
798
  app.onError((err, c)=>{
2620
799
  const ctx = c.get('c15tContext');
2621
800
  const log = ctx?.logger || logger;
@@ -2700,4 +879,7 @@ const c15tInstance = (options)=>{
2700
879
  getDocsUI
2701
880
  };
2702
881
  };
2703
- export { c15tInstance, defineConfig, version_version as version };
882
+ export { defineConfig } from "./define-config.js";
883
+ export { inspectPolicies } from "./583.js";
884
+ export { version } from "./302.js";
885
+ export { EEA_COUNTRY_CODES, EU_COUNTRY_CODES, POLICY_MATCH_DATASET_VERSION, UK_COUNTRY_CODES, c15tInstance, policyBuilder, policyMatchers, policyPackPresets };