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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/dist/302.js +473 -0
  2. package/dist/364.js +1140 -0
  3. package/dist/583.js +540 -0
  4. package/dist/cache.cjs +1 -1
  5. package/dist/cache.js +4 -415
  6. package/dist/core.cjs +849 -96
  7. package/dist/core.js +147 -1817
  8. package/dist/db/adapters/drizzle.cjs +1 -1
  9. package/dist/db/adapters/drizzle.js +1 -2
  10. package/dist/db/adapters/kysely.cjs +1 -1
  11. package/dist/db/adapters/kysely.js +1 -2
  12. package/dist/db/adapters/mongo.cjs +1 -1
  13. package/dist/db/adapters/mongo.js +1 -2
  14. package/dist/db/adapters/prisma.cjs +1 -1
  15. package/dist/db/adapters/prisma.js +1 -2
  16. package/dist/db/adapters/typeorm.cjs +1 -1
  17. package/dist/db/adapters/typeorm.js +1 -2
  18. package/dist/db/adapters.cjs +1 -1
  19. package/dist/db/migrator.cjs +1 -1
  20. package/dist/db/schema.cjs +38 -1
  21. package/dist/db/schema.js +33 -2
  22. package/dist/define-config.cjs +1 -1
  23. package/dist/edge.cjs +1106 -0
  24. package/dist/edge.js +190 -0
  25. package/dist/router.cjs +629 -81
  26. package/dist/router.js +1 -1509
  27. package/dist/types/index.cjs +1 -1
  28. package/{dist → dist-types}/cache/adapters/cloudflare-kv.d.ts +0 -1
  29. package/{dist → dist-types}/cache/adapters/index.d.ts +0 -1
  30. package/{dist → dist-types}/cache/adapters/memory.d.ts +0 -1
  31. package/{dist → dist-types}/cache/adapters/upstash-redis.d.ts +0 -1
  32. package/{dist → dist-types}/cache/gvl-resolver.d.ts +1 -2
  33. package/{dist → dist-types}/cache/index.d.ts +0 -1
  34. package/{dist → dist-types}/cache/keys.d.ts +0 -1
  35. package/{dist → dist-types}/cache/types.d.ts +0 -1
  36. package/{dist → dist-types}/core.d.ts +8 -1
  37. package/{dist → dist-types}/db/migrator/index.d.ts +0 -1
  38. package/{dist → dist-types}/db/registry/consent-policy.d.ts +0 -1
  39. package/{dist → dist-types}/db/registry/consent-purpose.d.ts +0 -1
  40. package/{dist → dist-types}/db/registry/domain.d.ts +0 -1
  41. package/{dist → dist-types}/db/registry/index.d.ts +22 -2
  42. package/dist-types/db/registry/runtime-policy-decision.d.ts +60 -0
  43. package/{dist → dist-types}/db/registry/subject.d.ts +0 -1
  44. package/{dist → dist-types}/db/registry/types.d.ts +1 -2
  45. package/{dist → dist-types}/db/registry/utils/generate-id.d.ts +0 -1
  46. package/{dist → dist-types}/db/registry/utils.d.ts +0 -1
  47. package/{dist → dist-types}/db/schema/1.0.0/audit-log.d.ts +0 -1
  48. package/{dist → dist-types}/db/schema/1.0.0/consent-policy.d.ts +0 -1
  49. package/{dist → dist-types}/db/schema/1.0.0/consent-purpose.d.ts +0 -1
  50. package/{dist → dist-types}/db/schema/1.0.0/consent-record.d.ts +0 -1
  51. package/{dist → dist-types}/db/schema/1.0.0/consent.d.ts +2 -3
  52. package/{dist → dist-types}/db/schema/1.0.0/domain.d.ts +0 -1
  53. package/{dist → dist-types}/db/schema/1.0.0/index.d.ts +0 -1
  54. package/{dist → dist-types}/db/schema/1.0.0/subject.d.ts +0 -1
  55. package/{dist → dist-types}/db/schema/2.0.0/audit-log.d.ts +2 -3
  56. package/{dist → dist-types}/db/schema/2.0.0/consent-policy.d.ts +2 -3
  57. package/{dist → dist-types}/db/schema/2.0.0/consent-purpose.d.ts +2 -3
  58. package/{dist → dist-types}/db/schema/2.0.0/consent.d.ts +6 -3
  59. package/{dist → dist-types}/db/schema/2.0.0/domain.d.ts +2 -3
  60. package/{dist → dist-types}/db/schema/2.0.0/index.d.ts +432 -17
  61. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +23 -0
  62. package/{dist → dist-types}/db/schema/2.0.0/subject.d.ts +2 -3
  63. package/{dist → dist-types}/db/schema/index.d.ts +862 -33
  64. package/{dist → dist-types}/db/tenant-scope.d.ts +0 -1
  65. package/{dist → dist-types}/define-config.d.ts +0 -1
  66. package/dist-types/edge/index.d.ts +5 -0
  67. package/dist-types/edge/init-handler.d.ts +38 -0
  68. package/dist-types/edge/resolve-consent.d.ts +80 -0
  69. package/dist-types/edge/types.d.ts +13 -0
  70. package/{dist → dist-types}/handlers/consent/check.handler.d.ts +0 -1
  71. package/{src/handlers/consent/index.ts → dist-types/handlers/consent/index.d.ts} +0 -1
  72. package/{dist → dist-types}/handlers/init/geo.d.ts +2 -3
  73. package/{dist → dist-types}/handlers/init/index.d.ts +4 -5
  74. package/dist-types/handlers/init/policy.d.ts +26 -0
  75. package/dist-types/handlers/init/resolve-init.d.ts +44 -0
  76. package/dist-types/handlers/init/translations.d.ts +48 -0
  77. package/dist-types/handlers/policy/snapshot.d.ts +99 -0
  78. package/{src/handlers/status/index.ts → dist-types/handlers/status/index.d.ts} +0 -1
  79. package/{dist → dist-types}/handlers/status/status.handler.d.ts +0 -1
  80. package/{dist → dist-types}/handlers/subject/get.handler.d.ts +0 -1
  81. package/{src/handlers/subject/index.ts → dist-types/handlers/subject/index.d.ts} +0 -1
  82. package/{dist → dist-types}/handlers/subject/list.handler.d.ts +0 -1
  83. package/{dist → dist-types}/handlers/subject/patch.handler.d.ts +0 -1
  84. package/{dist → dist-types}/handlers/subject/post.handler.d.ts +12 -1
  85. package/{dist → dist-types}/handlers/utils/consent-enrichment.d.ts +0 -1
  86. package/{dist → dist-types}/init.d.ts +0 -1
  87. package/{dist → dist-types}/middleware/auth/index.d.ts +0 -1
  88. package/{dist → dist-types}/middleware/auth/validate-api-key.d.ts +0 -1
  89. package/{dist → dist-types}/middleware/cors/cors.d.ts +0 -1
  90. package/{src/middleware/cors/index.ts → dist-types/middleware/cors/index.d.ts} +0 -1
  91. package/{dist → dist-types}/middleware/cors/is-origin-trusted.d.ts +1 -2
  92. package/{dist → dist-types}/middleware/cors/process-cors.d.ts +0 -1
  93. package/{dist → dist-types}/middleware/openapi/config.d.ts +0 -1
  94. package/{dist → dist-types}/middleware/openapi/handlers.d.ts +0 -1
  95. package/{src/middleware/openapi/index.ts → dist-types/middleware/openapi/index.d.ts} +0 -1
  96. package/{dist → dist-types}/middleware/process-ip/index.d.ts +0 -1
  97. package/dist-types/policies/builder.d.ts +127 -0
  98. package/dist-types/policies/defaults.d.ts +2 -0
  99. package/dist-types/policies/matchers.d.ts +3 -0
  100. package/{dist → dist-types}/router.d.ts +0 -1
  101. package/{dist → dist-types}/routes/consent.d.ts +0 -1
  102. package/{src/routes/index.ts → dist-types/routes/index.d.ts} +0 -1
  103. package/{dist → dist-types}/routes/init.d.ts +0 -1
  104. package/{dist → dist-types}/routes/status.d.ts +0 -1
  105. package/{dist → dist-types}/routes/subject.d.ts +0 -1
  106. package/{dist → dist-types}/types/api.d.ts +0 -1
  107. package/{dist → dist-types}/types/index.d.ts +110 -6
  108. package/dist-types/utils/background.d.ts +6 -0
  109. package/{dist → dist-types}/utils/create-telemetry-options.d.ts +0 -1
  110. package/{dist → dist-types}/utils/env.d.ts +0 -1
  111. package/{dist → dist-types}/utils/extract-error-message.d.ts +0 -1
  112. package/{dist → dist-types}/utils/instrumentation.d.ts +0 -1
  113. package/{dist → dist-types}/utils/logger.d.ts +1 -2
  114. package/{dist → dist-types}/utils/metrics.d.ts +0 -1
  115. package/dist-types/version.d.ts +1 -0
  116. package/docs/README.md +49 -0
  117. package/docs/api/configuration.md +197 -0
  118. package/docs/api/endpoints.md +211 -0
  119. package/docs/guides/caching.md +85 -0
  120. package/docs/guides/database-setup.md +128 -0
  121. package/docs/guides/edge-deployment.md +248 -0
  122. package/docs/guides/framework-integration.md +142 -0
  123. package/docs/guides/iab-tcf.md +89 -0
  124. package/docs/guides/observability.md +96 -0
  125. package/docs/guides/policy-packs.md +396 -0
  126. package/docs/quickstart.md +129 -0
  127. package/package.json +45 -31
  128. package/.turbo/turbo-build.log +0 -49
  129. package/CHANGELOG.md +0 -123
  130. package/dist/cache/adapters/cloudflare-kv.d.ts.map +0 -1
  131. package/dist/cache/adapters/index.d.ts.map +0 -1
  132. package/dist/cache/adapters/memory.d.ts.map +0 -1
  133. package/dist/cache/adapters/upstash-redis.d.ts.map +0 -1
  134. package/dist/cache/gvl-resolver.d.ts.map +0 -1
  135. package/dist/cache/index.d.ts.map +0 -1
  136. package/dist/cache/keys.d.ts.map +0 -1
  137. package/dist/cache/types.d.ts.map +0 -1
  138. package/dist/core.d.ts.map +0 -1
  139. package/dist/db/adapters/drizzle.d.ts +0 -2
  140. package/dist/db/adapters/drizzle.d.ts.map +0 -1
  141. package/dist/db/adapters/index.d.ts +0 -2
  142. package/dist/db/adapters/index.d.ts.map +0 -1
  143. package/dist/db/adapters/kysely.d.ts +0 -2
  144. package/dist/db/adapters/kysely.d.ts.map +0 -1
  145. package/dist/db/adapters/mongo.d.ts +0 -2
  146. package/dist/db/adapters/mongo.d.ts.map +0 -1
  147. package/dist/db/adapters/prisma.d.ts +0 -2
  148. package/dist/db/adapters/prisma.d.ts.map +0 -1
  149. package/dist/db/adapters/typeorm.d.ts +0 -2
  150. package/dist/db/adapters/typeorm.d.ts.map +0 -1
  151. package/dist/db/migrator/index.d.ts.map +0 -1
  152. package/dist/db/registry/consent-policy.d.ts.map +0 -1
  153. package/dist/db/registry/consent-purpose.d.ts.map +0 -1
  154. package/dist/db/registry/domain.d.ts.map +0 -1
  155. package/dist/db/registry/index.d.ts.map +0 -1
  156. package/dist/db/registry/subject.d.ts.map +0 -1
  157. package/dist/db/registry/types.d.ts.map +0 -1
  158. package/dist/db/registry/utils/generate-id.d.ts.map +0 -1
  159. package/dist/db/registry/utils.d.ts.map +0 -1
  160. package/dist/db/schema/1.0.0/audit-log.d.ts.map +0 -1
  161. package/dist/db/schema/1.0.0/consent-policy.d.ts.map +0 -1
  162. package/dist/db/schema/1.0.0/consent-purpose.d.ts.map +0 -1
  163. package/dist/db/schema/1.0.0/consent-record.d.ts.map +0 -1
  164. package/dist/db/schema/1.0.0/consent.d.ts.map +0 -1
  165. package/dist/db/schema/1.0.0/domain.d.ts.map +0 -1
  166. package/dist/db/schema/1.0.0/index.d.ts.map +0 -1
  167. package/dist/db/schema/1.0.0/subject.d.ts.map +0 -1
  168. package/dist/db/schema/2.0.0/audit-log.d.ts.map +0 -1
  169. package/dist/db/schema/2.0.0/consent-policy.d.ts.map +0 -1
  170. package/dist/db/schema/2.0.0/consent-purpose.d.ts.map +0 -1
  171. package/dist/db/schema/2.0.0/consent.d.ts.map +0 -1
  172. package/dist/db/schema/2.0.0/domain.d.ts.map +0 -1
  173. package/dist/db/schema/2.0.0/index.d.ts.map +0 -1
  174. package/dist/db/schema/2.0.0/subject.d.ts.map +0 -1
  175. package/dist/db/schema/index.d.ts.map +0 -1
  176. package/dist/db/tenant-scope.d.ts.map +0 -1
  177. package/dist/define-config.d.ts.map +0 -1
  178. package/dist/handlers/consent/check.handler.d.ts.map +0 -1
  179. package/dist/handlers/consent/index.d.ts +0 -12
  180. package/dist/handlers/consent/index.d.ts.map +0 -1
  181. package/dist/handlers/init/geo.d.ts.map +0 -1
  182. package/dist/handlers/init/index.d.ts.map +0 -1
  183. package/dist/handlers/init/translations.d.ts +0 -26
  184. package/dist/handlers/init/translations.d.ts.map +0 -1
  185. package/dist/handlers/status/index.d.ts +0 -7
  186. package/dist/handlers/status/index.d.ts.map +0 -1
  187. package/dist/handlers/status/status.handler.d.ts.map +0 -1
  188. package/dist/handlers/subject/get.handler.d.ts.map +0 -1
  189. package/dist/handlers/subject/index.d.ts +0 -10
  190. package/dist/handlers/subject/index.d.ts.map +0 -1
  191. package/dist/handlers/subject/list.handler.d.ts.map +0 -1
  192. package/dist/handlers/subject/patch.handler.d.ts.map +0 -1
  193. package/dist/handlers/subject/post.handler.d.ts.map +0 -1
  194. package/dist/handlers/utils/consent-enrichment.d.ts.map +0 -1
  195. package/dist/init.d.ts.map +0 -1
  196. package/dist/middleware/auth/index.d.ts.map +0 -1
  197. package/dist/middleware/auth/validate-api-key.d.ts.map +0 -1
  198. package/dist/middleware/cors/cors.d.ts.map +0 -1
  199. package/dist/middleware/cors/index.d.ts +0 -30
  200. package/dist/middleware/cors/index.d.ts.map +0 -1
  201. package/dist/middleware/cors/is-origin-trusted.d.ts.map +0 -1
  202. package/dist/middleware/cors/process-cors.d.ts.map +0 -1
  203. package/dist/middleware/openapi/config.d.ts.map +0 -1
  204. package/dist/middleware/openapi/handlers.d.ts.map +0 -1
  205. package/dist/middleware/openapi/index.d.ts +0 -12
  206. package/dist/middleware/openapi/index.d.ts.map +0 -1
  207. package/dist/middleware/process-ip/index.d.ts.map +0 -1
  208. package/dist/router.d.ts.map +0 -1
  209. package/dist/routes/consent.d.ts.map +0 -1
  210. package/dist/routes/index.d.ts +0 -10
  211. package/dist/routes/index.d.ts.map +0 -1
  212. package/dist/routes/init.d.ts.map +0 -1
  213. package/dist/routes/status.d.ts.map +0 -1
  214. package/dist/routes/subject.d.ts.map +0 -1
  215. package/dist/types/api.d.ts.map +0 -1
  216. package/dist/types/index.d.ts.map +0 -1
  217. package/dist/utils/create-telemetry-options.d.ts.map +0 -1
  218. package/dist/utils/env.d.ts.map +0 -1
  219. package/dist/utils/extract-error-message.d.ts.map +0 -1
  220. package/dist/utils/index.d.ts +0 -4
  221. package/dist/utils/index.d.ts.map +0 -1
  222. package/dist/utils/instrumentation.d.ts.map +0 -1
  223. package/dist/utils/logger.d.ts.map +0 -1
  224. package/dist/utils/metrics.d.ts.map +0 -1
  225. package/dist/version.d.ts +0 -2
  226. package/dist/version.d.ts.map +0 -1
  227. package/knip.json +0 -31
  228. package/rslib.config.ts +0 -93
  229. package/src/cache/adapters/cloudflare-kv.ts +0 -71
  230. package/src/cache/adapters/index.ts +0 -22
  231. package/src/cache/adapters/memory.ts +0 -111
  232. package/src/cache/adapters/upstash-redis.ts +0 -113
  233. package/src/cache/gvl-resolver.ts +0 -289
  234. package/src/cache/index.ts +0 -34
  235. package/src/cache/keys.ts +0 -68
  236. package/src/cache/types.ts +0 -66
  237. package/src/core.ts +0 -369
  238. package/src/db/migrator/index.ts +0 -80
  239. package/src/db/registry/consent-policy.test.ts +0 -451
  240. package/src/db/registry/consent-policy.ts +0 -82
  241. package/src/db/registry/consent-purpose.test.ts +0 -428
  242. package/src/db/registry/consent-purpose.ts +0 -61
  243. package/src/db/registry/domain.test.ts +0 -445
  244. package/src/db/registry/domain.ts +0 -91
  245. package/src/db/registry/index.ts +0 -14
  246. package/src/db/registry/subject.test.ts +0 -371
  247. package/src/db/registry/subject.ts +0 -126
  248. package/src/db/registry/types.ts +0 -10
  249. package/src/db/registry/utils/generate-id.test.ts +0 -216
  250. package/src/db/registry/utils/generate-id.ts +0 -133
  251. package/src/db/registry/utils.ts +0 -133
  252. package/src/db/schema/1.0.0/audit-log.ts +0 -15
  253. package/src/db/schema/1.0.0/consent-policy.ts +0 -14
  254. package/src/db/schema/1.0.0/consent-purpose.ts +0 -14
  255. package/src/db/schema/1.0.0/consent-record.ts +0 -10
  256. package/src/db/schema/1.0.0/consent.ts +0 -20
  257. package/src/db/schema/1.0.0/domain.ts +0 -12
  258. package/src/db/schema/1.0.0/index.ts +0 -48
  259. package/src/db/schema/1.0.0/subject.ts +0 -11
  260. package/src/db/schema/2.0.0/audit-log.ts +0 -18
  261. package/src/db/schema/2.0.0/consent-policy.ts +0 -28
  262. package/src/db/schema/2.0.0/consent-purpose.ts +0 -12
  263. package/src/db/schema/2.0.0/consent.ts +0 -28
  264. package/src/db/schema/2.0.0/domain.ts +0 -12
  265. package/src/db/schema/2.0.0/index.ts +0 -47
  266. package/src/db/schema/2.0.0/subject.ts +0 -13
  267. package/src/db/schema/index.ts +0 -15
  268. package/src/db/tenant-scope.test.ts +0 -747
  269. package/src/db/tenant-scope.ts +0 -103
  270. package/src/define-config.ts +0 -19
  271. package/src/handlers/consent/check.handler.ts +0 -126
  272. package/src/handlers/init/geo.test.ts +0 -317
  273. package/src/handlers/init/geo.ts +0 -195
  274. package/src/handlers/init/index.test.ts +0 -205
  275. package/src/handlers/init/index.ts +0 -114
  276. package/src/handlers/init/translations.test.ts +0 -121
  277. package/src/handlers/init/translations.ts +0 -69
  278. package/src/handlers/status/status.handler.test.ts +0 -155
  279. package/src/handlers/status/status.handler.ts +0 -51
  280. package/src/handlers/subject/get.handler.ts +0 -92
  281. package/src/handlers/subject/list.handler.ts +0 -92
  282. package/src/handlers/subject/patch.handler.ts +0 -119
  283. package/src/handlers/subject/post.handler.test.ts +0 -294
  284. package/src/handlers/subject/post.handler.ts +0 -268
  285. package/src/handlers/utils/consent-enrichment.test.ts +0 -380
  286. package/src/handlers/utils/consent-enrichment.ts +0 -218
  287. package/src/init.test.ts +0 -122
  288. package/src/init.ts +0 -88
  289. package/src/middleware/auth/index.ts +0 -11
  290. package/src/middleware/auth/validate-api-key.test.ts +0 -86
  291. package/src/middleware/auth/validate-api-key.ts +0 -107
  292. package/src/middleware/cors/cors.test.ts +0 -135
  293. package/src/middleware/cors/cors.ts +0 -186
  294. package/src/middleware/cors/is-origin-trusted.test.ts +0 -164
  295. package/src/middleware/cors/is-origin-trusted.ts +0 -130
  296. package/src/middleware/cors/process-cors.ts +0 -91
  297. package/src/middleware/openapi/config.ts +0 -29
  298. package/src/middleware/openapi/handlers.ts +0 -34
  299. package/src/middleware/process-ip/index.test.ts +0 -193
  300. package/src/middleware/process-ip/index.ts +0 -199
  301. package/src/router.ts +0 -15
  302. package/src/routes/consent.ts +0 -52
  303. package/src/routes/init.ts +0 -105
  304. package/src/routes/status.ts +0 -46
  305. package/src/routes/subject.ts +0 -152
  306. package/src/types/api.ts +0 -48
  307. package/src/types/index.ts +0 -391
  308. package/src/utils/create-telemetry-options.test.ts +0 -286
  309. package/src/utils/create-telemetry-options.ts +0 -229
  310. package/src/utils/env.ts +0 -84
  311. package/src/utils/extract-error-message.ts +0 -21
  312. package/src/utils/instrumentation.test.ts +0 -183
  313. package/src/utils/instrumentation.ts +0 -194
  314. package/src/utils/logger.ts +0 -41
  315. package/src/utils/metrics.test.ts +0 -311
  316. package/src/utils/metrics.ts +0 -402
  317. package/src/utils/telemetry-pii.test.ts +0 -323
  318. package/src/version.ts +0 -2
  319. package/tsconfig.json +0 -11
  320. package/vitest.config.ts +0 -28
  321. /package/{src/db/adapters/drizzle.ts → dist-types/db/adapters/drizzle.d.ts} +0 -0
  322. /package/{src/db/adapters/index.ts → dist-types/db/adapters/index.d.ts} +0 -0
  323. /package/{src/db/adapters/kysely.ts → dist-types/db/adapters/kysely.d.ts} +0 -0
  324. /package/{src/db/adapters/mongo.ts → dist-types/db/adapters/mongo.d.ts} +0 -0
  325. /package/{src/db/adapters/prisma.ts → dist-types/db/adapters/prisma.d.ts} +0 -0
  326. /package/{src/db/adapters/typeorm.ts → dist-types/db/adapters/typeorm.d.ts} +0 -0
  327. /package/{src/utils/index.ts → dist-types/utils/index.d.ts} +0 -0
