@learncard/network-brain-service 4.1.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/.env.example +14 -0
  2. package/CHANGELOG.md +164 -0
  3. package/README.md +27 -1
  4. package/bootstrapLambda.test.ts +70 -32
  5. package/config/config.dev.json +1 -0
  6. package/config/config.example.json +2 -1
  7. package/config/config.production.json +1 -0
  8. package/config/config.scouts.dev.json +1 -0
  9. package/config/config.scouts.production.json +1 -0
  10. package/consentFlowDeadlineLambda.test.ts +108 -0
  11. package/consentFlowDeadlineLambda.ts +18 -0
  12. package/consentFlowDeadlineLambdaApp.ts +14 -0
  13. package/dist/accesslayer/boost/create.d.ts +8 -2
  14. package/dist/accesslayer/boost/create.d.ts.map +1 -1
  15. package/dist/accesslayer/campaign/audience.d.ts +4 -4
  16. package/dist/accesslayer/campaign/audience.d.ts.map +1 -1
  17. package/dist/accesslayer/campaign/create.d.ts +1 -1
  18. package/dist/accesslayer/campaign/create.d.ts.map +1 -1
  19. package/dist/accesslayer/campaign/delivery.d.ts +5 -0
  20. package/dist/accesslayer/campaign/delivery.d.ts.map +1 -0
  21. package/dist/accesslayer/campaign/dispatch.d.ts +5 -0
  22. package/dist/accesslayer/campaign/dispatch.d.ts.map +1 -0
  23. package/dist/accesslayer/campaign/dispatch.test.d.ts +2 -0
  24. package/dist/accesslayer/campaign/dispatch.test.d.ts.map +1 -0
  25. package/dist/accesslayer/campaign/email.d.ts +10 -0
  26. package/dist/accesslayer/campaign/email.d.ts.map +1 -0
  27. package/dist/accesslayer/campaign/email.test.d.ts +2 -0
  28. package/dist/accesslayer/campaign/email.test.d.ts.map +1 -0
  29. package/dist/accesslayer/campaign/preferences.d.ts +5 -0
  30. package/dist/accesslayer/campaign/preferences.d.ts.map +1 -0
  31. package/dist/accesslayer/campaign/read.d.ts.map +1 -1
  32. package/dist/accesslayer/campaign/test.d.ts +5 -0
  33. package/dist/accesslayer/campaign/test.d.ts.map +1 -0
  34. package/dist/accesslayer/campaign/test.test.d.ts +2 -0
  35. package/dist/accesslayer/campaign/test.test.d.ts.map +1 -0
  36. package/dist/accesslayer/campaign/update.d.ts.map +1 -1
  37. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  38. package/dist/accesslayer/consentflowcontract/delete.d.ts +1 -1
  39. package/dist/accesslayer/consentflowcontract/delete.d.ts.map +1 -1
  40. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  41. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +2 -1
  42. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  43. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +19 -4
  44. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  45. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +6 -1
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  47. package/dist/accesslayer/consentflowcontract/versions.d.ts +68 -0
  48. package/dist/accesslayer/consentflowcontract/versions.d.ts.map +1 -0
  49. package/dist/accesslayer/credential/create.d.ts +3 -1
  50. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  51. package/dist/accesslayer/credential/delete.d.ts +1 -0
  52. package/dist/accesslayer/credential/delete.d.ts.map +1 -1
  53. package/dist/accesslayer/credential/relationships/create.d.ts +15 -3
  54. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  55. package/dist/accesslayer/credential/relationships/read.d.ts +2 -0
  56. package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
  57. package/dist/accesslayer/credential/relationships/update.d.ts.map +1 -1
  58. package/dist/accesslayer/credential-refresh/read.d.ts +1 -0
  59. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  60. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  61. package/dist/accesslayer/inbox-credential/create.d.ts +1 -0
  62. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  63. package/dist/accesslayer/inbox-credential/refresh.d.ts +7 -0
  64. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -1
  65. package/dist/app.d.ts +11 -1
  66. package/dist/app.d.ts.map +1 -1
  67. package/dist/cache/app-store.caches.d.ts +61 -1
  68. package/dist/cache/app-store.caches.d.ts.map +1 -1
  69. package/dist/campaign-unsubscribe.d.ts +7 -0
  70. package/dist/campaign-unsubscribe.d.ts.map +1 -0
  71. package/dist/campaign-unsubscribe.test.d.ts +2 -0
  72. package/dist/campaign-unsubscribe.test.d.ts.map +1 -0
  73. package/dist/config/environment.d.ts +42 -0
  74. package/dist/config/environment.d.ts.map +1 -1
  75. package/dist/config/stageConfig.d.ts +4 -0
  76. package/dist/config/stageConfig.d.ts.map +1 -1
  77. package/dist/credential-refresh.d.ts.map +1 -1
  78. package/dist/helpers/boost.helpers.d.ts +4 -1
  79. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  80. package/dist/helpers/campaign-app-origin.helpers.d.ts +2 -0
  81. package/dist/helpers/campaign-app-origin.helpers.d.ts.map +1 -0
  82. package/dist/helpers/campaign-email.helpers.d.ts +13 -0
  83. package/dist/helpers/campaign-email.helpers.d.ts.map +1 -0
  84. package/dist/helpers/campaign-notifications.helpers.d.ts +7 -0
  85. package/dist/helpers/campaign-notifications.helpers.d.ts.map +1 -0
  86. package/dist/helpers/campaign-queue.helpers.d.ts +7 -0
  87. package/dist/helpers/campaign-queue.helpers.d.ts.map +1 -0
  88. package/dist/helpers/campaign-queue.helpers.test.d.ts +2 -0
  89. package/dist/helpers/campaign-queue.helpers.test.d.ts.map +1 -0
  90. package/dist/helpers/campaign-unsubscribe-page.d.ts +2 -0
  91. package/dist/helpers/campaign-unsubscribe-page.d.ts.map +1 -0
  92. package/dist/helpers/campaign-unsubscribe.helpers.d.ts +7 -0
  93. package/dist/helpers/campaign-unsubscribe.helpers.d.ts.map +1 -0
  94. package/dist/helpers/campaign-webhook.helpers.test.d.ts +2 -0
  95. package/dist/helpers/campaign-webhook.helpers.test.d.ts.map +1 -0
  96. package/dist/helpers/consentflow-deadline.helpers.d.ts +5 -0
  97. package/dist/helpers/consentflow-deadline.helpers.d.ts.map +1 -0
  98. package/dist/helpers/consentflow-legacy-migration.helpers.d.ts +2 -0
  99. package/dist/helpers/consentflow-legacy-migration.helpers.d.ts.map +1 -0
  100. package/dist/helpers/contract-events.helpers.d.ts.map +1 -1
  101. package/dist/helpers/contract.helpers.d.ts +1 -1
  102. package/dist/helpers/contract.helpers.d.ts.map +1 -1
  103. package/dist/helpers/credential-display.helpers.d.ts +5 -0
  104. package/dist/helpers/credential-display.helpers.d.ts.map +1 -0
  105. package/dist/helpers/credential-display.helpers.test.d.ts +2 -0
  106. package/dist/helpers/credential-display.helpers.test.d.ts.map +1 -0
  107. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -1
  108. package/dist/helpers/credential-refresh-materiality.helpers.d.ts +4 -0
  109. package/dist/helpers/credential-refresh-materiality.helpers.d.ts.map +1 -1
  110. package/dist/helpers/credential-refresh.helpers.d.ts +30 -1
  111. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  112. package/dist/helpers/credential-status-mutation.helpers.d.ts +5 -0
  113. package/dist/helpers/credential-status-mutation.helpers.d.ts.map +1 -0
  114. package/dist/helpers/credential-status-mutation.helpers.test.d.ts +2 -0
  115. package/dist/helpers/credential-status-mutation.helpers.test.d.ts.map +1 -0
  116. package/dist/helpers/credential.helpers.d.ts +1 -1
  117. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  118. package/dist/helpers/inbox-refresh.helpers.d.ts +4 -2
  119. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -1
  120. package/dist/helpers/inbox.helpers.d.ts +1 -0
  121. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  122. package/dist/helpers/learnCard.helpers.d.ts +1 -0
  123. package/dist/helpers/learnCard.helpers.d.ts.map +1 -1
  124. package/dist/helpers/managed-status.helpers.d.ts +7 -0
  125. package/dist/helpers/managed-status.helpers.d.ts.map +1 -0
  126. package/dist/helpers/managed-status.helpers.test.d.ts +2 -0
  127. package/dist/helpers/managed-status.helpers.test.d.ts.map +1 -0
  128. package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts +2 -0
  129. package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts.map +1 -0
  130. package/dist/helpers/notification-webhook-fetch.helpers.d.ts +13 -0
  131. package/dist/helpers/notification-webhook-fetch.helpers.d.ts.map +1 -0
  132. package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts +2 -0
  133. package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts.map +1 -0
  134. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  135. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  136. package/dist/helpers/refresh-send-intent.helpers.d.ts +13 -5
  137. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -1
  138. package/dist/helpers/refresh-send-intent.helpers.test.d.ts +2 -0
  139. package/dist/helpers/refresh-send-intent.helpers.test.d.ts.map +1 -0
  140. package/dist/helpers/send-contract.helpers.d.ts +11 -0
  141. package/dist/helpers/send-contract.helpers.d.ts.map +1 -0
  142. package/dist/helpers/status-list.helpers.d.ts +12 -1
  143. package/dist/helpers/status-list.helpers.d.ts.map +1 -1
  144. package/dist/index.js +2 -2
  145. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  146. package/dist/models/ConsentFlowContractVersion.d.ts +12 -0
  147. package/dist/models/ConsentFlowContractVersion.d.ts.map +1 -0
  148. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  149. package/dist/models/Credential.d.ts +2 -0
  150. package/dist/models/Credential.d.ts.map +1 -1
  151. package/dist/models/CredentialRefresh.d.ts +5 -0
  152. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  153. package/dist/models/Integration.d.ts +1 -1
  154. package/dist/models/Profile.d.ts.map +1 -1
  155. package/dist/models/campaign-constraints.d.ts.map +1 -1
  156. package/dist/models/index.d.ts +1 -0
  157. package/dist/models/index.d.ts.map +1 -1
  158. package/dist/openapi.d.ts.map +1 -1
  159. package/dist/publish-metafile.json +1 -1
  160. package/dist/routes/activity.d.ts +15 -5
  161. package/dist/routes/activity.d.ts.map +1 -1
  162. package/dist/routes/app-store.d.ts +15 -5
  163. package/dist/routes/app-store.d.ts.map +1 -1
  164. package/dist/routes/auth-grants.d.ts +12 -2
  165. package/dist/routes/auth-grants.d.ts.map +1 -1
  166. package/dist/routes/bench.d.ts +11 -1
  167. package/dist/routes/bench.d.ts.map +1 -1
  168. package/dist/routes/boosts.d.ts +70 -15
  169. package/dist/routes/boosts.d.ts.map +1 -1
  170. package/dist/routes/campaigns.d.ts +183 -51
  171. package/dist/routes/campaigns.d.ts.map +1 -1
  172. package/dist/routes/claim-hooks.d.ts +11 -1
  173. package/dist/routes/claim-hooks.d.ts.map +1 -1
  174. package/dist/routes/contact-methods.d.ts +11 -1
  175. package/dist/routes/contact-methods.d.ts.map +1 -1
  176. package/dist/routes/contracts.d.ts +374 -8
  177. package/dist/routes/contracts.d.ts.map +1 -1
  178. package/dist/routes/credential-input-privacy.test.d.ts +2 -0
  179. package/dist/routes/credential-input-privacy.test.d.ts.map +1 -0
  180. package/dist/routes/credential-refreshes.d.ts +210 -1
  181. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  182. package/dist/routes/credential-share-previews.d.ts +11 -1
  183. package/dist/routes/credential-share-previews.d.ts.map +1 -1
  184. package/dist/routes/credentials.d.ts +132 -3
  185. package/dist/routes/credentials.d.ts.map +1 -1
  186. package/dist/routes/did-metadata.d.ts +11 -1
  187. package/dist/routes/did-metadata.d.ts.map +1 -1
  188. package/dist/routes/federation.d.ts +11 -1
  189. package/dist/routes/federation.d.ts.map +1 -1
  190. package/dist/routes/inbox.d.ts +54 -19
  191. package/dist/routes/inbox.d.ts.map +1 -1
  192. package/dist/routes/index.d.ts +100 -1
  193. package/dist/routes/index.d.ts.map +1 -1
  194. package/dist/routes/integrations.d.ts +14 -4
  195. package/dist/routes/integrations.d.ts.map +1 -1
  196. package/dist/routes/presentations.d.ts +11 -1
  197. package/dist/routes/presentations.d.ts.map +1 -1
  198. package/dist/routes/profile-manager.d.ts +11 -1
  199. package/dist/routes/profile-manager.d.ts.map +1 -1
  200. package/dist/routes/profiles.d.ts +11 -1
  201. package/dist/routes/profiles.d.ts.map +1 -1
  202. package/dist/routes/public-share-links.d.ts +22 -2
  203. package/dist/routes/public-share-links.d.ts.map +1 -1
  204. package/dist/routes/share-links.d.ts +36 -16
  205. package/dist/routes/share-links.d.ts.map +1 -1
  206. package/dist/routes/skill-frameworks.d.ts +11 -1
  207. package/dist/routes/skill-frameworks.d.ts.map +1 -1
  208. package/dist/routes/skills.d.ts +11 -1
  209. package/dist/routes/skills.d.ts.map +1 -1
  210. package/dist/routes/storage.d.ts +37 -23
  211. package/dist/routes/storage.d.ts.map +1 -1
  212. package/dist/routes/test.d.ts +79 -3
  213. package/dist/routes/test.d.ts.map +1 -1
  214. package/dist/routes/utilities.d.ts +11 -1
  215. package/dist/routes/utilities.d.ts.map +1 -1
  216. package/dist/routes/workflows.d.ts +11 -1
  217. package/dist/routes/workflows.d.ts.map +1 -1
  218. package/dist/services/delivery/adapters/postmark.adapter.d.ts +3 -0
  219. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  220. package/dist/services/delivery/delivery.service.d.ts +10 -0
  221. package/dist/services/delivery/delivery.service.d.ts.map +1 -1
  222. package/dist/types/activity.d.ts +3 -3
  223. package/dist/types/app-manifest.d.ts +10 -10
  224. package/dist/types/campaign.d.ts +13 -1
  225. package/dist/types/campaign.d.ts.map +1 -1
  226. package/dist/types/consentflowcontract.d.ts +175 -7
  227. package/dist/types/consentflowcontract.d.ts.map +1 -1
  228. package/dist/types/credential-refresh.d.ts +27 -0
  229. package/dist/types/credential-refresh.d.ts.map +1 -1
  230. package/dist/types/credential.d.ts +5 -0
  231. package/dist/types/credential.d.ts.map +1 -1
  232. package/dist/types/integration.d.ts +3 -3
  233. package/lambda.ts +44 -14
  234. package/lambdaApp.ts +20 -0
  235. package/package.json +20 -19
  236. package/project.json +5 -0
  237. package/scripts/migrate-consent-flow-versions.ts +9 -0
  238. package/scripts/report-send-template-exposure.ts +47 -0
  239. package/scripts/verify-python-contracts.ts +16 -1
  240. package/serverless-local.yml +15 -2
  241. package/serverless.function-env.cjs +4 -0
  242. package/serverless.local.yml +12 -0
  243. package/serverless.yml +48 -3
  244. package/src/accesslayer/boost/create.ts +62 -7
  245. package/src/accesslayer/campaign/README.md +275 -6
  246. package/src/accesslayer/campaign/audience.ts +39 -7
  247. package/src/accesslayer/campaign/create.ts +4 -2
  248. package/src/accesslayer/campaign/delivery.ts +66 -0
  249. package/src/accesslayer/campaign/dispatch.test.ts +59 -0
  250. package/src/accesslayer/campaign/dispatch.ts +172 -0
  251. package/src/accesslayer/campaign/email.test.ts +73 -0
  252. package/src/accesslayer/campaign/email.ts +91 -0
  253. package/src/accesslayer/campaign/preferences.ts +53 -0
  254. package/src/accesslayer/campaign/read.ts +3 -1
  255. package/src/accesslayer/campaign/state.ts +1 -1
  256. package/src/accesslayer/campaign/test.test.ts +31 -0
  257. package/src/accesslayer/campaign/test.ts +75 -0
  258. package/src/accesslayer/campaign/update.ts +15 -4
  259. package/src/accesslayer/consentflowcontract/create.ts +20 -1
  260. package/src/accesslayer/consentflowcontract/delete.ts +121 -3
  261. package/src/accesslayer/consentflowcontract/read.ts +15 -9
  262. package/src/accesslayer/consentflowcontract/relationships/create.ts +70 -36
  263. package/src/accesslayer/consentflowcontract/relationships/read.ts +305 -73
  264. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +1 -1
  265. package/src/accesslayer/consentflowcontract/relationships/update.ts +269 -98
  266. package/src/accesslayer/consentflowcontract/versions.ts +790 -0
  267. package/src/accesslayer/credential/create.ts +33 -9
  268. package/src/accesslayer/credential/delete.ts +45 -0
  269. package/src/accesslayer/credential/relationships/create.ts +102 -47
  270. package/src/accesslayer/credential/relationships/update.ts +8 -116
  271. package/src/accesslayer/credential-refresh/read.ts +26 -2
  272. package/src/accesslayer/credential-refresh/update.ts +6 -2
  273. package/src/accesslayer/inbox-credential/create.ts +7 -1
  274. package/src/accesslayer/inbox-credential/refresh.ts +14 -4
  275. package/src/campaign-unsubscribe.test.ts +158 -0
  276. package/src/campaign-unsubscribe.ts +86 -0
  277. package/src/config/environment.test.ts +52 -0
  278. package/src/config/environment.ts +58 -0
  279. package/src/config/serverlessFunctionEnv.test.ts +59 -0
  280. package/src/credential-refresh.ts +1 -0
  281. package/src/docker-entry.ts +26 -1
  282. package/src/helpers/boost-storage.helpers.test.ts +107 -41
  283. package/src/helpers/boost.helpers.ts +88 -40
  284. package/src/helpers/campaign-app-origin.helpers.ts +25 -0
  285. package/src/helpers/campaign-email.helpers.ts +113 -0
  286. package/src/helpers/campaign-notifications.helpers.ts +127 -0
  287. package/src/helpers/campaign-queue.helpers.test.ts +442 -0
  288. package/src/helpers/campaign-queue.helpers.ts +122 -0
  289. package/src/helpers/campaign-unsubscribe-page.ts +107 -0
  290. package/src/helpers/campaign-unsubscribe.helpers.ts +56 -0
  291. package/src/helpers/campaign-webhook.helpers.test.ts +137 -0
  292. package/src/helpers/consentflow-deadline.helpers.ts +22 -0
  293. package/src/helpers/consentflow-legacy-migration.helpers.ts +179 -0
  294. package/src/helpers/contract-events.helpers.ts +35 -3
  295. package/src/helpers/contract-requests.helpers.ts +1 -1
  296. package/src/helpers/contract.helpers.ts +5 -1
  297. package/src/helpers/credential-display.helpers.test.ts +68 -0
  298. package/src/helpers/credential-display.helpers.ts +15 -0
  299. package/src/helpers/credential-refresh-email.helpers.ts +5 -12
  300. package/src/helpers/credential-refresh-materiality.helpers.test.ts +138 -1
  301. package/src/helpers/credential-refresh-materiality.helpers.ts +62 -1
  302. package/src/helpers/credential-refresh-proof.helpers.test.ts +1 -1
  303. package/src/helpers/credential-refresh-proof.helpers.ts +2 -2
  304. package/src/helpers/credential-refresh.helpers.ts +1166 -376
  305. package/src/helpers/credential-status-mutation.helpers.test.ts +155 -0
  306. package/src/helpers/credential-status-mutation.helpers.ts +166 -0
  307. package/src/helpers/credential.helpers.ts +19 -11
  308. package/src/helpers/finalize-inbox.helpers.ts +1 -1
  309. package/src/helpers/inbox-refresh.helpers.ts +34 -8
  310. package/src/helpers/inbox.helpers.ts +2 -0
  311. package/src/helpers/learnCard.helpers.ts +23 -0
  312. package/src/helpers/managed-status.helpers.test.ts +101 -0
  313. package/src/helpers/managed-status.helpers.ts +147 -0
  314. package/src/helpers/notification-delivery-boundary.helpers.test.ts +114 -0
  315. package/src/helpers/notification-webhook-fetch.helpers.test.ts +228 -0
  316. package/src/helpers/notification-webhook-fetch.helpers.ts +166 -0
  317. package/src/helpers/notificationQueue.helpers.ts +5 -0
  318. package/src/helpers/notifications.helpers.ts +19 -12
  319. package/src/helpers/refresh-send-intent.helpers.test.ts +60 -0
  320. package/src/helpers/refresh-send-intent.helpers.ts +81 -18
  321. package/src/helpers/send-contract.helpers.ts +36 -0
  322. package/src/helpers/status-list.helpers.test.ts +29 -12
  323. package/src/helpers/status-list.helpers.ts +329 -20
  324. package/src/models/ConsentFlowContract.ts +3 -0
  325. package/src/models/ConsentFlowContractVersion.ts +50 -0
  326. package/src/models/ConsentFlowTerms.ts +4 -0
  327. package/src/models/Credential.ts +2 -0
  328. package/src/models/CredentialRefresh.ts +2 -0
  329. package/src/models/Profile.ts +2 -0
  330. package/src/models/campaign-constraints.ts +13 -8
  331. package/src/models/index.ts +2 -0
  332. package/src/openapi.ts +21 -0
  333. package/src/routes/app-store.ts +2 -1
  334. package/src/routes/boosts.ts +219 -137
  335. package/src/routes/campaigns.ts +98 -19
  336. package/src/routes/contracts.ts +433 -115
  337. package/src/routes/credential-input-privacy.test.ts +92 -0
  338. package/src/routes/credential-refreshes.ts +273 -2
  339. package/src/routes/federation.ts +7 -2
  340. package/src/routes/inbox.ts +6 -1
  341. package/src/routes/index.ts +31 -2
  342. package/src/routes/workflows.ts +61 -46
  343. package/src/services/delivery/adapters/postmark.adapter.test.ts +70 -1
  344. package/src/services/delivery/adapters/postmark.adapter.ts +84 -11
  345. package/src/services/delivery/delivery.service.ts +4 -0
  346. package/src/types/campaign.ts +14 -1
  347. package/src/types/consentflowcontract.ts +43 -2
  348. package/src/types/credential-refresh.ts +11 -1
  349. package/src/types/credential.ts +2 -0
  350. package/test/boost-status-cleanup.spec.ts +123 -0
  351. package/test/campaigns-email.unit.spec.ts +268 -0
  352. package/test/campaigns.spec.ts +804 -30
  353. package/test/consent-autoboost-idempotency.spec.ts +432 -0
  354. package/test/consent-data-boundaries.spec.ts +322 -26
  355. package/test/consent-legacy-migration.spec.ts +315 -0
  356. package/test/consent-recipients.spec.ts +7 -7
  357. package/test/consent-version-updates.spec.ts +761 -0
  358. package/test/consentflow.spec.ts +698 -226
  359. package/test/contract-requests.spec.ts +55 -9
  360. package/test/credential-refresh-allocation.spec.ts +212 -14
  361. package/test/credential-refresh-email.spec.ts +6 -10
  362. package/test/credential-refresh-endpoint.spec.ts +3 -0
  363. package/test/credential-refresh-publication.spec.ts +52 -15
  364. package/test/credential-storage.neo4j.spec.ts +33 -6
  365. package/test/helpers/legacy-autoboost.ts +48 -0
  366. package/test/holder-mode-send.spec.ts +710 -0
  367. package/test/inbox-refresh.spec.ts +105 -0
  368. package/test/inbox-security.e2e.ts +18 -22
  369. package/test/managed-status-revocation.spec.ts +970 -0
  370. package/test/notifications-worker.spec.ts +31 -25
  371. package/test/refresh-send.spec.ts +334 -5
  372. package/test/smart-resume-consent.spec.ts +1 -1
  373. package/test/verification/campaigns.ts +40 -0
  374. package/vitest.campaigns.config.ts +2 -0
  375. package/vitest.config.ts +14 -0
