@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
@@ -6,10 +6,14 @@ import { isIssuedCredential } from '@helpers/issuedCredentialStatus.helpers';
6
6
  import type { IssuedCredential } from 'types/credential';
7
7
 
8
8
  import { Credential, CredentialInstance } from '@models';
9
+ import { neogma } from '@instance';
10
+ import { reserveCredentialStatusEntries } from '@helpers/status-list.helpers';
9
11
 
10
12
  export const storeCredential = async (
11
13
  input: UnsignedVC | VC | JWE | IssuedCredential,
12
- transaction?: Transaction
14
+ transaction?: Transaction,
15
+ statusOwnerProfileId?: string,
16
+ options: { lenient?: boolean } = {}
13
17
  ): Promise<CredentialInstance> => {
14
18
  const id = uuid();
15
19
 
@@ -21,6 +25,7 @@ export const storeCredential = async (
21
25
  ? BitstringStatusListEntryValidator.array().parse(input.statusEntries)
22
26
  : undefined;
23
27
  const encrypted = isEncrypted(credential);
28
+ const lenient = options.lenient === true && !issued && !encrypted;
24
29
  if (
25
30
  !statusEntries?.length &&
26
31
  (encrypted || !getBitstringStatusListEntries(credential).length)
@@ -30,12 +35,31 @@ export const storeCredential = async (
30
35
  { credentialId: id }
31
36
  );
32
37
  }
33
- return Credential.createOne(
34
- {
35
- id,
36
- credential: JSON.stringify(credential),
37
- ...(statusEntries?.length ? { statusEntries: JSON.stringify(statusEntries) } : {}),
38
- },
39
- transaction ? { session: transaction } : undefined
40
- );
38
+ const create = (session?: Transaction, validatedOwner?: string) =>
39
+ Credential.createOne(
40
+ {
41
+ id,
42
+ credential: JSON.stringify(credential),
43
+ ...(statusEntries?.length ? { statusEntries: JSON.stringify(statusEntries) } : {}),
44
+ ...(validatedOwner
45
+ ? {
46
+ statusOwnerProfileId: validatedOwner,
47
+ statusReservationMode: lenient
48
+ ? ('lenient' as const)
49
+ : ('strict' as const),
50
+ }
51
+ : {}),
52
+ },
53
+ session ? { session } : undefined
54
+ );
55
+ const entries = statusEntries ?? getBitstringStatusListEntries(credential);
56
+ if (!statusOwnerProfileId || entries.length === 0) return create(transaction);
57
+ return neogma.getTransaction(transaction ?? null, async tx => {
58
+ await reserveCredentialStatusEntries(entries, statusOwnerProfileId, id, tx, {
59
+ lenient,
60
+ ...(!encrypted ? { credential } : {}),
61
+ });
62
+ // Inbox deliveries need a validated owner even without a CREDENTIAL_SENT edge.
63
+ return create(tx, statusOwnerProfileId);
64
+ });
41
65
  };
@@ -1,5 +1,50 @@
1
1
  import { CredentialInstance } from '@models';
2
+ import { neogma } from '@instance';
2
3
 
3
4
  export const deleteCredential = async (credential: CredentialInstance): Promise<void> => {
4
5
  await credential.delete({ detach: true });
5
6
  };
7
+
8
+ /** Release failed deliveries with no relationships beyond Boost instance membership. */
9
+ export const releaseUnlinkedCredential = async (id: string): Promise<void> => {
10
+ await neogma.getTransaction(null, async tx => {
11
+ const eligible = await tx.run(
12
+ `MATCH (credential:Credential {id: $id})
13
+ SET credential._statusMutationLock = true REMOVE credential._statusMutationLock
14
+ WITH credential
15
+ WHERE NOT EXISTS {
16
+ MATCH (credential)-[relationship]-()
17
+ WHERE type(relationship) <> 'INSTANCE_OF'
18
+ }
19
+ RETURN credential.id AS id`,
20
+ { id }
21
+ );
22
+ if (eligible.records.length === 0) return;
23
+ const lists = await tx.run(
24
+ `MATCH (list:BitstringStatusList)-[:HAS_STATUS_BINDING]->(binding:CredentialStatusBinding)
25
+ WHERE $id IN coalesce(binding.credentialIds, [binding.credentialId])
26
+ RETURN DISTINCT list.statusListCredential AS url ORDER BY url`,
27
+ { id }
28
+ );
29
+ // Match reservation lock order and re-read membership only after acquiring each list lock.
30
+ for (const row of [...lists.records].sort((a, b) =>
31
+ String(a.get('url')).localeCompare(String(b.get('url')))
32
+ )) {
33
+ const url = String(row.get('url'));
34
+ await tx.run(
35
+ `MATCH (list:BitstringStatusList {statusListCredential: $url})
36
+ SET list._statusBindingLock = true REMOVE list._statusBindingLock`,
37
+ { url }
38
+ );
39
+ await tx.run(
40
+ `MATCH (:BitstringStatusList {statusListCredential: $url})-[:HAS_STATUS_BINDING]->(binding:CredentialStatusBinding)
41
+ WHERE binding.refreshId IS NULL AND $id IN coalesce(binding.credentialIds, [binding.credentialId])
42
+ WITH binding, [member IN coalesce(binding.credentialIds, [binding.credentialId]) WHERE member <> $id] AS remaining
43
+ SET binding.credentialIds = remaining, binding.credentialId = head(remaining)
44
+ FOREACH (_ IN CASE WHEN size(remaining) = 0 THEN [1] ELSE [] END | DETACH DELETE binding)`,
45
+ { id, url }
46
+ );
47
+ }
48
+ await tx.run('MATCH (credential:Credential {id: $id}) DETACH DELETE credential', { id });
49
+ });
50
+ };
@@ -7,9 +7,9 @@ import {
7
7
  CredentialInstance,
8
8
  Credential,
9
9
  Boost,
10
+ BoostInstance,
10
11
  Role,
11
12
  Profile,
12
- ConsentFlowTerms,
13
13
  ConsentFlowTransaction,
14
14
  AppStoreListing,
15
15
  } from '@models';
@@ -19,6 +19,8 @@ import { CredentialIssuer, getIssuerOwnerProfile, isAppStoreListingIssuer } from
19
19
  import { clearDidWebCacheForChildProfileManagers } from '@accesslayer/boost/relationships/update';
20
20
  import { getBoostIdForCredentialInstance } from '@accesslayer/credential/relationships/read';
21
21
  import { DbTermsType } from 'types/consentflowcontract';
22
+ import { TRPCError } from '@trpc/server';
23
+ import { runAudienceMutation } from '@accesslayer/consentflowcontract/relationships/recipients';
22
24
 
23
25
  export const createSentCredentialRelationship = async (
24
26
  issuer: CredentialIssuer,
@@ -102,7 +104,8 @@ export const createListingSentCredentialRelationship = async (
102
104
  credential: CredentialInstance,
103
105
  metadata?: Record<string, unknown>,
104
106
  activityId?: string,
105
- integrationId?: string
107
+ integrationId?: string,
108
+ transaction?: Transaction
106
109
  ): Promise<void> => {
107
110
  const properties = flattenObject({
108
111
  to: to.profileId,
@@ -133,7 +136,7 @@ export const createListingSentCredentialRelationship = async (
133
136
  }]->(credential)`
134
137
  )
135
138
  .set('r = $params')
136
- .run();
139
+ .run(transaction);
137
140
  };
138
141
 
139
142
  export const createReceivedCredentialRelationship = async (
@@ -218,49 +221,101 @@ export const setDefaultClaimedRole = async (
218
221
  };
219
222
 
220
223
  /**
221
- * Creates a transaction for a credential being issued via a contract's write permission
222
- * and links the credential to that transaction (which is linked to the terms)
224
+ * Commit delivery and its audit entry under the same lock as access updates.
225
+ * Signing may have awaited an external service; the captured grant is not authority.
223
226
  */
224
- export const createCredentialIssuedViaContractRelationship = async (
225
- credential: CredentialInstance,
226
- terms: DbTermsType
227
- ): Promise<void> => {
228
- const currentDate = new Date().toISOString();
229
- const transactionId = uuid();
230
-
231
- await new QueryBuilder()
232
- .match({
233
- model: ConsentFlowTerms,
234
- where: { id: terms.id },
235
- identifier: 'terms',
236
- })
237
- .create({
238
- related: [
239
- {
240
- model: ConsentFlowTransaction,
241
- identifier: 'transaction',
242
- properties: { id: transactionId, action: 'write', date: currentDate },
243
- },
244
- ConsentFlowTransaction.getRelationshipByAlias('isFor'),
245
- { identifier: 'terms' },
246
- ],
247
- })
248
- .raw(termsReferralSnapshotCypher())
249
- .with('transaction')
250
- .match({
251
- model: Credential,
252
- where: { id: credential.id },
253
- identifier: 'credential',
254
- })
255
- .create({
256
- related: [
257
- { identifier: 'credential' },
258
- {
259
- ...Credential.getRelationshipByAlias('issuedViaTransaction'),
260
- properties: { date: currentDate },
261
- },
262
- { identifier: 'transaction' },
263
- ],
264
- })
265
- .run();
227
+ export const createContractCredentialRelationships = async ({
228
+ issuer,
229
+ writerProfileId,
230
+ recipient,
231
+ credential,
232
+ boost,
233
+ terms,
234
+ metadata,
235
+ activityId,
236
+ integrationId,
237
+ listingId,
238
+ allowConsumedOneTime = false,
239
+ }: {
240
+ issuer: CredentialIssuer;
241
+ writerProfileId: string;
242
+ recipient: ProfileType;
243
+ credential: CredentialInstance;
244
+ boost: BoostInstance;
245
+ terms: DbTermsType;
246
+ metadata?: Record<string, unknown>;
247
+ activityId?: string;
248
+ integrationId?: string;
249
+ listingId?: string;
250
+ allowConsumedOneTime?: boolean;
251
+ }): Promise<void> => {
252
+ const date = new Date().toISOString();
253
+ const owner = getIssuerOwnerProfile(issuer);
254
+ const listingIds = [
255
+ ...new Set([
256
+ ...(isAppStoreListingIssuer(issuer) ? [issuer.listing.listing_id] : []),
257
+ ...(listingId ? [listingId] : []),
258
+ ]),
259
+ ];
260
+ const result = await runAudienceMutation(
261
+ new QueryBuilder(
262
+ new BindParam({
263
+ termsId: terms.id,
264
+ expectedVersion: terms.consentedVersion,
265
+ issuerId: owner.profileId,
266
+ writerProfileId,
267
+ recipientId: recipient.profileId,
268
+ credentialId: credential.id,
269
+ boostId: boost.id,
270
+ listingIds,
271
+ allowConsumedOneTime,
272
+ sent: flattenObject({
273
+ to: recipient.profileId,
274
+ date,
275
+ ...(metadata ? { metadata } : {}),
276
+ ...(activityId ? { activityId } : {}),
277
+ ...(integrationId ? { integrationId } : {}),
278
+ }),
279
+ transactionId: uuid(),
280
+ date,
281
+ })
282
+ ).raw(`
283
+ MATCH (terms:ConsentFlowTerms {id:$termsId})-[:CONSENTS_TO]->(contract:ConsentFlowContract)
284
+ SET contract.audienceLock = coalesce(contract.audienceLock, 0) + 1
285
+ WITH terms, contract
286
+ WHERE coalesce(contract.status, 'active') <> 'retired'
287
+ AND contract.currentVersion = $expectedVersion
288
+ AND terms.consentedVersion = $expectedVersion
289
+ AND (terms.status = 'live' OR
290
+ ($allowConsumedOneTime AND terms.status = 'consumed' AND terms.oneTime = true))
291
+ AND (terms.expiresAt IS NULL OR trim(terms.expiresAt) = '' OR datetime(terms.expiresAt) > datetime.realtime())
292
+ AND (contract.expiresAt IS NULL OR trim(contract.expiresAt) = '' OR datetime(contract.expiresAt) > datetime.realtime())
293
+ AND NOT $writerProfileId IN coalesce(terms['terms.deniedWriters'], [])
294
+ AND EXISTS { MATCH (contract)-[:CREATED_BY|CAN_WRITE]->(:Profile {profileId:$writerProfileId}) }
295
+ AND EXISTS { MATCH (:Profile {profileId:$recipientId})-[:CREATED_BY]->(terms) }
296
+ MATCH (credential:Credential {id:$credentialId}), (boost:Boost {id:$boostId}),
297
+ (issuer:Profile {profileId:$issuerId})
298
+ WHERE boost.category IS NULL OR terms['terms.write.credentials.categories.' + boost.category] = true
299
+ OPTIONAL MATCH (listing:AppStoreListing) WHERE listing.listing_id IN $listingIds
300
+ WITH terms, contract, credential, boost, issuer, collect(DISTINCT listing) AS listings
301
+ WHERE size(listings) = size($listingIds)
302
+ CREATE (credential)-[:${Credential.getRelationshipByAlias('instanceOf').name}]->(boost)
303
+ CREATE (issuer)-[sent:${Profile.getRelationshipByAlias('credentialSent').name}]->(credential)
304
+ SET sent = $sent
305
+ FOREACH (listing IN listings |
306
+ CREATE (listing)-[delivery:CREDENTIAL_SENT]->(credential)
307
+ SET delivery = $sent)
308
+ CREATE (transaction:ConsentFlowTransaction {id:$transactionId, action:'write', date:$date, boostId:$boostId})
309
+ -[:${ConsentFlowTransaction.getRelationshipByAlias('isFor').name}]->(terms)
310
+ ${termsReferralSnapshotCypher()}
311
+ CREATE (credential)-[:${Credential.getRelationshipByAlias('issuedViaTransaction').name} {date:$date}]->(transaction)
312
+ RETURN transaction.id AS id
313
+ `)
314
+ );
315
+ if (!result.records.length) {
316
+ throw new TRPCError({
317
+ code: 'FORBIDDEN',
318
+ message: 'User has not yet approved the latest version of this contract',
319
+ });
320
+ }
266
321
  };
@@ -1,135 +1,27 @@
1
1
  import { QueryBuilder, BindParam } from 'neogma';
2
2
 
3
3
  import { Credential } from '@models';
4
- import { setCredentialBitstringStatus } from '@helpers/status-list.helpers';
5
- import { revokeCredentialRefreshForCredential } from '@accesslayer/credential-refresh/update';
4
+ import { mutateCredentialStatus } from '@helpers/credential-status-mutation.helpers';
6
5
 
7
- /**
8
- * Revoke a credential by setting its issuer-controlled status on the CREDENTIAL_SENT relationship.
9
- * This applies to both pending and claimed credentials without creating a received relationship.
10
- */
6
+ /** Revoke the root credential and its public bit atomically. */
11
7
  export const revokeCredentialReceived = async (
12
8
  credentialId: string,
13
9
  profileId: string
14
- ): Promise<boolean> => {
15
- const revokedAt = new Date().toISOString();
16
-
17
- const result = await new QueryBuilder(new BindParam({ profileId, revokedAt }))
18
- .match({ identifier: 'credential', model: Credential, where: { id: credentialId } })
19
- .raw(
20
- `MATCH (sender)-[sent:CREDENTIAL_SENT {to: $profileId}]->(credential)
21
- WHERE sender:Profile OR sender:AppStoreListing
22
- SET sent.status = "revoked",
23
- sent.revokedAt = $revokedAt
24
- RETURN sent`
25
- )
26
- .run();
27
-
28
- if (result.records.length > 0) {
29
- const bitSet = await setCredentialBitstringStatus(credentialId, 'revocation', true);
30
- if (!bitSet) {
31
- // The relationship is marked revoked but the credential has no 'revocation'
32
- // status-list entry, so the verifiable bit was NOT set. Holders relying purely
33
- // on verifyCredential can't see the revocation. Surface it instead of failing
34
- // silently (the authoritative relationship status still drives the UI).
35
- console.warn(
36
- `[revokeCredentialReceived] credential ${credentialId} has no verifiable 'revocation' status entry; bitstring bit not set`
37
- );
38
- }
39
- }
10
+ ): Promise<boolean> => mutateCredentialStatus(credentialId, profileId, 'revocation', true);
40
11
 
41
- // Managed credential refresh coupling (LC-2117/LC-2135): revoking the original
42
- // sent credential invalidates the whole refresh chain. Best-effort — the holder
43
- // endpoint independently cross-checks this canonical relationship state on every
44
- // authenticated request, so a failed write here cannot leave a revoked credential
45
- // servable.
46
- if (result.records.length > 0) {
47
- try {
48
- await revokeCredentialRefreshForCredential(credentialId);
49
- } catch (error) {
50
- console.error(
51
- '[revokeCredentialReceived] failed to revoke credential refresh aggregate:',
52
- error
53
- );
54
- }
55
- }
56
-
57
- return result.records.length > 0;
58
- };
59
-
60
- /**
61
- * Suspend a credential by setting its issuer-controlled status on the CREDENTIAL_SENT relationship.
62
- * If the credential has already been revoked, the revoked relationship state is preserved.
63
- */
12
+ /** Suspend the root credential without changing an irreversible revocation. */
64
13
  export const suspendCredentialReceived = async (
65
14
  credentialId: string,
66
15
  profileId: string
67
- ): Promise<boolean> => {
68
- const suspendedAt = new Date().toISOString();
69
-
70
- const result = await new QueryBuilder(new BindParam({ profileId, suspendedAt }))
71
- .match({ identifier: 'credential', model: Credential, where: { id: credentialId } })
72
- .raw(
73
- `MATCH (sender)-[sent:CREDENTIAL_SENT {to: $profileId}]->(credential)
74
- WHERE (sender:Profile OR sender:AppStoreListing)
75
- AND coalesce(sent.status, "") <> "revoked"
76
- SET sent.status = "suspended",
77
- sent.suspendedAt = $suspendedAt
78
- RETURN sent`
79
- )
80
- .run();
81
-
82
- if (result.records.length > 0) {
83
- const bitSet = await setCredentialBitstringStatus(credentialId, 'suspension', true);
84
- if (!bitSet) {
85
- // Relationship marked suspended but the credential has no 'suspension' status
86
- // entry, so the verifiable bit was NOT set. The authoritative relationship
87
- // status still drives the UI; log so the gap is visible.
88
- console.warn(
89
- `[suspendCredentialReceived] credential ${credentialId} has no verifiable 'suspension' status entry; bitstring bit not set`
90
- );
91
- }
92
- }
93
-
94
- return result.records.length > 0;
95
- };
16
+ ): Promise<boolean> => mutateCredentialStatus(credentialId, profileId, 'suspension', true);
96
17
 
97
- /**
98
- * Clear a reversible suspension. Revocation remains irreversible and is not cleared.
99
- */
18
+ /** Failed bit writes roll back the relationship, so unsuspension remains retryable. */
100
19
  export const unsuspendCredentialReceived = async (
101
20
  credentialId: string,
102
21
  profileId: string
103
- ): Promise<boolean> => {
104
- const unsuspendedAt = new Date().toISOString();
105
-
106
- const result = await new QueryBuilder(new BindParam({ profileId, unsuspendedAt }))
107
- .match({ identifier: 'credential', model: Credential, where: { id: credentialId } })
108
- .raw(
109
- `MATCH (sender)-[sent:CREDENTIAL_SENT {to: $profileId}]->(credential)
110
- WHERE (sender:Profile OR sender:AppStoreListing)
111
- AND sent.status = "suspended"
112
- SET sent.status = null,
113
- sent.unsuspendedAt = $unsuspendedAt
114
- RETURN sent`
115
- )
116
- .run();
117
-
118
- if (result.records.length > 0) {
119
- const bitCleared = await setCredentialBitstringStatus(credentialId, 'suspension', false);
120
- if (!bitCleared) {
121
- console.warn(
122
- `[unsuspendCredentialReceived] credential ${credentialId} has no verifiable 'suspension' status entry; bitstring bit not cleared`
123
- );
124
- }
125
- }
126
-
127
- return result.records.length > 0;
128
- };
22
+ ): Promise<boolean> => mutateCredentialStatus(credentialId, profileId, 'suspension', false);
129
23
 
130
- /**
131
- * Check if a credential has been revoked for a specific profile
132
- */
24
+ /** Check if a credential has been revoked for a specific profile. */
133
25
  export const isCredentialRevoked = async (
134
26
  credentialId: string,
135
27
  profileId: string
@@ -39,6 +39,28 @@ export const getCredentialRefresh = async (
39
39
  return CredentialRefreshRecordValidator.parse(normalizeProps(node.properties));
40
40
  };
41
41
 
42
+ /** Select only a holder's actual root delivery; acceptance is deliberately not required. */
43
+ export const getCredentialRefreshReceiptBinding = async (
44
+ rootId: string,
45
+ holderProfileId: string
46
+ ): Promise<CredentialRefreshRecord | null | undefined> => {
47
+ const result = await neogma.queryRunner.run(
48
+ `MATCH (sender)-[sent:CREDENTIAL_SENT {to: $holderProfileId}]->(root:Credential {id: $rootId})
49
+ WHERE (sender:Profile OR sender:AppStoreListing)
50
+ AND (sent.status IS NULL OR sent.status <> 'revoked')
51
+ AND NOT EXISTS {
52
+ MATCH (root)-[received:CREDENTIAL_RECEIVED]->(:Profile {profileId: $holderProfileId})
53
+ WHERE received.status = 'revoked'
54
+ }
55
+ OPTIONAL MATCH (refresh:CredentialRefresh)-[:ROOT]->(root)
56
+ RETURN refresh LIMIT 1`,
57
+ { rootId, holderProfileId }
58
+ );
59
+ if (!result.records.length) return undefined;
60
+ const node = result.records[0]?.get('refresh');
61
+ return node ? CredentialRefreshRecordValidator.parse(normalizeProps(node.properties)) : null;
62
+ };
63
+
42
64
  /** Fetches the current head version node (including its holder-encrypted JWE payload). */
43
65
  export const getCredentialRefreshHead = async (
44
66
  refreshId: string
@@ -205,7 +227,8 @@ export const getCredentialRefreshVersionsForHolder = async (
205
227
  effectiveAt: version.effectiveAt,
206
228
  etag: version.etag,
207
229
  signingMode: version.signingMode,
208
- updateSummary: version.updateSummary
230
+ updateSummary: version.updateSummary,
231
+ updateReason: version.updateReason
209
232
  } END) WHERE entry IS NOT NULL] AS metadata
210
233
  RETURN revoked, metadata`,
211
234
  { refreshId, beforeVersion: beforeVersion ?? null, limitPlusOne: int(limit + 1) }
@@ -279,7 +302,8 @@ export const getCredentialRefreshVersions = async (
279
302
  effectiveAt: version.effectiveAt,
280
303
  etag: version.etag,
281
304
  signingMode: version.signingMode,
282
- updateSummary: version.updateSummary
305
+ updateSummary: version.updateSummary,
306
+ updateReason: version.updateReason
283
307
  } AS metadata
284
308
  ORDER BY version.version DESC
285
309
  LIMIT $limitPlusOne`,
@@ -78,6 +78,7 @@ export const advanceCredentialRefreshHead = async (
78
78
  etag,
79
79
  materialDigest,
80
80
  updateSummary,
81
+ updateReason,
81
82
  credentialDisplayName,
82
83
  effectiveAt,
83
84
  notificationOutcome,
@@ -104,6 +105,7 @@ export const advanceCredentialRefreshHead = async (
104
105
  etag,
105
106
  signingMode,
106
107
  updateSummary,
108
+ updateReason,
107
109
  credentialDisplayName,
108
110
  idempotencyKey,
109
111
  refreshIdempotencyKey,
@@ -161,8 +163,9 @@ export const advanceCredentialRefreshHead = async (
161
163
  SET refresh.currentVersion = $nextVersion,
162
164
  refresh.etag = $etag,
163
165
  refresh.materialDigest = $materialDigest,
164
- refresh.updateSummary = $updateSummary,
165
- refresh.credentialDisplayName = coalesce($credentialDisplayName, refresh.credentialDisplayName),
166
+ refresh.updateSummary = $updateSummary,
167
+ refresh.updateReason = $updateReason,
168
+ refresh.credentialDisplayName = $credentialDisplayName,
166
169
  refresh.signingMode = $signingMode,
167
170
  refresh.lastPublishedAt = $publishedAt,
168
171
  refresh.idempotencyKey = $idempotencyKey,
@@ -179,6 +182,7 @@ export const advanceCredentialRefreshHead = async (
179
182
  etag: etag ?? null,
180
183
  materialDigest: materialDigest ?? null,
181
184
  updateSummary: updateSummary ?? null,
185
+ updateReason: updateReason ?? null,
182
186
  credentialDisplayName: credentialDisplayName ?? null,
183
187
  signingMode,
184
188
  publishedAt,
@@ -16,7 +16,12 @@ export const DEFAULT_INBOX_EXPIRY_DAYS = 30;
16
16
 
17
17
  export const createInboxCredential = async (input: {
18
18
  credential: string;
19
- refresh?: { aggregate: Record<string, unknown>; issueKey: string; requestDigest: string };
19
+ refresh?: {
20
+ aggregate: Record<string, unknown>;
21
+ issueKey: string;
22
+ requestDigest: string;
23
+ requestDigestCandidates?: string[];
24
+ };
20
25
  /** Only set after the normal delivery helper has durably stored the credential. */
21
26
  delivered?: boolean;
22
27
  isSigned: boolean;
@@ -93,6 +98,7 @@ export const createInboxCredential = async (input: {
93
98
  ),
94
99
  issueKey: input.refresh.issueKey,
95
100
  requestDigest: input.refresh.requestDigest,
101
+ requestDigestCandidates: input.refresh.requestDigestCandidates,
96
102
  });
97
103
  id = created.id;
98
104
  } else {
@@ -32,12 +32,13 @@ export const createPendingInboxRefresh = async (params: {
32
32
  aggregate: Record<string, unknown>;
33
33
  issueKey: string;
34
34
  requestDigest: string;
35
+ requestDigestCandidates?: string[];
35
36
  }): Promise<InboxCredentialType> => {
36
37
  const result = await neogma.queryRunner.run(
37
38
  `MATCH (issuer:Profile {profileId: $issuerProfileId})
38
39
  MERGE (ic:InboxCredential {refreshIssueKey: $issueKey})
39
40
  ON CREATE SET ic += $inbox, ic.refreshRequestDigest = $requestDigest
40
- WITH issuer, ic WHERE ic.refreshRequestDigest = $requestDigest
41
+ WITH issuer, ic WHERE ic.refreshRequestDigest IN $requestDigestCandidates
41
42
  MERGE (refresh:CredentialRefresh {refreshId: ic.refreshId})
42
43
  ON CREATE SET refresh += $aggregate
43
44
  MERGE (issuer)-[:ISSUED_REFRESH]->(refresh)
@@ -46,7 +47,11 @@ export const createPendingInboxRefresh = async (params: {
46
47
  ON CREATE SET p.id = ic.refreshId, p.refreshId = ic.refreshId, p.version = 1,
47
48
  p.publishedAt = ic.createdAt, p.signingMode = 'signing-authority'
48
49
  RETURN ic`,
49
- { ...params, issuerProfileId: params.aggregate.issuerProfileId }
50
+ {
51
+ ...params,
52
+ requestDigestCandidates: params.requestDigestCandidates ?? [params.requestDigest],
53
+ issuerProfileId: params.aggregate.issuerProfileId,
54
+ }
50
55
  );
51
56
  const props = result.records[0]?.get('ic')?.properties;
52
57
  if (!props)
@@ -87,6 +92,7 @@ export const advancePendingInboxRefresh = async (params: {
87
92
  materialDigest: string;
88
93
  idempotencyKey?: string;
89
94
  updateSummary?: string;
95
+ updateReason?: import('@learncard/types').CredentialRefreshUpdateReason;
90
96
  }): Promise<PublishCredentialRefreshResult | null> => {
91
97
  const { refreshId, expectedVersion, idempotencyKey } = params;
92
98
  const replay = await getPendingInboxPublication(refreshId, idempotencyKey);
@@ -101,11 +107,13 @@ export const advancePendingInboxRefresh = async (params: {
101
107
  AND ic.currentStatus = 'PENDING' AND datetime(ic.expiresAt) > datetime()
102
108
  AND ($key IS NULL OR NOT EXISTS { MATCH (:InboxRefreshPublication {refreshIdempotencyKey: $key}) })
103
109
  CREATE (p:InboxRefreshPublication $publication)
104
- SET ic.credential = $credential, ic.credentialName = $credentialName,
110
+ SET ic.credential = $credential,
111
+ ic.credentialName = coalesce($credentialName, ic.credentialName),
105
112
  refresh.currentVersion = $version, refresh.materialDigest = $materialDigest,
106
113
  refresh.credentialDisplayName = coalesce($credentialName, refresh.credentialDisplayName),
107
114
  refresh.lastPublishedAt = $publishedAt, refresh.updatedAt = $publishedAt,
108
- refresh.updateSummary = $updateSummary
115
+ refresh.updateSummary = $updateSummary,
116
+ refresh.updateReason = $updateReason
109
117
  RETURN p`,
110
118
  {
111
119
  ...params,
@@ -113,6 +121,7 @@ export const advancePendingInboxRefresh = async (params: {
113
121
  publishedAt,
114
122
  credentialName: params.credentialName ?? null,
115
123
  updateSummary: params.updateSummary ?? null,
124
+ updateReason: params.updateReason ?? null,
116
125
  key: idempotencyKey ? `${refreshId}:${idempotencyKey}` : null,
117
126
  publication: Object.fromEntries(
118
127
  Object.entries({
@@ -123,6 +132,7 @@ export const advancePendingInboxRefresh = async (params: {
123
132
  publishedAt,
124
133
  signingMode: 'signing-authority',
125
134
  updateSummary: params.updateSummary,
135
+ updateReason: params.updateReason,
126
136
  refreshIdempotencyKey: idempotencyKey
127
137
  ? `${refreshId}:${idempotencyKey}`
128
138
  : undefined,