@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
@@ -77,6 +77,7 @@ import {
77
77
  } from '@accesslayer/consentflowcontract/relationships/create';
78
78
  import { getProfileByDid, getProfileByProfileId } from '@accesslayer/profile/read';
79
79
  import { sendBoost, isDraftBoost } from '@helpers/boost.helpers';
80
+ import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
80
81
  import { isRelationshipBlocked } from '@helpers/connection.helpers';
81
82
  import { getBoostByUri } from '@accesslayer/boost/read';
82
83
  import { canProfileIssueBoost } from '@accesslayer/boost/relationships/read';
@@ -818,17 +819,7 @@ export const contractsRouter = t.router({
818
819
  unsignedVc.issuanceDate = new Date().toISOString();
819
820
  }
820
821
  unsignedVc.issuer = { id: getDidWeb(ctx.domain, profile.profileId) };
821
- if (Array.isArray(unsignedVc.credentialSubject)) {
822
- unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
823
- ...subject,
824
- id: getDidWeb(ctx.domain, otherProfile.profileId),
825
- }));
826
- } else {
827
- unsignedVc.credentialSubject = {
828
- ...unsignedVc.credentialSubject,
829
- id: getDidWeb(ctx.domain, otherProfile.profileId),
830
- };
831
- }
822
+ setCredentialSubjectIds(unsignedVc, getDidWeb(ctx.domain, otherProfile.profileId));
832
823
  if (unsignedVc?.type?.includes('BoostCredential')) unsignedVc.boostId = boostUri;
833
824
  // Inject OBv3 skill alignments based on boost's framework/skills
834
825
  await injectObv3AlignmentsIntoCredentialForBoost(unsignedVc, boost, ctx.domain);
@@ -116,7 +116,10 @@ describe('credential refresh issuer route gating', () => {
116
116
  markConstraintsReady = resolve;
117
117
  })
118
118
  );
119
- mocks.sendRefreshableCredential.mockResolvedValue('lc:network:credential:root');
119
+ mocks.sendRefreshableCredential.mockResolvedValue({
120
+ uri: 'lc:network:credential:root',
121
+ receipt: { refreshId: 'opaque-refresh-id' },
122
+ });
120
123
 
