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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/dist/core.cjs +830 -74
  2. package/dist/core.js +807 -75
  3. package/dist/db/schema.cjs +37 -0
  4. package/dist/db/schema.js +33 -2
  5. package/dist/edge.cjs +1106 -0
  6. package/dist/edge.js +1069 -0
  7. package/dist/router.cjs +613 -64
  8. package/dist/router.js +613 -64
  9. package/{dist → dist-types}/cache/adapters/cloudflare-kv.d.ts +0 -1
  10. package/{dist → dist-types}/cache/adapters/index.d.ts +0 -1
  11. package/{dist → dist-types}/cache/adapters/memory.d.ts +0 -1
  12. package/{dist → dist-types}/cache/adapters/upstash-redis.d.ts +0 -1
  13. package/{dist → dist-types}/cache/gvl-resolver.d.ts +1 -2
  14. package/{dist → dist-types}/cache/index.d.ts +0 -1
  15. package/{dist → dist-types}/cache/keys.d.ts +0 -1
  16. package/{dist → dist-types}/cache/types.d.ts +0 -1
  17. package/{dist → dist-types}/core.d.ts +8 -1
  18. package/{dist → dist-types}/db/migrator/index.d.ts +0 -1
  19. package/{dist → dist-types}/db/registry/consent-policy.d.ts +0 -1
  20. package/{dist → dist-types}/db/registry/consent-purpose.d.ts +0 -1
  21. package/{dist → dist-types}/db/registry/domain.d.ts +0 -1
  22. package/{dist → dist-types}/db/registry/index.d.ts +22 -2
  23. package/dist-types/db/registry/runtime-policy-decision.d.ts +60 -0
  24. package/{dist → dist-types}/db/registry/subject.d.ts +0 -1
  25. package/{dist → dist-types}/db/registry/types.d.ts +1 -2
  26. package/{dist → dist-types}/db/registry/utils/generate-id.d.ts +0 -1
  27. package/{dist → dist-types}/db/registry/utils.d.ts +0 -1
  28. package/{dist → dist-types}/db/schema/1.0.0/audit-log.d.ts +0 -1
  29. package/{dist → dist-types}/db/schema/1.0.0/consent-policy.d.ts +0 -1
  30. package/{dist → dist-types}/db/schema/1.0.0/consent-purpose.d.ts +0 -1
  31. package/{dist → dist-types}/db/schema/1.0.0/consent-record.d.ts +0 -1
  32. package/{dist → dist-types}/db/schema/1.0.0/consent.d.ts +1 -2
  33. package/{dist → dist-types}/db/schema/1.0.0/domain.d.ts +0 -1
  34. package/{dist → dist-types}/db/schema/1.0.0/index.d.ts +0 -1
  35. package/{dist → dist-types}/db/schema/1.0.0/subject.d.ts +0 -1
  36. package/{dist → dist-types}/db/schema/2.0.0/audit-log.d.ts +1 -2
  37. package/{dist → dist-types}/db/schema/2.0.0/consent-policy.d.ts +1 -2
  38. package/{dist → dist-types}/db/schema/2.0.0/consent-purpose.d.ts +1 -2
  39. package/{dist → dist-types}/db/schema/2.0.0/consent.d.ts +5 -2
  40. package/{dist → dist-types}/db/schema/2.0.0/domain.d.ts +1 -2
  41. package/{dist → dist-types}/db/schema/2.0.0/index.d.ts +432 -17
  42. package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +23 -0
  43. package/{dist → dist-types}/db/schema/2.0.0/subject.d.ts +1 -2
  44. package/{dist → dist-types}/db/schema/index.d.ts +862 -33
  45. package/{dist → dist-types}/db/tenant-scope.d.ts +0 -1
  46. package/{dist → dist-types}/define-config.d.ts +0 -1
  47. package/dist-types/edge/index.d.ts +5 -0
  48. package/dist-types/edge/init-handler.d.ts +38 -0
  49. package/dist-types/edge/resolve-consent.d.ts +80 -0
  50. package/dist-types/edge/types.d.ts +13 -0
  51. package/{dist → dist-types}/handlers/consent/check.handler.d.ts +0 -1
  52. package/{src/handlers/consent/index.ts → dist-types/handlers/consent/index.d.ts} +0 -1
  53. package/{dist → dist-types}/handlers/init/geo.d.ts +2 -3
  54. package/{dist → dist-types}/handlers/init/index.d.ts +4 -5
  55. package/dist-types/handlers/init/policy.d.ts +26 -0
  56. package/dist-types/handlers/init/resolve-init.d.ts +44 -0
  57. package/dist-types/handlers/init/translations.d.ts +48 -0
  58. package/dist-types/handlers/policy/snapshot.d.ts +99 -0
  59. package/{src/handlers/status/index.ts → dist-types/handlers/status/index.d.ts} +0 -1
  60. package/{dist → dist-types}/handlers/status/status.handler.d.ts +0 -1
  61. package/{dist → dist-types}/handlers/subject/get.handler.d.ts +0 -1
  62. package/{src/handlers/subject/index.ts → dist-types/handlers/subject/index.d.ts} +0 -1
  63. package/{dist → dist-types}/handlers/subject/list.handler.d.ts +0 -1
  64. package/{dist → dist-types}/handlers/subject/patch.handler.d.ts +0 -1
  65. package/{dist → dist-types}/handlers/subject/post.handler.d.ts +12 -1
  66. package/{dist → dist-types}/handlers/utils/consent-enrichment.d.ts +0 -1
  67. package/{dist → dist-types}/init.d.ts +0 -1
  68. package/{dist → dist-types}/middleware/auth/index.d.ts +0 -1
  69. package/{dist → dist-types}/middleware/auth/validate-api-key.d.ts +0 -1
  70. package/{dist → dist-types}/middleware/cors/cors.d.ts +0 -1
  71. package/{src/middleware/cors/index.ts → dist-types/middleware/cors/index.d.ts} +0 -1
  72. package/{dist → dist-types}/middleware/cors/is-origin-trusted.d.ts +1 -2
  73. package/{dist → dist-types}/middleware/cors/process-cors.d.ts +0 -1
  74. package/{dist → dist-types}/middleware/openapi/config.d.ts +0 -1
  75. package/{dist → dist-types}/middleware/openapi/handlers.d.ts +0 -1
  76. package/{src/middleware/openapi/index.ts → dist-types/middleware/openapi/index.d.ts} +0 -1
  77. package/{dist → dist-types}/middleware/process-ip/index.d.ts +0 -1
  78. package/dist-types/policies/builder.d.ts +127 -0
  79. package/dist-types/policies/defaults.d.ts +2 -0
  80. package/dist-types/policies/matchers.d.ts +3 -0
  81. package/{dist → dist-types}/router.d.ts +0 -1
  82. package/{dist → dist-types}/routes/consent.d.ts +0 -1
  83. package/{src/routes/index.ts → dist-types/routes/index.d.ts} +0 -1
  84. package/{dist → dist-types}/routes/init.d.ts +0 -1
  85. package/{dist → dist-types}/routes/status.d.ts +0 -1
  86. package/{dist → dist-types}/routes/subject.d.ts +0 -1
  87. package/{dist → dist-types}/types/api.d.ts +0 -1
  88. package/{dist → dist-types}/types/index.d.ts +110 -6
  89. package/dist-types/utils/background.d.ts +6 -0
  90. package/{dist → dist-types}/utils/create-telemetry-options.d.ts +0 -1
  91. package/{dist → dist-types}/utils/env.d.ts +0 -1
  92. package/{dist → dist-types}/utils/extract-error-message.d.ts +0 -1
  93. package/{dist → dist-types}/utils/instrumentation.d.ts +0 -1
  94. package/{dist → dist-types}/utils/logger.d.ts +1 -2
  95. package/{dist → dist-types}/utils/metrics.d.ts +0 -1
  96. package/dist-types/version.d.ts +1 -0
  97. package/docs/README.md +49 -0
  98. package/docs/api/configuration.md +197 -0
  99. package/docs/api/endpoints.md +211 -0
  100. package/docs/guides/caching.md +85 -0
  101. package/docs/guides/database-setup.md +128 -0
  102. package/docs/guides/edge-deployment.md +248 -0
  103. package/docs/guides/framework-integration.md +142 -0
  104. package/docs/guides/iab-tcf.md +89 -0
  105. package/docs/guides/observability.md +96 -0
  106. package/docs/guides/policy-packs.md +396 -0
  107. package/docs/quickstart.md +129 -0
  108. package/package.json +33 -19
  109. package/.turbo/turbo-build.log +0 -49
  110. package/CHANGELOG.md +0 -123
  111. package/dist/cache/adapters/cloudflare-kv.d.ts.map +0 -1
  112. package/dist/cache/adapters/index.d.ts.map +0 -1
  113. package/dist/cache/adapters/memory.d.ts.map +0 -1
  114. package/dist/cache/adapters/upstash-redis.d.ts.map +0 -1
  115. package/dist/cache/gvl-resolver.d.ts.map +0 -1
  116. package/dist/cache/index.d.ts.map +0 -1
  117. package/dist/cache/keys.d.ts.map +0 -1
  118. package/dist/cache/types.d.ts.map +0 -1
  119. package/dist/core.d.ts.map +0 -1
  120. package/dist/db/adapters/drizzle.d.ts +0 -2
  121. package/dist/db/adapters/drizzle.d.ts.map +0 -1
  122. package/dist/db/adapters/index.d.ts +0 -2
  123. package/dist/db/adapters/index.d.ts.map +0 -1
  124. package/dist/db/adapters/kysely.d.ts +0 -2
  125. package/dist/db/adapters/kysely.d.ts.map +0 -1
  126. package/dist/db/adapters/mongo.d.ts +0 -2
  127. package/dist/db/adapters/mongo.d.ts.map +0 -1
  128. package/dist/db/adapters/prisma.d.ts +0 -2
  129. package/dist/db/adapters/prisma.d.ts.map +0 -1
  130. package/dist/db/adapters/typeorm.d.ts +0 -2
  131. package/dist/db/adapters/typeorm.d.ts.map +0 -1
  132. package/dist/db/migrator/index.d.ts.map +0 -1
  133. package/dist/db/registry/consent-policy.d.ts.map +0 -1
  134. package/dist/db/registry/consent-purpose.d.ts.map +0 -1
  135. package/dist/db/registry/domain.d.ts.map +0 -1
  136. package/dist/db/registry/index.d.ts.map +0 -1
  137. package/dist/db/registry/subject.d.ts.map +0 -1
  138. package/dist/db/registry/types.d.ts.map +0 -1
  139. package/dist/db/registry/utils/generate-id.d.ts.map +0 -1
  140. package/dist/db/registry/utils.d.ts.map +0 -1
  141. package/dist/db/schema/1.0.0/audit-log.d.ts.map +0 -1
  142. package/dist/db/schema/1.0.0/consent-policy.d.ts.map +0 -1
  143. package/dist/db/schema/1.0.0/consent-purpose.d.ts.map +0 -1
  144. package/dist/db/schema/1.0.0/consent-record.d.ts.map +0 -1
  145. package/dist/db/schema/1.0.0/consent.d.ts.map +0 -1
  146. package/dist/db/schema/1.0.0/domain.d.ts.map +0 -1
  147. package/dist/db/schema/1.0.0/index.d.ts.map +0 -1
  148. package/dist/db/schema/1.0.0/subject.d.ts.map +0 -1
  149. package/dist/db/schema/2.0.0/audit-log.d.ts.map +0 -1
  150. package/dist/db/schema/2.0.0/consent-policy.d.ts.map +0 -1
  151. package/dist/db/schema/2.0.0/consent-purpose.d.ts.map +0 -1
  152. package/dist/db/schema/2.0.0/consent.d.ts.map +0 -1
  153. package/dist/db/schema/2.0.0/domain.d.ts.map +0 -1
  154. package/dist/db/schema/2.0.0/index.d.ts.map +0 -1
  155. package/dist/db/schema/2.0.0/subject.d.ts.map +0 -1
  156. package/dist/db/schema/index.d.ts.map +0 -1
  157. package/dist/db/tenant-scope.d.ts.map +0 -1
  158. package/dist/define-config.d.ts.map +0 -1
  159. package/dist/handlers/consent/check.handler.d.ts.map +0 -1
  160. package/dist/handlers/consent/index.d.ts +0 -12
  161. package/dist/handlers/consent/index.d.ts.map +0 -1
  162. package/dist/handlers/init/geo.d.ts.map +0 -1
  163. package/dist/handlers/init/index.d.ts.map +0 -1
  164. package/dist/handlers/init/translations.d.ts +0 -26
  165. package/dist/handlers/init/translations.d.ts.map +0 -1
  166. package/dist/handlers/status/index.d.ts +0 -7
  167. package/dist/handlers/status/index.d.ts.map +0 -1
  168. package/dist/handlers/status/status.handler.d.ts.map +0 -1
  169. package/dist/handlers/subject/get.handler.d.ts.map +0 -1
  170. package/dist/handlers/subject/index.d.ts +0 -10
  171. package/dist/handlers/subject/index.d.ts.map +0 -1
  172. package/dist/handlers/subject/list.handler.d.ts.map +0 -1
  173. package/dist/handlers/subject/patch.handler.d.ts.map +0 -1
  174. package/dist/handlers/subject/post.handler.d.ts.map +0 -1
  175. package/dist/handlers/utils/consent-enrichment.d.ts.map +0 -1
  176. package/dist/init.d.ts.map +0 -1
  177. package/dist/middleware/auth/index.d.ts.map +0 -1
  178. package/dist/middleware/auth/validate-api-key.d.ts.map +0 -1
  179. package/dist/middleware/cors/cors.d.ts.map +0 -1
  180. package/dist/middleware/cors/index.d.ts +0 -30
  181. package/dist/middleware/cors/index.d.ts.map +0 -1
  182. package/dist/middleware/cors/is-origin-trusted.d.ts.map +0 -1
  183. package/dist/middleware/cors/process-cors.d.ts.map +0 -1
  184. package/dist/middleware/openapi/config.d.ts.map +0 -1
  185. package/dist/middleware/openapi/handlers.d.ts.map +0 -1
  186. package/dist/middleware/openapi/index.d.ts +0 -12
  187. package/dist/middleware/openapi/index.d.ts.map +0 -1
  188. package/dist/middleware/process-ip/index.d.ts.map +0 -1
  189. package/dist/router.d.ts.map +0 -1
  190. package/dist/routes/consent.d.ts.map +0 -1
  191. package/dist/routes/index.d.ts +0 -10
  192. package/dist/routes/index.d.ts.map +0 -1
  193. package/dist/routes/init.d.ts.map +0 -1
  194. package/dist/routes/status.d.ts.map +0 -1
  195. package/dist/routes/subject.d.ts.map +0 -1
  196. package/dist/types/api.d.ts.map +0 -1
  197. package/dist/types/index.d.ts.map +0 -1
  198. package/dist/utils/create-telemetry-options.d.ts.map +0 -1
  199. package/dist/utils/env.d.ts.map +0 -1
  200. package/dist/utils/extract-error-message.d.ts.map +0 -1
  201. package/dist/utils/index.d.ts +0 -4
  202. package/dist/utils/index.d.ts.map +0 -1
  203. package/dist/utils/instrumentation.d.ts.map +0 -1
  204. package/dist/utils/logger.d.ts.map +0 -1
  205. package/dist/utils/metrics.d.ts.map +0 -1
  206. package/dist/version.d.ts +0 -2
  207. package/dist/version.d.ts.map +0 -1
  208. package/knip.json +0 -31
  209. package/rslib.config.ts +0 -93
  210. package/src/cache/adapters/cloudflare-kv.ts +0 -71
  211. package/src/cache/adapters/index.ts +0 -22
  212. package/src/cache/adapters/memory.ts +0 -111
  213. package/src/cache/adapters/upstash-redis.ts +0 -113
  214. package/src/cache/gvl-resolver.ts +0 -289
  215. package/src/cache/index.ts +0 -34
  216. package/src/cache/keys.ts +0 -68
  217. package/src/cache/types.ts +0 -66
  218. package/src/core.ts +0 -369
  219. package/src/db/migrator/index.ts +0 -80
  220. package/src/db/registry/consent-policy.test.ts +0 -451
  221. package/src/db/registry/consent-policy.ts +0 -82
  222. package/src/db/registry/consent-purpose.test.ts +0 -428
  223. package/src/db/registry/consent-purpose.ts +0 -61
  224. package/src/db/registry/domain.test.ts +0 -445
  225. package/src/db/registry/domain.ts +0 -91
  226. package/src/db/registry/index.ts +0 -14
  227. package/src/db/registry/subject.test.ts +0 -371
  228. package/src/db/registry/subject.ts +0 -126
  229. package/src/db/registry/types.ts +0 -10
  230. package/src/db/registry/utils/generate-id.test.ts +0 -216
  231. package/src/db/registry/utils/generate-id.ts +0 -133
  232. package/src/db/registry/utils.ts +0 -133
  233. package/src/db/schema/1.0.0/audit-log.ts +0 -15
  234. package/src/db/schema/1.0.0/consent-policy.ts +0 -14
  235. package/src/db/schema/1.0.0/consent-purpose.ts +0 -14
  236. package/src/db/schema/1.0.0/consent-record.ts +0 -10
  237. package/src/db/schema/1.0.0/consent.ts +0 -20
  238. package/src/db/schema/1.0.0/domain.ts +0 -12
  239. package/src/db/schema/1.0.0/index.ts +0 -48
  240. package/src/db/schema/1.0.0/subject.ts +0 -11
  241. package/src/db/schema/2.0.0/audit-log.ts +0 -18
  242. package/src/db/schema/2.0.0/consent-policy.ts +0 -28
  243. package/src/db/schema/2.0.0/consent-purpose.ts +0 -12
  244. package/src/db/schema/2.0.0/consent.ts +0 -28
  245. package/src/db/schema/2.0.0/domain.ts +0 -12
  246. package/src/db/schema/2.0.0/index.ts +0 -47
  247. package/src/db/schema/2.0.0/subject.ts +0 -13
  248. package/src/db/schema/index.ts +0 -15
  249. package/src/db/tenant-scope.test.ts +0 -747
  250. package/src/db/tenant-scope.ts +0 -103
  251. package/src/define-config.ts +0 -19
  252. package/src/handlers/consent/check.handler.ts +0 -126
  253. package/src/handlers/init/geo.test.ts +0 -317
  254. package/src/handlers/init/geo.ts +0 -195
  255. package/src/handlers/init/index.test.ts +0 -205
  256. package/src/handlers/init/index.ts +0 -114
  257. package/src/handlers/init/translations.test.ts +0 -121
  258. package/src/handlers/init/translations.ts +0 -69
  259. package/src/handlers/status/status.handler.test.ts +0 -155
  260. package/src/handlers/status/status.handler.ts +0 -51
  261. package/src/handlers/subject/get.handler.ts +0 -92
  262. package/src/handlers/subject/list.handler.ts +0 -92
  263. package/src/handlers/subject/patch.handler.ts +0 -119
  264. package/src/handlers/subject/post.handler.test.ts +0 -294
  265. package/src/handlers/subject/post.handler.ts +0 -268
  266. package/src/handlers/utils/consent-enrichment.test.ts +0 -380
  267. package/src/handlers/utils/consent-enrichment.ts +0 -218
  268. package/src/init.test.ts +0 -122
  269. package/src/init.ts +0 -88
  270. package/src/middleware/auth/index.ts +0 -11
  271. package/src/middleware/auth/validate-api-key.test.ts +0 -86
  272. package/src/middleware/auth/validate-api-key.ts +0 -107
  273. package/src/middleware/cors/cors.test.ts +0 -135
  274. package/src/middleware/cors/cors.ts +0 -186
  275. package/src/middleware/cors/is-origin-trusted.test.ts +0 -164
  276. package/src/middleware/cors/is-origin-trusted.ts +0 -130
  277. package/src/middleware/cors/process-cors.ts +0 -91
  278. package/src/middleware/openapi/config.ts +0 -29
  279. package/src/middleware/openapi/handlers.ts +0 -34
  280. package/src/middleware/process-ip/index.test.ts +0 -193
  281. package/src/middleware/process-ip/index.ts +0 -199
  282. package/src/router.ts +0 -15
  283. package/src/routes/consent.ts +0 -52
  284. package/src/routes/init.ts +0 -105
  285. package/src/routes/status.ts +0 -46
  286. package/src/routes/subject.ts +0 -152
  287. package/src/types/api.ts +0 -48
  288. package/src/types/index.ts +0 -391
  289. package/src/utils/create-telemetry-options.test.ts +0 -286
  290. package/src/utils/create-telemetry-options.ts +0 -229
  291. package/src/utils/env.ts +0 -84
  292. package/src/utils/extract-error-message.ts +0 -21
  293. package/src/utils/instrumentation.test.ts +0 -183
  294. package/src/utils/instrumentation.ts +0 -194
  295. package/src/utils/logger.ts +0 -41
  296. package/src/utils/metrics.test.ts +0 -311
  297. package/src/utils/metrics.ts +0 -402
  298. package/src/utils/telemetry-pii.test.ts +0 -323
  299. package/src/version.ts +0 -2
  300. package/tsconfig.json +0 -11
  301. package/vitest.config.ts +0 -28
  302. /package/{src/db/adapters/drizzle.ts → dist-types/db/adapters/drizzle.d.ts} +0 -0
  303. /package/{src/db/adapters/index.ts → dist-types/db/adapters/index.d.ts} +0 -0
  304. /package/{src/db/adapters/kysely.ts → dist-types/db/adapters/kysely.d.ts} +0 -0
  305. /package/{src/db/adapters/mongo.ts → dist-types/db/adapters/mongo.d.ts} +0 -0
  306. /package/{src/db/adapters/prisma.ts → dist-types/db/adapters/prisma.d.ts} +0 -0
  307. /package/{src/db/adapters/typeorm.ts → dist-types/db/adapters/typeorm.d.ts} +0 -0
  308. /package/{src/utils/index.ts → dist-types/utils/index.d.ts} +0 -0
