@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
@@ -1,4 +1,9 @@
1
- import { isServiceProfileExemptFromGuardianship } from '@helpers/profile.helpers';
1
+ import { TRPCError } from '@trpc/server';
2
+ import {
3
+ getConsentFlowContractVersionRecords,
4
+ getCumulativeContractUpdateById,
5
+ getCumulativeContractUpdates,
6
+ } from '@accesslayer/consentflowcontract/versions';
2
7
  import { QueryBuilder, BindParam } from 'neogma';
3
8
  import mapValues from 'lodash/mapValues';
4
9
  import {
@@ -34,12 +39,14 @@ import {
34
39
  ConsentFlowTransactionsQuery,
35
40
  HolderExportMetadata,
36
41
  LCNProfile,
42
+ ConsentFlowContractDetails,
37
43
  } from '@learncard/types';
38
44
  import { ConsentFlowGuardianApprovalValidator } from '@learncard/types';
39
45
  import { FlatBoostType } from 'types/boost';
40
46
  import { constructUri, getIdFromUri } from '@helpers/uri.helpers';
41
47
  import { flattenObject, inflateObject } from '@helpers/objects.helpers';
42
- import { shouldIncludeCategory } from '@helpers/contract.helpers';
48
+ import { areTermsValid, shouldIncludeCategory } from '@helpers/contract.helpers';
49
+ import { isServiceProfileExemptFromGuardianship } from '@helpers/profile.helpers';
43
50
  import { ProfileType } from 'types/profile';
44
51
  import { CredentialType } from 'types/credential';
45
52
  import { getBoostUri } from '@helpers/boost.helpers';
@@ -123,9 +130,16 @@ export const getContractDetailsById = async (
123
130
  profile: LCNProfile;
124
131
  }>(await query.return('contract, profile').run());
125
132
 
126
- return result.length > 0
127
- ? { contract: inflateObject(result[0]!.contract), contractOwner: result[0]!.profile }
128
- : null;
133
+ if (!result.length) return null;
134
+ const rawContract = DbContractValidator.parse(inflateObject(result[0]!.contract));
135
+ return {
136
+ contract: {
137
+ ...rawContract,
138
+ version: Number(rawContract.currentVersion ?? 1),
139
+ status: rawContract.status ?? 'active',
140
+ },
141
+ contractOwner: result[0]!.profile,
142
+ };
129
143
  };
130
144
 
