@learncard/network-brain-service 3.18.0 → 3.19.1

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 (478) hide show
  1. package/.env.example +38 -2
  2. package/CHANGELOG.md +118 -0
  3. package/README.md +42 -0
  4. package/compose.inbox.yml +7 -0
  5. package/dist/accesslayer/auth-grant/read.d.ts +1 -0
  6. package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
  7. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  8. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  9. package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
  10. package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
  11. package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
  12. package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
  13. package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
  14. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  15. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  16. package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
  17. package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
  18. package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
  19. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  20. package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
  21. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
  22. package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
  23. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  24. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  25. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  26. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  27. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  29. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  30. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  31. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  32. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  33. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  34. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  35. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  36. package/dist/accesslayer/share-link/helpers.d.ts +56 -0
  37. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  38. package/dist/accesslayer/share-link/index.d.ts +12 -0
  39. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  40. package/dist/accesslayer/share-link/list.d.ts +5 -0
  41. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  42. package/dist/accesslayer/share-link/read.d.ts +5 -0
  43. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  44. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  45. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  46. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  47. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  48. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  49. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  50. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  51. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  52. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  53. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  54. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  55. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  56. package/dist/accesslayer/share-link/types.d.ts +370 -0
  57. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  58. package/dist/app.d.ts +4 -0
  59. package/dist/app.d.ts.map +1 -1
  60. package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
  61. package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
  62. package/dist/cache/atomic-counter.test.d.ts +2 -0
  63. package/dist/cache/atomic-counter.test.d.ts.map +1 -0
  64. package/dist/cache/index.d.ts +1 -1
  65. package/dist/cache/index.d.ts.map +1 -1
  66. package/dist/config/environment.d.ts +55 -0
  67. package/dist/config/environment.d.ts.map +1 -1
  68. package/dist/constants/auth-grant.d.ts +2 -0
  69. package/dist/constants/auth-grant.d.ts.map +1 -1
  70. package/dist/credential-refresh.d.ts.map +1 -1
  71. package/dist/helpers/activity.helpers.d.ts +4 -0
  72. package/dist/helpers/activity.helpers.d.ts.map +1 -1
  73. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  74. package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
  75. package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
  76. package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
  77. package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
  78. package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
  79. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
  80. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
  81. package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
  82. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
  83. package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
  84. package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
  85. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
  86. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
  87. package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
  88. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  89. package/dist/helpers/credential.helpers.d.ts +2 -1
  90. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  91. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  92. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  93. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  94. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  95. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  96. package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
  97. package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
  98. package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
  99. package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
  100. package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
  101. package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
  102. package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
  103. package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
  104. package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
  105. package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
  106. package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
  107. package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
  108. package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
  109. package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
  110. package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
  111. package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
  112. package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
  113. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
  114. package/dist/helpers/inbox.helpers.d.ts +9 -1
  115. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  116. package/dist/helpers/notificationMessages.d.ts +1 -1
  117. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  118. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  119. package/dist/helpers/presentation-metadata.d.ts +4 -0
  120. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  121. package/dist/helpers/presentation.helpers.d.ts +1 -1
  122. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  123. package/dist/helpers/profile.helpers.d.ts +1 -0
  124. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  125. package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
  126. package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
  127. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
  128. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  129. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  130. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  131. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  132. package/dist/helpers/share-content-client/client.d.ts +5 -0
  133. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  134. package/dist/helpers/share-content-client/config.d.ts +11 -0
  135. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  136. package/dist/helpers/share-content-client/index.d.ts +6 -0
  137. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  138. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  139. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  140. package/dist/helpers/share-content-client/types.d.ts +145 -0
  141. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  142. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  143. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  144. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  145. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  146. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  147. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  148. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  149. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  150. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  151. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  152. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  153. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  154. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  155. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  156. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  157. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  158. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  159. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  160. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  161. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  162. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  163. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  164. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  165. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  166. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  167. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  168. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  169. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  170. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  171. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  172. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  173. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  174. package/dist/helpers/share-link-maintenance/runner.d.ts +19 -0
  175. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  176. package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -0
  177. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  178. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  179. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  180. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  181. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  182. package/dist/helpers/share-link-maintenance/types.d.ts +53 -0
  183. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  184. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  185. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  186. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  187. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  188. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  189. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  190. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  191. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  192. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  193. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  194. package/dist/helpers/share-link-passcode.d.ts +6 -0
  195. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  196. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  197. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  198. package/dist/helpers/share-link-policy/production.d.ts +13 -0
  199. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  200. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  201. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  202. package/dist/helpers/share-link-policy/types.d.ts +16 -0
  203. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  204. package/dist/helpers/share-link-public/config.d.ts +13 -0
  205. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  206. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  207. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  208. package/dist/helpers/shim.d.ts +1 -1
  209. package/dist/helpers/shim.d.ts.map +1 -1
  210. package/dist/inbox-queue-local.d.ts +2 -0
  211. package/dist/inbox-queue-local.d.ts.map +1 -0
  212. package/dist/index.js +2 -2
  213. package/dist/models/AuthGrant.d.ts.map +1 -1
  214. package/dist/models/CredentialActivity.d.ts +1 -0
  215. package/dist/models/CredentialActivity.d.ts.map +1 -1
  216. package/dist/models/CredentialRefresh.d.ts +5 -2
  217. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  218. package/dist/models/InboxCredential.d.ts +1 -0
  219. package/dist/models/InboxCredential.d.ts.map +1 -1
  220. package/dist/models/Presentation.d.ts +2 -0
  221. package/dist/models/Presentation.d.ts.map +1 -1
  222. package/dist/models/Profile.d.ts +2 -0
  223. package/dist/models/Profile.d.ts.map +1 -1
  224. package/dist/models/ShareLink.d.ts +44 -0
  225. package/dist/models/ShareLink.d.ts.map +1 -0
  226. package/dist/models/ShareViewReceipt.d.ts +22 -0
  227. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  228. package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
  229. package/dist/models/index.d.ts +1 -0
  230. package/dist/models/index.d.ts.map +1 -1
  231. package/dist/models/share-link-constraints.d.ts +3 -0
  232. package/dist/models/share-link-constraints.d.ts.map +1 -0
  233. package/dist/openapi.d.ts.map +1 -1
  234. package/dist/public-share-link-http.d.ts +4 -0
  235. package/dist/public-share-link-http.d.ts.map +1 -0
  236. package/dist/routes/act-as.test.d.ts +2 -0
  237. package/dist/routes/act-as.test.d.ts.map +1 -0
  238. package/dist/routes/activity.d.ts +3 -0
  239. package/dist/routes/activity.d.ts.map +1 -1
  240. package/dist/routes/app-store.d.ts.map +1 -1
  241. package/dist/routes/auth-grants.d.ts +10 -0
  242. package/dist/routes/auth-grants.d.ts.map +1 -1
  243. package/dist/routes/boosts.d.ts +494 -0
  244. package/dist/routes/boosts.d.ts.map +1 -1
  245. package/dist/routes/contracts.d.ts +2 -2
  246. package/dist/routes/contracts.d.ts.map +1 -1
  247. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  248. package/dist/routes/credentials.d.ts +1 -1
  249. package/dist/routes/credentials.d.ts.map +1 -1
  250. package/dist/routes/inbox.d.ts +953 -11
  251. package/dist/routes/inbox.d.ts.map +1 -1
  252. package/dist/routes/index.d.ts +154 -85
  253. package/dist/routes/index.d.ts.map +1 -1
  254. package/dist/routes/presentations.d.ts +1 -0
  255. package/dist/routes/presentations.d.ts.map +1 -1
  256. package/dist/routes/profile-manager.d.ts.map +1 -1
  257. package/dist/routes/profiles.d.ts.map +1 -1
  258. package/dist/routes/public-share-links.d.ts +230 -0
  259. package/dist/routes/public-share-links.d.ts.map +1 -0
  260. package/dist/routes/share-links.d.ts +844 -0
  261. package/dist/routes/share-links.d.ts.map +1 -0
  262. package/dist/routes/workflows.d.ts.map +1 -1
  263. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  264. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
  265. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
  266. package/dist/services/registry/registry.factory.d.ts.map +1 -1
  267. package/dist/types/activity.d.ts +4 -0
  268. package/dist/types/activity.d.ts.map +1 -1
  269. package/dist/types/consentflowcontract.d.ts +1 -1
  270. package/dist/types/credential-refresh.d.ts +10 -1
  271. package/dist/types/credential-refresh.d.ts.map +1 -1
  272. package/dist/types/inbox-batch.d.ts +38 -0
  273. package/dist/types/inbox-batch.d.ts.map +1 -0
  274. package/inbox-e2e-setup.ts +53 -1
  275. package/inbox-elasticmq.conf +10 -0
  276. package/lambda.ts +64 -16
  277. package/package.json +24 -16
  278. package/scripts/seed-demo-persona.ts +425 -0
  279. package/scripts/setup-inbox-queues.ts +60 -0
  280. package/serverless-local.yml +2 -0
  281. package/serverless.yml +111 -0
  282. package/shareLinkMaintenanceLambda.ts +49 -0
  283. package/src/accesslayer/auth-grant/read.ts +2 -1
  284. package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
  285. package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
  286. package/src/accesslayer/credential-activity/create.ts +1 -0
  287. package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
  288. package/src/accesslayer/credential-refresh/index.ts +1 -0
  289. package/src/accesslayer/credential-refresh/read.ts +8 -2
  290. package/src/accesslayer/credential-refresh/update.ts +4 -0
  291. package/src/accesslayer/inbox-batch/store.ts +469 -0
  292. package/src/accesslayer/inbox-credential/create.ts +51 -19
  293. package/src/accesslayer/inbox-credential/refresh.ts +221 -0
  294. package/src/accesslayer/inbox-credential/update.ts +1 -1
  295. package/src/accesslayer/presentation/read.ts +57 -25
  296. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  297. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  298. package/src/accesslayer/share-link/abandon.ts +100 -0
  299. package/src/accesslayer/share-link/cleanup.ts +271 -0
  300. package/src/accesslayer/share-link/errors.ts +32 -0
  301. package/src/accesslayer/share-link/finalize.ts +265 -0
  302. package/src/accesslayer/share-link/helpers.ts +419 -0
  303. package/src/accesslayer/share-link/index.ts +11 -0
  304. package/src/accesslayer/share-link/list.ts +162 -0
  305. package/src/accesslayer/share-link/read.ts +107 -0
  306. package/src/accesslayer/share-link/receipt.ts +548 -0
  307. package/src/accesslayer/share-link/recovery.ts +545 -0
  308. package/src/accesslayer/share-link/reserve.ts +684 -0
  309. package/src/accesslayer/share-link/revoke.ts +197 -0
  310. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  311. package/src/accesslayer/share-link/transaction.ts +126 -0
  312. package/src/accesslayer/share-link/types.ts +601 -0
  313. package/src/app.ts +6 -0
  314. package/src/cache/atomic-counter.test-helpers.ts +27 -0
  315. package/src/cache/atomic-counter.test.ts +6 -0
  316. package/src/cache/index.ts +25 -13
  317. package/src/config/environment.ts +40 -0
  318. package/src/constants/auth-grant.ts +5 -0
  319. package/src/credential-refresh.ts +5 -1
  320. package/src/docker-entry.ts +65 -0
  321. package/src/helpers/activity.helpers.ts +4 -0
  322. package/src/helpers/app-counter.helpers.ts +1 -1
  323. package/src/helpers/boost.helpers.ts +4 -31
  324. package/src/helpers/contact-method.helpers.test.ts +43 -0
  325. package/src/helpers/contact-method.helpers.ts +3 -4
  326. package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
  327. package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
  328. package/src/helpers/credential-refresh-email.helpers.ts +201 -0
  329. package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
  330. package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
  331. package/src/helpers/credential-refresh.helpers.ts +375 -99
  332. package/src/helpers/credential.helpers.ts +4 -1
  333. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  334. package/src/helpers/credentialSubject.helpers.ts +40 -0
  335. package/src/helpers/finalize-inbox.helpers.ts +29 -14
  336. package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
  337. package/src/helpers/inbox-batch-http.helpers.ts +150 -0
  338. package/src/helpers/inbox-batch-status.helpers.ts +11 -0
  339. package/src/helpers/inbox-batch.helpers.test.ts +668 -0
  340. package/src/helpers/inbox-batch.helpers.ts +374 -0
  341. package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
  342. package/src/helpers/inbox-queue.helpers.test.ts +253 -0
  343. package/src/helpers/inbox-queue.helpers.ts +372 -0
  344. package/src/helpers/inbox-refresh.helpers.ts +432 -0
  345. package/src/helpers/inbox.helpers.ts +275 -36
  346. package/src/helpers/notificationMessages.ts +18 -1
  347. package/src/helpers/notifications.helpers.ts +4 -2
  348. package/src/helpers/presentation-metadata.ts +41 -0
  349. package/src/helpers/presentation.helpers.ts +27 -20
  350. package/src/helpers/profile.helpers.ts +7 -0
  351. package/src/helpers/rateLimit.helpers.ts +2 -5
  352. package/src/helpers/refresh-send-intent.helpers.ts +273 -0
  353. package/src/helpers/share-content-client/adapters.ts +55 -0
  354. package/src/helpers/share-content-client/canonical.ts +25 -0
  355. package/src/helpers/share-content-client/client.ts +440 -0
  356. package/src/helpers/share-content-client/config.ts +189 -0
  357. package/src/helpers/share-content-client/index.ts +27 -0
  358. package/src/helpers/share-content-client/responses.ts +273 -0
  359. package/src/helpers/share-content-client/types.ts +253 -0
  360. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  361. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  362. package/src/helpers/share-link-coordinator/coordinator.ts +714 -0
  363. package/src/helpers/share-link-coordinator/index.ts +8 -0
  364. package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -0
  365. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  366. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  367. package/src/helpers/share-link-coordinator/types.ts +353 -0
  368. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  369. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  370. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  371. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  372. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  373. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  374. package/src/helpers/share-link-maintenance/config.ts +312 -0
  375. package/src/helpers/share-link-maintenance/index.ts +24 -0
  376. package/src/helpers/share-link-maintenance/runner.ts +223 -0
  377. package/src/helpers/share-link-maintenance/runtime.ts +334 -0
  378. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  379. package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
  380. package/src/helpers/share-link-maintenance/types.ts +89 -0
  381. package/src/helpers/share-link-owner/config.ts +48 -0
  382. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  383. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  384. package/src/helpers/share-link-owner-projection.ts +52 -0
  385. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  386. package/src/helpers/share-link-passcode.ts +47 -0
  387. package/src/helpers/share-link-policy/index.ts +7 -0
  388. package/src/helpers/share-link-policy/production.ts +88 -0
  389. package/src/helpers/share-link-policy/resolver.ts +108 -0
  390. package/src/helpers/share-link-policy/types.ts +48 -0
  391. package/src/helpers/share-link-public/config.ts +41 -0
  392. package/src/helpers/share-link-view-notification.ts +18 -0
  393. package/src/helpers/shim.ts +32 -31
  394. package/src/inbox-queue-local.ts +28 -0
  395. package/src/models/AuthGrant.ts +1 -0
  396. package/src/models/CredentialActivity.ts +3 -5
  397. package/src/models/CredentialRefresh.ts +4 -2
  398. package/src/models/InboxCredential.ts +2 -0
  399. package/src/models/Presentation.ts +2 -2
  400. package/src/models/Profile.ts +3 -2
  401. package/src/models/ShareLink.ts +130 -0
  402. package/src/models/ShareViewReceipt.ts +69 -0
  403. package/src/models/credential-refresh-constraints.test.ts +8 -3
  404. package/src/models/credential-refresh-constraints.ts +5 -0
  405. package/src/models/index.ts +2 -0
  406. package/src/models/share-link-constraints.ts +93 -0
  407. package/src/openapi.ts +8 -0
  408. package/src/public-share-link-http.ts +49 -0
  409. package/src/routes/act-as.test.ts +239 -0
  410. package/src/routes/app-store.ts +4 -13
  411. package/src/routes/auth-grants.ts +13 -11
  412. package/src/routes/boosts.ts +916 -103
  413. package/src/routes/contracts.ts +2 -11
  414. package/src/routes/credential-refreshes.test.ts +8 -2
  415. package/src/routes/credential-refreshes.ts +4 -1
  416. package/src/routes/credentials.ts +2 -1
  417. package/src/routes/inbox.ts +95 -49
  418. package/src/routes/index.ts +216 -46
  419. package/src/routes/presentations.ts +19 -3
  420. package/src/routes/profile-manager.ts +10 -2
  421. package/src/routes/profiles.ts +17 -6
  422. package/src/routes/public-share-links.ts +814 -0
  423. package/src/routes/share-links.ts +640 -0
  424. package/src/routes/skills.ts +1 -1
  425. package/src/routes/workflows.ts +43 -24
  426. package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
  427. package/src/services/delivery/adapters/postmark.adapter.ts +16 -1
  428. package/src/services/registry/registry.factory.ts +2 -1
  429. package/src/types/activity.ts +2 -0
  430. package/src/types/credential-refresh.ts +21 -2
  431. package/src/types/inbox-batch.ts +46 -0
  432. package/test/atomic-counter.e2e.ts +12 -0
  433. package/test/auth-grant.spec.ts +21 -21
  434. package/test/credential-refresh-email.spec.ts +663 -0
  435. package/test/credential-refresh-publication.spec.ts +2 -0
  436. package/test/dids.spec.ts +10 -1
  437. package/test/guardian-gated-routes.spec.ts +13 -0
  438. package/test/helpers/inbox-e2e-environment.ts +6 -0
  439. package/test/helpers/share-link-fixtures.ts +94 -0
  440. package/test/inbox-batch.spec.ts +1338 -0
  441. package/test/inbox-refresh.spec.ts +534 -0
  442. package/test/managed-recipient-inbox.spec.ts +130 -0
  443. package/test/notificationMessages.spec.ts +13 -0
  444. package/test/openapi.spec.ts +61 -1
  445. package/test/presentations.spec.ts +54 -6
  446. package/test/profiles.spec.ts +67 -1
  447. package/test/refresh-send.spec.ts +1799 -0
  448. package/test/registry.spec.ts +56 -11
  449. package/test/share-content-client.contract.spec.ts +365 -0
  450. package/test/share-content-client.unit.spec.ts +462 -0
  451. package/test/share-link-abuse.unit.spec.ts +65 -0
  452. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  453. package/test/share-link-coordinator.unit.spec.ts +990 -0
  454. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  455. package/test/share-link-lifecycle.neo4j.spec.ts +1268 -0
  456. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  457. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  458. package/test/share-link-list.neo4j.spec.ts +324 -0
  459. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  460. package/test/share-link-maintenance.unit.spec.ts +1113 -0
  461. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  462. package/test/share-link-owner-route.unit.spec.ts +752 -0
  463. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  464. package/test/share-link-passcode.unit.spec.ts +29 -0
  465. package/test/share-link-policy.unit.spec.ts +135 -0
  466. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  467. package/test/share-link-production-policy.unit.spec.ts +77 -0
  468. package/test/share-link-public-config.unit.spec.ts +64 -0
  469. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  470. package/test/share-link-public-route.unit.spec.ts +800 -0
  471. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  472. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  473. package/test/share-link-recovery.unit.spec.ts +726 -0
  474. package/vitest.config.ts +10 -0
  475. package/vitest.inbox-e2e.config.ts +5 -1
  476. package/vitest.integration.config.ts +2 -0
  477. package/vitest.share-links-neo4j.config.ts +19 -0
  478. package/vitest.share-links-unit.config.ts +28 -0