@@ -0,0 +1,92 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { resolveTenantFromRequest } from '@learncard/email-templates';
3
+ import type {
4
+ PrepareRefreshableSendInput,
5
+ SendBoostInput,
6
+ PublishCredentialRefreshInput,
7
+ } from '@learncard/types';
8
+
9
+ const transport = vi.hoisted(() => ({ send: vi.fn(), lookup: vi.fn() }));
10
+ vi.mock('@sentry/serverless', () => ({
11
+ Handlers: {
12
+ trpcMiddleware:
13
+ ({ attachRpcInput }: { attachRpcInput: boolean }) =>
14
+ async (opts: { next: () => Promise<unknown>; getRawInput: () => Promise<unknown> }) => {
15
+ const input = attachRpcInput ? await opts.getRawInput() : undefined;
16
+ try {
17
+ return await opts.next();
18
+ } finally {
19
+ transport.send({ input });
20
+ }
21
+ },
22
+ },
23
+ configureScope: vi.fn(),
24
+ setUser: vi.fn(),
25
+ }));
26
+ vi.mock('@accesslayer/profile/read', () => ({
27
+ getProfileByDid: transport.lookup,
28
+ getProfileByProfileId: transport.lookup,
29
+ }));
30
+ vi.mock('../models/credential-refresh-constraints', () => ({
31
+ ensureCredentialRefreshConstraints: vi.fn(),
32
+ }));
33
+
34
+ import { boostsRouter } from './boosts';
35
+ import { credentialRefreshesRouter } from './credential-refreshes';
36
+ import type { Context } from './index';
37
+
38
+ describe('credential route input telemetry', () => {
39
+ beforeEach(() => {
40
+ process.env.CREDENTIAL_REFRESH_ENABLED = 'true';
41
+ transport.send.mockClear();
42
+ transport.lookup.mockResolvedValue({ profileId: 'issuer', did: 'did:key:issuer' });
43
+ });
44
+
45
+ it.each(['prepare', 'send', 'publish'] as const)(
46
+ '%s never captures malformed, unauthenticated or failing request input',
47
+ async route => {
48
+ const privateInput = {
49
+ recipient: 42,
50
+ signedCredential: 'PRIVATE CLAIM',
51
+ templateData: { private: 'PRIVATE CLAIM' },
52
+ };
53
+ const invoke = (ctx: Context): Promise<unknown> => {
54
+ if (route === 'prepare')
55
+ return boostsRouter
56
+ .createCaller(ctx)
57
+ .prepareRefreshableSend(
58
+ privateInput as unknown as PrepareRefreshableSendInput
59
+ );
60
+ if (route === 'send')
61
+ return boostsRouter
62
+ .createCaller(ctx)
63
+ .send(privateInput as unknown as SendBoostInput);
64
+ return credentialRefreshesRouter
65
+ .createCaller(ctx)
66
+ .publishCredentialRefresh(
67
+ privateInput as unknown as PublishCredentialRefreshInput
68
+ );
69
+ };
70
+ const authenticated: Context = {
71
+ domain: 'example.com',
72
+ tenant: resolveTenantFromRequest({}),
73
+ user: { did: 'did:key:issuer', isChallengeValid: true, scope: '*:*' },
74
+ };
75
+ await expect(invoke(authenticated)).rejects.toMatchObject({ code: 'BAD_REQUEST' });
76
+ await expect(
77
+ invoke({ domain: 'example.com', tenant: resolveTenantFromRequest({}) })
78
+ ).rejects.toMatchObject({
79
+ code: 'UNAUTHORIZED',
80
+ });
81
+ transport.lookup.mockRejectedValueOnce(new Error('LOOKUP_FAILED'));
82
+ await expect(invoke(authenticated)).rejects.toThrow('LOOKUP_FAILED');
83
+ expect(transport.send).toHaveBeenCalledTimes(3);
84
+ expect(transport.send.mock.calls).toEqual([
85
+ [{ input: undefined }],
86
+ [{ input: undefined }],
87
+ [{ input: undefined }],
88
+ ]);
89
+ expect(JSON.stringify(transport.send.mock.calls)).not.toContain('PRIVATE CLAIM');
90
+ }
91
+ );
92
+ });
@@ -2,32 +2,50 @@ import { TRPCError } from '@trpc/server';
2
2
  import { z } from 'zod';
