@c15t/backend 2.0.0-rc.1 → 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 +51 -37
  132. package/.turbo/turbo-build.log +0 -49
  133. package/CHANGELOG.md +0 -99
  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/915.js ADDED
@@ -0,0 +1,1771 @@
1
+ import { hashSha256Hex } from "@c15t/schema/types";
2
+ import base_x from "base-x";
3
+ import { checkConsentOutputSchema, checkConsentQuerySchema, getSubjectInputSchema, getSubjectOutputSchema, initOutputSchema, legalDocumentCurrentInputSchema, legalDocumentCurrentOutputSchema, legalDocumentCurrentParamsSchema, listSubjectsOutputSchema, listSubjectsQuerySchema, patchSubjectOutputSchema, postSubjectInputSchema, postSubjectOutputSchema, statusOutputSchema, subjectIdSchema } from "@c15t/schema";
4
+ import { Hono } from "hono";
5
+ import { describeRoute, resolver, validator } from "hono-openapi";
6
+ import { HTTPException } from "hono/http-exception";
7
+ import { errors, jwtVerify } from "jose";
8
+ import { version, getMetrics, withDatabaseSpan, extractErrorMessage } from "./302.js";
9
+ import { getLocation, resolveInitPayload, policy_resolvePolicyDecision, verifyPolicySnapshotToken, getJurisdiction } from "./583.js";
10
+ import * as __rspack_external_valibot from "valibot";
11
+ const prefixes = {
12
+ auditLog: 'log',
13
+ consent: 'cns',
14
+ consentPolicy: 'pol',
15
+ consentPurpose: 'pur',
16
+ domain: 'dom',
17
+ runtimePolicyDecision: 'rpd',
18
+ subject: 'sub'
19
+ };
20
+ const b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
21
+ function generateId(model) {
22
+ const buf = crypto.getRandomValues(new Uint8Array(20));
23
+ const prefix = prefixes[model];
24
+ const EPOCH_TIMESTAMP = 1700000000000;
25
+ const t = Date.now() - EPOCH_TIMESTAMP;
26
+ const high = Math.floor(t / 0x100000000);
27
+ const low = t >>> 0;
28
+ buf[0] = high >>> 24 & 255;
29
+ buf[1] = high >>> 16 & 255;
30
+ buf[2] = high >>> 8 & 255;
31
+ buf[3] = 255 & high;
32
+ buf[4] = low >>> 24 & 255;
33
+ buf[5] = low >>> 16 & 255;
34
+ buf[6] = low >>> 8 & 255;
35
+ buf[7] = 255 & low;
36
+ return `${prefix}_${b58.encode(buf)}`;
37
+ }
38
+ async function generateUniqueId(db, model, ctx, options = {}) {
39
+ const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
40
+ if (attempt >= maxRetries) {
41
+ const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
42
+ ctx?.logger?.error?.('ID generation failed', {
43
+ model,
44
+ maxRetries
45
+ });
46
+ throw error;
47
+ }
48
+ const id = generateId(model);
49
+ try {
50
+ const existing = await db.findFirst(model, {
51
+ where: (b)=>b('id', '=', id)
52
+ });
53
+ if (existing) {
54
+ ctx?.logger?.debug?.('ID conflict detected', {
55
+ id,
56
+ model,
57
+ attempt: attempt + 1,
58
+ maxRetries
59
+ });
60
+ const delay = Math.min(baseDelay * 2 ** attempt, 1000);
61
+ await new Promise((resolve)=>setTimeout(resolve, delay));
62
+ return generateUniqueId(db, model, ctx, {
63
+ maxRetries,
64
+ attempt: attempt + 1,
65
+ baseDelay
66
+ });
67
+ }
68
+ return id;
69
+ } catch (error) {
70
+ ctx?.logger?.error?.('Error checking ID uniqueness', {
71
+ error: error.message,
72
+ model,
73
+ attempt
74
+ });
75
+ if (attempt < maxRetries - 1) {
76
+ const delay = Math.min(baseDelay * 2 ** attempt, 2000);
77
+ await new Promise((resolve)=>setTimeout(resolve, delay));
78
+ return generateUniqueId(db, model, ctx, {
79
+ maxRetries,
80
+ attempt: attempt + 1,
81
+ baseDelay
82
+ });
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+ class LegalDocumentPolicyConflictError extends Error {
88
+ constructor(message){
89
+ super(message);
90
+ this.name = 'LegalDocumentPolicyConflictError';
91
+ }
92
+ }
93
+ async function buildLegalDocumentPolicyId(input) {
94
+ const digest = await hashSha256Hex([
95
+ input.tenantId ?? 'default',
96
+ input.type,
97
+ input.hash
98
+ ].join('|'));
99
+ return `pol_${digest}`;
100
+ }
101
+ function hasLegalDocumentPolicyConflict(policy, input) {
102
+ return policy.version !== input.version || policy.hash !== input.hash || policy.effectiveDate.getTime() !== input.effectiveDate.getTime();
103
+ }
104
+ function policyRegistry({ db, ctx }) {
105
+ const { logger } = ctx;
106
+ return {
107
+ findConsentPolicyById: async (policyId)=>{
108
+ const start = Date.now();
109
+ try {
110
+ const result = await withDatabaseSpan({
111
+ operation: 'find',
112
+ entity: 'consentPolicy'
113
+ }, async ()=>{
114
+ const policy = await db.findFirst('consentPolicy', {
115
+ where: (b)=>b('id', '=', policyId)
116
+ });
117
+ return policy;
118
+ });
119
+ getMetrics()?.recordDbQuery({
120
+ operation: 'find',
121
+ entity: 'consentPolicy'
122
+ }, Date.now() - start);
123
+ return result;
124
+ } catch (error) {
125
+ getMetrics()?.recordDbError({
126
+ operation: 'find',
127
+ entity: 'consentPolicy'
128
+ });
129
+ throw error;
130
+ }
131
+ },
132
+ findLatestPolicyByType: async (type)=>{
133
+ const start = Date.now();
134
+ try {
135
+ const result = await withDatabaseSpan({
136
+ operation: 'findLatest',
137
+ entity: 'consentPolicy'
138
+ }, async ()=>db.findFirst('consentPolicy', {
139
+ where: (b)=>b.and(b('isActive', '=', true), b('type', '=', type)),
140
+ orderBy: [
141
+ 'effectiveDate',
142
+ 'desc'
143
+ ]
144
+ }));
145
+ getMetrics()?.recordDbQuery({
146
+ operation: 'findLatest',
147
+ entity: 'consentPolicy'
148
+ }, Date.now() - start);
149
+ return result;
150
+ } catch (error) {
151
+ getMetrics()?.recordDbError({
152
+ operation: 'findLatest',
153
+ entity: 'consentPolicy'
154
+ });
155
+ throw error;
156
+ }
157
+ },
158
+ findLegalDocumentPolicyByHash: async (type, hash)=>{
159
+ const start = Date.now();
160
+ try {
161
+ const policyId = await buildLegalDocumentPolicyId({
162
+ tenantId: ctx.tenantId,
163
+ type,
164
+ hash
165
+ });
166
+ const result = await withDatabaseSpan({
167
+ operation: 'findByHash',
168
+ entity: 'consentPolicy'
169
+ }, async ()=>db.findFirst('consentPolicy', {
170
+ where: (b)=>b('id', '=', policyId)
171
+ }));
172
+ getMetrics()?.recordDbQuery({
173
+ operation: 'findByHash',
174
+ entity: 'consentPolicy'
175
+ }, Date.now() - start);
176
+ return result;
177
+ } catch (error) {
178
+ getMetrics()?.recordDbError({
179
+ operation: 'findByHash',
180
+ entity: 'consentPolicy'
181
+ });
182
+ throw error;
183
+ }
184
+ },
185
+ syncCurrentLegalDocumentPolicy: async (input)=>{
186
+ const start = Date.now();
187
+ try {
188
+ const result = await withDatabaseSpan({
189
+ operation: 'syncCurrent',
190
+ entity: 'consentPolicy'
191
+ }, async ()=>{
192
+ const policyId = await buildLegalDocumentPolicyId({
193
+ tenantId: ctx.tenantId,
194
+ type: input.type,
195
+ hash: input.hash
196
+ });
197
+ return db.transaction(async (tx)=>{
198
+ const existing = await tx.findFirst('consentPolicy', {
199
+ where: (b)=>b('id', '=', policyId)
200
+ });
201
+ if (existing) {
202
+ if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
203
+ await tx.updateMany('consentPolicy', {
204
+ where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true), b('id', '!=', existing.id)),
205
+ set: {
206
+ isActive: false
207
+ }
208
+ });
209
+ if (!existing.isActive) {
210
+ await tx.updateMany('consentPolicy', {
211
+ where: (b)=>b('id', '=', existing.id),
212
+ set: {
213
+ isActive: true
214
+ }
215
+ });
216
+ return {
217
+ ...existing,
218
+ isActive: true
219
+ };
220
+ }
221
+ return existing;
222
+ }
223
+ await tx.updateMany('consentPolicy', {
224
+ where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true)),
225
+ set: {
226
+ isActive: false
227
+ }
228
+ });
229
+ const policy = await tx.create('consentPolicy', {
230
+ id: policyId,
231
+ version: input.version,
232
+ type: input.type,
233
+ hash: input.hash,
234
+ effectiveDate: input.effectiveDate,
235
+ isActive: true
236
+ });
237
+ return policy;
238
+ });
239
+ });
240
+ getMetrics()?.recordDbQuery({
241
+ operation: 'syncCurrent',
242
+ entity: 'consentPolicy'
243
+ }, Date.now() - start);
244
+ return result;
245
+ } catch (error) {
246
+ getMetrics()?.recordDbError({
247
+ operation: 'syncCurrent',
248
+ entity: 'consentPolicy'
249
+ });
250
+ throw error;
251
+ }
252
+ },
253
+ findOrCreateLegalDocumentPolicy: async (input)=>{
254
+ const start = Date.now();
255
+ try {
256
+ const result = await withDatabaseSpan({
257
+ operation: 'findOrCreateLegalDocument',
258
+ entity: 'consentPolicy'
259
+ }, async ()=>{
260
+ const policyId = await buildLegalDocumentPolicyId({
261
+ tenantId: ctx.tenantId,
262
+ type: input.type,
263
+ hash: input.hash
264
+ });
265
+ const existing = await db.findFirst('consentPolicy', {
266
+ where: (b)=>b('id', '=', policyId)
267
+ });
268
+ if (existing) {
269
+ if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
270
+ return existing;
271
+ }
272
+ const policy = await db.create('consentPolicy', {
273
+ id: policyId,
274
+ version: input.version,
275
+ type: input.type,
276
+ hash: input.hash,
277
+ effectiveDate: input.effectiveDate,
278
+ isActive: false
279
+ }).catch(async ()=>{
280
+ const concurrent = await db.findFirst('consentPolicy', {
281
+ where: (b)=>b('id', '=', policyId)
282
+ });
283
+ if (!concurrent) throw new LegalDocumentPolicyConflictError('Failed to create legal document consent policy');
284
+ if (hasLegalDocumentPolicyConflict(concurrent, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
285
+ return concurrent;
286
+ });
287
+ return policy;
288
+ });
289
+ getMetrics()?.recordDbQuery({
290
+ operation: 'findOrCreateLegalDocument',
291
+ entity: 'consentPolicy'
292
+ }, Date.now() - start);
293
+ return result;
294
+ } catch (error) {
295
+ getMetrics()?.recordDbError({
296
+ operation: 'findOrCreateLegalDocument',
297
+ entity: 'consentPolicy'
298
+ });
299
+ throw error;
300
+ }
301
+ },
302
+ findOrCreatePolicy: async (type)=>{
303
+ const start = Date.now();
304
+ try {
305
+ const result = await withDatabaseSpan({
306
+ operation: 'findOrCreate',
307
+ entity: 'consentPolicy'
308
+ }, async ()=>{
309
+ const existingPolicy = await db.findFirst('consentPolicy', {
310
+ where: (b)=>b.and(b('isActive', '=', true), b('type', '=', type)),
311
+ orderBy: [
312
+ 'effectiveDate',
313
+ 'desc'
314
+ ]
315
+ });
316
+ if (existingPolicy) {
317
+ logger.debug('Found existing policy', {
318
+ type,
319
+ policyId: existingPolicy.id
320
+ });
321
+ return existingPolicy;
322
+ }
323
+ const policy = await db.create('consentPolicy', {
324
+ id: await generateUniqueId(db, 'consentPolicy', ctx),
325
+ version: '1.0.0',
326
+ type,
327
+ effectiveDate: new Date(),
328
+ isActive: true
329
+ });
330
+ return policy;
331
+ });
332
+ getMetrics()?.recordDbQuery({
333
+ operation: 'findOrCreate',
334
+ entity: 'consentPolicy'
335
+ }, Date.now() - start);
336
+ return result;
337
+ } catch (error) {
338
+ getMetrics()?.recordDbError({
339
+ operation: 'findOrCreate',
340
+ entity: 'consentPolicy'
341
+ });
342
+ throw error;
343
+ }
344
+ }
345
+ };
346
+ }
347
+ function parsePurposeIds(purposeIds) {
348
+ if (null == purposeIds) return [];
349
+ const ids = 'object' == typeof purposeIds && 'json' in purposeIds ? purposeIds.json : purposeIds;
350
+ return Array.isArray(ids) ? ids : [];
351
+ }
352
+ async function batchLoadPolicies(policyIds, ctx) {
353
+ const { db, registry } = ctx;
354
+ const policyMap = new Map();
355
+ if (policyIds.size > 0) {
356
+ const policies = await db.findMany('consentPolicy', {
357
+ where: (b)=>b('id', 'in', [
358
+ ...policyIds
359
+ ])
360
+ });
361
+ for (const p of policies)policyMap.set(p.id, p);
362
+ }
363
+ const uniqueTypes = new Set();
364
+ for (const p of policyMap.values())uniqueTypes.add(p.type);
365
+ const latestPolicyByType = new Map();
366
+ for (const type of uniqueTypes){
367
+ const latest = await registry.findLatestPolicyByType(type);
368
+ if (latest) latestPolicyByType.set(type, latest.id);
369
+ }
370
+ return {
371
+ policyMap,
372
+ latestPolicyByType
373
+ };
374
+ }
375
+ async function enrichConsents(consents, ctx) {
376
+ if (0 === consents.length) return [];
377
+ const policyIds = new Set();
378
+ for (const c of consents)if (c.policyId) policyIds.add(c.policyId);
379
+ const { policyMap, latestPolicyByType } = await batchLoadPolicies(policyIds, ctx);
380
+ const allPurposeIds = new Set();
381
+ for (const c of consents)for (const id of parsePurposeIds(c.purposeIds))allPurposeIds.add(id);
382
+ const purposeMap = new Map();
383
+ if (allPurposeIds.size > 0) {
384
+ const purposes = await ctx.db.findMany('consentPurpose', {
385
+ where: (b)=>b('id', 'in', [
386
+ ...allPurposeIds
387
+ ])
388
+ });
389
+ for (const p of purposes)purposeMap.set(p.id, p.code);
390
+ }
391
+ return consents.map((consent)=>{
392
+ let policyType = 'unknown';
393
+ let policyVersion;
394
+ let policyHash;
395
+ let policyEffectiveDate;
396
+ let isLatestPolicy = false;
397
+ if (consent.policyId) {
398
+ const policy = policyMap.get(consent.policyId);
399
+ if (policy) {
400
+ policyType = policy.type;
401
+ policyVersion = policy.version;
402
+ policyHash = policy.hash ?? void 0;
403
+ policyEffectiveDate = policy.effectiveDate;
404
+ isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
405
+ }
406
+ }
407
+ let preferences;
408
+ const ids = parsePurposeIds(consent.purposeIds);
409
+ if (ids.length > 0) {
410
+ preferences = {};
411
+ for (const purposeId of ids){
412
+ const code = purposeMap.get(purposeId);
413
+ if (code) preferences[code] = true;
414
+ }
415
+ }
416
+ return {
417
+ id: consent.id,
418
+ type: policyType,
419
+ policyId: consent.policyId ?? void 0,
420
+ policyVersion,
421
+ policyHash,
422
+ policyEffectiveDate,
423
+ isLatestPolicy,
424
+ preferences,
425
+ givenAt: consent.givenAt
426
+ };
427
+ });
428
+ }
429
+ async function resolveConsentPolicies(consents, ctx) {
430
+ if (0 === consents.length) return [];
431
+ const policyIds = new Set();
432
+ for (const c of consents)if (c.policyId) policyIds.add(c.policyId);
433
+ const { policyMap, latestPolicyByType } = await batchLoadPolicies(policyIds, ctx);
434
+ return consents.map((consent)=>{
435
+ let policyType = 'unknown';
436
+ let isLatestPolicy = false;
437
+ if (consent.policyId) {
438
+ const policy = policyMap.get(consent.policyId);
439
+ if (policy) {
440
+ policyType = policy.type;
441
+ isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
442
+ }
443
+ }
444
+ return {
445
+ consentId: consent.id,
446
+ policyType,
447
+ policyId: consent.policyId ?? void 0,
448
+ isLatestPolicy
449
+ };
450
+ });
451
+ }
452
+ const checkConsentHandler = async (c)=>{
453
+ const ctx = c.get('c15tContext');
454
+ const logger = ctx.logger;
455
+ logger.info('Handling GET /consents/check request');
456
+ const { db, registry } = ctx;
457
+ const externalId = c.req.query('externalId');
458
+ const type = c.req.query('type');
459
+ if (!externalId) throw new HTTPException(422, {
460
+ message: 'externalId query parameter is required',
461
+ cause: {
462
+ code: 'EXTERNAL_ID_REQUIRED'
463
+ }
464
+ });
465
+ if (!type) throw new HTTPException(422, {
466
+ message: 'type query parameter is required',
467
+ cause: {
468
+ code: 'TYPE_REQUIRED'
469
+ }
470
+ });
471
+ const types = type.split(',').map((t)=>t.trim());
472
+ logger.debug('Request parameters', {
473
+ externalId,
474
+ types
475
+ });
476
+ try {
477
+ const subjects = await db.findMany('subject', {
478
+ where: (b)=>b('externalId', '=', externalId)
479
+ });
480
+ const subjectIds = subjects.map((s)=>s.id);
481
+ const results = {};
482
+ for (const t of types)results[t] = {
483
+ hasConsent: false,
484
+ isLatestPolicy: false
485
+ };
486
+ if (0 === subjectIds.length) {
487
+ logger.debug('No subjects found for externalId', {
488
+ externalId
489
+ });
490
+ return c.json({
491
+ results
492
+ });
493
+ }
494
+ const allConsents = await Promise.all(subjectIds.map((subjectId)=>db.findMany('consent', {
495
+ where: (b)=>b('subjectId', '=', subjectId)
496
+ })));
497
+ const consents = allConsents.flat();
498
+ const policyInfos = await resolveConsentPolicies(consents, {
499
+ db,
500
+ registry
501
+ });
502
+ for (const info of policyInfos){
503
+ if (!types.includes(info.policyType)) continue;
504
+ const entry = results[info.policyType];
505
+ if (entry) {
506
+ entry.hasConsent = true;
507
+ if (info.isLatestPolicy) entry.isLatestPolicy = true;
508
+ }
509
+ }
510
+ logger.debug('Consent check results', {
511
+ externalId,
512
+ results
513
+ });
514
+ const metrics = getMetrics();
515
+ if (metrics) for (const [type, result] of Object.entries(results))metrics.recordConsentCheck(type, result.hasConsent);
516
+ return c.json({
517
+ results
518
+ });
519
+ } catch (error) {
520
+ logger.error('Error in GET /consents/check handler', {
521
+ error: extractErrorMessage(error),
522
+ errorType: error instanceof Error ? error.constructor.name : typeof error
523
+ });
524
+ if (error instanceof HTTPException) throw error;
525
+ throw new HTTPException(500, {
526
+ message: 'Internal server error',
527
+ cause: {
528
+ code: 'INTERNAL_SERVER_ERROR'
529
+ }
530
+ });
531
+ }
532
+ };
533
+ const createConsentRoutes = ()=>{
534
+ const app = new Hono();
535
+ app.get('/check', describeRoute({
536
+ summary: 'Check consent by external user ID',
537
+ description: "Pre-banner cross-device consent check. Use to avoid showing the banner when the user has already consented on another device.\n\n**Query parameters:**\n- `externalId` – External user ID to check\n- `type` – Consent type(s) to check (comma-separated)",
538
+ tags: [
539
+ 'Consent'
540
+ ],
541
+ responses: {
542
+ 200: {
543
+ description: 'Consent check result per requested type(s)',
544
+ content: {
545
+ 'application/json': {
546
+ schema: resolver(checkConsentOutputSchema)
547
+ }
548
+ }
549
+ },
550
+ 422: {
551
+ description: 'Invalid or missing query parameters'
552
+ }
553
+ }
554
+ }), validator('query', checkConsentQuerySchema), checkConsentHandler);
555
+ return app;
556
+ };
557
+ const createInitRoute = (options)=>{
558
+ const app = new Hono();
559
+ app.get('/', describeRoute({
560
+ summary: 'Get initial consent manager state',
561
+ description: `Returns the initial state required to render the consent manager.
562
+
563
+ - **Jurisdiction** – User's jurisdiction (defaults to GDPR if geo-location is disabled)
564
+ - **Location** – User's location (null if geo-location is disabled)
565
+ - **Translations** – Consent manager copy (from \`Accept-Language\` header)
566
+ - **Branding** – Configured branding key
567
+ - **GVL** – Global Vendor List when IAB is active for the request
568
+
569
+ Use for geo-targeted consent banners and regional compliance.`,
570
+ tags: [
571
+ 'Init'
572
+ ],
573
+ responses: {
574
+ 200: {
575
+ description: 'Initialization payload (jurisdiction, location, translations, branding, GVL)',
576
+ content: {
577
+ 'application/json': {
578
+ schema: resolver(initOutputSchema)
579
+ }
580
+ }
581
+ }
582
+ }
583
+ }), async (c)=>{
584
+ const ctx = c.get('c15tContext');
585
+ const payload = await resolveInitPayload(c.req.raw, options, ctx?.logger);
586
+ return c.json(payload);
587
+ });
588
+ return app;
589
+ };
590
+ const syncCurrentLegalDocumentHandler = async (c)=>{
591
+ const ctx = c.get('c15tContext');
592
+ const logger = ctx.logger;
593
+ logger.info('Handling PUT /legal-documents/:type/current request');
594
+ if (!ctx.apiKeyAuthenticated) throw new HTTPException(401, {
595
+ message: 'API key required. Use Authorization: Bearer <api_key>',
596
+ cause: {
597
+ code: 'UNAUTHORIZED'
598
+ }
599
+ });
600
+ const type = c.req.param('type');
601
+ const body = await c.req.json();
602
+ const effectiveDate = new Date(body.effectiveDate);
603
+ if (Number.isNaN(effectiveDate.getTime())) throw new HTTPException(422, {
604
+ message: 'effectiveDate must be a valid ISO-8601 string',
605
+ cause: {
606
+ code: 'INPUT_VALIDATION_FAILED'
607
+ }
608
+ });
609
+ try {
610
+ const policy = await ctx.registry.syncCurrentLegalDocumentPolicy({
611
+ type,
612
+ version: body.version,
613
+ hash: body.hash,
614
+ effectiveDate
615
+ });
616
+ return c.json({
617
+ policy: {
618
+ id: policy.id,
619
+ type: policy.type,
620
+ version: policy.version,
621
+ hash: policy.hash,
622
+ effectiveDate: policy.effectiveDate,
623
+ isActive: policy.isActive
624
+ }
625
+ });
626
+ } catch (error) {
627
+ logger.error('Error in PUT /legal-documents/:type/current handler', {
628
+ error: extractErrorMessage(error),
629
+ errorType: error instanceof Error ? error.constructor.name : typeof error
630
+ });
631
+ if (error instanceof LegalDocumentPolicyConflictError) throw new HTTPException(409, {
632
+ message: error.message,
633
+ cause: {
634
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
635
+ }
636
+ });
637
+ if (error instanceof HTTPException) throw error;
638
+ throw new HTTPException(500, {
639
+ message: 'Internal server error',
640
+ cause: {
641
+ code: 'INTERNAL_SERVER_ERROR'
642
+ }
643
+ });
644
+ }
645
+ };
646
+ const createLegalDocumentRoutes = ()=>{
647
+ const app = new Hono();
648
+ app.put('/:type/current', describeRoute({
649
+ summary: 'Sync the current legal document release (API key required)',
650
+ description: 'Marks a legal document release as the latest known version for its type. Requires a Bearer API key.',
651
+ tags: [
652
+ 'LegalDocument'
653
+ ],
654
+ security: [
655
+ {
656
+ bearerAuth: []
657
+ }
658
+ ],
659
+ responses: {
660
+ 200: {
661
+ description: 'Current legal document release synced successfully',
662
+ content: {
663
+ 'application/json': {
664
+ schema: resolver(legalDocumentCurrentOutputSchema)
665
+ }
666
+ }
667
+ },
668
+ 401: {
669
+ description: 'Missing or invalid API key'
670
+ },
671
+ 409: {
672
+ description: 'Release metadata conflicts with an existing release'
673
+ }
674
+ }
675
+ }), validator('param', legalDocumentCurrentParamsSchema), validator('json', legalDocumentCurrentInputSchema), syncCurrentLegalDocumentHandler);
676
+ return app;
677
+ };
678
+ function getHeaders(headers) {
679
+ if (!headers) return {
680
+ countryCode: null,
681
+ regionCode: null,
682
+ acceptLanguage: null
683
+ };
684
+ const normalizeHeader = (value)=>{
685
+ if (!value) return null;
686
+ return Array.isArray(value) ? value[0] ?? null : value;
687
+ };
688
+ 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'));
689
+ const regionCode = normalizeHeader(headers.get('x-c15t-region')) ?? normalizeHeader(headers.get('x-vercel-ip-country-region')) ?? normalizeHeader(headers.get('x-region-code'));
690
+ const acceptLanguage = normalizeHeader(headers.get('accept-language'));
691
+ return {
692
+ countryCode,
693
+ regionCode,
694
+ acceptLanguage
695
+ };
696
+ }
697
+ const statusHandler = async (c)=>{
698
+ const ctx = c.get('c15tContext');
699
+ const { countryCode, regionCode, acceptLanguage } = getHeaders(ctx.headers);
700
+ const clientInfo = {
701
+ ip: ctx.ipAddress ?? null,
702
+ acceptLanguage,
703
+ userAgent: ctx.userAgent ?? null,
704
+ region: {
705
+ countryCode,
706
+ regionCode
707
+ }
708
+ };
709
+ try {
710
+ await ctx.db.findFirst('subject', {});
711
+ return c.json({
712
+ version: version,
713
+ timestamp: new Date(),
714
+ client: clientInfo
715
+ });
716
+ } catch (error) {
717
+ ctx.logger.error('Database health check failed', {
718
+ error
719
+ });
720
+ throw new HTTPException(503, {
721
+ message: 'Database health check failed',
722
+ cause: {
723
+ code: 'SERVICE_UNAVAILABLE',
724
+ error
725
+ }
726
+ });
727
+ }
728
+ };
729
+ const createStatusRoute = ()=>{
730
+ const app = new Hono();
731
+ app.get('/', describeRoute({
732
+ summary: 'Health check and API status',
733
+ description: `Returns API version, timestamp, and client info (IP, region, user agent).
734
+
735
+ Use for health checks, load balancer probes, and debugging. Performs a lightweight DB check; returns 503 if the database is unreachable.`,
736
+ tags: [
737
+ 'Status'
738
+ ],
739
+ responses: {
740
+ 200: {
741
+ description: 'API is healthy (version, timestamp, client info)',
742
+ content: {
743
+ 'application/json': {
744
+ schema: resolver(statusOutputSchema)
745
+ }
746
+ }
747
+ },
748
+ 503: {
749
+ description: 'Service unavailable (e.g. database unreachable)'
750
+ }
751
+ }
752
+ }), statusHandler);
753
+ return app;
754
+ };
755
+ const getSubjectHandler = async (c)=>{
756
+ const ctx = c.get('c15tContext');
757
+ const logger = ctx.logger;
758
+ logger.info('Handling GET /subjects/:id request');
759
+ const { db, registry } = ctx;
760
+ const subjectId = c.req.param('id');
761
+ const type = c.req.query('type');
762
+ const typeFilter = type?.split(',').map((t)=>t.trim()) || [];
763
+ if (!subjectId) throw new HTTPException(400, {
764
+ message: 'Subject ID is required',
765
+ cause: {
766
+ code: 'SUBJECT_ID_REQUIRED'
767
+ }
768
+ });
769
+ logger.debug('Request parameters', {
770
+ subjectId,
771
+ typeFilter
772
+ });
773
+ try {
774
+ const subject = await db.findFirst('subject', {
775
+ where: (b)=>b('id', '=', subjectId)
776
+ });
777
+ if (!subject) throw new HTTPException(404, {
778
+ message: 'Subject not found',
779
+ cause: {
780
+ code: 'SUBJECT_NOT_FOUND',
781
+ subjectId
782
+ }
783
+ });
784
+ const consents = await db.findMany('consent', {
785
+ where: (b)=>b('subjectId', '=', subjectId)
786
+ });
787
+ const consentItems = await enrichConsents(consents, {
788
+ db,
789
+ registry
790
+ });
791
+ const filteredConsents = typeFilter.length > 0 ? consentItems.filter((consent)=>typeFilter.includes(consent.type)) : consentItems;
792
+ const isValid = 0 === typeFilter.length || typeFilter.every((t)=>filteredConsents.some((consent)=>consent.type === t && consent.isLatestPolicy));
793
+ return c.json({
794
+ subject: {
795
+ id: subject.id,
796
+ externalId: subject.externalId ?? void 0,
797
+ createdAt: subject.createdAt
798
+ },
799
+ consents: filteredConsents,
800
+ isValid
801
+ });
802
+ } catch (error) {
803
+ logger.error('Error in GET /subjects/:id handler', {
804
+ error: extractErrorMessage(error),
805
+ errorType: error instanceof Error ? error.constructor.name : typeof error
806
+ });
807
+ if (error instanceof HTTPException) throw error;
808
+ throw new HTTPException(500, {
809
+ message: 'Internal server error',
810
+ cause: {
811
+ code: 'INTERNAL_SERVER_ERROR'
812
+ }
813
+ });
814
+ }
815
+ };
816
+ const listSubjectsHandler = async (c)=>{
817
+ const ctx = c.get('c15tContext');
818
+ const logger = ctx.logger;
819
+ logger.info('Handling GET /subjects request');
820
+ const { db, registry } = ctx;
821
+ if (!ctx.apiKeyAuthenticated) throw new HTTPException(401, {
822
+ message: 'API key required. Use Authorization: Bearer <api_key>',
823
+ cause: {
824
+ code: 'UNAUTHORIZED'
825
+ }
826
+ });
827
+ const externalId = c.req.query('externalId');
828
+ if (!externalId) throw new HTTPException(422, {
829
+ message: 'externalId query parameter is required',
830
+ cause: {
831
+ code: 'EXTERNAL_ID_REQUIRED'
832
+ }
833
+ });
834
+ logger.debug('Request parameters', {
835
+ externalId
836
+ });
837
+ try {
838
+ const subjects = await db.findMany('subject', {
839
+ where: (b)=>b('externalId', '=', externalId)
840
+ });
841
+ const subjectItems = await Promise.all(subjects.map(async (subject)=>{
842
+ const consents = await db.findMany('consent', {
843
+ where: (b)=>b('subjectId', '=', subject.id)
844
+ });
845
+ const consentItems = await enrichConsents(consents, {
846
+ db,
847
+ registry
848
+ });
849
+ return {
850
+ id: subject.id,
851
+ externalId: subject.externalId ?? externalId,
852
+ createdAt: subject.createdAt,
853
+ consents: consentItems
854
+ };
855
+ }));
856
+ logger.info('Found subjects for externalId', {
857
+ externalId,
858
+ count: subjectItems.length
859
+ });
860
+ return c.json({
861
+ subjects: subjectItems
862
+ });
863
+ } catch (error) {
864
+ logger.error('Error in GET /subjects handler', {
865
+ error: extractErrorMessage(error),
866
+ errorType: error instanceof Error ? error.constructor.name : typeof error
867
+ });
868
+ if (error instanceof HTTPException) throw error;
869
+ throw new HTTPException(500, {
870
+ message: 'Internal server error',
871
+ cause: {
872
+ code: 'INTERNAL_SERVER_ERROR'
873
+ }
874
+ });
875
+ }
876
+ };
877
+ const utils_prefixes = {
878
+ auditLog: 'log',
879
+ consent: 'cns',
880
+ consentPolicy: 'pol',
881
+ consentPurpose: 'pur',
882
+ domain: 'dom',
883
+ subject: 'sub'
884
+ };
885
+ const utils_b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
886
+ function utils_generateId(model) {
887
+ const buf = crypto.getRandomValues(new Uint8Array(20));
888
+ const prefix = utils_prefixes[model];
889
+ const EPOCH_TIMESTAMP = 1700000000000;
890
+ const t = Date.now() - EPOCH_TIMESTAMP;
891
+ const high = Math.floor(t / 0x100000000);
892
+ const low = t >>> 0;
893
+ buf[0] = high >>> 24 & 255;
894
+ buf[1] = high >>> 16 & 255;
895
+ buf[2] = high >>> 8 & 255;
896
+ buf[3] = 255 & high;
897
+ buf[4] = low >>> 24 & 255;
898
+ buf[5] = low >>> 16 & 255;
899
+ buf[6] = low >>> 8 & 255;
900
+ buf[7] = 255 & low;
901
+ return `${prefix}_${utils_b58.encode(buf)}`;
902
+ }
903
+ async function utils_generateUniqueId(db, model, ctx, options = {}) {
904
+ const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
905
+ if (attempt >= maxRetries) {
906
+ const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
907
+ ctx?.logger?.error?.('ID generation failed', {
908
+ model,
909
+ maxRetries
910
+ });
911
+ throw error;
912
+ }
913
+ const id = utils_generateId(model);
914
+ try {
915
+ const existing = await db.findFirst(model, {
916
+ where: (b)=>b('id', '=', id)
917
+ });
918
+ if (existing) {
919
+ ctx?.logger?.debug?.('ID conflict detected', {
920
+ id,
921
+ model,
922
+ attempt: attempt + 1,
923
+ maxRetries
924
+ });
925
+ const delay = Math.min(baseDelay * 2 ** attempt, 1000);
926
+ await new Promise((resolve)=>setTimeout(resolve, delay));
927
+ return utils_generateUniqueId(db, model, ctx, {
928
+ maxRetries,
929
+ attempt: attempt + 1,
930
+ baseDelay
931
+ });
932
+ }
933
+ return id;
934
+ } catch (error) {
935
+ ctx?.logger?.error?.('Error checking ID uniqueness', {
936
+ error: error.message,
937
+ model,
938
+ attempt
939
+ });
940
+ if (attempt < maxRetries - 1) {
941
+ const delay = Math.min(baseDelay * 2 ** attempt, 2000);
942
+ await new Promise((resolve)=>setTimeout(resolve, delay));
943
+ return utils_generateUniqueId(db, model, ctx, {
944
+ maxRetries,
945
+ attempt: attempt + 1,
946
+ baseDelay
947
+ });
948
+ }
949
+ throw error;
950
+ }
951
+ }
952
+ const patchSubjectHandler = async (c)=>{
953
+ const ctx = c.get('c15tContext');
954
+ const logger = ctx.logger;
955
+ logger.info('Handling PATCH /subjects/:id request');
956
+ const { db } = ctx;
957
+ const subjectId = c.req.param('id');
958
+ const body = await c.req.json();
959
+ const { externalId, identityProvider = 'external' } = body;
960
+ if (!subjectId) throw new HTTPException(400, {
961
+ message: 'Subject ID is required',
962
+ cause: {
963
+ code: 'SUBJECT_ID_REQUIRED'
964
+ }
965
+ });
966
+ logger.debug('Request parameters', {
967
+ subjectId,
968
+ externalId,
969
+ identityProvider
970
+ });
971
+ try {
972
+ const subject = await db.findFirst('subject', {
973
+ where: (b)=>b('id', '=', subjectId)
974
+ });
975
+ if (!subject) throw new HTTPException(404, {
976
+ message: 'Subject not found',
977
+ cause: {
978
+ code: 'SUBJECT_NOT_FOUND',
979
+ subjectId
980
+ }
981
+ });
982
+ await db.transaction(async (tx)=>{
983
+ await tx.updateMany('subject', {
984
+ where: (b)=>b('id', '=', subjectId),
985
+ set: {
986
+ externalId,
987
+ identityProvider,
988
+ updatedAt: new Date()
989
+ }
990
+ });
991
+ await tx.create('auditLog', {
992
+ id: await utils_generateUniqueId(tx, 'auditLog', ctx),
993
+ subjectId,
994
+ entityType: 'subject',
995
+ entityId: subjectId,
996
+ actionType: 'identify_user',
997
+ ipAddress: ctx.ipAddress || null,
998
+ userAgent: ctx.userAgent || null,
999
+ changes: {
1000
+ externalId: {
1001
+ from: subject.externalId,
1002
+ to: externalId
1003
+ },
1004
+ identityProvider: {
1005
+ from: subject.identityProvider,
1006
+ to: identityProvider
1007
+ }
1008
+ },
1009
+ metadata: {
1010
+ externalId,
1011
+ identityProvider
1012
+ }
1013
+ });
1014
+ });
1015
+ logger.info('Subject linked to external ID', {
1016
+ subjectId,
1017
+ externalId,
1018
+ identityProvider
1019
+ });
1020
+ getMetrics()?.recordSubjectLinked(identityProvider);
1021
+ return c.json({
1022
+ success: true,
1023
+ subject: {
1024
+ id: subjectId,
1025
+ externalId
1026
+ }
1027
+ });
1028
+ } catch (error) {
1029
+ logger.error('Error in PATCH /subjects/:id handler', {
1030
+ error: extractErrorMessage(error),
1031
+ errorType: error instanceof Error ? error.constructor.name : typeof error
1032
+ });
1033
+ if (error instanceof HTTPException) throw error;
1034
+ throw new HTTPException(500, {
1035
+ message: 'Internal server error',
1036
+ cause: {
1037
+ code: 'INTERNAL_SERVER_ERROR'
1038
+ }
1039
+ });
1040
+ }
1041
+ };
1042
+ const DEFAULT_ISSUER = 'c15t';
1043
+ const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
1044
+ function isLegalDocumentPolicyType(type) {
1045
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1046
+ }
1047
+ function resolveSnapshotIssuer(options) {
1048
+ return options?.issuer?.trim() || DEFAULT_ISSUER;
1049
+ }
1050
+ function resolveSnapshotAudience(params) {
1051
+ const configuredAudience = params.options?.audience?.trim();
1052
+ if (configuredAudience) return configuredAudience;
1053
+ return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
1054
+ }
1055
+ function getSigningKey(secret) {
1056
+ return new TextEncoder().encode(secret);
1057
+ }
1058
+ function isLegalDocumentSnapshotPayload(payload) {
1059
+ return 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub && isLegalDocumentPolicyType(payload.type) && 'string' == typeof payload.version && 'string' == typeof payload.hash && 'string' == typeof payload.effectiveDate && 'number' == typeof payload.iat && 'number' == typeof payload.exp;
1060
+ }
1061
+ async function verifyLegalDocumentSnapshotToken(params) {
1062
+ const { token, options, tenantId } = params;
1063
+ if (!options?.signingKey) return {
1064
+ valid: false,
1065
+ reason: 'missing'
1066
+ };
1067
+ if (!token) return {
1068
+ valid: false,
1069
+ reason: 'missing'
1070
+ };
1071
+ if (3 !== token.split('.').length) return {
1072
+ valid: false,
1073
+ reason: 'malformed'
1074
+ };
1075
+ try {
1076
+ const { payload, protectedHeader } = await jwtVerify(token, getSigningKey(options.signingKey), {
1077
+ issuer: resolveSnapshotIssuer(options),
1078
+ audience: resolveSnapshotAudience({
1079
+ options,
1080
+ tenantId
1081
+ })
1082
+ });
1083
+ const header = protectedHeader;
1084
+ if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
1085
+ valid: false,
1086
+ reason: 'invalid'
1087
+ };
1088
+ if (!isLegalDocumentSnapshotPayload(payload)) return {
1089
+ valid: false,
1090
+ reason: 'invalid'
1091
+ };
1092
+ if (payload.sub !== payload.hash) return {
1093
+ valid: false,
1094
+ reason: 'invalid'
1095
+ };
1096
+ if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
1097
+ valid: false,
1098
+ reason: 'invalid'
1099
+ };
1100
+ return {
1101
+ valid: true,
1102
+ payload
1103
+ };
1104
+ } catch (error) {
1105
+ if (error instanceof errors.JWTExpired) return {
1106
+ valid: false,
1107
+ reason: 'expired'
1108
+ };
1109
+ return {
1110
+ valid: false,
1111
+ reason: 'invalid'
1112
+ };
1113
+ }
1114
+ }
1115
+ function buildRuntimeDecisionDedupeKey(input) {
1116
+ return [
1117
+ input.tenantId ?? 'default',
1118
+ input.fingerprint,
1119
+ input.matchedBy,
1120
+ input.countryCode ?? 'none',
1121
+ input.regionCode ?? 'none',
1122
+ input.jurisdiction,
1123
+ input.language ?? 'none'
1124
+ ].join('|');
1125
+ }
1126
+ function buildDecisionPayload(params) {
1127
+ const { tenantId, snapshot, decision, location, jurisdiction, language, proofConfig } = params;
1128
+ if (snapshot?.valid && snapshot.payload) {
1129
+ const sp = snapshot.payload;
1130
+ return {
1131
+ tenantId,
1132
+ policyId: sp.policyId,
1133
+ fingerprint: sp.fingerprint,
1134
+ matchedBy: sp.matchedBy,
1135
+ countryCode: sp.country,
1136
+ regionCode: sp.region,
1137
+ jurisdiction: sp.jurisdiction,
1138
+ language: sp.language,
1139
+ model: sp.model,
1140
+ policyI18n: sp.policyI18n,
1141
+ uiMode: sp.uiMode,
1142
+ bannerUi: sp.bannerUi,
1143
+ dialogUi: sp.dialogUi,
1144
+ categories: sp.categories,
1145
+ preselectedCategories: sp.preselectedCategories,
1146
+ proofConfig: sp.proofConfig,
1147
+ dedupeKey: buildRuntimeDecisionDedupeKey({
1148
+ tenantId,
1149
+ fingerprint: sp.fingerprint,
1150
+ matchedBy: sp.matchedBy,
1151
+ countryCode: sp.country,
1152
+ regionCode: sp.region,
1153
+ jurisdiction: sp.jurisdiction,
1154
+ language: sp.language
1155
+ }),
1156
+ source: 'snapshot_token'
1157
+ };
1158
+ }
1159
+ if (decision) return {
1160
+ tenantId,
1161
+ policyId: decision.policy.id,
1162
+ fingerprint: decision.fingerprint,
1163
+ matchedBy: decision.matchedBy,
1164
+ countryCode: location.countryCode,
1165
+ regionCode: location.regionCode,
1166
+ jurisdiction,
1167
+ language,
1168
+ model: decision.policy.model,
1169
+ policyI18n: decision.policy.i18n,
1170
+ uiMode: decision.policy.ui?.mode,
1171
+ bannerUi: decision.policy.ui?.banner,
1172
+ dialogUi: decision.policy.ui?.dialog,
1173
+ categories: decision.policy.consent?.categories,
1174
+ preselectedCategories: decision.policy.consent?.preselectedCategories,
1175
+ proofConfig,
1176
+ dedupeKey: buildRuntimeDecisionDedupeKey({
1177
+ tenantId,
1178
+ fingerprint: decision.fingerprint,
1179
+ matchedBy: decision.matchedBy,
1180
+ countryCode: location.countryCode,
1181
+ regionCode: location.regionCode,
1182
+ jurisdiction,
1183
+ language
1184
+ }),
1185
+ source: 'write_time_fallback'
1186
+ };
1187
+ }
1188
+ function parseLanguageFromHeader(header) {
1189
+ if (!header) return;
1190
+ const firstLanguage = header.split(',')[0]?.split(';')[0]?.trim();
1191
+ if (!firstLanguage) return;
1192
+ return firstLanguage.split('-')[0]?.toLowerCase();
1193
+ }
1194
+ function isLegalDocumentType(type) {
1195
+ return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
1196
+ }
1197
+ function resolveSnapshotFailureMode(ctx) {
1198
+ return ctx.policySnapshot?.onValidationFailure ?? 'reject';
1199
+ }
1200
+ function buildSnapshotHttpException(reason) {
1201
+ switch(reason){
1202
+ case 'missing':
1203
+ return new HTTPException(409, {
1204
+ message: 'Policy snapshot token is required',
1205
+ cause: {
1206
+ code: 'POLICY_SNAPSHOT_REQUIRED'
1207
+ }
1208
+ });
1209
+ case 'expired':
1210
+ return new HTTPException(409, {
1211
+ message: 'Policy snapshot token has expired',
1212
+ cause: {
1213
+ code: 'POLICY_SNAPSHOT_EXPIRED'
1214
+ }
1215
+ });
1216
+ case 'malformed':
1217
+ case 'invalid':
1218
+ return new HTTPException(409, {
1219
+ message: 'Policy snapshot token is invalid',
1220
+ cause: {
1221
+ code: 'POLICY_SNAPSHOT_INVALID'
1222
+ }
1223
+ });
1224
+ default:
1225
+ {
1226
+ const _exhaustive = reason;
1227
+ throw new Error(`Unhandled policy snapshot verification failure reason: ${_exhaustive}`);
1228
+ }
1229
+ }
1230
+ }
1231
+ function buildLegalDocumentSnapshotHttpException(reason) {
1232
+ switch(reason){
1233
+ case 'missing':
1234
+ return new HTTPException(409, {
1235
+ message: 'Legal document snapshot token is required',
1236
+ cause: {
1237
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
1238
+ }
1239
+ });
1240
+ case 'expired':
1241
+ return new HTTPException(409, {
1242
+ message: 'Legal document snapshot token has expired',
1243
+ cause: {
1244
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
1245
+ }
1246
+ });
1247
+ case 'malformed':
1248
+ case 'invalid':
1249
+ return new HTTPException(409, {
1250
+ message: 'Legal document snapshot token is invalid',
1251
+ cause: {
1252
+ code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
1253
+ }
1254
+ });
1255
+ default:
1256
+ {
1257
+ const _exhaustive = reason;
1258
+ throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
1259
+ }
1260
+ }
1261
+ }
1262
+ function buildLegalDocumentProofHttpException(message) {
1263
+ return new HTTPException(409, {
1264
+ message,
1265
+ cause: {
1266
+ code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
1267
+ }
1268
+ });
1269
+ }
1270
+ const postSubjectHandler = async (c)=>{
1271
+ const ctx = c.get('c15tContext');
1272
+ const logger = ctx.logger;
1273
+ logger.info('Handling POST /subjects request');
1274
+ const { db, registry } = ctx;
1275
+ const input = await c.req.json();
1276
+ const { type, subjectId, identityProvider, externalSubjectId, domain, metadata, givenAt: givenAtEpoch } = input;
1277
+ const preferences = 'preferences' in input ? input.preferences : void 0;
1278
+ const givenAt = new Date(givenAtEpoch);
1279
+ const rawConsentAction = 'consentAction' in input ? input.consentAction : void 0;
1280
+ let derivedConsentAction;
1281
+ logger.debug('Request parameters', {
1282
+ type,
1283
+ subjectId,
1284
+ identityProvider,
1285
+ externalSubjectId,
1286
+ domain
1287
+ });
1288
+ try {
1289
+ if ('cookie_banner' === type) logger.warn('`cookie_banner` policy type is deprecated in 2.0 RC and will be removed in 2.0 GA. Use backend runtime `policyPacks` for banner behavior.');
1290
+ const request = c.req.raw ?? new Request('https://c15t.local/subjects');
1291
+ const acceptLanguage = request.headers.get('accept-language');
1292
+ const requestLanguage = parseLanguageFromHeader(acceptLanguage);
1293
+ const location = await getLocation(request, ctx);
1294
+ const resolvedJurisdiction = getJurisdiction(location, ctx);
1295
+ const legalDocumentConsent = isLegalDocumentType(type);
1296
+ const runtimeSnapshotVerification = legalDocumentConsent ? {
1297
+ valid: false,
1298
+ reason: 'missing'
1299
+ } : await verifyPolicySnapshotToken({
1300
+ token: input.policySnapshotToken,
1301
+ options: ctx.policySnapshot,
1302
+ tenantId: ctx.tenantId
1303
+ });
1304
+ const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
1305
+ token: input.documentSnapshotToken,
1306
+ options: ctx.legalDocumentSnapshot,
1307
+ tenantId: ctx.tenantId
1308
+ }) : {
1309
+ valid: false,
1310
+ reason: 'missing'
1311
+ };
1312
+ const hasValidSnapshot = runtimeSnapshotVerification.valid;
1313
+ const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
1314
+ const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
1315
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
1316
+ const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
1317
+ if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
1318
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await policy_resolvePolicyDecision({
1319
+ policies: ctx.policyPacks,
1320
+ countryCode: location.countryCode,
1321
+ regionCode: location.regionCode,
1322
+ jurisdiction: resolvedJurisdiction,
1323
+ iabEnabled: ctx.iab?.enabled === true
1324
+ });
1325
+ const effectivePolicy = hasValidSnapshot && snapshotPayload ? {
1326
+ id: snapshotPayload.policyId,
1327
+ model: snapshotPayload.model,
1328
+ i18n: snapshotPayload.policyI18n,
1329
+ consent: {
1330
+ expiryDays: snapshotPayload.expiryDays,
1331
+ scopeMode: snapshotPayload.scopeMode,
1332
+ categories: snapshotPayload.categories,
1333
+ preselectedCategories: snapshotPayload.preselectedCategories,
1334
+ gpc: snapshotPayload.gpc
1335
+ },
1336
+ ui: {
1337
+ mode: snapshotPayload.uiMode,
1338
+ banner: snapshotPayload.bannerUi,
1339
+ dialog: snapshotPayload.dialogUi
1340
+ },
1341
+ proof: snapshotPayload.proofConfig
1342
+ } : resolvedPolicyDecision?.policy;
1343
+ const effectiveModel = effectivePolicy?.model ?? ('opt-in' === input.jurisdictionModel || 'opt-out' === input.jurisdictionModel || 'iab' === input.jurisdictionModel ? input.jurisdictionModel : void 0);
1344
+ if ('all' === rawConsentAction) derivedConsentAction = 'accept_all';
1345
+ else if ('necessary' === rawConsentAction) derivedConsentAction = 'opt-out' === effectiveModel ? 'opt_out' : 'reject_all';
1346
+ else if ('custom' === rawConsentAction) derivedConsentAction = 'custom';
1347
+ const subject = await registry.findOrCreateSubject({
1348
+ subjectId,
1349
+ externalSubjectId,
1350
+ identityProvider,
1351
+ ipAddress: ctx.ipAddress
1352
+ });
1353
+ if (!subject) throw new HTTPException(500, {
1354
+ message: 'Failed to create subject',
1355
+ cause: {
1356
+ code: 'SUBJECT_CREATION_FAILED',
1357
+ subjectId
1358
+ }
1359
+ });
1360
+ logger.debug('Subject found/created', {
1361
+ subjectId: subject.id
1362
+ });
1363
+ const domainRecord = await registry.findOrCreateDomain(domain);
1364
+ if (!domainRecord) throw new HTTPException(500, {
1365
+ message: 'Failed to create domain',
1366
+ cause: {
1367
+ code: 'DOMAIN_CREATION_FAILED',
1368
+ domain
1369
+ }
1370
+ });
1371
+ let policyId;
1372
+ let purposeIds = [];
1373
+ let appliedPreferences;
1374
+ const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
1375
+ const inputPolicyHash = 'policyHash' in input ? input.policyHash : void 0;
1376
+ if (legalDocumentConsent && legalDocumentSnapshotVerification.valid) {
1377
+ if (legalDocumentSnapshotVerification.payload.type !== type) throw buildLegalDocumentSnapshotHttpException('invalid');
1378
+ const effectiveDate = new Date(legalDocumentSnapshotVerification.payload.effectiveDate);
1379
+ if (Number.isNaN(effectiveDate.getTime())) throw buildLegalDocumentSnapshotHttpException('invalid');
1380
+ const documentPolicy = await registry.findOrCreateLegalDocumentPolicy({
1381
+ type,
1382
+ version: legalDocumentSnapshotVerification.payload.version,
1383
+ hash: legalDocumentSnapshotVerification.payload.hash,
1384
+ effectiveDate
1385
+ });
1386
+ policyId = documentPolicy.id;
1387
+ } else if (legalDocumentConsent) {
1388
+ if (!ctx.legalDocumentSnapshot?.signingKey && !inputPolicyId && !inputPolicyHash) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId or policyHash when snapshot verification is disabled');
1389
+ if (inputPolicyId) {
1390
+ policyId = inputPolicyId;
1391
+ const policy = await registry.findConsentPolicyById(inputPolicyId);
1392
+ if (!policy) throw new HTTPException(404, {
1393
+ message: 'Policy not found',
1394
+ cause: {
1395
+ code: 'POLICY_NOT_FOUND',
1396
+ policyId,
1397
+ type
1398
+ }
1399
+ });
1400
+ if (!policy.isActive) throw new HTTPException(400, {
1401
+ message: 'Policy is inactive',
1402
+ cause: {
1403
+ code: 'POLICY_INACTIVE',
1404
+ policyId,
1405
+ type
1406
+ }
1407
+ });
1408
+ } else if (inputPolicyHash) {
1409
+ const policy = await registry.findLegalDocumentPolicyByHash(type, inputPolicyHash);
1410
+ if (!policy) throw new HTTPException(404, {
1411
+ message: 'Policy not found',
1412
+ cause: {
1413
+ code: 'POLICY_NOT_FOUND',
1414
+ type,
1415
+ policyHash: inputPolicyHash
1416
+ }
1417
+ });
1418
+ if (!policy.isActive) throw new HTTPException(400, {
1419
+ message: 'Policy is inactive',
1420
+ cause: {
1421
+ code: 'POLICY_INACTIVE',
1422
+ policyId: policy.id,
1423
+ type,
1424
+ policyHash: inputPolicyHash
1425
+ }
1426
+ });
1427
+ policyId = policy.id;
1428
+ }
1429
+ } else if (inputPolicyId) {
1430
+ policyId = inputPolicyId;
1431
+ const policy = await registry.findConsentPolicyById(inputPolicyId);
1432
+ if (!policy) throw new HTTPException(404, {
1433
+ message: 'Policy not found',
1434
+ cause: {
1435
+ code: 'POLICY_NOT_FOUND',
1436
+ policyId,
1437
+ type
1438
+ }
1439
+ });
1440
+ if (!policy.isActive) throw new HTTPException(400, {
1441
+ message: 'Policy is inactive',
1442
+ cause: {
1443
+ code: 'POLICY_INACTIVE',
1444
+ policyId,
1445
+ type
1446
+ }
1447
+ });
1448
+ } else {
1449
+ const policy = await registry.findOrCreatePolicy(type);
1450
+ if (!policy) throw new HTTPException(500, {
1451
+ message: 'Failed to create policy',
1452
+ cause: {
1453
+ code: 'POLICY_CREATION_FAILED',
1454
+ type
1455
+ }
1456
+ });
1457
+ policyId = policy.id;
1458
+ }
1459
+ if (preferences) {
1460
+ const allowedCategories = effectivePolicy?.consent?.categories;
1461
+ const effectiveScopeMode = effectivePolicy?.consent?.scopeMode ?? 'permissive';
1462
+ const hasWildcardCategoryScope = allowedCategories?.includes('*') === true;
1463
+ const appliedPreferenceEntries = Object.entries(preferences);
1464
+ let filteredAppliedPreferenceEntries = appliedPreferenceEntries;
1465
+ if (allowedCategories && allowedCategories.length > 0 && !hasWildcardCategoryScope) {
1466
+ const disallowed = appliedPreferenceEntries.map(([purpose])=>purpose).filter((purpose)=>!allowedCategories.includes(purpose));
1467
+ filteredAppliedPreferenceEntries = appliedPreferenceEntries.filter(([purpose])=>allowedCategories.includes(purpose));
1468
+ if (disallowed.length > 0 && 'strict' === effectiveScopeMode) throw new HTTPException(400, {
1469
+ message: 'Preferences include categories not allowed by policy',
1470
+ cause: {
1471
+ code: 'PURPOSE_NOT_ALLOWED',
1472
+ disallowed
1473
+ }
1474
+ });
1475
+ }
1476
+ appliedPreferences = Object.fromEntries(filteredAppliedPreferenceEntries);
1477
+ const filteredConsentedPurposeCodes = filteredAppliedPreferenceEntries.filter(([_, isConsented])=>isConsented).map(([purposeCode])=>purposeCode);
1478
+ logger.debug('Consented purposes', {
1479
+ consentedPurposes: filteredConsentedPurposeCodes
1480
+ });
1481
+ const purposesRaw = await Promise.all(filteredConsentedPurposeCodes.map((purposeCode)=>registry.findOrCreateConsentPurposeByCode(purposeCode)));
1482
+ const purposes = purposesRaw.map((purpose)=>purpose?.id ?? null).filter((id)=>Boolean(id));
1483
+ logger.debug('Filtered purposes', {
1484
+ purposes
1485
+ });
1486
+ if (0 === purposes.length) logger.warn('No valid purpose IDs found after filtering. Using empty list.', {
1487
+ consentedPurposes: filteredConsentedPurposeCodes
1488
+ });
1489
+ purposeIds = purposes;
1490
+ }
1491
+ if (!policyId) throw new HTTPException(500, {
1492
+ message: 'Failed to resolve policy',
1493
+ cause: {
1494
+ code: 'POLICY_RESOLUTION_FAILED',
1495
+ type
1496
+ }
1497
+ });
1498
+ const expiryDays = effectivePolicy?.consent?.expiryDays;
1499
+ const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
1500
+ const proofConfig = effectivePolicy?.proof;
1501
+ const shouldStoreIp = proofConfig?.storeIp ?? true;
1502
+ const shouldStoreUserAgent = proofConfig?.storeUserAgent ?? true;
1503
+ const shouldStoreLanguage = proofConfig?.storeLanguage ?? false;
1504
+ const effectiveLanguage = (snapshotPayload?.language && hasValidSnapshot ? snapshotPayload.language : requestLanguage) ?? void 0;
1505
+ const metadataWithPolicy = {
1506
+ ...metadata ?? {},
1507
+ ...shouldStoreLanguage && effectiveLanguage ? {
1508
+ policyLanguage: effectiveLanguage
1509
+ } : {}
1510
+ };
1511
+ const effectiveJurisdiction = hasValidSnapshot && snapshotPayload ? snapshotPayload.jurisdiction : resolvedJurisdiction;
1512
+ const decisionPayload = buildDecisionPayload({
1513
+ tenantId: ctx.tenantId,
1514
+ snapshot: hasValidSnapshot && snapshotPayload ? {
1515
+ valid: true,
1516
+ payload: snapshotPayload
1517
+ } : null,
1518
+ decision: resolvedPolicyDecision,
1519
+ location: {
1520
+ countryCode: location.countryCode,
1521
+ regionCode: location.regionCode
1522
+ },
1523
+ jurisdiction: resolvedJurisdiction,
1524
+ language: effectiveLanguage,
1525
+ proofConfig
1526
+ });
1527
+ const existingConsent = await db.findFirst('consent', {
1528
+ where: (b)=>b.and(b('subjectId', '=', subject.id), b('domainId', '=', domainRecord.id), b('policyId', '=', policyId), b('givenAt', '=', givenAt))
1529
+ });
1530
+ if (existingConsent) {
1531
+ logger.debug('Duplicate consent detected, returning existing record', {
1532
+ consentId: existingConsent.id
1533
+ });
1534
+ return c.json({
1535
+ subjectId: subject.id,
1536
+ consentId: existingConsent.id,
1537
+ domainId: domainRecord.id,
1538
+ domain: domainRecord.name,
1539
+ type,
1540
+ metadata,
1541
+ appliedPreferences,
1542
+ uiSource: input.uiSource,
1543
+ givenAt: existingConsent.givenAt
1544
+ });
1545
+ }
1546
+ const result = await db.transaction(async (tx)=>{
1547
+ logger.debug('Creating consent record', {
1548
+ subjectId: subject.id,
1549
+ domainId: domainRecord.id,
1550
+ policyId,
1551
+ purposeIds
1552
+ });
1553
+ const runtimePolicyDecision = decisionPayload ? await tx.findFirst('runtimePolicyDecision', {
1554
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1555
+ }) ?? await tx.create('runtimePolicyDecision', {
1556
+ id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
1557
+ tenantId: decisionPayload.tenantId,
1558
+ policyId: decisionPayload.policyId,
1559
+ fingerprint: decisionPayload.fingerprint,
1560
+ matchedBy: decisionPayload.matchedBy,
1561
+ countryCode: decisionPayload.countryCode,
1562
+ regionCode: decisionPayload.regionCode,
1563
+ jurisdiction: decisionPayload.jurisdiction,
1564
+ language: decisionPayload.language,
1565
+ model: decisionPayload.model,
1566
+ policyI18n: decisionPayload.policyI18n ? {
1567
+ json: decisionPayload.policyI18n
1568
+ } : void 0,
1569
+ uiMode: decisionPayload.uiMode,
1570
+ bannerUi: decisionPayload.bannerUi ? {
1571
+ json: decisionPayload.bannerUi
1572
+ } : void 0,
1573
+ dialogUi: decisionPayload.dialogUi ? {
1574
+ json: decisionPayload.dialogUi
1575
+ } : void 0,
1576
+ categories: decisionPayload.categories ? {
1577
+ json: decisionPayload.categories
1578
+ } : void 0,
1579
+ preselectedCategories: decisionPayload.preselectedCategories ? {
1580
+ json: decisionPayload.preselectedCategories
1581
+ } : void 0,
1582
+ proofConfig: decisionPayload.proofConfig ? {
1583
+ json: decisionPayload.proofConfig
1584
+ } : void 0,
1585
+ dedupeKey: decisionPayload.dedupeKey
1586
+ }).catch(async ()=>tx.findFirst('runtimePolicyDecision', {
1587
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1588
+ })) : void 0;
1589
+ const consentRecord = await tx.create('consent', {
1590
+ id: await utils_generateUniqueId(tx, 'consent', ctx),
1591
+ subjectId: subject.id,
1592
+ domainId: domainRecord.id,
1593
+ policyId,
1594
+ purposeIds: {
1595
+ json: purposeIds
1596
+ },
1597
+ metadata: Object.keys(metadataWithPolicy).length > 0 ? {
1598
+ json: metadataWithPolicy
1599
+ } : void 0,
1600
+ ipAddress: shouldStoreIp ? ctx.ipAddress : null,
1601
+ userAgent: shouldStoreUserAgent ? ctx.userAgent : null,
1602
+ jurisdiction: effectiveJurisdiction,
1603
+ jurisdictionModel: effectiveModel,
1604
+ tcString: input.tcString,
1605
+ uiSource: input.uiSource,
1606
+ consentAction: derivedConsentAction,
1607
+ givenAt,
1608
+ validUntil,
1609
+ runtimePolicyDecisionId: runtimePolicyDecision?.id,
1610
+ runtimePolicySource: decisionPayload?.source
1611
+ });
1612
+ logger.debug('Created consent', {
1613
+ consentRecord: consentRecord.id
1614
+ });
1615
+ if (!consentRecord) throw new HTTPException(500, {
1616
+ message: 'Failed to create consent',
1617
+ cause: {
1618
+ code: 'CONSENT_CREATION_FAILED',
1619
+ subjectId: subject.id,
1620
+ domain
1621
+ }
1622
+ });
1623
+ return {
1624
+ consent: consentRecord
1625
+ };
1626
+ });
1627
+ const metrics = getMetrics();
1628
+ if (metrics) {
1629
+ const jurisdiction = effectiveJurisdiction;
1630
+ metrics.recordConsentCreated({
1631
+ type,
1632
+ jurisdiction
1633
+ });
1634
+ const hasAccepted = preferences && Object.values(preferences).some(Boolean);
1635
+ if (hasAccepted) metrics.recordConsentAccepted({
1636
+ type,
1637
+ jurisdiction
1638
+ });
1639
+ else metrics.recordConsentRejected({
1640
+ type,
1641
+ jurisdiction
1642
+ });
1643
+ }
1644
+ return c.json({
1645
+ subjectId: subject.id,
1646
+ consentId: result.consent.id,
1647
+ domainId: domainRecord.id,
1648
+ domain: domainRecord.name,
1649
+ type,
1650
+ metadata,
1651
+ appliedPreferences,
1652
+ uiSource: input.uiSource,
1653
+ givenAt: result.consent.givenAt
1654
+ });
1655
+ } catch (error) {
1656
+ logger.error('Error in POST /subjects handler', {
1657
+ error: extractErrorMessage(error),
1658
+ errorType: error instanceof Error ? error.constructor.name : typeof error
1659
+ });
1660
+ if (error instanceof HTTPException) throw error;
1661
+ if (error instanceof LegalDocumentPolicyConflictError) throw new HTTPException(409, {
1662
+ message: error.message,
1663
+ cause: {
1664
+ code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
1665
+ }
1666
+ });
1667
+ throw new HTTPException(500, {
1668
+ message: 'Internal server error',
1669
+ cause: {
1670
+ code: 'INTERNAL_SERVER_ERROR'
1671
+ }
1672
+ });
1673
+ }
1674
+ };
1675
+ const createSubjectRoutes = ()=>{
1676
+ const app = new Hono();
1677
+ app.get('/:id', describeRoute({
1678
+ summary: 'Get subject consent status',
1679
+ description: "Returns the subject's consent status for this device. Use to check if the subject has valid consent for given policy types.\n\n**Query:** `type` – Filter by consent type(s), comma-separated (e.g. `privacy_policy,cookie_banner`).\n\n**Response:** `subject`, `consents` (matching filter), `isValid` (valid consent for requested type(s)).",
1680
+ tags: [
1681
+ 'Subject',
1682
+ 'Consent'
1683
+ ],
1684
+ responses: {
1685
+ 200: {
1686
+ description: 'Subject and consent records for the requested type(s)',
1687
+ content: {
1688
+ 'application/json': {
1689
+ schema: resolver(getSubjectOutputSchema)
1690
+ }
1691
+ }
1692
+ },
1693
+ 404: {
1694
+ description: 'Subject not found for the given ID'
1695
+ }
1696
+ }
1697
+ }), validator('param', getSubjectInputSchema), getSubjectHandler);
1698
+ app.post('/', describeRoute({
1699
+ summary: 'Record consent for a subject',
1700
+ description: "Creates a new consent record (append-only). Creates the subject if it does not exist.\n\n**Request body by `type`:**\n- `cookie_banner` – Requires `preferences` object\n- `privacy_policy`, `dpa`, `terms_and_conditions` – Prefer a signed `documentSnapshotToken`; otherwise use a release `policyHash`, with `policyId` kept only for compatibility\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
1701
+ tags: [
1702
+ 'Subject',
1703
+ 'Consent'
1704
+ ],
1705
+ responses: {
1706
+ 200: {
1707
+ description: 'Consent recorded; subject and consent in response',
1708
+ content: {
1709
+ 'application/json': {
1710
+ schema: resolver(postSubjectOutputSchema)
1711
+ }
1712
+ }
1713
+ },
1714
+ 422: {
1715
+ description: 'Invalid request body (schema or validation failed)'
1716
+ }
1717
+ }
1718
+ }), validator('json', postSubjectInputSchema), postSubjectHandler);
1719
+ app.patch('/:id', describeRoute({
1720
+ summary: 'Link external ID to subject',
1721
+ description: 'Associates an external user ID with an existing subject (e.g. after login). Enables cross-device consent sync.',
1722
+ tags: [
1723
+ 'Subject'
1724
+ ],
1725
+ responses: {
1726
+ 200: {
1727
+ description: 'Subject updated with external ID',
1728
+ content: {
1729
+ 'application/json': {
1730
+ schema: resolver(patchSubjectOutputSchema)
1731
+ }
1732
+ }
1733
+ },
1734
+ 404: {
1735
+ description: 'Subject not found for the given ID'
1736
+ }
1737
+ }
1738
+ }), validator('param', __rspack_external_valibot.object({
1739
+ id: subjectIdSchema
1740
+ })), validator('json', __rspack_external_valibot.object({
1741
+ externalId: __rspack_external_valibot.string(),
1742
+ identityProvider: __rspack_external_valibot.optional(__rspack_external_valibot.string())
1743
+ })), patchSubjectHandler);
1744
+ app.get('/', describeRoute({
1745
+ summary: 'List subjects by external ID (API key required)',
1746
+ description: 'Returns all subjects linked to the given external ID. Requires Bearer token (API key). Use for server-side consent lookups.',
1747
+ tags: [
1748
+ 'Subject'
1749
+ ],
1750
+ security: [
1751
+ {
1752
+ bearerAuth: []
1753
+ }
1754
+ ],
1755
+ responses: {
1756
+ 200: {
1757
+ description: 'List of subjects for the external ID',
1758
+ content: {
1759
+ 'application/json': {
1760
+ schema: resolver(listSubjectsOutputSchema)
1761
+ }
1762
+ }
1763
+ },
1764
+ 401: {
1765
+ description: 'Missing or invalid API key'
1766
+ }
1767
+ }
1768
+ }), validator('query', listSubjectsQuerySchema), listSubjectsHandler);
1769
+ return app;
1770
+ };
1771
+ export { createConsentRoutes, createInitRoute, createLegalDocumentRoutes, createStatusRoute, createSubjectRoutes, generateUniqueId, policyRegistry };