@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
@@ -7,6 +7,7 @@ import { environment } from '@environment';
7
7
  import {
8
8
  ConsentFlowTransactionValidator,
9
9
  ConsentFlowWebhookMetadataValidator,
10
+ ConsentFlowContractNotificationDataValidator,
10
11
  LCNNotificationTypeEnumValidator,
11
12
  type ConsentFlowTransaction,
12
13
  type ConsentFlowWebhookMetadata,
@@ -25,8 +26,10 @@ import { sanitizeProfileForTier, stripSensitiveProfileListFields } from './profi
25
26
  import { getStoredContractRequest } from '@accesslayer/consentflowcontract/read';
26
27
  import { resolveRecipientLocale } from './getRecipientLocale.helpers';
27
28
  import { ensureContractEventMaintenance } from './contract-event-maintenance.helpers';
29
+ import { updateDidForProfile } from './did.helpers';
30
+ import { buildContractUpdatePreview } from '@learncard/helpers';
28
31
 
29
- type EventKind = ConsentFlowWebhookMetadata['event'];
32
+ type EventKind = ConsentFlowWebhookMetadata['event'] | 'contract_updated';
30
33
  const defaultDomain = (): string => environment.DOMAIN_NAME ?? 'network.learncard.com';
31
34
 
32
35
  const MAX_DELIVERY_ATTEMPTS = 12;
@@ -62,7 +65,7 @@ export const appendConsentEvent = (
62
65
  const kind: EventKind =
63
66
  transaction.action === 'consent'
64
67
  ? 'consent_created'
65
- : transaction.action === 'update'
68
+ : transaction.action === 'update' || transaction.action === 'reconsent'
66
69
  ? 'consent_updated'
67
70
  : transaction.action === 'withdraw'
68
71
  ? 'consent_withdrawn'
@@ -210,7 +213,7 @@ export const authorizeContractNotification = async (
210
213
  if (!relationship || !parsedTerms.success) return false;
211
214
  const terms = parsedTerms.data;
212
215
  if (
213
- !(terms.status === 'live' || (terms.status === 'stale' && terms.oneTime)) ||
216
+ !(terms.status === 'live' || (terms.status === 'consumed' && terms.oneTime)) ||
214
217
  !expiryActive(terms.expiresAt) ||
215
218
  !expiryActive(relationship.contract.expiresAt)
216
219
  )
@@ -245,6 +248,35 @@ const buildNotification = async (
245
248
  getContractById(event.contractId),
246
249
  ]);
247
250
  if (!from || !to || !owner) return null;
251
+ if (event.kind === 'contract_updated') {
252
+ if (!contract) return null;
253
+ const data = ConsentFlowContractNotificationDataValidator.parse(
254
+ JSON.parse(event.payload ?? '')
255
+ );
256
+ const preview =
257
+ data.preview ??
258
+ buildContractUpdatePreview({
259
+ name: contract.name,
260
+ kind: data.kind,
261
+ diff: data.diff,
262
+ });
263
+ const sender = updateDidForProfile(event.domain, from);
264
+ return {
265
+ type: LCNNotificationTypeEnumValidator.enum.CONSENT_FLOW_CONTRACT_UPDATED,
266
+ from: {
267
+ ...stripSensitiveProfileListFields(
268
+ sanitizeProfileForTier(sender, 'unauthenticated')
269
+ ),
270
+ did: sender.did,
271
+ },
272
+ to: updateDidForProfile(event.domain, to),
273
+ sent: event.createdAt,
274
+ ...(data.kind === 'requires_consent'
275
+ ? { message: { title: preview.headline, body: data.reason ?? preview.subhead } }
276
+ : {}),
277
+ data,
278
+ };
279
+ }
248
280
  const eventKind =
249
281
  delivery.role === 'requester' && event.requestAccepted ? 'request_accepted' : event.kind;
250
282
  const metadata = ConsentFlowWebhookMetadataValidator.parse({
@@ -99,7 +99,7 @@ export const sendGenericContractRequest = async (
99
99
  } AND CASE WHEN contract.expiresAt IS NULL OR trim(contract.expiresAt) = '' THEN true ELSE datetime(contract.expiresAt) > datetime($now) END
100
100
  AND NOT EXISTS {
101
101
  MATCH (target)-[:CREATED_BY]->(terms:ConsentFlowTerms)-[:CONSENTS_TO]->(contract)
102
- WHERE (terms.status = 'live' OR (terms.status = 'stale' AND terms.oneTime = true)) AND
102
+ WHERE (terms.status = 'live' OR (terms.status = 'consumed' AND terms.oneTime = true)) AND
103
103
  CASE WHEN terms.expiresAt IS NULL OR trim(terms.expiresAt) = '' THEN true ELSE datetime(terms.expiresAt) > datetime($now) END
104
104
  }
105
105
  OPTIONAL MATCH (contract)-[existing:REQUESTED_FOR]->(target)
@@ -5,7 +5,11 @@ import {
5
5
  } from '@learncard/types';
6
6
  import cloneDeep from 'lodash/cloneDeep';
7
7
 
8
- export const areTermsValid = (terms: ConsentFlowTermsInput, contract: ConsentFlowContractInput) => {
8
+ export const areTermsValid = (
9
+ terms: ConsentFlowTermsInput,
10
+ contractSnapshot: ConsentFlowContractInput
11
+ ) => {
12
+ const contract = contractSnapshot;
9
13
  const contractPersonalReadKeys = Object.keys(contract.read?.personal ?? {});
10
14
  const contractPersonalWriteKeys = Object.keys(contract.write?.personal ?? {});
11
15
  const contractCategoryReadKeys = Object.keys(contract.read?.credentials?.categories ?? {});
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ CredentialDisplayValidator,
4
+ PublishCredentialRefreshInputValidator,
5
+ } from '@learncard/types';
6
+ import { getPublicBoostDisplayName } from './credential-display.helpers';
7
+ import { buildCredentialUpdatedEmailModel } from './credential-refresh-email-content.helpers';
8
+
9
+ describe('public credential display', () => {
10
+ it('never infers an anchor title from private claims or the generic list label', () => {
11
+ const name = getPublicBoostDisplayName({
12
+ name: 'Credential',
13
+ boost: JSON.stringify({ displayOnlyAnchor: true, display: {}, name: 'PRIVATE CLAIM' }),
14
+ });
15
+ expect(name).toBeUndefined();
16
+ expect(
17
+ JSON.stringify(buildCredentialUpdatedEmailModel({ credentialTitle: name }))
18
+ ).not.toContain('PRIVATE CLAIM');
19
+ });
20
+
21
+ it('uses only explicit public display fields', () => {
22
+ const display = CredentialDisplayValidator.parse({
23
+ name: 'Public label',
24
+ claims: 'PRIVATE',
25
+ });
26
+ expect(display).toEqual({ name: 'Public label' });
27
+ expect(
28
+ getPublicBoostDisplayName({
29
+ boost: JSON.stringify({ displayOnlyAnchor: true, display }),
30
+ })
31
+ ).toBe('Public label');
32
+ });
33
+
34
+ it('leaves reusable Boost metadata unchanged', () => {
35
+ expect(getPublicBoostDisplayName({ name: 'Public template', boost: '{}' })).toBe(
36
+ 'Public template'
37
+ );
38
+ });
39
+
40
+ it("does not mistake an ordinary Boost's own display object (e.g. app appearance fields) for a display-only anchor", () => {
41
+ // Real app-created BoostCredentials have their own top-level `display` object
42
+ // (backgroundImage, backgroundColor, displayType) — `'display' in boost` alone
43
+ // is not a safe anchor marker, unlike `displayOnlyAnchor`.
44
+ const name = getPublicBoostDisplayName({
45
+ name: 'Real Boost Name',
46
+ boost: JSON.stringify({
47
+ display: { backgroundColor: '#fff', backgroundImage: '', displayType: 'card' },
48
+ }),
49
+ });
50
+ expect(name).toBe('Real Boost Name');
51
+ });
52
+
53
+ it('bounds public summaries and rejects arbitrary update reasons', () => {
54
+ const input = {
55
+ refreshId: 'refresh',
56
+ mode: 'issuer-signed',
57
+ updateSummary: 'x'.repeat(281),
58
+ updateReason: 'PRIVATE',
59
+ };
60
+ const result = PublishCredentialRefreshInputValidator.safeParse(input);
61
+ expect(result.success).toBe(false);
62
+ if (!result.success) {
63
+ expect(result.error.issues.map(issue => issue.path[0])).toEqual(
64
+ expect.arrayContaining(['updateSummary', 'updateReason'])
65
+ );
66
+ }
67
+ });
68
+ });
@@ -0,0 +1,15 @@
1
+ import { CredentialDisplayValidator } from '@learncard/types';
2
+ import { isDisplayOnlyAnchorTemplate } from '@learncard/helpers';
3
+
4
+ /** Display-only anchors distinguish an omitted public name from the generic list label. */
5
+ export const getPublicBoostDisplayName = (
6
+ boost: { boost: string; name?: string } | null | undefined
7
+ ): string | undefined => {
8
+ if (!boost) return undefined;
9
+ const template: unknown = JSON.parse(boost.boost);
10
+ if (isDisplayOnlyAnchorTemplate(template)) {
11
+ return CredentialDisplayValidator.parse((template as { display?: unknown }).display).name;
12
+ }
13
+ // Explicit reusable Boost/CMS metadata retains its existing public contract.
14
+ return boost.name;
15
+ };
@@ -2,6 +2,7 @@ import { environment } from '@environment';
2
2
  import { generateClaimUrl } from './contact-method.helpers';
3
3
  import type { BoostInstance } from '@models';
4
4
  import { getBoostById } from '@accesslayer/boost/read';
5
+ import { getPublicBoostDisplayName } from './credential-display.helpers';
5
6
  import { getContactMethodsForProfile } from '@accesslayer/contact-method/read';
6
7
  import {
7
8
  claimCredentialRefreshEmailDelivery,
@@ -84,29 +85,21 @@ export const resolveCredentialRefreshEmailTarget = async (
84
85
  };
85
86
 
86
87
  /**
87
- * Resolves the bounded email-only title: the persisted publish-input title first,
88
- * then the aggregate's coalesced title, then the original boost template name.
89
- * The holder-encrypted payload is never decrypted to recover a title.
88
+ * Only explicit public Boost display metadata can supply an email title.
89
+ * Historical publication titles may contain claims, so they are never used.
90
90
  */
91
91
  export const resolveCredentialRefreshEmailDisplayTitle = async (params: {
92
92
  version: CredentialRefreshVersionNode;
93
93
  refreshId: string;
94
94
  }): Promise<string | undefined> => {
95
- const { version, refreshId } = params;
96
- const fromVersion = extractBoundedCredentialDisplayTitle(version.credentialDisplayName);
97
-
98
- if (fromVersion) return fromVersion;
99
-
95
+ const { refreshId } = params;
100
96
  const aggregate = await getCredentialRefresh(refreshId);
101
- const fromAggregate = extractBoundedCredentialDisplayTitle(aggregate?.credentialDisplayName);
102
-
103
- if (fromAggregate) return fromAggregate;
104
97
 
105
98
  if (!aggregate?.boostId) return undefined;
106
99
 
107
100
  const boost: BoostInstance | null = await getBoostById(aggregate.boostId);
108
101
 
109
- return extractBoundedCredentialDisplayTitle(boost?.name);
102
+ return extractBoundedCredentialDisplayTitle(getPublicBoostDisplayName(boost));
110
103
  };
111
104
 
112
105
  /**
@@ -1,11 +1,148 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import {
4
+ computeCredentialMaterialDigest,
5
+ matchesCredentialMaterialDigest,
6
+ matchesCredentialSubjectDigest,
7
+ getCredentialRefreshDigestCandidateSecrets,
8
+ decideCredentialRefreshNotification,
4
9
  computeCredentialSubjectDigest,
10
+ computeCredentialStatusDigest,
11
+ matchesCredentialStatusDigest,
5
12
  computeCredentialRefreshInitialDeliveryKey,
6
13
  computeCredentialRefreshRouteKey,
7
14
  } from './credential-refresh-materiality.helpers';
8
15
 
16
+ describe('versioned status fingerprints', () => {
17
+ afterEach(() => vi.unstubAllEnvs());
18
+
19
+ it('matches current-version digests without parsing previous keys', () => {
20
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v2');
21
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'current-test-key');
22
+ const status = { type: 'BitstringStatusListEntry', statusPurpose: 'revocation' };
23
+ const digest = computeCredentialStatusDigest(status);
24
+ // Startup validation is separate; current matches never consult old keys.
25
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', 'invalid-json');
26
+ expect(getCredentialRefreshDigestCandidateSecrets('v2')).toEqual(['current-test-key']);
27
+ expect(matchesCredentialStatusDigest(status, digest)).toBe(true);
28
+ expect(() => getCredentialRefreshDigestCandidateSecrets('v1')).toThrow();
29
+ });
30
+
31
+ it('retains shape, array order and fields, and accepts unversioned current-key digests', () => {
32
+ const status = { type: 'BitstringStatusListEntry', statusPurpose: 'revocation' };
33
+ const digest = computeCredentialStatusDigest(status);
34
+ expect(digest).toMatch(/^v1:/);
35
+ expect(matchesCredentialStatusDigest(status, digest)).toBe(true);
36
+ expect(matchesCredentialStatusDigest(status, digest.split(':')[1])).toBe(true);
37
+ expect(matchesCredentialStatusDigest([status], digest)).toBe(false);
38
+ expect(matchesCredentialStatusDigest({ ...status, extra: true }, digest)).toBe(false);
39
+ expect(
40
+ matchesCredentialStatusDigest([status, {}], computeCredentialStatusDigest([{}, status]))
41
+ ).toBe(false);
42
+ });
43
+
44
+ it('accepts only current or explicitly retained previous keys', () => {
45
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'old-test-key');
46
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v1');
47
+ const digest = computeCredentialStatusDigest({ type: 'example' });
48
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'new-test-key');
49
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v2');
50
+ vi.stubEnv(
51
+ 'CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS',
52
+ JSON.stringify({ v1: 'old-test-key' })
53
+ );
54
+ expect(matchesCredentialStatusDigest({ type: 'example' }, digest)).toBe(true);
55
+ expect(matchesCredentialStatusDigest({ type: 'example' }, digest.split(':')[1])).toBe(true);
56
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
57
+ expect(matchesCredentialStatusDigest({ type: 'example' }, digest)).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe('subject and material digest key rotation', () => {
62
+ afterEach(() => vi.unstubAllEnvs());
63
+
64
+ it.each([false, true])('matches retained old keys (version prefix: %s)', prefixed => {
65
+ const subject = { id: 'holder' };
66
+ const credential = { credentialSubject: subject, name: 'Original' };
67
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'old-test-key');
68
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v1');
69
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
70
+ const prefix = prefixed ? 'v1:' : '';
71
+ const subjectDigest = prefix + computeCredentialSubjectDigest(subject);
72
+ const materialDigest = prefix + computeCredentialMaterialDigest(credential);
73
+
74
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'new-test-key');
75
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v2');
76
+ vi.stubEnv(
77
+ 'CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS',
78
+ JSON.stringify({ v1: 'old-test-key' })
79
+ );
80
+ expect(getCredentialRefreshDigestCandidateSecrets()).toEqual([
81
+ 'new-test-key',
82
+ 'old-test-key',
83
+ ]);
84
+ expect(matchesCredentialSubjectDigest(subject, subjectDigest)).toBe(true);
85
+ expect(matchesCredentialMaterialDigest(credential, materialDigest)).toBe(true);
86
+ expect(matchesCredentialSubjectDigest({ id: 'other' }, subjectDigest)).toBe(false);
87
+ expect(
88
+ matchesCredentialMaterialDigest({ ...credential, name: 'Changed' }, materialDigest)
89
+ ).toBe(false);
90
+ expect(matchesCredentialSubjectDigest(subject, `unknown:${subjectDigest}`)).toBe(false);
91
+ expect(matchesCredentialMaterialDigest(credential, `unknown:${materialDigest}`)).toBe(
92
+ false
93
+ );
94
+ const nextDigest = computeCredentialMaterialDigest(credential);
95
+ expect(nextDigest).toBe(computeCredentialMaterialDigest(credential, 'new-test-key'));
96
+ expect(computeCredentialSubjectDigest(subject)).toBe(
97
+ computeCredentialSubjectDigest(subject, 'new-test-key')
98
+ );
99
+ expect(
100
+ decideCredentialRefreshNotification({
101
+ state: 'active',
102
+ previousDigest: matchesCredentialMaterialDigest(credential, materialDigest)
103
+ ? nextDigest
104
+ : materialDigest,
105
+ nextDigest,
106
+ })
107
+ ).toBe('suppressed');
108
+
109
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
110
+ expect(matchesCredentialSubjectDigest(subject, subjectDigest)).toBe(false);
111
+ expect(matchesCredentialMaterialDigest(credential, materialDigest)).toBe(false);
112
+ });
113
+
114
+ it('rejects missing digests and mismatched version prefixes', () => {
115
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'current-test-key');
116
+ vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v2');
117
+ vi.stubEnv(
118
+ 'CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS',
119
+ JSON.stringify({ v1: 'old-test-key' })
120
+ );
121
+ const subject = { id: 'holder' };
122
+ const credential = { credentialSubject: subject };
123
+ expect(matchesCredentialSubjectDigest(subject)).toBe(false);
124
+ expect(matchesCredentialMaterialDigest(credential, '')).toBe(false);
125
+ expect(
126
+ matchesCredentialSubjectDigest(subject, `v1:${computeCredentialSubjectDigest(subject)}`)
127
+ ).toBe(false);
128
+ expect(
129
+ matchesCredentialMaterialDigest(
130
+ credential,
131
+ `v1:${computeCredentialMaterialDigest(credential)}`
132
+ )
133
+ ).toBe(false);
134
+ expect(
135
+ matchesCredentialSubjectDigest(subject, `v2:${computeCredentialSubjectDigest(subject)}`)
136
+ ).toBe(true);
137
+ expect(
138
+ matchesCredentialMaterialDigest(
139
+ credential,
140
+ `v2:${computeCredentialMaterialDigest(credential)}`
141
+ )
142
+ ).toBe(true);
143
+ });
144
+ });
145
+
9
146
  describe('managed initial credential notification keys', () => {
10
147
  it('derives a stable opaque initial-delivery key from the refresh route key', () => {
11
148
  const secret = 'test-secret';
@@ -1,6 +1,9 @@
1
1
  import { createHmac } from 'crypto';
2
2
 
3
- import { getCredentialRefreshRuntimeEnvironment } from '@environment';
3
+ import {
4
+ getCredentialRefreshRuntimeEnvironment,
5
+ getCredentialRefreshPreviousDigestKeys,
6
+ } from '@environment';
4
7
  import { canonicalizeCredentialContent, canonicalizeCredentialJson } from '@learncard/helpers';
5
8
  import type { PublishCredentialRefreshNotification } from '@learncard/types';
6
9
 
@@ -37,6 +40,55 @@ export const getCredentialRefreshDigestSecret = (): string => {
37
40
  );
38
41
  };
39
42
 
43
+ /**
44
+ * Returns current then explicitly retained previous HMAC secrets for legacy digests.
45
+ * A version prefix restricts matching to that version's key, just as status digests do.
46
+ * Candidates are read-only compatibility keys; new writes use the current secret.
47
+ * Rotate SECRET and KEY_VERSION together, retaining the old secret under its old
48
+ * version in CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS until all aggregates republish.
49
+ * Original subject/status/root fingerprints survive republishing, so retain their
50
+ * keys while those aggregates still need publication, repair, or initial-send retries.
51
+ * Notification route/delivery keys also rotate, potentially producing one duplicate
52
+ * notification across the rotation boundary.
53
+ */
54
+ export const getCredentialRefreshDigestCandidateSecrets = (version?: string): string[] => {
55
+ const runtime = getCredentialRefreshRuntimeEnvironment();
56
+ const current = getCredentialRefreshDigestSecret();
57
+ if (version === runtime.CREDENTIAL_REFRESH_DIGEST_KEY_VERSION) return [current];
58
+ const previous = getCredentialRefreshPreviousDigestKeys();
59
+ if (version === undefined) return [current, ...Object.values(previous)];
60
+ return Object.prototype.hasOwnProperty.call(previous, version) ? [previous[version]!] : [];
61
+ };
62
+
63
+ const matchesStoredDigest = (
64
+ stored: string | undefined,
65
+ compute: (secret: string) => string
66
+ ): boolean => {
67
+ if (!stored) return false;
68
+ const separator = stored.indexOf(':');
69
+ const version = separator < 0 ? undefined : stored.slice(0, separator);
70
+ const digest = separator < 0 ? stored : stored.slice(separator + 1);
71
+ return getCredentialRefreshDigestCandidateSecrets(version).some(
72
+ secret => compute(secret) === digest
73
+ );
74
+ };
75
+
76
+ /** Matches persisted material fingerprints across explicitly retained key rotations. */
77
+ export const matchesCredentialMaterialDigest = (
78
+ credential: CredentialLike,
79
+ stored?: string
80
+ ): boolean =>
81
+ matchesStoredDigest(stored, secret => computeCredentialMaterialDigest(credential, secret));
82
+
83
+ /** Matches persisted subject identities across explicitly retained key rotations. */
84
+ export const matchesCredentialSubjectDigest = (
85
+ credentialSubject: unknown,
86
+ stored?: string
87
+ ): boolean =>
88
+ matchesStoredDigest(stored, secret =>
89
+ computeCredentialSubjectDigest(credentialSubject, secret)
90
+ );
91
+
40
92
  /**
41
93
  * Keys excluded from the user-visible projection:
42
94
  *
@@ -96,11 +148,20 @@ export const computeCredentialStatusDigest = (
96
148
  credentialStatus: unknown,
97
149
  secret: string = getCredentialRefreshDigestSecret()
98
150
  ): string =>
151
+ `${getCredentialRefreshRuntimeEnvironment().CREDENTIAL_REFRESH_DIGEST_KEY_VERSION}:${statusDigest(credentialStatus, secret)}`;
152
+
153
+ const statusDigest = (credentialStatus: unknown, secret: string): string =>
99
154
  createHmac('sha256', secret)
100
155
  .update('credential-status:v1\0')
101
156
  .update(canonicalizeCredentialJson(credentialStatus ?? null))
102
157
  .digest('base64url');
103
158
 
159
+ /** Legacy fingerprints accept the current key or explicitly retained previous keys. */
160
+ export const matchesCredentialStatusDigest = (
161
+ credentialStatus: unknown,
162
+ stored?: string
163
+ ): boolean => matchesStoredDigest(stored, secret => statusDigest(credentialStatus, secret));
164
+
104
165
  /** Keyed fingerprint matching the holder SDK's sorted subject identity comparison. */
105
166
  export const computeCredentialSubjectDigest = (
106
167
  credentialSubject: unknown,
@@ -82,7 +82,7 @@ describe('managed refresh proof verification', () => {
82
82
  {
83
83
  code: 'BAD_REQUEST',
84
84
  message: 'Credential proof could not be verified',
85
- cause,
85
+ cause: expect.objectContaining({ message: 'CREDENTIAL_PROOF_INVALID' }),
86
86
  }
87
87
  );
88
88
  expect(verifier.verifyCredential).toHaveBeenCalledTimes(retry ? 2 : 1);
@@ -28,11 +28,11 @@ export const verifyManagedRefreshProof = async (
28
28
  result = await verifier.verifyCredential(credential, { checks: ['proof'] });
29
29
  if (proofVerified(result)) return;
30
30
  }
31
- } catch (cause) {
31
+ } catch {
32
32
  throw new TRPCError({
33
33
  code: 'BAD_REQUEST',
34
34
  message: 'Credential proof could not be verified',
35
- cause,
35
+ cause: 'CREDENTIAL_PROOF_INVALID',
36
36
  });
37
37
  }
38
38