@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
@@ -20,6 +20,7 @@ import {
20
20
  } from './credential-refresh-materiality.helpers';
21
21
  import { getNotificationMessage } from './notificationMessages';
22
22
  import { resolveRecipientLocale } from './getRecipientLocale.helpers';
23
+ import { fetchNotificationWebhook } from './notification-webhook-fetch.helpers';
23
24
 
24
25
  // Timeout value in milliseconds for aborting the request
25
26
  const TIMEOUT = 6000;
@@ -357,18 +358,25 @@ export async function sendNotification(
357
358
  // Set a timeout to abort the fetch request
358
359
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
359
360
 
360
- let response: Response;
361
+ let response: Pick<Response, 'ok' | 'status' | 'text'>;
362
+ let responseText: string;
361
363
  try {
362
364
  directWebhookRequestStarted = true;
363
- response = await fetch(notificationsWebhook, {
364
- method: 'POST',
365
- headers: {
366
- 'Content-Type': 'application/json',
367
- 'Authorization': `Bearer ${didJwt}`,
365
+ response = await fetchNotificationWebhook(
366
+ notificationsWebhook,
367
+ {
368
+ method: 'POST',
369
+ headers: {
370
+ 'Content-Type': 'application/json',
371
+ 'Authorization': `Bearer ${didJwt}`,
372
+ },
373
+ body: JSON.stringify(notification),
374
+ signal,
368
375
  },
369
- body: JSON.stringify(notification),
370
- signal,
371
- });
376
+ runtimeEnvironment.IS_OFFLINE
377
+ );
378
+ // Keep the deadline active while reading the acknowledgement body too.
379
+ responseText = await response.text();
372
380
  } finally {
373
381
  clearTimeout(timeoutId);
374
382
  }
@@ -386,8 +394,6 @@ export async function sendNotification(
386
394
  throw createWebhookTransportError(response.status);
387
395
  }
388
396
 
389
- const responseText = await response.text();
390
-
391
397
  let responseBody: unknown;
392
398
  if (responseText.trim().length > 0) {
393
399
  try {
@@ -398,7 +404,8 @@ export async function sendNotification(
398
404
  }
399
405
 
400
406
  if (
401
- notification.data?.metadata?.connectionPrompt &&
407
+ (notification.data?.metadata?.connectionPrompt ||
408
+ notification.type === 'ANNOUNCEMENT') &&
402
409
  extractNotificationWebhookSuccess(responseBody) === null
403
410
  ) {
404
411
  throw createUnrecognizedWebhookAcknowledgementError(response.status);
@@ -0,0 +1,60 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('@instance', () => ({ neogma: {} }));
4
+ vi.mock('./credential-refresh.helpers', () => ({}));
5
+
6
+ import {
7
+ computeRefreshSendRequestDigest,
8
+ matchesRefreshSendRequestDigest,
9
+ } from './refresh-send-intent.helpers';
10
+
11
+ describe('managed send metadata digest', () => {
12
+ afterEach(() => vi.unstubAllEnvs());
13
+ const request = { recipientProfileId: 'recipient', holderDid: 'did:example:holder' };
14
+
15
+ it('matches current and retained keys without accepting removed keys or changed metadata', () => {
16
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'old-send-test-key');
17
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v1');
18
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
19
+ const stored = computeRefreshSendRequestDigest(request);
20
+ expect(matchesRefreshSendRequestDigest(request, stored)).toBe(true);
21
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'new-send-test-key');
22
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v2');
23
+ vi.stubEnv(
24
+ 'CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS',
25
+ JSON.stringify({ v1: 'old-send-test-key' })
26
+ );
27
+ expect(matchesRefreshSendRequestDigest(request, stored)).toBe(true);
28
+ expect(
29
+ matchesRefreshSendRequestDigest(request, computeRefreshSendRequestDigest(request))
30
+ ).toBe(true);
31
+ expect(
32
+ matchesRefreshSendRequestDigest({ ...request, integrationId: 'other' }, stored)
33
+ ).toBe(false);
34
+ expect(computeRefreshSendRequestDigest(request)).toMatch(/^v1:[A-Za-z0-9_-]{43}$/);
35
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
36
+ expect(matchesRefreshSendRequestDigest(request, stored)).toBe(false);
37
+ });
38
+
39
+ it('ignores claim content and uses a versioned HMAC', () => {
40
+ const original = {
41
+ ...request,
42
+ template: { name: 'PRIVATE' },
43
+ templateData: { grade: 'A' },
44
+ };
45
+ const changed = { ...request, template: { name: 'OTHER' }, templateData: { grade: 'F' } };
46
+ expect(computeRefreshSendRequestDigest(original)).toMatch(/^v1:[A-Za-z0-9_-]{43}$/);
47
+ expect(computeRefreshSendRequestDigest(original)).toBe(
48
+ computeRefreshSendRequestDigest(changed)
49
+ );
50
+ });
51
+
52
+ it('binds explicitly public display and routing metadata', () => {
53
+ expect(
54
+ computeRefreshSendRequestDigest({ ...request, display: { name: 'Public' } })
55
+ ).not.toBe(computeRefreshSendRequestDigest(request));
56
+ expect(computeRefreshSendRequestDigest({ ...request, integrationId: 'other' })).not.toBe(
57
+ computeRefreshSendRequestDigest(request)
58
+ );
59
+ });
60
+ });
@@ -1,11 +1,16 @@
1
- import { createHash, randomUUID } from 'crypto';
1
+ import { createHmac, randomUUID } from 'crypto';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import type { ManagedCredentialRefreshReceipt, SendBoostResponse } from '@learncard/types';
4
+ import { getStatusEntriesForCredential, getStatusManagement } from './status-list.helpers';
4
5
  import { canonicalizeCredentialJson } from '@learncard/helpers';
5
6
 
6
7
  import { neogma } from '@instance';
7
8
 
8
9
  import { getInitialRefreshRoot, getManagedCredentialUri } from './credential-refresh.helpers';
10
+ import {
11
+ getCredentialRefreshDigestSecret,
12
+ getCredentialRefreshDigestCandidateSecrets,
13
+ } from './credential-refresh-materiality.helpers';
9
14
 
10
15
  /** A preparation not touched for this long is considered abandoned and may be taken over. */
11
16
  export const REFRESH_SEND_INTENT_STALE_MS = 5 * 60_000;
@@ -26,6 +31,8 @@ export type RefreshSendIntent = {
26
31
  credentialId?: string;
27
32
  refreshId?: string;
28
33
  holderDid?: string;
34
+ contractUri?: string;
35
+ integrationId?: string;
29
36
  pendingReceipt?: ManagedCredentialRefreshReceipt;
30
37
  pendingActivityId?: string;
31
38
  result?: SendBoostResponse;
@@ -40,32 +47,58 @@ export type ClaimRefreshSendIntentResult =
40
47
  const getIntentKey = (issuerProfileId: string, idempotencyKey: string): string =>
41
48
  `${issuerProfileId}:${idempotencyKey}`;
42
49
 
43
- /** Digest of everything that defines a prepare request; a reused key must match it. */
44
- export const computeRefreshSendRequestDigest = (request: {
45
- recipientProfileId: string;
46
- holderDid: string;
47
- templateUri?: string;
48
- template?: unknown;
49
- contractUri?: string;
50
- templateData?: Record<string, unknown>;
51
- integrationId?: string;
52
- credentialId?: string;
53
- }): string =>
54
- createHash('sha256')
50
+ /** Versioned, domain-separated public-metadata digest. Legacy unkeyed intents fail closed. */
51
+ export const computeRefreshSendRequestDigest = (
52
+ request: {
53
+ statusBinding?: 'server';
54
+ statusPurposes?: ('revocation' | 'suspension')[];
55
+ recipientProfileId: string;
56
+ holderDid: string;
57
+ credentialId?: string;
58
+ templateUri?: string;
59
+ display?: import('@learncard/types').CredentialDisplay;
60
+ contractUri?: string;
61
+ integrationId?: string;
62
+ /** Included so an idempotencyKey can never silently replay across modes. */
63
+ encryption?: 'network' | 'holder';
64
+ },
65
+ secret: string = getCredentialRefreshDigestSecret()
66
+ ): string =>
67
+ 'v1:' +
68
+ createHmac('sha256', secret)
69
+ .update('refresh-send-intent:v1\0')
55
70
  .update(
56
71
  canonicalizeCredentialJson({
57
72
  recipientProfileId: request.recipientProfileId,
58
73
  holderDid: request.holderDid,
59
74
  templateUri: request.templateUri ?? null,
60
- template: request.template ?? null,
75
+ display: request.display ?? {},
61
76
  contractUri: request.contractUri ?? null,
62
- templateData: request.templateData ?? {},
63
77
  integrationId: request.integrationId ?? null,
78
+ refresh: true,
64
79
  credentialId: request.credentialId ?? null,
80
+ encryption: request.encryption ?? 'network',
81
+ ...(request.statusBinding ? { statusBinding: request.statusBinding } : {}),
82
+ ...(request.statusPurposes?.includes('suspension')
83
+ ? { suspensionRequested: true }
84
+ : {}),
65
85
  })
66
86
  )
67
87
  .digest('base64url');
68
88
 
89
+ /** The v1 prefix identifies the request scheme, not the rotating key version. */
90
+ export const getRefreshSendRequestDigestCandidates = (
91
+ request: Parameters<typeof computeRefreshSendRequestDigest>[0]
92
+ ): string[] =>
93
+ getCredentialRefreshDigestCandidateSecrets().map(secret =>
94
+ computeRefreshSendRequestDigest(request, secret)
95
+ );
96
+
97
+ export const matchesRefreshSendRequestDigest = (
98
+ request: Parameters<typeof computeRefreshSendRequestDigest>[0],
99
+ stored: string
100
+ ): boolean => getRefreshSendRequestDigestCandidates(request).includes(stored);
101
+
69
102
  const parseIntent = (props: Record<string, unknown>): RefreshSendIntent => ({
70
103
  intentKey: props.intentKey as string,
71
104
  issuerProfileId: props.issuerProfileId as string,
@@ -76,6 +109,8 @@ const parseIntent = (props: Record<string, unknown>): RefreshSendIntent => ({
76
109
  credentialId: (props.credentialId as string | undefined) ?? undefined,
77
110
  refreshId: (props.refreshId as string | undefined) ?? undefined,
78
111
  holderDid: (props.holderDid as string | undefined) ?? undefined,
112
+ contractUri: (props.contractUri as string | undefined) ?? undefined,
113
+ integrationId: (props.integrationId as string | undefined) ?? undefined,
79
114
  pendingReceipt: props.pendingReceipt
80
115
  ? (JSON.parse(props.pendingReceipt as string) as ManagedCredentialRefreshReceipt)
81
116
  : undefined,
@@ -97,6 +132,19 @@ export const getRefreshSendIntent = async (
97
132
  return node ? parseIntent(node.properties) : null;
98
133
  };
99
134
 
135
+ /** Resolve the preparation even when the caller did not supply an idempotency key. */
136
+ export const getRefreshSendIntentForRefresh = async (
137
+ issuerProfileId: string,
138
+ refreshId: string
139
+ ): Promise<RefreshSendIntent | null> => {
140
+ const result = await neogma.queryRunner.run(
141
+ 'MATCH (i:RefreshSendIntent {issuerProfileId: $issuerProfileId, refreshId: $refreshId}) RETURN i',
142
+ { issuerProfileId, refreshId }
143
+ );
144
+ const node = result.records[0]?.get('i');
145
+ return node ? parseIntent(node.properties) : null;
146
+ };
147
+
100
148
  /**
101
149
  * Creates or claims the intent for (issuer, idempotencyKey). Concurrency-safe through
102
150
  * the `refresh_send_intent_key_unique` constraint. Throws CONFLICT when the key was used
@@ -106,8 +154,10 @@ export const claimRefreshSendIntent = async (params: {
106
154
  issuerProfileId: string;
107
155
  idempotencyKey: string;
108
156
  requestDigest: string;
157
+ requestDigestCandidates?: string[];
109
158
  }): Promise<ClaimRefreshSendIntentResult> => {
110
159
  const { issuerProfileId, idempotencyKey, requestDigest } = params;
160
+ const requestDigestCandidates = params.requestDigestCandidates ?? [requestDigest];
111
161
  const claimToken = randomUUID();
112
162
  const now = new Date();
113
163
 
@@ -125,7 +175,7 @@ export const claimRefreshSendIntent = async (params: {
125
175
  WITH i
126
176
  WITH i WHERE i.state = 'preparing'
127
177
  AND i.claimToken <> $claimToken
128
- AND i.requestDigest = $requestDigest
178
+ AND i.requestDigest IN $requestDigestCandidates
129
179
  AND i.updatedAt < $staleBefore
130
180
  SET i.claimToken = $claimToken, i.updatedAt = $now
131
181
  }
@@ -134,6 +184,7 @@ export const claimRefreshSendIntent = async (params: {
134
184
  intentKey: getIntentKey(issuerProfileId, idempotencyKey),
135
185
  issuerProfileId,
136
186
  requestDigest,
187
+ requestDigestCandidates,
137
188
  claimToken,
138
189
  now: now.toISOString(),
139
190
  staleBefore: new Date(now.getTime() - REFRESH_SEND_INTENT_STALE_MS).toISOString(),
@@ -141,7 +192,7 @@ export const claimRefreshSendIntent = async (params: {
141
192
  );
142
193
  const intent = parseIntent(result.records[0]!.get('i').properties);
143
194
 
144
- if (intent.requestDigest !== requestDigest) {
195
+ if (!requestDigestCandidates.includes(intent.requestDigest)) {
145
196
  throw new TRPCError({
146
197
  code: 'CONFLICT',
147
198
  message: 'This idempotencyKey was already used for a different send request.',
@@ -173,7 +224,15 @@ const lostIntentClaim = (): TRPCError =>
173
224
  export const recordRefreshSendIntent = async (
174
225
  intent: RefreshSendIntent,
175
226
  fields: Partial<
176
- Pick<RefreshSendIntent, 'boostUri' | 'credentialId' | 'refreshId' | 'holderDid'>
227
+ Pick<
228
+ RefreshSendIntent,
229
+ | 'boostUri'
230
+ | 'credentialId'
231
+ | 'refreshId'
232
+ | 'holderDid'
233
+ | 'contractUri'
234
+ | 'integrationId'
235
+ >
177
236
  > & { state?: 'prepared' }
178
237
  ): Promise<RefreshSendIntent> => {
179
238
  const result = await neogma.queryRunner.run(
@@ -265,6 +324,10 @@ export const reconcileBoundRefreshSendIntent = async (
265
324
  credentialUri: getManagedCredentialUri(root.rootId, domain),
266
325
  activityId: intent.pendingActivityId ?? '',
267
326
  refresh: intent.pendingReceipt,
327
+ statusManagement: getStatusManagement(
328
+ await getStatusEntriesForCredential(root.rootId),
329
+ domain
330
+ ),
268
331
  };
269
332
 
270
333
  await markRefreshSendIntentDelivered(intent, result);
@@ -0,0 +1,36 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import type { BoostInstance } from '@models';
3
+ import type { ProfileType } from 'types/profile';
4
+ import {
5
+ getContractDetailsByUri,
6
+ getContractTermsForProfile,
7
+ getWritersForContract,
8
+ } from '@accesslayer/consentflowcontract/relationships/read';
9
+ import { setRelatedBoostForContract } from '@accesslayer/consentflowcontract/relationships/create';
10
+
11
+ /** Resolve current send permissions; delivery must recheck the returned terms transactionally. */
12
+ export const resolveContractForSend = async (params: {
13
+ profile: ProfileType;
14
+ targetProfile: ProfileType;
15
+ boost: BoostInstance;
16
+ boostCreated: boolean;
17
+ contractUri?: string;
18
+ }): Promise<Awaited<ReturnType<typeof getContractTermsForProfile>> | null> => {
19
+ const { profile, targetProfile, boost, boostCreated, contractUri } = params;
20
+ if (!contractUri) return null;
21
+
22
+ const details = await getContractDetailsByUri(decodeURIComponent(contractUri));
23
+ if (!details) {
24
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
25
+ }
26
+ const terms = await getContractTermsForProfile(targetProfile, details.contract);
27
+ const writers = await getWritersForContract(details.contract);
28
+ const allowed =
29
+ terms &&
30
+ writers.some(writer => writer.profileId === profile.profileId) &&
31
+ !terms.terms.deniedWriters?.includes(profile.profileId) &&
32
+ (!boost.category || terms.terms.write?.credentials?.categories?.[boost.category] === true);
33
+
34
+ if (boostCreated) await setRelatedBoostForContract(details.contract, boost);
35
+ return allowed ? terms : null;
36
+ };
@@ -1,10 +1,13 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- const mocks = vi.hoisted(() => ({ findOne: vi.fn(), session: vi.fn(), transaction: vi.fn() }));
3
+ const mocks = vi.hoisted(() => ({ run: vi.fn(), session: vi.fn(), transaction: vi.fn() }));
4
4
  vi.mock('@environment', () => ({ environment: {} }));
5
- vi.mock('@models', () => ({ Credential: { findOne: mocks.findOne } }));
6
5
  vi.mock('@instance', () => ({
7
- neogma: { driver: { session: mocks.session }, getTransaction: mocks.transaction },
6
+ neogma: {
7
+ driver: { session: mocks.session },
8
+ getTransaction: mocks.transaction,
9
+ queryRunner: { run: mocks.run },
10
+ },
8
11
  }));
9
12
  vi.mock('@helpers/learnCard.helpers', () => ({
10
13
  getLearnCard: vi.fn(),
@@ -13,6 +16,21 @@ vi.mock('@helpers/learnCard.helpers', () => ({
13
16
 
14
17
  import { setCredentialBitstringStatus } from './status-list.helpers';
15
18
 
19
+ type StoredRow = { direct?: string; body?: string; entries?: string };
20
+
21
+ /** Mock the stored-status lookup, then the issuer-owner lookup. */
22
+ const mockStored = (row: StoredRow | null, owner: string | null = 'issuer') => {
23
+ const record = (values: Record<string, unknown>) => ({
24
+ get: (key: string) => values[key] ?? null,
25
+ });
26
+ mocks.run.mockImplementation(async (query: string) => {
27
+ if (query.includes('AS owner')) {
28
+ return { records: owner ? [record({ owner })] : [] };
29
+ }
30
+ return { records: row ? [record({ ...row })] : [] };
31
+ });
32
+ };
33
+
16
34
  describe('stored credential status validation', () => {
17
35
  beforeEach(() => {
18
36
  vi.clearAllMocks();
@@ -21,7 +39,7 @@ describe('stored credential status validation', () => {
21
39
  it.each(['{truncated', 'null', '{}', '[null]', '[{}]', '"not an array"'])(
22
40
  'returns false for malformed status metadata (%s) without starting a status write',
23
41
  async statusEntries => {
24
- mocks.findOne.mockResolvedValue({ statusEntries, credential: '{}' });
42
+ mockStored({ direct: statusEntries, body: '{}' });
25
43
  await expect(
26
44
  setCredentialBitstringStatus('credential', 'revocation', true)
27
45
  ).resolves.toBe(false);
@@ -30,7 +48,7 @@ describe('stored credential status validation', () => {
30
48
  );
31
49
 
32
50
  it('returns false when legacy credential JSON cannot be parsed', async () => {
33
- mocks.findOne.mockResolvedValue({ credential: '{truncated' });
51
+ mockStored({ body: '{truncated' });
34
52
  await expect(setCredentialBitstringStatus('credential', 'suspension', true)).resolves.toBe(
35
53
  false
36
54
  );
@@ -38,16 +56,15 @@ describe('stored credential status validation', () => {
38
56
  });
39
57
 
40
58
  it('returns false for valid empty metadata or a missing credential', async () => {
41
- mocks.findOne
42
- .mockResolvedValueOnce({ statusEntries: '[]', credential: '{}' })
43
- .mockResolvedValueOnce(null);
59
+ mockStored({ direct: '[]', body: '{}' });
44
60
  expect(await setCredentialBitstringStatus('credential', 'revocation', true)).toBe(false);
61
+ mockStored(null);
45
62
  expect(await setCredentialBitstringStatus('missing', 'revocation', true)).toBe(false);
46
63
  });
47
64
 
48
65
  it('returns false when a valid entry cannot be updated', async () => {
49
- mocks.findOne.mockResolvedValue({
50
- statusEntries: JSON.stringify([
66
+ mockStored({
67
+ direct: JSON.stringify([
51
68
  {
52
69
  type: 'BitstringStatusListEntry',
53
70
  statusPurpose: 'revocation',
@@ -61,8 +78,8 @@ describe('stored credential status validation', () => {
61
78
  });
62
79
 
63
80
  it('rejects malformed numeric coordinates before any status write', async () => {
64
- mocks.findOne.mockResolvedValue({
65
- statusEntries: JSON.stringify([
81
+ mockStored({
82
+ direct: JSON.stringify([
66
83
  {
67
84
  type: 'BitstringStatusListEntry',
68
85
  statusPurpose: 'revocation',