@@ -1,3 +1,10 @@
1
+ import {
2
+ assertInboxRefreshEnabled,
3
+ inboxRefreshRequestDigest,
4
+ getInboxRefreshReplay,
5
+ getInboxRefreshReceipt,
6
+ resumeInboxRefreshDelivery,
7
+ } from '@helpers/inbox-refresh.helpers';
1
8
  import { TRPCError } from '@trpc/server';
2
9
  import { z } from 'zod';
3
10
  import { v4 as uuid } from 'uuid';
@@ -27,11 +34,19 @@ import {
27
34
  PaginatedBoostRecipientsWithChildrenValidator,
28
35
  SkillQueryValidator,
29
36
  SendBoostInputValidator,
37
+ SendBoostTemplateValidator,
30
38
  SendBoostResponseValidator,
39
+ PrepareRefreshableSendInputValidator,
40
+ PrepareRefreshableSendResultValidator,
41
+ PrepareRefreshableSendResult,
31
42
  AllocateCredentialStatusInputValidator,
32
43
  AllocatedBitstringStatusListEntryValidator,
33
44
  } from '@learncard/types';
34
- import { isVC2Format } from '@learncard/helpers';
45
+ import {
46
+ isVC2Format,
47
+ injectManagedRefreshService,
48
+ getCredentialIssuerId,
49
+ } from '@learncard/helpers';
35
50
  import {
36
51
  renderBoostTemplate,
37
52
  parseRenderedTemplate,
@@ -47,6 +62,7 @@ import { t, profileRoute } from '@routes';
47
62
 
48
63
  import {
49
64
  getBoostByUri,
65
+ getBoostById,
50
66
  getBoostsForProfile,
51
67
  countBoostsForProfile,
52
68
  getBoostsByUri,
@@ -99,6 +115,7 @@ import {
99
115
  prepareCredentialFromBoost,
100
116
  appendTemplateEvidenceToCredential,
101
117
  } from '@helpers/boost.helpers';
118
+ import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
102
119
  import {
103
120
  BoostValidator,
104
121
  BoostGenerateClaimLinkInput,
@@ -176,6 +193,28 @@ import {
176
193
  import { updateDefaultPermissionsForBoost } from '@accesslayer/role/relationships/update';
177
194
  import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
178
195
  import type { IssuedCredential } from '../types/credential';
196
+ import {
197
+ allocateCredentialRefresh,
198
+ extractManagedRefreshHandoff,
199
+ getBoundRefreshBoostId,
200
+ getCredentialRefreshServiceUrl,
201
+ peekCredentialRefreshInitialBinding,
202
+ sendRefreshableCredential,
203
+ } from '@helpers/credential-refresh.helpers';
204
+ import {
205
+ claimRefreshSendIntent,
206
+ computeRefreshSendRequestDigest,
207
+ getRefreshSendIntent,
208
+ markRefreshSendIntentDelivered,
209
+ reconcileBoundRefreshSendIntent,
210
+ recordRefreshSendIntent,
211
+ recordRefreshSendIntentPending,
212
+ type RefreshSendIntent,
213
+ } from '@helpers/refresh-send-intent.helpers';
214
+ import { getCredentialRefreshRuntimeEnvironment } from '@environment';
215
+ import { ensureCredentialRefreshConstraints } from '../models/credential-refresh-constraints';
216
+ import { userHasRequiredScopes } from '@helpers/auth-grant.helpers';
217
+ import { AUTH_GRANT_NO_ACCESS_SCOPE } from 'src/constants/auth-grant';
179
218
  import { removeConnectionsForBoost } from '@helpers/connection.helpers';
180
219
  import { issueToInbox } from '@helpers/inbox.helpers';
181
220
  import { findInboxServiceEndpoint } from '@helpers/federation.helpers';
@@ -242,7 +281,336 @@ const resolveBoostCredentialInstance = async ({
242
281
  return getCredentialInstanceForBoostAndProfile(boostId, recipientProfileId);
243
282
  };
244
283
 
284
+ /**
285
+ * Resolves the approved consent-flow contract terms for a unified send, and links a
286
+ * newly created boost to the contract. Shared by the normal and managed-refresh send
287
+ * paths so contract linkage semantics stay identical.
288
+ */
289
+ const resolveContractForSend = async (params: {
290
+ profile: ProfileType;
291
+ targetProfile: ProfileType;
292
+ boost: BoostInstance;
293
+ boostCreated: boolean;
294
+ contractUri?: string;
295
+ }) => {
296
+ const { profile, targetProfile, boost, boostCreated, contractUri } = params;
297
+
298
+ let contractTerms = null as Awaited<ReturnType<typeof getContractTermsForProfile>> | null;
299
+ let contractDetails: Awaited<ReturnType<typeof getContractDetailsByUri>> | null = null;
300
+
301
+ if (contractUri) {
302
+ const decodedContractUri = decodeURIComponent(contractUri);
303
+ contractDetails = await traceDb('getContractDetailsByUri', () =>
304
+ getContractDetailsByUri(decodedContractUri)
305
+ );
306
+
307
+ if (!contractDetails) {
308
+ throw new TRPCError({
309
+ code: 'NOT_FOUND',
310
+ message: 'Could not find contract',
311
+ });
312
+ }
313
+
314
+ const terms = await traceDb('getContractTermsForProfile', () =>
315
+ getContractTermsForProfile(targetProfile, contractDetails!.contract)
316
+ );
317
+
318
+ const writers = await traceDb('getWritersForContract', () =>
319
+ getWritersForContract(contractDetails!.contract)
320
+ );
321
+ const isWriter = writers.some(writer => writer.profileId === profile.profileId);
322
+ const isDenied = terms?.terms.deniedWriters?.includes(profile.profileId) ?? false;
323
+ const categoryAllowed = boost.category
324
+ ? terms?.terms.write?.credentials?.categories?.[boost.category] === true
325
+ : true;
326
+
327
+ if (terms && isWriter && !isDenied && categoryAllowed) {
328
+ contractTerms = terms;
329
+ }
330
+ }
331
+
332
+ if (boostCreated && contractDetails) {
333
+ await traceDb('setRelatedBoostForContract', () =>
334
+ setRelatedBoostForContract(contractDetails!.contract, boost)
335
+ );
336
+ }
337
+
338
+ return contractTerms;
339
+ };
340
+
341
+ /** Validate managed send support and recipient before creating any send state. */
342
+ const validateRefreshSendRecipient = async ({
343
+ profile,
344
+ scope,
345
+ recipient,
346
+ domain,
347
+ }: {
348
+ profile: ProfileType;
349
+ scope?: string;
350
+ recipient: string;
351
+ domain: string;
352
+ }): Promise<ProfileType> => {
353
+ if (!getCredentialRefreshRuntimeEnvironment().CREDENTIAL_REFRESH_ENABLED) {
354
+ throw new TRPCError({
355
+ code: 'NOT_FOUND',
356
+ message: 'Credential refresh is not available',
357
+ });
358
+ }
359
+
360
+ await ensureCredentialRefreshConstraints();
361
+
362
+ // The route itself requires boosts:write; managed refresh additionally
363
+ // requires the same credentials:write scope as the dedicated
364
+ // /credential-refresh routes.
365
+ if (!userHasRequiredScopes(scope ?? AUTH_GRANT_NO_ACCESS_SCOPE, 'credentials:write')) {
366
+ throw new TRPCError({
367
+ code: 'UNAUTHORIZED',
368
+ message: 'This operation requires credentials:write scope',
369
+ });
370
+ }
371
+
372
+ if (isInboxRecipient(recipient)) {
373
+ throw new TRPCError({
374
+ code: 'BAD_REQUEST',
375
+ message:
376
+ 'Managed credential refresh requires a profile or DID recipient; email and phone recipients cannot request refresh.',
377
+ });
378
+ }
379
+
380
+ // Recipients must resolve to local profiles before anything is
381
+ // created; remote or unresolvable DIDs are rejected here instead of
382
+ // entering the federation/inbox flows.
383
+ const refreshRecipientProfileId = await traceInternal('getProfileIdFromString:refresh', () =>
384
+ getProfileIdFromString(recipient, domain)
385
+ );
386
+ const refreshTargetProfile = refreshRecipientProfileId
387
+ ? await traceDb('getProfileByProfileId:refresh', () =>
388
+ getProfileByProfileId(refreshRecipientProfileId)
389
+ )
390
+ : null;
391
+
392
+ if (!refreshTargetProfile) {
393
+ throw new TRPCError({
394
+ code: 'NOT_FOUND',
395
+ message:
396
+ 'Managed credential refresh requires a recipient resolvable to a local profile. Remote or unresolvable DIDs are not supported.',
397
+ });
398
+ }
399
+
400
+ const refreshTarget = refreshTargetProfile;
401
+
402
+ if (
403
+ await traceDb('isRelationshipBlocked:refresh', () =>
404
+ isRelationshipBlocked(profile, refreshTarget)
405
+ )
406
+ ) {
407
+ throw new TRPCError({
408
+ code: 'NOT_FOUND',
409
+ message: 'Profile not found. Are you sure this person exists?',
410
+ });
411
+ }
412
+
413
+ return refreshTargetProfile;
414
+ };
415
+
416
+ /** Preserve the inline send template's metadata, skills and claim permissions. */
417
+ const createInlineBoostForSend = async (
418
+ template: z.infer<typeof SendBoostTemplateValidator>,
419
+ profile: ProfileType,
420
+ domain: string
421
+ ): Promise<BoostInstance> => {
422
+ const { credential, claimPermissions, skills, ...metadata } = template;
423
+ const boost = await traceDb('createBoost', () =>
424
+ createBoost(credential, profile, metadata, domain)
425
+ );
426
+ if (Array.isArray(skills) && skills.length > 0) {
427
+ await traceDb('addAlignedSkillsToBoost', () => addAlignedSkillsToBoost(boost, skills));
428
+ }
429
+ if (claimPermissions) {
430
+ await traceDb('addClaimPermissionsForBoost', () =>
431
+ addClaimPermissionsForBoost(boost, { ...EMPTY_PERMISSIONS, ...claimPermissions })
432
+ );
433
+ }
434
+ return boost;
435
+ };
436
+
437
+ const managedRefreshServiceFor = (refreshId: string, domain: string) => ({
438
+ id: getCredentialRefreshServiceUrl(refreshId, domain),
439
+ type: 'LearnCardCredentialRefresh2026' as const,
440
+ authorization: { type: 'LearnCardDIDAuth' as const },
441
+ });
442
+
443
+ /**
444
+ * The single server preparation step for a managed refresh send (SDK and signing
445
+ * authority paths): after recipient validation, create/reuse the boost and allocate
446
+ * the refresh. With an idempotencyKey, progress is recorded on a RefreshSendIntent so a
447
+ * retried call reuses the same boost/allocation, or returns the completed result.
448
+ */
449
+ const prepareManagedRefreshSend = async (params: {
450
+ profile: ProfileType;
451
+ /** Returned by validateRefreshSendRecipient in this request, before any mutation. */
452
+ targetProfile: ProfileType;
453
+ domain: string;
454
+ templateUri?: string;
455
+ template?: z.infer<typeof SendBoostTemplateValidator>;
456
+ contractUri?: string;
457
+ credentialId?: string;
458
+ templateData?: Record<string, unknown>;
459
+ integrationId?: string;
460
+ idempotencyKey?: string;
461
+ }): Promise<PrepareRefreshableSendResult & { intent?: RefreshSendIntent }> => {
462
+ const { profile, targetProfile, domain, templateUri, template, contractUri } = params;
463
+ // Local recipients authenticate refresh requests as their network profile DID,
464
+ // including when the caller addressed them by their controller did:key.
465
+ const holderDid = getDidWeb(domain, targetProfile.profileId);
466
+
467
+ let intent: RefreshSendIntent | undefined;
468
+
469
+ if (params.idempotencyKey) {
470
+ const claim = await claimRefreshSendIntent({
471
+ issuerProfileId: profile.profileId,
472
+ idempotencyKey: params.idempotencyKey,
473
+ requestDigest: computeRefreshSendRequestDigest({
474
+ recipientProfileId: targetProfile.profileId,
475
+ holderDid,
476
+ templateUri,
477
+ template,
478
+ contractUri,
479
+ templateData: params.templateData,
480
+ integrationId: params.integrationId,
481
+ credentialId: params.credentialId,
482
+ }),
483
+ });
484
+
485
+ intent = claim.intent;
486
+
487
+ if (claim.kind !== 'owned') {
488
+ const prepared = {
489
+ boostUri: intent.boostUri!,
490
+ credentialId: intent.credentialId!,
491
+ refreshId: intent.refreshId!,
492
+ refreshService: managedRefreshServiceFor(intent.refreshId!, domain),
493
+ holderDid: intent.holderDid!,
494
+ intent,
495
+ };
496
+ const completed =
497
+ claim.kind === 'delivered'
498
+ ? intent.result
499
+ : await reconcileBoundRefreshSendIntent(intent, domain);
500
+
501
+ return completed ? { ...prepared, completed } : prepared;
502
+ }
503
+ }
504
+
505
+ if (template?.status === 'DRAFT') {
506
+ throw new TRPCError({
507
+ code: 'FORBIDDEN',
508
+ message: 'Draft Boosts can not be sent. Only Published Boosts can be sent.',
509
+ });
510
+ }
511
+
512
+ let boostUri = intent?.boostUri ?? templateUri;
513
+ let boost: BoostInstance | null;
514
+
515
+ if (boostUri) {
516
+ boost = await traceDb('getBoostByUri:prepareRefresh', () => getBoostByUri(boostUri!));
517
+
518
+ if (!boost) throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find boost' });
519
+ } else {
520
+ boost = await createInlineBoostForSend(template!, profile, domain);
521
+ boostUri = getBoostUri(boost.id, domain);
522
+
523
+ if (intent) intent = await recordRefreshSendIntent(intent, { boostUri });
524
+
525
+ await resolveContractForSend({
526
+ profile,
527
+ targetProfile,
528
+ boost,
529
+ boostCreated: true,
530
+ contractUri,
531
+ });
532
+ }
533
+
534
+ if (
535
+ !(await traceDb('canProfileIssueBoost:prepareRefresh', () =>
536
+ canProfileIssueBoost(profile, boost!)
537
+ ))
538
+ ) {
539
+ throw new TRPCError({
540
+ code: 'UNAUTHORIZED',
541
+ message: 'Profile does not have permissions to issue boost',
542
+ });
543
+ }
544
+
545
+ if (isDraftBoost(boost)) {
546
+ throw new TRPCError({
547
+ code: 'FORBIDDEN',
548
+ message: 'Draft Boosts can not be sent. Only Published Boosts can be sent.',
549
+ });
550
+ }
551
+
552
+ const credentialId = intent?.credentialId ?? params.credentialId ?? `urn:uuid:${uuid()}`;
553
+ let refreshId = intent?.refreshId;
554
+
555
+ if (!refreshId) {
556
+ const allocation = await traceInternal('allocateCredentialRefresh:prepare', () =>
557
+ allocateCredentialRefresh({
558
+ issuerProfile: profile,
559
+ holderProfile: targetProfile,
560
+ holderDid,
561
+ credentialId,
562
+ domain,
563
+ })
564
+ );
565
+
566
+ refreshId = allocation.refreshId;
567
+ }
568
+
569
+ if (intent) {
570
+ intent = await recordRefreshSendIntent(intent, {
571
+ boostUri,
572
+ credentialId,
573
+ refreshId,
574
+ holderDid,
575
+ state: 'prepared',
576
+ });
577
+ }
578
+
579
+ return {
580
+ boostUri,
581
+ credentialId,
582
+ refreshId,
583
+ refreshService: managedRefreshServiceFor(refreshId, domain),
584
+ holderDid,
585
+ ...(intent ? { intent } : {}),
586
+ };
587
+ };
588
+
245
589
  export const boostsRouter = t.router({
590
+ // SDK preparation only: guards, boost anchor and refresh allocation. Never signs or
591
+ // delivers. With an idempotencyKey a retried call reuses the same boost/allocation,
592
+ // or returns `completed` when that key already delivered.
593
+ prepareRefreshableSend: profileRoute
594
+ .meta({ requiredScope: 'boosts:write' })
595
+ .input(PrepareRefreshableSendInputValidator)
596
+ .output(PrepareRefreshableSendResultValidator)
597
+ .mutation(async ({ ctx, input }) => {
598
+ const targetProfile = await validateRefreshSendRecipient({
599
+ profile: ctx.user.profile,
600
+ scope: ctx.user.scope,
601
+ recipient: input.recipient,
602
+ domain: ctx.domain,
603
+ });
604
+ const { intent: _intent, ...prepared } = await prepareManagedRefreshSend({
605
+ ...input,
606
+ profile: ctx.user.profile,
607
+ targetProfile,
608
+ domain: ctx.domain,
609
+ });
610
+
611
+ return prepared;
612
+ }),
613
+
246
614
  getBoostAlignments: profileRoute
247
615
  .meta({
248
616
  openapi: {
@@ -692,6 +1060,7 @@ export const boostsRouter = t.router({
692
1060
  // Log credential activity FIRST to get activityId for chaining
693
1061
  const activityId = await logCredentialSent({
694
1062
  actorProfileId: profile.profileId,
1063
+ onBehalfOf: ctx.user.onBehalfOf,
695
1064
  recipientType: 'profile',
696
1065
  recipientIdentifier: targetProfile.profileId,
697
1066
  recipientProfileId: targetProfile.profileId,
@@ -736,6 +1105,147 @@ export const boostsRouter = t.router({
736
1105
  const { contractUri } = input;
737
1106
  const { domain } = ctx;
738
1107
 
1108
+ // LC-2198: managed refresh requests are validated in full BEFORE any
1109
+ // mutation — an unsupported refresh send must not create a boost,
1110
+ // allocation, inbox entry, activity, or delivery.
1111
+ const inboxRefreshRequested =
1112
+ input.refresh === true && !!isInboxRecipient(input.recipient);
1113
+ const refreshRequested = input.refresh === true && !inboxRefreshRequested;
1114
+ let inboxIntent: RefreshSendIntent | undefined;
1115
+ const inboxDigest = inboxRefreshRequested
1116
+ ? inboxRefreshRequestDigest(input)
1117
+ : undefined;
1118
+ const inboxKey = input.idempotencyKey
1119
+ ? `send:${input.idempotencyKey}`
1120
+ : undefined;
1121
+ if (inboxRefreshRequested) {
1122
+ await assertInboxRefreshEnabled(ctx.user.scope);
1123
+ if (input.signedCredential || input.contractUri)
1124
+ throw new TRPCError({
1125
+ code: 'BAD_REQUEST',
1126
+ message:
1127
+ 'Inbox refresh requires unsigned content and does not support a consent contract before holder binding.',
1128
+ });
1129
+ if (input.idempotencyKey) {
1130
+ const claim = await claimRefreshSendIntent({
1131
+ issuerProfileId: profile.profileId,
1132
+ idempotencyKey: input.idempotencyKey,
1133
+ requestDigest: inboxDigest!,
1134
+ });
1135
+ if (claim.kind === 'delivered' && claim.intent.result)
1136
+ return claim.intent.result;
1137
+ inboxIntent = claim.intent;
1138
+ const replay = await getInboxRefreshReplay(
1139
+ profile.profileId,
1140
+ inboxKey,
1141
+ inboxDigest!
1142
+ );
1143
+ if (
1144
+ replay &&
1145
+ (replay.claimUrl || replay.inbox.currentStatus !== 'PENDING')
1146
+ ) {
1147
+ await resumeInboxRefreshDelivery(replay.inbox.refreshId!, domain);
1148
+ const result = {
1149
+ type: 'boost' as const,
1150
+ uri: inboxIntent.boostUri!,
1151
+ credentialUri: '',
1152
+ activityId: replay.inbox.activityId ?? '',
1153
+ inbox: {
1154
+ issuanceId: replay.inbox.id,
1155
+ status: replay.inbox.currentStatus,
1156
+ claimUrl: replay.claimUrl,
1157
+ guardianStatus: replay.inbox.guardianStatus,
1158
+ refresh: await getInboxRefreshReceipt(
1159
+ replay.inbox.refreshId!,
1160
+ domain
1161
+ ),
1162
+ },
1163
+ };
1164
+ await markRefreshSendIntentDelivered(inboxIntent, result);
1165
+ return result;
1166
+ }
1167
+ }
1168
+ }
1169
+ let refreshTargetProfile: ProfileType | null = null;
1170
+
1171
+ if (input.idempotencyKey && !input.refresh) {
1172
+ throw new TRPCError({
1173
+ code: 'BAD_REQUEST',
1174
+ message: 'idempotencyKey is only supported with refresh: true.',
1175
+ });
1176
+ }
1177
+
1178
+ if (refreshRequested) {
1179
+ refreshTargetProfile = await validateRefreshSendRecipient({
1180
+ profile,
1181
+ scope: ctx.user.scope,
1182
+ recipient: input.recipient,
1183
+ domain,
1184
+ });
1185
+
1186
+ if (input.signedCredential) {
1187
+ // Reject signed credentials without a local managed allocation
1188
+ // before any mutation (e.g. boost auto-creation). No unsigned
1189
+ // fallback and no second allocation (plan decision 6).
1190
+ const handoff = extractManagedRefreshHandoff(
1191
+ input.signedCredential,
1192
+ domain
1193
+ );
1194
+
1195
+ if (!handoff) {
1196
+ throw new TRPCError({
1197
+ code: 'BAD_REQUEST',
1198
+ message:
1199
+ 'A signed credential sent with refresh must already contain its allocated managed refresh service.',
1200
+ });
1201
+ }
1202
+
1203
+ if (input.idempotencyKey) {
1204
+ const intent = await getRefreshSendIntent(
1205
+ profile.profileId,
1206
+ input.idempotencyKey
1207
+ );
1208
+ if (!intent) {
1209
+ throw new TRPCError({
1210
+ code: 'BAD_REQUEST',
1211
+ message:
1212
+ 'idempotencyKey with a pre-signed credential requires a prior boost.prepareRefreshableSend call (tRPC only). For a directly signed REST send, omit idempotencyKey and retry the same signed credential and templateUri.',
1213
+ });
1214
+ }
1215
+ if (
1216
+ intent.refreshId !== handoff.refreshId ||
1217
+ (input.templateUri && intent.boostUri !== input.templateUri)
1218
+ ) {
1219
+ throw new TRPCError({
1220
+ code: 'CONFLICT',
1221
+ message:
1222
+ 'This idempotencyKey does not match the prepared refresh send for this credential.',
1223
+ });
1224
+ }
1225
+ }
1226
+ }
1227
+ }
1228
+
1229
+ // Keyed managed send without a pre-signed credential: preparation (boost +
1230
+ // allocation) goes through the idempotency intent instead of the generic
1231
+ // boost creation below.
1232
+ const keyedPrepared =
1233
+ refreshRequested && input.idempotencyKey && !input.signedCredential
1234
+ ? await prepareManagedRefreshSend({
1235
+ profile,
1236
+ targetProfile: refreshTargetProfile!,
1237
+ domain,
1238
+ templateUri: input.templateUri,
1239
+ template: input.templateUri ? undefined : input.template,
1240
+ contractUri,
1241
+ templateData: input.templateData,
1242
+ integrationId: input.integrationId,
1243
+ idempotencyKey: input.idempotencyKey,
1244
+ })
1245
+ : undefined;
1246
+
1247
+ if (keyedPrepared?.completed) return keyedPrepared.completed;
1248
+
739
1249
  // Check if recipient is email/phone (routes to Universal Inbox)
740
1250
  const inboxRecipient = isInboxRecipient(input.recipient);
741
1251
 
@@ -744,7 +1254,22 @@ export const boostsRouter = t.router({
744
1254
  let boostUri = '';
745
1255
  let boostCreated = false;
746
1256
 
747
- if (input.templateUri) {
1257
+ if (inboxIntent?.boostUri) {
1258
+ boost = await getBoostByUri(inboxIntent.boostUri);
1259
+ boostUri = inboxIntent.boostUri;
1260
+ } else if (keyedPrepared) {
1261
+ const resolved = await traceDb('getBoostByUri', () =>
1262
+ getBoostByUri(keyedPrepared.boostUri)
1263
+ );
1264
+ if (!resolved) {
1265
+ throw new TRPCError({
1266
+ code: 'NOT_FOUND',
1267
+ message: 'Could not find boost',
1268
+ });
1269
+ }
1270
+ boost = resolved;
1271
+ boostUri = keyedPrepared.boostUri;
1272
+ } else if (input.templateUri) {
748
1273
  const resolved = await traceDb('getBoostByUri', () =>
749
1274
  getBoostByUri(input.templateUri!)
750
1275
  );
@@ -757,47 +1282,77 @@ export const boostsRouter = t.router({
757
1282
  boost = resolved;
758
1283
  boostUri = input.templateUri;
759
1284
  } else if (input.template) {
760
- const { credential, claimPermissions, skills, ...metadata } =
761
- input.template;
762
-
763
- boost = await traceDb('createBoost', () =>
764
- createBoost(credential, profile, metadata, domain)
765
- );
766
-
767
- if (Array.isArray(skills) && skills.length > 0) {
768
- await traceDb('addAlignedSkillsToBoost', () =>
769
- addAlignedSkillsToBoost(boost!, skills)
770
- );
771
- }
772
-
773
- if (claimPermissions) {
774
- await traceDb('addClaimPermissionsForBoost', () =>
775
- addClaimPermissionsForBoost(boost!, {
776
- ...EMPTY_PERMISSIONS,
777
- ...claimPermissions,
778
- })
779
- );
780
- }
1285
+ boost = await createInlineBoostForSend(input.template, profile, domain);
781
1286
 
782
1287
  boostUri = getBoostUri(boost.id, domain);
783
1288
  boostCreated = true;
784
1289
  } else if (input.signedCredential) {
785
- // Auto-create boost from the signed credential
786
- const credential = input.signedCredential as Record<string, unknown>;
787
- const name =
788
- typeof credential.name === 'string' ? credential.name : undefined;
789
-
790
- boost = await traceDb('createBoost:fromSignedCredential', () =>
791
- createBoost(
792
- input.signedCredential!,
793
- profile,
794
- { ...(name ? { name } : {}) },
795
- domain
796
- )
797
- );
1290
+ // A replayed managed handoff reuses the boost its refresh is already
1291
+ // bound to (the pre-mutation guard validated the handoff above).
1292
+ const boundBoostId = refreshRequested
1293
+ ? await traceDb('getBoundRefreshBoostId', () =>
1294
+ getBoundRefreshBoostId(
1295
+ extractManagedRefreshHandoff(input.signedCredential!, domain)!
1296
+ .refreshId
1297
+ )
1298
+ )
1299
+ : undefined;
1300
+ const boundBoost = boundBoostId
1301
+ ? await traceDb('getBoostById:boundRefresh', () =>
1302
+ getBoostById(boundBoostId)
1303
+ )
1304
+ : null;
1305
+ // A keyed handoff whose preparation already created/resolved a
1306
+ // boost (but never delivered) reuses that prepared boost.
1307
+ const preparedBoostUri =
1308
+ !boundBoost && refreshRequested && input.idempotencyKey
1309
+ ? (
1310
+ await traceDb('getRefreshSendIntent:signedCredential', () =>
1311
+ getRefreshSendIntent(
1312
+ profile.profileId,
1313
+ input.idempotencyKey!
1314
+ )
1315
+ )
1316
+ )?.boostUri
1317
+ : undefined;
1318
+ const preparedBoost = preparedBoostUri
1319
+ ? await traceDb('getBoostByUri:preparedRefresh', () =>
1320
+ getBoostByUri(preparedBoostUri)
1321
+ )
1322
+ : null;
1323
+
1324
+ if (boundBoost) {
1325
+ boost = boundBoost;
1326
+ boostUri = getBoostUri(boundBoost.id, domain);
1327
+ } else if (preparedBoost && preparedBoostUri) {
1328
+ boost = preparedBoost;
1329
+ boostUri = preparedBoostUri;
1330
+ } else {
1331
+ // Auto-create boost from the signed credential. A managed refresh
1332
+ // service belongs to one holder's credential, never to a reusable
1333
+ // template, so it is not stored on the boost.
1334
+ const { refreshService: _refreshService, ...templateCredential } =
1335
+ input.signedCredential as Record<string, unknown>;
1336
+ const name =
1337
+ typeof templateCredential.name === 'string'
1338
+ ? templateCredential.name
1339
+ : undefined;
1340
+
1341
+ boost = await traceDb('createBoost:fromSignedCredential', () =>
1342
+ createBoost(
1343
+ (refreshRequested
1344
+ ? templateCredential
1345
+ : input.signedCredential!) as typeof input.signedCredential &
1346
+ object,
1347
+ profile,
1348
+ { ...(name ? { name } : {}) },
1349
+ domain
1350
+ )
1351
+ );
798
1352
 
799
- boostUri = getBoostUri(boost.id, domain);
800
- boostCreated = true;
1353
+ boostUri = getBoostUri(boost.id, domain);
1354
+ boostCreated = true;
1355
+ }
801
1356
  }
802
1357
 
803
1358
  if (!boost) {
@@ -827,6 +1382,298 @@ export const boostsRouter = t.router({
827
1382
  });
828
1383
  }
829
1384
 
1385
+ if (inboxIntent?.state === 'preparing')
1386
+ inboxIntent = await recordRefreshSendIntent(inboxIntent, {
1387
+ boostUri,
1388
+ state: 'prepared',
1389
+ });
1390
+
1391
+ // LC-2198: managed refresh delivery. The recipient was fully validated
1392
+ // in the pre-mutation guard above (feature, scope, supported recipient,
1393
+ // blocklist); the boost passed the same issue-permission and draft
1394
+ // checks as a normal send.
1395
+ if (refreshRequested) {
1396
+ return trace('route', 'sendRefreshable', async () => {
1397
+ const targetProfile = refreshTargetProfile!;
1398
+ let intent: RefreshSendIntent | undefined = keyedPrepared?.intent;
1399
+
1400
+ const contractTerms = await resolveContractForSend({
1401
+ profile,
1402
+ targetProfile,
1403
+ boost: boost!,
1404
+ boostCreated,
1405
+ contractUri,
1406
+ });
1407
+
1408
+ // Matches the unified send's self-send notification behavior.
1409
+ const skipNotification = profile.profileId === targetProfile.profileId;
1410
+
1411
+ let signedVc: VC;
1412
+ let refreshId: string;
1413
+
1414
+ if (input.signedCredential) {
1415
+ // Preallocated handoff (plan decision 6): the caller already
1416
+ // allocated and embedded the managed service before signing.
1417
+ // Ownership, holder, ID, boost anchor, proof, and descriptor
1418
+ // are enforced by the managed send helper.
1419
+ const handoff = extractManagedRefreshHandoff(
1420
+ input.signedCredential,
1421
+ domain
1422
+ )!;
1423
+ signedVc = input.signedCredential;
1424
+ refreshId = handoff.refreshId;
1425
+
1426
+ if (input.idempotencyKey) {
1427
+ const existing = await getRefreshSendIntent(
1428
+ profile.profileId,
1429
+ input.idempotencyKey
1430
+ );
1431
+
1432
+ if (
1433
+ !existing ||
1434
+ existing.refreshId !== refreshId ||
1435
+ existing.boostUri !== boostUri
1436
+ ) {
1437
+ throw new TRPCError({
1438
+ code: 'CONFLICT',
1439
+ message:
1440
+ 'This idempotencyKey does not match the prepared refresh send for this credential.',
1441
+ });
1442
+ }
1443
+
1444
+ if (existing.state === 'delivered' && existing.result) {
1445
+ return existing.result;
1446
+ }
1447
+
1448
+ intent = existing;
1449
+ const reconciled = await reconcileBoundRefreshSendIntent(
1450
+ existing,
1451
+ domain
1452
+ );
1453
+ if (reconciled) return reconciled;
1454
+ }
1455
+
1456
+ // Resume detection: reuse the original delivery activity
1457
+ // instead of duplicating issuance or activity when the exact
1458
+ // same credential is redelivered (e.g. a network retry after
1459
+ // binding already succeeded).
1460
+ const binding = await traceInternal(
1461
+ 'peekCredentialRefreshInitialBinding',
1462
+ () =>
1463
+ peekCredentialRefreshInitialBinding({
1464
+ refreshId,
1465
+ credential: signedVc,
1466
+ boostId: boost!.id,
1467
+ })
1468
+ );
1469
+
1470
+ if (binding.bound) {
1471
+ const { uri, receipt } = await sendRefreshableCredential({
1472
+ issuerProfile: profile,
1473
+ refreshId,
1474
+ credential: signedVc,
1475
+ boostUri,
1476
+ skipNotification,
1477
+ domain,
1478
+ activityId: binding.activityId,
1479
+ integrationId: input.integrationId,
1480
+ contractTerms: contractTerms ?? undefined,
1481
+ });
1482
+
1483
+ const response = {
1484
+ type: 'boost' as const,
1485
+ credentialUri: uri,
1486
+ uri: boostUri,
1487
+ activityId: binding.activityId ?? '',
1488
+ refresh: receipt,
1489
+ };
1490
+
1491
+ if (intent)
1492
+ await markRefreshSendIntentDelivered(intent, response);
1493
+
1494
+ return response;
1495
+ }
1496
+ } else {
1497
+ if (keyedPrepared && intent) {
1498
+ const reconciled = await reconcileBoundRefreshSendIntent(
1499
+ intent,
1500
+ domain
1501
+ );
1502
+
1503
+ if (reconciled) return reconciled;
1504
+ }
1505
+
1506
+ const signingAuthority = await traceDb(
1507
+ 'getPrimarySigningAuthorityForUser:refresh',
1508
+ () => getPrimarySigningAuthorityForUser(profile)
1509
+ );
1510
+
1511
+ if (!signingAuthority) {
1512
+ throw new TRPCError({
1513
+ code: 'PRECONDITION_FAILED',
1514
+ message:
1515
+ 'You must register a signing authority before using send without a pre-signed credential. Please register one via registerSigningAuthority or sign the credential client-side.',
1516
+ });
1517
+ }
1518
+
1519
+ let unsignedVc: UnsignedVC;
1520
+
1521
+ try {
1522
+ unsignedVc = await traceInternal(
1523
+ 'prepareCredentialFromBoost:refresh',
1524
+ () =>
1525
+ prepareCredentialFromBoost(boost!, boostUri, domain, {
1526
+ templateData: input.templateData as Record<
1527
+ string,
1528
+ unknown
1529
+ >,
1530
+ issuerDid: signingAuthority.relationship.did,
1531
+ recipientDid:
1532
+ keyedPrepared?.holderDid ??
1533
+ getDidWeb(domain, targetProfile.profileId),
1534
+ recipientName: targetProfile.displayName,
1535
+ })
1536
+ );
1537
+ } catch (e) {
1538
+ console.error('Failed to prepare boost credential', e);
1539
+ throw new TRPCError({
1540
+ code: 'INTERNAL_SERVER_ERROR',
1541
+ message: 'Failed to prepare boost credential',
1542
+ });
1543
+ }
1544
+
1545
+ // A stable credential ID must exist before allocation: the
1546
+ // refresh aggregate is permanently bound to it.
1547
+ unsignedVc.id =
1548
+ keyedPrepared?.credentialId ??
1549
+ unsignedVc.id ??
1550
+ `urn:uuid:${uuid()}`;
1551
+
1552
+ // A keyed send already allocated its refresh during preparation;
1553
+ // otherwise allocate once, then inject the managed service +
1554
+ // inline JSON-LD context so both become part of the signed payload.
1555
+ const allocation = keyedPrepared
1556
+ ? {
1557
+ refreshId: keyedPrepared.refreshId,
1558
+ refreshService: keyedPrepared.refreshService,
1559
+ }
1560
+ : await traceInternal('allocateCredentialRefresh:send', () =>
1561
+ allocateCredentialRefresh({
1562
+ issuerProfile: profile,
1563
+ holderProfile: targetProfile,
1564
+ holderDid: getDidWeb(domain, targetProfile.profileId),
1565
+ credentialId: unsignedVc.id!,
1566
+ domain,
1567
+ })
1568
+ );
1569
+
1570
+ refreshId = allocation.refreshId;
1571
+
1572
+ const prepared = injectManagedRefreshService(
1573
+ unsignedVc,
1574
+ allocation.refreshService
1575
+ );
1576
+
1577
+ signedVc = await traceInternal(
1578
+ 'issueCredentialWithSigningAuthority:refresh',
1579
+ async () =>
1580
+ (
1581
+ await issueCredentialWithSigningAuthority(
1582
+ { type: 'profile', profile },
1583
+ prepared,
1584
+ signingAuthority,
1585
+ domain,
1586
+ false
1587
+ )
1588
+ ).credential as VC
1589
+ );
1590
+ }
1591
+
1592
+ // Plaintext templateData is intentionally omitted from refresh
1593
+ // activity metadata: refresh records never persist claim content.
1594
+ const activityId = await traceDb('logCredentialSent:refresh', () =>
1595
+ logCredentialSent({
1596
+ actorProfileId: profile.profileId,
1597
+ onBehalfOf: ctx.user.onBehalfOf,
1598
+ recipientType: 'profile',
1599
+ recipientIdentifier: targetProfile.profileId,
1600
+ recipientProfileId: targetProfile.profileId,
1601
+ boostUri,
1602
+ source: 'send',
1603
+ integrationId: input.integrationId,
1604
+ })
1605
+ );
1606
+
1607
+ if (intent) {
1608
+ await recordRefreshSendIntentPending(
1609
+ intent,
1610
+ {
1611
+ refreshId,
1612
+ refreshService: managedRefreshServiceFor(refreshId, domain),
1613
+ credentialId: signedVc.id as string,
1614
+ issuerDid:
1615
+ getCredentialIssuerId(signedVc) ??
1616
+ getDidWeb(domain, profile.profileId),
1617
+ holderDid:
1618
+ intent.holderDid ??
1619
+ getDidWeb(domain, targetProfile.profileId),
1620
+ ...(signedVc.credentialStatus
1621
+ ? { credentialStatus: signedVc.credentialStatus }
1622
+ : {}),
1623
+ },
1624
+ activityId
1625
+ );
1626
+ }
1627
+
1628
+ try {
1629
+ const { uri, receipt } = await sendRefreshableCredential({
1630
+ issuerProfile: profile,
1631
+ refreshId,
1632
+ credential: signedVc,
1633
+ boostUri,
1634
+ skipNotification,
1635
+ domain,
1636
+ activityId,
1637
+ integrationId: input.integrationId,
1638
+ contractTerms: contractTerms ?? undefined,
1639
+ });
1640
+
1641
+ const response = {
1642
+ type: 'boost' as const,
1643
+ credentialUri: uri,
1644
+ uri: boostUri,
1645
+ activityId,
1646
+ refresh: receipt,
1647
+ };
1648
+
1649
+ if (intent) await markRefreshSendIntentDelivered(intent, response);
1650
+
1651
+ return response;
1652
+ } catch (error) {
1653
+ await traceDb('logCredentialFailed:refresh', () =>
1654
+ logCredentialFailed({
1655
+ activityId,
1656
+ actorProfileId: profile.profileId,
1657
+ onBehalfOf: ctx.user.onBehalfOf,
1658
+ recipientType: 'profile',
1659
+ recipientIdentifier: targetProfile.profileId,
1660
+ recipientProfileId: targetProfile.profileId,
1661
+ boostUri,
1662
+ integrationId: input.integrationId,
1663
+ source: 'send',
1664
+ metadata: {
1665
+ error:
1666
+ error instanceof Error
1667
+ ? error.message
1668
+ : 'Unknown error',
1669
+ },
1670
+ })
1671
+ );
1672
+ throw error;
1673
+ }
1674
+ });
1675
+ }
1676
+
830
1677
  // Route to Universal Inbox for email/phone recipients
831
1678
  if (inboxRecipient) {
832
1679
  // Try to resolve recipient profile for auto-populating template variables + recipient DID
@@ -883,11 +1730,12 @@ export const boostsRouter = t.router({
883
1730
  });
884
1731
  }
885
1732
 
886
- credential = await appendBitstringStatusListEntries(
887
- credential,
888
- profile.profileId,
889
- domain
890
- );
1733
+ if (!inboxRefreshRequested)
1734
+ credential = await appendBitstringStatusListEntries(
1735
+ credential,
1736
+ profile.profileId,
1737
+ domain
1738
+ );
891
1739
  }
892
1740
 
893
1741
  // Build inbox configuration from SendOptions
@@ -895,6 +1743,7 @@ export const boostsRouter = t.router({
895
1743
  const activityId = await traceDb('logCredentialSent:inbox', () =>
896
1744
  logCredentialSent({
897
1745
  actorProfileId: profile.profileId,
1746
+ onBehalfOf: ctx.user.onBehalfOf,
898
1747
  recipientType: inboxRecipient.type,
899
1748
  recipientIdentifier: inboxRecipient.value,
900
1749
  boostUri,
@@ -914,6 +1763,9 @@ export const boostsRouter = t.router({
914
1763
  credential,
915
1764
  {
916
1765
  ...inboxConfig,
1766
+ refresh: inboxRefreshRequested,
1767
+ idempotencyKey: inboxKey,
1768
+ refreshRequestDigest: inboxDigest,
917
1769
  activityId,
918
1770
  integrationId: input.integrationId,
919
1771
  },
@@ -921,12 +1773,15 @@ export const boostsRouter = t.router({
921
1773
  )
922
1774
  );
923
1775
 
924
- return {
1776
+ const result = {
925
1777
  type: 'boost' as const,
926
1778
  credentialUri: '',
927
1779
  uri: boostUri,
928
- activityId,
1780
+ activityId: inboxResult.inboxCredential.activityId ?? activityId,
929
1781
  inbox: {
1782
+ ...(inboxResult.refresh
1783
+ ? { refresh: inboxResult.refresh }
1784
+ : {}),
930
1785
  issuanceId: inboxResult.inboxCredential.id,
931
1786
  status: inboxResult.status,
932
1787
  claimUrl: inboxResult.claimUrl,
@@ -935,12 +1790,16 @@ export const boostsRouter = t.router({
935
1790
  : {}),
936
1791
  },
937
1792
  };
1793
+ if (inboxIntent)
1794
+ await markRefreshSendIntentDelivered(inboxIntent, result);
1795
+ return result;
938
1796
  } catch (error) {
939
1797
  // Log FAILED activity when issueToInbox fails
940
1798
  await traceDb('logCredentialFailed:inbox', () =>
941
1799
  logCredentialFailed({
942
1800
  activityId,
943
1801
  actorProfileId: profile.profileId,
1802
+ onBehalfOf: ctx.user.onBehalfOf,
944
1803
  recipientType: inboxRecipient.type,
945
1804
  recipientIdentifier: inboxRecipient.value,
946
1805
  boostUri,
@@ -1029,6 +1888,7 @@ export const boostsRouter = t.router({
1029
1888
  const activityId = await traceDb('logCredentialSent:remoteInbox', () =>
1030
1889
  logCredentialSent({
1031
1890
  actorProfileId: profile.profileId,
1891
+ onBehalfOf: ctx.user.onBehalfOf,
1032
1892
  recipientType: 'profile',
1033
1893
  recipientIdentifier: input.recipient,
1034
1894
  boostUri,
@@ -1071,6 +1931,7 @@ export const boostsRouter = t.router({
1071
1931
  logCredentialFailed({
1072
1932
  activityId,
1073
1933
  actorProfileId: profile.profileId,
1934
+ onBehalfOf: ctx.user.onBehalfOf,
1074
1935
  recipientType: 'profile',
1075
1936
  recipientIdentifier: input.recipient,
1076
1937
  boostUri,
@@ -1130,53 +1991,13 @@ export const boostsRouter = t.router({
1130
1991
  });
1131
1992
  }
1132
1993
 
1133
- let contractTerms = null as Awaited<
1134
- ReturnType<typeof getContractTermsForProfile>
1135
- > | null;
1136
- let decodedContractUri: string | null = null;
1137
- let contractDetails: Awaited<
1138
- ReturnType<typeof getContractDetailsByUri>
1139
- > | null = null;
1140
-
1141
- if (contractUri) {
1142
- decodedContractUri = decodeURIComponent(contractUri);
1143
- contractDetails = await traceDb('getContractDetailsByUri', () =>
1144
- getContractDetailsByUri(decodedContractUri!)
1145
- );
1146
-
1147
- if (!contractDetails) {
1148
- throw new TRPCError({
1149
- code: 'NOT_FOUND',
1150
- message: 'Could not find contract',
1151
- });
1152
- }
1153
-
1154
- const terms = await traceDb('getContractTermsForProfile', () =>
1155
- getContractTermsForProfile(targetProfile, contractDetails!.contract)
1156
- );
1157
-
1158
- const writers = await traceDb('getWritersForContract', () =>
1159
- getWritersForContract(contractDetails!.contract)
1160
- );
1161
- const isWriter = writers.some(
1162
- writer => writer.profileId === profile.profileId
1163
- );
1164
- const isDenied =
1165
- terms?.terms.deniedWriters?.includes(profile.profileId) ?? false;
1166
- const categoryAllowed = boost.category
1167
- ? terms?.terms.write?.credentials?.categories?.[boost.category] === true
1168
- : true;
1169
-
1170
- if (terms && isWriter && !isDenied && categoryAllowed) {
1171
- contractTerms = terms;
1172
- }
1173
- }
1174
-
1175
- if (boostCreated && contractDetails) {
1176
- await traceDb('setRelatedBoostForContract', () =>
1177
- setRelatedBoostForContract(contractDetails!.contract, boost!)
1178
- );
1179
- }
1994
+ const contractTerms = await resolveContractForSend({
1995
+ profile,
1996
+ targetProfile: targetProfile!,
1997
+ boost: boost!,
1998
+ boostCreated,
1999
+ contractUri,
2000
+ });
1180
2001
 
1181
2002
  let signedVc: VC | JWE;
1182
2003
 
@@ -1240,6 +2061,7 @@ export const boostsRouter = t.router({
1240
2061
  const activityId = await traceDb('logCredentialSent', () =>
1241
2062
  logCredentialSent({
1242
2063
  actorProfileId: profile.profileId,
2064
+ onBehalfOf: ctx.user.onBehalfOf,
1243
2065
  recipientType: 'profile',
1244
2066
  recipientIdentifier: targetProfile.profileId,
1245
2067
  recipientProfileId: targetProfile.profileId,
@@ -1270,6 +2092,7 @@ export const boostsRouter = t.router({
1270
2092
  logCredentialFailed({
1271
2093
  activityId,
1272
2094
  actorProfileId: profile.profileId,
2095
+ onBehalfOf: ctx.user.onBehalfOf,
1273
2096
  recipientType: 'profile',
1274
2097
  recipientIdentifier: targetProfile.profileId,
1275
2098
  recipientProfileId: targetProfile.profileId,
@@ -3727,17 +4550,7 @@ export const boostsRouter = t.router({
3727
4550
  }
3728
4551
  unsignedVc.issuer = { id: getDidWeb(ctx.domain, profile.profileId) };
3729
4552
 
3730
- if (Array.isArray(unsignedVc.credentialSubject)) {
3731
- unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
3732
- ...subject,
3733
- id: getDidWeb(ctx.domain, targetProfile.profileId),
3734
- }));
3735
- } else {
3736
- unsignedVc.credentialSubject = {
3737
- ...unsignedVc.credentialSubject,
3738
- id: getDidWeb(ctx.domain, targetProfile.profileId),
3739
- };
3740
- }
4553
+ setCredentialSubjectIds(unsignedVc, getDidWeb(ctx.domain, targetProfile.profileId));
3741
4554
  if (unsignedVc?.type?.includes('BoostCredential')) unsignedVc.boostId = boostUri;
3742
4555
  // Inject OBv3 skill alignments based on boost's framework/skills
3743
4556
  await injectObv3AlignmentsIntoCredentialForBoost(unsignedVc, boost, ctx.domain);