131
145
  export const getContractDetailsByUri = async (
@@ -172,6 +186,7 @@ export const getContractTermsById = async (
172
186
  consenter: LCNProfile;
173
187
  contract: DbContractType;
174
188
  contractOwner: LCNProfile;
189
+ cumulativeUpdate?: DbContractType['latestUpdate'];
175
190
  } | null> => {
176
191
  const result = convertQueryResultToPropertiesObjectArray<{
177
192
  consenter: LCNProfile;
@@ -195,14 +210,28 @@ export const getContractTermsById = async (
195
210
  .run()
196
211
  );
197
212
 
198
- return result.length > 0
199
- ? {
200
- consenter: result[0]!.consenter,
201
- terms: inflateObject(result[0]!.terms),
202
- contract: inflateObject(result[0]!.contract),
203
- contractOwner: result[0]!.contractOwner,
204
- }
205
- : null;
213
+ if (!result.length) return null;
214
+ const terms = DbTermsValidator.parse(inflateObject(result[0]!.terms));
215
+ const contract = DbContractValidator.parse(inflateObject(result[0]!.contract));
216
+ const acceptedVersion = Number(terms.lastReviewedVersion ?? terms.consentedVersion ?? 1);
217
+ const currentVersion = Number(contract.currentVersion ?? 1);
218
+ const cumulativeUpdate =
219
+ ['live', 'needs_reconsent', 'suspended'].includes(terms.status) &&
220
+ acceptedVersion < currentVersion
221
+ ? await getCumulativeContractUpdateById(
222
+ contract.id,
223
+ acceptedVersion,
224
+ terms.status,
225
+ terms.reconsentDeadline
226
+ )
227
+ : undefined;
228
+ return {
229
+ consenter: result[0]!.consenter,
230
+ terms,
231
+ contract,
232
+ contractOwner: result[0]!.contractOwner,
233
+ ...(cumulativeUpdate ? { cumulativeUpdate } : {}),
234
+ };
206
235
  };
207
236
 
208
237
  export const getContractTermsByUri = async (
@@ -212,6 +241,7 @@ export const getContractTermsByUri = async (
212
241
  consenter: LCNProfile;
213
242
  contract: DbContractType;
214
243
  contractOwner: LCNProfile;
244
+ cumulativeUpdate?: DbContractType['latestUpdate'];
215
245
  } | null> => {
216
246
  return getContractTermsById(getIdFromUri(uri));
217
247
  };
@@ -262,10 +292,17 @@ export const getConsentFlowContractsForProfile = async (
262
292
  .run()
263
293
  );
264
294
 
265
- return results.map(({ contract, boost }) => ({
266
- contract: inflateObject(contract),
267
- autoBoosts: boost.map(boost => getBoostUri(boost.properties.id, domain)),
268
- }));
295
+ return results.map(({ contract, boost }) => {
296
+ const details = DbContractValidator.parse(inflateObject(contract));
297
+ return {
298
+ contract: {
299
+ ...details,
300
+ version: Number(details.currentVersion ?? 1),
301
+ status: details.status ?? 'active',
302
+ },
303
+ autoBoosts: boost.map(boost => getBoostUri(boost.properties.id, domain)),
304
+ };
305
+ });
269
306
  };
270
307
 
271
308
  export const getConsentedContractsForProfile = async (
@@ -283,10 +320,16 @@ export const getConsentedContractsForProfile = async (
283
320
  terms: DbTermsType;
284
321
  autoBoosts: string[];
285
322
  recipients: ProfileType[];
323
+ cumulativeUpdate?: ConsentFlowContractDetails['latestUpdate'];
286
324
  }[]
287
325
  > => {
326
+ const decodedCursor = decodeConsentedDataCursor(cursor);
288
327
  let query = new QueryBuilder(
289
- new BindParam({ params: flattenObject({ terms: matchQuery }), cursor })
328
+ new BindParam({
329
+ params: flattenObject({ terms: matchQuery }),
330
+ cursorDate: decodedCursor?.updatedAt,
331
+ cursorId: decodedCursor?.id,
332
+ })
290
333
  )
291
334
  .match({
292
335
  related: [
@@ -301,7 +344,11 @@ export const getConsentedContractsForProfile = async (
301
344
  })
302
345
  .where('all(key IN keys($params) WHERE terms[key] = $params[key])');
303
346
 
304
- query = cursor ? query.raw('AND terms.updatedAt > $cursor') : query;
347
+ query = decodedCursor
348
+ ? query.raw(
349
+ "AND (coalesce(terms.updatedAt, terms.createdAt, '') > $cursorDate OR (coalesce(terms.updatedAt, terms.createdAt, '') = $cursorDate AND terms.id > $cursorId))"
350
+ )
351
+ : query;
305
352
 
306
353
  query = query.with('contract, owner, terms').match({
307
354
  optional: true,
@@ -330,18 +377,54 @@ export const getConsentedContractsForProfile = async (
330
377
  ],
331
378
  })
332
379
  .return('contract, owner, terms, boost, COLLECT(DISTINCT recipient) as recipients')
333
- .orderBy('terms.updatedAt')
380
+ .orderBy("coalesce(terms.updatedAt, terms.createdAt, '') ASC, terms.id ASC")
334
381
  .limit(limit)
335
382
  .run()
336
383
  );
337
384
 
338
- return results.map(result => ({
339
- ...result,
340
- recipients: result.recipients.map(recipient => recipient.properties),
341
- contract: inflateObject(result.contract),
342
- terms: inflateObject(result.terms),
343
- autoBoosts: result.boost.map(boost => getBoostUri(boost.properties.id, domain)),
385
+ const parsedResults = results.map(result => ({
386
+ result,
387
+ details: DbContractValidator.parse(inflateObject(result.contract)),
388
+ terms: DbTermsValidator.parse(inflateObject(result.terms)),
344
389
  }));
390
+ const updates = await getCumulativeContractUpdates(
391
+ parsedResults.flatMap(({ details, terms }, index) => {
392
+ const acceptedVersion = Number(
393
+ terms.lastReviewedVersion ?? terms.consentedVersion ?? 1
394
+ );
395
+ const currentVersion = Number(details.currentVersion ?? 1);
396
+ if (
397
+ !['live', 'needs_reconsent', 'suspended'].includes(terms.status) ||
398
+ acceptedVersion >= currentVersion
399
+ )
400
+ return [];
401
+ return [
402
+ {
403
+ key: String(index),
404
+ contractId: details.id,
405
+ acceptedVersion,
406
+ status: terms.status,
407
+ reconsentDeadline: terms.reconsentDeadline,
408
+ },
409
+ ];
410
+ })
411
+ );
412
+
413
+ return parsedResults.map(({ result, details, terms }, index) => {
414
+ const cumulativeUpdate = updates.get(String(index));
415
+ return {
416
+ ...result,
417
+ recipients: result.recipients.map(recipient => recipient.properties),
418
+ contract: {
419
+ ...details,
420
+ version: Number(details.currentVersion ?? 1),
421
+ status: details.status ?? 'active',
422
+ },
423
+ terms,
424
+ ...(cumulativeUpdate ? { cumulativeUpdate } : {}),
425
+ autoBoosts: result.boost.map(boost => getBoostUri(boost.properties.id, domain)),
426
+ };
427
+ });
345
428
  };
346
429
 
347
430
  const HOLDER_EXPORT_PAGE_SIZE = 100;
@@ -397,8 +480,11 @@ export const getHolderExportMetadataForProfile = async (
397
480
  );
398
481
 
399
482
  transactionCursor =
400
- transactionResults.length > transactionLimit
401
- ? transactionPage.at(-1)?.date
483
+ transactionResults.length > transactionLimit && transactionPage.length
484
+ ? encodeConsentedDataCursor(
485
+ transactionPage.at(-1)!.date,
486
+ transactionPage.at(-1)!.id
487
+ )
402
488
  : undefined;
403
489
 
404
490
  if (!transactionCursor) break;
@@ -411,11 +497,21 @@ export const getHolderExportMetadataForProfile = async (
411
497
  );
412
498
  }
413
499
 
500
+ const cumulativeUpdate = record.cumulativeUpdate;
414
501
  consentRecords.push({
502
+ ...(cumulativeUpdate ? { cumulativeUpdate } : {}),
415
503
  termsUri: constructUri('terms', record.terms.id, domain),
416
504
  status: record.terms.status,
505
+ consentedVersion: record.terms.consentedVersion!,
506
+ ...(record.terms.reconsentDeadline
507
+ ? { reconsentDeadline: record.terms.reconsentDeadline }
508
+ : {}),
509
+ ...(record.terms.newOptionsUntil
510
+ ? { newOptionsUntil: record.terms.newOptionsUntil }
511
+ : {}),
417
512
  contract: {
418
513
  contract: record.contract.contract,
514
+ version: Number(record.contract.currentVersion ?? 1),
419
515
  name: record.contract.name,
420
516
  subtitle: record.contract.subtitle,
421
517
  description: record.contract.description,
@@ -446,7 +542,13 @@ export const getHolderExportMetadataForProfile = async (
446
542
  });
447
543
  }
448
544
 
449
- cursor = records.length > pageLimit ? page.at(-1)?.terms.updatedAt : undefined;
545
+ cursor =
546
+ records.length > pageLimit && page.length
547
+ ? encodeConsentedDataCursor(
548
+ page.at(-1)!.terms.updatedAt ?? page.at(-1)!.terms.createdAt ?? '',
549
+ page.at(-1)!.terms.id
550
+ )
551
+ : undefined;
450
552
 
451
553
  if (!cursor) break;
452
554
  }
@@ -493,7 +595,7 @@ const projectConsentedData = (
493
595
  query: ConsentFlowDataQuery,
494
596
  now: number
495
597
  ): ConsentFlowContractData => ({
496
- date: term.updatedAt!,
598
+ date: term.updatedAt ?? term.createdAt ?? '',
497
599
  credentials: {
498
600
  categories: mapValues(term.terms.read.credentials?.categories ?? {}, (category, name) =>
499
601
  term.terms.read.credentials.sharing !== false &&
@@ -511,11 +613,49 @@ const projectConsentedData = (
511
613
  * Do not cap scanned candidates: doing so would silently hide valid matches later in a page.
512
614
  * Expiry and negative category filters still require the authoritative JS permission check.
513
615
  */
616
+ const CONSENT_DATA_CURSOR_PREFIX = 'v1.';
617
+
618
+ const decodeConsentedDataCursor = (
619
+ cursor?: string
620
+ ): { updatedAt: string; id: string } | undefined => {
621
+ if (!cursor) return undefined;
622
+ try {
623
+ if (!cursor.startsWith(CONSENT_DATA_CURSOR_PREFIX)) throw new Error('Invalid cursor');
624
+ const encoded = cursor.slice(CONSENT_DATA_CURSOR_PREFIX.length);
625
+ const decoded = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
626
+ if (
627
+ !Array.isArray(decoded) ||
628
+ decoded.length !== 2 ||
629
+ typeof decoded[0] !== 'string' ||
630
+ typeof decoded[1] !== 'string' ||
631
+ (!Number.isFinite(Date.parse(decoded[0])) && decoded[0] !== '') ||
632
+ !decoded[1] ||
633
+ Buffer.from(JSON.stringify(decoded), 'utf8').toString('base64url') !== encoded
634
+ )
635
+ throw new Error('Invalid cursor');
636
+ return { updatedAt: decoded[0], id: decoded[1] };
637
+ } catch {
638
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid consent cursor' });
639
+ }
640
+ };
641
+
642
+ export const encodeConsentedDataCursor = (updatedAt: string, id: string): string =>
643
+ `${CONSENT_DATA_CURSOR_PREFIX}${Buffer.from(JSON.stringify([updatedAt, id]), 'utf8').toString(
644
+ 'base64url'
645
+ )}`;
646
+
647
+ type ConsentedDataPage = {
648
+ records: ConsentFlowContractData[];
649
+ hasMore: boolean;
650
+ nextCursor?: string;
651
+ };
652
+
514
653
  const getPermittedContractData = async (
515
654
  viewerProfileId: string,
516
655
  { query = {}, limit, cursor }: { query?: ConsentFlowDataQuery; limit: number; cursor?: string },
517
656
  contractId?: string
518
- ): Promise<ConsentFlowContractData[]> => {
657
+ ): Promise<ConsentedDataPage> => {
658
+ const decodedCursor = decodeConsentedDataCursor(cursor);
519
659
  const now = Date.now();
520
660
  const records: ConsentFlowContractData[] = [];
521
661
  const personalFilters = Object.entries(query.personal ?? {}).map(([name, required]) => ({
@@ -532,7 +672,12 @@ const getPermittedContractData = async (
532
672
  let offset = 0;
533
673
  while (records.length < limit) {
534
674
  const dbQuery = new QueryBuilder(
535
- new BindParam({ cursor, personalFilters, sharedCategories })
675
+ new BindParam({
676
+ cursorDate: decodedCursor?.updatedAt,
677
+ cursorId: decodedCursor?.id,
678
+ personalFilters,
679
+ sharedCategories,
680
+ })
536
681
  )
537
682
  .match({
538
683
  related: [
@@ -548,8 +693,11 @@ const getPermittedContractData = async (
548
693
  ],
549
694
  })
550
695
  .where(
551
- "(terms.status = 'live' OR (terms.status = 'stale' AND terms.oneTime = true))" +
552
- (cursor ? ' AND terms.updatedAt < $cursor' : '') +
696
+ "(coalesce(contract.status, 'active') <> 'retired') AND " +
697
+ "(terms.status IN ['live', 'needs_reconsent', 'suspended'] OR (terms.status = 'consumed' AND terms.oneTime = true))" +
698
+ (decodedCursor
699
+ ? " AND (coalesce(terms.updatedAt, terms.createdAt, '') < $cursorDate OR (coalesce(terms.updatedAt, terms.createdAt, '') = $cursorDate AND terms.id > $cursorId))"
700
+ : '') +
553
701
  ' AND all(filter IN $personalFilters WHERE filter.required = (terms[filter.key] IS NOT NULL))' +
554
702
  ' AND (size($sharedCategories) = 0 OR coalesce(terms.`terms.read.credentials.sharing`, true) = true)' +
555
703
  ' AND all(category IN $sharedCategories WHERE terms[category.sharingKey] = true AND' +
@@ -561,41 +709,79 @@ const getPermittedContractData = async (
561
709
  }>(
562
710
  await dbQuery
563
711
  .return('DISTINCT terms, contract')
564
- .orderBy('terms.updatedAt DESC, terms.id ASC')
712
+ .orderBy("coalesce(terms.updatedAt, terms.createdAt, '') DESC, terms.id ASC")
565
713
  .skip(offset)
566
714
  .limit(batchSize)
567
715
  .run()
568
716
  );
569
- for (const result of results) {
717
+ const validResults = results.flatMap((result, index) => {
570
718
  const term = DbTermsValidator.safeParse(inflateObject(result.terms));
571
719
  const contract = DbContractValidator.safeParse(inflateObject(result.contract));
572
720
  if (
573
721
  !term.success ||
574
722
  !contract.success ||
575
723
  !isConsentExpiryActive(term.data.expiresAt, now) ||
576
- !isConsentExpiryActive(contract.data.expiresAt, now) ||
577
724
  !matchesConsentedDataQuery(term.data, query, now)
578
725
  )
579
- continue;
580
- records.push(projectConsentedData(term.data, query, now));
581
- if (records.length >= limit) return records;
726
+ return [];
727
+ return [{ term: term.data, contract: contract.data, index }];
728
+ });
729
+ const acceptedVersions = await getConsentFlowContractVersionRecords(
730
+ validResults.map(({ term, contract }) => ({
731
+ contractId: contract.id,
732
+ version: Number(term.consentedVersion ?? 1),
733
+ }))
734
+ );
735
+ const validByIndex = new Map(validResults.map(result => [result.index, result]));
736
+ for (let index = 0; index < results.length; index++) {
737
+ const raw = results[index]!;
738
+ const scanned = inflateObject(raw.terms) as FlatDbTermsType;
739
+ const candidate = validByIndex.get(index);
740
+ if (candidate) {
741
+ const { term, contract } = candidate;
742
+ const acceptedVersion = acceptedVersions.get(
743
+ `${contract.id}:${Number(term.consentedVersion ?? 1)}`
744
+ );
745
+ if (
746
+ acceptedVersion &&
747
+ isConsentExpiryActive(acceptedVersion.expiresAt, now) &&
748
+ areTermsValid(term.terms, acceptedVersion.contract)
749
+ )
750
+ records.push(projectConsentedData(term, query, now));
751
+ }
752
+ if (records.length >= limit) {
753
+ const moreInBatch = index + 1 < results.length;
754
+ const hasMore = moreInBatch || results.length === batchSize;
755
+ return {
756
+ records,
757
+ hasMore,
758
+ ...(records.length
759
+ ? {
760
+ nextCursor: encodeConsentedDataCursor(
761
+ scanned.updatedAt ?? scanned.createdAt ?? '',
762
+ scanned.id
763
+ ),
764
+ }
765
+ : {}),
766
+ };
767
+ }
582
768
  }
583
769
  if (results.length < batchSize) break;
584
770
  offset += batchSize;
585
771
  }
586
- return records;
772
+ return { records, hasMore: false };
587
773
  };
588
774
 
589
775
  export const getConsentedDataForProfile = async (
590
776
  profileId: string,
591
777
  options: { query?: ConsentFlowDataQuery; limit: number; cursor?: string }
592
- ): Promise<ConsentFlowContractData[]> => getPermittedContractData(profileId, options);
778
+ ): Promise<ConsentedDataPage> => getPermittedContractData(profileId, options);
593
779
 
594
780
  export const getConsentedDataForContract = async (
595
781
  id: string,
596
782
  viewerProfileId: string,
597
783
  options: { query?: ConsentFlowDataQuery; limit: number; cursor?: string }
598
- ): Promise<ConsentFlowContractData[]> => getPermittedContractData(viewerProfileId, options, id);
784
+ ): Promise<ConsentedDataPage> => getPermittedContractData(viewerProfileId, options, id);
599
785
 
600
786
  // Empty expiry values historically mean no expiry. Invalid nonempty values never authorize access.
601
787
  const isConsentExpiryActive = (expiresAt: string | undefined, now: number): boolean => {
@@ -604,6 +790,12 @@ const isConsentExpiryActive = (expiresAt: string | undefined, now: number): bool
604
790
  return Number.isFinite(expiry) && expiry > now;
605
791
  };
606
792
 
793
+ export type ConsentedDataForDidPage = {
794
+ records: ConsentFlowContractDataForDid[];
795
+ hasMore: boolean;
796
+ nextCursor?: string;
797
+ };
798
+
607
799
  export const getConsentedDataBetweenProfiles = async (
608
800
  ownerProfileId: string,
609
801
  consenterProfileId: string,
@@ -613,7 +805,8 @@ export const getConsentedDataBetweenProfiles = async (
613
805
  cursor,
614
806
  domain,
615
807
  }: { query?: ConsentFlowDataForDidQuery; limit: number; cursor?: string; domain: string }
616
- ): Promise<ConsentFlowContractDataForDid[]> => {
808
+ ): Promise<ConsentedDataForDidPage> => {
809
+ const decodedCursor = decodeConsentedDataCursor(cursor);
617
810
  const { id, ...params } = query;
618
811
  const convertedContractQuery = id ? convertObjectRegExpToNeo4j({ id }) : {};
619
812
  const { whereClause: contractWhereClause, params: contractQueryParams } =
@@ -639,7 +832,8 @@ export const getConsentedDataBetweenProfiles = async (
639
832
  while (records.length < limit) {
640
833
  const _dbQuery = new QueryBuilder(
641
834
  new BindParam({
642
- cursor,
835
+ cursorDate: decodedCursor?.updatedAt,
836
+ cursorId: decodedCursor?.id,
643
837
  ...contractQueryParams,
644
838
  })
645
839
  ).match({
@@ -652,12 +846,9 @@ export const getConsentedDataBetweenProfiles = async (
652
846
  '-[:CREATED_BY|SHARES_DATA_WITH]->',
653
847
  { model: Profile, where: { profileId: ownerProfileId } },
654
848
  ],
655
- // The following is custom Cypher logic that has the following behavior:
656
- // If query key is true, ensure all resulting terms have that key
657
- // If query key is false, ensure no resulting terms have that key
658
- // If query key is not present, return all terms whether they have it or not
659
849
  }).where(`
660
- (terms.status = 'live' OR (terms.status = 'stale' AND terms.oneTime = true))
850
+ (coalesce(contract.status, 'active') <> 'retired') AND
851
+ (terms.status IN ['live', 'needs_reconsent', 'suspended'] OR (terms.status = 'consumed' AND terms.oneTime = true))
661
852
  AND ${contractWhereClause}
662
853
  `);
663
854
 
@@ -670,10 +861,11 @@ AND ${contractWhereClause}
670
861
  ],
671
862
  }).with(`terms, contract,
672
863
  coalesce(terms.createdAt, min(CASE WHEN transaction.action = 'consent' THEN transaction.date END)) AS createdAt,
673
- coalesce(terms.updatedAt, max(transaction.date), terms.createdAt) AS date,
864
+ coalesce(terms.updatedAt, max(transaction.date), terms.createdAt, '') AS date,
674
865
  count(transaction.\`guardianApproval.guardianProfileId\`) > 0 AS hadGuardianApproval`);
675
866
 
676
- if (cursor) dbQuery.where('date < $cursor');
867
+ if (decodedCursor)
868
+ dbQuery.where('(date < $cursorDate OR (date = $cursorDate AND terms.id > $cursorId))');
677
869
 
678
870
  const results = convertQueryResultToPropertiesObjectArray<{
679
871
  terms: FlatDbTermsType;
@@ -690,24 +882,39 @@ AND ${contractWhereClause}
690
882
  .run()
691
883
  );
692
884
 
693
- for (const result of results) {
694
- const parsedTerm = DbTermsValidator.safeParse(inflateObject(result.terms));
695
- const parsedContract = DbContractValidator.safeParse(inflateObject(result.contract));
696
- if (!parsedTerm.success || !parsedContract.success) {
885
+ const validResults = results.flatMap(result => {
886
+ const term = DbTermsValidator.safeParse(inflateObject(result.terms));
887
+ const contract = DbContractValidator.safeParse(inflateObject(result.contract));
888
+ if (!term.success || !contract.success) {
697
889
  // Never log stored consent data, identifiers, or raw validation errors.
698
890
  console.warn('consented_data: invalid_stored_grant');
699
- continue;
891
+ return [];
700
892
  }
701
- const term = parsedTerm.data;
702
- const contract = parsedContract.data;
703
893
  if (
704
- !isConsentExpiryActive(term.expiresAt, now) ||
705
- !isConsentExpiryActive(contract.expiresAt, now)
894
+ !isConsentExpiryActive(term.data.expiresAt, now) ||
895
+ !matchesConsentedDataQuery(term.data, params, now)
896
+ )
897
+ return [];
898
+ return [{ result, term: term.data, contract: contract.data }];
899
+ });
900
+ const acceptedVersions = await getConsentFlowContractVersionRecords(
901
+ validResults.map(({ term, contract }) => ({
902
+ contractId: contract.id,
903
+ version: Number(term.consentedVersion ?? 1),
904
+ }))
905
+ );
906
+ for (let resultIndex = 0; resultIndex < validResults.length; resultIndex++) {
907
+ const { result, term, contract } = validResults[resultIndex]!;
908
+ const acceptedVersion = acceptedVersions.get(
909
+ `${contract.id}:${Number(term.consentedVersion ?? 1)}`
910
+ );
911
+ if (
912
+ !acceptedVersion ||
913
+ !isConsentExpiryActive(acceptedVersion.expiresAt, now) ||
914
+ !areTermsValid(term.terms, acceptedVersion.contract)
706
915
  )
707
916
  continue;
708
917
 
709
- if (!matchesConsentedDataQuery(term, params, now)) continue;
710
-
711
918
  const parsedApproval = ConsentFlowGuardianApprovalValidator.safeParse(
712
919
  term.guardianApproval
713
920
  );
@@ -716,7 +923,7 @@ AND ${contractWhereClause}
716
923
  requiresManagerApproval || !!term.guardianApproval || result.hadGuardianApproval;
717
924
  const approved =
718
925
  !!approval &&
719
- approval.contractUpdatedAt === contract.updatedAt &&
926
+ approval.contractUpdatedAt === acceptedVersion.createdAt &&
720
927
  managers.some(
721
928
  manager =>
722
929
  manager.profileId === approval.guardianProfileId &&
@@ -728,10 +935,12 @@ AND ${contractWhereClause}
728
935
  records.push({
729
936
  date: result.date,
730
937
  ...(result.createdAt ? { createdAt: result.createdAt } : {}),
731
- contractUpdatedAt: contract.updatedAt,
732
- ...(contract.expiresAt?.trim() ? { contractExpiresAt: contract.expiresAt } : {}),
733
- ...(contract.reasonForAccessing !== undefined
734
- ? { reasonForAccessing: contract.reasonForAccessing }
938
+ contractUpdatedAt: acceptedVersion.createdAt,
939
+ ...(acceptedVersion.expiresAt?.trim()
940
+ ? { contractExpiresAt: acceptedVersion.expiresAt }
941
+ : {}),
942
+ ...(acceptedVersion.reasonForAccessing !== undefined
943
+ ? { reasonForAccessing: acceptedVersion.reasonForAccessing }
735
944
  : {}),
736
945
  guardian: { required, approved, ...(approval ? { approval } : {}) },
737
946
  contractUri: constructUri('contract', contract.id, domain),
@@ -769,13 +978,23 @@ AND ${contractWhereClause}
769
978
  ),
770
979
  personal: term.terms.read.personal,
771
980
  });
772
- if (records.length >= limit) return records;
981
+ if (records.length >= limit) {
982
+ const hasMore =
983
+ resultIndex + 1 < validResults.length || results.length === batchSize;
984
+ return {
985
+ records,
986
+ hasMore,
987
+ ...(records.length
988
+ ? { nextCursor: encodeConsentedDataCursor(result.date, term.id) }
989
+ : {}),
990
+ };
991
+ }
773
992
  }
774
993
  if (results.length < batchSize) break;
775
994
  offset += batchSize;
776
995
  }
777
996
 
778
- return records;
997
+ return { records, hasMore: false };
779
998
  };
780
999
 
781
1000
  export const getTransactionsForTerms = async (
@@ -792,7 +1011,14 @@ export const getTransactionsForTerms = async (
792
1011
  convertedQuery as any
793
1012
  );
794
1013
 
795
- const query = new QueryBuilder(new BindParam({ cursor, ...transactionQueryParams }))
1014
+ const decodedCursor = decodeConsentedDataCursor(cursor);
1015
+ const query = new QueryBuilder(
1016
+ new BindParam({
1017
+ cursorDate: decodedCursor?.updatedAt,
1018
+ cursorId: decodedCursor?.id,
1019
+ ...transactionQueryParams,
1020
+ })
1021
+ )
796
1022
  .match({
797
1023
  related: [
798
1024
  { identifier: 'transaction', model: ConsentFlowTransaction },
@@ -800,7 +1026,13 @@ export const getTransactionsForTerms = async (
800
1026
  { model: ConsentFlowTerms, where: { id: termsId } },
801
1027
  ],
802
1028
  })
803
- .where(`${whereClause}${cursor ? ' AND transaction.date < $cursor' : ''}`)
1029
+ .where(
1030
+ `${whereClause}${
1031
+ decodedCursor
1032
+ ? ' AND (transaction.date < $cursorDate OR (transaction.date = $cursorDate AND transaction.id < $cursorId))'
1033
+ : ''
1034
+ }`
1035
+ )
804
1036
  .with('transaction')
805
1037
  .match({
806
1038
  optional: true,
@@ -818,7 +1050,7 @@ export const getTransactionsForTerms = async (
818
1050
  }>(
819
1051
  await query
820
1052
  .return('DISTINCT transaction, [node in credentials | properties(node)] AS credentials')
821
- .orderBy('transaction.date DESC')
1053
+ .orderBy('transaction.date DESC, transaction.id DESC')
822
1054
  .limit(limit)
823
1055
  .run()
824
1056
  );
@@ -80,7 +80,7 @@ export const publishSmartResume = async ({
80
80
  WHERE request.requestId = $expectedRequestId AND request.status = 'accepted'
81
81
  })
82
82
  AND terms.smartResumeMutationVersion = coalesce(terms.mutationVersion, 0)
83
- AND (terms.status = 'live' OR (terms.status = 'stale' AND terms.oneTime = true))
83
+ AND (terms.status = 'live' OR (terms.status = 'consumed' AND terms.oneTime = true))
84
84
  AND CASE WHEN terms.expiresAt IS NULL OR trim(terms.expiresAt) = '' THEN true ELSE datetime(terms.expiresAt) > datetime.realtime() END
85
85
  AND CASE WHEN contract.expiresAt IS NULL OR trim(contract.expiresAt) = '' THEN true ELSE datetime(contract.expiresAt) > datetime.realtime() END
86
86
  AND (terms.smartResumePublicationStatus IN ['pending', 'failed', 'succeeded']