package/dist/router.js CHANGED
@@ -3,6 +3,8 @@ import { Hono } from "hono";
3
3
  import { describeRoute, resolver, validator } from "hono-openapi";
4
4
  import { HTTPException } from "hono/http-exception";
5
5
  import { SpanKind as api_SpanKind, SpanStatusCode as api_SpanStatusCode, context, metrics as api_metrics, trace as api_trace } from "@opentelemetry/api";
6
+ import { SignJWT, errors, jwtVerify } from "jose";
7
+ import { resolvePolicyDecision } from "@c15t/schema/types";
6
8
  import { deepMergeTranslations, selectLanguage } from "@c15t/translations";
7
9
  import { baseTranslations } from "@c15t/translations/all";
8
10
  import { object, optional, string } from "valibot";
@@ -15,7 +17,7 @@ function extractErrorMessage(error) {
15
17
  if (error instanceof Error) return error.message || error.name;
16
18
  return String(error);
17
19
  }
18
- const version_version = '2.0.0-rc.4';
20
+ const version_version = '2.0.0-rc.5';
19
21
  let cachedConfig = null;
20
22
  let cachedDefaultAttributes = {};
21
23
  function create_telemetry_options_isTelemetryEnabled(options) {
@@ -611,6 +613,123 @@ function createGVLResolver(options) {
611
613
  }
612
614
  };