3
3
  import {
4
4
  AllocateCredentialRefreshInputValidator,
5
+ AttachCredentialRefreshStatusEntriesInputValidator,
5
6
  AllocateCredentialRefreshResultValidator,
7
+ EncryptionRecipientsResultValidator,
6
8
  GetCredentialRefreshHistoryInputValidator,
7
9
  GetCredentialRefreshHistoryResultValidator,
10
+ GetEncryptionRecipientsInputValidator,
8
11
  PublishCredentialRefreshInputValidator,
9
12
  PublishCredentialRefreshResultValidator,
13
+ PublishHolderEncryptedRefreshInputValidator,
14
+ SendHolderEncryptedRefreshableInputValidator,
10
15
  VCValidator,
16
+ GetCredentialRefreshReceiptInputValidator,
17
+ CredentialRefreshReceiptValidator,
11
18
  } from '@learncard/types';
12
19
  import type { PublishCredentialRefreshInput } from '@learncard/types';
13
20
 
14
- import { t, profileRoute } from '@routes';
21
+ import { t, credentialProfileRoute } from '@routes';
15
22
  import { getCredentialRefreshRuntimeEnvironment } from '@environment';
16
23
  import { getProfileByDid, getProfileByProfileId } from '@accesslayer/profile/read';
17
24
  import {
18
25
  getCredentialRefresh,
19
26
  getCredentialRefreshVersions,
27
+ getCredentialRefreshReceiptBinding,
20
28
  } from '@accesslayer/credential-refresh';
