@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
@@ -0,0 +1,469 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { TRPCError } from '@trpc/server';
3
+ import type { ManagedTransaction } from 'neo4j-driver';
4
+ import type { InboxBatchReceipt, IssueInboxCredentialBatchItemResult } from '@learncard/types';
5
+ import { neogma } from '@instance';
6
+ import { encryptInboxCredential, decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
7
+ import type { BatchItem, BatchJob, BatchReplayStore, InboxDispatchLease } from 'types/inbox-batch';
8
+ import { getInboxBatchState } from '@helpers/inbox-batch-status.helpers';
9
+
10
+ export const DAY = 86_400_000;
11
+ export const LEASE_MS = 360_000;
12
+ const CLEANUP_MARGIN_MS = 5_000;
13
+
14
+ let readiness: Promise<void> | undefined;
15
+ export const ensureInboxBatchConstraints = (): Promise<void> => {
16
+ if (!readiness) {
17
+ readiness = (async () => {
18
+ for (const label of [
19
+ 'InboxBatch',
20
+ 'InboxBatchItem',
21
+ 'InboxBatchIssuer',
22
+ 'InboxBatchReplay',
23
+ ]) {
24
+ try {
25
+ await neogma.queryRunner.run(
26
+ `CREATE CONSTRAINT ${label.toLowerCase()}_id IF NOT EXISTS FOR (n:${label}) REQUIRE n.id IS UNIQUE`
27
+ );
28
+ } catch (error) {
29
+ if (
30
+ (error as { code?: string }).code !==
31
+ 'Neo.ClientError.Schema.EquivalentSchemaRuleAlreadyExists'
32
+ )
33
+ throw error;
34
+ }
35
+ }
36
+ for (const query of [
37
+ 'CREATE INDEX inbox_batch_request IF NOT EXISTS FOR (b:InboxBatch) ON (b.issuer, b.requestId)',
38
+ 'CREATE INDEX inbox_batch_dispatch IF NOT EXISTS FOR (i:InboxBatchItem) ON (i.state, i.dispatchAt)',
39
+ 'CREATE INDEX inbox_batch_recovery IF NOT EXISTS FOR (i:InboxBatchItem) ON (i.state, i.leaseUntil)',
40
+ 'CREATE INDEX inbox_batch_replay_owner IF NOT EXISTS FOR (r:InboxBatchReplay) ON (r.itemId)',
41
+ 'CREATE INDEX inbox_batch_completed IF NOT EXISTS FOR (b:InboxBatch) ON (b.completedAt)',
42
+ 'CREATE INDEX inbox_batch_replay_expiry IF NOT EXISTS FOR (r:InboxBatchReplay) ON (r.expiresAt)',
43
+ ])
44
+ await neogma.queryRunner.run(query);
45
+ })();
46
+ void readiness.catch(() => {
47
+ readiness = undefined;
48
+ });
49
+ }
50
+ return readiness;
51
+ };
52
+
53
+ const transaction = async <T>(fn: (tx: ManagedTransaction) => Promise<T>): Promise<T> => {
54
+ await ensureInboxBatchConstraints();
55
+ const session = neogma.driver.session();
56
+ try {
57
+ return await session.executeWrite(fn);
58
+ } finally {
59
+ await session.close();
60
+ }
61
+ };
62
+
63
+ /** The issuer node serializes submission replay and quota admission in the same transaction. */
64
+ export const createBatchJob = async (input: {
65
+ issuer: string;
66
+ requestId?: string;
67
+ requestHash: string;
68
+ payload: string;
69
+ items: { replayKey: string; duplicate: boolean; correlation?: string }[];
70
+ limit: number;
71
+ }): Promise<InboxBatchReceipt> => {
72
+ const now = Date.now();
73
+ const id = randomUUID();
74
+ return transaction(async tx => {
75
+ // The property write acquires Neo4j's exclusive node lock until commit. Concurrent
76
+ // admissions therefore cannot read quota between this statement and the update below.
77
+ const locked = await tx.run(
78
+ `MERGE (q:InboxBatchIssuer {id: $issuer})
79
+ ON CREATE SET q.used = 0, q.window = $now
80
+ SET q.lock = coalesce(q.lock, 0) + 1 RETURN q`,
81
+ { issuer: input.issuer, now }
82
+ );
83
+ if (input.requestId) {
84
+ // Creation atomically creates at least one HAS_ITEM relationship. Requiring it here
85
+ // prevents a structurally incomplete batch from being returned as a valid replay.
86
+ const prior = await tx.run(
87
+ `MATCH (b:InboxBatch {issuer: $issuer, requestId: $requestId})
88
+ WHERE b.createdAt > $cutoff
89
+ MATCH (b)-[:HAS_ITEM]->(i:InboxBatchItem) RETURN b, collect(i.state) AS states`,
90
+ { issuer: input.issuer, requestId: input.requestId, cutoff: now - DAY }
91
+ );
92
+ const batch = prior.records[0]?.get('b').properties;
93
+ if (batch) {
94
+ if (batch.requestHash !== input.requestHash)
95
+ throw new TRPCError({
96
+ code: 'CONFLICT',
97
+ message: 'requestId was used for a different batch.',
98
+ });
99
+ return {
100
+ batchId: batch.id,
101
+ status: getInboxBatchState(prior.records[0]!.get('states')),
102
+ createdAt: new Date(Number(batch.createdAt)).toISOString(),
103
+ };
104
+ }
105
+ }
106
+ const quota = locked.records[0]!.get('q').properties;
107
+ const expired = now - Number(quota.window) >= 3_600_000;
108
+ const used = expired ? 0 : Number(quota.used);
109
+ if (used + input.items.length > input.limit)
110
+ throw new TRPCError({
111
+ code: 'TOO_MANY_REQUESTS',
112
+ message: `${input.limit} inbox items per hour; retry after the current window expires (at most 3600 seconds).`,
113
+ });
114
+ await tx.run(
115
+ `MATCH (q:InboxBatchIssuer {id: $issuer}) SET q.used = $used, q.window = $window
116
+ CREATE (b:InboxBatch {id: $id, issuer: $issuer, createdAt: $now, requestHash: $hash, payload: $payload})
117
+ SET b.requestId = $requestId
118
+ WITH b UNWIND $items AS item
119
+ CREATE (b)-[:HAS_ITEM]->(:InboxBatchItem {id: item.id, batchId: $id,
120
+ index: item.index, replayKey: item.replayKey, duplicate: item.duplicate,
121
+ correlation: item.correlation,
122
+ state: 'QUEUED', attempts: 0, dispatchAt: 0})`,
123
+ {
124
+ issuer: input.issuer,
125
+ id,
126
+ now,
127
+ used: used + input.items.length,
128
+ window: expired ? now : Number(quota.window),
129
+ hash: input.requestHash,
130
+ payload: input.payload,
131
+ requestId: input.requestId ?? null,
132
+ items: input.items.map((item, index) => ({ ...item, index, id: `${id}:${index}` })),
133
+ }
134
+ );
135
+ return { batchId: id, status: 'QUEUED', createdAt: new Date(now).toISOString() };
136
+ });
137
+ };
138
+
139
+ export const readBatchJob = async (
140
+ id: string,
141
+ issuer: string
142
+ ): Promise<{ job: BatchJob; items: BatchItem[] }> => {
143
+ const result = await neogma.queryRunner.run(
144
+ `MATCH (b:InboxBatch {id: $id, issuer: $issuer})-[:HAS_ITEM]->(i)
145
+ RETURN b, i ORDER BY i.index`,
146
+ { id, issuer }
147
+ );
148
+ if (!result.records.length) throw new TRPCError({ code: 'NOT_FOUND' });
149
+ return {
150
+ job: result.records[0]!.get('b').properties,
151
+ items: result.records.map(r => r.get('i').properties),
152
+ };
153
+ };
154
+
155
+ /** Lock before testing ownership: concurrent SQS deliveries must not both enter issuance. */
156
+ export const claimBatchItem = async (
157
+ id: string,
158
+ owner: string
159
+ ): Promise<{ job: BatchJob; item: BatchItem } | undefined> =>
160
+ transaction(async tx => {
161
+ const result = await tx.run(
162
+ `MATCH (b:InboxBatch)-[:HAS_ITEM]->(i:InboxBatchItem {id: $id})
163
+ SET i.lock = coalesce(i.lock, 0) + 1 RETURN b, i`,
164
+ { id }
165
+ );
166
+ if (!result.records.length) return;
167
+ const item = result.records[0]!.get('i').properties as BatchItem;
168
+ if (item.state !== 'QUEUED') return;
169
+ await tx.run(
170
+ `MATCH (i:InboxBatchItem {id: $id}) SET i.state = 'PROCESSING', i.phase = 'PREPARING',
171
+ i.owner = $owner, i.leaseUntil = $lease, i.attempts = i.attempts + 1`,
172
+ { id, owner, lease: Date.now() + LEASE_MS }
173
+ );
174
+ return { job: result.records[0]!.get('b').properties, item };
175
+ });
176
+
177
+ const lockOwnedItem = async (
178
+ tx: ManagedTransaction,
179
+ id: string,
180
+ owner: string
181
+ ): Promise<BatchItem> => {
182
+ const result = await tx.run(
183
+ `MATCH (i:InboxBatchItem {id: $id}) SET i.lock = coalesce(i.lock, 0) + 1 RETURN i`,
184
+ { id }
185
+ );
186
+ const item = result.records[0]?.get('i').properties as BatchItem | undefined;
187
+ if (
188
+ !item ||
189
+ item.owner !== owner ||
190
+ item.state !== 'PROCESSING' ||
191
+ Number(item.leaseUntil) <= Date.now()
192
+ )
193
+ throw new Error('Inbox item lease lost');
194
+ return item;
195
+ };
196
+
197
+ export const markBatchIssuanceStarted = async (id: string, owner: string): Promise<void> =>
198
+ transaction(async tx => {
199
+ await lockOwnedItem(tx, id, owner);
200
+ await tx.run(`MATCH (i:InboxBatchItem {id: $id}) SET i.phase = 'ISSUING'`, { id });
201
+ });
202
+
203
+ /** Adapter retains the existing request fingerprint/CAS protocol with durable, encrypted records.
204
+ * Processing markers have no expiry. Orphaned internal markers are removed after job retention;
205
+ * client-keyed markers remain blocked. Only confirmed successes get the 24-hour replay expiry.
206
+ */
207
+ export const batchReplayStore = (
208
+ itemId: string,
209
+ owner: string,
210
+ replayKey: string
211
+ ): BatchReplayStore => ({
212
+ get: async (_key: string): Promise<string | null> => {
213
+ const result = await neogma.queryRunner.run(
214
+ `MATCH (r:InboxBatchReplay {id: $id})
215
+ WHERE r.expiresAt IS NULL OR r.expiresAt > $now RETURN r.value AS value`,
216
+ { id: replayKey, now: Date.now() }
217
+ );
218
+ const value = result.records[0]?.get('value');
219
+ return value ? decryptInboxCredential(value) : null;
220
+ },
221
+ setIfAbsent: async (_key: string, value: string, _ttl: number): Promise<'OK' | null> => {
222
+ const encrypted = await encryptInboxCredential(value);
223
+ return transaction(async tx => {
224
+ await lockOwnedItem(tx, itemId, owner);
225
+ const rows = await tx.run(
226
+ `MERGE (r:InboxBatchReplay {id: $id}) SET r.lock = coalesce(r.lock, 0) + 1 RETURN r`,
227
+ { id: replayKey }
228
+ );
229
+ const replay = rows.records[0]!.get('r').properties;
230
+ if (replay.value && (replay.expiresAt == null || Number(replay.expiresAt) > Date.now()))
231
+ return null;
232
+ await tx.run(
233
+ `MATCH (r:InboxBatchReplay {id: $id}) SET r.value = $value, r.marker = $marker,
234
+ r.itemId = $itemId, r.expiresAt = null`,
235
+ { id: replayKey, value: encrypted, marker: value, itemId }
236
+ );
237
+ return 'OK';
238
+ });
239
+ },
240
+ compareAndSet: async (
241
+ _key: string,
242
+ expected: string,
243
+ value: string | null,
244
+ ttl: number
245
+ ): Promise<boolean> => {
246
+ const encrypted = value === null ? null : await encryptInboxCredential(value);
247
+ return transaction(async tx => {
248
+ await lockOwnedItem(tx, itemId, owner);
249
+ const rows = await tx.run(
250
+ `MATCH (r:InboxBatchReplay {id: $id}) SET r.lock = coalesce(r.lock, 0) + 1 RETURN r.marker AS marker`,
251
+ { id: replayKey }
252
+ );
253
+ if (rows.records[0]?.get('marker') !== expected) return false;
254
+ if (value === null) {
255
+ await tx.run(`MATCH (r:InboxBatchReplay {id: $id}) DELETE r`, { id: replayKey });
256
+ // The shared helper only releases after an explicitly safe preflight failure.
257
+ await tx.run(`MATCH (i:InboxBatchItem {id: $itemId}) SET i.phase = 'PREPARING'`, {
258
+ itemId,
259
+ });
260
+ } else {
261
+ await tx.run(
262
+ `MATCH (r:InboxBatchReplay {id: $id}) SET r.value = $value,
263
+ r.marker = null, r.expiresAt = $expiry`,
264
+ { id: replayKey, value: encrypted, expiry: Date.now() + ttl * 1000 }
265
+ );
266
+ }
267
+ return true;
268
+ });
269
+ },
270
+ });
271
+
272
+ /** Terminal items no longer need the original credential payload, including uncertain outcomes.
273
+ * Client-keyed replay reservations are separate nodes and survive the 30-day job retention period.
274
+ */
275
+ const finalizeSettledBatch = async (tx: ManagedTransaction, id: string): Promise<void> => {
276
+ await tx.run(
277
+ `MATCH (b:InboxBatch {id: $id}) SET b.lock = coalesce(b.lock, 0) + 1
278
+ WITH b WHERE NOT EXISTS {
279
+ MATCH (b)-[:HAS_ITEM]->(i) WHERE i.state IN ['QUEUED', 'PROCESSING']
280
+ }
281
+ SET b.completedAt = coalesce(b.completedAt, $now) REMOVE b.payload`,
282
+ { id, now: Date.now() }
283
+ );
284
+ };
285
+
286
+ export const finishBatchItem = async (
287
+ id: string,
288
+ owner: string,
289
+ result: IssueInboxCredentialBatchItemResult
290
+ ): Promise<void> => {
291
+ const encrypted = await encryptInboxCredential(JSON.stringify(result));
292
+ await transaction(async tx => {
293
+ const item = await lockOwnedItem(tx, id, owner);
294
+ const unconfirmed = !result.success && item.phase === 'ISSUING';
295
+ const retry =
296
+ !result.success &&
297
+ (result.error.code === 'INTERNAL_SERVER_ERROR' ||
298
+ result.error.reason === 'IN_PROGRESS') &&
299
+ !unconfirmed &&
300
+ Number(item.attempts) < 5;
301
+ if (retry)
302
+ await tx.run(
303
+ `MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NOT NULL DELETE r`,
304
+ { id }
305
+ );
306
+ await tx.run(
307
+ `MATCH (i:InboxBatchItem {id: $id}) SET i.state = $state, i.result = $result,
308
+ i.owner = null, i.leaseUntil = null, i.dispatchAt = $dispatchAt`,
309
+ {
310
+ id,
311
+ state: retry ? 'QUEUED' : unconfirmed ? 'NEEDS_RECONCILIATION' : 'COMPLETED',
312
+ result: retry ? null : encrypted,
313
+ dispatchAt:
314
+ retry && !result.success && result.error.reason === 'IN_PROGRESS'
315
+ ? Date.now() + Math.min(60_000, 2_000 * 2 ** (Number(item.attempts) - 1))
316
+ : 0,
317
+ }
318
+ );
319
+ await finalizeSettledBatch(tx, item.batchId);
320
+ });
321
+ };
322
+
323
+ /** Claim dispatch leases before publishing. A lost publish acknowledgement causes a safe duplicate. */
324
+ export const takeInboxDispatches = async (): Promise<InboxDispatchLease[]> =>
325
+ transaction(async tx => {
326
+ const result = await tx.run(
327
+ `MATCH (i:InboxBatchItem) WHERE i.state = 'QUEUED' AND i.dispatchAt < $now
328
+ WITH i LIMIT 1000 SET i.lock = coalesce(i.lock, 0) + 1
329
+ WITH i WHERE i.state = 'QUEUED' AND i.dispatchAt < $now
330
+ SET i.dispatchAt = $next RETURN i.id AS id, i.dispatchAt AS dispatchAt`,
331
+ { now: Date.now(), next: Date.now() + 60_000 }
332
+ );
333
+ return result.records.map(r => ({
334
+ id: r.get('id'),
335
+ dispatchAt: Number(r.get('dispatchAt')),
336
+ }));
337
+ });
338
+
339
+ export const acknowledgeInboxDispatches = async (leases: InboxDispatchLease[]): Promise<void> => {
340
+ await neogma.queryRunner.run(
341
+ `UNWIND $leases AS lease MATCH (i:InboxBatchItem {id: lease.id})
342
+ SET i.lock = coalesce(i.lock, 0) + 1
343
+ WITH i, lease WHERE i.state = 'QUEUED' AND i.dispatchAt = lease.dispatchAt
344
+ SET i.dispatchAt = $next`,
345
+ { leases, next: Date.now() + 1_800_000 }
346
+ );
347
+ };
348
+
349
+ /** Poison messages become visible terminal outcomes instead of disappearing into the DLQ. */
350
+ export const deadLetterBatchItem = async (id: string): Promise<void> =>
351
+ transaction(async tx => {
352
+ const rows = await tx.run(
353
+ `MATCH (i:InboxBatchItem {id: $id}) SET i.lock = coalesce(i.lock, 0) + 1 RETURN i`,
354
+ { id }
355
+ );
356
+ const item = rows.records[0]?.get('i').properties as BatchItem | undefined;
357
+ if (!item || ['COMPLETED', 'NEEDS_RECONCILIATION'].includes(item.state)) return;
358
+ if (item.state === 'PROCESSING' && Number(item.leaseUntil) > Date.now())
359
+ throw new Error('Inbox item still has an active worker');
360
+ const uncertain = item.phase === 'ISSUING';
361
+ const replay = await tx.run(
362
+ `MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NULL RETURN r.value AS value`,
363
+ { id }
364
+ );
365
+ const saved = replay.records[0]?.get('value');
366
+ if (!uncertain)
367
+ await tx.run(
368
+ `MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NOT NULL DELETE r`,
369
+ { id }
370
+ );
371
+ await tx.run(
372
+ `MATCH (i:InboxBatchItem {id: $id}) SET i.state = $state, i.owner = null, i.leaseUntil = null, i.result = $result`,
373
+ {
374
+ id,
375
+ state: uncertain && !saved ? 'NEEDS_RECONCILIATION' : 'COMPLETED',
376
+ result: saved ?? null,
377
+ }
378
+ );
379
+ await finalizeSettledBatch(tx, item.batchId);
380
+ });
381
+
382
+ /** Recover bounded groups in short transactions without holding locks across the whole maintenance run. */
383
+ export const recoverInboxJobs = async (deadline = Date.now() + 20_000): Promise<void> => {
384
+ await ensureInboxBatchConstraints();
385
+ if (Date.now() >= deadline) return;
386
+ const expired = await neogma.queryRunner.run(
387
+ `MATCH (i:InboxBatchItem) WHERE i.state = 'PROCESSING' AND i.leaseUntil < $now
388
+ RETURN i.id AS id LIMIT 100`,
389
+ { now: Date.now() }
390
+ );
391
+ for (const row of expired.records) {
392
+ if (Date.now() >= deadline) break;
393
+ await transaction(async tx => {
394
+ const rows = await tx.run(
395
+ `MATCH (i:InboxBatchItem {id: $id})
396
+ SET i.lock = coalesce(i.lock, 0) + 1
397
+ WITH i WHERE i.state = 'PROCESSING' AND i.leaseUntil < $now RETURN i`,
398
+ { id: row.get('id'), now: Date.now() }
399
+ );
400
+ const item = rows.records[0]?.get('i').properties as BatchItem | undefined;
401
+ if (!item) return;
402
+ // A persisted success is authoritative even if the worker lost its completion reply.
403
+ const replay = await tx.run(
404
+ `MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NULL RETURN r.value AS value`,
405
+ { id: item.id }
406
+ );
407
+ const saved = replay.records[0]?.get('value');
408
+ const state = saved
409
+ ? 'COMPLETED'
410
+ : item.phase === 'ISSUING'
411
+ ? 'NEEDS_RECONCILIATION'
412
+ : Number(item.attempts) >= 5
413
+ ? 'COMPLETED'
414
+ : 'QUEUED';
415
+ if (item.phase !== 'ISSUING')
416
+ await tx.run(
417
+ `MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NOT NULL DELETE r`,
418
+ { id: item.id }
419
+ );
420
+ await tx.run(
421
+ `MATCH (i:InboxBatchItem {id: $id}) SET i.state = $state, i.owner = null,
422
+ i.leaseUntil = null, i.dispatchAt = 0, i.result = $result`,
423
+ { id: item.id, state, result: saved ?? null }
424
+ );
425
+ await finalizeSettledBatch(tx, item.batchId);
426
+ });
427
+ }
428
+ if (Date.now() < deadline) {
429
+ // Also finalize older terminal batches that predate immediate terminal cleanup.
430
+ const settled = await neogma.queryRunner.run(
431
+ `MATCH (b:InboxBatch) WHERE b.completedAt IS NULL
432
+ AND NOT EXISTS { MATCH (b)-[:HAS_ITEM]->(i) WHERE i.state IN ['QUEUED', 'PROCESSING'] }
433
+ RETURN b.id AS id LIMIT 100`
434
+ );
435
+ for (const row of settled.records) {
436
+ if (Date.now() >= deadline) break;
437
+ await transaction(tx => finalizeSettledBatch(tx, row.get('id')));
438
+ }
439
+ }
440
+
441
+ // Leave room for each transaction to finish; checking only for a future deadline can
442
+ // start a large delete immediately before Lambda shutdown. Recheck between steps.
443
+ if (deadline - Date.now() > CLEANUP_MARGIN_MS)
444
+ await transaction(async tx => {
445
+ await tx.run(
446
+ `MATCH (b:InboxBatch) WHERE b.completedAt < $cutoff WITH b LIMIT 25
447
+ MATCH (b)-[:HAS_ITEM]->(i) DETACH DELETE i, b`,
448
+ { cutoff: Date.now() - 30 * DAY }
449
+ );
450
+ });
451
+ if (deadline - Date.now() > CLEANUP_MARGIN_MS)
452
+ await transaction(async tx => {
453
+ await tx.run(
454
+ `MATCH (r:InboxBatchReplay) WHERE r.expiresAt < $now WITH r LIMIT 1000 DELETE r`,
455
+ { now: Date.now() }
456
+ );
457
+ });
458
+ if (deadline - Date.now() > CLEANUP_MARGIN_MS)
459
+ await transaction(async tx => {
460
+ // Internal keys cannot be supplied by clients, and messages cannot claim a deleted
461
+ // item. Client-keyed reservations must continue blocking uncertain reissuance.
462
+ await tx.run(
463
+ `MATCH (r:InboxBatchReplay)
464
+ WHERE r.id STARTS WITH 'internal:' AND r.expiresAt IS NULL
465
+ AND NOT EXISTS { MATCH (i:InboxBatchItem) WHERE i.id = r.itemId }
466
+ WITH r LIMIT 1000 DELETE r`
467
+ );
468
+ });
469
+ };
@@ -9,12 +9,14 @@ import { getContactMethodByValue } from '@accesslayer/contact-method/read';
9
9
  import { createContactMethod } from '@accesslayer/contact-method/create';
10
10
  import { ProfileType } from 'types/profile';
11
11
  import { encryptInboxCredential } from '@helpers/inbox-encryption.helpers';
12
+ import { createPendingInboxRefresh } from './refresh';
12
13
  import { parseCredentialMeta } from '@helpers/credential-meta.helpers';
13
14
 
14
15
  export const DEFAULT_INBOX_EXPIRY_DAYS = 30;
15
16
 
16
17
  export const createInboxCredential = async (input: {
17
18
  credential: string;
19
+ refresh?: { aggregate: Record<string, unknown>; issueKey: string; requestDigest: string };
18
20
  /** Only set after the normal delivery helper has durably stored the credential. */
19
21
  delivered?: boolean;
20
22
  isSigned: boolean;
@@ -39,7 +41,7 @@ export const createInboxCredential = async (input: {
39
41
  throw new Error('expiresInDays must be an integer between 1 and 720');
40
42
  }
41
43
 
42
- const id = uuid();
44
+ let id = uuid();
43
45
  const expiresAt = new Date();
44
46
  expiresAt.setDate(expiresAt.getDate() + (input.expiresInDays ?? DEFAULT_INBOX_EXPIRY_DAYS));
45
47
  // Encrypt before persisting so the credential itself is not stored in plaintext in the inbox.
@@ -50,6 +52,7 @@ export const createInboxCredential = async (input: {
50
52
 
51
53
  const inboxCredentialData = {
52
54
  id,
55
+ ...(input.refresh ? { refreshId: input.refresh.aggregate.refreshId } : {}),
53
56
  credential: encryptedCredential,
54
57
  ...credentialMeta,
55
58
  isSigned: input.isSigned,
@@ -76,28 +79,55 @@ export const createInboxCredential = async (input: {
76
79
  ...(input.guardianStatus ? { guardianStatus: input.guardianStatus } : {}),
77
80
  };
78
81
 
79
- await new QueryBuilder(
80
- new BindParam({
81
- params: flattenObject(inboxCredentialData),
82
- issuerProfileId: input.issuerProfile.profileId,
83
- timestamp: new Date().toISOString(),
84
- })
85
- )
86
- .create({ model: InboxCredential, identifier: 'inboxCredential' })
87
- .set('inboxCredential += $params')
88
- .run();
82
+ if (input.refresh) {
83
+ const created = await createPendingInboxRefresh({
84
+ inbox: Object.fromEntries(
85
+ Object.entries(flattenObject(inboxCredentialData)).filter(
86
+ ([, value]) => value !== undefined
87
+ )
88
+ ),
89
+ aggregate: Object.fromEntries(
90
+ Object.entries({ ...input.refresh.aggregate, inboxCredentialId: id }).filter(
91
+ ([, value]) => value !== undefined
92
+ )
93
+ ),
94
+ issueKey: input.refresh.issueKey,
95
+ requestDigest: input.refresh.requestDigest,
96
+ });
97
+ id = created.id;
98
+ } else {
99
+ await new QueryBuilder(
100
+ new BindParam({
101
+ params: flattenObject(inboxCredentialData),
102
+ issuerProfileId: input.issuerProfile.profileId,
103
+ timestamp: new Date().toISOString(),
104
+ })
105
+ )
106
+ .create({ model: InboxCredential, identifier: 'inboxCredential' })
107
+ .set('inboxCredential += $params')
108
+ .run();
109
+ }
89
110
 
90
111
  const contactMethod = await getContactMethodByValue(
91
112
  input.recipient.type,
92
113
  input.recipient.value
93
114
  );
94
115
  if (!contactMethod) {
95
- await createContactMethod({
96
- type: input.recipient.type,
97
- value: input.recipient.value,
98
- isVerified: false,
99
- isPrimary: false,
100
- });
116
+ try {
117
+ await createContactMethod({
118
+ type: input.recipient.type,
119
+ value: input.recipient.value,
120
+ isVerified: false,
121
+ isPrimary: false,
122
+ });
123
+ } catch (error) {
124
+ // A concurrent idempotent issue may have created the same contact first.
125
+ if (
126
+ !input.refresh ||
127
+ !(await getContactMethodByValue(input.recipient.type, input.recipient.value))
128
+ )
129
+ throw error;
130
+ }
101
131
  }
102
132
 
103
133
  // Create relationships SEQUENTIALLY to prevent deadlocks
@@ -112,7 +142,8 @@ export const createInboxCredential = async (input: {
112
142
  .where('ic.id = $inboxId')
113
143
  .match('(profile:Profile)')
114
144
  .where('profile.profileId = $profileId')
115
- .create('(profile)-[:CREATED_INBOX_CREDENTIAL { timestamp: $timestamp }]->(ic)')
145
+ .merge('(profile)-[created:CREATED_INBOX_CREDENTIAL]->(ic)')
146
+ .set('created.timestamp = coalesce(created.timestamp, $timestamp)')
116
147
  .run();
117
148
 
118
149
  await new QueryBuilder(
@@ -127,7 +158,8 @@ export const createInboxCredential = async (input: {
127
158
  .where('ic.id = $inboxId')
128
159
  .match('(contactMethod:ContactMethod)')
129
160
  .where('contactMethod.type = $type AND contactMethod.value = $value')
130
- .create('(ic)-[:ADDRESSED_TO { timestamp: $timestamp }]->(contactMethod)')
161
+ .merge('(ic)-[addressed:ADDRESSED_TO]->(contactMethod)')
162
+ .set('addressed.timestamp = coalesce(addressed.timestamp, $timestamp)')
131
163
  .run();
132
164
 
133
165
  return (await getInboxCredentialById(id))!;