@learncard/network-brain-service 3.19.0 → 4.0.0

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 (507) hide show
  1. package/.env.example +47 -82
  2. package/AGENTS.md +52 -0
  3. package/CHANGELOG.md +147 -0
  4. package/README.md +51 -2
  5. package/bootstrapLambda.test.ts +72 -0
  6. package/config/config.dev.json +29 -0
  7. package/config/config.example.json +29 -0
  8. package/config/config.json +1 -0
  9. package/config/config.production.json +29 -0
  10. package/config/config.scouts.dev.json +24 -0
  11. package/config/config.scouts.production.json +24 -0
  12. package/contractEventsLambda.ts +18 -0
  13. package/contractEventsLambdaApp.ts +19 -0
  14. package/didWebLambda.ts +13 -18
  15. package/didWebLambdaApp.ts +22 -0
  16. package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
  17. package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
  18. package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
  19. package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
  20. package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
  21. package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
  22. package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
  23. package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
  24. package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
  25. package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
  26. package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
  27. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
  29. package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
  30. package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
  31. package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
  32. package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
  33. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  34. package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
  35. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  36. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
  37. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  38. package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
  39. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
  40. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  41. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
  42. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
  43. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
  44. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
  45. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  47. package/dist/accesslayer/credential/create.d.ts +2 -1
  48. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  49. package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
  50. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  51. package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
  52. package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
  53. package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
  54. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  55. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  56. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  57. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  58. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  59. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  60. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  61. package/dist/accesslayer/share-link/attachment.d.ts +37 -0
  62. package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
  63. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  64. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  65. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  66. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  67. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  68. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  69. package/dist/accesslayer/share-link/helpers.d.ts +59 -0
  70. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  71. package/dist/accesslayer/share-link/index.d.ts +12 -0
  72. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  73. package/dist/accesslayer/share-link/list.d.ts +5 -0
  74. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  75. package/dist/accesslayer/share-link/read.d.ts +5 -0
  76. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  77. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  78. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  79. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  80. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  81. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  82. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  83. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  84. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  85. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  86. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  87. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  88. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  89. package/dist/accesslayer/share-link/types.d.ts +375 -0
  90. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  91. package/dist/app.d.ts +4 -0
  92. package/dist/app.d.ts.map +1 -1
  93. package/dist/cache/app-store.caches.d.ts +49 -0
  94. package/dist/cache/app-store.caches.d.ts.map +1 -1
  95. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  96. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  97. package/dist/config/environment.d.ts +14 -0
  98. package/dist/config/environment.d.ts.map +1 -1
  99. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  100. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  101. package/dist/config/stageConfig.d.ts +112 -0
  102. package/dist/config/stageConfig.d.ts.map +1 -0
  103. package/dist/config/stageConfig.test.d.ts +2 -0
  104. package/dist/config/stageConfig.test.d.ts.map +1 -0
  105. package/dist/constants/auth-grant.d.ts +2 -0
  106. package/dist/constants/auth-grant.d.ts.map +1 -1
  107. package/dist/didkit_wasm_bg.wasm +0 -0
  108. package/dist/docker-entry.d.ts +1 -1
  109. package/dist/docker-entry.d.ts.map +1 -1
  110. package/dist/helpers/app-manifest.helpers.d.ts +5 -0
  111. package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
  112. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  113. package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
  114. package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
  115. package/dist/helpers/consent-referral.helpers.d.ts +2 -0
  116. package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
  117. package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
  118. package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
  119. package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
  120. package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
  121. package/dist/helpers/contract-events.helpers.d.ts +25 -0
  122. package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
  123. package/dist/helpers/contract-requests.helpers.d.ts +19 -0
  124. package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
  125. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  126. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  127. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  128. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  129. package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
  130. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  131. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  132. package/dist/helpers/inline-template.helpers.d.ts +45 -0
  133. package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
  134. package/dist/helpers/notificationMessages.d.ts +1 -1
  135. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  136. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  137. package/dist/helpers/notifications.helpers.d.ts +6 -0
  138. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  139. package/dist/helpers/presentation-metadata.d.ts +4 -0
  140. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  141. package/dist/helpers/presentation.helpers.d.ts +1 -1
  142. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  143. package/dist/helpers/profile.helpers.d.ts +2 -0
  144. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  145. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  146. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  147. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  148. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  149. package/dist/helpers/share-content-client/client.d.ts +5 -0
  150. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  151. package/dist/helpers/share-content-client/config.d.ts +11 -0
  152. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  153. package/dist/helpers/share-content-client/index.d.ts +6 -0
  154. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  155. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  156. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  157. package/dist/helpers/share-content-client/types.d.ts +145 -0
  158. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  159. package/dist/helpers/share-link-attachment-access.d.ts +4 -0
  160. package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
  161. package/dist/helpers/share-link-attachment.d.ts +18 -0
  162. package/dist/helpers/share-link-attachment.d.ts.map +1 -0
  163. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
  164. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
  165. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  166. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  167. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  168. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  169. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  170. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  171. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  172. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  173. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  174. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  175. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  176. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  177. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  178. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  179. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  180. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  181. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  182. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  183. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  184. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  185. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  186. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  187. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  188. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  189. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  190. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  191. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  192. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  193. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  194. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  195. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  196. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  197. package/dist/helpers/share-link-maintenance/runner.d.ts +20 -0
  198. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  199. package/dist/helpers/share-link-maintenance/runtime.d.ts +38 -0
  200. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  201. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  202. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  203. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  204. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  205. package/dist/helpers/share-link-maintenance/types.d.ts +56 -0
  206. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  207. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  208. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  209. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  210. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  211. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  212. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  213. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  214. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  215. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  216. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  217. package/dist/helpers/share-link-passcode.d.ts +6 -0
  218. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  219. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  220. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  221. package/dist/helpers/share-link-policy/production.d.ts +11 -0
  222. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  223. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  224. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  225. package/dist/helpers/share-link-policy/types.d.ts +22 -0
  226. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  227. package/dist/helpers/share-link-public/config.d.ts +13 -0
  228. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  229. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  230. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  231. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  232. package/dist/helpers/smartResume.helpers.d.ts +3 -0
  233. package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
  234. package/dist/index.js +2 -2
  235. package/dist/models/AppManifestVersion.d.ts +20 -0
  236. package/dist/models/AppManifestVersion.d.ts.map +1 -0
  237. package/dist/models/AppStoreListing.d.ts +13 -0
  238. package/dist/models/AppStoreListing.d.ts.map +1 -1
  239. package/dist/models/ConsentFlowContract.d.ts +13 -2
  240. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  241. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  242. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  243. package/dist/models/Integration.d.ts +2 -0
  244. package/dist/models/Integration.d.ts.map +1 -1
  245. package/dist/models/Presentation.d.ts +2 -0
  246. package/dist/models/Presentation.d.ts.map +1 -1
  247. package/dist/models/Profile.d.ts +2 -0
  248. package/dist/models/Profile.d.ts.map +1 -1
  249. package/dist/models/ShareLink.d.ts +46 -0
  250. package/dist/models/ShareLink.d.ts.map +1 -0
  251. package/dist/models/ShareViewReceipt.d.ts +22 -0
  252. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  253. package/dist/models/index.d.ts +2 -0
  254. package/dist/models/index.d.ts.map +1 -1
  255. package/dist/models/share-link-constraints.d.ts +3 -0
  256. package/dist/models/share-link-constraints.d.ts.map +1 -0
  257. package/dist/openapi.d.ts +1 -1
  258. package/dist/openapi.d.ts.map +1 -1
  259. package/dist/public-share-link-http.d.ts +4 -0
  260. package/dist/public-share-link-http.d.ts.map +1 -0
  261. package/dist/routes/app-store.d.ts +619 -0
  262. package/dist/routes/app-store.d.ts.map +1 -1
  263. package/dist/routes/boosts.d.ts +2 -2
  264. package/dist/routes/boosts.d.ts.map +1 -1
  265. package/dist/routes/contracts.d.ts +148 -5
  266. package/dist/routes/contracts.d.ts.map +1 -1
  267. package/dist/routes/credentials.d.ts +35 -1
  268. package/dist/routes/credentials.d.ts.map +1 -1
  269. package/dist/routes/inbox.d.ts +52 -2
  270. package/dist/routes/inbox.d.ts.map +1 -1
  271. package/dist/routes/index.d.ts +153 -0
  272. package/dist/routes/index.d.ts.map +1 -1
  273. package/dist/routes/presentations.d.ts +1 -0
  274. package/dist/routes/presentations.d.ts.map +1 -1
  275. package/dist/routes/profile-manager.d.ts.map +1 -1
  276. package/dist/routes/profiles.d.ts.map +1 -1
  277. package/dist/routes/public-share-links.d.ts +285 -0
  278. package/dist/routes/public-share-links.d.ts.map +1 -0
  279. package/dist/routes/share-links.d.ts +1005 -0
  280. package/dist/routes/share-links.d.ts.map +1 -0
  281. package/dist/routes/test.d.ts +5 -0
  282. package/dist/routes/test.d.ts.map +1 -1
  283. package/dist/routes/workflows.d.ts.map +1 -1
  284. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  285. package/dist/types/app-manifest.d.ts +339 -0
  286. package/dist/types/app-manifest.d.ts.map +1 -0
  287. package/dist/types/app-store-listing.d.ts +34 -0
  288. package/dist/types/app-store-listing.d.ts.map +1 -1
  289. package/dist/types/consentflowcontract.d.ts +31 -4
  290. package/dist/types/consentflowcontract.d.ts.map +1 -1
  291. package/dist/types/credential-refresh.d.ts +1 -1
  292. package/esbuildPlugins.cjs +1 -0
  293. package/lambda.ts +49 -192
  294. package/lambdaApp.ts +203 -0
  295. package/package.json +25 -17
  296. package/scripts/export-openapi.ts +20 -0
  297. package/scripts/offline-schema-environment.ts +18 -0
  298. package/scripts/seed-demo-persona.ts +425 -0
  299. package/scripts/verify-python-contracts.ts +72 -0
  300. package/serverless-local.yml +3 -6
  301. package/serverless.function-env.cjs +50 -0
  302. package/serverless.local.yml +0 -1
  303. package/serverless.yml +44 -55
  304. package/shareLinkMaintenanceLambda.ts +18 -0
  305. package/shareLinkMaintenanceLambdaApp.ts +50 -0
  306. package/src/accesslayer/app-manifest-version/create.ts +57 -0
  307. package/src/accesslayer/app-manifest-version/read.ts +129 -0
  308. package/src/accesslayer/app-manifest-version/update.ts +61 -0
  309. package/src/accesslayer/app-store-listing/read.ts +6 -0
  310. package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
  311. package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
  312. package/src/accesslayer/app-store-listing/update.ts +15 -0
  313. package/src/accesslayer/boost/relationships/create.ts +8 -2
  314. package/src/accesslayer/consentflowcontract/create.ts +81 -8
  315. package/src/accesslayer/consentflowcontract/read.ts +84 -10
  316. package/src/accesslayer/consentflowcontract/relationships/create.ts +125 -109
  317. package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
  318. package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
  319. package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
  320. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
  321. package/src/accesslayer/consentflowcontract/relationships/update.ts +255 -217
  322. package/src/accesslayer/credential/create.test.ts +2 -115
  323. package/src/accesslayer/credential/create.ts +11 -6
  324. package/src/accesslayer/credential/relationships/create.ts +17 -10
  325. package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
  326. package/src/accesslayer/inbox-credential/update.ts +4 -2
  327. package/src/accesslayer/presentation/read.ts +57 -25
  328. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  329. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  330. package/src/accesslayer/share-link/abandon.ts +120 -0
  331. package/src/accesslayer/share-link/attachment.ts +360 -0
  332. package/src/accesslayer/share-link/cleanup.ts +271 -0
  333. package/src/accesslayer/share-link/errors.ts +32 -0
  334. package/src/accesslayer/share-link/finalize.ts +313 -0
  335. package/src/accesslayer/share-link/helpers.ts +450 -0
  336. package/src/accesslayer/share-link/index.ts +11 -0
  337. package/src/accesslayer/share-link/list.ts +162 -0
  338. package/src/accesslayer/share-link/read.ts +107 -0
  339. package/src/accesslayer/share-link/receipt.ts +548 -0
  340. package/src/accesslayer/share-link/recovery.ts +565 -0
  341. package/src/accesslayer/share-link/reserve.ts +763 -0
  342. package/src/accesslayer/share-link/revoke.ts +232 -0
  343. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  344. package/src/accesslayer/share-link/transaction.ts +126 -0
  345. package/src/accesslayer/share-link/types.ts +606 -0
  346. package/src/app.ts +6 -0
  347. package/src/cache/app-store.caches.ts +35 -0
  348. package/src/config/applyDockerStageConfig.ts +6 -0
  349. package/src/config/environment.test.ts +17 -12
  350. package/src/config/environment.ts +15 -0
  351. package/src/config/serverlessFunctionEnv.test.ts +105 -0
  352. package/src/config/stageConfig.test.ts +25 -0
  353. package/src/config/stageConfig.ts +24 -0
  354. package/src/constants/auth-grant.ts +5 -0
  355. package/src/docker-entry.ts +81 -0
  356. package/src/helpers/app-manifest.helpers.ts +185 -0
  357. package/src/helpers/boost.helpers.ts +4 -31
  358. package/src/helpers/claim-hooks.helpers.ts +20 -7
  359. package/src/helpers/consent-referral.helpers.ts +6 -0
  360. package/src/helpers/consent-scopes.helpers.ts +58 -0
  361. package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
  362. package/src/helpers/contract-events.helpers.ts +540 -0
  363. package/src/helpers/contract-requests.helpers.ts +211 -0
  364. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  365. package/src/helpers/credentialSubject.helpers.ts +40 -0
  366. package/src/helpers/finalize-inbox.helpers.ts +163 -44
  367. package/src/helpers/inbox.helpers.ts +58 -70
  368. package/src/helpers/inline-template.helpers.ts +560 -0
  369. package/src/helpers/notificationMessages.ts +77 -1
  370. package/src/helpers/notificationQueue.helpers.ts +23 -5
  371. package/src/helpers/notifications.helpers.ts +25 -1
  372. package/src/helpers/presentation-metadata.ts +41 -0
  373. package/src/helpers/presentation.helpers.ts +27 -20
  374. package/src/helpers/profile.helpers.ts +13 -0
  375. package/src/helpers/share-content-client/adapters.ts +55 -0
  376. package/src/helpers/share-content-client/canonical.ts +25 -0
  377. package/src/helpers/share-content-client/client.ts +440 -0
  378. package/src/helpers/share-content-client/config.ts +189 -0
  379. package/src/helpers/share-content-client/index.ts +27 -0
  380. package/src/helpers/share-content-client/responses.ts +273 -0
  381. package/src/helpers/share-content-client/types.ts +253 -0
  382. package/src/helpers/share-link-attachment-access.ts +87 -0
  383. package/src/helpers/share-link-attachment.ts +115 -0
  384. package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
  385. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  386. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  387. package/src/helpers/share-link-coordinator/coordinator.ts +727 -0
  388. package/src/helpers/share-link-coordinator/index.ts +8 -0
  389. package/src/helpers/share-link-coordinator/recovery-runner.ts +443 -0
  390. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  391. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  392. package/src/helpers/share-link-coordinator/types.ts +353 -0
  393. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  394. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  395. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  396. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  397. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  398. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  399. package/src/helpers/share-link-maintenance/config.ts +312 -0
  400. package/src/helpers/share-link-maintenance/index.ts +24 -0
  401. package/src/helpers/share-link-maintenance/runner.ts +251 -0
  402. package/src/helpers/share-link-maintenance/runtime.ts +340 -0
  403. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  404. package/src/helpers/share-link-maintenance/telemetry.ts +73 -0
  405. package/src/helpers/share-link-maintenance/types.ts +91 -0
  406. package/src/helpers/share-link-owner/config.ts +48 -0
  407. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  408. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  409. package/src/helpers/share-link-owner-projection.ts +54 -0
  410. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  411. package/src/helpers/share-link-passcode.ts +47 -0
  412. package/src/helpers/share-link-policy/index.ts +7 -0
  413. package/src/helpers/share-link-policy/production.ts +108 -0
  414. package/src/helpers/share-link-policy/resolver.ts +113 -0
  415. package/src/helpers/share-link-policy/types.ts +56 -0
  416. package/src/helpers/share-link-public/config.ts +41 -0
  417. package/src/helpers/share-link-view-notification.ts +18 -0
  418. package/src/helpers/signingAuthority.helpers.ts +23 -14
  419. package/src/helpers/smartResume.helpers.ts +123 -0
  420. package/src/models/AppManifestVersion.ts +43 -0
  421. package/src/models/AppStoreListing.ts +32 -0
  422. package/src/models/ConsentFlowContract.ts +34 -3
  423. package/src/models/ConsentFlowTerms.ts +10 -0
  424. package/src/models/ConsentFlowTransaction.ts +3 -0
  425. package/src/models/Integration.ts +2 -0
  426. package/src/models/Presentation.ts +2 -2
  427. package/src/models/Profile.ts +3 -2
  428. package/src/models/ShareLink.ts +134 -0
  429. package/src/models/ShareViewReceipt.ts +69 -0
  430. package/src/models/index.ts +25 -0
  431. package/src/models/share-link-constraints.ts +98 -0
  432. package/src/openapi.ts +44 -8
  433. package/src/public-share-link-http.ts +49 -0
  434. package/src/routes/app-store.ts +1056 -38
  435. package/src/routes/boosts.ts +14 -25
  436. package/src/routes/contracts.ts +384 -273
  437. package/src/routes/inbox.ts +4 -2
  438. package/src/routes/index.ts +142 -44
  439. package/src/routes/presentations.ts +19 -3
  440. package/src/routes/profile-manager.ts +29 -2
  441. package/src/routes/profiles.ts +29 -6
  442. package/src/routes/public-share-links.ts +983 -0
  443. package/src/routes/share-links.ts +709 -0
  444. package/src/routes/workflows.ts +3 -2
  445. package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
  446. package/src/types/app-manifest.ts +28 -0
  447. package/src/types/app-store-listing.ts +13 -0
  448. package/src/types/consentflowcontract.ts +25 -4
  449. package/test/app-manifest.helpers.spec.ts +170 -0
  450. package/test/app-manifest.routes.spec.ts +299 -0
  451. package/test/app-store-listing-updates.spec.ts +236 -0
  452. package/test/app-store-listing.spec.ts +65 -3
  453. package/test/consent-data-boundaries.spec.ts +352 -0
  454. package/test/consent-recipients.spec.ts +513 -0
  455. package/test/consent-scopes.helpers.spec.ts +67 -0
  456. package/test/consentflow.spec.ts +63 -2
  457. package/test/contract-requests.spec.ts +1588 -0
  458. package/test/credential-storage.neo4j.spec.ts +122 -0
  459. package/test/dids.spec.ts +10 -1
  460. package/test/guardian-gated-routes.spec.ts +49 -0
  461. package/test/helpers/share-link-fixtures.ts +94 -0
  462. package/test/inbox-security.e2e.ts +26 -10
  463. package/test/inbox.spec.ts +184 -8
  464. package/test/inline-template.helpers.spec.ts +49 -0
  465. package/test/managed-recipient-inbox.spec.ts +152 -0
  466. package/test/notificationMessages.spec.ts +13 -0
  467. package/test/openapi.spec.ts +14 -53
  468. package/test/presentations.spec.ts +54 -6
  469. package/test/profiles.spec.ts +97 -1
  470. package/test/share-content-client.contract.spec.ts +365 -0
  471. package/test/share-content-client.unit.spec.ts +462 -0
  472. package/test/share-link-abuse.unit.spec.ts +65 -0
  473. package/test/share-link-attachment.neo4j.spec.ts +262 -0
  474. package/test/share-link-attachment.unit.spec.ts +244 -0
  475. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  476. package/test/share-link-coordinator.unit.spec.ts +990 -0
  477. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  478. package/test/share-link-lifecycle.neo4j.spec.ts +1411 -0
  479. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  480. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  481. package/test/share-link-list.neo4j.spec.ts +324 -0
  482. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  483. package/test/share-link-maintenance.unit.spec.ts +1140 -0
  484. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  485. package/test/share-link-owner-route.unit.spec.ts +820 -0
  486. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  487. package/test/share-link-passcode.unit.spec.ts +29 -0
  488. package/test/share-link-policy.unit.spec.ts +149 -0
  489. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  490. package/test/share-link-production-policy.unit.spec.ts +126 -0
  491. package/test/share-link-public-config.unit.spec.ts +64 -0
  492. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  493. package/test/share-link-public-route.unit.spec.ts +1079 -0
  494. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  495. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  496. package/test/share-link-recovery.unit.spec.ts +726 -0
  497. package/test/share-link-service-policy.neo4j.spec.ts +403 -0
  498. package/test/signing-authority-encryption.spec.ts +75 -0
  499. package/test/smart-resume-consent.spec.ts +233 -0
  500. package/test/smart-resume-upload.spec.ts +65 -0
  501. package/tsconfig.build.json +1 -1
  502. package/tsconfig.json +1 -0
  503. package/vitest.config.ts +14 -0
  504. package/vitest.inbox-e2e.config.ts +2 -0
  505. package/vitest.share-links-neo4j.config.ts +19 -0
  506. package/vitest.share-links-unit.config.ts +28 -0
  507. package/workerLambda.test.ts +100 -0