613
615
  }
616
+ const POLICY_SNAPSHOT_JWT_HEADER = {
617
+ alg: 'HS256',
618
+ typ: 'JWT'
619
+ };
620
+ const DEFAULT_POLICY_SNAPSHOT_ISSUER = 'c15t';
621
+ const DEFAULT_POLICY_SNAPSHOT_AUDIENCE = 'c15t-policy-snapshot';
622
+ function resolveSnapshotIssuer(options) {
623
+ return options?.issuer?.trim() || DEFAULT_POLICY_SNAPSHOT_ISSUER;
624
+ }
625
+ function resolveSnapshotAudience(params) {
626
+ const configuredAudience = params.options?.audience?.trim();
627
+ if (configuredAudience) return configuredAudience;
628
+ return params.tenantId ? `${DEFAULT_POLICY_SNAPSHOT_AUDIENCE}:${params.tenantId}` : DEFAULT_POLICY_SNAPSHOT_AUDIENCE;
629
+ }
630
+ function getSigningKey(secret) {
631
+ return new TextEncoder().encode(secret);
632
+ }
633
+ function isPolicySnapshotPayload(payload) {
634
+ return 'string' == typeof payload.policyId && 'string' == typeof payload.fingerprint && 'string' == typeof payload.matchedBy && 'string' == typeof payload.jurisdiction && 'string' == typeof payload.model && 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub && 'number' == typeof payload.iat && 'number' == typeof payload.exp;
635
+ }
636
+ async function createPolicySnapshotToken(params) {
637
+ const { options } = params;
638
+ if (!options?.signingKey) return;
639
+ const iat = Math.floor(Date.now() / 1000);
640
+ const ttlSeconds = options.ttlSeconds ?? 1800;
641
+ const exp = iat + ttlSeconds;
642
+ const iss = resolveSnapshotIssuer(options);
643
+ const aud = resolveSnapshotAudience({
644
+ options,
645
+ tenantId: params.tenantId
646
+ });
647
+ const payload = {
648
+ iss,
649
+ aud,
650
+ sub: params.policyId,
651
+ tenantId: params.tenantId,
652
+ policyId: params.policyId,
653
+ fingerprint: params.fingerprint,
654
+ matchedBy: params.matchedBy,
655
+ country: params.country,
656
+ region: params.region,
657
+ jurisdiction: params.jurisdiction,
658
+ language: params.language,
659
+ model: params.model,
660
+ policyI18n: params.policyI18n,
661
+ expiryDays: params.expiryDays,
662
+ scopeMode: params.scopeMode,
663
+ uiMode: params.uiMode,
664
+ bannerUi: params.bannerUi,
665
+ dialogUi: params.dialogUi,
666
+ categories: params.categories,
667
+ preselectedCategories: params.preselectedCategories,
668
+ gpc: params.gpc,
669
+ proofConfig: params.proofConfig,
670
+ iat,
671
+ exp
672
+ };
673
+ const token = await new SignJWT(payload).setProtectedHeader(POLICY_SNAPSHOT_JWT_HEADER).setIssuedAt(iat).setExpirationTime(exp).sign(getSigningKey(options.signingKey));
674
+ return {
675
+ token,
676
+ payload
677
+ };
678
+ }
679
+ async function verifyPolicySnapshotToken(params) {
680
+ const { token, options, tenantId } = params;
681
+ if (!options?.signingKey) return {
682
+ valid: false,
683
+ reason: 'missing'
684
+ };
685
+ if (!token) return {
686
+ valid: false,
687
+ reason: 'missing'
688
+ };
689
+ if (3 !== token.split('.').length) return {
690
+ valid: false,
691
+ reason: 'malformed'
692
+ };
693
+ try {
694
+ const { payload, protectedHeader } = await jwtVerify(token, getSigningKey(options.signingKey), {
695
+ issuer: resolveSnapshotIssuer(options),
696
+ audience: resolveSnapshotAudience({
697
+ options,
698
+ tenantId
699
+ })
700
+ });
701
+ const header = protectedHeader;
702
+ if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
703
+ valid: false,
704
+ reason: 'invalid'
705
+ };
706
+ if (!isPolicySnapshotPayload(payload)) return {
707
+ valid: false,
708
+ reason: 'invalid'
709
+ };
710
+ if (payload.sub !== payload.policyId) return {
711
+ valid: false,
712
+ reason: 'invalid'
713
+ };
714
+ if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
715
+ valid: false,
716
+ reason: 'invalid'
717
+ };
718
+ return {
719
+ valid: true,
720
+ payload
721
+ };
722
+ } catch (error) {
723
+ if (error instanceof errors.JWTExpired) return {
724
+ valid: false,
725
+ reason: 'expired'
726
+ };
727
+ return {
728
+ valid: false,
729
+ reason: 'invalid'
730
+ };
731
+ }
732
+ }
614
733
  function geo_normalizeHeader(value) {
615
734
  if (!value) return null;
616
735
  return Array.isArray(value) ? value[0] ?? null : value;
@@ -766,26 +885,247 @@ function getJurisdiction(location, options) {
766
885
  if (options.disableGeoLocation) return 'GDPR';
767
886
  return checkJurisdiction(location.countryCode, location.regionCode);
768
887
  }
888
+ async function policy_resolvePolicyDecision(params) {
889
+ return resolvePolicyDecision({
890
+ policies: params.policies,
891
+ countryCode: params.countryCode,
892
+ regionCode: params.regionCode,
893
+ jurisdiction: params.jurisdiction,
894
+ iabEnabled: params.iabEnabled
895
+ });
896
+ }
897
+ const DEFAULT_PROFILE = 'default';
898
+ const warnedKeys = new Set();
769
899
  function isSupportedBaseLanguage(lang) {
770
900
  return lang in baseTranslations;
771
901
  }
772
- function translations_getTranslationsData(acceptLanguage, customTranslations) {
773
- const supportedDefaultLanguages = Object.keys(baseTranslations);
774
- const supportedCustomLanguages = Object.keys(customTranslations || {});
775
- const supportedLanguages = [
776
- ...supportedDefaultLanguages,
777
- ...supportedCustomLanguages
902
+ function warnOnce(logger, key, message, metadata) {
903
+ if (!logger || warnedKeys.has(key)) return;
904
+ warnedKeys.add(key);
905
+ logger.warn(message, metadata);
906
+ }
907
+ function normalizeLanguage(value) {
908
+ if (!value) return;
909
+ const normalized = value.split(',')[0]?.split(';')[0]?.trim().toLowerCase();
910
+ if (!normalized) return;
911
+ return normalized.split('-')[0] ?? void 0;
912
+ }
913
+ function normalizeProfiles(params) {
914
+ const profiles = params.i18n?.messages;
915
+ const legacy = params.customTranslations;
916
+ if (profiles && Object.keys(profiles).length > 0) {
917
+ if (legacy && Object.keys(legacy).length > 0) warnOnce(params.logger, 'i18n.customTranslations.ignored', '`customTranslations` is deprecated and ignored when `i18n.messages` is configured.');
918
+ return profiles;
919
+ }
920
+ if (legacy && Object.keys(legacy).length > 0) {
921
+ warnOnce(params.logger, 'i18n.customTranslations.deprecated', '`customTranslations` is deprecated. Use `i18n.messages` instead.');
922
+ return {
923
+ [DEFAULT_PROFILE]: {
924
+ translations: legacy
925
+ }
926
+ };
927
+ }
928
+ return {};
929
+ }
930
+ function buildCandidates(input) {
931
+ const raw = [
932
+ {
933
+ language: input.language,
934
+ reason: 'profile_language'
935
+ },
936
+ {
937
+ language: input.fallbackLanguage,
938
+ reason: 'profile_fallback'
939
+ }
778
940
  ];
779
- const preferredLanguage = selectLanguage(supportedLanguages, {
941
+ const dedupe = new Set();
942
+ return raw.filter((candidate)=>{
943
+ const key = candidate.language;
944
+ if (dedupe.has(key)) return false;
945
+ dedupe.add(key);
946
+ return true;
947
+ });
948
+ }
949
+ function getProfileLanguages(profiles, profile) {
950
+ return Object.keys(profiles[profile]?.translations ?? {}).sort();
951
+ }
952
+ function getSelectableLanguages(input) {
953
+ return getProfileLanguages(input.profiles, input.profile);
954
+ }
955
+ function resolveFallbackLanguage(input) {
956
+ const configuredFallbackLanguage = normalizeLanguage(input.profile?.fallbackLanguage) ?? 'en';
957
+ const profileLanguages = Object.keys(input.profile?.translations ?? {}).sort();
958
+ if (profileLanguages.includes(configuredFallbackLanguage)) return configuredFallbackLanguage;
959
+ if (profileLanguages.includes('en')) return 'en';
960
+ return profileLanguages[0] ?? configuredFallbackLanguage;
961
+ }
962
+ function resolveActiveProfile(input) {
963
+ const requestedProfile = input.policyProfile ?? input.defaultProfile;
964
+ if (input.profiles[requestedProfile]) return requestedProfile;
965
+ if (input.policyProfile) warnOnce(input.logger, `i18n.profile.missing:${requestedProfile}`, `Policy i18n profile '${requestedProfile}' does not exist. Falling back to default profile '${input.defaultProfile}'.`);
966
+ return input.defaultProfile;
967
+ }
968
+ function translations_getTranslationsData(acceptLanguage, customTranslations, options) {
969
+ const profiles = normalizeProfiles({
970
+ customTranslations,
971
+ i18n: options?.i18n,
972
+ logger: options?.logger
973
+ });
974
+ const defaultProfile = options?.i18n?.defaultProfile ?? DEFAULT_PROFILE;
975
+ const profile = resolveActiveProfile({
976
+ profiles,
977
+ defaultProfile,
978
+ policyProfile: options?.policyI18n?.messageProfile,
979
+ logger: options?.logger
980
+ });
981
+ const configuredLanguages = Object.keys(profiles).length > 0 ? getSelectableLanguages({
982
+ profiles,
983
+ profile
984
+ }) : Object.keys(baseTranslations);
985
+ const fallbackLanguage = Object.keys(profiles).length > 0 ? resolveFallbackLanguage({
986
+ profile: profiles[profile]
987
+ }) : 'en';
988
+ const policyLanguage = normalizeLanguage(options?.policyI18n?.language);
989
+ const requestedLanguage = policyLanguage ?? selectLanguage(configuredLanguages, {
780
990
  header: acceptLanguage,
781
- fallback: 'en'
991
+ fallback: fallbackLanguage
992
+ });
993
+ const candidates = buildCandidates({
994
+ language: requestedLanguage,
995
+ fallbackLanguage
996
+ });
997
+ const selectedCandidate = candidates.find((candidate)=>!!profiles[profile]?.translations[candidate.language]);
998
+ if (selectedCandidate && 'profile_language' !== selectedCandidate.reason) warnOnce(options?.logger, `i18n.fallback:${profile}:${requestedLanguage}:${selectedCandidate.language}`, `Policy translation fallback used (${selectedCandidate.reason}).`, {
999
+ requestedProfile: profile,
1000
+ requestedLanguage,
1001
+ resolvedProfile: profile,
1002
+ resolvedLanguage: selectedCandidate.language
782
1003
  });
783
- const base = isSupportedBaseLanguage(preferredLanguage) ? baseTranslations[preferredLanguage] : baseTranslations.en;
784
- const custom = supportedCustomLanguages.includes(preferredLanguage) ? customTranslations?.[preferredLanguage] : {};
1004
+ let language = selectedCandidate?.language ?? requestedLanguage;
1005
+ if (!selectedCandidate && !isSupportedBaseLanguage(language)) {
1006
+ warnOnce(options?.logger, `i18n.base-fallback:${language}`, `No translation found for '${language}'. Falling back to base English translations.`);
1007
+ language = 'en';
1008
+ }
1009
+ const base = isSupportedBaseLanguage(language) ? baseTranslations[language] : baseTranslations.en;
1010
+ const custom = selectedCandidate ? profiles[profile]?.translations[selectedCandidate.language] : void 0;
785
1011
  const translations = custom ? deepMergeTranslations(base, custom) : base;
786
1012
  return {
787
1013
  translations: translations,
788
- language: preferredLanguage
1014
+ language
1015
+ };
1016
+ }
1017
+ function stripIabTranslations(translations) {
1018
+ const { iab: _iab, ...rest } = translations;
1019
+ return rest;
1020
+ }
1021
+ function resolveNoPolicyFallback() {
1022
+ return {
1023
+ id: 'no_banner',
1024
+ model: 'none',
1025
+ ui: {
1026
+ mode: 'none'
1027
+ }
1028
+ };
1029
+ }
1030
+ async function resolveInitPayload(request, options, logger) {
1031
+ const acceptLanguage = request.headers.get('accept-language') || 'en';
1032
+ const location = await getLocation(request, options);
1033
+ const jurisdiction = getJurisdiction(location, options);
1034
+ const hasExplicitPolicyPack = void 0 !== options.policyPacks;
1035
+ const isExplicitEmptyPolicyPack = hasExplicitPolicyPack && (options.policyPacks?.length ?? 0) === 0;
1036
+ const policyDecision = isExplicitEmptyPolicyPack ? void 0 : await policy_resolvePolicyDecision({
1037
+ policies: options.policyPacks,
1038
+ countryCode: location.countryCode,
1039
+ regionCode: location.regionCode,
1040
+ jurisdiction,
1041
+ iabEnabled: options.iab?.enabled === true
1042
+ });
1043
+ if (hasExplicitPolicyPack && !isExplicitEmptyPolicyPack && !policyDecision) logger?.warn('Policy packs configured but no policy matched', {
1044
+ country: location.countryCode,
1045
+ region: location.regionCode
1046
+ });
1047
+ const resolvedPolicy = hasExplicitPolicyPack ? policyDecision?.policy ?? resolveNoPolicyFallback() : void 0;
1048
+ const iabOptions = options.iab;
1049
+ const shouldIncludeIabPayload = iabOptions?.enabled === true && (!hasExplicitPolicyPack || resolvedPolicy?.model === 'iab');
1050
+ const translationsResult = translations_getTranslationsData(acceptLanguage, options.customTranslations, {
1051
+ i18n: options.i18n,
1052
+ policyI18n: resolvedPolicy?.i18n,
1053
+ logger
1054
+ });
1055
+ const responseTranslations = shouldIncludeIabPayload ? translationsResult : {
1056
+ ...translationsResult,
1057
+ translations: stripIabTranslations(translationsResult.translations)
1058
+ };
1059
+ let gvl = null;
1060
+ if (shouldIncludeIabPayload && iabOptions) {
1061
+ const language = translationsResult.language.split('-')[0] || 'en';
1062
+ const gvlResolver = createGVLResolver({
1063
+ appName: options.appName || 'c15t',
1064
+ bundled: iabOptions.bundled,
1065
+ cacheAdapter: options.cache?.adapter,
1066
+ vendorIds: iabOptions.vendorIds,
1067
+ endpoint: iabOptions.endpoint
1068
+ });
1069
+ gvl = await gvlResolver.get(language);
1070
+ }
1071
+ const customVendors = shouldIncludeIabPayload ? iabOptions?.customVendors : void 0;
1072
+ const snapshot = policyDecision ? await createPolicySnapshotToken({
1073
+ options: options.policySnapshot,
1074
+ tenantId: options.tenantId,
1075
+ policyId: policyDecision.policy.id,
1076
+ fingerprint: policyDecision.fingerprint,
1077
+ matchedBy: policyDecision.matchedBy,
1078
+ country: location?.countryCode ?? null,
1079
+ region: location?.regionCode ?? null,
1080
+ jurisdiction,
1081
+ language: translationsResult.language,
1082
+ model: policyDecision.policy.model,
1083
+ policyI18n: policyDecision.policy.i18n,
1084
+ expiryDays: policyDecision.policy.consent?.expiryDays,
1085
+ scopeMode: policyDecision.policy.consent?.scopeMode,
1086
+ uiMode: policyDecision.policy.ui?.mode,
1087
+ bannerUi: policyDecision.policy.ui?.banner,
1088
+ dialogUi: policyDecision.policy.ui?.dialog,
1089
+ categories: policyDecision.policy.consent?.categories,
1090
+ preselectedCategories: policyDecision.policy.consent?.preselectedCategories,
1091
+ gpc: policyDecision.policy.consent?.gpc,
1092
+ proofConfig: policyDecision.policy.proof
1093
+ }) : void 0;
1094
+ const gpc = '1' === request.headers.get('sec-gpc');
1095
+ getMetrics()?.recordInit({
1096
+ jurisdiction,
1097
+ country: location?.countryCode ?? void 0,
1098
+ region: location?.regionCode ?? void 0,
1099
+ gpc
1100
+ });
1101
+ return {
1102
+ jurisdiction,
1103
+ location,
1104
+ translations: responseTranslations,
1105
+ branding: options.branding || 'c15t',
1106
+ ...shouldIncludeIabPayload && {
1107
+ gvl,
1108
+ customVendors
1109
+ },
1110
+ ...resolvedPolicy && {
1111
+ policy: resolvedPolicy
1112
+ },
1113
+ ...policyDecision && {
1114
+ policyDecision: {
1115
+ policyId: policyDecision.policy.id,
1116
+ fingerprint: policyDecision.fingerprint,
1117
+ matchedBy: policyDecision.matchedBy,
1118
+ country: location.countryCode,
1119
+ region: location.regionCode,
1120
+ jurisdiction
1121
+ }
1122
+ },
1123
+ ...snapshot?.token && {
1124
+ policySnapshotToken: snapshot.token
1125
+ },
1126
+ ...shouldIncludeIabPayload && iabOptions?.cmpId != null && {
1127
+ cmpId: iabOptions.cmpId
1128
+ }
789
1129
  };
790
1130
  }
791
1131
  const createInitRoute = (options)=>{
@@ -798,7 +1138,7 @@ const createInitRoute = (options)=>{
798
1138
  - **Location** – User's location (null if geo-location is disabled)
799
1139
  - **Translations** – Consent manager copy (from \`Accept-Language\` header)
800
1140
  - **Branding** – Configured branding key
801
- - **GVL** – Global Vendor List when enabled
1141
+ - **GVL** – Global Vendor List when IAB is active for the request
802
1142
 
803
1143
  Use for geo-targeted consent banners and regional compliance.`,
804
1144
  tags: [
@@ -815,42 +1155,9 @@ Use for geo-targeted consent banners and regional compliance.`,
815
1155
  }
816
1156
  }
817
1157
  }), async (c)=>{
818
- const request = c.req.raw;
819
- const acceptLanguage = request.headers.get('accept-language') || 'en';
820
- const location = await getLocation(request, options);
821
- const jurisdiction = getJurisdiction(location, options);
822
- const translationsResult = translations_getTranslationsData(acceptLanguage, options.customTranslations);
823
- let gvl = null;
824
- if (options.iab?.enabled) {
825
- const language = translationsResult.language.split('-')[0] || 'en';
826
- const gvlResolver = createGVLResolver({
827
- appName: options.appName || 'c15t',
828
- bundled: options.iab.bundled,
829
- cacheAdapter: options.cache?.adapter,
830
- vendorIds: options.iab.vendorIds,
831
- endpoint: options.iab.endpoint
832
- });
833
- gvl = await gvlResolver.get(language);
834
- }
835
- const customVendors = options.iab?.customVendors;
836
- const gpc = '1' === request.headers.get('sec-gpc');
837
- getMetrics()?.recordInit({
838
- jurisdiction,
839
- country: location?.countryCode ?? void 0,
840
- region: location?.regionCode ?? void 0,
841
- gpc
842
- });
843
- return c.json({
844
- jurisdiction,
845
- location,
846
- translations: translationsResult,
847
- branding: options.branding || 'c15t',
848
- gvl,
849
- customVendors,
850
- ...options.iab?.cmpId != null && {
851
- cmpId: options.iab.cmpId
852
- }
853
- });
1158
+ const ctx = c.get('c15tContext');
1159
+ const payload = await resolveInitPayload(c.req.raw, options, ctx?.logger);
1160
+ return c.json(payload);
854
1161
  });
855
1162
  return app;
856
1163
  };
@@ -1206,6 +1513,119 @@ const patchSubjectHandler = async (c)=>{
1206
1513
  });
1207
1514
  }
1208
1515
  };
1516
+ function buildRuntimeDecisionDedupeKey(input) {
1517
+ return [
1518
+ input.tenantId ?? 'default',
1519
+ input.fingerprint,
1520
+ input.matchedBy,
1521
+ input.countryCode ?? 'none',
1522
+ input.regionCode ?? 'none',
1523
+ input.jurisdiction,
1524
+ input.language ?? 'none'
1525
+ ].join('|');
1526
+ }
1527
+ function buildDecisionPayload(params) {
1528
+ const { tenantId, snapshot, decision, location, jurisdiction, language, proofConfig } = params;
1529
+ if (snapshot?.valid && snapshot.payload) {
1530
+ const sp = snapshot.payload;
1531
+ return {
1532
+ tenantId,
1533
+ policyId: sp.policyId,
1534
+ fingerprint: sp.fingerprint,
1535
+ matchedBy: sp.matchedBy,
1536
+ countryCode: sp.country,
1537
+ regionCode: sp.region,
1538
+ jurisdiction: sp.jurisdiction,
1539
+ language: sp.language,
1540
+ model: sp.model,
1541
+ policyI18n: sp.policyI18n,
1542
+ uiMode: sp.uiMode,
1543
+ bannerUi: sp.bannerUi,
1544
+ dialogUi: sp.dialogUi,
1545
+ categories: sp.categories,
1546
+ preselectedCategories: sp.preselectedCategories,
1547
+ proofConfig: sp.proofConfig,
1548
+ dedupeKey: buildRuntimeDecisionDedupeKey({
1549
+ tenantId,
1550
+ fingerprint: sp.fingerprint,
1551
+ matchedBy: sp.matchedBy,
1552
+ countryCode: sp.country,
1553
+ regionCode: sp.region,
1554
+ jurisdiction: sp.jurisdiction,
1555
+ language: sp.language
1556
+ }),
1557
+ source: 'snapshot_token'
1558
+ };
1559
+ }
1560
+ if (decision) return {
1561
+ tenantId,
1562
+ policyId: decision.policy.id,
1563
+ fingerprint: decision.fingerprint,
1564
+ matchedBy: decision.matchedBy,
1565
+ countryCode: location.countryCode,
1566
+ regionCode: location.regionCode,
1567
+ jurisdiction,
1568
+ language,
1569
+ model: decision.policy.model,
1570
+ policyI18n: decision.policy.i18n,
1571
+ uiMode: decision.policy.ui?.mode,
1572
+ bannerUi: decision.policy.ui?.banner,
1573
+ dialogUi: decision.policy.ui?.dialog,
1574
+ categories: decision.policy.consent?.categories,
1575
+ preselectedCategories: decision.policy.consent?.preselectedCategories,
1576
+ proofConfig,
1577
+ dedupeKey: buildRuntimeDecisionDedupeKey({
1578
+ tenantId,
1579
+ fingerprint: decision.fingerprint,
1580
+ matchedBy: decision.matchedBy,
1581
+ countryCode: location.countryCode,
1582
+ regionCode: location.regionCode,
1583
+ jurisdiction,
1584
+ language
1585
+ }),
1586
+ source: 'write_time_fallback'
1587
+ };
1588
+ }
1589
+ function parseLanguageFromHeader(header) {
1590
+ if (!header) return;
1591
+ const firstLanguage = header.split(',')[0]?.split(';')[0]?.trim();
1592
+ if (!firstLanguage) return;
1593
+ return firstLanguage.split('-')[0]?.toLowerCase();
1594
+ }
1595
+ function resolveSnapshotFailureMode(ctx) {
1596
+ return ctx.policySnapshot?.onValidationFailure ?? 'reject';
1597
+ }
1598
+ function buildSnapshotHttpException(reason) {
1599
+ switch(reason){
1600
+ case 'missing':
1601
+ return new HTTPException(409, {
1602
+ message: 'Policy snapshot token is required',
1603
+ cause: {
1604
+ code: 'POLICY_SNAPSHOT_REQUIRED'
1605
+ }
1606
+ });
1607
+ case 'expired':
1608
+ return new HTTPException(409, {
1609
+ message: 'Policy snapshot token has expired',
1610
+ cause: {
1611
+ code: 'POLICY_SNAPSHOT_EXPIRED'
1612
+ }
1613
+ });
1614
+ case 'malformed':
1615
+ case 'invalid':
1616
+ return new HTTPException(409, {
1617
+ message: 'Policy snapshot token is invalid',
1618
+ cause: {
1619
+ code: 'POLICY_SNAPSHOT_INVALID'
1620
+ }
1621
+ });
1622
+ default:
1623
+ {
1624
+ const _exhaustive = reason;
1625
+ throw new Error(`Unhandled policy snapshot verification failure reason: ${_exhaustive}`);
1626
+ }
1627
+ }
1628
+ }
1209
1629
  const postSubjectHandler = async (c)=>{
1210
1630
  const ctx = c.get('c15tContext');
1211
1631
  const logger = ctx.logger;
@@ -1217,9 +1637,6 @@ const postSubjectHandler = async (c)=>{
1217
1637
  const givenAt = new Date(givenAtEpoch);
1218
1638
  const rawConsentAction = 'consentAction' in input ? input.consentAction : void 0;
1219
1639
  let derivedConsentAction;
1220
- if ('all' === rawConsentAction) derivedConsentAction = 'accept_all';
1221
- else if ('necessary' === rawConsentAction) derivedConsentAction = 'opt-out' === input.jurisdictionModel ? 'opt_out' : 'reject_all';
1222
- else if ('custom' === rawConsentAction) derivedConsentAction = 'custom';
1223
1640
  logger.debug('Request parameters', {
1224
1641
  type,
1225
1642
  subjectId,
@@ -1228,6 +1645,50 @@ const postSubjectHandler = async (c)=>{
1228
1645
  domain
1229
1646
  });
1230
1647
  try {
1648
+ 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.');
1649
+ const request = c.req.raw ?? new Request('https://c15t.local/subjects');
1650
+ const acceptLanguage = request.headers.get('accept-language');
1651
+ const requestLanguage = parseLanguageFromHeader(acceptLanguage);
1652
+ const location = await getLocation(request, ctx);
1653
+ const resolvedJurisdiction = getJurisdiction(location, ctx);
1654
+ const snapshotVerification = await verifyPolicySnapshotToken({
1655
+ token: input.policySnapshotToken,
1656
+ options: ctx.policySnapshot,
1657
+ tenantId: ctx.tenantId
1658
+ });
1659
+ const hasValidSnapshot = snapshotVerification.valid;
1660
+ const snapshotPayload = snapshotVerification.valid ? snapshotVerification.payload : null;
1661
+ const shouldRequireSnapshot = !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
1662
+ if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(snapshotVerification.reason);
1663
+ const resolvedPolicyDecision = hasValidSnapshot ? void 0 : await policy_resolvePolicyDecision({
1664
+ policies: ctx.policyPacks,
1665
+ countryCode: location.countryCode,
1666
+ regionCode: location.regionCode,
1667
+ jurisdiction: resolvedJurisdiction,
1668
+ iabEnabled: ctx.iab?.enabled === true
1669
+ });
1670
+ const effectivePolicy = hasValidSnapshot && snapshotPayload ? {
1671
+ id: snapshotPayload.policyId,
1672
+ model: snapshotPayload.model,
1673
+ i18n: snapshotPayload.policyI18n,
1674
+ consent: {
1675
+ expiryDays: snapshotPayload.expiryDays,
1676
+ scopeMode: snapshotPayload.scopeMode,
1677
+ categories: snapshotPayload.categories,
1678
+ preselectedCategories: snapshotPayload.preselectedCategories,
1679
+ gpc: snapshotPayload.gpc
1680
+ },
1681
+ ui: {
1682
+ mode: snapshotPayload.uiMode,
1683
+ banner: snapshotPayload.bannerUi,
1684
+ dialog: snapshotPayload.dialogUi
1685
+ },
1686
+ proof: snapshotPayload.proofConfig
1687
+ } : resolvedPolicyDecision?.policy;
1688
+ const effectiveModel = effectivePolicy?.model ?? ('opt-in' === input.jurisdictionModel || 'opt-out' === input.jurisdictionModel || 'iab' === input.jurisdictionModel ? input.jurisdictionModel : void 0);
1689
+ if ('all' === rawConsentAction) derivedConsentAction = 'accept_all';
1690
+ else if ('necessary' === rawConsentAction) derivedConsentAction = 'opt-out' === effectiveModel ? 'opt_out' : 'reject_all';
1691
+ else if ('custom' === rawConsentAction) derivedConsentAction = 'custom';
1231
1692
  const subject = await registry.findOrCreateSubject({
1232
1693
  subjectId,
1233
1694
  externalSubjectId,
@@ -1254,6 +1715,7 @@ const postSubjectHandler = async (c)=>{
1254
1715
  });
1255
1716
  let policyId;
1256
1717
  let purposeIds = [];
1718
+ let appliedPreferences;
1257
1719
  const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
1258
1720
  if (inputPolicyId) {
1259
1721
  policyId = inputPolicyId;
@@ -1286,20 +1748,66 @@ const postSubjectHandler = async (c)=>{
1286
1748
  policyId = policy.id;
1287
1749
  }
1288
1750
  if (preferences) {
1289
- const consentedPurposes = Object.entries(preferences).filter(([_, isConsented])=>isConsented).map(([purposeCode])=>purposeCode);
1751
+ const allowedCategories = effectivePolicy?.consent?.categories;
1752
+ const effectiveScopeMode = effectivePolicy?.consent?.scopeMode ?? 'permissive';
1753
+ const hasWildcardCategoryScope = allowedCategories?.includes('*') === true;
1754
+ const appliedPreferenceEntries = Object.entries(preferences);
1755
+ let filteredAppliedPreferenceEntries = appliedPreferenceEntries;
1756
+ if (allowedCategories && allowedCategories.length > 0 && !hasWildcardCategoryScope) {
1757
+ const disallowed = appliedPreferenceEntries.map(([purpose])=>purpose).filter((purpose)=>!allowedCategories.includes(purpose));
1758
+ filteredAppliedPreferenceEntries = appliedPreferenceEntries.filter(([purpose])=>allowedCategories.includes(purpose));
1759
+ if (disallowed.length > 0 && 'strict' === effectiveScopeMode) throw new HTTPException(400, {
1760
+ message: 'Preferences include categories not allowed by policy',
1761
+ cause: {
1762
+ code: 'PURPOSE_NOT_ALLOWED',
1763
+ disallowed
1764
+ }
1765
+ });
1766
+ }
1767
+ appliedPreferences = Object.fromEntries(filteredAppliedPreferenceEntries);
1768
+ const filteredConsentedPurposeCodes = filteredAppliedPreferenceEntries.filter(([_, isConsented])=>isConsented).map(([purposeCode])=>purposeCode);
1290
1769
  logger.debug('Consented purposes', {
1291
- consentedPurposes
1770
+ consentedPurposes: filteredConsentedPurposeCodes
1292
1771
  });
1293
- const purposesRaw = await Promise.all(consentedPurposes.map((purposeCode)=>registry.findOrCreateConsentPurposeByCode(purposeCode)));
1772
+ const purposesRaw = await Promise.all(filteredConsentedPurposeCodes.map((purposeCode)=>registry.findOrCreateConsentPurposeByCode(purposeCode)));
1294
1773
  const purposes = purposesRaw.map((purpose)=>purpose?.id ?? null).filter((id)=>Boolean(id));
1295
1774
  logger.debug('Filtered purposes', {
1296
1775
  purposes
1297
1776
  });
1298
1777
  if (0 === purposes.length) logger.warn('No valid purpose IDs found after filtering. Using empty list.', {
1299
- consentedPurposes
1778
+ consentedPurposes: filteredConsentedPurposeCodes
1300
1779
  });
1301
1780
  purposeIds = purposes;
1302
1781
  }
1782
+ const expiryDays = effectivePolicy?.consent?.expiryDays;
1783
+ const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
1784
+ const proofConfig = effectivePolicy?.proof;
1785
+ const shouldStoreIp = proofConfig?.storeIp ?? true;
1786
+ const shouldStoreUserAgent = proofConfig?.storeUserAgent ?? true;
1787
+ const shouldStoreLanguage = proofConfig?.storeLanguage ?? false;
1788
+ const effectiveLanguage = (snapshotPayload?.language && hasValidSnapshot ? snapshotPayload.language : requestLanguage) ?? void 0;
1789
+ const metadataWithPolicy = {
1790
+ ...metadata ?? {},
1791
+ ...shouldStoreLanguage && effectiveLanguage ? {
1792
+ policyLanguage: effectiveLanguage
1793
+ } : {}
1794
+ };
1795
+ const effectiveJurisdiction = hasValidSnapshot && snapshotPayload ? snapshotPayload.jurisdiction : resolvedJurisdiction;
1796
+ const decisionPayload = buildDecisionPayload({
1797
+ tenantId: ctx.tenantId,
1798
+ snapshot: hasValidSnapshot && snapshotPayload ? {
1799
+ valid: true,
1800
+ payload: snapshotPayload
1801
+ } : null,
1802
+ decision: resolvedPolicyDecision,
1803
+ location: {
1804
+ countryCode: location.countryCode,
1805
+ regionCode: location.regionCode
1806
+ },
1807
+ jurisdiction: resolvedJurisdiction,
1808
+ language: effectiveLanguage,
1809
+ proofConfig
1810
+ });
1303
1811
  const existingConsent = await db.findFirst('consent', {
1304
1812
  where: (b)=>b.and(b('subjectId', '=', subject.id), b('domainId', '=', domainRecord.id), b('policyId', '=', policyId), b('givenAt', '=', givenAt))
1305
1813
  });
@@ -1314,6 +1822,7 @@ const postSubjectHandler = async (c)=>{
1314
1822
  domain: domainRecord.name,
1315
1823
  type,
1316
1824
  metadata,
1825
+ appliedPreferences,
1317
1826
  uiSource: input.uiSource,
1318
1827
  givenAt: existingConsent.givenAt
1319
1828
  });
@@ -1325,6 +1834,42 @@ const postSubjectHandler = async (c)=>{
1325
1834
  policyId,
1326
1835
  purposeIds
1327
1836
  });
1837
+ const runtimePolicyDecision = decisionPayload ? await tx.findFirst('runtimePolicyDecision', {
1838
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1839
+ }) ?? await tx.create('runtimePolicyDecision', {
1840
+ id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
1841
+ tenantId: decisionPayload.tenantId,
1842
+ policyId: decisionPayload.policyId,
1843
+ fingerprint: decisionPayload.fingerprint,
1844
+ matchedBy: decisionPayload.matchedBy,
1845
+ countryCode: decisionPayload.countryCode,
1846
+ regionCode: decisionPayload.regionCode,
1847
+ jurisdiction: decisionPayload.jurisdiction,
1848
+ language: decisionPayload.language,
1849
+ model: decisionPayload.model,
1850
+ policyI18n: decisionPayload.policyI18n ? {
1851
+ json: decisionPayload.policyI18n
1852
+ } : void 0,
1853
+ uiMode: decisionPayload.uiMode,
1854
+ bannerUi: decisionPayload.bannerUi ? {
1855
+ json: decisionPayload.bannerUi
1856
+ } : void 0,
1857
+ dialogUi: decisionPayload.dialogUi ? {
1858
+ json: decisionPayload.dialogUi
1859
+ } : void 0,
1860
+ categories: decisionPayload.categories ? {
1861
+ json: decisionPayload.categories
1862
+ } : void 0,
1863
+ preselectedCategories: decisionPayload.preselectedCategories ? {
1864
+ json: decisionPayload.preselectedCategories
1865
+ } : void 0,
1866
+ proofConfig: decisionPayload.proofConfig ? {
1867
+ json: decisionPayload.proofConfig
1868
+ } : void 0,
1869
+ dedupeKey: decisionPayload.dedupeKey
1870
+ }).catch(async ()=>tx.findFirst('runtimePolicyDecision', {
1871
+ where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
1872
+ })) : void 0;
1328
1873
  const consentRecord = await tx.create('consent', {
1329
1874
  id: await generateUniqueId(tx, 'consent', ctx),
1330
1875
  subjectId: subject.id,
@@ -1333,17 +1878,20 @@ const postSubjectHandler = async (c)=>{
1333
1878
  purposeIds: {
1334
1879
  json: purposeIds
1335
1880
  },
1336
- metadata: metadata ? {
1337
- json: metadata
1881
+ metadata: Object.keys(metadataWithPolicy).length > 0 ? {
1882
+ json: metadataWithPolicy
1338
1883
  } : void 0,
1339
- ipAddress: ctx.ipAddress,
1340
- userAgent: ctx.userAgent,
1341
- jurisdiction: input.jurisdiction,
1342
- jurisdictionModel: input.jurisdictionModel,
1884
+ ipAddress: shouldStoreIp ? ctx.ipAddress : null,
1885
+ userAgent: shouldStoreUserAgent ? ctx.userAgent : null,
1886
+ jurisdiction: effectiveJurisdiction,
1887
+ jurisdictionModel: effectiveModel,
1343
1888
  tcString: input.tcString,
1344
1889
  uiSource: input.uiSource,
1345
1890
  consentAction: derivedConsentAction,
1346
- givenAt
1891
+ givenAt,
1892
+ validUntil,
1893
+ runtimePolicyDecisionId: runtimePolicyDecision?.id,
1894
+ runtimePolicySource: decisionPayload?.source
1347
1895
  });
1348
1896
  logger.debug('Created consent', {
1349
1897
  consentRecord: consentRecord.id
@@ -1362,7 +1910,7 @@ const postSubjectHandler = async (c)=>{
1362
1910
  });
1363
1911
  const metrics = getMetrics();
1364
1912
  if (metrics) {
1365
- const jurisdiction = input.jurisdiction;
1913
+ const jurisdiction = effectiveJurisdiction;
1366
1914
  metrics.recordConsentCreated({
1367
1915
  type,
1368
1916
  jurisdiction
@@ -1384,6 +1932,7 @@ const postSubjectHandler = async (c)=>{
1384
1932
  domain: domainRecord.name,
1385
1933
  type,
1386
1934
  metadata,
1935
+ appliedPreferences,
1387
1936
  uiSource: input.uiSource,
1388
1937
  givenAt: result.consent.givenAt
1389
1938
  });