@learncard/network-brain-service 3.18.0 → 3.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/.env.example +38 -2
  2. package/CHANGELOG.md +118 -0
  3. package/README.md +42 -0
  4. package/compose.inbox.yml +7 -0
  5. package/dist/accesslayer/auth-grant/read.d.ts +1 -0
  6. package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
  7. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  8. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  9. package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
  10. package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
  11. package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
  12. package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
  13. package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
  14. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  15. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  16. package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
  17. package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
  18. package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
  19. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  20. package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
  21. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
  22. package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
  23. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  24. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  25. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  26. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  27. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  29. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  30. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  31. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  32. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  33. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  34. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  35. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  36. package/dist/accesslayer/share-link/helpers.d.ts +56 -0
  37. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  38. package/dist/accesslayer/share-link/index.d.ts +12 -0
  39. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  40. package/dist/accesslayer/share-link/list.d.ts +5 -0
  41. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  42. package/dist/accesslayer/share-link/read.d.ts +5 -0
  43. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  44. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  45. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  46. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  47. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  48. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  49. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  50. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  51. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  52. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  53. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  54. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  55. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  56. package/dist/accesslayer/share-link/types.d.ts +370 -0
  57. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  58. package/dist/app.d.ts +4 -0
  59. package/dist/app.d.ts.map +1 -1
  60. package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
  61. package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
  62. package/dist/cache/atomic-counter.test.d.ts +2 -0
  63. package/dist/cache/atomic-counter.test.d.ts.map +1 -0
  64. package/dist/cache/index.d.ts +1 -1
  65. package/dist/cache/index.d.ts.map +1 -1
  66. package/dist/config/environment.d.ts +55 -0
  67. package/dist/config/environment.d.ts.map +1 -1
  68. package/dist/constants/auth-grant.d.ts +2 -0
  69. package/dist/constants/auth-grant.d.ts.map +1 -1
  70. package/dist/credential-refresh.d.ts.map +1 -1
  71. package/dist/helpers/activity.helpers.d.ts +4 -0
  72. package/dist/helpers/activity.helpers.d.ts.map +1 -1
  73. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  74. package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
  75. package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
  76. package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
  77. package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
  78. package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
  79. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
  80. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
  81. package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
  82. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
  83. package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
  84. package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
  85. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
  86. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
  87. package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
  88. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  89. package/dist/helpers/credential.helpers.d.ts +2 -1
  90. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  91. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  92. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  93. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  94. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  95. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  96. package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
  97. package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
  98. package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
  99. package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
  100. package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
  101. package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
  102. package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
  103. package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
  104. package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
  105. package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
  106. package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
  107. package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
  108. package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
  109. package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
  110. package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
  111. package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
  112. package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
  113. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
  114. package/dist/helpers/inbox.helpers.d.ts +9 -1
  115. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  116. package/dist/helpers/notificationMessages.d.ts +1 -1
  117. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  118. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  119. package/dist/helpers/presentation-metadata.d.ts +4 -0
  120. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  121. package/dist/helpers/presentation.helpers.d.ts +1 -1
  122. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  123. package/dist/helpers/profile.helpers.d.ts +1 -0
  124. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  125. package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
  126. package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
  127. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
  128. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  129. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  130. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  131. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  132. package/dist/helpers/share-content-client/client.d.ts +5 -0
  133. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  134. package/dist/helpers/share-content-client/config.d.ts +11 -0
  135. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  136. package/dist/helpers/share-content-client/index.d.ts +6 -0
  137. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  138. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  139. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  140. package/dist/helpers/share-content-client/types.d.ts +145 -0
  141. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  142. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  143. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  144. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  145. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  146. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  147. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  148. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  149. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  150. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  151. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  152. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  153. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  154. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  155. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  156. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  157. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  158. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  159. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  160. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  161. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  162. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  163. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  164. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  165. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  166. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  167. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  168. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  169. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  170. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  171. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  172. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  173. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  174. package/dist/helpers/share-link-maintenance/runner.d.ts +19 -0
  175. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  176. package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -0
  177. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  178. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  179. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  180. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  181. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  182. package/dist/helpers/share-link-maintenance/types.d.ts +53 -0
  183. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  184. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  185. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  186. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  187. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  188. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  189. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  190. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  191. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  192. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  193. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  194. package/dist/helpers/share-link-passcode.d.ts +6 -0
  195. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  196. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  197. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  198. package/dist/helpers/share-link-policy/production.d.ts +13 -0
  199. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  200. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  201. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  202. package/dist/helpers/share-link-policy/types.d.ts +16 -0
  203. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  204. package/dist/helpers/share-link-public/config.d.ts +13 -0
  205. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  206. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  207. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  208. package/dist/helpers/shim.d.ts +1 -1
  209. package/dist/helpers/shim.d.ts.map +1 -1
  210. package/dist/inbox-queue-local.d.ts +2 -0
  211. package/dist/inbox-queue-local.d.ts.map +1 -0
  212. package/dist/index.js +2 -2
  213. package/dist/models/AuthGrant.d.ts.map +1 -1
  214. package/dist/models/CredentialActivity.d.ts +1 -0
  215. package/dist/models/CredentialActivity.d.ts.map +1 -1
  216. package/dist/models/CredentialRefresh.d.ts +5 -2
  217. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  218. package/dist/models/InboxCredential.d.ts +1 -0
  219. package/dist/models/InboxCredential.d.ts.map +1 -1
  220. package/dist/models/Presentation.d.ts +2 -0
  221. package/dist/models/Presentation.d.ts.map +1 -1
  222. package/dist/models/Profile.d.ts +2 -0
  223. package/dist/models/Profile.d.ts.map +1 -1
  224. package/dist/models/ShareLink.d.ts +44 -0
  225. package/dist/models/ShareLink.d.ts.map +1 -0
  226. package/dist/models/ShareViewReceipt.d.ts +22 -0
  227. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  228. package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
  229. package/dist/models/index.d.ts +1 -0
  230. package/dist/models/index.d.ts.map +1 -1
  231. package/dist/models/share-link-constraints.d.ts +3 -0
  232. package/dist/models/share-link-constraints.d.ts.map +1 -0
  233. package/dist/openapi.d.ts.map +1 -1
  234. package/dist/public-share-link-http.d.ts +4 -0
  235. package/dist/public-share-link-http.d.ts.map +1 -0
  236. package/dist/routes/act-as.test.d.ts +2 -0
  237. package/dist/routes/act-as.test.d.ts.map +1 -0
  238. package/dist/routes/activity.d.ts +3 -0
  239. package/dist/routes/activity.d.ts.map +1 -1
  240. package/dist/routes/app-store.d.ts.map +1 -1
  241. package/dist/routes/auth-grants.d.ts +10 -0
  242. package/dist/routes/auth-grants.d.ts.map +1 -1
  243. package/dist/routes/boosts.d.ts +494 -0
  244. package/dist/routes/boosts.d.ts.map +1 -1
  245. package/dist/routes/contracts.d.ts +2 -2
  246. package/dist/routes/contracts.d.ts.map +1 -1
  247. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  248. package/dist/routes/credentials.d.ts +1 -1
  249. package/dist/routes/credentials.d.ts.map +1 -1
  250. package/dist/routes/inbox.d.ts +953 -11
  251. package/dist/routes/inbox.d.ts.map +1 -1
  252. package/dist/routes/index.d.ts +154 -85
  253. package/dist/routes/index.d.ts.map +1 -1
  254. package/dist/routes/presentations.d.ts +1 -0
  255. package/dist/routes/presentations.d.ts.map +1 -1
  256. package/dist/routes/profile-manager.d.ts.map +1 -1
  257. package/dist/routes/profiles.d.ts.map +1 -1
  258. package/dist/routes/public-share-links.d.ts +230 -0
  259. package/dist/routes/public-share-links.d.ts.map +1 -0
  260. package/dist/routes/share-links.d.ts +844 -0
  261. package/dist/routes/share-links.d.ts.map +1 -0
  262. package/dist/routes/workflows.d.ts.map +1 -1
  263. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  264. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
  265. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
  266. package/dist/services/registry/registry.factory.d.ts.map +1 -1
  267. package/dist/types/activity.d.ts +4 -0
  268. package/dist/types/activity.d.ts.map +1 -1
  269. package/dist/types/consentflowcontract.d.ts +1 -1
  270. package/dist/types/credential-refresh.d.ts +10 -1
  271. package/dist/types/credential-refresh.d.ts.map +1 -1
  272. package/dist/types/inbox-batch.d.ts +38 -0
  273. package/dist/types/inbox-batch.d.ts.map +1 -0
  274. package/inbox-e2e-setup.ts +53 -1
  275. package/inbox-elasticmq.conf +10 -0
  276. package/lambda.ts +64 -16
  277. package/package.json +24 -16
  278. package/scripts/seed-demo-persona.ts +425 -0
  279. package/scripts/setup-inbox-queues.ts +60 -0
  280. package/serverless-local.yml +2 -0
  281. package/serverless.yml +111 -0
  282. package/shareLinkMaintenanceLambda.ts +49 -0
  283. package/src/accesslayer/auth-grant/read.ts +2 -1
  284. package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
  285. package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
  286. package/src/accesslayer/credential-activity/create.ts +1 -0
  287. package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
  288. package/src/accesslayer/credential-refresh/index.ts +1 -0
  289. package/src/accesslayer/credential-refresh/read.ts +8 -2
  290. package/src/accesslayer/credential-refresh/update.ts +4 -0
  291. package/src/accesslayer/inbox-batch/store.ts +469 -0
  292. package/src/accesslayer/inbox-credential/create.ts +51 -19
  293. package/src/accesslayer/inbox-credential/refresh.ts +221 -0
  294. package/src/accesslayer/inbox-credential/update.ts +1 -1
  295. package/src/accesslayer/presentation/read.ts +57 -25
  296. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  297. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  298. package/src/accesslayer/share-link/abandon.ts +100 -0
  299. package/src/accesslayer/share-link/cleanup.ts +271 -0
  300. package/src/accesslayer/share-link/errors.ts +32 -0
  301. package/src/accesslayer/share-link/finalize.ts +265 -0
  302. package/src/accesslayer/share-link/helpers.ts +419 -0
  303. package/src/accesslayer/share-link/index.ts +11 -0
  304. package/src/accesslayer/share-link/list.ts +162 -0
  305. package/src/accesslayer/share-link/read.ts +107 -0
  306. package/src/accesslayer/share-link/receipt.ts +548 -0
  307. package/src/accesslayer/share-link/recovery.ts +545 -0
  308. package/src/accesslayer/share-link/reserve.ts +684 -0
  309. package/src/accesslayer/share-link/revoke.ts +197 -0
  310. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  311. package/src/accesslayer/share-link/transaction.ts +126 -0
  312. package/src/accesslayer/share-link/types.ts +601 -0
  313. package/src/app.ts +6 -0
  314. package/src/cache/atomic-counter.test-helpers.ts +27 -0
  315. package/src/cache/atomic-counter.test.ts +6 -0
  316. package/src/cache/index.ts +25 -13
  317. package/src/config/environment.ts +40 -0
  318. package/src/constants/auth-grant.ts +5 -0
  319. package/src/credential-refresh.ts +5 -1
  320. package/src/docker-entry.ts +65 -0
  321. package/src/helpers/activity.helpers.ts +4 -0
  322. package/src/helpers/app-counter.helpers.ts +1 -1
  323. package/src/helpers/boost.helpers.ts +4 -31
  324. package/src/helpers/contact-method.helpers.test.ts +43 -0
  325. package/src/helpers/contact-method.helpers.ts +3 -4
  326. package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
  327. package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
  328. package/src/helpers/credential-refresh-email.helpers.ts +201 -0
  329. package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
  330. package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
  331. package/src/helpers/credential-refresh.helpers.ts +375 -99
  332. package/src/helpers/credential.helpers.ts +4 -1
  333. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  334. package/src/helpers/credentialSubject.helpers.ts +40 -0
  335. package/src/helpers/finalize-inbox.helpers.ts +29 -14
  336. package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
  337. package/src/helpers/inbox-batch-http.helpers.ts +150 -0
  338. package/src/helpers/inbox-batch-status.helpers.ts +11 -0
  339. package/src/helpers/inbox-batch.helpers.test.ts +668 -0
  340. package/src/helpers/inbox-batch.helpers.ts +374 -0
  341. package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
  342. package/src/helpers/inbox-queue.helpers.test.ts +253 -0
  343. package/src/helpers/inbox-queue.helpers.ts +372 -0
  344. package/src/helpers/inbox-refresh.helpers.ts +432 -0
  345. package/src/helpers/inbox.helpers.ts +275 -36
  346. package/src/helpers/notificationMessages.ts +18 -1
  347. package/src/helpers/notifications.helpers.ts +4 -2
  348. package/src/helpers/presentation-metadata.ts +41 -0
  349. package/src/helpers/presentation.helpers.ts +27 -20
  350. package/src/helpers/profile.helpers.ts +7 -0
  351. package/src/helpers/rateLimit.helpers.ts +2 -5
  352. package/src/helpers/refresh-send-intent.helpers.ts +273 -0
  353. package/src/helpers/share-content-client/adapters.ts +55 -0
  354. package/src/helpers/share-content-client/canonical.ts +25 -0
  355. package/src/helpers/share-content-client/client.ts +440 -0
  356. package/src/helpers/share-content-client/config.ts +189 -0
  357. package/src/helpers/share-content-client/index.ts +27 -0
  358. package/src/helpers/share-content-client/responses.ts +273 -0
  359. package/src/helpers/share-content-client/types.ts +253 -0
  360. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  361. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  362. package/src/helpers/share-link-coordinator/coordinator.ts +714 -0
  363. package/src/helpers/share-link-coordinator/index.ts +8 -0
  364. package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -0
  365. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  366. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  367. package/src/helpers/share-link-coordinator/types.ts +353 -0
  368. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  369. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  370. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  371. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  372. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  373. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  374. package/src/helpers/share-link-maintenance/config.ts +312 -0
  375. package/src/helpers/share-link-maintenance/index.ts +24 -0
  376. package/src/helpers/share-link-maintenance/runner.ts +223 -0
  377. package/src/helpers/share-link-maintenance/runtime.ts +334 -0
  378. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  379. package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
  380. package/src/helpers/share-link-maintenance/types.ts +89 -0
  381. package/src/helpers/share-link-owner/config.ts +48 -0
  382. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  383. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  384. package/src/helpers/share-link-owner-projection.ts +52 -0
  385. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  386. package/src/helpers/share-link-passcode.ts +47 -0
  387. package/src/helpers/share-link-policy/index.ts +7 -0
  388. package/src/helpers/share-link-policy/production.ts +88 -0
  389. package/src/helpers/share-link-policy/resolver.ts +108 -0
  390. package/src/helpers/share-link-policy/types.ts +48 -0
  391. package/src/helpers/share-link-public/config.ts +41 -0
  392. package/src/helpers/share-link-view-notification.ts +18 -0
  393. package/src/helpers/shim.ts +32 -31
  394. package/src/inbox-queue-local.ts +28 -0
  395. package/src/models/AuthGrant.ts +1 -0
  396. package/src/models/CredentialActivity.ts +3 -5
  397. package/src/models/CredentialRefresh.ts +4 -2
  398. package/src/models/InboxCredential.ts +2 -0
  399. package/src/models/Presentation.ts +2 -2
  400. package/src/models/Profile.ts +3 -2
  401. package/src/models/ShareLink.ts +130 -0
  402. package/src/models/ShareViewReceipt.ts +69 -0
  403. package/src/models/credential-refresh-constraints.test.ts +8 -3
  404. package/src/models/credential-refresh-constraints.ts +5 -0
  405. package/src/models/index.ts +2 -0
  406. package/src/models/share-link-constraints.ts +93 -0
  407. package/src/openapi.ts +8 -0
  408. package/src/public-share-link-http.ts +49 -0
  409. package/src/routes/act-as.test.ts +239 -0
  410. package/src/routes/app-store.ts +4 -13
  411. package/src/routes/auth-grants.ts +13 -11
  412. package/src/routes/boosts.ts +916 -103
  413. package/src/routes/contracts.ts +2 -11
  414. package/src/routes/credential-refreshes.test.ts +8 -2
  415. package/src/routes/credential-refreshes.ts +4 -1
  416. package/src/routes/credentials.ts +2 -1
  417. package/src/routes/inbox.ts +95 -49
  418. package/src/routes/index.ts +216 -46
  419. package/src/routes/presentations.ts +19 -3
  420. package/src/routes/profile-manager.ts +10 -2
  421. package/src/routes/profiles.ts +17 -6
  422. package/src/routes/public-share-links.ts +814 -0
  423. package/src/routes/share-links.ts +640 -0
  424. package/src/routes/skills.ts +1 -1
  425. package/src/routes/workflows.ts +43 -24
  426. package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
  427. package/src/services/delivery/adapters/postmark.adapter.ts +16 -1
  428. package/src/services/registry/registry.factory.ts +2 -1
  429. package/src/types/activity.ts +2 -0
  430. package/src/types/credential-refresh.ts +21 -2
  431. package/src/types/inbox-batch.ts +46 -0
  432. package/test/atomic-counter.e2e.ts +12 -0
  433. package/test/auth-grant.spec.ts +21 -21
  434. package/test/credential-refresh-email.spec.ts +663 -0
  435. package/test/credential-refresh-publication.spec.ts +2 -0
  436. package/test/dids.spec.ts +10 -1
  437. package/test/guardian-gated-routes.spec.ts +13 -0
  438. package/test/helpers/inbox-e2e-environment.ts +6 -0
  439. package/test/helpers/share-link-fixtures.ts +94 -0
  440. package/test/inbox-batch.spec.ts +1338 -0
  441. package/test/inbox-refresh.spec.ts +534 -0
  442. package/test/managed-recipient-inbox.spec.ts +130 -0
  443. package/test/notificationMessages.spec.ts +13 -0
  444. package/test/openapi.spec.ts +61 -1
  445. package/test/presentations.spec.ts +54 -6
  446. package/test/profiles.spec.ts +67 -1
  447. package/test/refresh-send.spec.ts +1799 -0
  448. package/test/registry.spec.ts +56 -11
  449. package/test/share-content-client.contract.spec.ts +365 -0
  450. package/test/share-content-client.unit.spec.ts +462 -0
  451. package/test/share-link-abuse.unit.spec.ts +65 -0
  452. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  453. package/test/share-link-coordinator.unit.spec.ts +990 -0
  454. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  455. package/test/share-link-lifecycle.neo4j.spec.ts +1268 -0
  456. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  457. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  458. package/test/share-link-list.neo4j.spec.ts +324 -0
  459. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  460. package/test/share-link-maintenance.unit.spec.ts +1113 -0
  461. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  462. package/test/share-link-owner-route.unit.spec.ts +752 -0
  463. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  464. package/test/share-link-passcode.unit.spec.ts +29 -0
  465. package/test/share-link-policy.unit.spec.ts +135 -0
  466. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  467. package/test/share-link-production-policy.unit.spec.ts +77 -0
  468. package/test/share-link-public-config.unit.spec.ts +64 -0
  469. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  470. package/test/share-link-public-route.unit.spec.ts +800 -0
  471. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  472. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  473. package/test/share-link-recovery.unit.spec.ts +726 -0
  474. package/vitest.config.ts +10 -0
  475. package/vitest.inbox-e2e.config.ts +5 -1
  476. package/vitest.integration.config.ts +2 -0
  477. package/vitest.share-links-neo4j.config.ts +19 -0
  478. package/vitest.share-links-unit.config.ts +28 -0