package/dist/364.js ADDED
@@ -0,0 +1,1140 @@
1
+ import { checkConsentOutputSchema, checkConsentQuerySchema, getSubjectInputSchema, getSubjectOutputSchema, initOutputSchema, listSubjectsOutputSchema, listSubjectsQuerySchema, patchSubjectOutputSchema, postSubjectInputSchema, postSubjectOutputSchema, statusOutputSchema, subjectIdSchema } from "@c15t/schema";
2
+ import { Hono } from "hono";
3
+ import { describeRoute, resolver, validator } from "hono-openapi";
4
+ import { HTTPException } from "hono/http-exception";
5
+ import base_x from "base-x";
6
+ import { version, getMetrics, extractErrorMessage } from "./302.js";
7
+ import { getLocation, resolveInitPayload, policy_resolvePolicyDecision, verifyPolicySnapshotToken, getJurisdiction } from "./583.js";
8
+ import * as __rspack_external_valibot from "valibot";
9
+ function parsePurposeIds(purposeIds) {
10
+ if (null == purposeIds) return [];
11
+ const ids = 'object' == typeof purposeIds && 'json' in purposeIds ? purposeIds.json : purposeIds;
12
+ return Array.isArray(ids) ? ids : [];
13
+ }
14
+ async function batchLoadPolicies(policyIds, ctx) {
15
+ const { db, registry } = ctx;
16
+ const policyMap = new Map();
17
+ if (policyIds.size > 0) {
18
+ const policies = await db.findMany('consentPolicy', {
19
+ where: (b)=>b('id', 'in', [
20
+ ...policyIds
21
+ ])
22
+ });
23
+ for (const p of policies)policyMap.set(p.id, p);
24
+ }
25
+ const uniqueTypes = new Set();
26
+ for (const p of policyMap.values())uniqueTypes.add(p.type);
27
+ const latestPolicyByType = new Map();
28
+ for (const type of uniqueTypes){
29
+ const latest = await registry.findOrCreatePolicy(type);
30
+ if (latest) latestPolicyByType.set(type, latest.id);
31
+ }
32
+ return {
33
+ policyMap,
34
+ latestPolicyByType
35
+ };
36
+ }
37
+ async function enrichConsents(consents, ctx) {
38
+ if (0 === consents.length) return [];
39
+ const policyIds = new Set();
40
+ for (const c of consents)if (c.policyId) policyIds.add(c.policyId);
41
+ const { policyMap, latestPolicyByType } = await batchLoadPolicies(policyIds, ctx);
42
+ const allPurposeIds = new Set();
43
+ for (const c of consents)for (const id of parsePurposeIds(c.purposeIds))allPurposeIds.add(id);
44
+ const purposeMap = new Map();
45
+ if (allPurposeIds.size > 0) {
46
+ const purposes = await ctx.db.findMany('consentPurpose', {
47
+ where: (b)=>b('id', 'in', [
48
+ ...allPurposeIds
49
+ ])
50
+ });
51
+ for (const p of purposes)purposeMap.set(p.id, p.code);
52
+ }
53
+ return consents.map((consent)=>{
54
+ let policyType = 'unknown';
55
+ let isLatestPolicy = false;
56
+ if (consent.policyId) {
57
+ const policy = policyMap.get(consent.policyId);
58
+ if (policy) {
59
+ policyType = policy.type;
60
+ isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
61
+ }
62
+ }
63
+ let preferences;
64
+ const ids = parsePurposeIds(consent.purposeIds);
65
+ if (ids.length > 0) {
66
+ preferences = {};
67
+ for (const purposeId of ids){
68
+ const code = purposeMap.get(purposeId);
69
+ if (code) preferences[code] = true;
70
+ }
71
+ }
72
+ return {
73
+ id: consent.id,
74
+ type: policyType,
75
+ policyId: consent.policyId ?? void 0,
76
+ isLatestPolicy,
77
+ preferences,
78
+ givenAt: consent.givenAt
79
+ };
80
+ });
81
+ }
82
+ async function resolveConsentPolicies(consents, ctx) {
83
+ if (0 === consents.length) return [];
84
+ const policyIds = new Set();
85
+ for (const c of consents)if (c.policyId) policyIds.add(c.policyId);
86
+ const { policyMap, latestPolicyByType } = await batchLoadPolicies(policyIds, ctx);
87
+ return consents.map((consent)=>{
88
+ let policyType = 'unknown';
89
+ let isLatestPolicy = false;
90
+ if (consent.policyId) {
91
+ const policy = policyMap.get(consent.policyId);
92
+ if (policy) {
93
+ policyType = policy.type;
94
+ isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
95
+ }
96
+ }
97
+ return {
98
+ consentId: consent.id,
99
+ policyType,
100
+ policyId: consent.policyId ?? void 0,
101
+ isLatestPolicy
102
+ };
103
+ });
104
+ }
105
+ const checkConsentHandler = async (c)=>{
106
+ const ctx = c.get('c15tContext');
107
+ const logger = ctx.logger;
108
+ logger.info('Handling GET /consents/check request');
109
+ const { db, registry } = ctx;
110
+ const externalId = c.req.query('externalId');
111
+ const type = c.req.query('type');
112
+ if (!externalId) throw new HTTPException(422, {
113
+ message: 'externalId query parameter is required',
114
+ cause: {
115
+ code: 'EXTERNAL_ID_REQUIRED'
116
+ }
117
+ });
118
+ if (!type) throw new HTTPException(422, {
119
+ message: 'type query parameter is required',
120
+ cause: {
121
+ code: 'TYPE_REQUIRED'
122
+ }
123
+ });
124
+ const types = type.split(',').map((t)=>t.trim());
125
+ logger.debug('Request parameters', {
126
+ externalId,
127
+ types
128
+ });
129
+ try {
130
+ const subjects = await db.findMany('subject', {
131
+ where: (b)=>b('externalId', '=', externalId)
132
+ });
133
+ const subjectIds = subjects.map((s)=>s.id);
134
+ const results = {};
135
+ for (const t of types)results[t] = {
136
+ hasConsent: false,
137
+ isLatestPolicy: false
138
+ };
139
+ if (0 === subjectIds.length) {
140
+ logger.debug('No subjects found for externalId', {
141
+ externalId
142
+ });
143
+ return c.json({
144
+ results
145
+ });
146
+ }
147
+ const allConsents = await Promise.all(subjectIds.map((subjectId)=>db.findMany('consent', {
148
+ where: (b)=>b('subjectId', '=', subjectId)
149
+ })));
150
+ const consents = allConsents.flat();
151
+ const policyInfos = await resolveConsentPolicies(consents, {
152
+ db,
153
+ registry
154
+ });
155
+ for (const info of policyInfos){
156
+ if (!types.includes(info.policyType)) continue;
157
+ const entry = results[info.policyType];
158
+ if (entry) {
159
+ entry.hasConsent = true;
160
+ if (info.isLatestPolicy) entry.isLatestPolicy = true;
161
+ }
162
+ }
163
+ logger.debug('Consent check results', {
164
+ externalId,
165
+ results
166
+ });
167
+ const metrics = getMetrics();
168
+ if (metrics) for (const [type, result] of Object.entries(results))metrics.recordConsentCheck(type, result.hasConsent);
169
+ return c.json({
170
+ results
171
+ });
172
+ } catch (error) {
173
+ logger.error('Error in GET /consents/check handler', {
174
+ error: extractErrorMessage(error),
175
+ errorType: error instanceof Error ? error.constructor.name : typeof error
176
+ });
177
+ if (error instanceof HTTPException) throw error;
178
+ throw new HTTPException(500, {
179
+ message: 'Internal server error',
180
+ cause: {
181
+ code: 'INTERNAL_SERVER_ERROR'
182
+ }
183
+ });
184
+ }
185
+ };
186
+ const createConsentRoutes = ()=>{
187
+ const app = new Hono();
188
+ app.get('/check', describeRoute({
189
+ summary: 'Check consent by external user ID',
190
+ 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)",
191
+ tags: [
192
+ 'Consent'
193
+ ],
194
+ responses: {
195
+ 200: {
196
+ description: 'Consent check result per requested type(s)',
197
+ content: {
198
+ 'application/json': {
199
+ schema: resolver(checkConsentOutputSchema)
200
+ }
201
+ }
202
+ },
203
+ 422: {
204
+ description: 'Invalid or missing query parameters'
205
+ }
206
+ }
207
+ }), validator('query', checkConsentQuerySchema), checkConsentHandler);
208
+ return app;
209
+ };
210
+ const createInitRoute = (options)=>{
211
+ const app = new Hono();
212
+ app.get('/', describeRoute({
213
+ summary: 'Get initial consent manager state',
214
+ description: `Returns the initial state required to render the consent manager.
215
+
216
+ - **Jurisdiction** – User's jurisdiction (defaults to GDPR if geo-location is disabled)
217
+ - **Location** – User's location (null if geo-location is disabled)
218
+ - **Translations** – Consent manager copy (from \`Accept-Language\` header)
219
+ - **Branding** – Configured branding key
220
+ - **GVL** – Global Vendor List when IAB is active for the request
221
+
222
+ Use for geo-targeted consent banners and regional compliance.`,
223
+ tags: [
224
+ 'Init'
225
+ ],
226
+ responses: {
227
+ 200: {
228
+ description: 'Initialization payload (jurisdiction, location, translations, branding, GVL)',
229
+ content: {
230
+ 'application/json': {
231
+ schema: resolver(initOutputSchema)
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }), async (c)=>{
237
+ const ctx = c.get('c15tContext');
238
+ const payload = await resolveInitPayload(c.req.raw, options, ctx?.logger);
239
+ return c.json(payload);
240
+ });
241
+ return app;
242
+ };
243
+ function getHeaders(headers) {
244
+ if (!headers) return {
245
+ countryCode: null,
246
+ regionCode: null,
247
+ acceptLanguage: null
248
+ };
249
+ const normalizeHeader = (value)=>{
250
+ if (!value) return null;
251
+ return Array.isArray(value) ? value[0] ?? null : value;
252
+ };
253
+ 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'));
254
+ const regionCode = normalizeHeader(headers.get('x-c15t-region')) ?? normalizeHeader(headers.get('x-vercel-ip-country-region')) ?? normalizeHeader(headers.get('x-region-code'));
255
+ const acceptLanguage = normalizeHeader(headers.get('accept-language'));
256
+ return {
257
+ countryCode,
258
+ regionCode,
259
+ acceptLanguage
260
+ };
261
+ }
262
+ const statusHandler = async (c)=>{
263
+ const ctx = c.get('c15tContext');
264
+ const { countryCode, regionCode, acceptLanguage } = getHeaders(ctx.headers);
265
+ const clientInfo = {
266
+ ip: ctx.ipAddress ?? null,
267
+ acceptLanguage,
268
+ userAgent: ctx.userAgent ?? null,
269
+ region: {
270
+ countryCode,
271
+ regionCode
272
+ }
273
+ };
274
+ try {
275
+ await ctx.db.findFirst('subject', {});
276
+ return c.json({
277
+ version: version,
278
+ timestamp: new Date(),
279
+ client: clientInfo
280
+ });
281
+ } catch (error) {
282
+ ctx.logger.error('Database health check failed', {
283
+ error
284
+ });
285
+ throw new HTTPException(503, {
286
+ message: 'Database health check failed',
287
+ cause: {
288
+ code: 'SERVICE_UNAVAILABLE',
289
+ error
290
+ }
291
+ });
292
+ }
293
+ };
294
+ const createStatusRoute = ()=>{
295
+ const app = new Hono();
296
+ app.get('/', describeRoute({
297
+ summary: 'Health check and API status',
298
+ description: `Returns API version, timestamp, and client info (IP, region, user agent).
299
+
300
+ Use for health checks, load balancer probes, and debugging. Performs a lightweight DB check; returns 503 if the database is unreachable.`,
301
+ tags: [
302
+ 'Status'
303
+ ],
304
+ responses: {
305
+ 200: {
306
+ description: 'API is healthy (version, timestamp, client info)',
307
+ content: {
308
+ 'application/json': {
309
+ schema: resolver(statusOutputSchema)
310
+ }
311
+ }
312
+ },
313
+ 503: {
314
+ description: 'Service unavailable (e.g. database unreachable)'
315
+ }
316
+ }
317
+ }), statusHandler);
318
+ return app;
319
+ };
320
+ const getSubjectHandler = async (c)=>{
321
+ const ctx = c.get('c15tContext');
322
+ const logger = ctx.logger;
323
+ logger.info('Handling GET /subjects/:id request');
324
+ const { db, registry } = ctx;
325
+ const subjectId = c.req.param('id');
326
+ const type = c.req.query('type');
327
+ const typeFilter = type?.split(',').map((t)=>t.trim()) || [];
328
+ if (!subjectId) throw new HTTPException(400, {
329
+ message: 'Subject ID is required',
330
+ cause: {
331
+ code: 'SUBJECT_ID_REQUIRED'
332
+ }
333
+ });
334
+ logger.debug('Request parameters', {
335
+ subjectId,
336
+ typeFilter
337
+ });
338
+ try {
339
+ const subject = await db.findFirst('subject', {
340
+ where: (b)=>b('id', '=', subjectId)
341
+ });
342
+ if (!subject) throw new HTTPException(404, {
343
+ message: 'Subject not found',
344
+ cause: {
345
+ code: 'SUBJECT_NOT_FOUND',
346
+ subjectId
347
+ }
348
+ });
349
+ const consents = await db.findMany('consent', {
350
+ where: (b)=>b('subjectId', '=', subjectId)
351
+ });
352
+ const consentItems = await enrichConsents(consents, {
353
+ db,
354
+ registry
355
+ });
356
+ const filteredConsents = typeFilter.length > 0 ? consentItems.filter((consent)=>typeFilter.includes(consent.type)) : consentItems;
357
+ const isValid = 0 === typeFilter.length || typeFilter.every((t)=>filteredConsents.some((consent)=>consent.type === t && consent.isLatestPolicy));
358
+ return c.json({
359
+ subject: {
360
+ id: subject.id,
361
+ externalId: subject.externalId ?? void 0,
362
+ createdAt: subject.createdAt
363
+ },
364
+ consents: filteredConsents,
365
+ isValid
366
+ });
367
+ } catch (error) {
368
+ logger.error('Error in GET /subjects/:id handler', {
369
+ error: extractErrorMessage(error),
370
+ errorType: error instanceof Error ? error.constructor.name : typeof error
371
+ });
372
+ if (error instanceof HTTPException) throw error;
373
+ throw new HTTPException(500, {
374
+ message: 'Internal server error',
375
+ cause: {
376
+ code: 'INTERNAL_SERVER_ERROR'
377
+ }
378
+ });
379
+ }
380
+ };
381
+ const listSubjectsHandler = async (c)=>{
382
+ const ctx = c.get('c15tContext');
383
+ const logger = ctx.logger;
384
+ logger.info('Handling GET /subjects request');
385
+ const { db, registry } = ctx;
386
+ if (!ctx.apiKeyAuthenticated) throw new HTTPException(401, {
387
+ message: 'API key required. Use Authorization: Bearer <api_key>',
388
+ cause: {
389
+ code: 'UNAUTHORIZED'
390
+ }
391
+ });
392
+ const externalId = c.req.query('externalId');
393
+ if (!externalId) throw new HTTPException(422, {
394
+ message: 'externalId query parameter is required',
395
+ cause: {
396
+ code: 'EXTERNAL_ID_REQUIRED'
397
+ }
398
+ });
399
+ logger.debug('Request parameters', {
400
+ externalId
401
+ });
402
+ try {
403
+ const subjects = await db.findMany('subject', {
404
+ where: (b)=>b('externalId', '=', externalId)
405
+ });
406
+ const subjectItems = await Promise.all(subjects.map(async (subject)=>{
407
+ const consents = await db.findMany('consent', {
408
+ where: (b)=>b('subjectId', '=', subject.id)
409
+ });
410
+ const consentItems = await enrichConsents(consents, {
411
+ db,
412
+ registry
413
+ });
414
+ return {
415
+ id: subject.id,
416
+ externalId: subject.externalId ?? externalId,
417
+ createdAt: subject.createdAt,
418
+ consents: consentItems
419
+ };
420
+ }));
421
+ logger.info('Found subjects for externalId', {
422
+ externalId,
423
+ count: subjectItems.length
424
+ });
425
+ return c.json({
426
+ subjects: subjectItems
427
+ });
428
+ } catch (error) {
429
+ logger.error('Error in GET /subjects handler', {
430
+ error: extractErrorMessage(error),
431
+ errorType: error instanceof Error ? error.constructor.name : typeof error
432
+ });
433
+ if (error instanceof HTTPException) throw error;
434
+ throw new HTTPException(500, {
435
+ message: 'Internal server error',
436
+ cause: {
437
+ code: 'INTERNAL_SERVER_ERROR'
438
+ }
439
+ });
440
+ }
441
+ };
442
+ const prefixes = {
443
+ auditLog: 'log',
444
+ consent: 'cns',
445
+ consentPolicy: 'pol',
446
+ consentPurpose: 'pur',
447
+ domain: 'dom',
448
+ subject: 'sub'
449
+ };
450
+ const b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
451
+ function generateId(model) {
452
+ const buf = crypto.getRandomValues(new Uint8Array(20));
453
+ const prefix = prefixes[model];
454
+ const EPOCH_TIMESTAMP = 1700000000000;
455
+ const t = Date.now() - EPOCH_TIMESTAMP;
456
+ const high = Math.floor(t / 0x100000000);
457
+ const low = t >>> 0;
458
+ buf[0] = high >>> 24 & 255;
459
+ buf[1] = high >>> 16 & 255;
460
+ buf[2] = high >>> 8 & 255;
461
+ buf[3] = 255 & high;
462
+ buf[4] = low >>> 24 & 255;
463
+ buf[5] = low >>> 16 & 255;
464
+ buf[6] = low >>> 8 & 255;
465
+ buf[7] = 255 & low;
466
+ return `${prefix}_${b58.encode(buf)}`;
467
+ }
468
+ async function generateUniqueId(db, model, ctx, options = {}) {
469
+ const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
470
+ if (attempt >= maxRetries) {
471
+ const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
472
+ ctx?.logger?.error?.('ID generation failed', {
473
+ model,
474
+ maxRetries
475
+ });
476
+ throw error;
477
+ }
478
+ const id = generateId(model);
479
+ try {
480
+ const existing = await db.findFirst(model, {
481
+ where: (b)=>b('id', '=', id)
482
+ });
483
+ if (existing) {
484
+ ctx?.logger?.debug?.('ID conflict detected', {
485
+ id,
486
+ model,
487
+ attempt: attempt + 1,
488
+ maxRetries
489
+ });
490
+ const delay = Math.min(baseDelay * 2 ** attempt, 1000);
491
+ await new Promise((resolve)=>setTimeout(resolve, delay));
492
+ return generateUniqueId(db, model, ctx, {
493
+ maxRetries,
494
+ attempt: attempt + 1,
495
+ baseDelay
496
+ });
497
+ }
498
+ return id;
499
+ } catch (error) {
500
+ ctx?.logger?.error?.('Error checking ID uniqueness', {
501
+ error: error.message,
502
+ model,
503
+ attempt
504
+ });
505
+ if (attempt < maxRetries - 1) {
506
+ const delay = Math.min(baseDelay * 2 ** attempt, 2000);
507
+ await new Promise((resolve)=>setTimeout(resolve, delay));
508
+ return generateUniqueId(db, model, ctx, {
509
+ maxRetries,
510
+ attempt: attempt + 1,
511
+ baseDelay
512
+ });
513
+ }
514
+ throw error;
515
+ }
516
+ }
517
+ const patchSubjectHandler = async (c)=>{
518
+ const ctx = c.get('c15tContext');
519
+ const logger = ctx.logger;
520
+ logger.info('Handling PATCH /subjects/:id request');
521
+ const { db } = ctx;
522
+ const subjectId = c.req.param('id');
523
+ const body = await c.req.json();
524
+ const { externalId, identityProvider = 'external' } = body;
525
+ if (!subjectId) throw new HTTPException(400, {
526
+ message: 'Subject ID is required',
527
+ cause: {
528
+ code: 'SUBJECT_ID_REQUIRED'
529
+ }
530
+ });
531
+ logger.debug('Request parameters', {
532
+ subjectId,
533
+ externalId,
534
+ identityProvider
535
+ });
536
+ try {
537
+ const subject = await db.findFirst('subject', {
538
+ where: (b)=>b('id', '=', subjectId)
539
+ });
540
+ if (!subject) throw new HTTPException(404, {
541
+ message: 'Subject not found',
542
+ cause: {
543
+ code: 'SUBJECT_NOT_FOUND',
544
+ subjectId
545
+ }
546
+ });
547
+ await db.transaction(async (tx)=>{
548
+ await tx.updateMany('subject', {
549
+ where: (b)=>b('id', '=', subjectId),
550
+ set: {
551
+ externalId,
552
+ identityProvider,
553
+ updatedAt: new Date()
554
+ }
555
+ });
556
+ await tx.create('auditLog', {
557
+ id: await generateUniqueId(tx, 'auditLog', ctx),
558
+ subjectId,
559
+ entityType: 'subject',
560
+ entityId: subjectId,
561
+ actionType: 'identify_user',
562
+ ipAddress: ctx.ipAddress || null,
563
+ userAgent: ctx.userAgent || null,
564
+ changes: {
565
+ externalId: {
566
+ from: subject.externalId,
567
+ to: externalId
568
+ },
569
+ identityProvider: {
570
+ from: subject.identityProvider,
571
+ to: identityProvider
572
+ }
573
+ },
574
+ metadata: {
575
+ externalId,
576
+ identityProvider
577
+ }
578
+ });
579
+ });
580
+ logger.info('Subject linked to external ID', {
581
+ subjectId,
582
+ externalId,
583
+ identityProvider
584
+ });
585
+ getMetrics()?.recordSubjectLinked(identityProvider);
586
+ return c.json({
587
+ success: true,
588
+ subject: {
589
+ id: subjectId,
590
+ externalId
591
+ }
592
+ });
593
+ } catch (error) {
594
+ logger.error('Error in PATCH /subjects/:id handler', {
595
+ error: extractErrorMessage(error),
596
+ errorType: error instanceof Error ? error.constructor.name : typeof error
597
+ });
598
+ if (error instanceof HTTPException) throw error;
599
+ throw new HTTPException(500, {
600
+ message: 'Internal server error',
601
+ cause: {
602
+ code: 'INTERNAL_SERVER_ERROR'
603
+ }
604
+ });
605
+ }
606
+ };
607
+ function buildRuntimeDecisionDedupeKey(input) {
608
+ return [
609
+ input.tenantId ?? 'default',
610
+ input.fingerprint,
611
+ input.matchedBy,
612
+ input.countryCode ?? 'none',
613
+ input.regionCode ?? 'none',
614
+ input.jurisdiction,
615
+ input.language ?? 'none'
616
+ ].join('|');
617
+ }
618
+ function buildDecisionPayload(params) {
619
+ const { tenantId, snapshot, decision, location, jurisdiction, language, proofConfig } = params;
620
+ if (snapshot?.valid && snapshot.payload) {
621
+ const sp = snapshot.payload;
622
+ return {
623
+ tenantId,
624
+ policyId: sp.policyId,
625
+ fingerprint: sp.fingerprint,
626
+ matchedBy: sp.matchedBy,
627
+ countryCode: sp.country,
628
+ regionCode: sp.region,
629
+ jurisdiction: sp.jurisdiction,
630
+ language: sp.language,
631
+ model: sp.model,
632
+ policyI18n: sp.policyI18n,
633
+ uiMode: sp.uiMode,
634
+ bannerUi: sp.bannerUi,
635
+ dialogUi: sp.dialogUi,
636
+ categories: sp.categories,
637
+ preselectedCategories: sp.preselectedCategories,
638
+ proofConfig: sp.proofConfig,
639
+ dedupeKey: buildRuntimeDecisionDedupeKey({
640
+ tenantId,
641
+ fingerprint: sp.fingerprint,
642
+ matchedBy: sp.matchedBy,
643
+ countryCode: sp.country,
644
+ regionCode: sp.region,
645
+ jurisdiction: sp.jurisdiction,
646
+ language: sp.language
647
+ }),
648
+ source: 'snapshot_token'
649
+ };
650
+ }
651
+ if (decision) return {
652
+ tenantId,
653
+ policyId: decision.policy.id,
654
+ fingerprint: decision.fingerprint,
655
+ matchedBy: decision.matchedBy,
656
+ countryCode: location.countryCode,
657
+ regionCode: location.regionCode,
658
+ jurisdiction,
659
+ language,
660
+ model: decision.policy.model,
661
+ policyI18n: decision.policy.i18n,
662
+ uiMode: decision.policy.ui?.mode,
663
+ bannerUi: decision.policy.ui?.banner,
664
+ dialogUi: decision.policy.ui?.dialog,
665
+ categories: decision.policy.consent?.categories,
666
+ preselectedCategories: decision.policy.consent?.preselectedCategories,
667
+ proofConfig,
668
+ dedupeKey: buildRuntimeDecisionDedupeKey({
669
+ tenantId,
670
+ fingerprint: decision.fingerprint,
671
+ matchedBy: decision.matchedBy,
672
+ countryCode: location.countryCode,
673
+ regionCode: location.regionCode,
674
+ jurisdiction,
675
+ language
676
+ }),
677
+ source: 'write_time_fallback'
678
+ };
679
+ }
680
+ function parseLanguageFromHeader(header) {
681
+ if (!header) return;
682
+ const firstLanguage = header.split(',')[0]?.split(';')[0]?.trim();
683
+ if (!firstLanguage) return;
684
+ return firstLanguage.split('-')[0]?.toLowerCase();
685
+ }
686
+ function resolveSnapshotFailureMode(ctx) {
687
+ return ctx.policySnapshot?.onValidationFailure ?? 'reject';
688
+ }
689
+ function buildSnapshotHttpException(reason) {
690
+ switch(reason){
691
+ case 'missing':
692
+ return new HTTPException(409, {
693
+ message: 'Policy snapshot token is required',
694
+ cause: {
695
+ code: 'POLICY_SNAPSHOT_REQUIRED'
696
+ }
697
+ });
698
+ case 'expired':
699
+ return new HTTPException(409, {
700
+ message: 'Policy snapshot token has expired',
701
+ cause: {
702
+ code: 'POLICY_SNAPSHOT_EXPIRED'
703
+ }
704
+ });
705
+ case 'malformed':
706
+ case 'invalid':
707
+ return new HTTPException(409, {
708
+ message: 'Policy snapshot token is invalid',
709
+ cause: {
710
+ code: 'POLICY_SNAPSHOT_INVALID'
711
+ }
712
+ });
713
+ default:
714
+ {
715
+ const _exhaustive = reason;
716
+ throw new Error(`Unhandled policy snapshot verification failure reason: ${_exhaustive}`);
717
+ }
718
+ }
719
+ }
720
+ const postSubjectHandler = async (c)=>{
721
+ const ctx = c.get('c15tContext');
722
+ const logger = ctx.logger;
723
+ logger.info('Handling POST /subjects request');
724
+ const { db, registry } = ctx;
725
+ const input = await c.req.json();
726
+ const { type, subjectId, identityProvider, externalSubjectId, domain, metadata, givenAt: givenAtEpoch } = input;
727
+ const preferences = 'preferences' in input ? input.preferences : void 0;
728
+ const givenAt = new Date(givenAtEpoch);
729
+ const rawConsentAction = 'consentAction' in input ? input.consentAction : void 0;
730
+ let derivedConsentAction;
731
+ logger.debug('Request parameters', {
732
+ type,
733
+ subjectId,
734
+ identityProvider,
735
+ externalSubjectId,
736
+ domain
737
+ });
738
+ try {
739
+ 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.');
740
+ const request = c.req.raw ?? new Request('https://c15t.local/subjects');
741
+ const acceptLanguage = request.headers.get('accept-language');
742
+ const requestLanguage = parseLanguageFromHeader(acceptLanguage);
743
+ const location = await getLocation(request, ctx);
744
+ const resolvedJurisdiction = getJurisdiction(location, ctx);
745
+ const snapshotVerification = await verifyPolicySnapshotToken({
746
+ token: input.policySnapshotToken,
747
+ options: ctx.policySnapshot,
748
+ tenantId: ctx.tenantId
749
+ });
750
+ const hasValidSnapshot = snapshotVerification.valid;
751
+ const snapshotPayload = snapshotVerification.valid ? snapshotVerification.payload : null;
752
+ const shouldRequireSnapshot = !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
753
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(snapshotVerification.reason);
754
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : await policy_resolvePolicyDecision({
755
+ policies: ctx.policyPacks,
756
+ countryCode: location.countryCode,
757
+ regionCode: location.regionCode,
758
+ jurisdiction: resolvedJurisdiction,
759
+ iabEnabled: ctx.iab?.enabled === true
760
+ });
761
+ const effectivePolicy = hasValidSnapshot && snapshotPayload ? {
762
+ id: snapshotPayload.policyId,
763
+ model: snapshotPayload.model,
764
+ i18n: snapshotPayload.policyI18n,
765
+ consent: {
766
+ expiryDays: snapshotPayload.expiryDays,
767
+ scopeMode: snapshotPayload.scopeMode,
768
+ categories: snapshotPayload.categories,
769
+ preselectedCategories: snapshotPayload.preselectedCategories,
770
+ gpc: snapshotPayload.gpc
771
+ },
772
+ ui: {
773
+ mode: snapshotPayload.uiMode,
774
+ banner: snapshotPayload.bannerUi,
775
+ dialog: snapshotPayload.dialogUi
776
+ },
777
+ proof: snapshotPayload.proofConfig
778
+ } : resolvedPolicyDecision?.policy;
779
+ const effectiveModel = effectivePolicy?.model ?? ('opt-in' === input.jurisdictionModel || 'opt-out' === input.jurisdictionModel || 'iab' === input.jurisdictionModel ? input.jurisdictionModel : void 0);
780
+ if ('all' === rawConsentAction) derivedConsentAction = 'accept_all';
781
+ else if ('necessary' === rawConsentAction) derivedConsentAction = 'opt-out' === effectiveModel ? 'opt_out' : 'reject_all';
782
+ else if ('custom' === rawConsentAction) derivedConsentAction = 'custom';
783
+ const subject = await registry.findOrCreateSubject({
784
+ subjectId,
785
+ externalSubjectId,
786
+ identityProvider,
787
+ ipAddress: ctx.ipAddress
788
+ });
789
+ if (!subject) throw new HTTPException(500, {
790
+ message: 'Failed to create subject',
791
+ cause: {
792
+ code: 'SUBJECT_CREATION_FAILED',
793
+ subjectId
794
+ }
795
+ });
796
+ logger.debug('Subject found/created', {
797
+ subjectId: subject.id
798
+ });
799
+ const domainRecord = await registry.findOrCreateDomain(domain);
800
+ if (!domainRecord) throw new HTTPException(500, {
801
+ message: 'Failed to create domain',
802
+ cause: {
803
+ code: 'DOMAIN_CREATION_FAILED',
804
+ domain
805
+ }
806
+ });
807
+ let policyId;
808
+ let purposeIds = [];
809
+ let appliedPreferences;
810
+ const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
811
+ if (inputPolicyId) {
812
+ policyId = inputPolicyId;
813
+ const policy = await registry.findConsentPolicyById(inputPolicyId);
814
+ if (!policy) throw new HTTPException(404, {
815
+ message: 'Policy not found',
816
+ cause: {
817
+ code: 'POLICY_NOT_FOUND',
818
+ policyId,
819
+ type
820
+ }
821
+ });
822
+ if (!policy.isActive) throw new HTTPException(400, {
823
+ message: 'Policy is inactive',
824
+ cause: {
825
+ code: 'POLICY_INACTIVE',
826
+ policyId,
827
+ type
828
+ }
829
+ });
830
+ } else {
831
+ const policy = await registry.findOrCreatePolicy(type);
832
+ if (!policy) throw new HTTPException(500, {
833
+ message: 'Failed to create policy',
834
+ cause: {
835
+ code: 'POLICY_CREATION_FAILED',
836
+ type
837
+ }
838
+ });
839
+ policyId = policy.id;
840
+ }
841
+ if (preferences) {
842
+ const allowedCategories = effectivePolicy?.consent?.categories;
843
+ const effectiveScopeMode = effectivePolicy?.consent?.scopeMode ?? 'permissive';
844
+ const hasWildcardCategoryScope = allowedCategories?.includes('*') === true;
845
+ const appliedPreferenceEntries = Object.entries(preferences);
846
+ let filteredAppliedPreferenceEntries = appliedPreferenceEntries;
847
+ if (allowedCategories && allowedCategories.length > 0 && !hasWildcardCategoryScope) {
848
+ const disallowed = appliedPreferenceEntries.map(([purpose])=>purpose).filter((purpose)=>!allowedCategories.includes(purpose));
849
+ filteredAppliedPreferenceEntries = appliedPreferenceEntries.filter(([purpose])=>allowedCategories.includes(purpose));
850
+ if (disallowed.length > 0 && 'strict' === effectiveScopeMode) throw new HTTPException(400, {
851
+ message: 'Preferences include categories not allowed by policy',
852
+ cause: {
853
+ code: 'PURPOSE_NOT_ALLOWED',
854
+ disallowed
855
+ }
856
+ });
857
+ }
858
+ appliedPreferences = Object.fromEntries(filteredAppliedPreferenceEntries);
859
+ const filteredConsentedPurposeCodes = filteredAppliedPreferenceEntries.filter(([_, isConsented])=>isConsented).map(([purposeCode])=>purposeCode);
860
+ logger.debug('Consented purposes', {
861
+ consentedPurposes: filteredConsentedPurposeCodes
862
+ });
863
+ const purposesRaw = await Promise.all(filteredConsentedPurposeCodes.map((purposeCode)=>registry.findOrCreateConsentPurposeByCode(purposeCode)));
864
+ const purposes = purposesRaw.map((purpose)=>purpose?.id ?? null).filter((id)=>Boolean(id));
865
+ logger.debug('Filtered purposes', {
866
+ purposes
867
+ });
868
+ if (0 === purposes.length) logger.warn('No valid purpose IDs found after filtering. Using empty list.', {
869
+ consentedPurposes: filteredConsentedPurposeCodes
870
+ });
871
+ purposeIds = purposes;
872
+ }
873
+ const expiryDays = effectivePolicy?.consent?.expiryDays;
874
+ const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
875
+ const proofConfig = effectivePolicy?.proof;
876
+ const shouldStoreIp = proofConfig?.storeIp ?? true;
877
+ const shouldStoreUserAgent = proofConfig?.storeUserAgent ?? true;
878
+ const shouldStoreLanguage = proofConfig?.storeLanguage ?? false;
879
+ const effectiveLanguage = (snapshotPayload?.language && hasValidSnapshot ? snapshotPayload.language : requestLanguage) ?? void 0;
880
+ const metadataWithPolicy = {
881
+ ...metadata ?? {},
882
+ ...shouldStoreLanguage && effectiveLanguage ? {
883
+ policyLanguage: effectiveLanguage
884
+ } : {}
885
+ };
886
+ const effectiveJurisdiction = hasValidSnapshot && snapshotPayload ? snapshotPayload.jurisdiction : resolvedJurisdiction;
887
+ const decisionPayload = buildDecisionPayload({
888
+ tenantId: ctx.tenantId,
889
+ snapshot: hasValidSnapshot && snapshotPayload ? {
890
+ valid: true,
891
+ payload: snapshotPayload
892
+ } : null,
893
+ decision: resolvedPolicyDecision,
894
+ location: {
895
+ countryCode: location.countryCode,
896
+ regionCode: location.regionCode
897
+ },
898
+ jurisdiction: resolvedJurisdiction,
899
+ language: effectiveLanguage,
900
+ proofConfig
901
+ });
902
+ const existingConsent = await db.findFirst('consent', {
903
+ where: (b)=>b.and(b('subjectId', '=', subject.id), b('domainId', '=', domainRecord.id), b('policyId', '=', policyId), b('givenAt', '=', givenAt))
904
+ });
905
+ if (existingConsent) {
906
+ logger.debug('Duplicate consent detected, returning existing record', {
907
+ consentId: existingConsent.id
908
+ });
909
+ return c.json({
910
+ subjectId: subject.id,
911
+ consentId: existingConsent.id,
912
+ domainId: domainRecord.id,
913
+ domain: domainRecord.name,
914
+ type,
915
+ metadata,
916
+ appliedPreferences,
917
+ uiSource: input.uiSource,
918
+ givenAt: existingConsent.givenAt
919
+ });
920
+ }
921
+ const result = await db.transaction(async (tx)=>{
922
+ logger.debug('Creating consent record', {
923
+ subjectId: subject.id,
924
+ domainId: domainRecord.id,
925
+ policyId,
926
+ purposeIds
927
+ });
928
+ const runtimePolicyDecision = decisionPayload ? await tx.findFirst('runtimePolicyDecision', {
929
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
930
+ }) ?? await tx.create('runtimePolicyDecision', {
931
+ id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
932
+ tenantId: decisionPayload.tenantId,
933
+ policyId: decisionPayload.policyId,
934
+ fingerprint: decisionPayload.fingerprint,
935
+ matchedBy: decisionPayload.matchedBy,
936
+ countryCode: decisionPayload.countryCode,
937
+ regionCode: decisionPayload.regionCode,
938
+ jurisdiction: decisionPayload.jurisdiction,
939
+ language: decisionPayload.language,
940
+ model: decisionPayload.model,
941
+ policyI18n: decisionPayload.policyI18n ? {
942
+ json: decisionPayload.policyI18n
943
+ } : void 0,
944
+ uiMode: decisionPayload.uiMode,
945
+ bannerUi: decisionPayload.bannerUi ? {
946
+ json: decisionPayload.bannerUi
947
+ } : void 0,
948
+ dialogUi: decisionPayload.dialogUi ? {
949
+ json: decisionPayload.dialogUi
950
+ } : void 0,
951
+ categories: decisionPayload.categories ? {
952
+ json: decisionPayload.categories
953
+ } : void 0,
954
+ preselectedCategories: decisionPayload.preselectedCategories ? {
955
+ json: decisionPayload.preselectedCategories
956
+ } : void 0,
957
+ proofConfig: decisionPayload.proofConfig ? {
958
+ json: decisionPayload.proofConfig
959
+ } : void 0,
960
+ dedupeKey: decisionPayload.dedupeKey
961
+ }).catch(async ()=>tx.findFirst('runtimePolicyDecision', {
962
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
963
+ })) : void 0;
964
+ const consentRecord = await tx.create('consent', {
965
+ id: await generateUniqueId(tx, 'consent', ctx),
966
+ subjectId: subject.id,
967
+ domainId: domainRecord.id,
968
+ policyId,
969
+ purposeIds: {
970
+ json: purposeIds
971
+ },
972
+ metadata: Object.keys(metadataWithPolicy).length > 0 ? {
973
+ json: metadataWithPolicy
974
+ } : void 0,
975
+ ipAddress: shouldStoreIp ? ctx.ipAddress : null,
976
+ userAgent: shouldStoreUserAgent ? ctx.userAgent : null,
977
+ jurisdiction: effectiveJurisdiction,
978
+ jurisdictionModel: effectiveModel,
979
+ tcString: input.tcString,
980
+ uiSource: input.uiSource,
981
+ consentAction: derivedConsentAction,
982
+ givenAt,
983
+ validUntil,
984
+ runtimePolicyDecisionId: runtimePolicyDecision?.id,
985
+ runtimePolicySource: decisionPayload?.source
986
+ });
987
+ logger.debug('Created consent', {
988
+ consentRecord: consentRecord.id
989
+ });
990
+ if (!consentRecord) throw new HTTPException(500, {
991
+ message: 'Failed to create consent',
992
+ cause: {
993
+ code: 'CONSENT_CREATION_FAILED',
994
+ subjectId: subject.id,
995
+ domain
996
+ }
997
+ });
998
+ return {
999
+ consent: consentRecord
1000
+ };
1001
+ });
1002
+ const metrics = getMetrics();
1003
+ if (metrics) {
1004
+ const jurisdiction = effectiveJurisdiction;
1005
+ metrics.recordConsentCreated({
1006
+ type,
1007
+ jurisdiction
1008
+ });
1009
+ const hasAccepted = preferences && Object.values(preferences).some(Boolean);
1010
+ if (hasAccepted) metrics.recordConsentAccepted({
1011
+ type,
1012
+ jurisdiction
1013
+ });
1014
+ else metrics.recordConsentRejected({
1015
+ type,
1016
+ jurisdiction
1017
+ });
1018
+ }
1019
+ return c.json({
1020
+ subjectId: subject.id,
1021
+ consentId: result.consent.id,
1022
+ domainId: domainRecord.id,
1023
+ domain: domainRecord.name,
1024
+ type,
1025
+ metadata,
1026
+ appliedPreferences,
1027
+ uiSource: input.uiSource,
1028
+ givenAt: result.consent.givenAt
1029
+ });
1030
+ } catch (error) {
1031
+ logger.error('Error in POST /subjects handler', {
1032
+ error: extractErrorMessage(error),
1033
+ errorType: error instanceof Error ? error.constructor.name : typeof error
1034
+ });
1035
+ if (error instanceof HTTPException) throw error;
1036
+ throw new HTTPException(500, {
1037
+ message: 'Internal server error',
1038
+ cause: {
1039
+ code: 'INTERNAL_SERVER_ERROR'
1040
+ }
1041
+ });
1042
+ }
1043
+ };
1044
+ const createSubjectRoutes = ()=>{
1045
+ const app = new Hono();
1046
+ app.get('/:id', describeRoute({
1047
+ summary: 'Get subject consent status',
1048
+ 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)).",
1049
+ tags: [
1050
+ 'Subject',
1051
+ 'Consent'
1052
+ ],
1053
+ responses: {
1054
+ 200: {
1055
+ description: 'Subject and consent records for the requested type(s)',
1056
+ content: {
1057
+ 'application/json': {
1058
+ schema: resolver(getSubjectOutputSchema)
1059
+ }
1060
+ }
1061
+ },
1062
+ 404: {
1063
+ description: 'Subject not found for the given ID'
1064
+ }
1065
+ }
1066
+ }), validator('param', getSubjectInputSchema), getSubjectHandler);
1067
+ app.post('/', describeRoute({
1068
+ summary: 'Record consent for a subject',
1069
+ description: "Creates a new consent record (append-only). Creates the subject if it does not exist.\n\n**Request body by `type`:**\n- `cookie_banner` – Requires `preferences` object\n- `privacy_policy`, `dpa`, `terms_and_conditions` – Optional `policyId`\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
1070
+ tags: [
1071
+ 'Subject',
1072
+ 'Consent'
1073
+ ],
1074
+ responses: {
1075
+ 200: {
1076
+ description: 'Consent recorded; subject and consent in response',
1077
+ content: {
1078
+ 'application/json': {
1079
+ schema: resolver(postSubjectOutputSchema)
1080
+ }
1081
+ }
1082
+ },
1083
+ 422: {
1084
+ description: 'Invalid request body (schema or validation failed)'
1085
+ }
1086
+ }
1087
+ }), validator('json', postSubjectInputSchema), postSubjectHandler);
1088
+ app.patch('/:id', describeRoute({
1089
+ summary: 'Link external ID to subject',
1090
+ description: 'Associates an external user ID with an existing subject (e.g. after login). Enables cross-device consent sync.',
1091
+ tags: [
1092
+ 'Subject'
1093
+ ],
1094
+ responses: {
1095
+ 200: {
1096
+ description: 'Subject updated with external ID',
1097
+ content: {
1098
+ 'application/json': {
1099
+ schema: resolver(patchSubjectOutputSchema)
1100
+ }
1101
+ }
1102
+ },
1103
+ 404: {
1104
+ description: 'Subject not found for the given ID'
1105
+ }
1106
+ }
1107
+ }), validator('param', __rspack_external_valibot.object({
1108
+ id: subjectIdSchema
1109
+ })), validator('json', __rspack_external_valibot.object({
1110
+ externalId: __rspack_external_valibot.string(),
1111
+ identityProvider: __rspack_external_valibot.optional(__rspack_external_valibot.string())
1112
+ })), patchSubjectHandler);
1113
+ app.get('/', describeRoute({
1114
+ summary: 'List subjects by external ID (API key required)',
1115
+ description: 'Returns all subjects linked to the given external ID. Requires Bearer token (API key). Use for server-side consent lookups.',
1116
+ tags: [
1117
+ 'Subject'
1118
+ ],
1119
+ security: [
1120
+ {
1121
+ bearerAuth: []
1122
+ }
1123
+ ],
1124
+ responses: {
1125
+ 200: {
1126
+ description: 'List of subjects for the external ID',
1127
+ content: {
1128
+ 'application/json': {
1129
+ schema: resolver(listSubjectsOutputSchema)
1130
+ }
1131
+ }
1132
+ },
1133
+ 401: {
1134
+ description: 'Missing or invalid API key'
1135
+ }
1136
+ }
1137
+ }), validator('query', listSubjectsQuerySchema), listSubjectsHandler);
1138
+ return app;
1139
+ };
1140
+ export { createConsentRoutes, createInitRoute, createStatusRoute, createSubjectRoutes };