@learncard/network-brain-service 4.1.2 → 5.0.0

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 (375) hide show
  1. package/.env.example +14 -0
  2. package/CHANGELOG.md +164 -0
  3. package/README.md +27 -1
  4. package/bootstrapLambda.test.ts +70 -32
  5. package/config/config.dev.json +1 -0
  6. package/config/config.example.json +2 -1
  7. package/config/config.production.json +1 -0
  8. package/config/config.scouts.dev.json +1 -0
  9. package/config/config.scouts.production.json +1 -0
  10. package/consentFlowDeadlineLambda.test.ts +108 -0
  11. package/consentFlowDeadlineLambda.ts +18 -0
  12. package/consentFlowDeadlineLambdaApp.ts +14 -0
  13. package/dist/accesslayer/boost/create.d.ts +8 -2
  14. package/dist/accesslayer/boost/create.d.ts.map +1 -1
  15. package/dist/accesslayer/campaign/audience.d.ts +4 -4
  16. package/dist/accesslayer/campaign/audience.d.ts.map +1 -1
  17. package/dist/accesslayer/campaign/create.d.ts +1 -1
  18. package/dist/accesslayer/campaign/create.d.ts.map +1 -1
  19. package/dist/accesslayer/campaign/delivery.d.ts +5 -0
  20. package/dist/accesslayer/campaign/delivery.d.ts.map +1 -0
  21. package/dist/accesslayer/campaign/dispatch.d.ts +5 -0
  22. package/dist/accesslayer/campaign/dispatch.d.ts.map +1 -0
  23. package/dist/accesslayer/campaign/dispatch.test.d.ts +2 -0
  24. package/dist/accesslayer/campaign/dispatch.test.d.ts.map +1 -0
  25. package/dist/accesslayer/campaign/email.d.ts +10 -0
  26. package/dist/accesslayer/campaign/email.d.ts.map +1 -0
  27. package/dist/accesslayer/campaign/email.test.d.ts +2 -0
  28. package/dist/accesslayer/campaign/email.test.d.ts.map +1 -0
  29. package/dist/accesslayer/campaign/preferences.d.ts +5 -0
  30. package/dist/accesslayer/campaign/preferences.d.ts.map +1 -0
  31. package/dist/accesslayer/campaign/read.d.ts.map +1 -1
  32. package/dist/accesslayer/campaign/test.d.ts +5 -0
  33. package/dist/accesslayer/campaign/test.d.ts.map +1 -0
  34. package/dist/accesslayer/campaign/test.test.d.ts +2 -0
  35. package/dist/accesslayer/campaign/test.test.d.ts.map +1 -0
  36. package/dist/accesslayer/campaign/update.d.ts.map +1 -1
  37. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  38. package/dist/accesslayer/consentflowcontract/delete.d.ts +1 -1
  39. package/dist/accesslayer/consentflowcontract/delete.d.ts.map +1 -1
  40. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  41. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +2 -1
  42. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  43. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +19 -4
  44. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  45. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +6 -1
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  47. package/dist/accesslayer/consentflowcontract/versions.d.ts +68 -0
  48. package/dist/accesslayer/consentflowcontract/versions.d.ts.map +1 -0
  49. package/dist/accesslayer/credential/create.d.ts +3 -1
  50. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  51. package/dist/accesslayer/credential/delete.d.ts +1 -0
  52. package/dist/accesslayer/credential/delete.d.ts.map +1 -1
  53. package/dist/accesslayer/credential/relationships/create.d.ts +15 -3
  54. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  55. package/dist/accesslayer/credential/relationships/read.d.ts +2 -0
  56. package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
  57. package/dist/accesslayer/credential/relationships/update.d.ts.map +1 -1
  58. package/dist/accesslayer/credential-refresh/read.d.ts +1 -0
  59. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  60. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  61. package/dist/accesslayer/inbox-credential/create.d.ts +1 -0
  62. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  63. package/dist/accesslayer/inbox-credential/refresh.d.ts +7 -0
  64. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -1
  65. package/dist/app.d.ts +11 -1
  66. package/dist/app.d.ts.map +1 -1
  67. package/dist/cache/app-store.caches.d.ts +61 -1
  68. package/dist/cache/app-store.caches.d.ts.map +1 -1
  69. package/dist/campaign-unsubscribe.d.ts +7 -0
  70. package/dist/campaign-unsubscribe.d.ts.map +1 -0
  71. package/dist/campaign-unsubscribe.test.d.ts +2 -0
  72. package/dist/campaign-unsubscribe.test.d.ts.map +1 -0
  73. package/dist/config/environment.d.ts +42 -0
  74. package/dist/config/environment.d.ts.map +1 -1
  75. package/dist/config/stageConfig.d.ts +4 -0
  76. package/dist/config/stageConfig.d.ts.map +1 -1
  77. package/dist/credential-refresh.d.ts.map +1 -1
  78. package/dist/helpers/boost.helpers.d.ts +4 -1
  79. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  80. package/dist/helpers/campaign-app-origin.helpers.d.ts +2 -0
  81. package/dist/helpers/campaign-app-origin.helpers.d.ts.map +1 -0
  82. package/dist/helpers/campaign-email.helpers.d.ts +13 -0
  83. package/dist/helpers/campaign-email.helpers.d.ts.map +1 -0
  84. package/dist/helpers/campaign-notifications.helpers.d.ts +7 -0
  85. package/dist/helpers/campaign-notifications.helpers.d.ts.map +1 -0
  86. package/dist/helpers/campaign-queue.helpers.d.ts +7 -0
  87. package/dist/helpers/campaign-queue.helpers.d.ts.map +1 -0
  88. package/dist/helpers/campaign-queue.helpers.test.d.ts +2 -0
  89. package/dist/helpers/campaign-queue.helpers.test.d.ts.map +1 -0
  90. package/dist/helpers/campaign-unsubscribe-page.d.ts +2 -0
  91. package/dist/helpers/campaign-unsubscribe-page.d.ts.map +1 -0
  92. package/dist/helpers/campaign-unsubscribe.helpers.d.ts +7 -0
  93. package/dist/helpers/campaign-unsubscribe.helpers.d.ts.map +1 -0
  94. package/dist/helpers/campaign-webhook.helpers.test.d.ts +2 -0
  95. package/dist/helpers/campaign-webhook.helpers.test.d.ts.map +1 -0
  96. package/dist/helpers/consentflow-deadline.helpers.d.ts +5 -0
  97. package/dist/helpers/consentflow-deadline.helpers.d.ts.map +1 -0
  98. package/dist/helpers/consentflow-legacy-migration.helpers.d.ts +2 -0
  99. package/dist/helpers/consentflow-legacy-migration.helpers.d.ts.map +1 -0
  100. package/dist/helpers/contract-events.helpers.d.ts.map +1 -1
  101. package/dist/helpers/contract.helpers.d.ts +1 -1
  102. package/dist/helpers/contract.helpers.d.ts.map +1 -1
  103. package/dist/helpers/credential-display.helpers.d.ts +5 -0
  104. package/dist/helpers/credential-display.helpers.d.ts.map +1 -0
  105. package/dist/helpers/credential-display.helpers.test.d.ts +2 -0
  106. package/dist/helpers/credential-display.helpers.test.d.ts.map +1 -0
  107. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -1
  108. package/dist/helpers/credential-refresh-materiality.helpers.d.ts +4 -0
  109. package/dist/helpers/credential-refresh-materiality.helpers.d.ts.map +1 -1
  110. package/dist/helpers/credential-refresh.helpers.d.ts +30 -1
  111. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  112. package/dist/helpers/credential-status-mutation.helpers.d.ts +5 -0
  113. package/dist/helpers/credential-status-mutation.helpers.d.ts.map +1 -0
  114. package/dist/helpers/credential-status-mutation.helpers.test.d.ts +2 -0
  115. package/dist/helpers/credential-status-mutation.helpers.test.d.ts.map +1 -0
  116. package/dist/helpers/credential.helpers.d.ts +1 -1
  117. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  118. package/dist/helpers/inbox-refresh.helpers.d.ts +4 -2
  119. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -1
  120. package/dist/helpers/inbox.helpers.d.ts +1 -0
  121. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  122. package/dist/helpers/learnCard.helpers.d.ts +1 -0
  123. package/dist/helpers/learnCard.helpers.d.ts.map +1 -1
  124. package/dist/helpers/managed-status.helpers.d.ts +7 -0
  125. package/dist/helpers/managed-status.helpers.d.ts.map +1 -0
  126. package/dist/helpers/managed-status.helpers.test.d.ts +2 -0
  127. package/dist/helpers/managed-status.helpers.test.d.ts.map +1 -0
  128. package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts +2 -0
  129. package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts.map +1 -0
  130. package/dist/helpers/notification-webhook-fetch.helpers.d.ts +13 -0
  131. package/dist/helpers/notification-webhook-fetch.helpers.d.ts.map +1 -0
  132. package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts +2 -0
  133. package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts.map +1 -0
  134. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  135. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  136. package/dist/helpers/refresh-send-intent.helpers.d.ts +13 -5
  137. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -1
  138. package/dist/helpers/refresh-send-intent.helpers.test.d.ts +2 -0
  139. package/dist/helpers/refresh-send-intent.helpers.test.d.ts.map +1 -0
  140. package/dist/helpers/send-contract.helpers.d.ts +11 -0
  141. package/dist/helpers/send-contract.helpers.d.ts.map +1 -0
  142. package/dist/helpers/status-list.helpers.d.ts +12 -1
  143. package/dist/helpers/status-list.helpers.d.ts.map +1 -1
  144. package/dist/index.js +2 -2
  145. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  146. package/dist/models/ConsentFlowContractVersion.d.ts +12 -0
  147. package/dist/models/ConsentFlowContractVersion.d.ts.map +1 -0
  148. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  149. package/dist/models/Credential.d.ts +2 -0
  150. package/dist/models/Credential.d.ts.map +1 -1
  151. package/dist/models/CredentialRefresh.d.ts +5 -0
  152. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  153. package/dist/models/Integration.d.ts +1 -1
  154. package/dist/models/Profile.d.ts.map +1 -1
  155. package/dist/models/campaign-constraints.d.ts.map +1 -1
  156. package/dist/models/index.d.ts +1 -0
  157. package/dist/models/index.d.ts.map +1 -1
  158. package/dist/openapi.d.ts.map +1 -1
  159. package/dist/publish-metafile.json +1 -1
  160. package/dist/routes/activity.d.ts +15 -5
  161. package/dist/routes/activity.d.ts.map +1 -1
  162. package/dist/routes/app-store.d.ts +15 -5
  163. package/dist/routes/app-store.d.ts.map +1 -1
  164. package/dist/routes/auth-grants.d.ts +12 -2
  165. package/dist/routes/auth-grants.d.ts.map +1 -1
  166. package/dist/routes/bench.d.ts +11 -1
  167. package/dist/routes/bench.d.ts.map +1 -1
  168. package/dist/routes/boosts.d.ts +70 -15
  169. package/dist/routes/boosts.d.ts.map +1 -1
  170. package/dist/routes/campaigns.d.ts +183 -51
  171. package/dist/routes/campaigns.d.ts.map +1 -1
  172. package/dist/routes/claim-hooks.d.ts +11 -1
  173. package/dist/routes/claim-hooks.d.ts.map +1 -1
  174. package/dist/routes/contact-methods.d.ts +11 -1
  175. package/dist/routes/contact-methods.d.ts.map +1 -1
  176. package/dist/routes/contracts.d.ts +374 -8
  177. package/dist/routes/contracts.d.ts.map +1 -1
  178. package/dist/routes/credential-input-privacy.test.d.ts +2 -0
  179. package/dist/routes/credential-input-privacy.test.d.ts.map +1 -0
  180. package/dist/routes/credential-refreshes.d.ts +210 -1
  181. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  182. package/dist/routes/credential-share-previews.d.ts +11 -1
  183. package/dist/routes/credential-share-previews.d.ts.map +1 -1
  184. package/dist/routes/credentials.d.ts +132 -3
  185. package/dist/routes/credentials.d.ts.map +1 -1
  186. package/dist/routes/did-metadata.d.ts +11 -1
  187. package/dist/routes/did-metadata.d.ts.map +1 -1
  188. package/dist/routes/federation.d.ts +11 -1
  189. package/dist/routes/federation.d.ts.map +1 -1
  190. package/dist/routes/inbox.d.ts +54 -19
  191. package/dist/routes/inbox.d.ts.map +1 -1
  192. package/dist/routes/index.d.ts +100 -1
  193. package/dist/routes/index.d.ts.map +1 -1
  194. package/dist/routes/integrations.d.ts +14 -4
  195. package/dist/routes/integrations.d.ts.map +1 -1
  196. package/dist/routes/presentations.d.ts +11 -1
  197. package/dist/routes/presentations.d.ts.map +1 -1
  198. package/dist/routes/profile-manager.d.ts +11 -1
  199. package/dist/routes/profile-manager.d.ts.map +1 -1
  200. package/dist/routes/profiles.d.ts +11 -1
  201. package/dist/routes/profiles.d.ts.map +1 -1
  202. package/dist/routes/public-share-links.d.ts +22 -2
  203. package/dist/routes/public-share-links.d.ts.map +1 -1
  204. package/dist/routes/share-links.d.ts +36 -16
  205. package/dist/routes/share-links.d.ts.map +1 -1
  206. package/dist/routes/skill-frameworks.d.ts +11 -1
  207. package/dist/routes/skill-frameworks.d.ts.map +1 -1
  208. package/dist/routes/skills.d.ts +11 -1
  209. package/dist/routes/skills.d.ts.map +1 -1
  210. package/dist/routes/storage.d.ts +37 -23
  211. package/dist/routes/storage.d.ts.map +1 -1
  212. package/dist/routes/test.d.ts +79 -3
  213. package/dist/routes/test.d.ts.map +1 -1
  214. package/dist/routes/utilities.d.ts +11 -1
  215. package/dist/routes/utilities.d.ts.map +1 -1
  216. package/dist/routes/workflows.d.ts +11 -1
  217. package/dist/routes/workflows.d.ts.map +1 -1
  218. package/dist/services/delivery/adapters/postmark.adapter.d.ts +3 -0
  219. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  220. package/dist/services/delivery/delivery.service.d.ts +10 -0
  221. package/dist/services/delivery/delivery.service.d.ts.map +1 -1
  222. package/dist/types/activity.d.ts +3 -3
  223. package/dist/types/app-manifest.d.ts +10 -10
  224. package/dist/types/campaign.d.ts +13 -1
  225. package/dist/types/campaign.d.ts.map +1 -1
  226. package/dist/types/consentflowcontract.d.ts +175 -7
  227. package/dist/types/consentflowcontract.d.ts.map +1 -1
  228. package/dist/types/credential-refresh.d.ts +27 -0
  229. package/dist/types/credential-refresh.d.ts.map +1 -1
  230. package/dist/types/credential.d.ts +5 -0
  231. package/dist/types/credential.d.ts.map +1 -1
  232. package/dist/types/integration.d.ts +3 -3
  233. package/lambda.ts +44 -14
  234. package/lambdaApp.ts +20 -0
  235. package/package.json +20 -19
  236. package/project.json +5 -0
  237. package/scripts/migrate-consent-flow-versions.ts +9 -0
  238. package/scripts/report-send-template-exposure.ts +47 -0
  239. package/scripts/verify-python-contracts.ts +16 -1
  240. package/serverless-local.yml +15 -2
  241. package/serverless.function-env.cjs +4 -0
  242. package/serverless.local.yml +12 -0
  243. package/serverless.yml +48 -3
  244. package/src/accesslayer/boost/create.ts +62 -7
  245. package/src/accesslayer/campaign/README.md +275 -6
  246. package/src/accesslayer/campaign/audience.ts +39 -7
  247. package/src/accesslayer/campaign/create.ts +4 -2
  248. package/src/accesslayer/campaign/delivery.ts +66 -0
  249. package/src/accesslayer/campaign/dispatch.test.ts +59 -0
  250. package/src/accesslayer/campaign/dispatch.ts +172 -0
  251. package/src/accesslayer/campaign/email.test.ts +73 -0
  252. package/src/accesslayer/campaign/email.ts +91 -0
  253. package/src/accesslayer/campaign/preferences.ts +53 -0
  254. package/src/accesslayer/campaign/read.ts +3 -1
  255. package/src/accesslayer/campaign/state.ts +1 -1
  256. package/src/accesslayer/campaign/test.test.ts +31 -0
  257. package/src/accesslayer/campaign/test.ts +75 -0
  258. package/src/accesslayer/campaign/update.ts +15 -4
  259. package/src/accesslayer/consentflowcontract/create.ts +20 -1
  260. package/src/accesslayer/consentflowcontract/delete.ts +121 -3
  261. package/src/accesslayer/consentflowcontract/read.ts +15 -9
  262. package/src/accesslayer/consentflowcontract/relationships/create.ts +70 -36
  263. package/src/accesslayer/consentflowcontract/relationships/read.ts +305 -73
  264. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +1 -1
  265. package/src/accesslayer/consentflowcontract/relationships/update.ts +269 -98
  266. package/src/accesslayer/consentflowcontract/versions.ts +790 -0
  267. package/src/accesslayer/credential/create.ts +33 -9
  268. package/src/accesslayer/credential/delete.ts +45 -0
  269. package/src/accesslayer/credential/relationships/create.ts +102 -47
  270. package/src/accesslayer/credential/relationships/update.ts +8 -116
  271. package/src/accesslayer/credential-refresh/read.ts +26 -2
  272. package/src/accesslayer/credential-refresh/update.ts +6 -2
  273. package/src/accesslayer/inbox-credential/create.ts +7 -1
  274. package/src/accesslayer/inbox-credential/refresh.ts +14 -4
  275. package/src/campaign-unsubscribe.test.ts +158 -0
  276. package/src/campaign-unsubscribe.ts +86 -0
  277. package/src/config/environment.test.ts +52 -0
  278. package/src/config/environment.ts +58 -0
  279. package/src/config/serverlessFunctionEnv.test.ts +59 -0
  280. package/src/credential-refresh.ts +1 -0
  281. package/src/docker-entry.ts +26 -1
  282. package/src/helpers/boost-storage.helpers.test.ts +107 -41
  283. package/src/helpers/boost.helpers.ts +88 -40
  284. package/src/helpers/campaign-app-origin.helpers.ts +25 -0
  285. package/src/helpers/campaign-email.helpers.ts +113 -0
  286. package/src/helpers/campaign-notifications.helpers.ts +127 -0
  287. package/src/helpers/campaign-queue.helpers.test.ts +442 -0
  288. package/src/helpers/campaign-queue.helpers.ts +122 -0
  289. package/src/helpers/campaign-unsubscribe-page.ts +107 -0
  290. package/src/helpers/campaign-unsubscribe.helpers.ts +56 -0
  291. package/src/helpers/campaign-webhook.helpers.test.ts +137 -0
  292. package/src/helpers/consentflow-deadline.helpers.ts +22 -0
  293. package/src/helpers/consentflow-legacy-migration.helpers.ts +179 -0
  294. package/src/helpers/contract-events.helpers.ts +35 -3
  295. package/src/helpers/contract-requests.helpers.ts +1 -1
  296. package/src/helpers/contract.helpers.ts +5 -1
  297. package/src/helpers/credential-display.helpers.test.ts +68 -0
  298. package/src/helpers/credential-display.helpers.ts +15 -0
  299. package/src/helpers/credential-refresh-email.helpers.ts +5 -12
  300. package/src/helpers/credential-refresh-materiality.helpers.test.ts +138 -1
  301. package/src/helpers/credential-refresh-materiality.helpers.ts +62 -1
  302. package/src/helpers/credential-refresh-proof.helpers.test.ts +1 -1
  303. package/src/helpers/credential-refresh-proof.helpers.ts +2 -2
  304. package/src/helpers/credential-refresh.helpers.ts +1166 -376
  305. package/src/helpers/credential-status-mutation.helpers.test.ts +155 -0
  306. package/src/helpers/credential-status-mutation.helpers.ts +166 -0
  307. package/src/helpers/credential.helpers.ts +19 -11
  308. package/src/helpers/finalize-inbox.helpers.ts +1 -1
  309. package/src/helpers/inbox-refresh.helpers.ts +34 -8
  310. package/src/helpers/inbox.helpers.ts +2 -0
  311. package/src/helpers/learnCard.helpers.ts +23 -0
  312. package/src/helpers/managed-status.helpers.test.ts +101 -0
  313. package/src/helpers/managed-status.helpers.ts +147 -0
  314. package/src/helpers/notification-delivery-boundary.helpers.test.ts +114 -0
  315. package/src/helpers/notification-webhook-fetch.helpers.test.ts +228 -0
  316. package/src/helpers/notification-webhook-fetch.helpers.ts +166 -0
  317. package/src/helpers/notificationQueue.helpers.ts +5 -0
  318. package/src/helpers/notifications.helpers.ts +19 -12
  319. package/src/helpers/refresh-send-intent.helpers.test.ts +60 -0
  320. package/src/helpers/refresh-send-intent.helpers.ts +81 -18
  321. package/src/helpers/send-contract.helpers.ts +36 -0
  322. package/src/helpers/status-list.helpers.test.ts +29 -12
  323. package/src/helpers/status-list.helpers.ts +329 -20
  324. package/src/models/ConsentFlowContract.ts +3 -0
  325. package/src/models/ConsentFlowContractVersion.ts +50 -0
  326. package/src/models/ConsentFlowTerms.ts +4 -0
  327. package/src/models/Credential.ts +2 -0
  328. package/src/models/CredentialRefresh.ts +2 -0
  329. package/src/models/Profile.ts +2 -0
  330. package/src/models/campaign-constraints.ts +13 -8
  331. package/src/models/index.ts +2 -0
  332. package/src/openapi.ts +21 -0
  333. package/src/routes/app-store.ts +2 -1
  334. package/src/routes/boosts.ts +219 -137
  335. package/src/routes/campaigns.ts +98 -19
  336. package/src/routes/contracts.ts +433 -115
  337. package/src/routes/credential-input-privacy.test.ts +92 -0
  338. package/src/routes/credential-refreshes.ts +273 -2
  339. package/src/routes/federation.ts +7 -2
  340. package/src/routes/inbox.ts +6 -1
  341. package/src/routes/index.ts +31 -2
  342. package/src/routes/workflows.ts +61 -46
  343. package/src/services/delivery/adapters/postmark.adapter.test.ts +70 -1
  344. package/src/services/delivery/adapters/postmark.adapter.ts +84 -11
  345. package/src/services/delivery/delivery.service.ts +4 -0
  346. package/src/types/campaign.ts +14 -1
  347. package/src/types/consentflowcontract.ts +43 -2
  348. package/src/types/credential-refresh.ts +11 -1
  349. package/src/types/credential.ts +2 -0
  350. package/test/boost-status-cleanup.spec.ts +123 -0
  351. package/test/campaigns-email.unit.spec.ts +268 -0
  352. package/test/campaigns.spec.ts +804 -30
  353. package/test/consent-autoboost-idempotency.spec.ts +432 -0
  354. package/test/consent-data-boundaries.spec.ts +322 -26
  355. package/test/consent-legacy-migration.spec.ts +315 -0
  356. package/test/consent-recipients.spec.ts +7 -7
  357. package/test/consent-version-updates.spec.ts +761 -0
  358. package/test/consentflow.spec.ts +698 -226
  359. package/test/contract-requests.spec.ts +55 -9
  360. package/test/credential-refresh-allocation.spec.ts +212 -14
  361. package/test/credential-refresh-email.spec.ts +6 -10
  362. package/test/credential-refresh-endpoint.spec.ts +3 -0
  363. package/test/credential-refresh-publication.spec.ts +52 -15
  364. package/test/credential-storage.neo4j.spec.ts +33 -6
  365. package/test/helpers/legacy-autoboost.ts +48 -0
  366. package/test/holder-mode-send.spec.ts +710 -0
  367. package/test/inbox-refresh.spec.ts +105 -0
  368. package/test/inbox-security.e2e.ts +18 -22
  369. package/test/managed-status-revocation.spec.ts +970 -0
  370. package/test/notifications-worker.spec.ts +31 -25
  371. package/test/refresh-send.spec.ts +334 -5
  372. package/test/smart-resume-consent.spec.ts +1 -1
  373. package/test/verification/campaigns.ts +40 -0
  374. package/vitest.campaigns.config.ts +2 -0
  375. package/vitest.config.ts +14 -0