121
124
  const caller = credentialRefreshesRouter.createCaller({
122
125
  domain: 'network.example.com',
@@ -174,7 +177,10 @@ describe('credential refresh issuer route gating', () => {
174
177
 
175
178
  it('forwards initial notification suppression to managed delivery', async () => {
176
179
  process.env.CREDENTIAL_REFRESH_ENABLED = 'true';
177
- mocks.sendRefreshableCredential.mockResolvedValue('lc:network:credential:root');
180
+ mocks.sendRefreshableCredential.mockResolvedValue({
181
+ uri: 'lc:network:credential:root',
182
+ receipt: { refreshId: 'opaque-refresh-id' },
183
+ });
178
184
 
179
185
  const caller = credentialRefreshesRouter.createCaller({
180
186
  domain: 'network.example.com',
@@ -124,7 +124,7 @@ export const credentialRefreshesRouter = t.router({
124
124
  .mutation(async ({ ctx, input }) => {
125
125
  const { profile } = ctx.user;
126
126
 
127
- return sendRefreshableCredential({
127
+ const { uri } = await sendRefreshableCredential({
128
128
  issuerProfile: profile,
129
129
  refreshId: input.refreshId,
130
130
  credential: input.credential,
@@ -132,6 +132,8 @@ export const credentialRefreshesRouter = t.router({
132
132
  skipNotification: input.skipNotification,
133
133
  domain: ctx.domain,
134
134
  });
135
+
136
+ return uri;
135
137
  }),
136
138
 
137
139
  publishCredentialRefresh: credentialRefreshRoute
@@ -156,6 +158,7 @@ export const credentialRefreshesRouter = t.router({
156
158
  issuerProfile: profile,
157
159
  input: input as PublishCredentialRefreshInput,
158
160
  domain: ctx.domain,
161
+ branding: ctx.tenant?.emailBranding,
159
162
  });
160
163
  }),
161
164
 
@@ -71,6 +71,7 @@ export const credentialsRouter = t.router({
71
71
  // Log credential activity FIRST to get activityId
72
72
  const activityId = await logCredentialSent({
73
73
  actorProfileId: profile.profileId,
74
+ onBehalfOf: ctx.user.onBehalfOf,
74
75
  recipientType: 'profile',
75
76
  recipientIdentifier: targetProfile.profileId,
76
77
  recipientProfileId: targetProfile.profileId,
@@ -118,7 +119,7 @@ export const credentialsRouter = t.router({
118
119
  const { profile } = ctx.user;
119
120
  const { uri } = input;
120
121
 
121
- return acceptCredential(profile, uri, input?.options ?? {});
122
+ return acceptCredential(profile, uri, input?.options ?? {}, ctx.tenant?.emailBranding);
122
123
  }),
123
124
 
124
125
  receivedCredentials: profileRoute
@@ -1,3 +1,12 @@
1
+ import { trace } from '@tracing';
2
+ import { submitInboxBatch, getInboxBatch } from '@helpers/inbox-queue.helpers';
3
+ import {
4
+ assertInboxRefreshEnabled,
5
+ inboxRefreshRequestDigest,
6
+ getInboxRefreshReplay,
7
+ getInboxRefreshReceipt,
8
+ resumeInboxRefreshDelivery,
9
+ } from '@helpers/inbox-refresh.helpers';
1
10
  import { getDidWeb } from '@helpers/did.helpers';
2
11
  import { TRPCError } from '@trpc/server';
3
12
  import { z } from 'zod';
@@ -6,6 +15,9 @@ import { t, profileRoute, openRoute, verifiedContactRoute, scopedRoute } from '@
6
15
  import {
7
16
  PaginationOptionsValidator,
8
17
  IssueInboxCredentialValidator,
18
+ IssueInboxCredentialBatchValidator,
19
+ InboxBatchReceiptValidator,
20
+ InboxBatchStatusValidator,
9
21
  IssueInboxCredentialResponseValidator,
10
22
  InboxCredentialValidator,
11
23
  PaginatedInboxCredentialsValidator,
@@ -18,15 +30,14 @@ import {
18
30
  JWEValidator,
19
31
  } from '@learncard/types';
20
32
  import { getInboxCredentialMeta } from '@helpers/credential-meta.helpers';
21
- import { claimIntoInbox, issueToInbox } from '@helpers/inbox.helpers';
22
- import { prepareCredentialFromBoost, getBoostUri } from '@helpers/boost.helpers';
33
+ import { claimIntoInbox, issueToInbox, resolveInboxCredentialInput } from '@helpers/inbox.helpers';
23
34
  import {
24
35
  hasMustacheVariables,
25
36
  renderBoostTemplate,
26
37
  parseRenderedTemplate,
27
38
  } from '@helpers/template.helpers';
28
39
  import { getProfileByVerifiedContactMethod } from '@accesslayer/contact-method/relationships/read';
29
- import { getBoostByUri, getBoostsForProfile } from '@accesslayer/boost/read';
40
+ import { getBoostsForProfile } from '@accesslayer/boost/read';
30
41
  import {
31
42
  generateGuardianApprovalToken,
32
43
  generateGuardianApprovalUrl,
@@ -447,55 +458,36 @@ export const inboxRouter = t.router({
447
458
  .output(IssueInboxCredentialResponseValidator)
448
459
  .mutation(async ({ ctx, input }) => {
449
460
  const { profile } = ctx.user;
450
- const { recipient, credential: inputCredential, templateUri, configuration } = input;
451
-
452
- // Resolve credential from templateUri if provided
453
- let credential = inputCredential;
454
- let resolvedBoostUri: string | undefined;
455
-
456
- if (templateUri && !credential) {
457
- const boostInstance = await getBoostByUri(templateUri);
458
-
459
- if (!boostInstance) {
460
- throw new TRPCError({
461
- code: 'NOT_FOUND',
462
- message: `Boost not found: ${templateUri}`,
463
- });
464
- }
465
-
466
- if (!boostInstance.dataValues.boost) {
467
- throw new TRPCError({
468
- code: 'BAD_REQUEST',
469
- message: `Boost does not contain a credential template: ${templateUri}`,
470
- });
471
- }
461
+ const { recipient, configuration } = input;
472
462
 
473
- try {
474
- // Use shared helper to prepare credential with templateData rendering,
475
- // issuance date, boostId injection, and OBv3 alignments
476
- resolvedBoostUri = getBoostUri(boostInstance.id, ctx.domain);
477
-
478
- credential = await prepareCredentialFromBoost(
479
- boostInstance,
480
- resolvedBoostUri,
481
- ctx.domain,
482
- { templateData: configuration?.templateData as Record<string, unknown> }
463
+ const refreshDigest = input.refresh ? inboxRefreshRequestDigest(input) : undefined;
464
+ if (input.refresh) {
465
+ await assertInboxRefreshEnabled(ctx.user.scope);
466
+ const replay = await getInboxRefreshReplay(
467
+ profile.profileId,
468
+ input.idempotencyKey,
469
+ refreshDigest!
470
+ );
471
+ if (replay && (replay.claimUrl || replay.inbox.currentStatus !== 'PENDING')) {
472
+ await resumeInboxRefreshDelivery(replay.inbox.refreshId!, ctx.domain);
473
+ const receipt = await getInboxRefreshReceipt(
474
+ replay.inbox.refreshId!,
475
+ ctx.domain
483
476
  );
484
- } catch (e) {
485
- console.error('Failed to prepare boost credential', e);
486
- throw new TRPCError({
487
- code: 'BAD_REQUEST',
488
- message: `Failed to prepare boost credential template: ${templateUri}`,
489
- });
477
+ return {
478
+ issuanceId: replay.inbox.id,
479
+ status: replay.inbox.currentStatus,
480
+ recipient,
481
+ claimUrl: replay.claimUrl,
482
+ refresh: receipt,
483
+ ...(replay.inbox.currentStatus === 'ISSUED'
484
+ ? { recipientDid: receipt.holderDid }
485
+ : {}),
486
+ };
490
487
  }
491
488
  }
492
489
 
493
- if (!credential) {
494
- throw new TRPCError({
495
- code: 'BAD_REQUEST',
496
- message: 'Either credential or templateUri must be provided',
497
- });
498
- }
490
+ const { credential, resolvedBoostUri } = await resolveInboxCredentialInput(input, ctx);
499
491
 
500
492
  // Normalize signing authority name if provided
501
493
  const normalizedConfiguration = configuration?.signingAuthority
@@ -513,11 +505,18 @@ export const inboxRouter = t.router({
513
505
  profile,
514
506
  recipient,
515
507
  credential,
516
- normalizedConfiguration,
508
+ {
509
+ ...normalizedConfiguration,
510
+ boostUri: resolvedBoostUri,
511
+ refresh: input.refresh,
512
+ idempotencyKey: input.idempotencyKey,
513
+ refreshRequestDigest: refreshDigest,
514
+ },
517
515
  ctx
518
516
  );
519
517
 
520
518
  return {
519
+ refresh: result.refresh,
521
520
  issuanceId: result.inboxCredential.id,
522
521
  status: result.status,
523
522
  recipient,
@@ -536,6 +535,42 @@ export const inboxRouter = t.router({
536
535
  }
537
536
  }),
538
537
 
538
+ issueBatch: profileRoute
539
+ .meta({
540
+ openapi: {
541
+ protect: true,
542
+ method: 'POST',
543
+ path: '/inbox/issue-batch',
544
+ tags: ['Universal Inbox'],
545
+ summary: 'Issue Credentials to Universal Inbox (Batch)',
546
+ description:
547
+ 'Queue 1–100 credentials for background issuance. Returns a durable batch ID; poll GET /inbox/batches/{batchId} for ordered results. Request and item idempotency keys are issuer-scoped for 24 hours. Maximum JSON payload: 4 MiB.',
548
+ },
549
+ requiredScope: 'inbox:write',
550
+ })
551
+ .input(IssueInboxCredentialBatchValidator)
552
+ .output(InboxBatchReceiptValidator)
553
+ .mutation(({ ctx, input }) =>
554
+ trace('route', 'issueBatch', () => submitInboxBatch(ctx.user.profile, input, ctx), {
555
+ itemCount: input.items.length,
556
+ })
557
+ ),
558
+
559
+ getBatch: profileRoute
560
+ .meta({
561
+ openapi: {
562
+ protect: true,
563
+ method: 'GET',
564
+ path: '/inbox/batches/{batchId}',
565
+ tags: ['Universal Inbox'],
566
+ summary: 'Get Inbox Batch Progress',
567
+ },
568
+ requiredScope: 'inbox:read',
569
+ })
570
+ .input(z.object({ batchId: z.string() }))
571
+ .output(InboxBatchStatusValidator)
572
+ .query(({ ctx, input }) => getInboxBatch(ctx.user.profile.profileId, input.batchId)),
573
+
539
574
  claim: verifiedContactRoute
540
575
  .meta({
541
576
  openapi: {
@@ -682,6 +717,7 @@ export const inboxRouter = t.router({
682
717
  // Log initial activity so embed claims appear in the dashboard
683
718
  const activityId = await logCredentialSent({
684
719
  actorProfileId: issuerProfile.profileId,
720
+ onBehalfOf: ctx.user?.onBehalfOf,
685
721
  recipientType: contactMethod.type as 'email' | 'phone',
686
722
  recipientIdentifier: contactMethod.value,
687
723
  integrationId: integration.id,
@@ -825,7 +861,17 @@ export const inboxRouter = t.router({
825
861
  });
826
862
  }
827
863
 
828
- return inboxCredential;
864
+ return {
865
+ ...inboxCredential,
866
+ ...(inboxCredential.refreshId
867
+ ? {
868
+ refresh: await getInboxRefreshReceipt(
869
+ inboxCredential.refreshId,
870
+ ctx.domain
871
+ ),
872
+ }
873
+ : {}),
874
+ };
829
875
  }),
830
876
 
831
877
  // ─── Guardian Credential Approval Routes ─────────────────────────────────────
@@ -9,8 +9,9 @@ import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
9
9
  import { OpenApiMeta } from 'trpc-to-openapi';
10
10
  import jwtDecode from 'jwt-decode';
11
11
  import * as Sentry from '@sentry/serverless';
12
- import { AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX } from '@learncard/types';
12
+ import { ACT_AS_HEADER, AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX } from '@learncard/types';
13
13
  import { ContactMethodType } from '@learncard/types';
14
+ import { MAX_SHARE_LINK_REQUEST_BYTES, utf8ByteLength } from '@learncard/types';
14
15
 
15
16
  import { RegExpTransformer } from '@learncard/helpers';
16
17
  import { resolveTenantFromRequest, type ResolvedTenant } from '@learncard/email-templates';
@@ -48,7 +49,11 @@ export type Context = {
48
49
  did: string;
49
50
  isChallengeValid: boolean;
50
51
  scope?: string;
52
+ isAuthGrant?: boolean;
53
+ actAsPolicy?: string;
54
+ onBehalfOf?: string;
51
55
  };
56
+ actAs?: string;
52
57
  contactMethod?: ContactMethodType;
53
58
  domain: string;
54
59
  tenant: ResolvedTenant;
@@ -84,6 +89,14 @@ export const createContext = async (
84
89
  | { req: { headers: Map<string, string> } }
85
90
  ): Promise<Context> => {
86
91
  const event = 'event' in options ? options.event : options.req;
92
+ const headerEntries =
93
+ 'get' in event.headers
94
+ ? Array.from(event.headers as Map<string, string>)
95
+ : Object.entries(event.headers);
96
+ const rawActAs = headerEntries.find(
97
+ ([name]) => name.toLowerCase() === ACT_AS_HEADER.toLowerCase()
98
+ )?.[1];
99
+ const actAs = Array.isArray(rawActAs) ? rawActAs.join(',') : rawActAs;
87
100
  const authHeader =
88
101
  'get' in event.headers
89
102
  ? (event.headers as Map<string, string>).get('authorization')
@@ -141,6 +154,7 @@ export const createContext = async (
141
154
  if (!challenge)
142
155
  return {
143
156
  user: { did, isChallengeValid: false, scope: AUTH_GRANT_NO_ACCESS_SCOPE },
157
+ actAs,
144
158
  domain,
145
159
  tenant,
146
160
  sourceIp,
@@ -148,6 +162,8 @@ export const createContext = async (
148
162
 
149
163
  let isChallengeValid = false;
150
164
  let scope = AUTH_GRANT_FULL_ACCESS_SCOPE;
165
+ let isAuthGrant = false;
166
+ let actAsPolicy: string | undefined;
151
167
 
152
168
  // If the user is using a provisional auth token for a contact method:
153
169
  if (challenge?.includes(CONTACT_METHOD_SESSION_PREFIX)) {
@@ -159,6 +175,7 @@ export const createContext = async (
159
175
  if (!contactMethod) throw new TRPCError({ code: 'NOT_FOUND' });
160
176
  return {
161
177
  contactMethod,
178
+ actAs,
162
179
  domain,
163
180
  tenant,
164
181
  sourceIp,
@@ -166,9 +183,14 @@ export const createContext = async (
166
183
  }
167
184
  // If the user is using a real auth grant i.e. an API Token.
168
185
  } else if (challenge?.includes(AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX)) {
169
- const { isChallengeValid: _isChallengeValid, scope: _scope } =
170
- await isAuthGrantChallengeValidForDID(challenge, did);
171
-
186
+ const {
187
+ isChallengeValid: _isChallengeValid,
188
+ scope: _scope,
189
+ actAs: policy,
190
+ } = await isAuthGrantChallengeValidForDID(challenge, did);
191
+
192
+ isAuthGrant = true;
193
+ actAsPolicy = policy;
172
194
  isChallengeValid = _isChallengeValid;
173
195
  scope = _scope;
174
196
  // If the user is using a real challenge signed by their private key.
@@ -182,7 +204,8 @@ export const createContext = async (
182
204
  Sentry.setUser({ id: did });
183
205
 
184
206
  return {
185
- user: { did, isChallengeValid, scope },
207
+ user: { did, isChallengeValid, scope, isAuthGrant, actAsPolicy },
208
+ actAs,
186
209
  domain,
187
210
  tenant,
188
211
  _guardianApprovalToken,
@@ -192,17 +215,96 @@ export const createContext = async (
192
215
  }
193
216
  }
194
217
 
195
- return { domain, tenant, _guardianApprovalToken, sourceIp };
218
+ return { domain, tenant, _guardianApprovalToken, sourceIp, actAs };
196
219
  };
197
220
 
198
- export const openRoute = t.procedure
199
- .use(t.middleware(Sentry.Handlers.trpcMiddleware({ attachRpcInput: true }) as any))
200
- .use(({ ctx, next, path }) => {
201
- Sentry.configureScope(scope => {
202
- scope.setTransactionName(`trpc-${path}`);
203
- });
204
- return next({ ctx });
221
+ const sentryTransactionNameMiddleware = t.middleware(({ ctx, next, path }) => {
222
+ Sentry.configureScope(scope => {
223
+ scope.setTransactionName(`trpc-${path}`);
205
224
  });
225
+ return next({ ctx });
226
+ });
227
+
228
+ // Sentry's tRPC middleware type predates this repo's generic context. Cast
229
+ // through the tRPC builder's expected parameter type instead of `any`.
230
+ type SentryTrpcMiddleware = Parameters<typeof t.middleware>[0];
231
+
232
+ const sentryInputCaptureMiddleware = Sentry.Handlers.trpcMiddleware({
233
+ attachRpcInput: true,
234
+ }) as unknown as SentryTrpcMiddleware;
235
+
236
+ const sentryNoInputCaptureMiddleware = Sentry.Handlers.trpcMiddleware({
237
+ attachRpcInput: false,
238
+ }) as unknown as SentryTrpcMiddleware;
239
+
240
+ /**
241
+ * Complete-request byte bound for owner share-link routes, enforced on the RAW
242
+ * input before Zod parsing/stripping. Individual field validators cannot bound a
243
+ * request whose unknown or duplicate fields are stripped first, so this measures
244
+ * the whole serialized body (UTF-8) and fails closed over the shared 1 MiB cap.
245
+ *
246
+ * It is composed into the base so it runs before authentication and before any
247
+ * input parser, and can never be skipped by a malformed or unauthenticated call.
248
+ */
249
+ const enforceShareLinkRequestByteBound = t.middleware(async ({ ctx, next, getRawInput }) => {
250
+ const raw = await getRawInput();
251
+
252
+ let bytes: number;
253
+ if (raw === undefined || raw === null) {
254
+ bytes = 0;
255
+ } else if (typeof raw === 'string') {
256
+ bytes = utf8ByteLength(raw);
257
+ } else {
258
+ let serialized: string;
259
+ try {
260
+ serialized = JSON.stringify(raw);
261
+ } catch {
262
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'invalid share-link request' });
263
+ }
264
+ bytes = utf8ByteLength(serialized);
265
+ }
266
+
267
+ if (bytes > MAX_SHARE_LINK_REQUEST_BYTES) {
268
+ throw new TRPCError({
269
+ code: 'PAYLOAD_TOO_LARGE',
270
+ message: 'share-link request exceeds the 1 MiB limit',
271
+ });
272
+ }
273
+
274
+ return next({ ctx });
275
+ });
276
+
277
+ export const openRoute = t.procedure
278
+ .use(t.middleware(sentryInputCaptureMiddleware))
279
+ .use(sentryTransactionNameMiddleware);
280
+
281
+ /**
282
+ * LC-2187 public share-link paths must never be cached by an intermediary or a
283
+ * browser. tRPC has no per-procedure header API, so the actual HTTP adapters
284
+ * (Fastify and Lambda, both tRPC and OpenAPI) apply `Cache-Control` from the
285
+ * resolved procedure path through this single helper.
286
+ */
287
+ export const PUBLIC_SHARE_LINK_ROUTE_PREFIX = 'publicShareLinks.';
288
+
289
+ export const publicShareLinkCacheControlHeaders = (
290
+ paths: readonly string[] | undefined
291
+ ): Record<string, string> =>
292
+ (paths ?? []).some(path => path.startsWith(PUBLIC_SHARE_LINK_ROUTE_PREFIX))
293
+ ? { 'Cache-Control': 'private, no-store' }
294
+ : {};
295
+
296
+ /**
297
+ * Route base for procedures whose input carries owner-private material (titles,
298
+ * notes, ciphertext envelopes, recovery JWEs) and whose error paths must not
299
+ * attach raw request bodies to Sentry. It runs the same Sentry error/transaction
300
+ * handling but with input attachment disabled, so no branch — malformed,
301
+ * unauthenticated or failing — can capture the payload, and it enforces the
302
+ * complete-request byte bound before input parsing.
303
+ */
304
+ export const openRouteWithoutInputCapture = t.procedure
305
+ .use(t.middleware(sentryNoInputCaptureMiddleware))
306
+ .use(sentryTransactionNameMiddleware)
307
+ .use(enforceShareLinkRequestByteBound);
206
308
 
207
309
  export const resolveProfileFromContextDid = async (
208
310
  didFromContext: string | undefined,
@@ -245,23 +347,82 @@ export const resolveProfileFromContextDid = async (
245
347
  return getProfileByDid(did);
246
348
  };
247
349
 
248
- export const didRoute = openRoute.use(async ({ ctx, next }) => {
249
- if (!ctx.user?.did) {
250
- throw new TRPCError({ code: 'UNAUTHORIZED' });
350
+ type ActingUser = NonNullable<Context['user']> & { profile: ProfileType | null };
351
+
352
+ const resolveActAs = async (
353
+ user: ActingUser,
354
+ actAs: string | undefined,
355
+ domain: string
356
+ ): Promise<ActingUser> => {
357
+ const requested = actAs?.trim();
358
+ if (!requested) return user;
359
+
360
+ const { profile } = user;
361
+ const prefix = `did:web:${domain}:users:`;
362
+ const profileId = requested.startsWith(prefix) ? requested.slice(prefix.length) : requested;
363
+ if (!profileId || /[:/?#]/.test(profileId)) {
364
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid act-as profile identifier' });
251
365
  }
252
366
 
253
- const profile = await resolveProfileFromContextDid(ctx.user.did, ctx.domain);
367
+ // Two DB lookups per request when the act-as header is present (target profile,
368
+ // then its managers) — acceptable at backend issuance volume; not worth caching.
369
+ const target = await getProfileByProfileId(profileId);
370
+ if (!target)
371
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Act-as target profile not found' });
254
372
 
255
- if (profile) Sentry.setUser({ id: profile.profileId, username: profile.displayName });
373
+ const managers = await getProfilesThatManageAProfile(target.profileId);
374
+ if (!profile || !managers.some(manager => manager.profileId === profile.profileId)) {
375
+ throw new TRPCError({
376
+ code: 'FORBIDDEN',
377
+ message: `You do not manage profile "${profileId}".`,
378
+ });
379
+ }
256
380
 
257
- return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
258
- });
381
+ const policy = user.actAsPolicy;
382
+ const permitted =
383
+ policy === '*' ||
384
+ policy
385
+ ?.split(',')
386
+ .map(id => id.trim())
387
+ .includes(target.profileId);
388
+ if (user.isAuthGrant && !permitted) {
389
+ throw new TRPCError({
390
+ code: 'FORBIDDEN',
391
+ message: `This API token may not act as "${profileId}". Grant actAs on the token.`,
392
+ });
393
+ }
259
394
 
260
- export const didAndChallengeRoute = didRoute.use(({ ctx, next }) => {
261
- if (!ctx.user?.isChallengeValid) throw new TRPCError({ code: 'UNAUTHORIZED' });
395
+ Sentry.setUser({ id: target.profileId, username: target.displayName });
396
+ return { ...user, profile: target, did: target.did, onBehalfOf: profile.profileId };
397
+ };
262
398
 
263
- return next({ ctx: { ...ctx, user: ctx.user } });
264
- });
399
+ const withDid = (base: typeof openRoute) =>
400
+ base.use(async ({ ctx, next }) => {
401
+ if (!ctx.user?.did) {
402
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
403
+ }
404
+
405
+ const profile = await resolveProfileFromContextDid(ctx.user.did, ctx.domain);
406
+
407
+ if (profile) Sentry.setUser({ id: profile.profileId, username: profile.displayName });
408
+
409
+ const user = ctx.user.isChallengeValid
410
+ ? await resolveActAs({ ...ctx.user, profile }, ctx.actAs, ctx.domain)
411
+ : { ...ctx.user, profile };
412
+
413
+ return next({ ctx: { ...ctx, user } });
414
+ });
415
+
416
+ export const didRoute = withDid(openRoute);
417
+
418
+ const withChallenge = (base: typeof didRoute) =>
419
+ base.use(({ ctx, next }) => {
420
+ if (!ctx.user?.isChallengeValid) throw new TRPCError({ code: 'UNAUTHORIZED' });
421
+
422
+ return next({ ctx: { ...ctx, user: ctx.user } });
423
+ });
424
+
425
+ export const didAndChallengeRoute = withChallenge(didRoute);
265
426
 
266
427
  export const scopedRoute = didAndChallengeRoute.use(({ ctx, next, meta }) => {
267
428
  if (!meta?.requiredScope) {
@@ -295,33 +456,41 @@ export const openProfileRoute = didRoute.use(async ({ ctx, next }) => {
295
456
  return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
296
457
  });
297
458
 
298
- export const profileRoute = didAndChallengeRoute.use(async ({ ctx, next, meta }) => {
299
- const { profile } = ctx.user;
459
+ const withProfile = (base: typeof didAndChallengeRoute) =>
460
+ base.use(async ({ ctx, next, meta }) => {
461
+ const { profile } = ctx.user;
300
462
 
301
- if (!profile) {
302
- throw new TRPCError({
303
- code: 'NOT_FOUND',
304
- message: 'Profile not found. Please make a profile!',
305
- });
306
- }
463
+ if (!profile) {
464
+ throw new TRPCError({
465
+ code: 'NOT_FOUND',
466
+ message: 'Profile not found. Please make a profile!',
467
+ });
468
+ }
307
469
 
308
- if (!meta?.requiredScope) {
309
- return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
310
- }
470
+ if (!meta?.requiredScope) {
471
+ return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
472
+ }
311
473
 
312
- const userScope = ctx.user?.scope || AUTH_GRANT_NO_ACCESS_SCOPE;
474
+ const userScope = ctx.user?.scope || AUTH_GRANT_NO_ACCESS_SCOPE;
313
475
 
314
- const hasRequiredScope = userHasRequiredScopes(userScope, meta.requiredScope);
476
+ const hasRequiredScope = userHasRequiredScopes(userScope, meta.requiredScope);
315
477
 
316
- if (!hasRequiredScope) {
317
- throw new TRPCError({
318
- code: 'UNAUTHORIZED',
319
- message: `This operation requires ${meta.requiredScope} scope`,
320
- });
321
- }
478
+ if (!hasRequiredScope) {
479
+ throw new TRPCError({
480
+ code: 'UNAUTHORIZED',
481
+ message: `This operation requires ${meta.requiredScope} scope`,
482
+ });
483
+ }
322
484
 
323
- return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
324
- });
485
+ return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
486
+ });
487
+
488
+ export const profileRoute = withProfile(didAndChallengeRoute);
489
+
490
+ /** Same auth chain, composed on a base that never attaches private RPC input to Sentry. */
491
+ export const didRouteWithoutInputCapture = withDid(openRouteWithoutInputCapture);
492
+ export const didAndChallengeRouteWithoutInputCapture = withChallenge(didRouteWithoutInputCapture);
493
+ export const profileRouteWithoutInputCapture = withProfile(didAndChallengeRouteWithoutInputCapture);
325
494
 
326
495
  export const openProfileManagerRoute = openRoute.use(async ({ ctx, next }) => {
327
496
  if (!ctx.user?.did) throw new TRPCError({ code: 'UNAUTHORIZED' });
@@ -385,7 +554,8 @@ const GUARDIAN_APPROVAL_CLOCK_SKEW_SECONDS = 60;
385
554
  export const guardianGatedRoute = profileRoute.use(async ({ ctx, next }) => {
386
555
  const { profile } = ctx.user;
387
556
  const guardianApprovalToken = ctx._guardianApprovalToken;
388
- const isChildAccount = await isProfileManaged(profile.profileId);
557
+ // Service profiles (orgs) are managed for admin purposes, not guardianship.
558
+ const isChildAccount = !profile.isServiceProfile && (await isProfileManaged(profile.profileId));
389
559
  let guardianIdentity: { profileId: string; did: string } | undefined;
390
560
 
391
561
  if (isChildAccount && guardianApprovalToken) {