@learncard/network-brain-service 4.1.3 → 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 +143 -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
@@ -1,8 +1,9 @@
1
+ import { v4 as uuid } from 'uuid';
2
+ import { getConsentFlowContractVersionRecord } from '@accesslayer/consentflowcontract/versions';
1
3
  import { termsReferralSnapshotCypher } from '@helpers/consent-referral.helpers';
2
4
  import { appendConsentEvent, tryDispatchContractEvent } from '@helpers/contract-events.helpers';
3
5
  import { QueryBuilder, BindParam } from 'neogma';
4
6
  import { TRPCError } from '@trpc/server';
5
- import { v4 as uuid } from 'uuid';
6
7
  import {
7
8
  ConsentFlowTerms as ConsentFlowTermsType,
8
9
  ConsentFlowTransaction as ConsentFlowTransactionType,
@@ -31,6 +32,78 @@ import {
31
32
  runAudienceMutation,
32
33
  } from './recipients';
33
34
 
35
+ export const claimAutoBoostTransaction = async (
36
+ contractId: string,
37
+ termsId: string,
38
+ boostId: string,
39
+ allowConsumedOneTime = false
40
+ ) => {
41
+ const id = uuid();
42
+ const result = await neogma.queryRunner.run(
43
+ `MATCH (contract:ConsentFlowContract {id:$contractId})
44
+ SET contract.autoBoostClaimLock = coalesce(contract.autoBoostClaimLock, 0) + 1
45
+ WITH contract
46
+ MATCH (terms:ConsentFlowTerms {id:$termsId})-[:CONSENTS_TO]->(contract)
47
+ WHERE coalesce(contract.status, 'active') <> 'retired'
48
+ AND (terms.status = 'live' OR
49
+ ($allowConsumedOneTime AND terms.status = 'consumed' AND terms.oneTime = true))
50
+ AND coalesce(terms.consentedVersion, 1) = coalesce(contract.currentVersion, 1)
51
+ SET terms.autoBoostClaimLock = coalesce(terms.autoBoostClaimLock, 0) + 1
52
+ WITH terms
53
+ OPTIONAL MATCH (terms)<-[:IS_FOR]-(existing:ConsentFlowTransaction {boostId:$boostId})
54
+ OPTIONAL MATCH (terms)<-[:IS_FOR]-(legacy:ConsentFlowTransaction)-[:ISSUED_VIA_TRANSACTION]-
55
+ (credential:Credential)-[:INSTANCE_OF]->(:Boost {id:$boostId})
56
+ OPTIONAL MATCH (claim:ConsentFlowAutoBoostClaim {termsId:$termsId, boostId:$boostId})
57
+ WITH terms, existing, legacy, claim
58
+ WHERE existing IS NULL AND legacy IS NULL AND claim IS NULL
59
+ CREATE (:ConsentFlowAutoBoostClaim {id:$id, termsId:$termsId, boostId:$boostId})
60
+ RETURN $id AS id`,
61
+ { contractId, termsId, boostId, id, allowConsumedOneTime }
62
+ );
63
+ const created = result.records[0]?.get('id');
64
+ return created ? String(created) : null;
65
+ };
66
+
67
+ export const completeAutoBoostTransaction = async (
68
+ claimId: string,
69
+ termsId: string,
70
+ boostId: string
71
+ ) => {
72
+ await neogma.queryRunner.run(
73
+ `MATCH (terms:ConsentFlowTerms {id:$termsId})
74
+ MATCH (claim:ConsentFlowAutoBoostClaim {id:$claimId})
75
+ MATCH (terms)<-[:IS_FOR]-(transaction:ConsentFlowTransaction)
76
+ <-[:ISSUED_VIA_TRANSACTION]-(credential:Credential)-[:INSTANCE_OF]->(:Boost {id:$boostId})
77
+ WITH claim, collect(DISTINCT transaction) AS transactions
78
+ FOREACH (transaction IN transactions | SET transaction.boostId = $boostId)
79
+ DETACH DELETE claim`,
80
+ { termsId, claimId, boostId }
81
+ );
82
+ };
83
+
84
+ export const releaseAutoBoostClaim = async (claimId: string) => {
85
+ await neogma.queryRunner.run(
86
+ `MATCH (claim:ConsentFlowAutoBoostClaim {id:$claimId}) DETACH DELETE claim`,
87
+ { claimId }
88
+ );
89
+ };
90
+ export const isAutoBoostConsentCurrent = async (
91
+ contractId: string,
92
+ termsId: string,
93
+ allowConsumedOneTime = false
94
+ ) => {
95
+ const result = await neogma.queryRunner.run(
96
+ `MATCH (contract:ConsentFlowContract {id:$contractId})
97
+ MATCH (terms:ConsentFlowTerms {id:$termsId})-[:CONSENTS_TO]->(contract)
98
+ RETURN coalesce(contract.status, 'active') <> 'retired'
99
+ AND (terms.status = 'live' OR
100
+ ($allowConsumedOneTime AND terms.status = 'consumed' AND terms.oneTime = true))
101
+ AND coalesce(terms.consentedVersion, 1) = coalesce(contract.currentVersion, 1) AS current`,
102
+ { contractId, termsId, allowConsumedOneTime }
103
+ );
104
+ return result.records[0]?.get('current') === true;
105
+ };
106
+
34
107
  export const reconsentTerms = async (
35
108
  relationship: {
36
109
  terms: DbTermsType;
@@ -46,6 +119,7 @@ export const reconsentTerms = async (
46
119
  audienceVersion,
47
120
  expectedRequestId,
48
121
  smartResumeFingerprint,
122
+ acceptVersion,
49
123
  }: {
50
124
  terms: ConsentFlowTermsType;
51
125
  expiresAt?: string;
@@ -54,12 +128,23 @@ export const reconsentTerms = async (
54
128
  audienceVersion?: number;
55
129
  expectedRequestId?: string;
56
130
  smartResumeFingerprint?: string;
131
+ acceptVersion?: number;
57
132
  },
58
133
  domain: string
59
134
  ): Promise<boolean> => {
135
+ const currentVersion = Number(relationship.contract.currentVersion ?? 1);
136
+ const version = acceptVersion ?? currentVersion;
137
+ if (!Number.isInteger(version) || version !== currentVersion) {
138
+ throw new TRPCError({
139
+ code: 'CONFLICT',
140
+ message: 'Only the latest contract version can be accepted',
141
+ });
142
+ }
143
+ const effectiveOneTime = oneTime ?? relationship.terms.oneTime ?? false;
144
+
60
145
  const transaction = {
61
146
  id: uuid(),
62
- action: 'consent',
147
+ action: 'reconsent',
63
148
  date: new Date().toISOString(),
64
149
  ...(typeof expiresAt === 'string' ? { expiresAt } : {}),
65
150
  ...(typeof oneTime === 'boolean' ? { oneTime } : {}),
@@ -81,6 +166,7 @@ export const reconsentTerms = async (
81
166
  lockContractAudience(
82
167
  new QueryBuilder(
83
168
  new BindParam({
169
+ expectedContractVersion: version,
84
170
  audienceVersion: audienceVersion ?? null,
85
171
  expectedRequestId: expectedRequestId ?? null,
86
172
  consenterProfileId: relationship.consenter.profileId,
@@ -100,7 +186,12 @@ export const reconsentTerms = async (
100
186
  }
101
187
  : {}),
102
188
  updatedAt: transaction.date,
103
- status: oneTime ? 'stale' : 'live',
189
+ status: effectiveOneTime ? 'consumed' : 'live',
190
+
191
+ consentedVersion: version,
192
+ lastReviewedVersion: version,
193
+ reconsentDeadline: null,
194
+ newOptionsUntil: null,
104
195
  ...(typeof expiresAt === 'string' ? { expiresAt } : {}),
105
196
  ...(typeof oneTime === 'boolean' ? { oneTime } : {}),
106
197
  },
@@ -108,7 +199,9 @@ export const reconsentTerms = async (
108
199
  ),
109
200
  relationship.contract.id
110
201
  )
111
- .where(`${audienceVersionWhere} AND ${consentMutationWhere}`)
202
+ .where(
203
+ `${audienceVersionWhere} AND ${consentMutationWhere} AND coalesce(contract.status, 'active') <> 'retired' AND coalesce(contract.currentVersion, 1) = $expectedContractVersion`
204
+ )
112
205
  .set('contract.hasConsented = true')
113
206
  .with('contract')
114
207
  .match({
@@ -143,28 +236,34 @@ export const reconsentTerms = async (
143
236
  );
144
237
  assertAudienceMutation(result.records.length);
145
238
 
146
- // Process Auto-Boosts
147
- const autoBoostsResult = await ConsentFlowContract.findRelationships({
148
- alias: 'autoReceive',
149
- where: { source: { id: relationship.contract.id } },
150
- });
239
+ const versionSnapshot = await getConsentFlowContractVersionRecord(
240
+ relationship.contract.id,
241
+ version
242
+ );
243
+ const configuredAutoBoosts = new Map(
244
+ (versionSnapshot?.autoboosts ?? []).map(config => [config.id, config])
245
+ );
246
+ const autoBoostsResult = (
247
+ await ConsentFlowContract.findRelationships({
248
+ alias: 'autoReceive',
249
+ where: { source: { id: relationship.contract.id } },
250
+ })
251
+ ).filter(boost => configuredAutoBoosts.has(boost.target.id));
151
252
 
152
253
  if (autoBoostsResult.length > 0) {
153
254
  // For each auto-boost, issue it to the consenter
154
255
  await Promise.all(
155
256
  autoBoostsResult.map(async boostRel => {
257
+ let claimId: string | undefined;
258
+ let sendStarted = false;
156
259
  try {
157
260
  const boost = boostRel.target;
158
261
 
159
- // Get the signing authority information from the relationship
160
- const signingAuthorityEndpoint =
161
- boostRel.relationship?.signingAuthorityEndpoint || 'default';
162
- const signingAuthorityName =
163
- boostRel.relationship?.signingAuthorityName || 'default';
164
-
262
+ const config = configuredAutoBoosts.get(boost.id)!;
263
+ const signingAuthorityEndpoint = config.signingAuthorityEndpoint || 'default';
264
+ const signingAuthorityName = config.signingAuthorityName || 'default';
165
265
  const issuer =
166
- (boostRel.relationship?.issuer &&
167
- (await getProfileByProfileId(boostRel.relationship.issuer))) ||
266
+ (config.issuer && (await getProfileByProfileId(config.issuer))) ||
168
267
  relationship.contractOwner;
169
268
 
170
269
  if (terms.deniedWriters?.includes(issuer.profileId)) return;
@@ -195,6 +294,14 @@ export const reconsentTerms = async (
195
294
  // Only issue autoboost if write permission is explicitly granted (true)
196
295
  if (categoryWritePermission !== true) return;
197
296
  }
297
+ claimId =
298
+ (await claimAutoBoostTransaction(
299
+ relationship.contract.id,
300
+ relationship.terms.id,
301
+ String(boost.dataValues.id),
302
+ effectiveOneTime
303
+ )) ?? undefined;
304
+ if (!claimId) return;
198
305
  // Get boost instance
199
306
  const boostCredential = JSON.parse(boost.dataValues?.boost) as UnsignedVC | VC;
200
307
 
@@ -230,35 +337,19 @@ export const reconsentTerms = async (
230
337
  [getDidWeb(domain, relationship.contractOwner.profileId)]
231
338
  );
232
339
 
233
- // Create transaction to record the boost issuance
234
- const boostTransaction = {
235
- id: uuid(),
236
- action: 'write',
237
- date: new Date().toISOString(),
238
- } as const satisfies ConsentFlowTransactionType;
239
-
240
- // Create the transaction in the database
241
- await new QueryBuilder()
242
- .match({
243
- model: ConsentFlowTerms,
244
- where: { id: relationship.terms.id },
245
- identifier: 'terms',
246
- })
247
- .create({
248
- related: [
249
- {
250
- identifier: 'boostTransaction',
251
- model: ConsentFlowTransaction,
252
- properties: boostTransaction,
253
- },
254
- ConsentFlowTransaction.getRelationshipByAlias('isFor'),
255
- { identifier: 'terms' },
256
- ],
257
- })
258
- .raw(termsReferralSnapshotCypher('boostTransaction'))
259
- .run();
260
-
261
340
  // Send the boost to the consenter
341
+ if (
342
+ !(await isAutoBoostConsentCurrent(
343
+ relationship.contract.id,
344
+ relationship.terms.id,
345
+ effectiveOneTime
346
+ ))
347
+ ) {
348
+ await releaseAutoBoostClaim(claimId);
349
+ claimId = undefined;
350
+ return;
351
+ }
352
+ sendStarted = true;
262
353
  await sendBoost({
263
354
  from: { type: 'profile', profile: relationship.contractOwner },
264
355
  to: relationship.consenter,
@@ -267,15 +358,41 @@ export const reconsentTerms = async (
267
358
  domain,
268
359
  skipNotification: true,
269
360
  autoAcceptCredential: false,
270
- contractTerms: relationship.terms,
361
+ allowConsumedOneTime: effectiveOneTime,
362
+ contractTerms: {
363
+ ...relationship.terms,
364
+ terms,
365
+ oneTime: oneTime ?? relationship.terms.oneTime,
366
+ consentedVersion: version,
367
+ status: effectiveOneTime ? 'consumed' : 'live',
368
+ },
369
+ contractWriterProfileId: issuer.profileId,
271
370
  });
371
+ await completeAutoBoostTransaction(
372
+ claimId,
373
+ relationship.terms.id,
374
+ String(boost.dataValues.id)
375
+ );
376
+ claimId = undefined;
272
377
  } catch (error) {
273
- console.error('Error processing auto-boost:', error);
378
+ if (claimId && sendStarted) {
379
+ console.error(
380
+ 'Auto-boost claim send failed; delivery may be ambiguous',
381
+ {
382
+ claimId,
383
+ termsId: relationship.terms.id,
384
+ boostId: String(boostRel.target.dataValues.id),
385
+ },
386
+ error
387
+ );
388
+ } else {
389
+ console.error('Error processing auto-boost:', error);
390
+ }
391
+ if (claimId && !sendStarted) await releaseAutoBoostClaim(claimId);
274
392
  }
275
393
  })
276
394
  );
277
395
  }
278
-
279
396
  await tryDispatchContractEvent(transaction.id);
280
397
 
281
398
  return result.summary.counters.containsUpdates();
@@ -303,6 +420,17 @@ export const updateTerms = async (
303
420
  },
304
421
  domain: string
305
422
  ): Promise<boolean> => {
423
+ if (
424
+ relationship.terms.status === 'needs_reconsent' ||
425
+ relationship.terms.status === 'suspended'
426
+ ) {
427
+ throw new TRPCError({
428
+ code: 'FORBIDDEN',
429
+ message: 'User has not yet approved the latest version of this contract',
430
+ });
431
+ }
432
+ const effectiveOneTime = oneTime ?? relationship.terms.oneTime ?? false;
433
+
306
434
  const transaction = {
307
435
  id: uuid(),
308
436
  action: 'update',
@@ -332,10 +460,13 @@ export const updateTerms = async (
332
460
  const paramsForSet = {
333
461
  ...newFlatInner,
334
462
  updatedAt: transaction.date,
335
- status: oneTime ? 'stale' : 'live',
463
+ status: effectiveOneTime ? 'consumed' : 'live',
464
+
465
+ lastReviewedVersion: Number(relationship.contract.currentVersion ?? 1),
336
466
  ...(typeof expiresAt === 'string' ? { expiresAt } : {}),
337
467
  ...(typeof oneTime === 'boolean' ? { oneTime } : {}),
338
- ...Object.fromEntries(keysToRemove.map(k => [k, null as any])),
468
+ newOptionsUntil: null,
469
+ ...Object.fromEntries(keysToRemove.map(k => [k, null])),
339
470
  };
340
471
 
341
472
  const result = await runAudienceMutation(
@@ -344,13 +475,16 @@ export const updateTerms = async (
344
475
  new QueryBuilder(
345
476
  new BindParam({
346
477
  params: paramsForSet,
478
+ expectedContractVersion: Number(relationship.contract.currentVersion ?? 1),
347
479
  audienceVersion: audienceVersion ?? null,
348
480
  termsMutationVersion: Number(relationship.terms.mutationVersion ?? 0),
349
481
  })
350
482
  ),
351
483
  relationship.contract.id
352
484
  )
353
- .where(audienceVersionWhere)
485
+ .where(
486
+ `${audienceVersionWhere} AND coalesce(contract.status, 'active') <> 'retired' AND coalesce(contract.currentVersion, 1) = $expectedContractVersion`
487
+ )
354
488
  .set('contract.hasConsented = true')
355
489
  .with('contract')
356
490
  .match({
@@ -385,26 +519,33 @@ export const updateTerms = async (
385
519
  );
386
520
  assertAudienceMutation(result.records.length);
387
521
 
388
- // Process Auto-Boosts (for updates too)
389
- const autoBoosts = await ConsentFlowContract.findRelationships({
390
- alias: 'autoReceive',
391
- where: { source: { id: relationship.contract.id } },
392
- });
522
+ const latestVersion = Number(relationship.contract.currentVersion ?? 1);
523
+ const versionSnapshot = await getConsentFlowContractVersionRecord(
524
+ relationship.contract.id,
525
+ latestVersion
526
+ );
527
+ const configuredAutoBoosts = new Map(
528
+ (versionSnapshot?.autoboosts ?? []).map(config => [config.id, config])
529
+ );
530
+ const autoBoosts = (
531
+ await ConsentFlowContract.findRelationships({
532
+ alias: 'autoReceive',
533
+ where: { source: { id: relationship.contract.id } },
534
+ })
535
+ ).filter(boost => configuredAutoBoosts.has(boost.target.id));
393
536
 
394
537
  if (autoBoosts.length > 0) {
395
538
  // For each auto-boost, issue it to the consenter
396
539
  await Promise.all(
397
540
  autoBoosts.map(async boost => {
541
+ let claimId: string | undefined;
542
+ let sendStarted = false;
398
543
  try {
399
- // Get the signing authority information from the relationship
400
- const signingAuthorityEndpoint =
401
- boost.relationship?.signingAuthorityEndpoint || 'default';
402
- const signingAuthorityName =
403
- boost.relationship?.signingAuthorityName || 'default';
404
-
544
+ const config = configuredAutoBoosts.get(boost.target.id)!;
545
+ const signingAuthorityEndpoint = config.signingAuthorityEndpoint || 'default';
546
+ const signingAuthorityName = config.signingAuthorityName || 'default';
405
547
  const issuer =
406
- (boost.relationship?.issuer &&
407
- (await getProfileByProfileId(boost.relationship.issuer))) ||
548
+ (config.issuer && (await getProfileByProfileId(config.issuer))) ||
408
549
  relationship.contractOwner;
409
550
 
410
551
  if (terms.deniedWriters?.includes(issuer.profileId)) return;
@@ -434,6 +575,14 @@ export const updateTerms = async (
434
575
  // Only issue autoboost if write permission is explicitly granted (true)
435
576
  if (categoryWritePermission !== true) return;
436
577
  }
578
+ claimId =
579
+ (await claimAutoBoostTransaction(
580
+ relationship.contract.id,
581
+ relationship.terms.id,
582
+ String(boost.target.id),
583
+ effectiveOneTime
584
+ )) ?? undefined;
585
+ if (!claimId) return;
437
586
 
438
587
  // Get boost instance
439
588
  const boostCredential = JSON.parse(boost.target.boost) as UnsignedVC | VC;
@@ -470,47 +619,57 @@ export const updateTerms = async (
470
619
  [getDidWeb(domain, relationship.contractOwner.profileId)]
471
620
  );
472
621
 
473
- // Create transaction to record the boost issuance
474
- const boostTransaction = {
475
- id: uuid(),
476
- action: 'write',
477
- date: new Date().toISOString(),
478
- } as const satisfies ConsentFlowTransactionType;
479
-
480
- // Create the transaction in the database
481
- await new QueryBuilder()
482
- .match({
483
- model: ConsentFlowTerms,
484
- where: { id: relationship.terms.id },
485
- identifier: 'terms',
486
- })
487
- .create({
488
- related: [
489
- {
490
- identifier: 'boostTransaction',
491
- model: ConsentFlowTransaction,
492
- properties: boostTransaction,
493
- },
494
- ConsentFlowTransaction.getRelationshipByAlias('isFor'),
495
- { identifier: 'terms' },
496
- ],
497
- })
498
- .raw(termsReferralSnapshotCypher('boostTransaction'))
499
- .run();
500
-
501
- // Send the boost to the consenter
622
+ if (
623
+ !(await isAutoBoostConsentCurrent(
624
+ relationship.contract.id,
625
+ relationship.terms.id,
626
+ effectiveOneTime
627
+ ))
628
+ ) {
629
+ await releaseAutoBoostClaim(claimId);
630
+ claimId = undefined;
631
+ return;
632
+ }
633
+ sendStarted = true;
502
634
  await sendBoost({
503
635
  from: { type: 'profile', profile: relationship.contractOwner },
504
636
  to: relationship.consenter,
505
637
  boost: boost.target,
506
638
  credential: vc,
507
639
  domain,
508
- skipNotification: false,
509
- autoAcceptCredential: true,
510
- contractTerms: relationship.terms,
640
+ skipNotification: true,
641
+ autoAcceptCredential: false,
642
+ allowConsumedOneTime: effectiveOneTime,
643
+ contractTerms: {
644
+ ...relationship.terms,
645
+ terms,
646
+ oneTime: oneTime ?? relationship.terms.oneTime,
647
+ consentedVersion: latestVersion,
648
+ status: effectiveOneTime ? 'consumed' : 'live',
649
+ },
650
+ contractWriterProfileId: issuer.profileId,
511
651
  });
652
+ await completeAutoBoostTransaction(
653
+ claimId,
654
+ relationship.terms.id,
655
+ String(boost.target.id)
656
+ );
657
+ claimId = undefined;
512
658
  } catch (error) {
513
- console.error('Error processing auto-boost:', error);
659
+ if (claimId && sendStarted) {
660
+ console.error(
661
+ 'Auto-boost claim send failed; delivery may be ambiguous',
662
+ {
663
+ claimId,
664
+ termsId: relationship.terms.id,
665
+ boostId: String(boost.target.id),
666
+ },
667
+ error
668
+ );
669
+ } else {
670
+ console.error('Error processing auto-boost:', error);
671
+ }
672
+ if (claimId && !sendStarted) await releaseAutoBoostClaim(claimId);
514
673
  }
515
674
  })
516
675
  );
@@ -593,6 +752,15 @@ export const syncCredentialsToContract = async (
593
752
  audienceVersion?: number,
594
753
  domain?: string
595
754
  ): Promise<boolean> => {
755
+ if (
756
+ relationship.terms.status === 'needs_reconsent' ||
757
+ relationship.terms.status === 'suspended'
758
+ ) {
759
+ throw new TRPCError({
760
+ code: 'FORBIDDEN',
761
+ message: 'User has not yet approved the latest version of this contract',
762
+ });
763
+ }
596
764
  // First define the transaction with sync terms data
597
765
  // Create a structure that matches the terms format with multiple categories
598
766
  const syncTerms: ConsentFlowTermsType = {
@@ -659,6 +827,7 @@ export const syncCredentialsToContract = async (
659
827
  lockContractAudience(
660
828
  new QueryBuilder(
661
829
  new BindParam({
830
+ expectedContractVersion: Number(relationship.contract.currentVersion ?? 1),
662
831
  audienceVersion: audienceVersion ?? null,
663
832
  termsMutationVersion: Number(relationship.terms.mutationVersion ?? 0),
664
833
  now: new Date().toISOString(),
@@ -671,7 +840,9 @@ export const syncCredentialsToContract = async (
671
840
  ),
672
841
  relationship.contract.id
673
842
  )
674
- .where(audienceVersionWhere)
843
+ .where(
844
+ `${audienceVersionWhere} AND coalesce(contract.status, 'active') <> 'retired' AND coalesce(contract.currentVersion, 1) = $expectedContractVersion`
845
+ )
675
846
  .match({
676
847
  model: ConsentFlowTerms,
677
848
  where: { id: relationship.terms.id },