@@ -19,6 +19,7 @@ import { z } from 'zod';
19
19
  import { t, profileRoute, openRoute, guardianGatedRoute } from '@routes';
20
20
 
21
21
  import {
22
+ CONTRACT_APPROVAL_REQUIRED,
22
23
  ConsentFlowContractValidator,
23
24
  ConsentFlowContractDetailsValidator,
24
25
  ConsentFlowContractQueryValidator,
@@ -38,16 +39,26 @@ import {
38
39
  JWEValidator,
39
40
  UnsignedVC,
40
41
  AutoBoostConfigValidator,
42
+ ConsentFlowContractChangesValidator,
43
+ ConsentFlowContractUpdateResultValidator,
44
+ ConsentFlowContractVersionValidator,
45
+ ConsentFlowConsentStatusValidator,
41
46
  LCNNotificationTypeEnumValidator,
42
47
  LCNProfileValidator,
43
48
  } from '@learncard/types';
44
49
  import { isVC2Format } from '@learncard/helpers';
45
50
  import { injectObv3AlignmentsIntoCredentialForBoost } from '@services/skills-provider/inject';
51
+ import {
52
+ getConsentFlowContractVersion,
53
+ getConsentFlowContractVersions,
54
+ updateConsentFlowContractVersion,
55
+ } from '@accesslayer/consentflowcontract/versions';
46
56
  import { createConsentFlowContract } from '@accesslayer/consentflowcontract/create';
47
57
  import {
48
58
  getAutoBoostsForContract,
49
59
  getConsentFlowContractsForProfile,
50
60
  getConsentedContractsForProfile,
61
+ encodeConsentedDataCursor,
51
62
  getConsentedDataBetweenProfiles,
52
63
  getConsentedDataForContract,
53
64
  getConsentedDataForProfile,
@@ -77,6 +88,7 @@ import { removeRequestedForRelationship } from '@accesslayer/consentflowcontract
77
88
  import { areTermsValid } from '@helpers/contract.helpers';
78
89
  import { updateDidForProfile, getProfileIdFromString } from '@helpers/did.helpers';
79
90
  import {
91
+ reconsentTerms,
80
92
  syncCredentialsToContract,
81
93
  updateTerms,
82
94
  pruneDeletedUrisFromConsentTerms,
@@ -106,10 +118,6 @@ import {
106
118
  sanitizeProfileForTier,
107
119
  stripSensitiveProfileListFields,
108
120
  } from '@helpers/profile-privacy.helpers';
109
- import {
110
- addAutoBoostsToContractDb,
111
- removeAutoBoostsFromContractDb,
112
- } from '@accesslayer/consentflowcontract/relationships/manageAutoboosts';
113
121
  import { addNotificationToQueue } from '@helpers/notifications.helpers';
114
122
  import { getNotificationMessage } from '@helpers/notificationMessages';
115
123
  import { resolveRecipientLocale } from '@helpers/getRecipientLocale.helpers';
@@ -133,6 +141,29 @@ const resolveRecipientProfileId = async (identifier: string, domain: string): Pr
133
141
  return profileId;
134
142
  };
135
143
 
144
+ const LATEST_CONTRACT_APPROVAL_REQUIRED =
145
+ 'User has not yet approved the latest version of this contract';
146
+
147
+ const assertLatestContractApproval = (
148
+ terms: { status?: string; consentedVersion?: number } | null | undefined,
149
+ currentVersion: number
150
+ ) => {
151
+ if (!terms) return;
152
+ if (
153
+ terms.status === 'needs_reconsent' ||
154
+ terms.status === 'suspended' ||
155
+ (terms.status === 'live' &&
156
+ terms.consentedVersion !== undefined &&
157
+ terms.consentedVersion < currentVersion)
158
+ ) {
159
+ throw new TRPCError({
160
+ code: 'FORBIDDEN',
161
+ message: LATEST_CONTRACT_APPROVAL_REQUIRED,
162
+ cause: { code: CONTRACT_APPROVAL_REQUIRED },
163
+ });
164
+ }
165
+ };
166
+
136
167
  export const contractsRouter = t.router({
137
168
  createConsentFlowContract: profileRoute
138
169
  .meta({
@@ -360,7 +391,7 @@ export const contractsRouter = t.router({
360
391
  method: 'GET',
361
392
  path: '/consent-flow-contract',
362
393
  tags: ['Contracts'],
363
- summary: 'Get Consent Flow Contracts',
394
+ summary: 'Get Consent Flow Contract',
364
395
  description: 'Gets Consent Flow Contract Details',
365
396
  },
366
397
  requiredScope: 'contracts:read',
@@ -368,9 +399,7 @@ export const contractsRouter = t.router({
368
399
  .input(z.object({ uri: z.string() }))
369
400
  .output(ConsentFlowContractDetailsValidator)
370
401
  .query(async ({ input, ctx }) => {
371
- const { uri } = input;
372
-
373
- const decodedUri = decodeURIComponent(uri);
402
+ const decodedUri = decodeURIComponent(input.uri);
374
403
  const result = await getContractDetailsByUri(decodedUri);
375
404
 
376
405
  if (!result) {
@@ -400,6 +429,141 @@ export const contractsRouter = t.router({
400
429
  publicContractRecipient(ctx.domain, recipient)
401
430
  ),
402
431
  audienceVersion: Number(result.contract.audienceVersion ?? 0),
432
+ version: Number(result.contract.currentVersion ?? 1),
433
+ status: result.contract.status === 'retired' ? 'retired' : 'active',
434
+ latestUpdate: result.contract.latestUpdate,
435
+ };
436
+ }),
437
+
438
+ updateConsentFlowContract: profileRoute
439
+ .meta({
440
+ requiredScope: 'contracts:write',
441
+ openapi: {
442
+ protect: true,
443
+ method: 'POST',
444
+ path: '/consent-flow-contract/update',
445
+ tags: ['Contracts'],
446
+ summary: 'Update Consent Flow Contract',
447
+ description: 'Creates a versioned update to an owned consent flow contract',
448
+ },
449
+ })
450
+ .input(
451
+ z.object({
452
+ uri: z.string(),
453
+ changes: ConsentFlowContractChangesValidator,
454
+ reason: z.string().max(140).optional(),
455
+ reconsentDeadline: z.string().datetime().optional(),
456
+ dryRun: z.boolean().optional(),
457
+ expectedVersion: z.number().int().positive().optional(),
458
+ })
459
+ )
460
+ .output(ConsentFlowContractUpdateResultValidator)
461
+ .mutation(async ({ ctx, input }) => {
462
+ const contract = await getContractByUri(decodeURIComponent(input.uri));
463
+ if (!contract)
464
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
465
+ if (!(await isProfileConsentFlowContractAdmin(ctx.user.profile, contract))) {
466
+ throw new TRPCError({
467
+ code: 'UNAUTHORIZED',
468
+ message: 'Profile does not own contract',
469
+ });
470
+ }
471
+ const engineChanges = {
472
+ ...input.changes,
473
+ autoboosts: undefined as
474
+ | {
475
+ id: string;
476
+ signingAuthorityEndpoint: string;
477
+ signingAuthorityName: string;
478
+ }[]
479
+ | undefined,
480
+ };
481
+ if (input.changes.autoboosts) {
482
+ engineChanges.autoboosts = await Promise.all(
483
+ input.changes.autoboosts.map(async config => {
484
+ const boost = await getBoostByUri(config.boostUri);
485
+ if (!boost) {
486
+ throw new TRPCError({
487
+ code: 'NOT_FOUND',
488
+ message: `Could not find boost: ${config.boostUri}`,
489
+ });
490
+ }
491
+ if (isDraftBoost(boost)) {
492
+ throw new TRPCError({
493
+ code: 'FORBIDDEN',
494
+ message:
495
+ 'Draft Boosts cannot be used for automatic credential delivery',
496
+ });
497
+ }
498
+ const name = config.signingAuthority.name.toLowerCase();
499
+ return {
500
+ id: boost.id,
501
+ signingAuthorityEndpoint: config.signingAuthority.endpoint,
502
+ signingAuthorityName: name,
503
+ };
504
+ })
505
+ );
506
+ }
507
+ const appHost =
508
+ environment.CLIENT_APP_DOMAIN_NAME ??
509
+ `localhost:${environment.CLIENT_APP_PORT || 3000}`;
510
+ const scheme = environment.IS_OFFLINE ? 'http' : 'https';
511
+ const appBaseUrl =
512
+ ctx.tenant?.emailBranding?.appUrl?.replace(/\/+$/, '') ?? `${scheme}://${appHost}`;
513
+ return updateConsentFlowContractVersion(
514
+ { ...input, changes: engineChanges },
515
+ ctx.user.profile.profileId,
516
+ ctx.domain,
517
+ appBaseUrl
518
+ );
519
+ }),
520
+
521
+ getContractVersions: openRoute
522
+ .meta({ requiredScope: 'contracts:read' })
523
+ .input(z.object({ uri: z.string() }))
524
+ .output(z.array(ConsentFlowContractVersionValidator))
525
+ .query(async ({ input }) => getConsentFlowContractVersions(input.uri)),
526
+
527
+ getConsentStatus: profileRoute
528
+ .meta({ requiredScope: 'contracts:read' })
529
+ .input(z.object({ uri: z.string(), profileId: z.string() }))
530
+ .output(ConsentFlowConsentStatusValidator)
531
+ .query(async ({ input, ctx }) => {
532
+ const profileId = await getProfileIdFromString(input.profileId, ctx.domain);
533
+ if (!profileId)
534
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found' });
535
+ const contract = await getContractByUri(decodeURIComponent(input.uri));
536
+ if (!contract)
537
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
538
+ const isOwner = await isProfileConsentFlowContractAdmin(ctx.user.profile, contract);
539
+ if (profileId !== ctx.user.profile.profileId && !isOwner) {
540
+ throw new TRPCError({
541
+ code: 'UNAUTHORIZED',
542
+ message: 'Profile cannot inspect this consent',
543
+ });
544
+ }
545
+ const terms = await getContractTermsForProfile(profileId, contract);
546
+ const currentVersion = Number(contract.currentVersion ?? 1);
547
+ const needsReconsent =
548
+ terms?.status === 'needs_reconsent' ||
549
+ terms?.status === 'suspended' ||
550
+ (terms?.consentedVersion !== undefined && terms.consentedVersion < currentVersion);
551
+ const appHost =
552
+ environment.CLIENT_APP_DOMAIN_NAME ??
553
+ `localhost:${environment.CLIENT_APP_PORT || 3000}`;
554
+ const scheme = environment.IS_OFFLINE ? 'http' : 'https';
555
+ const appBaseUrl =
556
+ ctx.tenant?.emailBranding?.appUrl?.replace(/\/+$/, '') ?? `${scheme}://${appHost}`;
557
+ const reviewUrl =
558
+ terms && needsReconsent
559
+ ? `${appBaseUrl}/consent-flow/review?uri=${encodeURIComponent(input.uri)}`
560
+ : undefined;
561
+ return {
562
+ status: terms?.status ?? 'not_consented',
563
+ ...(terms?.consentedVersion ? { consentedVersion: terms.consentedVersion } : {}),
564
+ currentVersion,
565
+ needsReconsent,
566
+ ...(reviewUrl ? { reviewUrl } : {}),
403
567
  };
404
568
  }),
405
569
 
@@ -456,6 +620,9 @@ export const contractsRouter = t.router({
456
620
  uri: constructUri('contract', contract.id, ctx.domain),
457
621
  ...(contract.expiresAt ? { expiresAt: contract.expiresAt } : {}),
458
622
  autoBoosts,
623
+ version: Number(contract.currentVersion ?? 1),
624
+ status: contract.status === 'retired' ? 'retired' : 'active',
625
+ latestUpdate: contract.latestUpdate,
459
626
  })),
460
627
  };
461
628
  }),
@@ -492,7 +659,16 @@ export const contractsRouter = t.router({
492
659
  message: 'Profile does not own contract',
493
660
  });
494
661
  }
495
- await Promise.all([deleteConsentFlowContract(contract), deleteStorageForUri(uri)]);
662
+ const appHost =
663
+ environment.CLIENT_APP_DOMAIN_NAME ??
664
+ `localhost:${environment.CLIENT_APP_PORT || 3000}`;
665
+ const scheme = environment.IS_OFFLINE ? 'http' : 'https';
666
+ const appBaseUrl =
667
+ ctx.tenant?.emailBranding?.appUrl?.replace(/\/+$/, '') ?? `${scheme}://${appHost}`;
668
+ await Promise.all([
669
+ deleteConsentFlowContract(contract, ctx.domain, appBaseUrl),
670
+ deleteStorageForUri(uri),
671
+ ]);
496
672
 
497
673
  return true;
498
674
  }),
@@ -538,21 +714,16 @@ export const contractsRouter = t.router({
538
714
 
539
715
  const contractId = getIdFromUri(uri);
540
716
 
541
- const results = await getConsentedDataForContract(contractId, profile.profileId, {
717
+ const page = await getConsentedDataForContract(contractId, profile.profileId, {
542
718
  query,
543
- limit: limit + 1,
719
+ limit,
544
720
  cursor,
545
721
  });
546
722
 
547
- const data = results.slice(0, limit);
548
-
549
- const hasMore = results.length > limit;
550
- const nextCursor = data.at(-1)?.date;
551
-
552
723
  return {
553
- hasMore,
554
- cursor: nextCursor,
555
- records: data,
724
+ hasMore: page.hasMore,
725
+ cursor: page.nextCursor,
726
+ records: page.records,
556
727
  };
557
728
  }),
558
729
 
@@ -594,17 +765,14 @@ export const contractsRouter = t.router({
594
765
  const results = await getConsentedDataBetweenProfiles(
595
766
  profile.profileId,
596
767
  otherProfile.profileId,
597
- { query, limit: limit + 1, cursor, domain: ctx.domain }
768
+ { query, limit, cursor, domain: ctx.domain }
598
769
  );
599
770
 
600
- const data = results.slice(0, limit);
601
-
602
- const hasMore = results.length > limit;
603
- const nextCursor = data.at(-1)?.date;
771
+ const data = results.records;
604
772
 
605
773
  return {
606
- hasMore,
607
- cursor: nextCursor,
774
+ hasMore: results.hasMore,
775
+ cursor: results.nextCursor,
608
776
  records: data,
609
777
  };
610
778
  }),
@@ -634,21 +802,16 @@ export const contractsRouter = t.router({
634
802
 
635
803
  const { query, limit, cursor } = input;
636
804
 
637
- const results = await getConsentedDataForProfile(profile.profileId, {
805
+ const page = await getConsentedDataForProfile(profile.profileId, {
638
806
  query,
639
- limit: limit + 1,
807
+ limit,
640
808
  cursor,
641
809
  });
642
810
 
643
- const data = results.slice(0, limit);
644
-
645
- const hasMore = results.length > limit;
646
- const nextCursor = data.at(-1)?.date;
647
-
648
811
  return {
649
- hasMore,
650
- cursor: nextCursor,
651
- records: data,
812
+ hasMore: page.hasMore,
813
+ cursor: page.nextCursor,
814
+ records: page.records,
652
815
  };
653
816
  }),
654
817
 
@@ -706,9 +869,22 @@ export const contractsRouter = t.router({
706
869
  message: 'Could not find contract',
707
870
  });
708
871
  }
872
+ if (contractDetails.contract.status === 'retired') {
873
+ throw new TRPCError({
874
+ code: 'FORBIDDEN',
875
+ message: 'This contract is no longer active',
876
+ });
877
+ }
878
+ const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
879
+ assertLatestContractApproval(
880
+ terms,
881
+ Number(contractDetails.contract.currentVersion ?? 1)
882
+ );
709
883
 
710
- // Verify if the other profile has consented to the contract
711
- if (!(await hasProfileConsentedToContract(otherProfile, contractDetails.contract))) {
884
+ if (
885
+ !(await hasProfileConsentedToContract(otherProfile, contractDetails.contract)) ||
886
+ !terms
887
+ ) {
712
888
  throw new TRPCError({
713
889
  code: 'FORBIDDEN',
714
890
  message: 'Target profile has not consented to this contract',
@@ -740,13 +916,6 @@ export const contractsRouter = t.router({
740
916
  }
741
917
 
742
918
  // Check if this boost category is allowed in the contract's terms
743
- const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
744
- if (!terms) {
745
- throw new TRPCError({
746
- code: 'FORBIDDEN',
747
- message: 'Target profile has not consented to this contract',
748
- });
749
- }
750
919
 
751
920
  // Check if the boost category is included in the contract terms
752
921
  const boostCategory = boost.category;
@@ -849,9 +1018,23 @@ export const contractsRouter = t.router({
849
1018
  if (!contractDetails) {
850
1019
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
851
1020
  }
1021
+ if (contractDetails.contract.status === 'retired') {
1022
+ throw new TRPCError({
1023
+ code: 'FORBIDDEN',
1024
+ message: 'This contract is no longer active',
1025
+ });
1026
+ }
1027
+ const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
1028
+ assertLatestContractApproval(
1029
+ terms,
1030
+ Number(contractDetails.contract.currentVersion ?? 1)
1031
+ );
852
1032
 
853
1033
  // Verify consent
854
- if (!(await hasProfileConsentedToContract(otherProfile, contractDetails.contract))) {
1034
+ if (
1035
+ !(await hasProfileConsentedToContract(otherProfile, contractDetails.contract)) ||
1036
+ !terms
1037
+ ) {
855
1038
  throw new TRPCError({
856
1039
  code: 'FORBIDDEN',
857
1040
  message: 'Target profile has not consented to this contract',
@@ -874,15 +1057,6 @@ export const contractsRouter = t.router({
874
1057
  });
875
1058
  }
876
1059
 
877
- // Get contract terms
878
- const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
879
- if (!terms) {
880
- throw new TRPCError({
881
- code: 'FORBIDDEN',
882
- message: 'Target profile has not consented to this contract',
883
- });
884
- }
885
-
886
1060
  // Check category allowed
887
1061
  const boostCategory = boost.category;
888
1062
  if (boostCategory) {
@@ -1027,6 +1201,12 @@ export const contractsRouter = t.router({
1027
1201
  if (!contractDetails) {
1028
1202
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
1029
1203
  }
1204
+ if (contractDetails.contract.status === 'retired') {
1205
+ throw new TRPCError({
1206
+ code: 'FORBIDDEN',
1207
+ message: 'This contract is no longer active',
1208
+ });
1209
+ }
1030
1210
 
1031
1211
  if (
1032
1212
  contractDetails.contract.expiresAt?.trim() &&
@@ -1049,11 +1229,15 @@ export const contractsRouter = t.router({
1049
1229
  profile,
1050
1230
  contractDetails.contract
1051
1231
  );
1232
+ assertLatestContractApproval(
1233
+ previousTerms,
1234
+ Number(contractDetails.contract.currentVersion ?? 1)
1235
+ );
1052
1236
  const retrySmartResume =
1053
1237
  isSmartResume &&
1054
1238
  Boolean(previousTerms?.smartResumeFingerprint) &&
1055
1239
  (previousTerms?.status === 'live' ||
1056
- (previousTerms?.status === 'stale' && previousTerms.oneTime));
1240
+ (previousTerms?.status === 'consumed' && previousTerms.oneTime));
1057
1241
 
1058
1242
  if (
1059
1243
  !retrySmartResume &&
@@ -1107,7 +1291,7 @@ export const contractsRouter = t.router({
1107
1291
  // Identical submissions still replay the completed upload instead.
1108
1292
  const newDecisionAfterOneTime =
1109
1293
  retrySmartResume &&
1110
- previousTerms?.status === 'stale' &&
1294
+ previousTerms?.status === 'consumed' &&
1111
1295
  previousTerms.oneTime &&
1112
1296
  previousTerms.smartResumePublicationStatus === 'succeeded' &&
1113
1297
  fingerprint !== previousTerms.smartResumeFingerprint;
@@ -1194,12 +1378,16 @@ export const contractsRouter = t.router({
1194
1378
 
1195
1379
  const contracts = results.slice(0, limit);
1196
1380
 
1197
- const hasMore = results.length > limit;
1198
- const nextCursor = contracts.at(-1)?.terms.updatedAt;
1199
-
1200
1381
  return {
1201
- hasMore,
1202
- cursor: nextCursor,
1382
+ hasMore: results.length > limit,
1383
+ cursor: contracts.length
1384
+ ? encodeConsentedDataCursor(
1385
+ contracts.at(-1)!.terms.updatedAt ??
1386
+ contracts.at(-1)!.terms.createdAt ??
1387
+ '',
1388
+ contracts.at(-1)!.terms.id
1389
+ )
1390
+ : undefined,
1203
1391
  records: contracts.map(record => ({
1204
1392
  contract: {
1205
1393
  contract: record.contract.contract,
@@ -1220,6 +1408,9 @@ export const contractsRouter = t.router({
1220
1408
  : {}),
1221
1409
  autoBoosts: record.autoBoosts,
1222
1410
  audienceVersion: Number(record.contract.audienceVersion ?? 0),
1411
+ version: Number(record.contract.currentVersion ?? 1),
1412
+ status: record.contract.status === 'retired' ? 'retired' : 'active',
1413
+ latestUpdate: record.contract.latestUpdate,
1223
1414
  recipients: record.recipients.map(recipient =>
1224
1415
  publicContractRecipient(ctx.domain, recipient)
1225
1416
  ),
@@ -1231,6 +1422,19 @@ export const contractsRouter = t.router({
1231
1422
  ...(record.terms.oneTime ? { oneTime: record.terms.oneTime } : {}),
1232
1423
  consenter: updateDidForProfile(ctx.domain, profile),
1233
1424
  status: record.terms.status,
1425
+ consentedVersion: Number(record.terms.consentedVersion ?? 1),
1426
+ lastReviewedVersion: Number(
1427
+ record.terms.lastReviewedVersion ?? record.terms.consentedVersion ?? 1
1428
+ ),
1429
+ ...(record.cumulativeUpdate
1430
+ ? { cumulativeUpdate: record.cumulativeUpdate }
1431
+ : {}),
1432
+ ...(record.terms.reconsentDeadline
1433
+ ? { reconsentDeadline: record.terms.reconsentDeadline }
1434
+ : {}),
1435
+ ...(record.terms.newOptionsUntil
1436
+ ? { newOptionsUntil: record.terms.newOptionsUntil }
1437
+ : {}),
1234
1438
  })),
1235
1439
  };
1236
1440
  }),
@@ -1251,6 +1455,7 @@ export const contractsRouter = t.router({
1251
1455
  z.object({
1252
1456
  uri: z.string(),
1253
1457
  terms: ConsentFlowTermsValidator,
1458
+ acceptVersion: z.number().int().positive().optional(),
1254
1459
  expiresAt: z.string().optional(),
1255
1460
  oneTime: z.boolean().optional(),
1256
1461
  audienceVersion: z.number().int().nonnegative().optional(),
@@ -1269,7 +1474,7 @@ export const contractsRouter = t.router({
1269
1474
  });
1270
1475
  }
1271
1476
 
1272
- const { uri, terms, expiresAt, oneTime, audienceVersion } = input;
1477
+ const { uri, terms, acceptVersion, expiresAt, oneTime, audienceVersion } = input;
1273
1478
 
1274
1479
  const decodedUri = decodeURIComponent(uri);
1275
1480
  const relationship = await getContractTermsByUri(decodedUri);
@@ -1287,6 +1492,12 @@ export const contractsRouter = t.router({
1287
1492
  message: 'Profile does not own terms',
1288
1493
  });
1289
1494
  }
1495
+ if (relationship.contract.status === 'retired') {
1496
+ throw new TRPCError({
1497
+ code: 'FORBIDDEN',
1498
+ message: 'This contract is no longer active',
1499
+ });
1500
+ }
1290
1501
 
1291
1502
  if (!guardianIdentity && (await hasGuardianApprovalHistory(relationship.terms))) {
1292
1503
  throw new TRPCError({
@@ -1295,14 +1506,32 @@ export const contractsRouter = t.router({
1295
1506
  });
1296
1507
  }
1297
1508
 
1298
- if (!areTermsValid(terms, relationship.contract.contract)) {
1509
+ if (!acceptVersion) {
1510
+ assertLatestContractApproval(
1511
+ relationship.terms,
1512
+ Number(relationship.contract.currentVersion ?? 1)
1513
+ );
1514
+ if (relationship.terms.status !== 'live') {
1515
+ throw new TRPCError({
1516
+ code: 'FORBIDDEN',
1517
+ message: 'Only live contract terms can be updated',
1518
+ });
1519
+ }
1520
+ }
1521
+ const acceptedContractVersion = await getConsentFlowContractVersion(
1522
+ relationship.contract.id,
1523
+ acceptVersion ?? Number(relationship.contract.currentVersion ?? 1)
1524
+ );
1525
+ if (!acceptedContractVersion) {
1526
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Unknown contract version' });
1527
+ }
1528
+ if (!areTermsValid(terms, acceptedContractVersion.contract)) {
1299
1529
  throw new TRPCError({
1300
1530
  code: 'BAD_REQUEST',
1301
1531
  message: 'New Terms are invalid for Contract',
1302
1532
  });
1303
1533
  }
1304
1534
 
1305
- // Convert deniedWriters DIDs to profile IDs if needed
1306
1535
  if (terms.deniedWriters?.length) {
1307
1536
  terms.deniedWriters = await resolveAndValidateDeniedWriters(
1308
1537
  relationship.contract,
@@ -1311,27 +1540,28 @@ export const contractsRouter = t.router({
1311
1540
  );
1312
1541
  }
1313
1542
 
1314
- await Promise.all([
1315
- updateTerms(
1316
- relationship,
1317
- {
1318
- terms,
1319
- expiresAt,
1320
- oneTime,
1321
- audienceVersion,
1322
- guardianApproval: guardianIdentity
1323
- ? {
1324
- guardianProfileId: guardianIdentity.profileId,
1325
- guardianDid: guardianIdentity.did,
1326
- approvedAt: new Date().toISOString(),
1327
- contractUpdatedAt: relationship.contract.updatedAt,
1328
- }
1329
- : undefined,
1330
- },
1331
- ctx.domain
1332
- ),
1333
- deleteStorageForUri(uri),
1334
- ]);
1543
+ const changes = {
1544
+ terms,
1545
+ expiresAt,
1546
+ oneTime,
1547
+ audienceVersion,
1548
+ ...(guardianIdentity
1549
+ ? {
1550
+ guardianApproval: {
1551
+ guardianProfileId: guardianIdentity.profileId,
1552
+ guardianDid: guardianIdentity.did,
1553
+ approvedAt: new Date().toISOString(),
1554
+ contractUpdatedAt: relationship.contract.updatedAt,
1555
+ },
1556
+ }
1557
+ : {}),
1558
+ };
1559
+ if (acceptVersion) {
1560
+ await reconsentTerms(relationship, { ...changes, acceptVersion }, ctx.domain);
1561
+ } else {
1562
+ await updateTerms(relationship, changes, ctx.domain);
1563
+ }
1564
+ await deleteStorageForUri(uri);
1335
1565
 
1336
1566
  return true;
1337
1567
  }),
@@ -1386,7 +1616,15 @@ export const contractsRouter = t.router({
1386
1616
  const contracts = results.slice(0, pageSize);
1387
1617
  allContracts.push(...contracts);
1388
1618
 
1389
- cursor = results.length > pageSize ? contracts.at(-1)?.terms.updatedAt : undefined;
1619
+ cursor =
1620
+ results.length > pageSize && contracts.length
1621
+ ? encodeConsentedDataCursor(
1622
+ contracts.at(-1)!.terms.updatedAt ??
1623
+ contracts.at(-1)!.terms.createdAt ??
1624
+ '',
1625
+ contracts.at(-1)!.terms.id
1626
+ )
1627
+ : undefined;
1390
1628
  } while (cursor);
1391
1629
 
1392
1630
  for (const contract of allContracts) {
@@ -1502,8 +1740,10 @@ export const contractsRouter = t.router({
1502
1740
  const transactions = results.slice(0, limit);
1503
1741
 
1504
1742
  const hasMore = results.length > limit;
1505
- const nextCursor = transactions.at(-1)?.date;
1506
-
1743
+ const lastTransaction = transactions.at(-1);
1744
+ const nextCursor = lastTransaction
1745
+ ? encodeConsentedDataCursor(lastTransaction.date, lastTransaction.id)
1746
+ : undefined;
1507
1747
  return {
1508
1748
  hasMore,
1509
1749
  cursor: nextCursor,
@@ -1547,6 +1787,7 @@ export const contractsRouter = t.router({
1547
1787
  if (!contract) {
1548
1788
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
1549
1789
  }
1790
+ if (contract.status === 'retired') return false;
1550
1791
 
1551
1792
  if (contract.expiresAt && new Date() > new Date(contract.expiresAt)) return false;
1552
1793
 
@@ -1555,6 +1796,11 @@ export const contractsRouter = t.router({
1555
1796
  if (!terms) return false;
1556
1797
 
1557
1798
  if (terms.status !== 'live') return false;
1799
+ if (
1800
+ terms.consentedVersion !== undefined &&
1801
+ terms.consentedVersion < Number(contract.currentVersion ?? 1)
1802
+ )
1803
+ return false;
1558
1804
 
1559
1805
  if (terms.expiresAt && new Date() > new Date(terms.expiresAt)) return false;
1560
1806
 
@@ -1601,12 +1847,22 @@ export const contractsRouter = t.router({
1601
1847
  message: 'Profile does not own these terms',
1602
1848
  });
1603
1849
  }
1850
+ if (relationship.contract.status === 'retired') {
1851
+ throw new TRPCError({
1852
+ code: 'FORBIDDEN',
1853
+ message: 'This contract is no longer active',
1854
+ });
1855
+ }
1604
1856
 
1857
+ assertLatestContractApproval(
1858
+ relationship.terms,
1859
+ Number(relationship.contract.currentVersion ?? 1)
1860
+ );
1605
1861
  // Check if the terms are still live
1606
1862
  if (relationship.terms.status !== 'live') {
1607
1863
  throw new TRPCError({
1608
1864
  code: 'FORBIDDEN',
1609
- message: 'Cannot sync credentials to withdrawn or stale terms',
1865
+ message: 'Cannot sync credentials to terms that are not live',
1610
1866
  });
1611
1867
  }
1612
1868
 
@@ -1787,9 +2043,9 @@ export const contractsRouter = t.router({
1787
2043
  method: 'POST',
1788
2044
  path: '/consent-flow-contracts/autoboosts/add',
1789
2045
  tags: ['Contracts'],
1790
- summary: 'Add autoboosts to a contract',
2046
+ summary: 'Deprecated: add autoboosts to a contract',
1791
2047
  description:
1792
- 'Adds one or more autoboost configurations to an existing consent flow contract. The caller must be the contract owner or a designated writer. The signing authority for each autoboost must be registered to the caller.',
2048
+ 'Deprecated compatibility route. Only the contract owner can add autoboosts through a versioned update; additions require reapproval.',
1793
2049
  },
1794
2050
  requiredScope: 'contracts:write autoboosts:write',
1795
2051
  })
@@ -1805,30 +2061,78 @@ export const contractsRouter = t.router({
1805
2061
  const { contractUri, autoboosts } = input;
1806
2062
 
1807
2063
  const contract = await getContractByUri(contractUri);
1808
-
1809
- if (!contract) {
2064
+ if (!contract)
1810
2065
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
1811
- }
1812
-
1813
- const writers = await getWritersForContract(contract);
1814
- const isAuthorized = writers.some(writer => writer.profileId === profile.profileId);
1815
2066
 
1816
- if (!isAuthorized) {
2067
+ if (!(await isProfileConsentFlowContractAdmin(profile, contract))) {
1817
2068
  throw new TRPCError({
1818
2069
  code: 'UNAUTHORIZED',
1819
2070
  message: 'You do not have permission to modify autoboosts for this contract.',
1820
2071
  });
1821
2072
  }
1822
2073
 
1823
- const normalizedAutoboosts = autoboosts.map(ab => ({
1824
- ...ab,
1825
- signingAuthority: {
1826
- ...ab.signingAuthority,
1827
- name: ab.signingAuthority.name.toLowerCase(),
2074
+ const version = await getConsentFlowContractVersion(
2075
+ contract.id,
2076
+ Number(contract.currentVersion ?? 1)
2077
+ );
2078
+ if (!version)
2079
+ throw new TRPCError({
2080
+ code: 'PRECONDITION_FAILED',
2081
+ message: 'Contract version snapshot is missing',
2082
+ });
2083
+ const existingBoostIds = new Set(version.autoboosts.map(boost => boost.id));
2084
+ const additions = await Promise.all(
2085
+ autoboosts.map(async ab => {
2086
+ const boost = await getBoostByUri(ab.boostUri);
2087
+ if (!boost)
2088
+ throw new TRPCError({
2089
+ code: 'NOT_FOUND',
2090
+ message: `Could not find boost: ${ab.boostUri}`,
2091
+ });
2092
+ if (!(await canProfileIssueBoost(profile, boost))) {
2093
+ throw new TRPCError({
2094
+ code: 'UNAUTHORIZED',
2095
+ message: `Profile does not have permissions to issue boost: ${ab.boostUri}`,
2096
+ });
2097
+ }
2098
+ if (isDraftBoost(boost))
2099
+ throw new TRPCError({
2100
+ code: 'FORBIDDEN',
2101
+ message:
2102
+ 'Draft Boosts cannot be used for automatic credential delivery',
2103
+ });
2104
+ const signingAuthorityName = ab.signingAuthority.name.toLowerCase();
2105
+ const signingAuthority = await getSigningAuthorityForUserByName(
2106
+ profile,
2107
+ ab.signingAuthority.endpoint,
2108
+ signingAuthorityName
2109
+ );
2110
+ if (!signingAuthority)
2111
+ throw new TRPCError({
2112
+ code: 'FORBIDDEN',
2113
+ message: `Signing authority "${signingAuthorityName}" at endpoint "${ab.signingAuthority.endpoint}" not found for this profile`,
2114
+ });
2115
+ return {
2116
+ id: boost.id,
2117
+ signingAuthorityEndpoint: ab.signingAuthority.endpoint,
2118
+ signingAuthorityName,
2119
+ };
2120
+ })
2121
+ );
2122
+ const uniqueAdditions = additions.filter(
2123
+ (boost, index) =>
2124
+ !existingBoostIds.has(boost.id) &&
2125
+ additions.findIndex(candidate => candidate.id === boost.id) === index
2126
+ );
2127
+ await updateConsentFlowContractVersion(
2128
+ {
2129
+ uri: contractUri,
2130
+ changes: { autoboosts: [...version.autoboosts, ...uniqueAdditions] },
2131
+ reason: 'The contract added automatic credential delivery options.',
1828
2132
  },
1829
- }));
1830
-
1831
- await addAutoBoostsToContractDb(contract.id, normalizedAutoboosts, profile, ctx.domain);
2133
+ profile.profileId,
2134
+ ctx.domain
2135
+ );
1832
2136
 
1833
2137
  return true;
1834
2138
  }),
@@ -1842,7 +2146,7 @@ export const contractsRouter = t.router({
1842
2146
  tags: ['Contracts'],
1843
2147
  summary: 'Remove autoboosts from a contract',
1844
2148
  description:
1845
- 'Removes one or more autoboosts from an existing consent flow contract, identified by their boost URIs. The caller must be the contract owner or a designated writer.',
2149
+ 'Removes one or more autoboosts from an existing consent flow contract, identified by their boost URIs. Only the contract owner may change this configuration.',
1846
2150
  },
1847
2151
  requiredScope: 'contracts:write autoboosts:write',
1848
2152
  })
@@ -1863,18 +2167,32 @@ export const contractsRouter = t.router({
1863
2167
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
1864
2168
  }
1865
2169
 
1866
- const writers = await getWritersForContract(contract);
1867
- const isAuthorized = writers.some(writer => writer.profileId === profile.profileId);
1868
-
1869
- if (!isAuthorized) {
2170
+ if (!(await isProfileConsentFlowContractAdmin(profile, contract))) {
1870
2171
  throw new TRPCError({
1871
2172
  code: 'UNAUTHORIZED',
1872
2173
  message: 'You do not have permission to modify autoboosts for this contract.',
1873
2174
  });
1874
2175
  }
1875
-
1876
- await removeAutoBoostsFromContractDb(contract.id, boostUris, ctx.domain);
1877
-
2176
+ const version = await getConsentFlowContractVersion(
2177
+ contract.id,
2178
+ Number(contract.currentVersion ?? 1)
2179
+ );
2180
+ if (!version)
2181
+ throw new TRPCError({
2182
+ code: 'PRECONDITION_FAILED',
2183
+ message: 'Contract version snapshot is missing',
2184
+ });
2185
+ const removedIds = new Set(boostUris.map(uri => getIdFromUri(uri)));
2186
+ await updateConsentFlowContractVersion(
2187
+ {
2188
+ uri: contractUri,
2189
+ changes: {
2190
+ autoboosts: version.autoboosts.filter(boost => !removedIds.has(boost.id)),
2191
+ },
2192
+ },
2193
+ profile.profileId,
2194
+ ctx.domain
2195
+ );
1878
2196
  return true;
1879
2197
  }),
1880
2198