@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,4 +1,19 @@
1
+ import { environment } from '@environment';
2
+ import { recordInboxRefreshClaimUrl } from '@accesslayer/inbox-credential/refresh';
3
+ import { randomUUID } from 'node:crypto';
4
+ import {
5
+ assertInboxRefreshEnabled,
6
+ prepareInboxRefresh,
7
+ getInboxRefreshReceipt,
8
+ finalizeInboxRefresh,
9
+ getInboxRefreshIssueKey,
10
+ resumeInboxRefreshDelivery,
11
+ } from './inbox-refresh.helpers';
1
12
  import { TRPCError } from '@trpc/server';
13
+ import {
14
+ InboxDeliveryCheckpointError,
15
+ InboxIssuancePreflightError,
16
+ } from './inbox-issuance-error.helpers';
2
17
  import {
3
18
  VC,
4
19
  UnsignedVC,
@@ -32,7 +47,7 @@ import { doesProfileManageProfile } from '@accesslayer/profile-manager/relations
32
47
  import { getProfilesThatManageAProfile } from '@accesslayer/profile/relationships/read';
33
48
  import { getProfileForInboxCredential } from '@accesslayer/inbox-credential/read';
34
49
  import { sendCredential } from '@helpers/credential.helpers';
35
- import { sendBoost } from '@helpers/boost.helpers';
50
+ import { prepareCredentialFromBoost, getBoostUri, sendBoost } from '@helpers/boost.helpers';
36
51
  import { getBoostByUri } from '@accesslayer/boost/read';
37
52
  import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
38
53
  import { getSigningAuthorityForUserByName } from '@accesslayer/signing-authority/relationships/read';
@@ -192,9 +207,15 @@ export const issueToInbox = async (
192
207
  activityId?: string;
193
208
  integrationId?: string;
194
209
  guardianEmail?: string;
210
+ refresh?: boolean;
211
+ idempotencyKey?: string;
212
+ refreshRequestDigest?: string;
195
213
  } = {},
196
- ctx: Context
214
+ ctx: Context,
215
+ /** Queue ownership checkpoint immediately before delivery or inbox persistence. */
216
+ beforeDelivery?: () => Promise<void>
197
217
  ): Promise<{
218
+ refresh?: import('@learncard/types').InboxCredentialRefreshReceipt;
198
219
  status: 'PENDING' | 'ISSUED' | 'EXPIRED' | 'DELIVERED' | 'CLAIMED'; // DELIVERED & CLAIMED are deprecated, use ISSUED
199
220
  inboxCredential: InboxCredentialType;
200
221
  claimUrl?: string;
@@ -211,13 +232,35 @@ export const issueToInbox = async (
211
232
  integrationId,
212
233
  guardianEmail,
213
234
  } = configuration;
235
+ if (configuration.refresh) {
236
+ await assertInboxRefreshEnabled(ctx.user?.scope);
237
+ if (
238
+ credential.proof ||
239
+ credential.type.includes('VerifiablePresentation') ||
240
+ Array.isArray(credential.credentialSubject)
241
+ ) {
242
+ throw new TRPCError({
243
+ code: 'BAD_REQUEST',
244
+ message:
245
+ 'Inbox refresh requires an unsigned credential with a single subject and a signing authority.',
246
+ });
247
+ }
248
+ }
214
249
  const isSigned = !!credential?.proof;
215
250
  let signingAuthority: IssueInboxSigningAuthority | undefined = _signingAuthority;
251
+ let deliveryCheckpointReached = beforeDelivery === undefined;
252
+ const checkpointDelivery = async (): Promise<void> => {
253
+ await beforeDelivery?.();
254
+ deliveryCheckpointReached = true;
255
+ };
256
+ const assertDeliveryCheckpoint = (result: InboxDeliveryCheckpointError['result']): void => {
257
+ if (!deliveryCheckpointReached) throw new InboxDeliveryCheckpointError(result);
258
+ };
216
259
 
217
260
  if (recipient.type === 'phone') {
218
261
  const isTrusted = await getRegistryService().isTrusted(issuerProfile.did);
219
262
  if (!isTrusted) {
220
- throw new TRPCError({
263
+ throw new InboxIssuancePreflightError({
221
264
  code: 'FORBIDDEN',
222
265
  message:
223
266
  'Sending credentials via phone is a feature reserved for members of the LearnCard Trusted Registry. Email delivery is available for all issuers. To verify your issuer, visit: https://docs.learncard.com/how-to-guides/verify-my-issuer',
@@ -245,20 +288,35 @@ export const issueToInbox = async (
245
288
  * By providing this parameter, the developer explicitly takes responsibility for the signing process. We trust them and proceed.
246
289
  **/
247
290
  if (!(await verifyCredentialCanBeSigned(credential as UnsignedVC))) {
248
- throw new TRPCError({
291
+ throw new InboxIssuancePreflightError({
249
292
  code: 'BAD_REQUEST',
250
293
  message:
251
294
  'Credential failed to pass a pre-flight issuance test. Please verify that the credential is well-formed and can be issued.',
252
295
  });
253
296
  }
254
297
  } else {
255
- throw new TRPCError({
298
+ throw new InboxIssuancePreflightError({
256
299
  code: 'BAD_REQUEST',
257
300
  message: 'Unsigned credentials require a signing authority',
258
301
  });
259
302
  }
260
303
  }
261
304
 
305
+ if (
306
+ configuration.refresh &&
307
+ (!signingAuthority ||
308
+ !(await getSigningAuthorityForUserByName(
309
+ issuerProfile,
310
+ signingAuthority.endpoint,
311
+ signingAuthority.name
312
+ )))
313
+ ) {
314
+ throw new TRPCError({
315
+ code: 'NOT_FOUND',
316
+ message: 'Signing authority not found for issuer',
317
+ });
318
+ }
319
+
262
320
  // Check if recipient already exists with verified email
263
321
  const existingProfile = await getProfileByVerifiedContactMethod(
264
322
  recipient.type,
@@ -267,12 +325,14 @@ export const issueToInbox = async (
267
325
 
268
326
  // Check if the recipient is a managed child (has a guardian via MANAGES relationship).
269
327
  // Managed children need guardian approval for all inbox credentials, even without explicit guardianEmail.
270
- const recipientManagers = existingProfile
271
- ? await getProfilesThatManageAProfile(existingProfile.profileId)
272
- : [];
328
+ // Service profiles (orgs) are managed for admin purposes, not guardianship.
329
+ const recipientManagers =
330
+ existingProfile && !existingProfile.isServiceProfile
331
+ ? await getProfilesThatManageAProfile(existingProfile.profileId)
332
+ : [];
273
333
  const recipientIsManaged = recipientManagers.length > 0;
274
334
 
275
- if (existingProfile && !guardianEmail && !recipientIsManaged) {
335
+ if (existingProfile && !guardianEmail && !recipientIsManaged && !configuration.refresh) {
276
336
  // Auto-deliver to existing user
277
337
  let finalCredential: VC;
278
338
 
@@ -287,7 +347,7 @@ export const issueToInbox = async (
287
347
  );
288
348
 
289
349
  if (!signingAuthorityForUser) {
290
- throw new TRPCError({
350
+ throw new InboxIssuancePreflightError({
291
351
  code: 'NOT_FOUND',
292
352
  message: 'Signing authority not found for issuer',
293
353
  });
@@ -317,31 +377,20 @@ export const issueToInbox = async (
317
377
  credential: encryptedDelivery,
318
378
  statusEntries: getBitstringStatusListEntries(finalCredential),
319
379
  };
320
- if (boostUri) {
321
- const boost = await getBoostByUri(boostUri);
322
- if (boost) {
323
- await sendBoost({
324
- from: { type: 'profile', profile: issuerProfile },
325
- to: existingProfile,
326
- boost,
327
- credential: delivery,
328
- domain: ctx.domain,
329
- activityId,
330
- integrationId,
331
- });
332
- } else {
333
- // Fallback to sendCredential if boost not found
334
- await sendCredential(
335
- issuerProfile,
336
- existingProfile,
337
- delivery,
338
- ctx.domain,
339
- undefined,
340
- activityId,
341
- integrationId
342
- );
343
- }
380
+ const boost = boostUri ? await getBoostByUri(boostUri) : undefined;
381
+ await checkpointDelivery();
382
+ if (boostUri && boost) {
383
+ await sendBoost({
384
+ from: { type: 'profile', profile: issuerProfile },
385
+ to: existingProfile,
386
+ boost,
387
+ credential: delivery,
388
+ domain: ctx.domain,
389
+ activityId,
390
+ integrationId,
391
+ });
344
392
  } else {
393
+ // Fall back to ordinary delivery when no boost exists for the supplied URI.
345
394
  await sendCredential(
346
395
  issuerProfile,
347
396
  existingProfile,
@@ -372,6 +421,7 @@ export const issueToInbox = async (
372
421
  await logCredentialDelivered({
373
422
  activityId,
374
423
  actorProfileId: issuerProfile.profileId,
424
+ onBehalfOf: ctx.user?.onBehalfOf,
375
425
  recipientType: recipient.type as 'email' | 'phone',
376
426
  recipientIdentifier: recipient.value,
377
427
  recipientProfileId: existingProfile.profileId,
@@ -420,6 +470,11 @@ export const issueToInbox = async (
420
470
  });
421
471
  }
422
472
 
473
+ assertDeliveryCheckpoint({
474
+ issuanceId: finalizedInboxCredential.id,
475
+ status: LCNInboxStatusEnumValidator.enum.ISSUED,
476
+ recipientDid: existingProfile.did,
477
+ });
423
478
  return {
424
479
  status: LCNInboxStatusEnumValidator.enum.ISSUED,
425
480
  inboxCredential: finalizedInboxCredential,
@@ -429,7 +484,24 @@ export const issueToInbox = async (
429
484
  // Store in inbox for claiming
430
485
  // Guardian gate if issuer specified guardianEmail OR recipient is a managed child
431
486
  const needsGuardianGate = !!guardianEmail || recipientIsManaged;
487
+ const preparedRefresh = configuration.refresh
488
+ ? await prepareInboxRefresh(credential, issuerProfile, ctx.domain)
489
+ : undefined;
490
+ if (preparedRefresh) credential = preparedRefresh.credential;
491
+ await checkpointDelivery();
432
492
  const inboxCredential = await createInboxCredential({
493
+ ...(preparedRefresh
494
+ ? {
495
+ refresh: {
496
+ aggregate: preparedRefresh.aggregate,
497
+ issueKey: getInboxRefreshIssueKey(
498
+ issuerProfile.profileId,
499
+ configuration.idempotencyKey ?? randomUUID()
500
+ ),
501
+ requestDigest: configuration.refreshRequestDigest!,
502
+ },
503
+ }
504
+ : {}),
433
505
  credential: JSON.stringify(credential),
434
506
  isSigned,
435
507
  recipient,
@@ -444,6 +516,101 @@ export const issueToInbox = async (
444
516
  ...(needsGuardianGate ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
445
517
  });
446
518
 
519
+ const refresh = inboxCredential.refreshId
520
+ ? await getInboxRefreshReceipt(inboxCredential.refreshId, ctx.domain)
521
+ : undefined;
522
+ if (refresh && existingProfile && !needsGuardianGate) {
523
+ await finalizeInboxRefresh({
524
+ inboxId: inboxCredential.id,
525
+ holderDid: existingProfile.did,
526
+ holderProfile: existingProfile,
527
+ domain: ctx.domain,
528
+ accepted: false,
529
+ });
530
+ await resumeInboxRefreshDelivery(refresh.refreshId, ctx.domain);
531
+ await createDeliveredRelationship(
532
+ issuerProfile.profileId,
533
+ inboxCredential.id,
534
+ existingProfile.did,
535
+ 'auto-delivery'
536
+ );
537
+ if (activityId)
538
+ await logCredentialDelivered({
539
+ activityId,
540
+ actorProfileId: issuerProfile.profileId,
541
+ onBehalfOf: ctx.user?.onBehalfOf,
542
+ recipientType: recipient.type,
543
+ recipientIdentifier: recipient.value,
544
+ recipientProfileId: existingProfile.profileId,
545
+ boostUri,
546
+ inboxCredentialId: inboxCredential.id,
547
+ integrationId,
548
+ source: 'send',
549
+ });
550
+ if (webhookUrl)
551
+ await addNotificationToQueue({
552
+ webhookUrl,
553
+ type: LCNNotificationTypeEnumValidator.enum.ISSUANCE_DELIVERED,
554
+ from: { did: (await getLearnCard()).id.did() },
555
+ to: issuerProfile,
556
+ message: getNotificationMessage(
557
+ 'issuanceDelivered',
558
+ resolveRecipientLocale(issuerProfile),
559
+ {
560
+ issuer: issuerProfile.displayName,
561
+ recipientType: recipient.type,
562
+ recipientValue: recipient.value,
563
+ }
564
+ ),
565
+ data: {
566
+ inbox: {
567
+ issuanceId: inboxCredential.id,
568
+ status: LCNInboxStatusEnumValidator.enum.ISSUED,
569
+ recipient: {
570
+ contactMethod: recipient,
571
+ learnCardId: existingProfile.did,
572
+ },
573
+ timestamp: new Date().toISOString(),
574
+ },
575
+ },
576
+ });
577
+ // Existing accounts receive the signed credential in their app inbox.
578
+ // Send the email there too, without a claim token for an already-bound delivery.
579
+ if (recipient.type === 'email' && !delivery?.suppress) {
580
+ try {
581
+ const appUrl = environment.IS_OFFLINE
582
+ ? generateClaimUrl('')
583
+ : ctx.tenant?.emailBranding?.appUrl || generateClaimUrl('');
584
+ await getDeliveryService(recipient).send({
585
+ contactMethod: recipient,
586
+ templateId: 'universal-inbox-claim',
587
+ templateModel: {
588
+ claimUrl: new URL('/notifications', appUrl).href,
589
+ issuer: { name: issuerProfile.displayName },
590
+ credential: { name: credential.name },
591
+ },
592
+ branding: ctx.tenant?.emailBranding,
593
+ locale: resolveRecipientLocale(existingProfile),
594
+ messageStream: 'universal-inbox',
595
+ });
596
+ } catch {
597
+ // The credential and in-app notification are already durable. An
598
+ // email transport failure must not turn that delivery into an error.
599
+ console.warn('Inbox refresh: initial email failed after auto-delivery.');
600
+ }
601
+ }
602
+ return {
603
+ status: 'ISSUED',
604
+ inboxCredential: {
605
+ ...inboxCredential,
606
+ currentStatus: 'ISSUED',
607
+ credential: undefined,
608
+ },
609
+ recipientDid: existingProfile.did,
610
+ refresh: { ...refresh, holderDid: existingProfile.did },
611
+ };
612
+ }
613
+
447
614
  // Generate claim token
448
615
  let recipientContactMethod = await getContactMethodByValue(recipient.type, recipient.value);
449
616
  if (!recipientContactMethod) {
@@ -712,8 +879,9 @@ export const issueToInbox = async (
712
879
 
713
880
  await deliveryService.send({
714
881
  contactMethod: recipient,
715
- templateId: delivery?.template?.id ?? 'universal-inbox',
882
+ templateId: delivery?.template?.id || 'universal-inbox-claim',
716
883
  templateModel: {
884
+ claimUrl: emailClaimUrl,
717
885
  emailClaimUrl,
718
886
  claimToken: emailClaimToken,
719
887
  ...injectedTemplateFields,
@@ -817,13 +985,84 @@ export const issueToInbox = async (
817
985
  expiresInDays ? 24 * expiresInDays : 24,
818
986
  false
819
987
  );
820
- const claimUrl = generateClaimUrl(claimToken);
988
+ const claimUrl = refresh
989
+ ? await recordInboxRefreshClaimUrl(inboxCredential.id, generateClaimUrl(claimToken))
990
+ : generateClaimUrl(claimToken);
821
991
 
992
+ assertDeliveryCheckpoint({
993
+ issuanceId: inboxCredential.id,
994
+ status: LCNInboxStatusEnumValidator.enum.PENDING,
995
+ claimUrl,
996
+ ...(guardianEmail ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
997
+ });
822
998
  return {
823
999
  status: LCNInboxStatusEnumValidator.enum.PENDING,
824
1000
  inboxCredential,
825
1001
  claimUrl,
1002
+ ...(refresh ? { refresh } : {}),
826
1003
  ...(guardianEmail ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
827
1004
  };
828
1005
  }
829
1006
  };
1007
+
1008
+ /**
1009
+ * Resolves an input into the credential that issueToInbox consumes. Keeping this outside either
1010
+ * route makes direct credentials and Boost templates behave identically for single and batch
1011
+ * issuance, including template rendering, Boost metadata, and standard client-facing errors.
1012
+ */
1013
+ export const resolveInboxCredentialInput = async (
1014
+ input: IssueInboxCredentialType,
1015
+ ctx: Context
1016
+ ): Promise<{ credential: VC | UnsignedVC | VP; resolvedBoostUri?: string }> => {
1017
+ const { templateUri, configuration } = input;
1018
+ // A direct credential wins when both fields are present, matching the existing single-route
1019
+ // behavior. A template is only loaded when the caller supplied no credential.
1020
+ let credential = input.credential;
1021
+ let resolvedBoostUri: string | undefined;
1022
+
1023
+ if (templateUri && !credential) {
1024
+ const boostInstance = await getBoostByUri(templateUri);
1025
+
1026
+ if (!boostInstance) {
1027
+ throw new TRPCError({
1028
+ code: 'NOT_FOUND',
1029
+ message: `Boost not found: ${templateUri}`,
1030
+ });
1031
+ }
1032
+
1033
+ if (!boostInstance.dataValues.boost) {
1034
+ throw new TRPCError({
1035
+ code: 'BAD_REQUEST',
1036
+ message: `Boost does not contain a credential template: ${templateUri}`,
1037
+ });
1038
+ }
1039
+
1040
+ try {
1041
+ // Use shared helper to prepare credential with templateData rendering,
1042
+ // issuance date, boostId injection, and OBv3 alignments
1043
+ resolvedBoostUri = getBoostUri(boostInstance.id, ctx.domain);
1044
+
1045
+ credential = await prepareCredentialFromBoost(
1046
+ boostInstance,
1047
+ resolvedBoostUri,
1048
+ ctx.domain,
1049
+ { templateData: configuration?.templateData as Record<string, unknown> }
1050
+ );
1051
+ } catch (e) {
1052
+ console.error('Failed to prepare boost credential', e);
1053
+ throw new TRPCError({
1054
+ code: 'BAD_REQUEST',
1055
+ message: `Failed to prepare boost credential template: ${templateUri}`,
1056
+ });
1057
+ }
1058
+ }
1059
+
1060
+ if (!credential) {
1061
+ throw new TRPCError({
1062
+ code: 'BAD_REQUEST',
1063
+ message: 'Either credential or templateUri must be provided',
1064
+ });
1065
+ }
1066
+
1067
+ return { credential, resolvedBoostUri };
1068
+ };
@@ -57,7 +57,8 @@ export type NotificationMessageKey =
57
57
  | 'credentialRestoredUnnamed'
58
58
  // --- Managed credential refresh (LC-2136) — generic copy only: never name the
59
59
  // credential, the holder's claims, or implementation terms (refresh/sync/etc.) ---
60
- | 'credentialRefreshed';
60
+ | 'credentialRefreshed'
61
+ | 'shareViewed';
61
62
 
62
63
  type MessageTemplate = { title: string; body: string };
63
64
  type LocaleCatalog = Record<NotificationMessageKey, MessageTemplate>;
@@ -216,6 +217,10 @@ const en: LocaleCatalog = {
216
217
  title: 'Credential updated',
217
218
  body: '{from} updated one of your credentials.',
218
219
  },
220
+ shareViewed: {
221
+ title: 'Share viewed',
222
+ body: 'Your share “{title} ({count})” was viewed.',
223
+ },
219
224
  };
220
225
 
221
226
  const es: LocaleCatalog = {
@@ -367,6 +372,10 @@ const es: LocaleCatalog = {
367
372
  title: 'Credencial actualizada',
368
373
  body: '{from} actualizó una de tus credenciales.',
369
374
  },
375
+ shareViewed: {
376
+ title: 'Enlace visto',
377
+ body: 'Se vio tu enlace «{title} ({count})».',
378
+ },
370
379
  };
371
380
 
372
381
  const fr: LocaleCatalog = {
@@ -518,6 +527,10 @@ const fr: LocaleCatalog = {
518
527
  title: 'Titre mis à jour',
519
528
  body: "{from} a mis à jour l'un de vos titres.",
520
529
  },
530
+ shareViewed: {
531
+ title: 'Partage consulté',
532
+ body: 'Votre partage « {title} ({count}) » a été consulté.',
533
+ },
521
534
  };
522
535
 
523
536
  const ar: LocaleCatalog = {
@@ -669,6 +682,10 @@ const ar: LocaleCatalog = {
669
682
  title: 'تم تحديث الشهادة',
670
683
  body: 'قام {from} بتحديث إحدى شهاداتك.',
671
684
  },
685
+ shareViewed: {
686
+ title: 'تمت مشاهدة المشاركة',
687
+ body: 'تمت مشاهدة مشاركتك «{title} ({count})».',
688
+ },
672
689
  };
673
690
 
674
691
  const CATALOGS: Record<NotificationLocale, LocaleCatalog> = { en, es, fr, ar };
@@ -223,8 +223,10 @@ export const buildCredentialRefreshedNotification = (
223
223
  return {
224
224
  notification: {
225
225
  type: LCNNotificationTypeEnumValidator.enum.CREDENTIAL_REFRESHED,
226
- to: holderProfile,
227
- from: issuerProfile,
226
+ // Local webhook delivery normalizes these DIDs in place. Keep the
227
+ // database profiles intact for the subsequent verified-email lookup.
228
+ to: { ...holderProfile },
229
+ from: { ...issuerProfile },
228
230
  message: getNotificationMessage(
229
231
  'credentialRefreshed',
230
232
  resolveRecipientLocale(holderProfile),
@@ -0,0 +1,41 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { ShareLinkIdValidator } from '@learncard/types';
3
+ import { z } from 'zod';
4
+
5
+ // Relationship metadata is not signed VP content. Bound generic metadata and
6
+ // reserve the share-link shape for the recipient's own send/accept workflow.
7
+ const ShareLinkPresentationMetadataValidator = z
8
+ .object({
9
+ type: z.literal('learncard.share-link.v1'),
10
+ shareId: ShareLinkIdValidator,
11
+ title: z.string().min(1).max(120),
12
+ note: z.string().max(500).optional(),
13
+ sharer: z
14
+ .object({
15
+ profileId: z.string().min(1).max(128),
16
+ displayName: z.string().min(1).max(120),
17
+ })
18
+ .strict(),
19
+ })
20
+ .strict();
21
+
22
+ export const PresentationMetadataValidator = z
23
+ .record(z.string().min(1).max(64), z.unknown())
24
+ .refine(value => JSON.stringify(value).length <= 4096, 'Metadata is too large')
25
+ .superRefine((value, ctx) => {
26
+ if (value.type !== 'learncard.share-link.v1') return;
27
+ if (!ShareLinkPresentationMetadataValidator.safeParse(value).success)
28
+ ctx.addIssue({ code: 'custom', message: 'Invalid share-link metadata' });
29
+ });
30
+
31
+ export const assertPresentationMetadataRecipient = (
32
+ metadata: Record<string, unknown> | undefined,
33
+ senderProfileId: string,
34
+ recipientProfileId: string
35
+ ): void => {
36
+ if (metadata?.type === 'learncard.share-link.v1' && senderProfileId !== recipientProfileId)
37
+ throw new TRPCError({
38
+ code: 'BAD_REQUEST',
39
+ message: 'Share-link metadata requires a self-save',
40
+ });
41
+ };
@@ -23,23 +23,29 @@ export const sendPresentation = async (
23
23
  from: ProfileType,
24
24
  to: ProfileType,
25
25
  presentation: VP | JWE,
26
- domain: string
26
+ domain: string,
27
+ metadata?: Record<string, unknown>
27
28
  ): Promise<string> => {
28
29
  const presentationInstance = await storePresentation(presentation);
29
30
 
30
- await createSentPresentationRelationship(from, to, presentationInstance);
31
+ await createSentPresentationRelationship(from, to, presentationInstance, metadata);
31
32
 
32
- let uri = getPresentationUri(presentationInstance.id, domain);
33
+ const uri = getPresentationUri(presentationInstance.id, domain);
33
34
 
34
- await addNotificationToQueue({
35
- type: LCNNotificationTypeEnumValidator.enum.PRESENTATION_RECEIVED,
36
- to,
37
- from,
38
- message: getNotificationMessage('presentationReceived', resolveRecipientLocale(to), {
39
- from: from.displayName,
40
- }),
41
- data: { vpUris: [uri] },
42
- });
35
+ // Saving a shared collection reuses the presentation send/accept pipeline by
36
+ // sending it to the current profile. Keep that storage behavior without
37
+ // creating a meaningless "you sent this to yourself" recipient alert.
38
+ if (from.profileId !== to.profileId) {
39
+ await addNotificationToQueue({
40
+ type: LCNNotificationTypeEnumValidator.enum.PRESENTATION_RECEIVED,
41
+ to,
42
+ from,
43
+ message: getNotificationMessage('presentationReceived', resolveRecipientLocale(to), {
44
+ from: from.displayName,
45
+ }),
46
+ data: { vpUris: [uri] },
47
+ });
48
+ }
43
49
 
44
50
  return uri;
45
51
  };
@@ -63,16 +69,17 @@ export const acceptPresentation = async (profile: ProfileType, uri: string): Pro
63
69
  });
64
70
  }
65
71
 
66
- // Check if presentation has already been received by this profile
72
+ // Acceptance is idempotent so a client can safely recover from a response
73
+ // interruption without producing a second received relationship.
67
74
  const alreadyReceived = await getPresentationReceivedByProfile(id, profile);
68
- if (alreadyReceived) {
69
- throw new TRPCError({
70
- code: 'BAD_REQUEST',
71
- message: 'Presentation has already been received',
72
- });
75
+ if (!alreadyReceived) {
76
+ await createReceivedPresentationRelationship(
77
+ profile,
78
+ pendingVp.source,
79
+ pendingVp.target,
80
+ pendingVp.relationship.metadata
81
+ );
73
82
  }
74
83
 
75
- await createReceivedPresentationRelationship(profile, pendingVp.source, pendingVp.target);
76
-
77
84
  return true;
78
85
  };
@@ -1,3 +1,5 @@
1
+ import { LCNProfileValidator } from '@learncard/types';
2
+
1
3
  import { getProfileByProfileId } from '@accesslayer/profile/read';
2
4
  import { createProfile } from '@accesslayer/profile/create';
3
5
  import { getProfileIdFromDid } from './did.helpers';
@@ -6,6 +8,11 @@ import { ProfileType } from 'types/profile';
6
8
  export const transformProfileId = (rawInput: string): string =>
7
9
  rawInput.toLowerCase().replaceAll(':', '%3A');
8
10
 
11
+ export const PublicProfileIdValidator = LCNProfileValidator.shape.profileId.refine(
12
+ profileId => !transformProfileId(profileId).startsWith('sample-'),
13
+ { message: 'Profile IDs beginning with "sample-" are reserved.' }
14
+ );
15
+
9
16
  /**
10
17
  * Gets or creates a federated profile for cross-instance credential sending.
11
18
  * Federated profiles use email-like format: profileId@service-domain to avoid collisions.
@@ -24,7 +24,7 @@ export type RateLimitWindow = {
24
24
  };
25
25
 
26
26
  /**
27
- * Consume one unit against each window, in order, and throw once any is
27
+ * Consume the requested units against each window, in order, and throw once any is
28
28
  * exhausted.
29
29
  *
30
30
  * Fails CLOSED: if the cache is unavailable, `cache.incr` returns `undefined`
@@ -48,11 +48,8 @@ export const enforceRateLimits = async (windows: RateLimitWindow[]): Promise<voi
48
48
  }
49
49
 
50
50
  if (count > limit) {
51
- // tRPC has no native 429; the existing convention in this service is
52
- // to cast to BAD_REQUEST while keeping the semantic code, so clients
53
- // can still discriminate on it.
54
51
  throw new TRPCError({
55
- code: 'TOO_MANY_REQUESTS' as 'BAD_REQUEST',
52
+ code: 'TOO_MANY_REQUESTS',
56
53
  message: `Rate limit exceeded: ${description}`,
57
54
  });
58
55
  }