@@ -1,3 +1,4 @@
1
+ import type { TenantBranding } from '@learncard/email-templates';
1
2
  import { TRPCError } from '@trpc/server';
2
3
  import { UnsignedVC, VC, JWE, LCNNotificationTypeEnumValidator } from '@learncard/types';
3
4
 
@@ -83,7 +84,8 @@ export const acceptCredential = async (
83
84
  uri: string,
84
85
  options: { skipNotification?: boolean; metadata?: Record<string, unknown> } = {
85
86
  skipNotification: false,
86
- }
87
+ },
88
+ branding?: Partial<TenantBranding>
87
89
  ): Promise<boolean> => {
88
90
  const { id, type } = getUriParts(uri);
89
91
 
@@ -174,6 +176,7 @@ export const acceptCredential = async (
174
176
  credentialNodeId: pendingVc.target.id,
175
177
  issuerProfile: sourceProfile,
176
178
  holderProfile: profile,
179
+ branding,
177
180
  });
178
181
  } catch (error) {
179
182
  console.error(
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { UnsignedVC } from '@learncard/types';
3
+
4
+ import { setCredentialSubjectIds } from './credentialSubject.helpers';
5
+
6
+ const collectSubjectIds = (credential: Record<string, unknown>): string[] => {
7
+ const subject = credential.credentialSubject;
8
+ const subjects = Array.isArray(subject) ? subject : [subject];
9
+
10
+ return subjects.flatMap(currentSubject => {
11
+ if (!currentSubject || typeof currentSubject !== 'object') return [];
12
+
13
+ const subjectRecord = currentSubject as Record<string, unknown>;
14
+ const embedded = subjectRecord.verifiableCredential;
15
+ const embeddedCredentials = Array.isArray(embedded) ? embedded : embedded ? [embedded] : [];
16
+
17
+ return [
18
+ subjectRecord.id as string,
19
+ ...embeddedCredentials.flatMap(nested =>
20
+ collectSubjectIds(nested as Record<string, unknown>)
21
+ ),
22
+ ];
23
+ });
24
+ };
25
+
26
+ describe('setCredentialSubjectIds', () => {
27
+ it('patches every CLR learner subject without rewriting achievement identities', () => {
28
+ const credential = {
29
+ credentialSubject: {
30
+ id: 'did:example:old-root',
31
+ achievement: {
32
+ id: 'https://example.edu/transcript',
33
+ creator: { id: 'https://example.edu' },
34
+ },
35
+ verifiableCredential: [
36
+ {
37
+ credentialSubject: {
38
+ id: 'did:example:old-course',
39
+ achievement: {
40
+ id: 'https://example.edu/courses/imd-120',
41
+ creator: { id: 'https://example.edu/departments/design' },
42
+ },
43
+ verifiableCredential: {
44
+ credentialSubject: { id: 'did:example:old-assessment' },
45
+ },
46
+ },
47
+ },
48
+ ],
49
+ },
50
+ } as unknown as UnsignedVC;
51
+
52
+ setCredentialSubjectIds(credential, 'did:example:learner');
53
+
54
+ expect(collectSubjectIds(credential as unknown as Record<string, unknown>)).toEqual([
55
+ 'did:example:learner',
56
+ 'did:example:learner',
57
+ 'did:example:learner',
58
+ ]);
59
+ expect(credential.credentialSubject).toMatchObject({
60
+ achievement: {
61
+ id: 'https://example.edu/transcript',
62
+ creator: { id: 'https://example.edu' },
63
+ },
64
+ verifiableCredential: [
65
+ {
66
+ credentialSubject: {
67
+ achievement: {
68
+ id: 'https://example.edu/courses/imd-120',
69
+ creator: { id: 'https://example.edu/departments/design' },
70
+ },
71
+ },
72
+ },
73
+ ],
74
+ });
75
+ });
76
+
77
+ it('preserves signed embedded credentials while patching unsigned subjects', () => {
78
+ const credential = {
79
+ credentialSubject: {
80
+ id: 'did:example:old-root',
81
+ verifiableCredential: [
82
+ {
83
+ credentialSubject: { id: 'did:example:signed-subject' },
84
+ proof: { type: 'DataIntegrityProof', proofValue: 'signed-value' },
85
+ },
86
+ {
87
+ credentialSubject: { id: 'did:example:unsigned-subject' },
88
+ },
89
+ ],
90
+ },
91
+ } as unknown as UnsignedVC;
92
+
93
+ setCredentialSubjectIds(credential, 'did:example:learner');
94
+
95
+ const subject = credential.credentialSubject as Record<string, unknown>;
96
+ const embedded = subject.verifiableCredential as Array<Record<string, unknown>>;
97
+
98
+ expect(subject.id).toBe('did:example:learner');
99
+ expect(embedded[0]?.credentialSubject).toEqual({
100
+ id: 'did:example:signed-subject',
101
+ });
102
+ expect(embedded[1]?.credentialSubject).toEqual({
103
+ id: 'did:example:learner',
104
+ });
105
+ });
106
+
107
+ it('creates an outer credential subject when a legacy template omits it', () => {
108
+ const credential = {} as UnsignedVC;
109
+
110
+ setCredentialSubjectIds(credential, 'did:example:learner');
111
+
112
+ expect(credential.credentialSubject).toEqual({ id: 'did:example:learner' });
113
+ });
114
+ });
@@ -0,0 +1,40 @@
1
+ import type { UnsignedVC, VC } from '@learncard/types';
2
+
3
+ const setSubjectIds = (subject: unknown, subjectDid: string): void => {
4
+ if (Array.isArray(subject)) {
5
+ subject.forEach(nestedSubject => setSubjectIds(nestedSubject, subjectDid));
6
+ return;
7
+ }
8
+
9
+ if (!subject || typeof subject !== 'object') return;
10
+
11
+ const mutableSubject = subject as Record<string, unknown>;
12
+ mutableSubject.id = subjectDid;
13
+
14
+ const embeddedCredentials = mutableSubject.verifiableCredential;
15
+ const credentials = Array.isArray(embeddedCredentials)
16
+ ? embeddedCredentials
17
+ : [embeddedCredentials];
18
+
19
+ credentials.forEach(credential => {
20
+ if (!credential || typeof credential !== 'object' || Array.isArray(credential)) return;
21
+
22
+ const credentialRecord = credential as Record<string, unknown>;
23
+ if (credentialRecord.proof !== undefined) return;
24
+
25
+ setSubjectIds(credentialRecord.credentialSubject, subjectDid);
26
+ });
27
+ };
28
+
29
+ /**
30
+ * Sets the recipient DID on the outer credential and unsigned credentials nested in a CLR.
31
+ * Signed embedded credentials are immutable because changing their subject invalidates the proof.
32
+ */
33
+ export const setCredentialSubjectIds = (credential: UnsignedVC | VC, subjectDid: string): void => {
34
+ if (!credential.credentialSubject) {
35
+ credential.credentialSubject = { id: subjectDid };
36
+ return;
37
+ }
38
+
39
+ setSubjectIds(credential.credentialSubject, subjectDid);
40
+ };
@@ -1,3 +1,4 @@
1
+ import { finalizeInboxRefresh } from './inbox-refresh.helpers';
1
2
  import {
2
3
  LCNNotificationTypeEnumValidator,
3
4
  LCNInboxStatusEnumValidator,
@@ -79,10 +80,21 @@ export async function finalizeInboxCredentialsForProfile(
79
80
 
80
81
  try {
81
82
  let finalCredential: VC;
82
- const credentialPayload = await decryptInboxCredential(inboxCredential.credential);
83
+ const credentialPayload = inboxCredential.refreshId
84
+ ? undefined
85
+ : await decryptInboxCredential(inboxCredential.credential);
83
86
 
84
- if (!inboxCredential.isSigned) {
85
- const unsignedCredential = JSON.parse(credentialPayload) as UnsignedVC;
87
+ if (inboxCredential.refreshId) {
88
+ finalCredential = (
89
+ await finalizeInboxRefresh({
90
+ inboxId: inboxCredential.id,
91
+ holderDid: profile.did,
92
+ holderProfile: profile,
93
+ domain,
94
+ })
95
+ ).credential;
96
+ } else if (!inboxCredential.isSigned) {
97
+ const unsignedCredential = JSON.parse(credentialPayload!) as UnsignedVC;
86
98
 
87
99
  const endpoint =
88
100
  (inboxCredential.signingAuthority?.endpoint as string) ?? undefined;
@@ -135,19 +147,22 @@ export async function finalizeInboxCredentialsForProfile(
135
147
  )
136
148
  ).credential as VC;
137
149
  } else {
138
- finalCredential = JSON.parse(credentialPayload) as VC;
150
+ finalCredential = JSON.parse(credentialPayload!) as VC;
139
151
  }
140
152
 
141
- // The seeded encryption plugin adds the service DID; use explicit recipients.
142
- const learnCard = await getEmptyLearnCard();
143
- const recoveryCredential = await learnCard.invoke.createDagJwe(finalCredential, [
144
- profile.did,
145
- ]);
146
- const finalized = await finalizeAndWipeInboxCredential(inboxCredential.id, {
147
- recipientDid: profile.did,
148
- credential: recoveryCredential,
149
- });
150
- if (!finalized) throw new Error('Inbox credential is no longer pending');
153
+ if (!inboxCredential.refreshId) {
154
+ // The seeded encryption plugin adds the service DID; use explicit recipients.
155
+ const learnCard = await getEmptyLearnCard();
156
+ const recoveryCredential = await learnCard.invoke.createDagJwe(
157
+ finalCredential,
158
+ [profile.did]
159
+ );
160
+ const finalized = await finalizeAndWipeInboxCredential(inboxCredential.id, {
161
+ recipientDid: profile.did,
162
+ credential: recoveryCredential,
163
+ });
164
+ if (!finalized) throw new Error('Inbox credential is no longer pending');
165
+ }
151
166
 
152
167
  // Only write a claim audit edge once the record is actually finalized.
153
168
  await createClaimedRelationship(profile.profileId, inboxCredential.id, 'finalize');
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { initTRPC } from '@trpc/server';
3
+ import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda';
4
+ import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
5
+ import { fastifyTRPCOpenApiPlugin, type OpenApiMeta } from 'trpc-to-openapi';
6
+ import Fastify from 'fastify';
7
+ import { z } from 'zod';
8
+ import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
9
+ import { Readable } from 'node:stream';
10
+ import {
11
+ configureInboxBatchBodyLimit,
12
+ INBOX_BATCH_MAX_BYTES,
13
+ withInboxBatchBodyLimit,
14
+ } from './inbox-batch-http.helpers';
15
+ import { createOpenApiAwsLambdaHandler } from './shim';
16
+ import { inboxBatchResponseMeta } from './inbox-batch-http.helpers';
17
+
18
+ const t = initTRPC.meta<OpenApiMeta>().create();
19
+ const calls = vi.fn(() => true);
20
+ const router = t.router({
21
+ inbox: t.router({
22
+ issueBatch: t.procedure
23
+ .meta({ openapi: { method: 'POST', path: '/inbox/issue-batch' } })
24
+ .input(z.object({ padding: z.string() }))
25
+ .output(z.boolean())
26
+ .mutation(calls),
27
+ issue: t.procedure
28
+ .meta({ openapi: { method: 'POST', path: '/inbox/issue' } })
29
+ .input(z.object({ padding: z.string() }))
30
+ .output(z.boolean())
31
+ .mutation(calls),
32
+ }),
33
+ });
34
+
35
+ const bodyOfSize = (size: number): string => JSON.stringify({ padding: 'x'.repeat(size - 14) });
36
+ const event = (path: string, body: string, base64 = false): APIGatewayProxyEventV2 => ({
37
+ version: '2.0',
38
+ routeKey: path.startsWith('/trpc/') ? 'ANY /trpc/{trpc+}' : 'ANY /api/{trpc+}',
39
+ rawPath: path,
40
+ rawQueryString: '',
41
+ headers: { 'content-type': 'application/json', host: 'localhost' },
42
+ requestContext: {
43
+ http: {
44
+ method: 'POST',
45
+ path,
46
+ protocol: 'HTTP/1.1',
47
+ sourceIp: '127.0.0.1',
48
+ userAgent: 'test',
49
+ },
50
+ accountId: 'test',
51
+ apiId: 'test',
52
+ domainName: 'localhost',
53
+ domainPrefix: 'test',
54
+ requestId: 'test',
55
+ routeKey: '$default',
56
+ stage: '$default',
57
+ time: '',
58
+ timeEpoch: 0,
59
+ },
60
+ isBase64Encoded: base64,
61
+ body: base64 ? Buffer.from(body).toString('base64') : body,
62
+ pathParameters: { trpc: path.replace(/^\/(api|trpc)\//, '') },
63
+ });
64
+
65
+ describe('batch production transports', () => {
66
+ it('enforces actual streamed bytes when Content-Length is absent', async () => {
67
+ const server = Fastify({ bodyLimit: 32 });
68
+ configureInboxBatchBodyLimit(server);
69
+ server.post('/api/*', async () => true);
70
+ try {
71
+ const response = await server.inject({
72
+ method: 'POST',
73
+ url: '/api/inbox/issue',
74
+ headers: { 'content-type': 'application/json' },
75
+ payload: Readable.from([bodyOfSize(33)]),
76
+ });
77
+ expect(response.statusCode).toBe(413);
78
+ const batchResponse = await server.inject({
79
+ method: 'POST',
80
+ url: '/api/inbox/issue-batch',
81
+ headers: { 'content-type': 'application/json' },
82
+ payload: Readable.from([bodyOfSize(33)]),
83
+ });
84
+ expect(batchResponse.statusCode).toBe(200);
85
+ } finally {
86
+ await server.close();
87
+ }
88
+ });
89
+ it('uses the actual wildcard adapters and preserves limits on other procedures', async () => {
90
+ const server = Fastify();
91
+ configureInboxBatchBodyLimit(server);
92
+ await server.register(fastifyTRPCPlugin, { prefix: '/trpc', trpcOptions: { router } });
93
+ await server.register(fastifyTRPCOpenApiPlugin, {
94
+ basePath: '/api',
95
+ router,
96
+ responseMeta: inboxBatchResponseMeta,
97
+ });
98
+ try {
99
+ const payload = bodyOfSize(INBOX_BATCH_MAX_BYTES);
100
+ expect(Buffer.byteLength(payload)).toBe(INBOX_BATCH_MAX_BYTES);
101
+ for (const url of ['/api/inbox/issue-batch', '/trpc/inbox.issueBatch']) {
102
+ const response = await server.inject({
103
+ method: 'POST',
104
+ url,
105
+ headers: { 'content-type': 'application/json' },
106
+ payload,
107
+ });
108
+ expect(response.statusCode, response.body).toBe(
109
+ url.startsWith('/api/') ? 202 : 200
110
+ );
111
+ const tooLarge = await server.inject({
112
+ method: 'POST',
113
+ url,
114
+ headers: { 'content-type': 'application/json' },
115
+ payload: payload + ' ',
116
+ });
117
+ expect(tooLarge.statusCode).toBe(413);
118
+ }
119
+ for (const url of ['/api/inbox/issue', '/trpc/inbox.issue']) {
120
+ expect(
121
+ (
122
+ await server.inject({
123
+ method: 'POST',
124
+ url,
125
+ headers: { 'content-type': 'application/json' },
126
+ payload,
127
+ })
128
+ ).statusCode
129
+ ).toBe(413);
130
+ }
131
+ } finally {
132
+ await server.close();
133
+ }
134
+ });
135
+
136
+ it.each([false, true])(
137
+ 'checks decoded Lambda bytes before context creation (base64=%s)',
138
+ async base64 => {
139
+ const createContext = vi.fn(() => ({}));
140
+ const rest = createOpenApiAwsLambdaHandler({
141
+ router,
142
+ createContext,
143
+ responseMeta: inboxBatchResponseMeta,
144
+ });
145
+ const trpc = withInboxBatchBodyLimit(
146
+ awsLambdaRequestHandler({ router, createContext }),
147
+ 'trpc'
148
+ );
149
+ const context = {} as Context;
150
+ for (const [path, handler] of [
151
+ ['/api/inbox/issue-batch', rest],
152
+ ['/trpc/inbox.issueBatch', trpc],
153
+ ] as const) {
154
+ createContext.mockClear();
155
+ calls.mockClear();
156
+ const valid = await handler(
157
+ event(path, bodyOfSize(INBOX_BATCH_MAX_BYTES), base64),
158
+ context
159
+ );
160
+ expect(valid.statusCode, valid.body).toBe(path.startsWith('/api/') ? 202 : 200);
161
+ expect(calls).toHaveBeenCalledTimes(1);
162
+ createContext.mockClear();
163
+ const response = await handler(
164
+ event(path, bodyOfSize(INBOX_BATCH_MAX_BYTES) + ' ', base64),
165
+ context
166
+ );
167
+ expect(response.statusCode).toBe(413);
168
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
169
+ expect(createContext).not.toHaveBeenCalled();
170
+ expect(calls).toHaveBeenCalledTimes(1);
171
+ }
172
+ }
173
+ );
174
+
175
+ it('counts UTF-8 bytes and rejects malformed oversized JSON before parsing', async () => {
176
+ const handler = vi.fn(async () => ({ statusCode: 200 }));
177
+ const wrapped = withInboxBatchBodyLimit(handler, 'trpc');
178
+ const request = event(
179
+ '/trpc/inbox.issue,inbox.issueBatch',
180
+ 'é'.repeat(INBOX_BATCH_MAX_BYTES / 2 + 1)
181
+ );
182
+ request.queryStringParameters = { batch: '1' };
183
+ const response = await wrapped(request, {} as Context);
184
+ expect(response.statusCode).toBe(413);
185
+ expect(handler).not.toHaveBeenCalled();
186
+ expect('body' in response && JSON.parse(response.body)).toHaveLength(2);
187
+ });
188
+ });
@@ -0,0 +1,150 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type {
3
+ APIGatewayProxyEvent,
4
+ APIGatewayProxyEventV2,
5
+ APIGatewayProxyResult,
6
+ Context,
7
+ } from 'aws-lambda';
8
+ import { Transform } from 'node:stream';
9
+
10
+ // This leaves headroom below Lambda's 6 MB synchronous invocation envelope. It is a byte limit,
11
+ // rather than an item limit, because large CLR credentials can make a small batch very large.
12
+ export const INBOX_BATCH_MAX_BYTES = 4 * 1024 * 1024;
13
+ const MESSAGE = 'Inbox batch exceeds the 4 MiB JSON payload limit';
14
+
15
+ const requestPath = (url: string): string => {
16
+ try {
17
+ return decodeURIComponent(url.split('?')[0]!).replace(/\/$/, '');
18
+ } catch {
19
+ return url.split('?')[0]!;
20
+ }
21
+ };
22
+
23
+ /**
24
+ * Identifies only the batch procedure across REST and tRPC transports. A tRPC URL may contain
25
+ * comma-separated procedures when the client batches calls, so any batch-issuance member raises
26
+ * the parser allowance for the shared request.
27
+ */
28
+ export const isInboxBatchRequest = (url: string): boolean => {
29
+ const path = requestPath(url);
30
+ return (
31
+ path === '/api/inbox/issue-batch' ||
32
+ path === '/inbox/issue-batch' ||
33
+ (path.startsWith('/trpc/') &&
34
+ path.slice('/trpc/'.length).split(',').includes('inbox.issueBatch'))
35
+ );
36
+ };
37
+
38
+ /**
39
+ * Gives adapter wildcard routes a 4 MiB parser ceiling, then enforces the real per-procedure
40
+ * limit in preParsing. trpc-to-openapi and tRPC each register one catch-all Fastify route, so
41
+ * setting a limit on `/api/inbox/issue-batch` would never run in the Docker server.
42
+ * Other procedures retain the server's default budget here. Future procedures needing a
43
+ * different budget must update this selector as well as the shared parser ceiling; a route
44
+ * bodyLimit alone cannot override this streaming guard.
45
+ */
46
+ export const configureInboxBatchBodyLimit = (server: FastifyInstance): void => {
47
+ const defaultLimit = server.initialConfig.bodyLimit ?? 1024 * 1024;
48
+ server.addHook('onRoute', options => {
49
+ // Both adapters register catch-all routes, not one Fastify route per procedure.
50
+ if (options.url.startsWith('/api/') || options.url.startsWith('/trpc/')) {
51
+ options.bodyLimit = INBOX_BATCH_MAX_BYTES;
52
+ }
53
+ });
54
+ server.addHook('preParsing', async (request, _reply, payload) => {
55
+ const path = requestPath(request.url);
56
+ if (!path.startsWith('/api/') && !path.startsWith('/trpc/')) return payload;
57
+ const limit = isInboxBatchRequest(request.url) ? INBOX_BATCH_MAX_BYTES : defaultLimit;
58
+ const tooLarge = (): Error =>
59
+ Object.assign(new Error('Request body is too large'), {
60
+ statusCode: 413,
61
+ code: 'FST_ERR_CTP_BODY_TOO_LARGE',
62
+ });
63
+ if (Number(request.headers['content-length']) > limit) throw tooLarge();
64
+ // Count actual bytes as well: Content-Length is optional and client controlled. Fastify
65
+ // requires receivedEncodedLength on replacement streams to retain its normal accounting.
66
+ let bytes = 0;
67
+ const stream = new Transform({
68
+ transform(chunk, _encoding, callback) {
69
+ bytes += chunk.length;
70
+ if (bytes > limit) callback(tooLarge());
71
+ else callback(null, chunk);
72
+ },
73
+ });
74
+ Object.defineProperty(stream, 'receivedEncodedLength', { get: () => bytes });
75
+ payload.once('error', error => stream.destroy(error));
76
+ return payload.pipe(stream);
77
+ });
78
+ };
79
+
80
+ /** REST adapters bypass Fastify onSend hooks. Keep the documented 202 in src/openapi.ts in sync. */
81
+ export const inboxBatchResponseMeta = ({
82
+ paths,
83
+ errors,
84
+ }: {
85
+ paths?: readonly string[];
86
+ errors: readonly unknown[];
87
+ }): { status?: number } =>
88
+ !errors.length && paths?.includes('inbox.issueBatch') ? { status: 202 } : {};
89
+
90
+ type GatewayEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
91
+
92
+ /**
93
+ * Enforces decoded request bytes before either Lambda adapter parses JSON or creates a context.
94
+ * API Gateway can base64 encode binary bodies; their encoded string length is not the payload size.
95
+ * The wrapper also mirrors each transport's error envelope so SDK clients retain normal semantics.
96
+ */
97
+ export const withInboxBatchBodyLimit =
98
+ <TEvent extends GatewayEvent, TResult>(
99
+ handler: (event: TEvent, context: Context) => Promise<TResult>,
100
+ transport: 'openapi' | 'trpc'
101
+ ): ((event: TEvent, context: Context) => Promise<TResult | APIGatewayProxyResult>) =>
102
+ async (event, context) => {
103
+ const route = event.pathParameters?.trpc;
104
+ // `trpc` is a path parameter in deployed Lambda events, while local/OpenAPI events expose
105
+ // the concrete path. Normalize both forms before applying the same route predicate.
106
+ let path: string;
107
+ if (route) {
108
+ const prefix = transport === 'trpc' ? '/trpc' : '';
109
+ path = `${prefix}/${route.replace(/^\//, '')}`;
110
+ } else if ('rawPath' in event) {
111
+ path = event.rawPath;
112
+ } else {
113
+ path = event.path;
114
+ }
115
+ const bytes = event.isBase64Encoded
116
+ ? Buffer.from(event.body ?? '', 'base64').length
117
+ : Buffer.byteLength(event.body ?? '', 'utf8');
118
+ if (isInboxBatchRequest(path) && bytes > INBOX_BATCH_MAX_BYTES) {
119
+ // A tRPC batch expects one response entry per requested procedure, including this
120
+ // transport-level failure, whereas REST returns its ordinary error object.
121
+ // REST matches trpc-to-openapi's { message, code } validation envelope, rather
122
+ // than the shim's { error: ... } fallback for unexpected adapter exceptions.
123
+ const paths = requestPath(path)
124
+ .replace(/^\/trpc\//, '')
125
+ .split(',');
126
+ const errors = paths.map(procedure => ({
127
+ error: {
128
+ message: MESSAGE,
129
+ code: -32013,
130
+ data: { code: 'PAYLOAD_TOO_LARGE', httpStatus: 413, path: procedure },
131
+ },
132
+ }));
133
+ const batched =
134
+ event.queryStringParameters?.batch === '1' ||
135
+ ('rawQueryString' in event &&
136
+ new URLSearchParams(event.rawQueryString).get('batch') === '1');
137
+ return {
138
+ statusCode: 413,
139
+ headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
140
+ body: JSON.stringify(
141
+ transport === 'openapi'
142
+ ? { message: MESSAGE, code: 'PAYLOAD_TOO_LARGE' }
143
+ : batched
144
+ ? errors
145
+ : errors[0]
146
+ ),
147
+ };
148
+ }
149
+ return handler(event, context);
150
+ };
@@ -0,0 +1,11 @@
1
+ import type { InboxBatchStatus } from '@learncard/types';
2
+
3
+ /** Use the same state precedence for polling and submission replay receipts. */
4
+ export const getInboxBatchState = (
5
+ states: InboxBatchStatus['items'][number]['state'][]
6
+ ): InboxBatchStatus['status'] => {
7
+ if (states.length === 0) return 'QUEUED';
8
+ if (states.includes('NEEDS_RECONCILIATION')) return 'NEEDS_RECONCILIATION';
9
+ if (states.every(state => state === 'COMPLETED')) return 'COMPLETED';
10
+ return states.every(state => state === 'QUEUED') ? 'QUEUED' : 'PROCESSING';
11
+ };