@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,7 +1,10 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import type { ConsentFlowDataQuery, ConsentFlowTermsInput } from '@learncard/types';
4
- import { ConsentFlowTerms, ConsentFlowContract } from '@models';
4
+ import { ConsentFlowTerms, ConsentFlowTransaction } from '@models';
5
+ import { neogma } from '@instance';
6
+ import { suspendExpiredConsentFlowTerms } from '@helpers/consentflow-deadline.helpers';
7
+ import { encodeConsentedDataCursor } from '@accesslayer/consentflowcontract/relationships/read';
5
8
  import { getUser } from './helpers/getClient';
6
9
  import { minimalContract, minimalTerms, normalContract, normalFullTerms } from './helpers/contract';
7
10
 
@@ -50,6 +53,27 @@ describe('consented data boundaries', () => {
50
53
  return { contractUri, termsUri };
51
54
  };
52
55
 
56
+ const seedLegacyExpiry = async (
57
+ kind: 'terms' | 'contract',
58
+ contractUri: string,
59
+ termsUri: string,
60
+ expiresAt: string
61
+ ) => {
62
+ if (kind === 'terms') {
63
+ await ConsentFlowTerms.update(
64
+ { expiresAt },
65
+ { where: { id: termsUri.split(':').at(-1)! } }
66
+ );
67
+ return;
68
+ }
69
+ // Legacy fixtures seed the accepted snapshot as well as the current request.
70
+ await neogma.queryRunner.run(
71
+ `MATCH (contract:ConsentFlowContract {id:$id})-[:HAS_VERSION]->(version:ConsentFlowContractVersion {version:1})
72
+ SET contract.expiresAt = $expiresAt, version.expiresAt = $expiresAt`,
73
+ { id: contractUri.split(':').at(-1)!, expiresAt }
74
+ );
75
+ };
76
+
53
77
  const readAll = async (
54
78
  contractUri: string,
55
79
  query: ConsentFlowDataQuery = {},
@@ -148,9 +172,7 @@ describe('consented data boundaries', () => {
148
172
  'allows locked sync when %s expiry contains only whitespace',
149
173
  async kind => {
150
174
  const { contractUri, termsUri } = await consent();
151
- const model = kind === 'terms' ? ConsentFlowTerms : ConsentFlowContract;
152
- const uri = kind === 'terms' ? termsUri : contractUri;
153
- await model.update({ expiresAt: ' ' }, { where: { id: uri.split(':').at(-1)! } });
175
+ await seedLegacyExpiry(kind, contractUri, termsUri, ' ');
154
176
  await expect(
155
177
  learner.clients.fullAuth.contracts.syncCredentialsToContract({
156
178
  termsUri,
@@ -170,22 +192,18 @@ describe('consented data boundaries', () => {
170
192
 
171
193
  it.each(['terms', 'contract'] as const)('excludes expired %s on every route', async kind => {
172
194
  const past = '2000-01-01T00:00:00.000Z';
173
- const { contractUri } = await consent({
195
+ const { contractUri, termsUri } = await consent({
174
196
  expiresAt: kind === 'terms' ? past : undefined,
175
197
  });
176
198
  // Seed an existing consent that subsequently expires, rather than consent to an expired contract.
177
- if (kind === 'contract')
178
- await ConsentFlowContract.update(
179
- { expiresAt: past },
180
- { where: { id: contractUri.split(':').at(-1)! } }
181
- );
199
+ if (kind === 'contract') await seedLegacyExpiry(kind, contractUri, termsUri, past);
182
200
  for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
183
201
  });
184
202
 
185
203
  it('preserves one-time snapshots without permitting ongoing sync, then honors withdrawal', async () => {
186
204
  const { contractUri, termsUri } = await consent({ oneTime: true });
187
205
  const history = await learner.clients.fullAuth.contracts.getConsentedContracts();
188
- expect(history.records.find(record => record.uri === termsUri)?.status).toBe('stale');
206
+ expect(history.records.find(record => record.uri === termsUri)?.status).toBe('consumed');
189
207
  for (const response of await readAll(contractUri)) {
190
208
  expect(response.records).toHaveLength(1);
191
209
  expect(JSON.stringify(response)).toContain('achievement1');
@@ -200,10 +218,28 @@ describe('consented data boundaries', () => {
200
218
  for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
201
219
  });
202
220
 
203
- it('excludes stale consent without the explicit one-time flag', async () => {
221
+ it('persists suspension when a re-consent deadline has passed', async () => {
222
+ const { termsUri } = await consent();
223
+ const termsId = termsUri.split(':').at(-1)!;
224
+ await ConsentFlowTerms.update(
225
+ { status: 'needs_reconsent', reconsentDeadline: '2026-10-14T00:00:00.000Z' },
226
+ { where: { id: termsId } }
227
+ );
228
+
229
+ await expect(suspendExpiredConsentFlowTerms('2026-10-15T00:00:00.000Z')).resolves.toEqual({
230
+ suspended: 1,
231
+ });
232
+ const terms = await ConsentFlowTerms.findOne({ where: { id: termsId } });
233
+ expect(terms?.dataValues.status).toBe('suspended');
234
+ expect(terms?.dataValues.updatedAt).toBe('2026-10-15T00:00:00.000Z');
235
+ await expect(suspendExpiredConsentFlowTerms('2026-10-15T00:00:00.000Z')).resolves.toEqual({
236
+ suspended: 0,
237
+ });
238
+ });
239
+ it('excludes consumed access without the explicit one-time flag', async () => {
204
240
  const { contractUri, termsUri } = await consent();
205
241
  await ConsentFlowTerms.update(
206
- { status: 'stale' },
242
+ { status: 'consumed' },
207
243
  { where: { id: termsUri.split(':').at(-1)! } }
208
244
  );
209
245
  for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
@@ -278,11 +314,7 @@ describe('consented data boundaries', () => {
278
314
  'excludes malformed legacy %s expiry without failing the query',
279
315
  async kind => {
280
316
  const { contractUri, termsUri } = await consent();
281
- const model = kind === 'terms' ? ConsentFlowTerms : ConsentFlowContract;
282
- await model.update(
283
- { expiresAt: 'invalid-legacy-date' },
284
- { where: { id: (kind === 'terms' ? termsUri : contractUri).split(':').at(-1)! } }
285
- );
317
+ await seedLegacyExpiry(kind, contractUri, termsUri, 'invalid-legacy-date');
286
318
  for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
287
319
  }
288
320
  );
@@ -297,7 +329,63 @@ describe('consented data boundaries', () => {
297
329
  }
298
330
  });
299
331
 
300
- it('applies eligibility before pagination and accepts a cursor on every route', async () => {
332
+ it('keeps accepted snapshots isolated across consenters, contracts, and versions', async () => {
333
+ const first = await consent({
334
+ terms: {
335
+ ...structuredClone(normalFullTerms),
336
+ read: { ...normalFullTerms.read, personal: { name: 'first-grant' } },
337
+ },
338
+ });
339
+ const changed = await owner.clients.fullAuth.contracts.updateConsentFlowContract({
340
+ uri: first.contractUri,
341
+ changes: { contract: { read: { personal: { email: { required: true } } } } },
342
+ reason: 'Verify enrolled learners',
343
+ });
344
+ const secondLearner = await getUser(randomBytes(32).toString('hex'));
345
+ await secondLearner.clients.fullAuth.profile.createProfile({
346
+ profileId: `boundary-third-${randomBytes(5).toString('hex')}`,
347
+ });
348
+ await secondLearner.clients.fullAuth.contracts.consentToContract({
349
+ contractUri: first.contractUri,
350
+ terms: {
351
+ ...structuredClone(normalFullTerms),
352
+ read: {
353
+ ...normalFullTerms.read,
354
+ personal: { name: 'second-grant', email: 'second@example.test' },
355
+ },
356
+ },
357
+ });
358
+ const otherContract = await consent({
359
+ terms: {
360
+ ...structuredClone(normalFullTerms),
361
+ read: { ...normalFullTerms.read, personal: { name: 'other-contract' } },
362
+ },
363
+ });
364
+
365
+ const response = await owner.clients.fullAuth.contracts.getConsentedData({ limit: 10 });
366
+ const names = response.records.map(record => record.personal?.name).sort();
367
+ expect(names).toEqual(['first-grant', 'other-contract', 'second-grant']);
368
+ expect(response.records).toHaveLength(3);
369
+
370
+ const firstContract = await owner.clients.fullAuth.contracts.getConsentedDataForContract({
371
+ uri: first.contractUri,
372
+ limit: 10,
373
+ });
374
+ expect(firstContract.records.map(record => record.personal?.name).sort()).toEqual([
375
+ 'first-grant',
376
+ 'second-grant',
377
+ ]);
378
+ expect(
379
+ firstContract.records.find(record => record.personal?.name === 'first-grant')?.personal
380
+ ).toEqual({ name: 'first-grant' });
381
+ expect(
382
+ firstContract.records.find(record => record.personal?.name === 'second-grant')?.personal
383
+ ).toEqual({ name: 'second-grant', email: 'second@example.test' });
384
+ expect(changed.version).toBe(2);
385
+ expect(otherContract.contractUri).not.toBe(first.contractUri);
386
+ });
387
+
388
+ it('filters expired terms and accepts opaque contract/profile cursors', async () => {
301
389
  const { contractUri, termsUri } = await consent();
302
390
  const secondLearner = await getUser(randomBytes(32).toString('hex'));
303
391
  await secondLearner.clients.fullAuth.profile.createProfile({
@@ -319,12 +407,19 @@ describe('consented data boundaries', () => {
319
407
  expect(response.records).toHaveLength(1);
320
408
  expect(response.hasMore).toBe(false);
321
409
  }
322
- for (const response of await readAll(
323
- contractUri,
324
- {},
325
- { cursor: '2001-01-01T00:00:00Z', limit: 1 }
326
- )) {
327
- expect(response.records).toEqual([]);
410
+ const cursor = encodeConsentedDataCursor(
411
+ '2001-01-01T00:00:00Z',
412
+ termsUri.split(':').at(-1)!
413
+ );
414
+ for (const page of await Promise.all([
415
+ owner.clients.fullAuth.contracts.getConsentedDataForContract({
416
+ uri: contractUri,
417
+ cursor,
418
+ limit: 1,
419
+ }),
420
+ owner.clients.fullAuth.contracts.getConsentedData({ cursor, limit: 1 }),
421
+ ])) {
422
+ expect(page.records).toEqual([]);
328
423
  }
329
424
  });
330
425
 
@@ -341,7 +436,6 @@ describe('consented data boundaries', () => {
341
436
  });
342
437
  expect(response.records).toHaveLength(1);
343
438
  expect(response.hasMore).toBe(false);
344
- expect(response.cursor).toBe('2001-01-01T00:00:00Z');
345
439
  const next = await owner.clients.fullAuth.contracts.getConsentedDataForDid({
346
440
  did: learnerDid,
347
441
  limit: 1,
@@ -349,4 +443,206 @@ describe('consented data boundaries', () => {
349
443
  });
350
444
  expect(next.records).toEqual([]);
351
445
  });
446
+ it('keyset-paginates tied timestamps and advances over expired scanned terms', async () => {
447
+ const contractUri = await owner.clients.fullAuth.contracts.createConsentFlowContract({
448
+ name: 'Tied-page regression',
449
+ contract: normalContract,
450
+ });
451
+ const termIds: string[] = [];
452
+ for (let index = 0; index < 51; index++) {
453
+ const holder = await getUser(randomBytes(32).toString('hex'));
454
+ await holder.clients.fullAuth.profile.createProfile({
455
+ profileId: `tied-holder-${randomBytes(5).toString('hex')}`,
456
+ });
457
+ const { termsUri } = await holder.clients.fullAuth.contracts.consentToContract({
458
+ contractUri,
459
+ terms: {
460
+ ...normalFullTerms,
461
+ read: {
462
+ ...normalFullTerms.read,
463
+ personal: { name: `Holder ${index}` },
464
+ },
465
+ },
466
+ });
467
+ termIds.push(termsUri.split(':').at(-1)!);
468
+ }
469
+ await owner.clients.fullAuth.contracts.updateConsentFlowContract({
470
+ uri: contractUri,
471
+ changes: { subtitle: 'One publication updates the entire cohort' },
472
+ });
473
+ const expired = await neogma.queryRunner.run(
474
+ `MATCH (terms:ConsentFlowTerms) WHERE terms.id IN $ids
475
+ WITH terms ORDER BY terms.id SKIP 24 LIMIT 1
476
+ SET terms.expiresAt='2000-01-01T00:00:00Z'
477
+ RETURN terms['terms.read.personal.name'] AS name`,
478
+ { ids: termIds }
479
+ );
480
+ const expiredName = String(expired.records[0].get('name'));
481
+ const expectedNames = new Set(
482
+ Array.from({ length: 51 }, (_, index) => `Holder ${index}`).filter(
483
+ name => name !== expiredName
484
+ )
485
+ );
486
+ const readers = [
487
+ (cursor?: string) =>
488
+ owner.clients.fullAuth.contracts.getConsentedDataForContract({
489
+ uri: contractUri,
490
+ limit: 25,
491
+ cursor,
492
+ }),
493
+ (cursor?: string) =>
494
+ owner.clients.fullAuth.contracts.getConsentedData({
495
+ limit: 25,
496
+ cursor,
497
+ }),
498
+ ];
499
+ for (const read of readers) {
500
+ const seenNames = new Set<string>();
501
+ let cursor: string | undefined;
502
+ do {
503
+ const page = await read(cursor);
504
+ for (const record of page.records) {
505
+ const name = record.personal.name;
506
+ expect(seenNames.has(name)).toBe(false);
507
+ seenNames.add(name);
508
+ }
509
+ if (!page.hasMore) break;
510
+ expect(page.cursor).toBeDefined();
511
+ expect(page.cursor).not.toBe(cursor);
512
+ cursor = page.cursor;
513
+ } while (cursor);
514
+ expect(seenNames).toEqual(expectedNames);
515
+ }
516
+ });
517
+ it('rejects malformed and obsolete date-only cursors with BAD_REQUEST', async () => {
518
+ const { contractUri, termsUri } = await consent();
519
+ const requests = [
520
+ () =>
521
+ owner.clients.fullAuth.contracts.getConsentedDataForContract({
522
+ uri: contractUri,
523
+ limit: 1,
524
+ cursor: '2025-01-01T00:00:00Z',
525
+ }),
526
+ () =>
527
+ owner.clients.fullAuth.contracts.getConsentedData({
528
+ limit: 1,
529
+ cursor: 'not-a-cursor',
530
+ }),
531
+ () =>
532
+ owner.clients.fullAuth.contracts.getConsentedDataForDid({
533
+ did: learnerDid,
534
+ limit: 1,
535
+ cursor: '2025-01-01T00:00:00Z',
536
+ }),
537
+ () =>
538
+ learner.clients.fullAuth.contracts.getConsentedContracts({
539
+ limit: 1,
540
+ cursor: '2025-01-01T00:00:00Z',
541
+ }),
542
+ () =>
543
+ learner.clients.fullAuth.contracts.getTermsTransactionHistory({
544
+ uri: termsUri,
545
+ limit: 1,
546
+ cursor: 'not-a-cursor',
547
+ }),
548
+ ];
549
+ for (const request of requests) {
550
+ await expect(request()).rejects.toMatchObject({ code: 'BAD_REQUEST' });
551
+ }
552
+ });
553
+
554
+ it('paginates tied holder-list, export, and between-profile cohorts with null update dates', async () => {
555
+ const termsIds: string[] = [];
556
+ for (let index = 0; index < 101; index++) {
557
+ const { termsUri } = await consent();
558
+ termsIds.push(termsUri.split(':').at(-1)!);
559
+ }
560
+ await neogma.queryRunner.run(
561
+ `MATCH (terms:ConsentFlowTerms) WHERE terms.id IN $ids
562
+ SET terms.updatedAt = '2020-01-01T00:00:00Z'`,
563
+ { ids: termsIds }
564
+ );
565
+ const nullTimestampId = termsIds.at(-1)!;
566
+ await neogma.queryRunner.run(
567
+ `MATCH (terms:ConsentFlowTerms {id:$id}) REMOVE terms.updatedAt`,
568
+ { id: nullTimestampId }
569
+ );
570
+
571
+ const seenTerms = new Set<string>();
572
+ let cursor: string | undefined;
573
+ do {
574
+ const page = await learner.clients.fullAuth.contracts.getConsentedContracts({
575
+ limit: 25,
576
+ cursor,
577
+ });
578
+ for (const record of page.records) {
579
+ expect(seenTerms.has(record.uri)).toBe(false);
580
+ seenTerms.add(record.uri);
581
+ }
582
+ if (!page.hasMore) break;
583
+ expect(page.cursor).toBeDefined();
584
+ cursor = page.cursor;
585
+ } while (cursor);
586
+ expect(seenTerms.size).toBe(101);
587
+
588
+ const exported = await learner.clients.fullAuth.credential.getHolderExportMetadata();
589
+ expect(new Set(exported.consentRecords.map(record => record.termsUri))).toEqual(seenTerms);
590
+
591
+ const betweenTerms = new Set<string>();
592
+ cursor = undefined;
593
+ do {
594
+ const page = await owner.clients.fullAuth.contracts.getConsentedDataForDid({
595
+ did: learnerDid,
596
+ limit: 25,
597
+ cursor,
598
+ });
599
+ for (const record of page.records) {
600
+ expect(betweenTerms.has(record.termsUri)).toBe(false);
601
+ betweenTerms.add(record.termsUri);
602
+ }
603
+ if (!page.hasMore) break;
604
+ expect(page.cursor).toBeDefined();
605
+ cursor = page.cursor;
606
+ } while (cursor);
607
+ expect(betweenTerms.size).toBe(101);
608
+ });
609
+
610
+ it('keeps tied transaction history complete in public pages and holder exports', async () => {
611
+ const { termsUri } = await consent();
612
+ const terms = structuredClone(normalFullTerms);
613
+ for (let index = 0; index < 101; index++) {
614
+ terms.read.personal.name = `Saved choice ${index}`;
615
+ await learner.clients.fullAuth.contracts.updateConsentedContractTerms({
616
+ uri: termsUri,
617
+ terms,
618
+ });
619
+ }
620
+ const tied = await neogma.queryRunner.run(
621
+ `MATCH (transaction:ConsentFlowTransaction)-[:${ConsentFlowTransaction.getRelationshipByAlias('isFor').name}]->(:ConsentFlowTerms {id:$id})
622
+ SET transaction.date='2020-01-01T00:00:00Z'
623
+ RETURN transaction.id AS id`,
624
+ { id: termsUri.split(':').at(-1)! }
625
+ );
626
+ const expected = new Set(tied.records.map(record => String(record.get('id'))));
627
+ expect(expected.size).toBe(102);
628
+ const seen = new Set<string>();
629
+ let cursor: string | undefined;
630
+ do {
631
+ const page = await learner.clients.fullAuth.contracts.getTermsTransactionHistory({
632
+ uri: termsUri,
633
+ limit: 25,
634
+ cursor,
635
+ });
636
+ for (const record of page.records) {
637
+ expect(seen.has(record.id)).toBe(false);
638
+ seen.add(record.id);
639
+ }
640
+ cursor = page.hasMore ? page.cursor : undefined;
641
+ } while (cursor);
642
+ expect(seen).toEqual(expected);
643
+ const exported = await learner.clients.fullAuth.credential.getHolderExportMetadata();
644
+ const record = exported.consentRecords.find(record => record.termsUri === termsUri);
645
+ expect(record?.transactions).toHaveLength(expected.size);
646
+ expect(new Set(record!.transactions.map(transaction => transaction.id))).toEqual(expected);
647
+ });
352
648
  });