@@ -0,0 +1,983 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ import { TRPCError } from '@trpc/server';
4
+
5
+ import {
6
+ AcknowledgeViewInputValidator,
7
+ AcknowledgeViewOutputValidator,
8
+ ResolveShareLinkInputValidator,
9
+ SHARE_VIEW_RECEIPT_TTL_SECONDS,
10
+ ShareEnvelopeValidator,
11
+ ShareLinkPublicContentViewValidator,
12
+ ShareLinkPublicStateValidator,
13
+ GetShareLinkAttachmentChunkInputValidator,
14
+ ShareLinkAttachmentChunkOutputValidator,
15
+ ShareLinkAttachmentChunkEnvelopeValidator,
16
+ } from '@learncard/types';
17
+ import { LCNNotificationTypeEnumValidator } from '@learncard/types';
18
+
19
+ import { openRouteWithoutInputCapture, t } from '@routes';
20
+ import type { ShareLinkPolicyResolver } from '@helpers/share-link-policy/types';
21
+ import { createShareLinkPolicyResolver } from '@helpers/share-link-policy/resolver';
22
+ import type { ShareContentContentProjection } from '@helpers/share-content-client/types';
23
+ import { createRetryableLazyInitializer } from '@helpers/share-link-owner/lazy-initializer';
24
+ import {
25
+ createBoundedShareLinkDiagnosticReporter,
26
+ createShareLinkDependencyResolver,
27
+ SHARE_LINK_COMPOSITION_DIAGNOSTIC,
28
+ } from '@helpers/share-link-owner/diagnostics';
29
+ import { resolveShareLinkPublicApiConfig } from '@helpers/share-link-public/config';
30
+ import type { ShareLinkPublicApiConfigResolution } from '@helpers/share-link-public/config';
31
+ import type {
32
+ ConsumeShareViewReceiptInput,
33
+ ConsumeShareViewReceiptOutcome,
34
+ PersistShareViewReceiptInput,
35
+ ShareViewEligibilitySource,
36
+ } from '@accesslayer/share-link';
37
+ import type { ShareLinkRecord } from '../models/ShareLink';
38
+ import { shareAttachmentTuple } from '@accesslayer/share-link/attachment';
39
+ import {
40
+ mintShareAttachmentAccessToken,
41
+ verifyShareAttachmentAccessToken,
42
+ } from '@helpers/share-link-attachment-access';
43
+
44
+ /**
45
+ * LC-2187 public (anonymous) share-link API: resolve, guarded content and
46
+ * privacy-uniform view acknowledgement.
47
+ *
48
+ * Disabled by default and fail-closed. There is no caller authority of any kind:
49
+ * namespace comes only from trusted server configuration, the owner only from
50
+ * the committed share, and there is never a caller-supplied object ref, URL,
51
+ * key or policy field. Resolve returns metadata only; resolve/previews never
52
+ * create a receipt or a count. The content route proxies the reviewed client
53
+ * transport and only releases bytes from one committed tuple after a final
54
+ * current-state recheck.
55
+ */
56
+
57
+ export type PublicShareLinkSharer = { displayName: string; avatar?: string };
58
+
59
+ export type PublicShareLinkRouterDependencies = {
60
+ readonly namespace: string;
61
+ readonly repository: {
62
+ getShareLink: (input: {
63
+ shareId: string;
64
+ namespace?: string;
65
+ }) => Promise<ShareLinkRecord | null>;
66
+ /**
67
+ * Namespace/owner-scoped read of the reviewed committted content tuple.
68
+ * Production delegates to `coordinator.fetchShareContent`, which applies
69
+ * the final committed-tuple recheck after the awaited remote read.
70
+ */
71
+ fetchContent: (input: {
72
+ shareId: string;
73
+ namespace: string;
74
+ ownerProfileId: string;
75
+ }) => Promise<
76
+ { ok: true; value: ShareContentContentProjection } | { ok: false; error: string }
77
+ >;
78
+ fetchAttachmentChunk?: (
79
+ input: import('@helpers/share-content-client/types').ShareContentTuple
80
+ ) => Promise<
81
+ { ok: true; value: ShareContentContentProjection } | { ok: false; error: string }
82
+ >;
83
+ };
84
+ readonly receipts: {
85
+ persist: (input: PersistShareViewReceiptInput) => Promise<boolean>;
86
+ /** Owner for a receipt that belongs to `namespace`; null otherwise. */
87
+ lookupOwner: (receipt: string, namespace: string) => Promise<string | null>;
88
+ lookupContext?: (
89
+ receipt: string,
90
+ namespace: string
91
+ ) => Promise<{ ownerProfileId: string; shareId: string } | null>;
92
+ consume: (input: ConsumeShareViewReceiptInput) => Promise<ConsumeShareViewReceiptOutcome>;
93
+ };
94
+ readonly policyResolver: ShareLinkPolicyResolver;
95
+ /**
96
+ * Optional transaction-compatible trusted eligibility source. Production
97
+ * rechecks persisted profile state under the share lock. An absent source
98
+ * remains fail-closed for tests and other compositions.
99
+ */
100
+ readonly eligibilitySource?: ShareViewEligibilitySource;
101
+ readonly getSharer: (ownerProfileId: string) => Promise<PublicShareLinkSharer | null>;
102
+ readonly verifyPasscode?: (passcodeHash: string, passcode: string) => Promise<boolean>;
103
+ readonly passcodeAttempts?: {
104
+ reserve: (shareId: string, sourceIp?: string) => Promise<boolean>;
105
+ };
106
+ readonly notifyView?: (input: {
107
+ shareId: string;
108
+ ownerProfileId: string;
109
+ title: string;
110
+ selectedCount: number;
111
+ viewCount: number;
112
+ viewedAt: string;
113
+ }) => Promise<void>;
114
+ readonly enforceRateLimit: (window: {
115
+ key: string;
116
+ limit: number;
117
+ windowSeconds: number;
118
+ description: string;
119
+ }) => Promise<void>;
120
+ readonly contentUrlFor: (shareId: string) => string;
121
+ readonly newReceipt: () => string;
122
+ readonly now: () => Date;
123
+ /** Aggregate-only dependency-failure telemetry; never includes input. */
124
+ readonly reportFailure: (category: string) => void;
125
+ };
126
+
127
+ type PublicShareClassification =
128
+ | { state: 'active'; record: ShareLinkRecord }
129
+ | { state: 'expired'; expiresAt: string }
130
+ | { state: 'stopped'; stoppedAt: string }
131
+ | { state: 'not_found' };
132
+
133
+ /**
134
+ * Strict public classification. Stopped wins over expired; `staging`,
135
+ * `content_missing`, `pending` and any missing object binding fail closed to
136
+ * `not_found` so a half-committed revision is never advertised.
137
+ */
138
+ export const classifyPublicShare = (
139
+ record: ShareLinkRecord | null,
140
+ now: Date
141
+ ): PublicShareClassification => {
142
+ if (!record) return { state: 'not_found' };
143
+
144
+ if (record.status === 'stopped') {
145
+ return { state: 'stopped', stoppedAt: record.stoppedAt ?? record.updatedAt };
146
+ }
147
+
148
+ if (record.expiresAt && Date.parse(record.expiresAt) <= now.getTime()) {
149
+ return { state: 'expired', expiresAt: record.expiresAt };
150
+ }
151
+
152
+ if (
153
+ record.status !== 'active' ||
154
+ record.contentState !== 'finalized' ||
155
+ !record.activeObjectRef ||
156
+ !record.activeObjectOperationId
157
+ ) {
158
+ return { state: 'not_found' };
159
+ }
160
+
161
+ return { state: 'active', record };
162
+ };
163
+
164
+ function notFound(): never {
165
+ throw new TRPCError({ code: 'NOT_FOUND' });
166
+ }
167
+
168
+ const openapi = (method: 'GET' | 'POST', path: `/${string}`, summary: string) => ({
169
+ protect: false as const,
170
+ method,
171
+ path,
172
+ tags: ['ShareLinks'],
173
+ summary,
174
+ });
175
+
176
+ const PUBLIC_RESOLVE_PATH = '/public/share-links/{id}' as const;
177
+ const PUBLIC_CONTENT_PATH = '/public/share-links/{id}/content' as const;
178
+ const PUBLIC_ACK_PATH = '/public/share-links/acknowledge-view' as const;
179
+
180
+ const passcodeAccepted = async (
181
+ record: ShareLinkRecord,
182
+ passcode: string | undefined,
183
+ dependencies: PublicShareLinkRouterDependencies,
184
+ sourceIp?: string
185
+ ): Promise<'accepted' | 'rejected' | 'try_later'> => {
186
+ if (record.passcodeHash == null) return 'accepted';
187
+ if (!passcode) return 'rejected';
188
+ if (!dependencies.verifyPasscode || !dependencies.passcodeAttempts) return 'try_later';
189
+ try {
190
+ if (!(await dependencies.passcodeAttempts.reserve(record.id, sourceIp))) return 'try_later';
191
+ const accepted = await dependencies.verifyPasscode(record.passcodeHash, passcode);
192
+ return accepted ? 'accepted' : 'rejected';
193
+ } catch (error) {
194
+ dependencies.reportFailure(
195
+ error instanceof Error && error.name === 'SharePasscodeCapacityError'
196
+ ? 'passcode_capacity'
197
+ : 'passcode_guard'
198
+ );
199
+ return 'try_later';
200
+ }
201
+ };
202
+
203
+ /**
204
+ * Trusted relative content route mounted under the OpenAPI adapter's `/api`
205
+ * base path. Never a LearnCloud object URL or a Host; a returned URL can be
206
+ * fetched directly from the supported local adapters.
207
+ */
208
+ export const publicShareContentUrl = (shareId: string): string =>
209
+ `/api/public/share-links/${encodeURIComponent(shareId)}/content`;
210
+
211
+ const rateLimitKey = (kind: string, namespace: string, sourceIp?: string): string =>
212
+ `share-link-public-${kind}:${namespace}:${sourceIp ?? 'unknown'}`;
213
+
214
+ export const createPublicShareLinksRouter = (
215
+ getDependencies: () => Promise<PublicShareLinkRouterDependencies | null>
216
+ ) => {
217
+ const resolve = async (): Promise<PublicShareLinkRouterDependencies> => {
218
+ let dependencies: PublicShareLinkRouterDependencies | null;
219
+
220
+ try {
221
+ dependencies = await getDependencies();
222
+ } catch {
223
+ throw new TRPCError({
224
+ code: 'INTERNAL_SERVER_ERROR',
225
+ message: 'share-link service is unavailable',
226
+ });
227
+ }
228
+
229
+ if (!dependencies) notFound();
230
+
231
+ return dependencies;
232
+ };
233
+
234
+ return t.router({
235
+ attachmentChunk: openRouteWithoutInputCapture
236
+ .meta({
237
+ openapi: openapi(
238
+ 'POST',
239
+ '/public/share-links/attachment-chunk',
240
+ 'Fetch guarded encrypted attachment chunk'
241
+ ),
242
+ })
243
+ .input(GetShareLinkAttachmentChunkInputValidator)
244
+ .output(ShareLinkAttachmentChunkOutputValidator)
245
+ .mutation(async ({ ctx, input }) => {
246
+ const dependencies = await resolve();
247
+ if (!dependencies.repository.fetchAttachmentChunk) notFound();
248
+ try {
249
+ await dependencies.enforceRateLimit({
250
+ key: rateLimitKey('attachment', dependencies.namespace, ctx.sourceIp),
251
+ limit: 240,
252
+ windowSeconds: 60,
253
+ description: 'public share-link attachment',
254
+ });
255
+ } catch (error) {
256
+ if (error instanceof TRPCError && error.code === 'TOO_MANY_REQUESTS')
257
+ throw new TRPCError({
258
+ code: 'TOO_MANY_REQUESTS',
259
+ message: 'try again later',
260
+ });
261
+ throw new TRPCError({
262
+ code: 'SERVICE_UNAVAILABLE',
263
+ message: 'share-link content is unavailable',
264
+ });
265
+ }
266
+ const read = async (): Promise<ShareLinkRecord> => {
267
+ let record: ShareLinkRecord | null;
268
+ try {
269
+ record = await dependencies.repository.getShareLink({
270
+ shareId: input.id,
271
+ namespace: dependencies.namespace,
272
+ });
273
+ } catch {
274
+ throw new TRPCError({
275
+ code: 'SERVICE_UNAVAILABLE',
276
+ message: 'share-link content is unavailable',
277
+ });
278
+ }
279
+ const state = classifyPublicShare(record, dependencies.now());
280
+ if (
281
+ state.state !== 'active' ||
282
+ state.record.namespace !== dependencies.namespace ||
283
+ state.record.contentVersion !== input.contentVersion ||
284
+ state.record.attachmentId !== input.attachmentId ||
285
+ !state.record.attachmentChunkCount ||
286
+ input.chunkIndex >= state.record.attachmentChunkCount
287
+ )
288
+ notFound();
289
+ return state.record;
290
+ };
291
+ const first = await read();
292
+ const grantAccepted =
293
+ input.accessToken &&
294
+ verifyShareAttachmentAccessToken(
295
+ input.accessToken,
296
+ first,
297
+ dependencies.namespace,
298
+ ctx.sourceIp,
299
+ dependencies.now()
300
+ );
301
+ const access = grantAccepted
302
+ ? 'accepted'
303
+ : await passcodeAccepted(first, input.passcode, dependencies, ctx.sourceIp);
304
+ if (access === 'try_later')
305
+ throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: 'try again later' });
306
+ if (access !== 'accepted')
307
+ throw new TRPCError({
308
+ code: 'UNAUTHORIZED',
309
+ message: 'share-link passcode required',
310
+ });
311
+ const tuple = shareAttachmentTuple(
312
+ {
313
+ namespace: dependencies.namespace,
314
+ ownerProfileId: first.ownerProfileId,
315
+ shareId: first.id,
316
+ contentVersion: first.contentVersion,
317
+ attachmentId: input.attachmentId,
318
+ chunkCount: first.attachmentChunkCount!,
319
+ },
320
+ input.chunkIndex
321
+ );
322
+ let fetched;
323
+ try {
324
+ fetched = await dependencies.repository.fetchAttachmentChunk(tuple);
325
+ } catch {
326
+ throw new TRPCError({
327
+ code: 'SERVICE_UNAVAILABLE',
328
+ message: 'share-link content is unavailable',
329
+ });
330
+ }
331
+ if (!fetched.ok) {
332
+ if (fetched.error === 'NOT_FOUND' || fetched.error === 'CONFLICT') notFound();
333
+ throw new TRPCError({
334
+ code: 'SERVICE_UNAVAILABLE',
335
+ message: 'share-link content is unavailable',
336
+ });
337
+ }
338
+ if (
339
+ Object.entries(tuple).some(
340
+ ([key, value]) => fetched.value[key as keyof typeof fetched.value] !== value
341
+ )
342
+ )
343
+ notFound();
344
+ const final = await read();
345
+ if (
346
+ final.version !== first.version ||
347
+ final.ownerProfileId !== first.ownerProfileId ||
348
+ final.activeObjectRef !== first.activeObjectRef ||
349
+ final.passcodeHash !== first.passcodeHash
350
+ )
351
+ notFound();
352
+ if (
353
+ grantAccepted &&
354
+ !verifyShareAttachmentAccessToken(
355
+ input.accessToken!,
356
+ final,
357
+ dependencies.namespace,
358
+ ctx.sourceIp,
359
+ dependencies.now()
360
+ )
361
+ ) {
362
+ throw new TRPCError({
363
+ code: 'UNAUTHORIZED',
364
+ message: 'share-link passcode required',
365
+ });
366
+ }
367
+ const envelope = ShareLinkAttachmentChunkEnvelopeValidator.safeParse(
368
+ fetched.value.envelope
369
+ );
370
+ if (!envelope.success) notFound();
371
+ return {
372
+ id: input.id,
373
+ contentVersion: input.contentVersion,
374
+ attachmentId: input.attachmentId,
375
+ chunkIndex: input.chunkIndex,
376
+ envelope: envelope.data,
377
+ ...(final.passcodeHash
378
+ ? {
379
+ // Renew only after the chunk and its current policy pass every check.
380
+ accessToken: mintShareAttachmentAccessToken(
381
+ final,
382
+ dependencies.namespace,
383
+ ctx.sourceIp,
384
+ dependencies.now()
385
+ ),
386
+ }
387
+ : {}),
388
+ };
389
+ }),
390
+ resolve: openRouteWithoutInputCapture
391
+ .meta({
392
+ openapi: openapi('POST', PUBLIC_RESOLVE_PATH, 'Resolve public share metadata'),
393
+ })
394
+ .input(ResolveShareLinkInputValidator)
395
+ .output(ShareLinkPublicStateValidator)
396
+ .mutation(async ({ ctx, input }) => {
397
+ let dependencies: PublicShareLinkRouterDependencies | null;
398
+ try {
399
+ dependencies = await getDependencies();
400
+ } catch {
401
+ return { state: 'not_found' as const, id: input.id };
402
+ }
403
+
404
+ // Disabled/invalid config: no repository, signer or remote call.
405
+ if (!dependencies) return { state: 'not_found' as const, id: input.id };
406
+
407
+ try {
408
+ await dependencies.enforceRateLimit({
409
+ key: rateLimitKey('resolve', dependencies.namespace, ctx.sourceIp),
410
+ limit: 120,
411
+ windowSeconds: 60,
412
+ description: 'public share-link resolve',
413
+ });
414
+ } catch {
415
+ dependencies.reportFailure('resolve_rate_limit');
416
+ throw new TRPCError({
417
+ code: 'TOO_MANY_REQUESTS',
418
+ message: 'rate limit exceeded',
419
+ });
420
+ }
421
+
422
+ let record: ShareLinkRecord | null;
423
+ try {
424
+ record = await dependencies.repository.getShareLink({
425
+ shareId: input.id,
426
+ namespace: dependencies.namespace,
427
+ });
428
+ } catch {
429
+ // Raw repository failures must not escape with a message.
430
+ dependencies.reportFailure('resolve_repository');
431
+ return { state: 'not_found' as const, id: input.id };
432
+ }
433
+ const classification = classifyPublicShare(record, dependencies.now());
434
+
435
+ if (classification.state === 'not_found') {
436
+ return { state: 'not_found' as const, id: input.id };
437
+ }
438
+ // Even lifecycle timestamps are private until a protected link
439
+ // has been unlocked. Missing records never enter Argon2.
440
+ if (record) {
441
+ const access = await passcodeAccepted(
442
+ record,
443
+ input.passcode,
444
+ dependencies,
445
+ ctx.sourceIp
446
+ );
447
+ if (access === 'rejected')
448
+ return { state: 'passcode_required' as const, id: input.id };
449
+ if (access === 'try_later')
450
+ return { state: 'try_later' as const, id: input.id };
451
+ }
452
+
453
+ if (classification.state === 'expired') {
454
+ return {
455
+ state: 'expired' as const,
456
+ id: input.id,
457
+ expiresAt: classification.expiresAt,
458
+ };
459
+ }
460
+ if (classification.state === 'stopped') {
461
+ return {
462
+ state: 'stopped' as const,
463
+ id: input.id,
464
+ stoppedAt: classification.stoppedAt,
465
+ };
466
+ }
467
+ let sharer: PublicShareLinkSharer | null;
468
+ try {
469
+ sharer = await dependencies.getSharer(classification.record.ownerProfileId);
470
+ } catch {
471
+ dependencies.reportFailure('resolve_sharer');
472
+ return { state: 'not_found' as const, id: input.id };
473
+ }
474
+ if (!sharer) return { state: 'not_found' as const, id: input.id };
475
+
476
+ // Final current-state guard AFTER the awaited sharer lookup: if the
477
+ // metadata revision changed while the sharer was being read, the
478
+ // projection is stale and must not be returned.
479
+ let latest: ShareLinkRecord | null;
480
+ try {
481
+ latest = await dependencies.repository.getShareLink({
482
+ shareId: input.id,
483
+ namespace: dependencies.namespace,
484
+ });
485
+ } catch {
486
+ dependencies.reportFailure('resolve_repository');
487
+ return { state: 'not_found' as const, id: input.id };
488
+ }
489
+ const latestClassification = classifyPublicShare(latest, dependencies.now());
490
+ if (
491
+ latestClassification.state !== 'active' ||
492
+ latestClassification.record.version !== classification.record.version
493
+ ) {
494
+ return { state: 'not_found' as const, id: input.id };
495
+ }
496
+
497
+ return {
498
+ state: 'active' as const,
499
+ id: classification.record.id,
500
+ title: classification.record.title,
501
+ ...(classification.record.note === null
502
+ ? {}
503
+ : { note: classification.record.note }),
504
+ selectedCount: classification.record.selectedCount,
505
+ contentVersion: classification.record.contentVersion,
506
+ contentUrl: dependencies.contentUrlFor(classification.record.id),
507
+ sharer: {
508
+ displayName: sharer.displayName.slice(0, 120),
509
+ ...(sharer.avatar ? { avatar: sharer.avatar.slice(0, 2048) } : {}),
510
+ },
511
+ createdAt: classification.record.createdAt,
512
+ updatedAt: classification.record.updatedAt,
513
+ expiresAt: classification.record.expiresAt,
514
+ };
515
+ }),
516
+
517
+ content: openRouteWithoutInputCapture
518
+ .meta({
519
+ openapi: openapi('POST', PUBLIC_CONTENT_PATH, 'Fetch guarded share content'),
520
+ })
521
+ .input(ResolveShareLinkInputValidator)
522
+ .output(ShareLinkPublicContentViewValidator)
523
+ .mutation(async ({ ctx, input }) => {
524
+ const dependencies = await resolve();
525
+
526
+ // The content response is large; a rate-limit/dependency failure
527
+ // fails closed with a fixed safe error (no raw message escapes).
528
+ try {
529
+ await dependencies.enforceRateLimit({
530
+ key: rateLimitKey('content', dependencies.namespace, ctx.sourceIp),
531
+ limit: 60,
532
+ windowSeconds: 60,
533
+ description: 'public share-link content',
534
+ });
535
+ } catch {
536
+ dependencies.reportFailure('content_rate_limit');
537
+ throw new TRPCError({
538
+ code: 'SERVICE_UNAVAILABLE',
539
+ message: 'share-link content is unavailable',
540
+ });
541
+ }
542
+
543
+ const readActiveShare = async (): Promise<ShareLinkRecord> => {
544
+ let candidate: ShareLinkRecord | null;
545
+ try {
546
+ candidate = await dependencies.repository.getShareLink({
547
+ shareId: input.id,
548
+ namespace: dependencies.namespace,
549
+ });
550
+ } catch {
551
+ dependencies.reportFailure('content_repository');
552
+ throw new TRPCError({
553
+ code: 'SERVICE_UNAVAILABLE',
554
+ message: 'share-link content is unavailable',
555
+ });
556
+ }
557
+
558
+ const classification = classifyPublicShare(candidate, dependencies.now());
559
+ if (classification.state !== 'active') notFound();
560
+
561
+ return classification.record;
562
+ };
563
+
564
+ const tupleMatches = (
565
+ projection: {
566
+ objectId: string;
567
+ operationId: string;
568
+ contentVersion: number;
569
+ payloadHash: string;
570
+ },
571
+ candidate: ShareLinkRecord
572
+ ): boolean =>
573
+ projection.objectId === candidate.activeObjectRef &&
574
+ projection.operationId === candidate.activeObjectOperationId &&
575
+ projection.contentVersion === candidate.contentVersion &&
576
+ // Brain's committed `activeContentHash` is the payload hash
577
+ // (envelope + recovery, before IDs): exactly what the
578
+ // reviewed client verifies on stat. Never compare the
579
+ // tuple-inclusive LearnCloud `contentHash` here.
580
+ (candidate.activeContentHash === null ||
581
+ projection.payloadHash === candidate.activeContentHash);
582
+
583
+ const first = await readActiveShare();
584
+
585
+ const access = await passcodeAccepted(
586
+ first,
587
+ input.passcode,
588
+ dependencies,
589
+ ctx.sourceIp
590
+ );
591
+ if (access === 'try_later') {
592
+ throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: 'try again later' });
593
+ }
594
+ if (access === 'rejected') {
595
+ throw new TRPCError({
596
+ code: 'UNAUTHORIZED',
597
+ message: 'share-link passcode required',
598
+ });
599
+ }
600
+
601
+ let fetched: Awaited<
602
+ ReturnType<PublicShareLinkRouterDependencies['repository']['fetchContent']>
603
+ >;
604
+ try {
605
+ fetched = await dependencies.repository.fetchContent({
606
+ shareId: input.id,
607
+ namespace: dependencies.namespace,
608
+ ownerProfileId: first.ownerProfileId,
609
+ });
610
+ } catch {
611
+ dependencies.reportFailure('content_fetch');
612
+ throw new TRPCError({
613
+ code: 'SERVICE_UNAVAILABLE',
614
+ message: 'share-link content is unavailable',
615
+ });
616
+ }
617
+
618
+ if (!fetched.ok) {
619
+ if (
620
+ fetched.error === 'UNAVAILABLE' ||
621
+ fetched.error === 'TIMEOUT' ||
622
+ fetched.error === 'NETWORK_ERROR' ||
623
+ fetched.error === 'MALFORMED_RESPONSE' ||
624
+ fetched.error === 'RESPONSE_TOO_LARGE' ||
625
+ fetched.error === 'UNEXPECTED_STATUS'
626
+ ) {
627
+ throw new TRPCError({
628
+ code: 'SERVICE_UNAVAILABLE',
629
+ message: 'share-link content is unavailable',
630
+ });
631
+ }
632
+ notFound();
633
+ }
634
+
635
+ // Fresh authoritative policy (never an earlier owner snapshot and
636
+ // never a caller field). A policy resolver failure is a fixed safe
637
+ // error, not a raw leak.
638
+ let policy: Awaited<ReturnType<ShareLinkPolicyResolver['resolve']>>;
639
+ try {
640
+ policy = await dependencies.policyResolver.resolve(first.ownerProfileId);
641
+ } catch {
642
+ dependencies.reportFailure('content_policy');
643
+ throw new TRPCError({
644
+ code: 'SERVICE_UNAVAILABLE',
645
+ message: 'share-link content is unavailable',
646
+ });
647
+ }
648
+
649
+ // Re-read AFTER the awaited policy resolve: a revoke/replacement/
650
+ // expiry during policy resolution must withhold the bytes.
651
+ const persistRecord = await readActiveShare();
652
+ if (
653
+ persistRecord.version !== first.version ||
654
+ !tupleMatches(fetched.value, persistRecord)
655
+ ) {
656
+ notFound();
657
+ }
658
+
659
+ const committedEligible =
660
+ persistRecord.minorPolicyViewCountingEnabled === true &&
661
+ persistRecord.minorPolicyResolved === true &&
662
+ persistRecord.minorPolicyIsMinor === false;
663
+ const eligible =
664
+ policy.viewCountingEnabled &&
665
+ policy.policyResolved &&
666
+ policy.isMinor === false &&
667
+ committedEligible;
668
+
669
+ const receipt = dependencies.newReceipt();
670
+
671
+ if (eligible) {
672
+ try {
673
+ await dependencies.receipts.persist({
674
+ receipt,
675
+ namespace: dependencies.namespace,
676
+ ownerProfileId: persistRecord.ownerProfileId,
677
+ shareId: persistRecord.id,
678
+ shareVersion: persistRecord.version,
679
+ contentVersion: persistRecord.contentVersion,
680
+ objectRef: fetched.value.objectId,
681
+ operationId: fetched.value.operationId,
682
+ ttlSeconds: SHARE_VIEW_RECEIPT_TTL_SECONDS,
683
+ eligibilitySource: dependencies.eligibilitySource,
684
+ now: dependencies.now,
685
+ });
686
+ } catch {
687
+ // Fail closed on counting; the response shape is unchanged.
688
+ dependencies.reportFailure('receipt_persist');
689
+ }
690
+ }
691
+
692
+ // FINAL current-state check after ALL awaited work (policy resolve,
693
+ // receipt persistence). No bytes are released unless the complete
694
+ // committed tuple/status/expiry/version is still current, so a
695
+ // revoke, replacement or expiry during persistence cannot return
696
+ // content authorized against an earlier state.
697
+ const finalRecord = await readActiveShare();
698
+ if (
699
+ finalRecord.version !== persistRecord.version ||
700
+ !tupleMatches(fetched.value, finalRecord)
701
+ ) {
702
+ notFound();
703
+ }
704
+
705
+ const envelope = ShareEnvelopeValidator.safeParse(fetched.value.envelope);
706
+ if (!envelope.success) notFound();
707
+
708
+ return {
709
+ id: finalRecord.id,
710
+ contentVersion: finalRecord.contentVersion,
711
+ envelope: envelope.data,
712
+ contentUrl: dependencies.contentUrlFor(finalRecord.id),
713
+ receipt,
714
+ };
715
+ }),
716
+
717
+ acknowledgeView: openRouteWithoutInputCapture
718
+ .meta({
719
+ openapi: openapi('POST', PUBLIC_ACK_PATH, 'Acknowledge a share view receipt'),
720
+ })
721
+ .input(AcknowledgeViewInputValidator)
722
+ .output(AcknowledgeViewOutputValidator)
723
+ .mutation(async ({ ctx, input }) => {
724
+ let dependencies: PublicShareLinkRouterDependencies | null;
725
+ try {
726
+ dependencies = await getDependencies();
727
+ } catch {
728
+ return { ok: true as const };
729
+ }
730
+
731
+ if (!dependencies) return { ok: true as const };
732
+
733
+ // Rate limiting and dependency failures must never change the
734
+ // uniform acknowledgement shape. Fail closed on counting only.
735
+ try {
736
+ await dependencies.enforceRateLimit({
737
+ key: rateLimitKey('ack', dependencies.namespace, ctx.sourceIp),
738
+ limit: 120,
739
+ windowSeconds: 60,
740
+ description: 'public share-link acknowledgement',
741
+ });
742
+ } catch {
743
+ dependencies.reportFailure('ack_rate_limit');
744
+ return { ok: true as const };
745
+ }
746
+
747
+ try {
748
+ // Owner is discovered from persisted receipt state, never from
749
+ // the caller. Unknown/padding tokens and tokens minted in
750
+ // another namespace simply return early.
751
+ const context = dependencies.receipts.lookupContext
752
+ ? await dependencies.receipts.lookupContext(
753
+ input.receipt,
754
+ dependencies.namespace
755
+ )
756
+ : null;
757
+ const ownerProfileId =
758
+ context?.ownerProfileId ??
759
+ (await dependencies.receipts.lookupOwner(
760
+ input.receipt,
761
+ dependencies.namespace
762
+ ));
763
+ if (ownerProfileId === null) return { ok: true as const };
764
+
765
+ const policy = await dependencies.policyResolver.resolve(ownerProfileId);
766
+
767
+ // The pre-lock resolve is a cheap fail-closed short-circuit,
768
+ // never authority: consume re-checks the committed snapshot
769
+ // and the transaction-compatible source under both locks.
770
+ if (!policy.viewCountingEnabled) return { ok: true as const };
771
+
772
+ const outcome = await dependencies.receipts.consume({
773
+ receipt: input.receipt,
774
+ namespace: dependencies.namespace,
775
+ eligibilitySource: dependencies.eligibilitySource,
776
+ });
777
+
778
+ if (outcome === 'consumed' && context && dependencies.notifyView) {
779
+ const share = await dependencies.repository.getShareLink({
780
+ shareId: context.shareId,
781
+ namespace: dependencies.namespace,
782
+ });
783
+ if (
784
+ share &&
785
+ share.ownerProfileId === context.ownerProfileId &&
786
+ share.notifyOnView === true &&
787
+ share.minorPolicyResolved === true &&
788
+ share.minorPolicyIsMinor === false &&
789
+ share.minorPolicyViewCountingEnabled === true &&
790
+ share.lastViewedAt
791
+ ) {
792
+ await dependencies.notifyView({
793
+ shareId: share.id,
794
+ ownerProfileId: share.ownerProfileId,
795
+ title: share.title,
796
+ selectedCount: share.selectedCount,
797
+ viewCount: share.viewCount,
798
+ viewedAt: share.lastViewedAt,
799
+ });
800
+ }
801
+ }
802
+ } catch {
803
+ dependencies.reportFailure('ack_consume');
804
+ }
805
+
806
+ return { ok: true as const };
807
+ }),
808
+ });
809
+ };
810
+
811
+ export type PublicShareLinksRouter = ReturnType<typeof createPublicShareLinksRouter>;
812
+
813
+ /**
814
+ * Lazy production composition. Config is resolved before any signer/graph/remote
815
+ * import; a disabled configuration is inert and silent, while malformed wiring
816
+ * emits a single sanitized diagnostic and still fails closed. A failed build is
817
+ * not cached, so a later request retries.
818
+ */
819
+ const initializeProductionDependencies =
820
+ createRetryableLazyInitializer<PublicShareLinkRouterDependencies>(() =>
821
+ buildProductionDependencies(
822
+ resolveShareLinkPublicApiConfig(process.env as Record<string, unknown>) as Extract<
823
+ ShareLinkPublicApiConfigResolution,
824
+ { status: 'enabled' }
825
+ >
826
+ )
827
+ );
828
+
829
+ export const getProductionDependencies =
830
+ createShareLinkDependencyResolver<PublicShareLinkRouterDependencies>({
831
+ resolveConfig: () =>
832
+ resolveShareLinkPublicApiConfig(process.env as Record<string, unknown>),
833
+ initializeDependencies: initializeProductionDependencies,
834
+ reportDiagnostic: createBoundedShareLinkDiagnosticReporter(),
835
+ configurationInvalidCategory:
836
+ SHARE_LINK_COMPOSITION_DIAGNOSTIC.PUBLIC_CONFIGURATION_INVALID,
837
+ initializationFailedCategory:
838
+ SHARE_LINK_COMPOSITION_DIAGNOSTIC.PUBLIC_INITIALIZATION_FAILED,
839
+ });
840
+
841
+ const buildProductionDependencies = async (
842
+ config: Extract<ShareLinkPublicApiConfigResolution, { status: 'enabled' }>
843
+ ): Promise<PublicShareLinkRouterDependencies> => {
844
+ const [
845
+ { getServerDidWebDID },
846
+ { createDidWebLearnCardTokenSigner },
847
+ clientModule,
848
+ runtimeModule,
849
+ { ensureShareLinkConstraints },
850
+ { createProductionShareLinkPolicySource, productionShareViewEligibilitySource },
851
+ { getProfileByProfileId },
852
+ { verifySharePasscode },
853
+ passcodeAbuse,
854
+ { addNotificationToQueue },
855
+ receiptModule,
856
+ { getShareLink },
857
+ { claimShareViewNotification },
858
+ { getNotificationMessage },
859
+ { resolveRecipientLocale },
860
+ ] = await Promise.all([
861
+ import('@helpers/learnCard.helpers'),
862
+ import('@helpers/share-content-client/adapters'),
863
+ import('@helpers/share-content-client'),
864
+ import('@helpers/share-link-coordinator/runtime'),
865
+ import('../models/share-link-constraints'),
866
+ import('@helpers/share-link-policy/production'),
867
+ import('@accesslayer/profile/read'),
868
+ import('@helpers/share-link-passcode'),
869
+ import('@helpers/share-link-passcode-abuse'),
870
+ import('@helpers/notifications.helpers'),
871
+ import('@accesslayer/share-link/receipt'),
872
+ import('@accesslayer/share-link/read'),
873
+ import('@helpers/share-link-view-notification'),
874
+ import('@helpers/notificationMessages'),
875
+ import('@helpers/getRecipientLocale.helpers'),
876
+ ]);
877
+
878
+ const clientConfig = clientModule.resolveShareContentClientConfig({
879
+ enabled: true,
880
+ origin: config.origin,
881
+ namespace: config.namespace,
882
+ audience: config.audience,
883
+ signerDid: getServerDidWebDID(),
884
+ allowInsecureLoopback: config.allowInsecureLoopback,
885
+ signer: createDidWebLearnCardTokenSigner(),
886
+ });
887
+
888
+ if (!clientConfig.enabled) {
889
+ // Fail the build so the retryable lazy initializer does not cache it.
890
+ throw new Error('share-content client configuration is invalid');
891
+ }
892
+
893
+ const client = clientModule.createShareContentClient(clientConfig);
894
+ const policyResolver = createShareLinkPolicyResolver(createProductionShareLinkPolicySource());
895
+ const coordinator = runtimeModule.createRuntimeShareLinkCoordinator({
896
+ client,
897
+ leaseOwner: 'brain-share-link-public-api',
898
+ policyResolver,
899
+ });
900
+
901
+ await ensureShareLinkConstraints();
902
+
903
+ return {
904
+ namespace: config.namespace,
905
+ repository: {
906
+ fetchAttachmentChunk: input => client.get(input),
907
+ getShareLink: input => getShareLink(input),
908
+ fetchContent: async input => {
909
+ const result = await coordinator.fetchShareContent(input.shareId, {
910
+ namespace: input.namespace,
911
+ ownerProfileId: input.ownerProfileId,
912
+ });
913
+
914
+ return result.ok ? { ok: true, value: result.value } : result;
915
+ },
916
+ },
917
+ receipts: {
918
+ persist: receiptModule.persistShareViewReceipt,
919
+ lookupOwner: async (receipt, namespace) => {
920
+ const owner = await receiptModule.readShareViewReceiptOwner(receipt, namespace);
921
+ return owner;
922
+ },
923
+ lookupContext: receiptModule.readShareViewReceiptContext,
924
+ consume: receiptModule.consumeShareViewReceipt,
925
+ },
926
+ policyResolver,
927
+ eligibilitySource: productionShareViewEligibilitySource,
928
+ verifyPasscode: verifySharePasscode,
929
+ passcodeAttempts: {
930
+ reserve: (shareId, sourceIp) =>
931
+ passcodeAbuse.reserveSharePasscodeAttempt(config.namespace, shareId, sourceIp),
932
+ },
933
+ notifyView: async notification => {
934
+ // Best effort: claim before enqueue to avoid duplicate alerts under
935
+ // concurrent views. A queue outage may suppress one window.
936
+ if (!(await claimShareViewNotification(config.namespace, notification.shareId))) return;
937
+ const profile = await getProfileByProfileId(notification.ownerProfileId);
938
+ if (!profile) return;
939
+ await addNotificationToQueue({
940
+ type: LCNNotificationTypeEnumValidator.enum.APP_NOTIFICATION,
941
+ to: profile,
942
+ from: {
943
+ did: getServerDidWebDID(),
944
+ displayName: 'LearnCard',
945
+ },
946
+ message: getNotificationMessage('shareViewed', resolveRecipientLocale(profile), {
947
+ title: notification.title,
948
+ count: String(notification.selectedCount),
949
+ }),
950
+ data: {
951
+ metadata: {
952
+ shareView: {
953
+ count: notification.viewCount,
954
+ viewedAt: notification.viewedAt,
955
+ },
956
+ },
957
+ },
958
+ });
959
+ },
960
+ getSharer: async ownerProfileId => {
961
+ const profile = await getProfileByProfileId(ownerProfileId);
962
+ if (!profile) return null;
963
+
964
+ return {
965
+ displayName: profile.displayName,
966
+ ...(profile.image ? { avatar: profile.image } : {}),
967
+ };
968
+ },
969
+ enforceRateLimit: async window => {
970
+ const { enforceRateLimits } = await import('@helpers/rateLimit.helpers');
971
+ await enforceRateLimits([window]);
972
+ },
973
+ contentUrlFor: publicShareContentUrl,
974
+ newReceipt: () => randomBytes(32).toString('base64url'),
975
+ now: () => new Date(),
976
+ reportFailure: category => {
977
+ // Aggregate-only: a fixed category string, no receipt/share/user data.
978
+ console.warn('share_link_public_dependency_failure', { category });
979
+ },
980
+ };
981
+ };
982
+
983
+ export const publicShareLinksRouter = createPublicShareLinksRouter(getProductionDependencies);