21
29
  import {
22
30
  allocateCredentialRefresh,
31
+ getCurrentEncryptionRecipients,
23
32
  publishCredentialRefresh,
33
+ publishHolderEncryptedRefreshableCredential,
34
+ sendHolderEncryptedRefreshableCredential,
24
35
  sendRefreshableCredential,
25
36
  } from '@helpers/credential-refresh.helpers';
26
37
  import { getDidWeb } from '@helpers/did.helpers';
27
38
  import { isRelationshipBlocked } from '@helpers/connection.helpers';
28
39
  import { ensureCredentialRefreshConstraints } from '../models/credential-refresh-constraints';
40
+ import { getBitstringStatusListEntries } from '@learncard/helpers';
41
+ import { neogma } from '@instance';
42
+ import { matchesCredentialStatusDigest } from '@helpers/credential-refresh-materiality.helpers';
43
+ import { bindRefreshStatusEntries } from '@helpers/managed-status.helpers';
44
+ import { getStatusManagement } from '@helpers/status-list.helpers';
45
+ import { constructUri, getUriParts } from '@helpers/uri.helpers';
46
+ import { getCredentialRefreshServiceUrl } from '@helpers/credential-refresh.helpers';
29
47
 
30
- const credentialRefreshRoute = profileRoute.use(async ({ ctx, next }) => {
48
+ const credentialRefreshRoute = credentialProfileRoute.use(async ({ ctx, next }) => {
31
49
  if (!getCredentialRefreshRuntimeEnvironment().CREDENTIAL_REFRESH_ENABLED) {
32
50
  throw new TRPCError({
33
51
  code: 'NOT_FOUND',
@@ -41,6 +59,149 @@ const credentialRefreshRoute = profileRoute.use(async ({ ctx, next }) => {
41
59
  });
42
60
 
43
61
  export const credentialRefreshesRouter = t.router({
62
+ getReceipt: credentialRefreshRoute
63
+ .meta({ requiredScope: 'credentials:read' })
64
+ .input(GetCredentialRefreshReceiptInputValidator)
65
+ .output(CredentialRefreshReceiptValidator.nullable())
66
+ .query(async ({ ctx, input }) => {
67
+ const { id, type } = getUriParts(input.uri);
68
+ if (type !== 'credential' || input.uri !== constructUri('credential', id, ctx.domain)) {
69
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid delivery URI' });
70
+ }
71
+ const refresh = await getCredentialRefreshReceiptBinding(
72
+ id,
73
+ ctx.user.profile.profileId
74
+ );
75
+ if (refresh === null) return null;
76
+ if (
77
+ !refresh ||
78
+ !refresh.holderDid ||
79
+ refresh.holderProfileId !== ctx.user.profile.profileId ||
80
+ !['awaiting_claim', 'active'].includes(refresh.state)
81
+ ) {
82
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Delivery not found' });
83
+ }
84
+ // Coordinates alone cannot reconstruct the exact signed descriptor of legacy sends.
85
+ let binding = refresh.credentialStatusBinding ?? refresh.inboxCredentialStatus;
86
+ if (binding === undefined) {
87
+ binding = await neogma.getTransaction(null, async tx => {
88
+ const result = await tx.run(
89
+ `MATCH (refresh:CredentialRefresh {refreshId: $refreshId})
90
+ SET refresh._statusBindingLock = true
91
+ REMOVE refresh._statusBindingLock
92
+ RETURN refresh.credentialStatusBinding AS binding,
93
+ refresh.inboxCredentialStatus AS inboxBinding,
94
+ refresh.credentialStatusDigest AS digest`,
95
+ { refreshId: refresh.refreshId }
96
+ );
97
+ const row = result.records[0];
98
+ const existing: unknown = row?.get('binding') ?? row?.get('inboxBinding');
99
+ if (typeof existing === 'string') return existing;
100
+ const digest: unknown = row?.get('digest');
101
+ if (
102
+ typeof digest !== 'string' ||
103
+ !matchesCredentialStatusDigest(null, digest)
104
+ ) {
105
+ return undefined;
106
+ }
107
+ // The JSON string "null" authenticates absence; an unset property
108
+ // means an unknown legacy descriptor. Never store Neo4j null.
109
+ await tx.run(
110
+ `MATCH (refresh:CredentialRefresh {refreshId: $refreshId})
111
+ SET refresh.credentialStatusBinding = 'null'`,
112
+ { refreshId: refresh.refreshId }
113
+ );
114
+ return 'null';
115
+ });
116
+ }
117
+ if (binding === undefined) {
118
+ throw new TRPCError({
119
+ code: 'PRECONDITION_FAILED',
120
+ message: 'Delivery binding unavailable',
121
+ });
122
+ }
123
+ const status: unknown = JSON.parse(binding);
124
+ return CredentialRefreshReceiptValidator.parse({
125
+ refreshId: refresh.refreshId,
126
+ credentialId: refresh.credentialId,
127
+ issuerDid: refresh.issuerDid,
128
+ holderDid: refresh.holderDid,
129
+ ...(refresh.refreshServiceBinding
130
+ ? {}
131
+ : { refreshServiceBinding: 'managed-entry' }),
132
+ refreshService: refresh.refreshServiceBinding
133
+ ? JSON.parse(refresh.refreshServiceBinding)
134
+ : {
135
+ id: getCredentialRefreshServiceUrl(refresh.refreshId, ctx.domain),
136
+ type: 'LearnCardCredentialRefresh2026',
137
+ authorization: { type: 'LearnCardDIDAuth' },
138
+ },
139
+ ...(status === null ? {} : { credentialStatus: status }),
140
+ encryptionMode: refresh.encryptionMode ?? 'network',
141
+ });
142
+ }),
143
+
144
+ attachStatusEntries: credentialRefreshRoute
145
+ .meta({ requiredScope: 'credentials:write' })
146
+ .input(AttachCredentialRefreshStatusEntriesInputValidator)
147
+ .output(z.boolean())
148
+ .mutation(async ({ ctx, input }) => {
149
+ const refresh = await getCredentialRefresh(input.refreshId);
150
+ if (!refresh || refresh.issuerProfileId !== ctx.user.profile.profileId) {
151
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Credential refresh not found' });
152
+ }
153
+ if (
154
+ !matchesCredentialStatusDigest(
155
+ input.credentialStatus,
156
+ refresh.credentialStatusDigest
157
+ )
158
+ ) {
159
+ throw new TRPCError({
160
+ code: 'BAD_REQUEST',
161
+ message: 'Credential status does not match the original credential',
162
+ });
163
+ }
164
+ const entries = getBitstringStatusListEntries({
165
+ credentialStatus: input.credentialStatus ?? undefined,
166
+ });
167
+ if (entries.length === 0 && input.credentialStatus !== null) {
168
+ throw new TRPCError({
169
+ code: 'BAD_REQUEST',
170
+ message: 'Credential has no supported public status entries',
171
+ });
172
+ }
173
+ await neogma.getTransaction(null, async tx => {
174
+ const result = await tx.run(
175
+ `MATCH (refresh:CredentialRefresh {refreshId: $refreshId})-[:ROOT]->(root:Credential)
176
+ SET root._statusMutationLock = true REMOVE root._statusMutationLock
177
+ RETURN root.id AS id`,
178
+ { refreshId: input.refreshId }
179
+ );
180
+ const root = result.records[0];
181
+ if (!root)
182
+ throw new TRPCError({
183
+ code: 'CONFLICT',
184
+ message: 'Credential has no original delivery',
185
+ });
186
+ await bindRefreshStatusEntries(
187
+ input.refreshId,
188
+ entries,
189
+ refresh.issuerProfileId,
190
+ ctx.domain,
191
+ tx
192
+ );
193
+ await tx.run(
194
+ `MATCH (refresh:CredentialRefresh {refreshId: $refreshId})
195
+ SET refresh.credentialStatusBinding = $credentialStatusBinding`,
196
+ {
197
+ refreshId: input.refreshId,
198
+ credentialStatusBinding: JSON.stringify(input.credentialStatus),
199
+ }
200
+ );
201
+ });
202
+ return true;
203
+ }),
204
+
44
205
  allocateCredentialRefresh: credentialRefreshRoute
45
206
  .meta({
46
207
  openapi: {
@@ -136,6 +297,83 @@ export const credentialRefreshesRouter = t.router({
136
297
  return uri;
137
298
  }),
138
299
 
300
+ /**
301
+ * Holder-mode counterpart of `sendRefreshableCredential` (LC-2345): a separate
302
+ * route rather than a branch of `send`/`sendRefreshableCredential`, so the
303
+ * ciphertext-only input shape (`HolderEncryptedHandoffValidator`) never has to
304
+ * share a validator with the plaintext `credential`/`signedCredential` paths.
305
+ */
306
+ sendHolderEncryptedRefreshable: credentialRefreshRoute
307
+ .meta({
308
+ openapi: {
309
+ protect: true,
310
+ method: 'POST',
311
+ path: '/credential-refresh/send-holder-encrypted',
312
+ tags: ['Credential Refresh'],
313
+ summary: 'Send a holder-mode (student-only encrypted) refreshable credential',
314
+ description:
315
+ 'Binds an already-encrypted handoff to its allocated refresh aggregate. The server never decrypts or processes the credential.',
316
+ },
317
+ requiredScope: 'credentials:write',
318
+ })
319
+ .input(SendHolderEncryptedRefreshableInputValidator)
320
+ .output(
321
+ z.object({
322
+ uri: z.string(),
323
+ activityId: z.string(),
324
+ statusManagement: z.enum(['network', 'external', 'mixed', 'none']),
325
+ })
326
+ )
327
+ .mutation(async ({ ctx, input }) => {
328
+ const { profile } = ctx.user;
329
+
330
+ const { uri, activityId } = await sendHolderEncryptedRefreshableCredential({
331
+ issuerProfile: profile,
332
+ handoff: input.handoff,
333
+ idempotencyKey: input.idempotencyKey,
334
+ boostUri: input.boostUri,
335
+ skipNotification: input.skipNotification,
336
+ domain: ctx.domain,
337
+ onBehalfOf: ctx.user.onBehalfOf,
338
+ });
339
+
340
+ return {
341
+ uri,
342
+ activityId,
343
+ statusManagement: getStatusManagement(
344
+ getBitstringStatusListEntries(input.handoff),
345
+ ctx.domain
346
+ ),
347
+ };
348
+ }),
349
+
350
+ /** Issuer-only: the current recipient set a holder-mode handoff must encrypt to. */
351
+ getEncryptionRecipients: credentialRefreshRoute
352
+ .meta({
353
+ openapi: {
354
+ protect: true,
355
+ method: 'GET',
356
+ path: '/credential-refresh/encryption-recipients',
357
+ tags: ['Credential Refresh'],
358
+ summary: 'Get the current holder-mode encryption recipients',
359
+ description:
360
+ 'Returns the current key-agreement verification-method ids (holder + managers) a holder-mode handoff must encrypt to. Used after a RECIPIENTS_CHANGED conflict.',
361
+ },
362
+ requiredScope: 'credentials:read',
363
+ })
364
+ .input(GetEncryptionRecipientsInputValidator)
365
+ .output(EncryptionRecipientsResultValidator)
366
+ .query(async ({ ctx, input }) => {
367
+ const { profile } = ctx.user;
368
+
369
+ const recipientKeyIds = await getCurrentEncryptionRecipients({
370
+ issuerProfile: profile,
371
+ refreshId: input.refreshId,
372
+ });
373
+
374
+ return { recipientKeyIds };
375
+ }),
376
+
139
377
  publishCredentialRefresh: credentialRefreshRoute
140
378
  .meta({
141
379
  openapi: {
@@ -162,6 +400,38 @@ export const credentialRefreshesRouter = t.router({
162
400
  });
163
401
  }),
164
402
 
403
+ /**
404
+ * Holder-mode counterpart of `publishCredentialRefresh`: a separate route, for the
405
+ * same reason as `sendHolderEncryptedRefreshable`. `PublishCredentialRefreshInputValidator`
406
+ * has no signing-authority equivalent for a handoff — a holder aggregate rejects
407
+ * signing-authority publication outright (`publishCredentialRefresh` enforces this).
408
+ */
409
+ publishHolderEncryptedRefreshable: credentialRefreshRoute
410
+ .meta({
411
+ openapi: {
412
+ protect: true,
413
+ method: 'POST',
414
+ path: '/credential-refresh/publish-holder-encrypted',
415
+ tags: ['Credential Refresh'],
416
+ summary: 'Publish a holder-mode (student-only encrypted) refresh version',
417
+ description:
418
+ 'Publishes a new immutable version from an already-encrypted handoff and atomically advances the refresh head. `notify` replaces server-inferred materiality.',
419
+ },
420
+ requiredScope: 'credentials:write',
421
+ })
422
+ .input(PublishHolderEncryptedRefreshInputValidator)
423
+ .output(PublishCredentialRefreshResultValidator)
424
+ .mutation(async ({ ctx, input }) => {
425
+ const { profile } = ctx.user;
426
+
427
+ return publishHolderEncryptedRefreshableCredential({
428
+ issuerProfile: profile,
429
+ input,
430
+ domain: ctx.domain,
431
+ branding: ctx.tenant?.emailBranding,
432
+ });
433
+ }),
434
+
165
435
  getCredentialRefreshHistory: credentialRefreshRoute
166
436
  .meta({
167
437
  openapi: {
@@ -212,6 +482,7 @@ export const credentialRefreshesRouter = t.router({
212
482
  etag: record.etag,
213
483
  signingMode: record.signingMode,
214
484
  updateSummary: record.updateSummary,
485
+ updateReason: record.updateReason,
215
486
  })),
216
487
  hasMore,
217
488
  cursor,
@@ -9,7 +9,7 @@ import {
9
9
  getServerDidWebDID,
10
10
  getOwningServiceDid,
11
11
  } from '@helpers/federation.helpers';
12
- import { getProfileByProfileId } from '@accesslayer/profile/read';
12
+ import { getProfileByDid, getProfileByProfileId } from '@accesslayer/profile/read';
13
13
  import { getProfileIdFromDid } from '@helpers/did.helpers';
14
14
 
15
15
  import { getOrCreateFederatedProfile } from '@helpers/profile.helpers';
@@ -99,13 +99,18 @@ export const federationRouter = t.router({
99
99
  senderDid,
100
100
  issuerDisplayName
101
101
  );
102
+ // A local sender's status lists belong to its real profile, not the federated alias.
103
+ const localIssuer = await getProfileByDid(issuerDid);
102
104
 
103
105
  const credentialUri = await sendCredential(
104
106
  federatedSender,
105
107
  recipientProfile,
106
108
  credential,
107
109
  ctx.domain,
108
- configuration
110
+ configuration,
111
+ undefined,
112
+ undefined,
113
+ localIssuer?.profileId ?? federatedSender.profileId
109
114
  );
110
115
 
111
116
  return {
@@ -3,6 +3,7 @@ import { submitInboxBatch, getInboxBatch } from '@helpers/inbox-queue.helpers';
3
3
  import {
4
4
  assertInboxRefreshEnabled,
5
5
  inboxRefreshRequestDigest,
6
+ getInboxRefreshRequestDigestCandidates,
6
7
  getInboxRefreshReplay,
7
8
  getInboxRefreshReceipt,
8
9
  resumeInboxRefreshDelivery,
@@ -466,7 +467,8 @@ export const inboxRouter = t.router({
466
467
  const replay = await getInboxRefreshReplay(
467
468
  profile.profileId,
468
469
  input.idempotencyKey,
469
- refreshDigest!
470
+ refreshDigest!,
471
+ getInboxRefreshRequestDigestCandidates(input)
470
472
  );
471
473
  if (replay && (replay.claimUrl || replay.inbox.currentStatus !== 'PENDING')) {
472
474
  await resumeInboxRefreshDelivery(replay.inbox.refreshId!, ctx.domain);
@@ -511,6 +513,9 @@ export const inboxRouter = t.router({
511
513
  refresh: input.refresh,
512
514
  idempotencyKey: input.idempotencyKey,
513
515
  refreshRequestDigest: refreshDigest,
516
+ refreshRequestDigestCandidates: input.refresh
517
+ ? getInboxRefreshRequestDigestCandidates(input)
518
+ : undefined,
514
519
  },
515
520
  ctx
516
521
  );
@@ -9,7 +9,11 @@ import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
9
9
  import { OpenApiMeta } from 'trpc-to-openapi';
10
10
  import jwtDecode from 'jwt-decode';
11
11
  import * as Sentry from '@sentry/serverless';
12
- import { ACT_AS_HEADER, AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX } from '@learncard/types';
12
+ import {
13
+ ACT_AS_HEADER,
14
+ AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX,
15
+ CONTRACT_APPROVAL_REQUIRED,
16
+ } from '@learncard/types';
13
17
  import { ContactMethodType } from '@learncard/types';
14
18
  import { MAX_SHARE_LINK_REQUEST_BYTES, utf8ByteLength } from '@learncard/types';
15
19
 
@@ -80,6 +84,18 @@ export const t = initTRPC
80
84
  input: RegExpTransformer,
81
85
  output: { serialize: o => o, deserialize: o => o },
82
86
  },
87
+ errorFormatter: ({ shape, error }) => ({
88
+ ...shape,
89
+ data: {
90
+ ...shape.data,
91
+ reason:
92
+ error.cause &&
93
+ 'code' in error.cause &&
94
+ error.cause.code === CONTRACT_APPROVAL_REQUIRED
95
+ ? CONTRACT_APPROVAL_REQUIRED
96
+ : undefined,
97
+ },
98
+ }),
83
99
  });
84
100
 
85
101
  export const createContext = async (
@@ -290,7 +306,9 @@ export const PUBLIC_SHARE_LINK_ROUTE_PREFIX = 'publicShareLinks.';
290
306
  export const publicShareLinkCacheControlHeaders = (
291
307
  paths: readonly string[] | undefined
292
308
  ): Record<string, string> =>
293
- (paths ?? []).some(path => path.startsWith(PUBLIC_SHARE_LINK_ROUTE_PREFIX))
309
+ (paths ?? []).some(
310
+ path => path.startsWith(PUBLIC_SHARE_LINK_ROUTE_PREFIX) || path === 'campaigns.unsubscribe'
311
+ )
294
312
  ? { 'Cache-Control': 'private, no-store' }
295
313
  : {};
296
314
 
@@ -493,6 +511,17 @@ export const didRouteWithoutInputCapture = withDid(openRouteWithoutInputCapture)
493
511
  export const didAndChallengeRouteWithoutInputCapture = withChallenge(didRouteWithoutInputCapture);
494
512
  export const profileRouteWithoutInputCapture = withProfile(didAndChallengeRouteWithoutInputCapture);
495
513
 
514
+ /** Credential payloads need the same privacy/auth chain, not the share-link byte limit. */
515
+ export const credentialProfileRoute = withProfile(
516
+ withChallenge(
517
+ withDid(
518
+ t.procedure
519
+ .use(t.middleware(sentryNoInputCaptureMiddleware))
520
+ .use(sentryTransactionNameMiddleware)
521
+ )
522
+ )
523
+ );
524
+
496
525
  export const openProfileManagerRoute = openRoute.use(async ({ ctx, next }) => {
497
526
  if (!ctx.user?.did) throw new TRPCError({ code: 'UNAUTHORIZED' });
498
527