@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
@@ -27,7 +27,7 @@ import {
27
27
  isClaimLinkAlreadySetForBoost,
28
28
  getClaimLinkSAInfoForBoost,
29
29
  getClaimLinkGeneratorProfileId,
30
- useClaimLinkForBoost,
30
+ useClaimLinkForBoost as claimLinkForBoost,
31
31
  } from '@cache/claim-links';
32
32
 
33
33
  import { validateInboxClaimToken } from '@helpers/contact-method.helpers';
@@ -56,6 +56,7 @@ import { logCredentialClaimed, logCredentialFailed } from '@helpers/activity.hel
56
56
  import { handleConnectionPromptsForCredentialClaim } from '@helpers/connectionPrompt.helpers';
57
57
  import { decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
58
58
  import { getBitstringStatusListEntries } from '@learncard/helpers';
59
+ import { neogma } from '@instance';
59
60
  import {
60
61
  EXHAUSTED,
61
62
  exhaustExchangeChallengeForToken,
@@ -428,7 +429,7 @@ async function handlePresentationForClaim(
428
429
  ).credential as VC;
429
430
 
430
431
  // Mark the challenge as used
431
- await useClaimLinkForBoost(exchangeInfo.boostUri, exchangeInfo.challenge);
432
+ await claimLinkForBoost(exchangeInfo.boostUri, exchangeInfo.challenge);
432
433
 
433
434
  const vp = await issueResponsePresentationWithVcs([vc]);
434
435
 
@@ -716,16 +717,60 @@ async function handleInboxClaimPresentation(
716
717
  deliveryEncryptionFailed = true;
717
718
  throw error;
718
719
  }
719
- const finalized = await finalizeAndWipeInboxCredential(inboxCredential.id, {
720
- recipientDid: holderDid,
721
- credential: encryptedDelivery,
720
+ const issuerProfile = await getProfileByDid(inboxCredential.issuerDid);
721
+ if (!issuerProfile) throw new Error('Issuer profile not found');
722
+ const boost = inboxCredential.boostUri
723
+ ? await getBoostByUri(inboxCredential.boostUri)
724
+ : undefined;
725
+ // Reservation is part of delivery, not best-effort indexing. A conflict
726
+ // must leave escrow pending and must never return the reused credential.
727
+ const delivery = encryptedDelivery;
728
+ const statusEntries = getBitstringStatusListEntries(finalCredential);
729
+ await neogma.getTransaction(null, async tx => {
730
+ const finalized = await finalizeAndWipeInboxCredential(
731
+ inboxCredential.id,
732
+ {
733
+ recipientDid: holderDid,
734
+ credential: delivery,
735
+ },
736
+ {},
737
+ tx
738
+ );
739
+ if (!finalized) throw new Error('Inbox credential is no longer pending');
740
+ const credentialInstance =
741
+ statusEntries.length > 0 || (holderProfile && boost)
742
+ ? await storeCredential(
743
+ {
744
+ kind: 'issued-credential',
745
+ credential: delivery,
746
+ statusEntries,
747
+ },
748
+ tx,
749
+ issuerProfile.profileId
750
+ )
751
+ : undefined;
752
+ if (holderProfile) {
753
+ await createClaimedRelationship(
754
+ holderProfile.profileId,
755
+ inboxCredential.id,
756
+ claimToken,
757
+ tx
758
+ );
759
+ if (boost && credentialInstance) {
760
+ await createBoostInstanceOfRelationship(credentialInstance, boost, tx);
761
+ await createSentCredentialRelationship(
762
+ { type: 'profile', profile: issuerProfile },
763
+ holderProfile,
764
+ credentialInstance,
765
+ undefined,
766
+ undefined,
767
+ undefined,
768
+ tx
769
+ );
770
+ }
771
+ }
722
772
  });
723
- if (!finalized) throw new Error('Inbox credential is no longer pending');
724
- }
725
-
726
- // Record the claim only after finalization succeeds so failed compare-and-swap
727
- // attempts cannot leave a misleading audit relationship behind.
728
- if (holderProfile) {
773
+ } else if (holderProfile) {
729
774
  await createClaimedRelationship(
730
775
  holderProfile.profileId,
731
776
  inboxCredential.id,
@@ -733,38 +778,8 @@ async function handleInboxClaimPresentation(
733
778
  );
734
779
  }
735
780
 
736
- // Store credential and create boost relationship if this was a boost issuance
737
- const boostUri = inboxCredential.boostUri;
738
- if (!inboxCredential.refreshId && holderProfile && boostUri) {
739
- try {
740
- const boost = await getBoostByUri(boostUri);
741
- const issuerProfile = await getProfileByDid(inboxCredential.issuerDid);
742
-
743
- if (boost && issuerProfile) {
744
- // Store the credential in the database
745
- const credentialInstance = await storeCredential({
746
- kind: 'issued-credential',
747
- credential: encryptedDelivery!,
748
- statusEntries: getBitstringStatusListEntries(finalCredential),
749
- });
750
-
751
- // Create the boost instance relationship
752
- await createBoostInstanceOfRelationship(credentialInstance, boost);
753
-
754
- // Create the sent/received credential relationship
755
- await createSentCredentialRelationship(
756
- { type: 'profile', profile: issuerProfile },
757
- holderProfile,
758
- credentialInstance
759
- );
760
- }
761
- } catch {
762
- // Escrow is already wiped; preserve delivery even if indexing fails.
763
- console.error('Failed to index claimed inbox boost', inboxCredential.id);
764
- }
765
- }
766
-
767
781
  // Log CLAIMED activity - chain to original activityId/integrationId if available
782
+ const boostUri = inboxCredential.boostUri;
768
783
  // activityId and integrationId are stored on the inbox credential
769
784
  const activityId = inboxCredential.activityId;
770
785
  const integrationId = inboxCredential.integrationId;
@@ -805,11 +820,11 @@ async function handleInboxClaimPresentation(
805
820
  // Trigger webhook if configured
806
821
  if (inboxCredential.webhookUrl) {
807
822
  try {
808
- const learnCard = await getLearnCard();
823
+ const webhookLearnCard = await getLearnCard();
809
824
  await addNotificationToQueue({
810
825
  webhookUrl: inboxCredential.webhookUrl,
811
826
  type: LCNNotificationTypeEnumValidator.enum.ISSUANCE_CLAIMED,
812
- from: { did: learnCard.id.did() },
827
+ from: { did: webhookLearnCard.id.did() },
813
828
  to: { did: inboxCredential.issuerDid },
814
829
  message: getNotificationMessage(
815
830
  'issuanceClaimed',
@@ -871,7 +886,7 @@ async function handleInboxClaimPresentation(
871
886
  try {
872
887
  // Trigger webhook for error if configured
873
888
  if (inboxCredential.webhookUrl) {
874
- const learnCard = await getLearnCard();
889
+ const webhookLearnCard = await getLearnCard();
875
890
  const issuanceErrorMsg = getNotificationMessage(
876
891
  'issuanceError',
877
892
  // failedIssuerProfile is the issuer (the webhook recipient),
@@ -883,7 +898,7 @@ async function handleInboxClaimPresentation(
883
898
  await addNotificationToQueue({
884
899
  webhookUrl: inboxCredential.webhookUrl,
885
900
  type: LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR,
886
- from: { did: learnCard.id.did() },
901
+ from: { did: webhookLearnCard.id.did() },
887
902
  to: { did: inboxCredential.issuerDid },
888
903
  message: {
889
904
  title: issuanceErrorMsg.title,
@@ -1,10 +1,16 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import type { Notification } from '../delivery.service';
3
3
 
4
- const mocks = vi.hoisted(() => ({ send: vi.fn(), legacySend: vi.fn(), render: vi.fn() }));
4
+ const mocks = vi.hoisted(() => ({
5
+ send: vi.fn(),
6
+ legacySend: vi.fn(),
7
+ render: vi.fn(),
8
+ stream: vi.fn(),
9
+ }));
5
10
  vi.mock('@environment', () => ({ environment: {} }));
6
11
  vi.mock('postmark', () => ({
7
12
  ServerClient: class {
13
+ getMessageStream = mocks.stream;
8
14
  sendEmail = mocks.send;
9
15
  sendEmailWithTemplate = mocks.legacySend;
10
16
  },
@@ -25,6 +31,7 @@ beforeEach(() => {
25
31
  vi.clearAllMocks();
26
32
  mocks.render.mockResolvedValue({ html: '<p>Updated</p>', text: 'Updated', subject: 'Updated' });
27
33
  mocks.send.mockResolvedValue({});
34
+ mocks.stream.mockResolvedValue({ MessageStreamType: 'Broadcasts' });
28
35
  });
29
36
 
30
37
  describe('update email provider attempts', () => {
@@ -52,3 +59,65 @@ describe('update email provider attempts', () => {
52
59
  expect(mocks.legacySend).not.toHaveBeenCalled();
53
60
  });
54
61
  });
62
+
63
+ describe('campaign provider boundary', () => {
64
+ const campaignNotification: Notification = {
65
+ ...notification,
66
+ templateId: 'campaign-announcement',
67
+ subject: 'Authored subject',
68
+ messageStream: 'news',
69
+ headers: [
70
+ {
71
+ Name: 'List-Unsubscribe',
72
+ Value: '<https://brain.example/campaigns/unsubscribe?token=test>',
73
+ },
74
+ ],
75
+ };
76
+ it('accepts the Postmark Broadcasts stream type and forwards headers and subject', async () => {
77
+ await new PostmarkAdapter('test-key').send(campaignNotification);
78
+ expect(mocks.stream).toHaveBeenCalledWith('news');
79
+ expect(mocks.send).toHaveBeenCalledWith(
80
+ expect.objectContaining({
81
+ Subject: 'Authored subject',
82
+ MessageStream: 'news',
83
+ Headers: campaignNotification.headers,
84
+ })
85
+ );
86
+ });
87
+ it('validates one stream per wave and prepares without sending', async () => {
88
+ const adapter = new PostmarkAdapter('test-key');
89
+ await adapter.assertBroadcastStream('news');
90
+ const first = await adapter.prepareCampaignEmail(campaignNotification);
91
+ const second = await adapter.prepareCampaignEmail(campaignNotification);
92
+ expect(mocks.stream).toHaveBeenCalledOnce();
93
+ expect(mocks.render).toHaveBeenCalledTimes(2);
94
+ expect(mocks.send).not.toHaveBeenCalled();
95
+ await first();
96
+ await second();
97
+ expect(mocks.send).toHaveBeenCalledTimes(2);
98
+ });
99
+ it('does not cache failed stream checks', async () => {
100
+ const adapter = new PostmarkAdapter('test-key');
101
+ mocks.stream.mockRejectedValueOnce(new Error('provider private detail'));
102
+ await expect(adapter.assertBroadcastStream('news')).rejects.toThrow(
103
+ 'stream validation failed'
104
+ );
105
+ await adapter.assertBroadcastStream('news');
106
+ expect(mocks.stream).toHaveBeenCalledTimes(2);
107
+ });
108
+ it('rejects a custom transactional stream', async () => {
109
+ mocks.stream.mockResolvedValue({ MessageStreamType: 'Transactional' });
110
+ await expect(new PostmarkAdapter('test-key').send(campaignNotification)).rejects.toThrow(
111
+ 'Broadcast'
112
+ );
113
+ expect(mocks.send).not.toHaveBeenCalled();
114
+ });
115
+ it('never falls back to another send after an uncertain acknowledgement', async () => {
116
+ mocks.send.mockRejectedValueOnce(new Error('timeout'));
117
+ await expect(new PostmarkAdapter('test-key').send(campaignNotification)).rejects.toThrow(
118
+ 'timeout'
119
+ );
120
+ expect(mocks.send).toHaveBeenCalledOnce();
121
+ expect(mocks.legacySend).not.toHaveBeenCalled();
122
+ });
123
+ });
@@ -42,6 +42,7 @@ const LOCAL_TEMPLATE_MAP: Record<string, TemplateId> = {
42
42
  'guardian-email-otp': 'guardian-email-otp',
43
43
  'guardian-rejected-credential': 'guardian-rejected-credential',
44
44
  'credential-updated': 'credential-updated',
45
+ 'campaign-announcement': 'announcement',
45
46
  };
46
47
 
47
48
  export class PostmarkAdapter implements DeliveryService {
@@ -51,7 +52,65 @@ export class PostmarkAdapter implements DeliveryService {
51
52
  this.client = new ServerClient(apiKey);
52
53
  }
53
54
 
55
+ private readonly broadcastStreams = new Map<string, Promise<void>>();
56
+
57
+ /** Validate once per adapter/wave; failed checks are never cached. */
58
+ public async assertBroadcastStream(streamId?: string): Promise<void> {
59
+ if (!streamId || streamId.toLowerCase() === 'outbound')
60
+ throw new Error('Campaign email requires a broadcast stream');
61
+ let check = this.broadcastStreams.get(streamId);
62
+ if (!check) {
63
+ check = this.client.getMessageStream(streamId).then(stream => {
64
+ if (stream.MessageStreamType !== 'Broadcasts')
65
+ throw new Error('Campaign email requires a Postmark Broadcast stream');
66
+ });
67
+ this.broadcastStreams.set(streamId, check);
68
+ }
69
+ try {
70
+ await check;
71
+ } catch {
72
+ this.broadcastStreams.delete(streamId);
73
+ throw new Error('Campaign Postmark Broadcast stream validation failed');
74
+ }
75
+ }
76
+
77
+ /** Prepare without sending, so callers can render before reserving a recipient. */
78
+ public async prepareCampaignEmail(notification: Notification): Promise<() => Promise<void>> {
79
+ await this.assertBroadcastStream(notification.messageStream);
80
+ const branding = resolveBranding(notification.branding);
81
+ const rendered = await renderEmail(
82
+ 'announcement',
83
+ branding,
84
+ notification.templateModel as TemplateDataMap['announcement'],
85
+ notification.locale
86
+ );
87
+ const brandName =
88
+ notification.branding?.brandName || environment.POSTMARK_BRAND_NAME || 'LearnCard';
89
+ const from = notification.branding?.fromDomain
90
+ ? buildFromAddress(brandName, `support@${notification.branding.fromDomain}`)
91
+ : buildFromAddress(
92
+ brandName,
93
+ environment.POSTMARK_FROM_EMAIL || 'support@learningeconomy.io'
94
+ );
95
+ return async () => {
96
+ await this.client.sendEmail({
97
+ From: from,
98
+ To: notification.contactMethod.value,
99
+ Subject: notification.subject ?? rendered.subject,
100
+ HtmlBody: rendered.html,
101
+ TextBody: rendered.text,
102
+ MessageStream: notification.messageStream,
103
+ Headers: notification.headers,
104
+ });
105
+ };
106
+ }
107
+
54
108
  public async send(notification: Notification): Promise<void> {
109
+ if (notification.templateId === 'campaign-announcement') {
110
+ const deliver = await this.prepareCampaignEmail(notification);
111
+ await deliver();
112
+ return;
113
+ }
55
114
  const defaultFrom = environment.POSTMARK_FROM_EMAIL || 'support@learningeconomy.io';
56
115
  const defaultBrandName = environment.POSTMARK_BRAND_NAME || 'LearnCard';
57
116
 
@@ -82,12 +141,18 @@ export class PostmarkAdapter implements DeliveryService {
82
141
  notification.locale
83
142
  );
84
143
  } catch (renderError) {
85
- console.error(
86
- `[PostmarkAdapter] Local render failed for "${notification.templateId}":`,
87
- renderError
88
- );
144
+ if (notification.templateId !== 'campaign-announcement')
145
+ console.error(
146
+ `[PostmarkAdapter] Local render failed for "${notification.templateId}":`,
147
+ renderError
148
+ );
89
149
  // This template is owned locally; there is no legacy provider alias.
90
- if (notification.templateId === 'credential-updated') throw renderError;
150
+ if (
151
+ ['credential-updated', 'campaign-announcement'].includes(
152
+ notification.templateId
153
+ )
154
+ )
155
+ throw renderError;
91
156
  }
92
157
 
93
158
  if (rendered) {
@@ -95,21 +160,28 @@ export class PostmarkAdapter implements DeliveryService {
95
160
  await this.client.sendEmail({
96
161
  From: from,
97
162
  To: notification.contactMethod.value,
98
- Subject: rendered.subject,
163
+ Subject: notification.subject ?? rendered.subject,
99
164
  HtmlBody: rendered.html,
100
165
  TextBody: rendered.text,
101
166
  MessageStream: notification.messageStream,
167
+ Headers: notification.headers,
102
168
  });
103
169
 
104
170
  return;
105
171
  } catch (sendError) {
106
- console.error(
107
- `[PostmarkAdapter] sendEmail API failed for "${notification.templateId}":`,
108
- sendError
109
- );
172
+ if (notification.templateId !== 'campaign-announcement')
173
+ console.error(
174
+ `[PostmarkAdapter] sendEmail API failed for "${notification.templateId}":`,
175
+ sendError
176
+ );
110
177
  // A lost provider acknowledgement may already have sent the email.
111
178
  // Do not bypass the refresh delivery claim with a second attempt.
112
- if (notification.templateId === 'credential-updated') throw sendError;
179
+ if (
180
+ ['credential-updated', 'campaign-announcement'].includes(
181
+ notification.templateId
182
+ )
183
+ )
184
+ throw sendError;
113
185
  }
114
186
  }
115
187
  }
@@ -122,6 +194,7 @@ export class PostmarkAdapter implements DeliveryService {
122
194
  TemplateAlias: notification.templateId,
123
195
  TemplateModel: notification.templateModel,
124
196
  MessageStream: notification.messageStream,
197
+ Headers: notification.headers,
125
198
  });
126
199
  } catch (error) {
127
200
  const detail = error instanceof Error ? error.message : String(error);
@@ -12,6 +12,8 @@ export interface Notification {
12
12
  templateId: string; // e.g., 'universal-inbox-claim'
13
13
  templateModel: Record<string, any>; // Data for the template
14
14
  messageStream?: string;
15
+ subject?: string;
16
+ headers?: Array<{ Name: string; Value: string }>;
15
17
  /** Optional tenant branding. When present, emails are rendered locally via @learncard/email-templates. */
16
18
  branding?: Partial<TenantBranding>;
17
19
  /** Recipient locale (BCP-47), resolved via resolveRecipientLocale. Defaults to English at render time. */
@@ -26,6 +28,8 @@ export const NotificationSchema = z.object({
26
28
  templateId: z.string(),
27
29
  templateModel: z.record(z.string(), z.any()),
28
30
  messageStream: z.string().optional(),
31
+ subject: z.string().optional(),
32
+ headers: z.array(z.object({ Name: z.string(), Value: z.string() })).optional(),
29
33
  branding: z.record(z.string(), z.any()).optional(),
30
34
  locale: z.string().optional(),
31
35
  });
@@ -1,4 +1,5 @@
1
- import type { CampaignStatus } from '@learncard/types';
1
+ import { z } from 'zod';
2
+ import { CampaignIdValidator, type CampaignStatus } from '@learncard/types';
2
3
 
3
4
  export type CampaignRecord = {
4
5
  id: string;
@@ -15,3 +16,15 @@ export type CampaignRecord = {
15
16
  };
16
17
 
17
18
  export type CampaignAudienceQuery = { match: string; params: Record<string, unknown> };
19
+
20
+ export const CampaignDispatchJobValidator = z
21
+ .object({
22
+ campaignId: CampaignIdValidator,
23
+ campaignCreatedAt: z.iso.datetime(),
24
+ channel: z.enum(['notify', 'email']).default('notify'),
25
+ afterProfileId: z.string().optional(),
26
+ dispatchId: z.uuid(),
27
+ })
28
+ .strict();
29
+
30
+ export type CampaignDispatchJob = z.input<typeof CampaignDispatchJobValidator>;
@@ -5,6 +5,7 @@ import {
5
5
  ConsentFlowTransactionActionValidator,
6
6
  ConsentFlowGuardianApprovalValidator,
7
7
  ConsentFlowReferralValidator,
8
+ ConsentFlowContractDetailsValidator,
8
9
  } from '@learncard/types';
9
10
  import { z } from 'zod';
10
11
 
@@ -22,6 +23,27 @@ type FlatReferral = {
22
23
 
23
24
  type FlatConsentTerms = Partial<Record<`terms.${string}`, string | boolean | string[]>>;
24
25
 
26
+ export const DbContractVersionValidator = z.object({
27
+ id: z.string(),
28
+ version: z.coerce.number().int().positive(),
29
+ contract: ConsentFlowContractValidator,
30
+ name: z.string(),
31
+ subtitle: z.string().optional(),
32
+ description: z.string().optional(),
33
+ reasonForAccessing: z.string().optional(),
34
+ image: z.string().optional(),
35
+ needsGuardianConsent: z.boolean().optional(),
36
+ redirectUrl: z.string().optional(),
37
+ frontDoorBoostUri: z.string().optional(),
38
+ expiresAt: z.string().optional(),
39
+ autoboosts: z.array(z.unknown()).default([]),
40
+ writers: z.array(z.string()).default([]),
41
+ reason: z.string().optional(),
42
+ createdAt: z.string(),
43
+ kind: z.enum(['relaxed', 'expanded_optional', 'requires_consent']).optional(),
44
+ });
45
+ export type DbContractVersionType = z.infer<typeof DbContractVersionValidator>;
46
+
25
47
  export const DbContractValidator = z.object({
26
48
  id: z.string(),
27
49
  name: z.string(),
@@ -32,16 +54,31 @@ export const DbContractValidator = z.object({
32
54
  redirectUrl: z.string().optional(),
33
55
  frontDoorBoostUri: z.string().optional(),
34
56
  image: z.string().optional(),
35
- contract: ConsentFlowContractValidator,
57
+ // An empty request has no flattened contract.* properties in Neo4j.
58
+ contract: ConsentFlowContractValidator.prefault({}),
36
59
  createdAt: z.string(),
37
60
  updatedAt: z.string(),
38
61
  expiresAt: z.string().optional(),
39
62
  audienceVersion: z.coerce.number().int().nonnegative().optional(),
40
63
  hasConsented: z.boolean().optional(),
64
+ currentVersion: z.coerce.number().int().positive().optional(),
65
+ status: z.enum(['active', 'retired']).optional(),
66
+ version: z.coerce.number().int().positive().optional(),
67
+ latestUpdate: z.preprocess(value => {
68
+ if (typeof value !== 'string') return value;
69
+ try {
70
+ return JSON.parse(value);
71
+ } catch {
72
+ return value;
73
+ }
74
+ }, ConsentFlowContractDetailsValidator.shape.latestUpdate.optional()),
41
75
  });
42
76
 
43
77
  export type DbContractType = z.infer<typeof DbContractValidator>;
44
- export type FlatDbContractType = DbContractType & { contract: any };
78
+ export type FlatDbContractType = Omit<DbContractType, 'contract' | 'latestUpdate'> &
79
+ Partial<Record<`contract.${string}`, string | boolean>> & {
80
+ latestUpdate?: string;
81
+ };
45
82
 
46
83
  export const DbTransactionValidator = z.object({
47
84
  id: z.string(),
@@ -73,6 +110,10 @@ export const DbTermsValidator = z.object({
73
110
  mutationVersion: z.coerce.number().int().nonnegative().optional(),
74
111
  id: z.string(),
75
112
  status: ConsentFlowTermsStatusValidator,
113
+ consentedVersion: z.coerce.number().int().positive().optional(),
114
+ lastReviewedVersion: z.coerce.number().int().positive().optional(),
115
+ reconsentDeadline: z.string().optional(),
116
+ newOptionsUntil: z.string().optional(),
76
117
  // Neo4j has no properties to store for entirely empty permission objects.
77
118
  terms: ConsentFlowTermsValidator.prefault({}),
78
119
  guardianApproval: ConsentFlowGuardianApprovalValidator.optional(),
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
 
3
3
  import {
4
4
  CredentialRefreshSigningModeValidator,
5
+ CredentialRefreshUpdateReasonValidator,
5
6
  PublishCredentialRefreshNotificationValidator,
6
7
  } from '@learncard/types';
7
8
 
@@ -45,6 +46,12 @@ export const CredentialRefreshRecordValidator = z.object({
45
46
  rootMaterialDigest: z.string().optional(),
46
47
  /** Server-keyed fingerprint of the original credentialStatus descriptor */
47
48
  credentialStatusDigest: z.string().optional(),
49
+ /** Exact public status descriptor, including object/array shape. JSON null means absent. */
50
+ credentialStatusBinding: z.string().optional(),
51
+ /** Exact signed refresh-service descriptor, including additional services and array order. */
52
+ refreshServiceBinding: z.string().optional(),
53
+ encryptionMode: z.enum(['network', 'holder']).optional(),
54
+ statusEntries: z.string().optional(),
48
55
  /** Keyed fingerprint of version 1 subject identifiers; no extra subject DIDs persisted. */
49
56
  credentialSubjectDigest: z.string().optional(),
50
57
  /** Optional boost permanently associated with the initial credential bind. */
@@ -54,6 +61,7 @@ export const CredentialRefreshRecordValidator = z.object({
54
61
  idempotencyKey: z.string().optional(),
55
62
  /** Issuer-authored, privacy-safe update summary */
56
63
  updateSummary: z.string().optional(),
64
+ updateReason: CredentialRefreshUpdateReasonValidator.optional(),
57
65
  /**
58
66
  * Bounded, normalized credential display title used only for the email update
59
67
  * notice. Never rendered in in-app/push payloads. Sourced from the unsigned
@@ -108,6 +116,7 @@ export const CredentialRefreshVersionNodeValidator = z.object({
108
116
  etag: z.string().optional(),
109
117
  signingMode: CredentialRefreshSigningModeValidator,
110
118
  updateSummary: z.string().optional(),
119
+ updateReason: CredentialRefreshUpdateReasonValidator.optional(),
111
120
  /** Bounded credential display title for the email update notice only. */
112
121
  credentialDisplayName: z.string().optional(),
113
122
  /** Caller-provided key for replaying this exact historical publication */
@@ -148,7 +157,8 @@ export const AdvanceCredentialRefreshHeadParamsValidator = z.object({
148
157
  etag: z.string().optional(),
149
158
  materialDigest: z.string().optional(),
150
159
  signingMode: CredentialRefreshSigningModeValidator,
151
- updateSummary: z.string().optional(),
160
+ updateSummary: z.string().max(280).optional(),
161
+ updateReason: CredentialRefreshUpdateReasonValidator.optional(),
152
162
  /** Bounded credential display title persisted for the email update notice only. */
153
163
  credentialDisplayName: z.string().optional(),
154
164
  idempotencyKey: z.string().optional(),
@@ -12,5 +12,7 @@ export const CredentialValidator = z.object({
12
12
  id: z.string(),
13
13
  credential: z.string(),
14
14
  statusEntries: z.string().optional(),
15
+ statusOwnerProfileId: z.string().optional(),
16
+ statusReservationMode: z.enum(['strict', 'lenient']).optional(),
15
17
  });
16
18
  export type CredentialType = z.infer<typeof CredentialValidator>;