@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
@@ -0,0 +1,123 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { neogma } from '@instance';
3
+ import { Boost } from '@models';
4
+ import { getUser } from './helpers/getClient';
5
+ import { sendSpy } from './helpers/spies';
6
+ import { getProfileByProfileId } from '@accesslayer/profile/read';
7
+ import { storeCredential } from '@accesslayer/credential/create';
8
+ import { releaseUnlinkedCredential } from '@accesslayer/credential/delete';
9
+ import * as boostRelationships from '@accesslayer/boost/relationships/create';
10
+ import * as relationships from '@accesslayer/credential/relationships/create';
11
+ import { sendBoost } from '@helpers/boost.helpers';
12
+
13
+ vi.mock('@services/delivery/delivery.factory', () => ({
14
+ getDeliveryService: () => ({ send: sendSpy }),
15
+ }));
16
+
17
+ let issuer: Awaited<ReturnType<typeof getUser>>;
18
+ let holder: Awaited<ReturnType<typeof getUser>>;
19
+
20
+ const prepare = async () => {
21
+ const entries = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
22
+ statusPurposes: ['revocation'],
23
+ });
24
+ const credential = await issuer.learnCard.invoke.issueCredential({
25
+ '@context': ['https://www.w3.org/ns/credentials/v2'],
26
+ type: ['VerifiableCredential'],
27
+ issuer: issuer.learnCard.id.did(),
28
+ credentialSubject: { id: holder.learnCard.id.did() },
29
+ credentialStatus: entries,
30
+ });
31
+ const from = await getProfileByProfileId('cleanup-issuer');
32
+ const to = await getProfileByProfileId('cleanup-holder');
33
+ if (!from || !to) throw new Error('Missing test profiles');
34
+ const boost = await Boost.createOne({ id: 'cleanup-boost', boost: JSON.stringify(credential) });
35
+ return {
36
+ from: { type: 'profile' as const, profile: from },
37
+ to,
38
+ boost,
39
+ credential,
40
+ domain: 'localhost:3000',
41
+ skipNotification: true,
42
+ };
43
+ };
44
+
45
+ const rows = async (label: 'Credential' | 'CredentialStatusBinding') =>
46
+ (await neogma.queryRunner.run(`MATCH (n:${label}) RETURN n`)).records;
47
+
48
+ describe('Boost status reservation cleanup', () => {
49
+ beforeAll(async () => {
50
+ issuer = await getUser('d'.repeat(64));
51
+ holder = await getUser('b'.repeat(64));
52
+ });
53
+ beforeEach(async () => {
54
+ await neogma.queryRunner.run('MATCH (n) DETACH DELETE n');
55
+ await issuer.clients.fullAuth.profile.createProfile({ profileId: 'cleanup-issuer' });
56
+ await holder.clients.fullAuth.profile.createProfile({ profileId: 'cleanup-holder' });
57
+ });
58
+
59
+ it('rolls back after the instance write and retries the identical signed VC', async () => {
60
+ const input = await prepare();
61
+ const instanceSpy = vi.spyOn(boostRelationships, 'createBoostInstanceOfRelationship');
62
+ const failure = vi
63
+ .spyOn(relationships, 'createSentCredentialRelationship')
64
+ .mockRejectedValueOnce(new Error('forced relationship failure'));
65
+ try {
66
+ await expect(sendBoost(input)).rejects.toThrow('forced relationship failure');
67
+ expect(instanceSpy).toHaveBeenCalledOnce();
68
+ expect(instanceSpy.mock.calls[0]?.[2]).toBeDefined();
69
+ expect(await rows('Credential')).toHaveLength(0);
70
+ expect(await rows('CredentialStatusBinding')).toHaveLength(0);
71
+ } finally {
72
+ instanceSpy.mockRestore();
73
+ failure.mockRestore();
74
+ }
75
+ await expect(sendBoost(input)).resolves.toEqual(expect.any(String));
76
+ expect(await rows('Credential')).toHaveLength(1);
77
+ expect(await rows('CredentialStatusBinding')).toHaveLength(1);
78
+ const sent = await neogma.queryRunner.run(
79
+ 'MATCH (:Profile)-[r:CREDENTIAL_SENT]->(:Credential)-[:INSTANCE_OF]->(:Boost) RETURN r'
80
+ );
81
+ expect(sent.records).toHaveLength(1);
82
+ });
83
+
84
+ it('releases instance-only credentials and their bindings', async () => {
85
+ const { credential, boost } = await prepare();
86
+ const stored = await storeCredential(credential, undefined, 'cleanup-issuer');
87
+ await boostRelationships.createBoostInstanceOfRelationship(stored, boost);
88
+ await releaseUnlinkedCredential(stored.id);
89
+ expect(await rows('Credential')).toHaveLength(0);
90
+ expect(await rows('CredentialStatusBinding')).toHaveLength(0);
91
+ expect(await Boost.findOne({ where: { id: boost.id } })).toBeTruthy();
92
+ await expect(
93
+ storeCredential(credential, undefined, 'cleanup-issuer')
94
+ ).resolves.toBeDefined();
95
+ });
96
+
97
+ it.each(['CREDENTIAL_SENT', 'OTHER_RELATIONSHIP'])(
98
+ 'preserves credentials and bindings with a %s edge',
99
+ async relationship => {
100
+ const { credential, boost } = await prepare();
101
+ const stored = await storeCredential(credential, undefined, 'cleanup-issuer');
102
+ await boostRelationships.createBoostInstanceOfRelationship(stored, boost);
103
+ await neogma.queryRunner.run(
104
+ `MATCH (p:Profile {profileId: $profile}), (c:Credential {id: $id})
105
+ CREATE (p)-[:${relationship} {to: $recipient}]->(c)`,
106
+ { profile: 'cleanup-issuer', recipient: 'cleanup-holder', id: stored.id }
107
+ );
108
+ await releaseUnlinkedCredential(stored.id);
109
+ expect(await rows('Credential')).toHaveLength(1);
110
+ expect(await rows('CredentialStatusBinding')).toHaveLength(1);
111
+ await expect(storeCredential(credential, undefined, 'cleanup-issuer')).rejects.toThrow(
112
+ 'already bound'
113
+ );
114
+ const edges = await neogma.queryRunner.run(
115
+ 'MATCH (:Credential {id: $id})-[r]-() RETURN type(r) AS type',
116
+ { id: stored.id }
117
+ );
118
+ expect(edges.records.map(row => row.get('type')).sort()).toEqual(
119
+ ['INSTANCE_OF', relationship].sort()
120
+ );
121
+ }
122
+ );
123
+ });
@@ -0,0 +1,268 @@
1
+ import devConfig from '../config/config.dev.json';
2
+ import productionConfig from '../config/config.production.json';
3
+ import scoutsDevConfig from '../config/config.scouts.dev.json';
4
+ import scoutsProductionConfig from '../config/config.scouts.production.json';
5
+ import { createHmac } from 'node:crypto';
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import type { Campaign } from '@learncard/types';
8
+ const mocks = vi.hoisted(() => ({
9
+ claim: vi.fn(),
10
+ acknowledge: vi.fn(),
11
+ send: vi.fn(),
12
+ prepare: vi.fn(),
13
+ stream: vi.fn(),
14
+ recipient: undefined as unknown,
15
+ update: vi.fn(),
16
+ environment: {
17
+ POSTMARK_API_KEY: 'test-only',
18
+ DOMAIN_NAME: 'brain.example',
19
+ CAMPAIGN_APP_ORIGINS: undefined as string | undefined,
20
+ POSTMARK_BROADCAST_STREAM: 'broadcast',
21
+ CAMPAIGN_UNSUBSCRIBE_SECRET: 'test-only-secret-at-least-32-bytes-long',
22
+ },
23
+ }));
24
+ vi.mock('@environment', () => ({ environment: mocks.environment }));
25
+ vi.mock('@accesslayer/campaign/email', () => ({
26
+ claimCampaignEmail: mocks.claim,
27
+ acknowledgeCampaignEmail: mocks.acknowledge,
28
+ }));
29
+ vi.mock('@accesslayer/campaign/preferences', () => ({
30
+ unsubscribeCampaignRecipient: mocks.update,
31
+ }));
32
+ vi.mock('../src/services/delivery/adapters/postmark.adapter', () => ({
33
+ PostmarkAdapter: class {
34
+ assertBroadcastStream = mocks.stream;
35
+ prepareCampaignEmail = mocks.prepare;
36
+ },
37
+ }));
38
+ import { campaignEmailLinks, sendCampaignEmail } from '../src/helpers/campaign-email.helpers';
39
+ import {
40
+ signCampaignUnsubscribeToken,
41
+ verifyCampaignUnsubscribeToken,
42
+ unsubscribeFromCampaignEmails,
43
+ } from '../src/helpers/campaign-unsubscribe.helpers';
44
+ const unsubscribeId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
45
+ const campaign = {
46
+ id: 'new-programs',
47
+ createdAt: '2026-01-01T00:00:00.000Z',
48
+ tenantId: 'vetpass',
49
+ channels: ['inApp', 'email'],
50
+ message: { id: 'new-programs', enabled: true },
51
+ email: {
52
+ subject: 'New programs',
53
+ title: 'Explore',
54
+ body: 'New opportunities',
55
+ ctaLabel: 'View',
56
+ },
57
+ } as Campaign;
58
+ beforeEach(() => {
59
+ vi.clearAllMocks();
60
+ mocks.environment.DOMAIN_NAME = 'brain.example';
61
+ mocks.environment.CAMPAIGN_APP_ORIGINS = JSON.stringify({ vetpass: 'https://vetpass.app' });
62
+ mocks.environment.POSTMARK_BROADCAST_STREAM = 'broadcast';
63
+ mocks.environment.POSTMARK_API_KEY = 'test-only';
64
+ mocks.recipient = { campaign, email: 'adult@example.com', unsubscribeId };
65
+ mocks.claim.mockImplementation(async (_id, _createdAt, _profileId, _test, prepare) => {
66
+ if (!mocks.recipient) return undefined;
67
+ await prepare(mocks.recipient);
68
+ return mocks.recipient;
69
+ });
70
+ mocks.prepare.mockResolvedValue(mocks.send);
71
+ mocks.stream.mockResolvedValue(undefined);
72
+ mocks.send.mockResolvedValue(undefined);
73
+ });
74
+ describe('campaign email', () => {
75
+ it('uses the configured default tenant for legacy records without a stored tenant', () => {
76
+ vi.stubEnv('DEFAULT_TENANT_ID', 'vetpass');
77
+ try {
78
+ const links = campaignEmailLinks({ ...campaign, tenantId: undefined }, unsubscribeId);
79
+ expect(new URL(links.ctaUrl!).origin).toBe('https://vetpass.app');
80
+ } finally {
81
+ vi.unstubAllEnvs();
82
+ }
83
+ });
84
+
85
+ it.each([
86
+ ['vetpass', devConfig, 'https://staging.vetpass.app'],
87
+ ['learncard', devConfig, 'https://staging.learncard.ai'],
88
+ ['scoutpass', scoutsDevConfig, 'https://staging.pass.scout.org'],
89
+ ['vetpass', productionConfig, 'https://vetpass.app'],
90
+ ['learncard', productionConfig, 'https://learncard.app'],
91
+ ['scoutpass', scoutsProductionConfig, 'https://pass.scout.org'],
92
+ ] as const)('uses deployment origins for %s (%s)', (tenantId, config, expectedOrigin) => {
93
+ mocks.environment.DOMAIN_NAME = config.DOMAIN_NAME;
94
+ mocks.environment.CAMPAIGN_APP_ORIGINS = config.CAMPAIGN_APP_ORIGINS;
95
+ const links = campaignEmailLinks({ ...campaign, tenantId }, unsubscribeId);
96
+ expect(new URL(links.ctaUrl!).origin).toBe(expectedOrigin);
97
+ expect(new URL(links.unsubscribeUrl).origin).toBe(`https://${config.DOMAIN_NAME}`);
98
+ expect(new URL(links.unsubscribeUrl).pathname).toBe('/campaigns/unsubscribe/browser');
99
+ expect(new URL(links.oneClickUrl).pathname).toBe('/campaigns/unsubscribe');
100
+ expect(
101
+ verifyCampaignUnsubscribeToken(new URL(links.unsubscribeUrl).searchParams.get('token')!)
102
+ ).toEqual({ unsubscribeId, campaignId: campaign.id });
103
+ });
104
+
105
+ it.each([
106
+ undefined,
107
+ '{}',
108
+ 'invalid',
109
+ '[]',
110
+ '{"vetpass":"http://vetpass.app"}',
111
+ '{"vetpass":"https://user:password@vetpass.app"}',
112
+ '{"vetpass":"https://vetpass.app/path"}',
113
+ '{"vetpass":"https://vetpass.app?redirect=elsewhere"}',
114
+ ])('rejects absent or unsafe deployment origins (%s)', origins => {
115
+ mocks.environment.CAMPAIGN_APP_ORIGINS = origins;
116
+ expect(() => campaignEmailLinks(campaign, unsubscribeId)).toThrow('Campaign app origin');
117
+ });
118
+
119
+ it('supports explicitly configured local app and Brain origins', () => {
120
+ mocks.environment.DOMAIN_NAME = 'localhost%3A4000/trpc';
121
+ mocks.environment.CAMPAIGN_APP_ORIGINS = '{"vetpass":"http://localhost:3000"}';
122
+ const links = campaignEmailLinks(campaign, unsubscribeId);
123
+ expect(new URL(links.ctaUrl!).origin).toBe('http://localhost:3000');
124
+ expect(new URL(links.unsubscribeUrl).origin).toBe('http://localhost:4000');
125
+ });
126
+
127
+ it('uses stored tenant, broadcast stream, authored subject and RFC 8058 headers', async () => {
128
+ await sendCampaignEmail(campaign, 'adult');
129
+ expect(mocks.claim).toHaveBeenCalledWith(
130
+ campaign.id,
131
+ campaign.createdAt,
132
+ 'adult',
133
+ false,
134
+ expect.any(Function)
135
+ );
136
+ expect(mocks.prepare).toHaveBeenCalledWith(
137
+ expect.objectContaining({
138
+ subject: 'New programs',
139
+ messageStream: 'broadcast',
140
+ contactMethod: { type: 'email', value: 'adult@example.com' },
141
+ branding: expect.objectContaining({ brandName: 'VetPass' }),
142
+ headers: [
143
+ {
144
+ Name: 'List-Unsubscribe',
145
+ Value: expect.stringContaining(
146
+ 'https://brain.example/campaigns/unsubscribe?token='
147
+ ),
148
+ },
149
+ { Name: 'List-Unsubscribe-Post', Value: 'List-Unsubscribe=One-Click' },
150
+ ],
151
+ })
152
+ );
153
+ expect(mocks.acknowledge).toHaveBeenCalledOnce();
154
+ const links = campaignEmailLinks(campaign, unsubscribeId);
155
+ expect(links.ctaUrl).toBe(
156
+ 'https://vetpass.app/m/new-programs?utm_source=email&utm_campaign=new-programs'
157
+ );
158
+ expect(links.unsubscribeUrl).toMatch(
159
+ /^https:\/\/brain.example\/campaigns\/unsubscribe\/browser\?token=/
160
+ );
161
+ });
162
+ it.each([
163
+ { channels: ['email'], message: campaign.message },
164
+ { channels: ['inApp', 'email'], message: { ...campaign.message!, enabled: false } },
165
+ { channels: ['inApp', 'email'], message: undefined },
166
+ ])('omits the campaign destination without enabled in-app content: %j', async variant => {
167
+ const stored = { ...campaign, ...variant } as Campaign;
168
+ mocks.recipient = { campaign: stored, email: 'adult@example.com', unsubscribeId };
169
+ await sendCampaignEmail(stored, 'adult', true);
170
+ expect(campaignEmailLinks(stored, unsubscribeId).ctaUrl).toBeUndefined();
171
+ const model = mocks.prepare.mock.calls[0][0].templateModel;
172
+ expect(model.ctaUrl).toBeUndefined();
173
+ expect(model.body).toBe(campaign.email!.body);
174
+ expect(model.unsubscribeUrl).toMatch(
175
+ /^https:\/\/brain.example\/campaigns\/unsubscribe\/browser\?token=/
176
+ );
177
+ });
178
+ it('fails stream preflight before attempting a claim', async () => {
179
+ mocks.stream.mockRejectedValueOnce(new Error('Invalid stream'));
180
+ await expect(sendCampaignEmail(campaign, 'adult')).rejects.toThrow('Invalid stream');
181
+ expect(mocks.claim).not.toHaveBeenCalled();
182
+ expect(mocks.send).not.toHaveBeenCalled();
183
+ });
184
+ it('prepares the actual recipient before sending and never sends on render failure', async () => {
185
+ mocks.prepare.mockRejectedValueOnce(new Error('Private render detail'));
186
+ await expect(sendCampaignEmail(campaign, 'adult')).rejects.toThrow(
187
+ 'Campaign email preparation failed'
188
+ );
189
+ expect(mocks.send).not.toHaveBeenCalled();
190
+ expect(mocks.acknowledge).not.toHaveBeenCalled();
191
+ });
192
+ it('skips recipients rejected by the durable eligibility/claim boundary', async () => {
193
+ mocks.recipient = undefined;
194
+ expect(await sendCampaignEmail(campaign, 'adult')).toBe(false);
195
+ expect(mocks.send).not.toHaveBeenCalled();
196
+ expect(mocks.acknowledge).not.toHaveBeenCalled();
197
+ });
198
+ it('never acknowledges an uncertain provider send or sends twice in one call', async () => {
199
+ mocks.send.mockRejectedValue(new Error('lost acknowledgement'));
200
+ await expect(sendCampaignEmail(campaign, 'adult')).rejects.toThrow();
201
+ expect(mocks.send).toHaveBeenCalledOnce();
202
+ expect(mocks.acknowledge).not.toHaveBeenCalled();
203
+ });
204
+ it('test sends do not write production emailed stats', async () => {
205
+ await sendCampaignEmail(campaign, 'adult', true);
206
+ expect(mocks.claim).toHaveBeenCalledWith(
207
+ campaign.id,
208
+ campaign.createdAt,
209
+ 'adult',
210
+ true,
211
+ expect.any(Function)
212
+ );
213
+ expect(mocks.acknowledge).not.toHaveBeenCalled();
214
+ });
215
+ it('does not claim a retained reservation or expose provider details for a failed self-test', async () => {
216
+ mocks.send.mockRejectedValue(new Error('provider detail with private recipient and token'));
217
+ await expect(sendCampaignEmail(campaign, 'adult', true)).rejects.toThrow(
218
+ /^Campaign test email delivery failed$/
219
+ );
220
+ expect(mocks.send).toHaveBeenCalledOnce();
221
+ expect(mocks.acknowledge).not.toHaveBeenCalled();
222
+ });
223
+ it.each(['', 'outbound', 'OUTBOUND'])(
224
+ 'rejects unsafe stream %s before claiming',
225
+ async stream => {
226
+ mocks.environment.POSTMARK_BROADCAST_STREAM = stream;
227
+ await expect(sendCampaignEmail(campaign, 'adult')).rejects.toThrow();
228
+ expect(mocks.claim).not.toHaveBeenCalled();
229
+ }
230
+ );
231
+ it('fails closed without provider configuration', async () => {
232
+ mocks.environment.POSTMARK_API_KEY = '';
233
+ await expect(sendCampaignEmail(campaign, 'adult')).rejects.toThrow();
234
+ expect(mocks.claim).not.toHaveBeenCalled();
235
+ });
236
+ });
237
+ describe('unsubscribe tokens', () => {
238
+ it('round-trips a versioned durable recipient and rejects tampering', () => {
239
+ const token = signCampaignUnsubscribeToken(unsubscribeId, campaign.id);
240
+ expect(verifyCampaignUnsubscribeToken(token)).toEqual({
241
+ unsubscribeId,
242
+ campaignId: campaign.id,
243
+ });
244
+ const altered = Buffer.from(
245
+ Buffer.from(token, 'base64url').toString().replace('new-programs', 'other-programs')
246
+ ).toString('base64url');
247
+ for (const invalid of ['', '%%%', altered, token + '.', token + '=', 'a'.repeat(2049)])
248
+ expect(() => verifyCampaignUnsubscribeToken(invalid)).toThrow();
249
+ });
250
+ it('rejects signed legacy name tokens without changing preferences', async () => {
251
+ const payload = `alice.${campaign.id}`;
252
+ const signature = createHmac('sha256', mocks.environment.CAMPAIGN_UNSUBSCRIBE_SECRET)
253
+ .update(`campaign-unsubscribe:${payload}`)
254
+ .digest('base64url');
255
+ const token = Buffer.from(`${payload}.${signature}`).toString('base64url');
256
+ await expect(unsubscribeFromCampaignEmails(token)).rejects.toThrow(
257
+ 'Invalid unsubscribe link'
258
+ );
259
+ expect(mocks.update).not.toHaveBeenCalled();
260
+ });
261
+ it('repeated unsubscribe requests resolve only the durable recipient', async () => {
262
+ const token = signCampaignUnsubscribeToken(unsubscribeId, campaign.id);
263
+ await unsubscribeFromCampaignEmails(token);
264
+ await unsubscribeFromCampaignEmails(token);
265
+ expect(mocks.update).toHaveBeenCalledTimes(2);
266
+ expect(mocks.update).toHaveBeenLastCalledWith(unsubscribeId);
267
+ });
268
+ });