@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
@@ -85,11 +85,11 @@ export type Cache = {
85
85
  lrange: (key: RedisKey, start: number, stop: number) => Promise<string[] | undefined>;
86
86
 
87
87
  /**
88
- * Atomically increments a key by 1 and returns the new value.
89
- * If the key does not exist it is created with value 1.
90
- * When ttl is provided and this is the first increment (result === 1), EXPIRE is set.
88
+ * Atomically increments a key by amount (default 1) and returns the new value.
89
+ * If the key does not exist it is created with value amount.
90
+ * When ttl is provided, increment and missing-expiry repair happen atomically.
91
91
  */
92
- incr: (key: RedisKey, ttl?: number) => Promise<number | undefined>;
92
+ incr: (key: RedisKey, ttl?: number, amount?: number) => Promise<number | undefined>;
93
93
  };
94
94
 
95
95
  /** Evict all keys after one hour by default */
@@ -239,19 +239,31 @@ export const getCache = (): Cache => {
239
239
 
240
240
  return undefined;
241
241
  },
242
- incr: async (key, ttl) => {
242
+ incr: async (key, ttl, amount = 1) => {
243
243
  try {
244
244
  const redis = cache?.redis ?? cache?.node;
245
245
  if (!redis) return undefined;
246
246
 
247
- const newVal = await redis.incr(key);
248
-
249
- // Set TTL on first increment so the window auto-expires
250
- if (newVal === 1 && ttl) {
251
- await redis.expire(key, ttl);
252
- }
253
-
254
- return newVal;
247
+ if (!Number.isSafeInteger(amount) || amount <= 0) return undefined;
248
+ // Increment and expiry must be atomic: a crash between commands must not
249
+ // leave a permanent quota. Also repair counters created without an expiry.
250
+ if (ttl)
251
+ return Number(
252
+ await redis.eval(
253
+ `
254
+ local count = redis.call('INCRBY', KEYS[1], ARGV[1])
255
+ if redis.call('TTL', KEYS[1]) < 0 then
256
+ redis.call('EXPIRE', KEYS[1], ARGV[2])
257
+ end
258
+ return count
259
+ `,
260
+ 1,
261
+ key,
262
+ amount,
263
+ ttl
264
+ )
265
+ );
266
+ return await redis.incrby(key, amount);
255
267
  } catch (e) {
256
268
  console.error('Cache incr error', e);
257
269
  }
@@ -36,6 +36,9 @@ export const brainServiceEnvironmentShape = {
36
36
  OIDC_EXPECTED_AUDIENCE: optionalEnvironmentString,
37
37
  AWS_REGION: optionalEnvironmentString,
38
38
  NOTIFICATIONS_QUEUE_URL: optionalEnvironmentUrl,
39
+ INBOX_QUEUE_URL: optionalEnvironmentUrl,
40
+ INBOX_DEAD_LETTER_QUEUE_URL: optionalEnvironmentUrl,
41
+ INBOX_QUEUE_ENDPOINT: optionalEnvironmentUrl,
39
42
  NOTIFICATIONS_QUEUE_POLL_URL: optionalEnvironmentUrl,
40
43
  NOTIFICATIONS_SERVICE_WEBHOOK_URL: optionalEnvironmentUrl.or(z.literal('false')),
41
44
  NOTIFICATIONS_SERVICE_PORT: optionalEnvironmentPort,
@@ -74,12 +77,15 @@ export const brainServiceEnvironmentShape = {
74
77
  SKILL_SEMANTIC_SEARCH_RATE_LIMIT_PER_MIN: optionalEnvironmentPort,
75
78
  BITSTRING_STATUS_LIST_SIZE: optionalEnvironmentPort,
76
79
  CREDENTIAL_REFRESH_DIGEST_SECRET: optionalEnvironmentString,
80
+ SHARE_LINK_REQUEST_HASH_SECRET: optionalEnvironmentString,
77
81
  CREDENTIAL_REFRESH_ENABLED: optionalEnvironmentBoolean.default(false),
78
82
  CREDENTIAL_REFRESH_NOTIFICATION_WINDOW_HOURS: credentialRefreshNotificationWindowHours,
79
83
  IS_OFFLINE: optionalEnvironmentBoolean.default(false),
80
84
  IS_CI: optionalEnvironmentBoolean.default(false),
81
85
  IS_E2E_TEST: optionalEnvironmentBoolean.default(false),
82
86
  INBOX_DELETE_EXPIRED_RECORDS: optionalEnvironmentBoolean.default(false),
87
+ INBOX_BATCH_ITEMS_PER_HOUR: optionalEnvironmentString,
88
+ AWS_LAMBDA_FUNCTION_NAME: optionalEnvironmentString,
83
89
  ENABLE_BENCH_ROUTES: optionalEnvironmentBoolean.default(false),
84
90
  ENABLE_SEND_CREDENTIAL_TELEMETRY: optionalEnvironmentBoolean.default(false),
85
91
  LC_PERF_LOG: optionalEnvironmentBoolean.default(false),
@@ -206,6 +212,40 @@ export const getCredentialRefreshRuntimeEnvironment = (): CredentialRefreshRunti
206
212
  examplePath: 'services/learn-card-network/brain-service/.env.example',
207
213
  });
208
214
 
215
+ const shareLinkRequestHashRuntimeEnvironmentSchema = z.object({
216
+ NODE_ENV: brainServiceEnvironmentShape.NODE_ENV,
217
+ SHARE_LINK_REQUEST_HASH_SECRET: brainServiceEnvironmentShape.SHARE_LINK_REQUEST_HASH_SECRET,
218
+ });
219
+
220
+ /** Reads the dedicated fingerprint secret at call time for enabled share-link APIs. */
221
+ export const getShareLinkRequestHashRuntimeEnvironment = () =>
222
+ parseEnvironment(shareLinkRequestHashRuntimeEnvironmentSchema, process.env, {
223
+ project: 'brain-service share links',
224
+ source: 'process environment',
225
+ examplePath: 'services/learn-card-network/brain-service/.env.example',
226
+ });
227
+
228
+ const inboxBatchRuntimeEnvironmentSchema = z.object({
229
+ INBOX_QUEUE_URL: brainServiceEnvironmentShape.INBOX_QUEUE_URL,
230
+ INBOX_DEAD_LETTER_QUEUE_URL: brainServiceEnvironmentShape.INBOX_DEAD_LETTER_QUEUE_URL,
231
+ INBOX_QUEUE_ENDPOINT: brainServiceEnvironmentShape.INBOX_QUEUE_ENDPOINT,
232
+ AWS_REGION: brainServiceEnvironmentShape.AWS_REGION,
233
+ NODE_ENV: brainServiceEnvironmentShape.NODE_ENV,
234
+ IS_OFFLINE: brainServiceEnvironmentShape.IS_OFFLINE,
235
+ AWS_LAMBDA_FUNCTION_NAME: brainServiceEnvironmentShape.AWS_LAMBDA_FUNCTION_NAME,
236
+ INBOX_BATCH_ITEMS_PER_HOUR: brainServiceEnvironmentShape.INBOX_BATCH_ITEMS_PER_HOUR,
237
+ });
238
+
239
+ export type InboxBatchRuntimeEnvironment = z.output<typeof inboxBatchRuntimeEnvironmentSchema>;
240
+
241
+ /** Reads batch settings at call time so local invocations and tests can override them safely. */
242
+ export const getInboxBatchRuntimeEnvironment = (): InboxBatchRuntimeEnvironment =>
243
+ parseEnvironment(inboxBatchRuntimeEnvironmentSchema, process.env, {
244
+ project: 'brain-service inbox batch',
245
+ source: 'process environment',
246
+ examplePath: 'services/learn-card-network/brain-service/.env.example',
247
+ });
248
+
209
249
  export type BrainServiceEnvironment = z.output<typeof brainServiceEnvironmentSchema>;
210
250
 
211
251
  export const parseBrainServiceEnvironment = (
@@ -45,3 +45,8 @@ export const AUTH_GRANT_CREDENTIAL_MANAGEMENT_SCOPE = 'credential:* presentation
45
45
  export const AUTH_GRANT_CONTRACTS_SCOPE = 'contracts:*';
46
46
  export const AUTH_GRANT_DID_METADATA_SCOPE = 'didMetadata:*';
47
47
  export const AUTH_GRANT_AUTH_GRANTS_SCOPE = 'authGrants:*';
48
+
49
+ // LC-2187 owner share-link APIs. Least privilege: owner metadata/recovery reads
50
+ // use `shareLinks:read`; create/update/revoke/retry use `shareLinks:write`.
51
+ export const AUTH_GRANT_SHARE_LINKS_READ_SCOPE = 'shareLinks:read';
52
+ export const AUTH_GRANT_SHARE_LINKS_WRITE_SCOPE = 'shareLinks:write';
@@ -172,7 +172,11 @@ const authenticateRefreshRequest = async (
172
172
 
173
173
  // Missing aggregates, wrong holders, and awaiting-claim aggregates deliberately
174
174
  // share one response so a valid DID cannot use the endpoint as an existence oracle.
175
- if (aggregate.holderDid !== auth.holderDid) {
175
+ if (
176
+ !aggregate.holderDid ||
177
+ aggregate.state === 'pending_holder' ||
178
+ aggregate.holderDid !== auth.holderDid
179
+ ) {
176
180
  logRefreshRequest(refreshId, 'unauthorized', startedAt);
177
181
  await reply.status(403).send({ code: 'CREDENTIAL_REFRESH_UNAUTHORIZED' });
178
182
  return null;
@@ -1,3 +1,4 @@
1
+ import { configureInboxBatchBodyLimit } from '@helpers/inbox-batch-http.helpers';
1
2
  import { environment } from '@environment';
2
3
  import Fastify from 'fastify';
3
4
  import fastifyCors from '@fastify/cors';
@@ -14,17 +15,41 @@ import {
14
15
 
15
16
  import { neogma } from '@instance';
16
17
  import { appRouter, type AppRouter, createContext } from './app';
18
+ import { publicShareLinkCacheControlHeaders } from './routes';
19
+ import { registerPublicShareLinkNoStore } from './public-share-link-http';
17
20
  import { openApiDocument } from './openapi';
21
+ import { inboxBatchResponseMeta } from './helpers/inbox-batch-http.helpers';
18
22
  import { didFastifyPlugin } from './dids';
19
23
  import { skillsViewerFastifyPlugin } from './skills-viewer';
20
24
  import { statusListsFastifyPlugin } from './status-lists';
21
25
  import { credentialRefreshFastifyPlugin } from './credential-refresh';
22
26
  import { deliverQueuedNotification } from '@helpers/notificationQueue.helpers';
23
27
  import { startSkillEmbeddingBackfill } from '@helpers/skill-embedding.helpers';
28
+ import {
29
+ createConsoleMaintenanceLogger,
30
+ createShareLinkMaintenanceRuntime,
31
+ createShareLinkMaintenanceScheduler,
32
+ type ShareLinkMaintenanceScheduler,
33
+ } from '@helpers/share-link-maintenance';
24
34
  import { maybeAutoSeedSkillFrameworks } from './seed/seedSkillFrameworks';
25
35
 
26
36
  const server = Fastify({ routerOptions: { maxParamLength: 5000 } });
27
37
 
38
+ /**
39
+ * The scheduled maintenance runtime is created only after the server is ready.
40
+ * When its config is disabled/invalid it is inert, so no interval and no
41
+ * graph/remote/signing dependency is initialized at all.
42
+ */
43
+ let shareLinkMaintenanceScheduler: ShareLinkMaintenanceScheduler | null = null;
44
+
45
+ server.addHook('onClose', async () => {
46
+ await shareLinkMaintenanceScheduler?.stop();
47
+ });
48
+ // Register before either OpenAPI or tRPC registers its wildcard route. The hook raises the
49
+ // parser ceiling for those shared routes, then narrows it back to 4 MiB only for batch issuance.
50
+ // Registering this later would leave the already-created adapter routes at Fastify's 1 MiB limit.
51
+ configureInboxBatchBodyLimit(server);
52
+
28
53
  server.addHook('onRequest', (request, _reply, done) => {
29
54
  type RawWithEmitter = typeof request.raw & {
30
55
  on?: (...args: unknown[]) => unknown;
@@ -71,6 +96,9 @@ server.register(fastifyTRPCPlugin, {
71
96
  allowMethodOverride: true,
72
97
  router: appRouter,
73
98
  createContext,
99
+ responseMeta: ({ paths }) => ({
100
+ headers: publicShareLinkCacheControlHeaders(paths),
101
+ }),
74
102
  onError({ path, error }) {
75
103
  // report to error monitoring
76
104
  console.error(`Error in tRPC handler on path '${path}':`, error);
@@ -79,6 +107,7 @@ server.register(fastifyTRPCPlugin, {
79
107
  });
80
108
 
81
109
  server.register(fastifyTRPCOpenApiPlugin, {
110
+ responseMeta: inboxBatchResponseMeta,
82
111
  basePath: '/api',
83
112
  router: appRouter,
84
113
  createContext,
@@ -88,6 +117,13 @@ server.register(fastifyTRPCOpenApiPlugin, {
88
117
  },
89
118
  } satisfies CreateOpenApiFastifyPluginOptions<AppRouter>);
90
119
 
120
+ /**
121
+ * LC-2187: the Fastify OpenAPI adapter has no `responseMeta` hook, so the
122
+ * no-store contract for the anonymous public share-link routes is applied at
123
+ * the adapter boundary itself. It never depends on a cache layer above.
124
+ */
125
+ registerPublicShareLinkNoStore(server);
126
+
91
127
  server.get('/docs/openapi.json', () => openApiDocument);
92
128
 
93
129
  const SCALAR_HTML = `<!DOCTYPE html>
@@ -130,12 +166,41 @@ server.register(credentialRefreshFastifyPlugin);
130
166
  } catch (error) {
131
167
  console.error('Skill embedding backfill failed', error);
132
168
  }
169
+
170
+ // Explicit startup after readiness. Disabled/invalid config stays inert.
171
+ const maintenanceRuntime = createShareLinkMaintenanceRuntime();
172
+
173
+ if (maintenanceRuntime.resolution.status === 'enabled') {
174
+ shareLinkMaintenanceScheduler = createShareLinkMaintenanceScheduler({
175
+ runOnce: () => maintenanceRuntime.runOnce(),
176
+ intervalMs: maintenanceRuntime.resolution.config.intervalMs,
177
+ logger: createConsoleMaintenanceLogger(),
178
+ });
179
+ shareLinkMaintenanceScheduler.start();
180
+ }
133
181
  } catch (err) {
134
182
  console.error(err);
135
183
  process.exit(1);
136
184
  }
137
185
  })();
138
186
 
187
+ const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
188
+ console.log(`Received ${signal}; closing server and draining maintenance`);
189
+ try {
190
+ await server.close();
191
+ } catch (error) {
192
+ console.error('Server shutdown failed', error);
193
+ }
194
+ process.exit(0);
195
+ };
196
+
197
+ process.once('SIGTERM', () => {
198
+ void shutdown('SIGTERM');
199
+ });
200
+ process.once('SIGINT', () => {
201
+ void shutdown('SIGINT');
202
+ });
203
+
139
204
  const pollUrl = environment.NOTIFICATIONS_QUEUE_POLL_URL;
140
205
 
141
206
  if (pollUrl) {
@@ -20,6 +20,7 @@ export const logCredentialActivity = async (
20
20
  };
21
21
 
22
22
  export const logCredentialSent = async (params: {
23
+ onBehalfOf?: string;
23
24
  actorProfileId?: string;
24
25
  recipientType: CredentialActivityRecipientType;
25
26
  recipientIdentifier: string;
@@ -45,6 +46,7 @@ export const logCredentialSent = async (params: {
45
46
  };
46
47
 
47
48
  export const logCredentialDelivered = async (params: {
49
+ onBehalfOf?: string;
48
50
  activityId: string;
49
51
  actorProfileId: string;
50
52
  recipientType: CredentialActivityRecipientType;
@@ -64,6 +66,7 @@ export const logCredentialDelivered = async (params: {
64
66
  };
65
67
 
66
68
  export const logCredentialClaimed = async (params: {
69
+ onBehalfOf?: string;
67
70
  activityId?: string;
68
71
  actorProfileId: string;
69
72
  recipientType: CredentialActivityRecipientType;
@@ -98,6 +101,7 @@ export const logCredentialExpired = async (params: {
98
101
  };
99
102
 
100
103
  export const logCredentialFailed = async (params: {
104
+ onBehalfOf?: string;
101
105
  activityId?: string;
102
106
  actorProfileId: string;
103
107
  recipientType: CredentialActivityRecipientType;
@@ -44,7 +44,7 @@ export const handleIncrementCounterEvent = async (
44
44
  // tRPC does not support HTTP 429 natively, so we cast to BAD_REQUEST
45
45
  // while keeping the semantic code in the message for clients.
46
46
  throw new TRPCError({
47
- code: 'TOO_MANY_REQUESTS' as 'BAD_REQUEST',
47
+ code: 'TOO_MANY_REQUESTS',
48
48
  message: 'Rate limit exceeded: max 100 counter writes per user per app per minute',
49
49
  });
50
50
  }
@@ -22,6 +22,7 @@ import {
22
22
  parseRenderedTemplate,
23
23
  shouldAutoAppendTemplateEvidence,
24
24
  } from '@helpers/template.helpers';
25
+ import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
25
26
 
26
27
  import { getBoostOwner } from '@accesslayer/boost/relationships/read';
27
28
  import { BoostInstance } from '@models';
@@ -95,15 +96,7 @@ export const convertCredentialToBoostTemplateJSON = (
95
96
  delete template.proof;
96
97
  template.issuer = defaultIssuerDid ?? 'did:example:123';
97
98
 
98
- if (Array.isArray(template.credentialSubject)) {
99
- template.credentialSubject = template.credentialSubject.map(subject => {
100
- subject.id = 'did:example:123';
101
-
102
- return subject;
103
- });
104
- } else {
105
- template.credentialSubject.id = 'did:example:123';
106
- }
99
+ setCredentialSubjectIds(template, 'did:example:123');
107
100
  return JSON.stringify(template);
108
101
  };
109
102
 
@@ -262,14 +255,7 @@ export const issueClaimLinkBoost = async (
262
255
 
263
256
  boostCredential.issuer = signingAuthorityForUser.relationship.did;
264
257
 
265
- if (Array.isArray(boostCredential.credentialSubject)) {
266
- boostCredential.credentialSubject = boostCredential.credentialSubject.map(subject => ({
267
- ...subject,
268
- id: getDidWeb(domain, to.profileId),
269
- }));
270
- } else {
271
- boostCredential.credentialSubject.id = getDidWeb(domain, to.profileId);
272
- }
258
+ setCredentialSubjectIds(boostCredential, getDidWeb(domain, to.profileId));
273
259
 
274
260
  // Embed the boostURI into the boost credential for verification purposes.
275
261
  if (boostCredential.type.includes('BoostCredential')) {
@@ -362,20 +348,7 @@ export const prepareCredentialFromBoost = async (
362
348
  credential.issuer = issuerDid;
363
349
  }
364
350
 
365
- // Set recipient DID in credentialSubject if provided
366
- if (recipientDid) {
367
- if (Array.isArray(credential.credentialSubject)) {
368
- credential.credentialSubject = credential.credentialSubject.map(subject => ({
369
- ...subject,
370
- id: recipientDid,
371
- }));
372
- } else {
373
- credential.credentialSubject = {
374
- ...(credential.credentialSubject || {}),
375
- id: recipientDid,
376
- };
377
- }
378
- }
351
+ if (recipientDid) setCredentialSubjectIds(credential, recipientDid);
379
352
 
380
353
  // Embed boostId for BoostCredential types
381
354
  if (credential?.type?.includes('BoostCredential')) {
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ const environment = vi.hoisted(() => ({
4
+ CLIENT_APP_DOMAIN_NAME: undefined as string | undefined,
5
+ CLIENT_APP_PORT: undefined as number | undefined,
6
+ PORT: 4000,
7
+ IS_OFFLINE: true,
8
+ }));
9
+ vi.mock('@environment', () => ({ environment }));
10
+ vi.mock('@cache', () => ({ default: {} }));
11
+
12
+ import { generateClaimUrl } from './contact-method.helpers';
13
+
14
+ describe('inbox claim links', () => {
15
+ it('opens the default frontend instead of the backend port', () => {
16
+ expect(generateClaimUrl('test-token')).toBe(
17
+ 'http://localhost:3000/interactions/inbox-claim/test-token?iuv=1'
18
+ );
19
+ });
20
+
21
+ it('uses the configured frontend port locally', () => {
22
+ environment.CLIENT_APP_PORT = 3100;
23
+ try {
24
+ expect(generateClaimUrl('test-token')).toContain('http://localhost:3100/');
25
+ } finally {
26
+ environment.CLIENT_APP_PORT = undefined;
27
+ }
28
+ });
29
+
30
+ it('honors a configured app domain locally and keeps production HTTPS', () => {
31
+ environment.CLIENT_APP_DOMAIN_NAME = 'app.example.org';
32
+ try {
33
+ expect(generateClaimUrl('test-token')).toContain('http://app.example.org/');
34
+ environment.IS_OFFLINE = false;
35
+ expect(generateClaimUrl('test-token')).toBe(
36
+ 'https://app.example.org/interactions/inbox-claim/test-token?iuv=1'
37
+ );
38
+ } finally {
39
+ environment.IS_OFFLINE = true;
40
+ environment.CLIENT_APP_DOMAIN_NAME = undefined;
41
+ }
42
+ });
43
+ });
@@ -110,10 +110,9 @@ export const markInboxClaimTokenAsUsed = async (token: string): Promise<boolean>
110
110
 
111
111
  export const generateClaimUrl = (token: string): string => {
112
112
  const domainName = environment.CLIENT_APP_DOMAIN_NAME;
113
- const domain =
114
- !domainName || environment.IS_OFFLINE
115
- ? `localhost:${environment.PORT || 3000}`
116
- : domainName;
113
+ // Claim links open the frontend, not the brain-service HTTP port. Honor an
114
+ // explicitly configured app domain in local development as well.
115
+ const domain = domainName || `localhost:${environment.CLIENT_APP_PORT || 3000}`;
117
116
 
118
117
  const protocol = environment.IS_OFFLINE ? 'http' : 'https';
119
118
 
@@ -0,0 +1,222 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ buildCredentialUpdatedEmailModel,
5
+ chooseCredentialRefreshEmailTarget,
6
+ extractBoundedCredentialDisplayTitle,
7
+ MAX_CREDENTIAL_DISPLAY_TITLE_LENGTH,
8
+ } from './credential-refresh-email-content.helpers';
9
+
10
+ const email = (value: string, { verified = true, primary = false } = {}) => ({
11
+ type: 'email',
12
+ value,
13
+ isVerified: verified,
14
+ isPrimary: primary,
15
+ });
16
+
17
+ const phone = (value: string) => ({
18
+ type: 'phone',
19
+ value,
20
+ isVerified: true,
21
+ isPrimary: true,
22
+ });
23
+
24
+ describe('extractBoundedCredentialDisplayTitle', () => {
25
+ it('returns a trimmed, whitespace-collapsed title', () => {
26
+ expect(extractBoundedCredentialDisplayTitle(' Introduction \n to\tBiology ')).toBe(
27
+ 'Introduction to Biology'
28
+ );
29
+ });
30
+
31
+ it('removes control characters', () => {
32
+ expect(extractBoundedCredentialDisplayTitle('Biology\u0000\u0007 101')).toBe('Biology 101');
33
+ });
34
+
35
+ it('truncates to the bounded maximum length', () => {
36
+ const long = 'x'.repeat(MAX_CREDENTIAL_DISPLAY_TITLE_LENGTH + 50);
37
+ const result = extractBoundedCredentialDisplayTitle(long);
38
+
39
+ expect(result).toHaveLength(MAX_CREDENTIAL_DISPLAY_TITLE_LENGTH);
40
+ });
41
+
42
+ it('honors an explicit maximum length', () => {
43
+ expect(extractBoundedCredentialDisplayTitle('abcdef', 3)).toBe('abc');
44
+ });
45
+
46
+ it.each([undefined, null, '', ' ', 42, {}, []])(
47
+ 'returns undefined for a non-title value (%s)',
48
+ value => {
49
+ expect(extractBoundedCredentialDisplayTitle(value)).toBeUndefined();
50
+ }
51
+ );
52
+
53
+ it('returns undefined when only whitespace remains after sanitization', () => {
54
+ expect(extractBoundedCredentialDisplayTitle('\u0000 \n\u0007')).toBeUndefined();
55
+ });
56
+ });
57
+
58
+ describe('chooseCredentialRefreshEmailTarget', () => {
59
+ it('resolves a verified holder email for an unmanaged holder', () => {
60
+ const result = chooseCredentialRefreshEmailTarget({
61
+ holder: { locale: 'es' },
62
+ holderContactMethods: [email('holder@example.com')],
63
+ managers: [],
64
+ });
65
+
66
+ expect(result).toEqual({
67
+ status: 'resolved',
68
+ kind: 'holder',
69
+ locale: 'es',
70
+ contactMethod: { type: 'email', value: 'holder@example.com' },
71
+ });
72
+ });
73
+
74
+ it('prefers the verified primary holder email', () => {
75
+ const result = chooseCredentialRefreshEmailTarget({
76
+ holder: {},
77
+ holderContactMethods: [
78
+ email('secondary@example.com'),
79
+ email('primary@example.com', { primary: true }),
80
+ ],
81
+ managers: [],
82
+ });
83
+
84
+ expect(result).toMatchObject({
85
+ status: 'resolved',
86
+ contactMethod: { value: 'primary@example.com' },
87
+ });
88
+ });
89
+
90
+ it('never uses an unverified holder email', () => {
91
+ const result = chooseCredentialRefreshEmailTarget({
92
+ holder: {},
93
+ holderContactMethods: [
94
+ email('unverified@example.com', { verified: false }),
95
+ email('verified@example.com'),
96
+ ],
97
+ managers: [],
98
+ });
99
+
100
+ expect(result).toMatchObject({
101
+ status: 'resolved',
102
+ contactMethod: { value: 'verified@example.com' },
103
+ });
104
+ });
105
+
106
+ it('ignores non-email contact methods', () => {
107
+ const result = chooseCredentialRefreshEmailTarget({
108
+ holder: {},
109
+ holderContactMethods: [phone('+15555550100')],
110
+ managers: [],
111
+ });
112
+
113
+ expect(result).toEqual({ status: 'skipped', reason: 'no-verified-holder-email' });
114
+ });
115
+
116
+ it('skips an unmanaged holder with no verified email', () => {
117
+ const result = chooseCredentialRefreshEmailTarget({
118
+ holder: {},
119
+ holderContactMethods: [],
120
+ managers: [],
121
+ });
122
+
123
+ expect(result).toEqual({ status: 'skipped', reason: 'no-verified-holder-email' });
124
+ });
125
+
126
+ it('routes a managed child to the verified guardian email, never the child', () => {
127
+ const result = chooseCredentialRefreshEmailTarget({
128
+ holder: { locale: 'en' },
129
+ holderContactMethods: [email('child@example.com')],
130
+ managers: [
131
+ {
132
+ locale: 'fr',
133
+ contactMethods: [email('guardian@example.com')],
134
+ },
135
+ ],
136
+ });
137
+
138
+ expect(result).toEqual({
139
+ status: 'resolved',
140
+ kind: 'guardian',
141
+ locale: 'fr',
142
+ contactMethod: { type: 'email', value: 'guardian@example.com' },
143
+ });
144
+ expect(JSON.stringify(result)).not.toContain('child@example.com');
145
+ });
146
+
147
+ it('skips a managed child when no guardian has a verified email', () => {
148
+ const result = chooseCredentialRefreshEmailTarget({
149
+ holder: {},
150
+ holderContactMethods: [email('child@example.com')],
151
+ managers: [
152
+ { contactMethods: [email('guardian@example.com', { verified: false })] },
153
+ { contactMethods: [] },
154
+ ],
155
+ });
156
+
157
+ expect(result).toEqual({ status: 'skipped', reason: 'no-verified-guardian-email' });
158
+ });
159
+
160
+ it('skips a managed child even when the child has a verified email', () => {
161
+ const result = chooseCredentialRefreshEmailTarget({
162
+ holder: {},
163
+ holderContactMethods: [email('child@example.com')],
164
+ managers: [{ contactMethods: [] }],
165
+ });
166
+
167
+ expect(result).toEqual({ status: 'skipped', reason: 'no-verified-guardian-email' });
168
+ });
169
+
170
+ it('prefers a primary verified guardian email across managers', () => {
171
+ const result = chooseCredentialRefreshEmailTarget({
172
+ holder: {},
173
+ holderContactMethods: [],
174
+ managers: [
175
+ { contactMethods: [email('guardian-a@example.com')] },
176
+ {
177
+ contactMethods: [
178
+ email('guardian-b@example.com', { primary: true }),
179
+ email('guardian-b2@example.com'),
180
+ ],
181
+ },
182
+ ],
183
+ });
184
+
185
+ expect(result).toMatchObject({
186
+ status: 'resolved',
187
+ kind: 'guardian',
188
+ contactMethod: { value: 'guardian-b@example.com' },
189
+ });
190
+ });
191
+ });
192
+
193
+ describe('buildCredentialUpdatedEmailModel', () => {
194
+ it('includes the issuer display name and credential title', () => {
195
+ expect(
196
+ buildCredentialUpdatedEmailModel({
197
+ issuerDisplayName: 'Inbox Demo School',
198
+ credentialTitle: 'Introduction to Biology',
199
+ })
200
+ ).toEqual({
201
+ issuer: { name: 'Inbox Demo School' },
202
+ credential: { name: 'Introduction to Biology' },
203
+ });
204
+ });
205
+
206
+ it('omits the credential title when unavailable', () => {
207
+ expect(
208
+ buildCredentialUpdatedEmailModel({
209
+ issuerDisplayName: 'Inbox Demo School',
210
+ })
211
+ ).toEqual({ issuer: { name: 'Inbox Demo School' } });
212
+ });
213
+
214
+ it('does not include any other credential content', () => {
215
+ const model = buildCredentialUpdatedEmailModel({
216
+ issuerDisplayName: 'Inbox Demo School',
217
+ credentialTitle: 'Introduction to Biology',
218
+ });
219
+
220
+ expect(Object.keys(model).sort()).toEqual(['credential', 'issuer']);
221
+ });
222
+ });