@learncard/network-brain-service 3.18.0 → 3.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/.env.example +38 -2
  2. package/CHANGELOG.md +118 -0
  3. package/README.md +42 -0
  4. package/compose.inbox.yml +7 -0
  5. package/dist/accesslayer/auth-grant/read.d.ts +1 -0
  6. package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
  7. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  8. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  9. package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
  10. package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
  11. package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
  12. package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
  13. package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
  14. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  15. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  16. package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
  17. package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
  18. package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
  19. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  20. package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
  21. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
  22. package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
  23. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  24. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  25. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  26. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  27. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  29. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  30. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  31. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  32. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  33. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  34. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  35. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  36. package/dist/accesslayer/share-link/helpers.d.ts +56 -0
  37. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  38. package/dist/accesslayer/share-link/index.d.ts +12 -0
  39. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  40. package/dist/accesslayer/share-link/list.d.ts +5 -0
  41. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  42. package/dist/accesslayer/share-link/read.d.ts +5 -0
  43. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  44. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  45. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  46. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  47. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  48. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  49. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  50. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  51. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  52. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  53. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  54. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  55. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  56. package/dist/accesslayer/share-link/types.d.ts +370 -0
  57. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  58. package/dist/app.d.ts +4 -0
  59. package/dist/app.d.ts.map +1 -1
  60. package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
  61. package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
  62. package/dist/cache/atomic-counter.test.d.ts +2 -0
  63. package/dist/cache/atomic-counter.test.d.ts.map +1 -0
  64. package/dist/cache/index.d.ts +1 -1
  65. package/dist/cache/index.d.ts.map +1 -1
  66. package/dist/config/environment.d.ts +55 -0
  67. package/dist/config/environment.d.ts.map +1 -1
  68. package/dist/constants/auth-grant.d.ts +2 -0
  69. package/dist/constants/auth-grant.d.ts.map +1 -1
  70. package/dist/credential-refresh.d.ts.map +1 -1
  71. package/dist/helpers/activity.helpers.d.ts +4 -0
  72. package/dist/helpers/activity.helpers.d.ts.map +1 -1
  73. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  74. package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
  75. package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
  76. package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
  77. package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
  78. package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
  79. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
  80. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
  81. package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
  82. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
  83. package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
  84. package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
  85. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
  86. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
  87. package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
  88. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  89. package/dist/helpers/credential.helpers.d.ts +2 -1
  90. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  91. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  92. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  93. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  94. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  95. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  96. package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
  97. package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
  98. package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
  99. package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
  100. package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
  101. package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
  102. package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
  103. package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
  104. package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
  105. package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
  106. package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
  107. package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
  108. package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
  109. package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
  110. package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
  111. package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
  112. package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
  113. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
  114. package/dist/helpers/inbox.helpers.d.ts +9 -1
  115. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  116. package/dist/helpers/notificationMessages.d.ts +1 -1
  117. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  118. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  119. package/dist/helpers/presentation-metadata.d.ts +4 -0
  120. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  121. package/dist/helpers/presentation.helpers.d.ts +1 -1
  122. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  123. package/dist/helpers/profile.helpers.d.ts +1 -0
  124. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  125. package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
  126. package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
  127. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
  128. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  129. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  130. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  131. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  132. package/dist/helpers/share-content-client/client.d.ts +5 -0
  133. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  134. package/dist/helpers/share-content-client/config.d.ts +11 -0
  135. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  136. package/dist/helpers/share-content-client/index.d.ts +6 -0
  137. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  138. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  139. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  140. package/dist/helpers/share-content-client/types.d.ts +145 -0
  141. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  142. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  143. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  144. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  145. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  146. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  147. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  148. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  149. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  150. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  151. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  152. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  153. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  154. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  155. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  156. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  157. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  158. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  159. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  160. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  161. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  162. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  163. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  164. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  165. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  166. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  167. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  168. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  169. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  170. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  171. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  172. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  173. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  174. package/dist/helpers/share-link-maintenance/runner.d.ts +19 -0
  175. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  176. package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -0
  177. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  178. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  179. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  180. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  181. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  182. package/dist/helpers/share-link-maintenance/types.d.ts +53 -0
  183. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  184. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  185. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  186. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  187. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  188. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  189. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  190. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  191. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  192. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  193. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  194. package/dist/helpers/share-link-passcode.d.ts +6 -0
  195. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  196. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  197. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  198. package/dist/helpers/share-link-policy/production.d.ts +13 -0
  199. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  200. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  201. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  202. package/dist/helpers/share-link-policy/types.d.ts +16 -0
  203. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  204. package/dist/helpers/share-link-public/config.d.ts +13 -0
  205. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  206. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  207. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  208. package/dist/helpers/shim.d.ts +1 -1
  209. package/dist/helpers/shim.d.ts.map +1 -1
  210. package/dist/inbox-queue-local.d.ts +2 -0
  211. package/dist/inbox-queue-local.d.ts.map +1 -0
  212. package/dist/index.js +2 -2
  213. package/dist/models/AuthGrant.d.ts.map +1 -1
  214. package/dist/models/CredentialActivity.d.ts +1 -0
  215. package/dist/models/CredentialActivity.d.ts.map +1 -1
  216. package/dist/models/CredentialRefresh.d.ts +5 -2
  217. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  218. package/dist/models/InboxCredential.d.ts +1 -0
  219. package/dist/models/InboxCredential.d.ts.map +1 -1
  220. package/dist/models/Presentation.d.ts +2 -0
  221. package/dist/models/Presentation.d.ts.map +1 -1
  222. package/dist/models/Profile.d.ts +2 -0
  223. package/dist/models/Profile.d.ts.map +1 -1
  224. package/dist/models/ShareLink.d.ts +44 -0
  225. package/dist/models/ShareLink.d.ts.map +1 -0
  226. package/dist/models/ShareViewReceipt.d.ts +22 -0
  227. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  228. package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
  229. package/dist/models/index.d.ts +1 -0
  230. package/dist/models/index.d.ts.map +1 -1
  231. package/dist/models/share-link-constraints.d.ts +3 -0
  232. package/dist/models/share-link-constraints.d.ts.map +1 -0
  233. package/dist/openapi.d.ts.map +1 -1
  234. package/dist/public-share-link-http.d.ts +4 -0
  235. package/dist/public-share-link-http.d.ts.map +1 -0
  236. package/dist/routes/act-as.test.d.ts +2 -0
  237. package/dist/routes/act-as.test.d.ts.map +1 -0
  238. package/dist/routes/activity.d.ts +3 -0
  239. package/dist/routes/activity.d.ts.map +1 -1
  240. package/dist/routes/app-store.d.ts.map +1 -1
  241. package/dist/routes/auth-grants.d.ts +10 -0
  242. package/dist/routes/auth-grants.d.ts.map +1 -1
  243. package/dist/routes/boosts.d.ts +494 -0
  244. package/dist/routes/boosts.d.ts.map +1 -1
  245. package/dist/routes/contracts.d.ts +2 -2
  246. package/dist/routes/contracts.d.ts.map +1 -1
  247. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  248. package/dist/routes/credentials.d.ts +1 -1
  249. package/dist/routes/credentials.d.ts.map +1 -1
  250. package/dist/routes/inbox.d.ts +953 -11
  251. package/dist/routes/inbox.d.ts.map +1 -1
  252. package/dist/routes/index.d.ts +154 -85
  253. package/dist/routes/index.d.ts.map +1 -1
  254. package/dist/routes/presentations.d.ts +1 -0
  255. package/dist/routes/presentations.d.ts.map +1 -1
  256. package/dist/routes/profile-manager.d.ts.map +1 -1
  257. package/dist/routes/profiles.d.ts.map +1 -1
  258. package/dist/routes/public-share-links.d.ts +230 -0
  259. package/dist/routes/public-share-links.d.ts.map +1 -0
  260. package/dist/routes/share-links.d.ts +844 -0
  261. package/dist/routes/share-links.d.ts.map +1 -0
  262. package/dist/routes/workflows.d.ts.map +1 -1
  263. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  264. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
  265. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
  266. package/dist/services/registry/registry.factory.d.ts.map +1 -1
  267. package/dist/types/activity.d.ts +4 -0
  268. package/dist/types/activity.d.ts.map +1 -1
  269. package/dist/types/consentflowcontract.d.ts +1 -1
  270. package/dist/types/credential-refresh.d.ts +10 -1
  271. package/dist/types/credential-refresh.d.ts.map +1 -1
  272. package/dist/types/inbox-batch.d.ts +38 -0
  273. package/dist/types/inbox-batch.d.ts.map +1 -0
  274. package/inbox-e2e-setup.ts +53 -1
  275. package/inbox-elasticmq.conf +10 -0
  276. package/lambda.ts +64 -16
  277. package/package.json +24 -16
  278. package/scripts/seed-demo-persona.ts +425 -0
  279. package/scripts/setup-inbox-queues.ts +60 -0
  280. package/serverless-local.yml +2 -0
  281. package/serverless.yml +111 -0
  282. package/shareLinkMaintenanceLambda.ts +49 -0
  283. package/src/accesslayer/auth-grant/read.ts +2 -1
  284. package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
  285. package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
  286. package/src/accesslayer/credential-activity/create.ts +1 -0
  287. package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
  288. package/src/accesslayer/credential-refresh/index.ts +1 -0
  289. package/src/accesslayer/credential-refresh/read.ts +8 -2
  290. package/src/accesslayer/credential-refresh/update.ts +4 -0
  291. package/src/accesslayer/inbox-batch/store.ts +469 -0
  292. package/src/accesslayer/inbox-credential/create.ts +51 -19
  293. package/src/accesslayer/inbox-credential/refresh.ts +221 -0
  294. package/src/accesslayer/inbox-credential/update.ts +1 -1
  295. package/src/accesslayer/presentation/read.ts +57 -25
  296. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  297. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  298. package/src/accesslayer/share-link/abandon.ts +100 -0
  299. package/src/accesslayer/share-link/cleanup.ts +271 -0
  300. package/src/accesslayer/share-link/errors.ts +32 -0
  301. package/src/accesslayer/share-link/finalize.ts +265 -0
  302. package/src/accesslayer/share-link/helpers.ts +419 -0
  303. package/src/accesslayer/share-link/index.ts +11 -0
  304. package/src/accesslayer/share-link/list.ts +162 -0
  305. package/src/accesslayer/share-link/read.ts +107 -0
  306. package/src/accesslayer/share-link/receipt.ts +548 -0
  307. package/src/accesslayer/share-link/recovery.ts +545 -0
  308. package/src/accesslayer/share-link/reserve.ts +684 -0
  309. package/src/accesslayer/share-link/revoke.ts +197 -0
  310. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  311. package/src/accesslayer/share-link/transaction.ts +126 -0
  312. package/src/accesslayer/share-link/types.ts +601 -0
  313. package/src/app.ts +6 -0
  314. package/src/cache/atomic-counter.test-helpers.ts +27 -0
  315. package/src/cache/atomic-counter.test.ts +6 -0
  316. package/src/cache/index.ts +25 -13
  317. package/src/config/environment.ts +40 -0
  318. package/src/constants/auth-grant.ts +5 -0
  319. package/src/credential-refresh.ts +5 -1
  320. package/src/docker-entry.ts +65 -0
  321. package/src/helpers/activity.helpers.ts +4 -0
  322. package/src/helpers/app-counter.helpers.ts +1 -1
  323. package/src/helpers/boost.helpers.ts +4 -31
  324. package/src/helpers/contact-method.helpers.test.ts +43 -0
  325. package/src/helpers/contact-method.helpers.ts +3 -4
  326. package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
  327. package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
  328. package/src/helpers/credential-refresh-email.helpers.ts +201 -0
  329. package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
  330. package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
  331. package/src/helpers/credential-refresh.helpers.ts +375 -99
  332. package/src/helpers/credential.helpers.ts +4 -1
  333. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  334. package/src/helpers/credentialSubject.helpers.ts +40 -0
  335. package/src/helpers/finalize-inbox.helpers.ts +29 -14
  336. package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
  337. package/src/helpers/inbox-batch-http.helpers.ts +150 -0
  338. package/src/helpers/inbox-batch-status.helpers.ts +11 -0
  339. package/src/helpers/inbox-batch.helpers.test.ts +668 -0
  340. package/src/helpers/inbox-batch.helpers.ts +374 -0
  341. package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
  342. package/src/helpers/inbox-queue.helpers.test.ts +253 -0
  343. package/src/helpers/inbox-queue.helpers.ts +372 -0
  344. package/src/helpers/inbox-refresh.helpers.ts +432 -0
  345. package/src/helpers/inbox.helpers.ts +275 -36
  346. package/src/helpers/notificationMessages.ts +18 -1
  347. package/src/helpers/notifications.helpers.ts +4 -2
  348. package/src/helpers/presentation-metadata.ts +41 -0
  349. package/src/helpers/presentation.helpers.ts +27 -20
  350. package/src/helpers/profile.helpers.ts +7 -0
  351. package/src/helpers/rateLimit.helpers.ts +2 -5
  352. package/src/helpers/refresh-send-intent.helpers.ts +273 -0
  353. package/src/helpers/share-content-client/adapters.ts +55 -0
  354. package/src/helpers/share-content-client/canonical.ts +25 -0
  355. package/src/helpers/share-content-client/client.ts +440 -0
  356. package/src/helpers/share-content-client/config.ts +189 -0
  357. package/src/helpers/share-content-client/index.ts +27 -0
  358. package/src/helpers/share-content-client/responses.ts +273 -0
  359. package/src/helpers/share-content-client/types.ts +253 -0
  360. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  361. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  362. package/src/helpers/share-link-coordinator/coordinator.ts +714 -0
  363. package/src/helpers/share-link-coordinator/index.ts +8 -0
  364. package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -0
  365. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  366. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  367. package/src/helpers/share-link-coordinator/types.ts +353 -0
  368. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  369. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  370. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  371. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  372. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  373. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  374. package/src/helpers/share-link-maintenance/config.ts +312 -0
  375. package/src/helpers/share-link-maintenance/index.ts +24 -0
  376. package/src/helpers/share-link-maintenance/runner.ts +223 -0
  377. package/src/helpers/share-link-maintenance/runtime.ts +334 -0
  378. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  379. package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
  380. package/src/helpers/share-link-maintenance/types.ts +89 -0
  381. package/src/helpers/share-link-owner/config.ts +48 -0
  382. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  383. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  384. package/src/helpers/share-link-owner-projection.ts +52 -0
  385. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  386. package/src/helpers/share-link-passcode.ts +47 -0
  387. package/src/helpers/share-link-policy/index.ts +7 -0
  388. package/src/helpers/share-link-policy/production.ts +88 -0
  389. package/src/helpers/share-link-policy/resolver.ts +108 -0
  390. package/src/helpers/share-link-policy/types.ts +48 -0
  391. package/src/helpers/share-link-public/config.ts +41 -0
  392. package/src/helpers/share-link-view-notification.ts +18 -0
  393. package/src/helpers/shim.ts +32 -31
  394. package/src/inbox-queue-local.ts +28 -0
  395. package/src/models/AuthGrant.ts +1 -0
  396. package/src/models/CredentialActivity.ts +3 -5
  397. package/src/models/CredentialRefresh.ts +4 -2
  398. package/src/models/InboxCredential.ts +2 -0
  399. package/src/models/Presentation.ts +2 -2
  400. package/src/models/Profile.ts +3 -2
  401. package/src/models/ShareLink.ts +130 -0
  402. package/src/models/ShareViewReceipt.ts +69 -0
  403. package/src/models/credential-refresh-constraints.test.ts +8 -3
  404. package/src/models/credential-refresh-constraints.ts +5 -0
  405. package/src/models/index.ts +2 -0
  406. package/src/models/share-link-constraints.ts +93 -0
  407. package/src/openapi.ts +8 -0
  408. package/src/public-share-link-http.ts +49 -0
  409. package/src/routes/act-as.test.ts +239 -0
  410. package/src/routes/app-store.ts +4 -13
  411. package/src/routes/auth-grants.ts +13 -11
  412. package/src/routes/boosts.ts +916 -103
  413. package/src/routes/contracts.ts +2 -11
  414. package/src/routes/credential-refreshes.test.ts +8 -2
  415. package/src/routes/credential-refreshes.ts +4 -1
  416. package/src/routes/credentials.ts +2 -1
  417. package/src/routes/inbox.ts +95 -49
  418. package/src/routes/index.ts +216 -46
  419. package/src/routes/presentations.ts +19 -3
  420. package/src/routes/profile-manager.ts +10 -2
  421. package/src/routes/profiles.ts +17 -6
  422. package/src/routes/public-share-links.ts +814 -0
  423. package/src/routes/share-links.ts +640 -0
  424. package/src/routes/skills.ts +1 -1
  425. package/src/routes/workflows.ts +43 -24
  426. package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
  427. package/src/services/delivery/adapters/postmark.adapter.ts +16 -1
  428. package/src/services/registry/registry.factory.ts +2 -1
  429. package/src/types/activity.ts +2 -0
  430. package/src/types/credential-refresh.ts +21 -2
  431. package/src/types/inbox-batch.ts +46 -0
  432. package/test/atomic-counter.e2e.ts +12 -0
  433. package/test/auth-grant.spec.ts +21 -21
  434. package/test/credential-refresh-email.spec.ts +663 -0
  435. package/test/credential-refresh-publication.spec.ts +2 -0
  436. package/test/dids.spec.ts +10 -1
  437. package/test/guardian-gated-routes.spec.ts +13 -0
  438. package/test/helpers/inbox-e2e-environment.ts +6 -0
  439. package/test/helpers/share-link-fixtures.ts +94 -0
  440. package/test/inbox-batch.spec.ts +1338 -0
  441. package/test/inbox-refresh.spec.ts +534 -0
  442. package/test/managed-recipient-inbox.spec.ts +130 -0
  443. package/test/notificationMessages.spec.ts +13 -0
  444. package/test/openapi.spec.ts +61 -1
  445. package/test/presentations.spec.ts +54 -6
  446. package/test/profiles.spec.ts +67 -1
  447. package/test/refresh-send.spec.ts +1799 -0
  448. package/test/registry.spec.ts +56 -11
  449. package/test/share-content-client.contract.spec.ts +365 -0
  450. package/test/share-content-client.unit.spec.ts +462 -0
  451. package/test/share-link-abuse.unit.spec.ts +65 -0
  452. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  453. package/test/share-link-coordinator.unit.spec.ts +990 -0
  454. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  455. package/test/share-link-lifecycle.neo4j.spec.ts +1268 -0
  456. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  457. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  458. package/test/share-link-list.neo4j.spec.ts +324 -0
  459. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  460. package/test/share-link-maintenance.unit.spec.ts +1113 -0
  461. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  462. package/test/share-link-owner-route.unit.spec.ts +752 -0
  463. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  464. package/test/share-link-passcode.unit.spec.ts +29 -0
  465. package/test/share-link-policy.unit.spec.ts +135 -0
  466. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  467. package/test/share-link-production-policy.unit.spec.ts +77 -0
  468. package/test/share-link-public-config.unit.spec.ts +64 -0
  469. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  470. package/test/share-link-public-route.unit.spec.ts +800 -0
  471. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  472. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  473. package/test/share-link-recovery.unit.spec.ts +726 -0
  474. package/vitest.config.ts +10 -0
  475. package/vitest.inbox-e2e.config.ts +5 -1
  476. package/vitest.integration.config.ts +2 -0
  477. package/vitest.share-links-neo4j.config.ts +19 -0
  478. package/vitest.share-links-unit.config.ts +28 -0
@@ -0,0 +1,714 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import {
4
+ CreateShareLinkInputValidator,
5
+ ShareLinkIdValidator,
6
+ UpdateShareLinkInputValidator,
7
+ decodedBase64UrlByteLength,
8
+ utf8ByteLength,
9
+ } from '@learncard/types';
10
+
11
+ import {
12
+ canonicalizeJson,
13
+ computeShareLinkPayloadHash,
14
+ computeShareLinkRequestHash,
15
+ } from '@helpers/share-link-lifecycle';
16
+ import {
17
+ createShareLinkPolicyResolver,
18
+ resolveShareLinkExpiry,
19
+ } from '@helpers/share-link-policy/resolver';
20
+ import type { ShareLinkPolicyResolver } from '@helpers/share-link-policy/types';
21
+ import { hashSharePasscode } from '@helpers/share-link-passcode';
22
+
23
+ import { isShareLinkRepositoryError } from '../../accesslayer/share-link/errors';
24
+ import type { ShareLinkReservationRecord } from '../../accesslayer/share-link';
25
+ import { isTransientShareContentError } from '../share-content-client/types';
26
+ import type {
27
+ ShareContentClientErrorCode,
28
+ ShareContentEnvelope,
29
+ } from '../share-content-client/types';
30
+ import { verifyShareContentActiveStat as verifyActiveStat } from './stat';
31
+ import {
32
+ ShareLinkCoordinatorError,
33
+ type PendingShareContentOperation,
34
+ type ShareContentPayload,
35
+ type ShareLinkCommitResult,
36
+ type ShareLinkCoordinator,
37
+ type ShareLinkCoordinatorErrorCode,
38
+ type ShareLinkCoordinatorDependencies,
39
+ type ShareLinkResumeResult,
40
+ type ShareOwnerContext,
41
+ } from './types';
42
+
43
+ const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
44
+
45
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
46
+ typeof value === 'object' &&
47
+ value !== null &&
48
+ !Array.isArray(value) &&
49
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
50
+
51
+ const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
52
+ typeof value === 'string' &&
53
+ value.length >= 1 &&
54
+ value.length <= maxLength &&
55
+ OPAQUE_IDENTIFIER_RE.test(value);
56
+
57
+ function fail(code: ShareLinkCoordinatorErrorCode, message: string): never {
58
+ throw new ShareLinkCoordinatorError(code, message);
59
+ }
60
+
61
+ const mapRepositoryErrorCode = (code: string): ShareLinkCoordinatorErrorCode => {
62
+ switch (code) {
63
+ case 'NOT_FOUND':
64
+ case 'CONFLICT':
65
+ case 'OPERATION_IN_FLIGHT':
66
+ case 'LEASE_EXPIRED':
67
+ case 'STALE_GENERATION':
68
+ case 'PRECONDITION_FAILED':
69
+ case 'INVALID_INPUT':
70
+ return code;
71
+ default:
72
+ return 'UNEXPECTED';
73
+ }
74
+ };
75
+
76
+ const toCoordinatorError = (error: unknown): ShareLinkCoordinatorError => {
77
+ if (isShareLinkRepositoryError(error)) {
78
+ return new ShareLinkCoordinatorError(
79
+ mapRepositoryErrorCode(error.code),
80
+ 'share-link lifecycle operation failed'
81
+ );
82
+ }
83
+
84
+ return new ShareLinkCoordinatorError('UNEXPECTED', 'share-link lifecycle operation failed');
85
+ };
86
+
87
+ const parseOwnerContext = (context: ShareOwnerContext): ShareOwnerContext => {
88
+ if (!isPlainObject(context)) fail('INVALID_INPUT', 'owner context must be an object');
89
+ if (!isOpaqueIdentifier(context.namespace)) {
90
+ fail('INVALID_INPUT', 'namespace must be an opaque configured identifier');
91
+ }
92
+ if (!isOpaqueIdentifier(context.ownerProfileId)) {
93
+ fail('INVALID_INPUT', 'ownerProfileId must be an opaque identifier');
94
+ }
95
+
96
+ return { namespace: context.namespace, ownerProfileId: context.ownerProfileId };
97
+ };
98
+
99
+ const isRetryableClientError = (code: ShareContentClientErrorCode): boolean =>
100
+ isTransientShareContentError(code) ||
101
+ code === 'MALFORMED_RESPONSE' ||
102
+ code === 'RESPONSE_TOO_LARGE';
103
+
104
+ /** Preserve the real cause for deterministic client failures instead of a generic conflict. */
105
+ const mapClientErrorCode = (code: ShareContentClientErrorCode): ShareLinkCoordinatorErrorCode => {
106
+ switch (code) {
107
+ case 'UNAUTHORIZED':
108
+ return 'UNAUTHORIZED';
109
+ case 'SIGNING_FAILED':
110
+ return 'SIGNING_FAILED';
111
+ case 'PAYLOAD_TOO_LARGE':
112
+ return 'PAYLOAD_TOO_LARGE';
113
+ case 'NOT_FOUND':
114
+ return 'NOT_FOUND';
115
+ case 'CONFLICT':
116
+ return 'CONFLICT';
117
+ case 'INVALID_INPUT':
118
+ return 'INVALID_INPUT';
119
+ default:
120
+ return 'UNEXPECTED';
121
+ }
122
+ };
123
+
124
+ const measurementFor = (
125
+ envelope: ShareContentEnvelope,
126
+ ownerEncryptedRecovery: Record<string, unknown>
127
+ ): { contentBytes: number; recoveryBytes: number; recoveryHash: string } => {
128
+ const contentBytes = decodedBase64UrlByteLength(envelope.ct);
129
+
130
+ if (contentBytes === null) fail('INVALID_INPUT', 'envelope ciphertext must be base64url');
131
+
132
+ const recoveryBytes = utf8ByteLength(JSON.stringify(ownerEncryptedRecovery));
133
+ const recoveryHash = createHash('sha256')
134
+ .update(canonicalizeJson(ownerEncryptedRecovery))
135
+ .digest('hex');
136
+
137
+ return { contentBytes, recoveryBytes, recoveryHash };
138
+ };
139
+
140
+ const contentBindingFor = (
141
+ envelope: ShareContentEnvelope,
142
+ ownerEncryptedRecovery: Record<string, unknown>
143
+ ): {
144
+ contentHash: string;
145
+ contentBytes: number;
146
+ recoveryHash: string;
147
+ recoveryBytes: number;
148
+ } => ({
149
+ contentHash: computeShareLinkPayloadHash({ envelope, ownerEncryptedRecovery }),
150
+ ...measurementFor(envelope, ownerEncryptedRecovery),
151
+ });
152
+
153
+ export const createShareLinkCoordinator = (
154
+ dependencies: ShareLinkCoordinatorDependencies
155
+ ): ShareLinkCoordinator => {
156
+ const { repository, client } = dependencies;
157
+ const now = (): Date => dependencies.now?.() ?? new Date();
158
+
159
+ // Fail-closed fallback policy: no views, 30 days, age unknown. Production
160
+ // wiring injects the authoritative source; a missing resolver must never
161
+ // behave as a known unmanaged adult.
162
+ const policyResolver: ShareLinkPolicyResolver =
163
+ dependencies.policyResolver ??
164
+ createShareLinkPolicyResolver({
165
+ resolveOwnerAge: async () => 'unknown',
166
+ isManaged: async () => false,
167
+ });
168
+
169
+ const finalizeOrPending = async (
170
+ reservation: ShareLinkReservationRecord,
171
+ objectRef: string | null
172
+ ): Promise<ShareLinkCommitResult> => {
173
+ try {
174
+ const finalized = await repository.finalizeReservation({
175
+ namespace: reservation.namespace,
176
+ ownerProfileId: reservation.ownerProfileId,
177
+ shareId: reservation.shareId,
178
+ operationId: reservation.operationId,
179
+ objectRef,
180
+ generation: reservation.generation,
181
+ leaseOwner: reservation.leaseOwner,
182
+ verifiedContentHash:
183
+ objectRef === null || reservation.contentHash === null
184
+ ? undefined
185
+ : reservation.contentHash,
186
+ now: now(),
187
+ });
188
+
189
+ return finalized.outcome === 'finalized'
190
+ ? { status: 'committed', share: finalized.share }
191
+ : { status: 'replayed', share: finalized.share, recorded: finalized.share };
192
+ } catch (error) {
193
+ if (isShareLinkRepositoryError(error)) throw toCoordinatorError(error);
194
+
195
+ // An unexpected failure may have committed the swap but lost the
196
+ // response. Keep the reservation resumable; a stateless retry will
197
+ // re-stat and finalize idempotently.
198
+ return {
199
+ status: 'pending',
200
+ reservation,
201
+ reason: 'finalize_unavailable',
202
+ };
203
+ }
204
+ };
205
+
206
+ const abandonBestEffort = async (reservation: ShareLinkReservationRecord): Promise<void> => {
207
+ try {
208
+ await repository.abandonReservation({
209
+ namespace: reservation.namespace,
210
+ ownerProfileId: reservation.ownerProfileId,
211
+ shareId: reservation.shareId,
212
+ operationId: reservation.operationId,
213
+ generation: reservation.generation,
214
+ leaseOwner: reservation.leaseOwner,
215
+ now: now(),
216
+ });
217
+ } catch {
218
+ // The reservation lease still bounds a later supersede; never hide the
219
+ // original failure that triggered the abandon.
220
+ }
221
+ };
222
+
223
+ const driveContentReservation = async (
224
+ reservation: ShareLinkReservationRecord,
225
+ payload: ShareContentPayload
226
+ ): Promise<ShareLinkCommitResult> => {
227
+ if (reservation.objectRef === null || reservation.contentVersion === null) {
228
+ fail('PRECONDITION_FAILED', 'content reservation is missing its object binding');
229
+ }
230
+
231
+ const tuple = {
232
+ namespace: reservation.namespace,
233
+ ownerProfileId: reservation.ownerProfileId,
234
+ shareId: reservation.shareId,
235
+ contentVersion: reservation.contentVersion,
236
+ objectId: reservation.objectRef,
237
+ operationId: reservation.operationId,
238
+ };
239
+
240
+ const upload = await client.put({
241
+ ...tuple,
242
+ envelope: payload.envelope,
243
+ ownerEncryptedRecovery: payload.ownerEncryptedRecovery,
244
+ });
245
+
246
+ if (!upload.ok) {
247
+ if (isRetryableClientError(upload.error)) {
248
+ return { status: 'pending', reservation, reason: 'upload_unavailable' };
249
+ }
250
+
251
+ // Deterministic rejection: release the reservation and never hide the
252
+ // failure behind a pending state. The exact object is queued for
253
+ // tracked cleanup by abandon.
254
+ await abandonBestEffort(reservation);
255
+ fail(mapClientErrorCode(upload.error), 'LearnCloud rejected the content upload');
256
+ }
257
+
258
+ const stat = await client.stat(tuple);
259
+
260
+ if (!stat.ok) {
261
+ if (stat.error === 'NOT_FOUND') {
262
+ return { status: 'pending', reservation, reason: 'stat_missing' };
263
+ }
264
+ if (isRetryableClientError(stat.error)) {
265
+ return { status: 'pending', reservation, reason: 'stat_unavailable' };
266
+ }
267
+
268
+ // Do not finalize on a failed stat, and do not destroy the staged
269
+ // object on an ambiguous response.
270
+ fail(mapClientErrorCode(stat.error), 'LearnCloud stat failed');
271
+ }
272
+
273
+ if (!verifyActiveStat(stat.value, reservation)) {
274
+ if (stat.value.kind === 'tombstone') {
275
+ await abandonBestEffort(reservation);
276
+ fail('CONFLICT', 'the reserved object is a permanent tombstone');
277
+ }
278
+
279
+ return { status: 'pending', reservation, reason: 'stat_mismatch' };
280
+ }
281
+
282
+ return finalizeOrPending(reservation, reservation.objectRef);
283
+ };
284
+
285
+ const handleReserveResult = async (
286
+ result: Awaited<
287
+ ReturnType<ShareLinkCoordinatorDependencies['repository']['reserveCreate']>
288
+ >,
289
+ payload?: ShareContentPayload
290
+ ): Promise<ShareLinkCommitResult> => {
291
+ if (result.outcome === 'already_committed') {
292
+ return { status: 'replayed', share: result.current, recorded: result.recorded };
293
+ }
294
+
295
+ if (!payload || result.reservation.objectRef === null) {
296
+ return finalizeOrPending(result.reservation, null);
297
+ }
298
+
299
+ return driveContentReservation(result.reservation, payload);
300
+ };
301
+
302
+ return {
303
+ createShareLink: async (request, context) => {
304
+ const owner = parseOwnerContext(context);
305
+ const parsed = CreateShareLinkInputValidator.safeParse(request);
306
+
307
+ if (!parsed.success) fail('INVALID_INPUT', 'invalid create share-link request');
308
+
309
+ const value = parsed.data;
310
+ const content = contentBindingFor(value.envelope, value.ownerEncryptedRecovery);
311
+ // Caller intent only: `expiresAt` remains exactly as supplied
312
+ // (including omitted) so a later-clock retry of an omitted-expiry
313
+ // create replays the original operation instead of deriving a new one.
314
+ const requestHash = computeShareLinkRequestHash(
315
+ 'create',
316
+ value as unknown as Record<string, unknown>
317
+ );
318
+ const policy = await policyResolver.resolve(owner.ownerProfileId);
319
+ const effectiveExpiresAt = resolveShareLinkExpiry(policy, value.expiresAt, now());
320
+ const passcodeHash = value.passcode ? await hashSharePasscode(value.passcode) : null;
321
+
322
+ let reserved;
323
+
324
+ try {
325
+ reserved = await repository.reserveCreate({
326
+ namespace: owner.namespace,
327
+ ownerProfileId: owner.ownerProfileId,
328
+ clientRequestId: value.clientRequestId,
329
+ shareId: value.id,
330
+ title: value.title,
331
+ note: value.note ?? null,
332
+ expiresAt: effectiveExpiresAt,
333
+ selectedCount: value.selectedCount,
334
+ passcodeHash,
335
+ // Notification consent cannot override the same trusted
336
+ // policy that disables view counting for minors/unknown age.
337
+ notifyOnView: value.notifyOnView && policy.viewCountingEnabled,
338
+ content,
339
+ requestHash,
340
+ policy,
341
+ leaseOwner: dependencies.leaseOwner,
342
+ leaseMs: dependencies.leaseMs,
343
+ now: now(),
344
+ });
345
+ } catch (error) {
346
+ throw toCoordinatorError(error);
347
+ }
348
+
349
+ return handleReserveResult(reserved, {
350
+ envelope: value.envelope,
351
+ ownerEncryptedRecovery: value.ownerEncryptedRecovery,
352
+ });
353
+ },
354
+
355
+ updateShareLink: async (request, context) => {
356
+ const owner = parseOwnerContext(context);
357
+ const parsed = UpdateShareLinkInputValidator.safeParse(request);
358
+
359
+ if (!parsed.success) fail('INVALID_INPUT', 'invalid update share-link request');
360
+
361
+ const value = parsed.data;
362
+ const hasContent = value.envelope !== undefined;
363
+ const requestHash = computeShareLinkRequestHash(
364
+ 'update',
365
+ value as unknown as Record<string, unknown>
366
+ );
367
+ // Policy is re-resolved on every mutation, even a metadata-only one,
368
+ // so a profile that became managed stops accumulating views going
369
+ // forward. An omitted `expiresAt` leaves the existing expiry intact.
370
+ const policy = await policyResolver.resolve(owner.ownerProfileId);
371
+ const passcodeHash =
372
+ value.passcode === undefined
373
+ ? undefined
374
+ : value.passcode === null
375
+ ? null
376
+ : await hashSharePasscode(value.passcode);
377
+
378
+ let reserved;
379
+
380
+ try {
381
+ reserved = await repository.reserveReplacement({
382
+ namespace: owner.namespace,
383
+ ownerProfileId: owner.ownerProfileId,
384
+ clientRequestId: value.clientRequestId,
385
+ shareId: value.id,
386
+ expectedVersion: value.expectedVersion,
387
+ requestHash,
388
+ leaseOwner: dependencies.leaseOwner,
389
+ leaseMs: dependencies.leaseMs,
390
+ now: now(),
391
+ ...(hasContent
392
+ ? {
393
+ content: {
394
+ ...contentBindingFor(
395
+ value.envelope!,
396
+ value.ownerEncryptedRecovery!
397
+ ),
398
+ contentVersion: value.contentVersion!,
399
+ selectedCount: value.selectedCount!,
400
+ },
401
+ }
402
+ : {}),
403
+ ...(value.title !== undefined ? { title: value.title } : {}),
404
+ ...(value.note !== undefined ? { note: value.note } : {}),
405
+ ...(value.expiresAt !== undefined ? { expiresAt: value.expiresAt } : {}),
406
+ ...(passcodeHash !== undefined ? { passcodeHash } : {}),
407
+ ...(value.notifyOnView !== undefined
408
+ ? { notifyOnView: value.notifyOnView && policy.viewCountingEnabled }
409
+ : {}),
410
+ policy,
411
+ });
412
+ } catch (error) {
413
+ throw toCoordinatorError(error);
414
+ }
415
+
416
+ if (reserved.outcome === 'already_committed') {
417
+ return {
418
+ status: 'replayed',
419
+ share: reserved.current,
420
+ recorded: reserved.recorded,
421
+ };
422
+ }
423
+
424
+ if (!hasContent || reserved.reservation.objectRef === null) {
425
+ // Metadata-only revision: never uploads, preserves the original
426
+ // object operation id via the C3 finalize path.
427
+ return finalizeOrPending(reserved.reservation, null);
428
+ }
429
+
430
+ return driveContentReservation(reserved.reservation, {
431
+ envelope: value.envelope!,
432
+ ownerEncryptedRecovery: value.ownerEncryptedRecovery!,
433
+ });
434
+ },
435
+
436
+ revokeShareLink: async (request, context) => {
437
+ const owner = parseOwnerContext(context);
438
+
439
+ if (!isPlainObject(request)) fail('INVALID_INPUT', 'invalid revoke request');
440
+
441
+ const allowedKeys = ['id', 'expectedVersion', 'clientRequestId'];
442
+
443
+ if (Object.keys(request).some(key => !allowedKeys.includes(key))) {
444
+ fail('INVALID_INPUT', 'invalid revoke request');
445
+ }
446
+ if (!ShareLinkIdValidator.safeParse(request.id).success) {
447
+ fail('INVALID_INPUT', 'invalid share id');
448
+ }
449
+ if (
450
+ request.expectedVersion !== undefined &&
451
+ (!Number.isSafeInteger(request.expectedVersion) ||
452
+ (request.expectedVersion as number) < 1)
453
+ ) {
454
+ fail('INVALID_INPUT', 'invalid expectedVersion');
455
+ }
456
+ if (
457
+ request.clientRequestId !== undefined &&
458
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
459
+ String(request.clientRequestId)
460
+ )
461
+ ) {
462
+ fail('INVALID_INPUT', 'invalid clientRequestId');
463
+ }
464
+
465
+ const clientRequestId = request.clientRequestId as string | undefined;
466
+ const requestHash = clientRequestId
467
+ ? computeShareLinkRequestHash('revoke', {
468
+ id: request.id,
469
+ expectedVersion: request.expectedVersion,
470
+ })
471
+ : undefined;
472
+
473
+ try {
474
+ const revoked = await repository.revokeShareLink({
475
+ namespace: owner.namespace,
476
+ ownerProfileId: owner.ownerProfileId,
477
+ shareId: request.id as string,
478
+ expectedVersion: request.expectedVersion as number | undefined,
479
+ clientRequestId,
480
+ requestHash,
481
+ now: now(),
482
+ });
483
+
484
+ return {
485
+ status: revoked.outcome,
486
+ share: revoked.share,
487
+ cleanupQueuedFor: revoked.cleanupQueuedFor,
488
+ };
489
+ } catch (error) {
490
+ throw toCoordinatorError(error);
491
+ }
492
+ },
493
+
494
+ resumePendingOperation: async (pending, content): Promise<ShareLinkResumeResult> => {
495
+ const reservation = pending.reservation;
496
+
497
+ if (reservation.objectRef === null || reservation.contentVersion === null) {
498
+ const finalized = await finalizeOrPending(reservation, null);
499
+
500
+ return finalized.status === 'pending'
501
+ ? { status: 'pending', reservation, reason: finalized.reason }
502
+ : finalized;
503
+ }
504
+
505
+ if (content) {
506
+ const result = await driveContentReservation(reservation, content);
507
+
508
+ return result.status === 'pending'
509
+ ? { status: 'pending', reservation, reason: result.reason }
510
+ : result;
511
+ }
512
+
513
+ const tuple = {
514
+ namespace: reservation.namespace,
515
+ ownerProfileId: reservation.ownerProfileId,
516
+ shareId: reservation.shareId,
517
+ contentVersion: reservation.contentVersion,
518
+ objectId: reservation.objectRef,
519
+ operationId: reservation.operationId,
520
+ };
521
+
522
+ const stat = await client.stat(tuple);
523
+
524
+ if (stat.ok) {
525
+ if (verifyActiveStat(stat.value, reservation)) {
526
+ const finalized = await finalizeOrPending(reservation, reservation.objectRef);
527
+
528
+ return finalized.status === 'pending'
529
+ ? { status: 'pending', reservation, reason: finalized.reason }
530
+ : finalized;
531
+ }
532
+
533
+ if (stat.value.kind === 'tombstone') {
534
+ const abandoned = await repository.abandonReservation({
535
+ namespace: reservation.namespace,
536
+ ownerProfileId: reservation.ownerProfileId,
537
+ shareId: reservation.shareId,
538
+ operationId: reservation.operationId,
539
+ generation: reservation.generation,
540
+ leaseOwner: reservation.leaseOwner,
541
+ now: now(),
542
+ });
543
+
544
+ return {
545
+ status: 'abandoned',
546
+ cleanupQueuedFor:
547
+ abandoned.outcome === 'abandoned' ? abandoned.cleanupQueuedFor : null,
548
+ };
549
+ }
550
+
551
+ // Active but divergent: never finalize and never destroy bytes.
552
+ return { status: 'pending', reservation, reason: 'stat_mismatch' };
553
+ }
554
+
555
+ if (stat.error === 'NOT_FOUND') {
556
+ const abandoned = await repository.abandonReservation({
557
+ namespace: reservation.namespace,
558
+ ownerProfileId: reservation.ownerProfileId,
559
+ shareId: reservation.shareId,
560
+ operationId: reservation.operationId,
561
+ generation: reservation.generation,
562
+ leaseOwner: reservation.leaseOwner,
563
+ now: now(),
564
+ });
565
+
566
+ return {
567
+ status: 'abandoned',
568
+ cleanupQueuedFor:
569
+ abandoned.outcome === 'abandoned' ? abandoned.cleanupQueuedFor : null,
570
+ };
571
+ }
572
+
573
+ if (isRetryableClientError(stat.error)) {
574
+ return { status: 'pending', reservation, reason: 'stat_unavailable' };
575
+ }
576
+
577
+ fail(mapClientErrorCode(stat.error), 'LearnCloud stat failed during recovery');
578
+ },
579
+
580
+ abandonPendingOperation: async pending =>
581
+ repository.abandonReservation({
582
+ namespace: pending.reservation.namespace,
583
+ ownerProfileId: pending.reservation.ownerProfileId,
584
+ shareId: pending.reservation.shareId,
585
+ operationId: pending.reservation.operationId,
586
+ generation: pending.reservation.generation,
587
+ leaseOwner: pending.reservation.leaseOwner,
588
+ now: now(),
589
+ }),
590
+
591
+ getShareLink: async (shareId, context) => {
592
+ const owner = parseOwnerContext(context);
593
+
594
+ return repository.getShareLink({
595
+ shareId,
596
+ namespace: owner.namespace,
597
+ ownerProfileId: owner.ownerProfileId,
598
+ });
599
+ },
600
+
601
+ getActiveShareContent: async (shareId, context) => {
602
+ const owner = parseOwnerContext(context);
603
+
604
+ return repository.getCurrentShareContent({
605
+ shareId,
606
+ namespace: owner.namespace,
607
+ ownerProfileId: owner.ownerProfileId,
608
+ now: now(),
609
+ });
610
+ },
611
+
612
+ fetchShareContent: async (shareId, context, options = {}) => {
613
+ const owner = parseOwnerContext(context);
614
+ const current = await repository.getCurrentShareContent({
615
+ shareId,
616
+ namespace: owner.namespace,
617
+ ownerProfileId: owner.ownerProfileId,
618
+ now: now(),
619
+ allowExpired: options.allowExpired,
620
+ });
621
+
622
+ if (current.state !== 'active') {
623
+ return {
624
+ ok: false,
625
+ error: 'NOT_FOUND',
626
+ };
627
+ }
628
+
629
+ // Use the object's ORIGINAL operation id, never the latest metadata
630
+ // operation id, for read/stat/delete of an immutable object.
631
+ const result = await client.get({
632
+ namespace: current.namespace,
633
+ ownerProfileId: current.ownerProfileId,
634
+ shareId: current.shareId,
635
+ contentVersion: current.contentVersion,
636
+ objectId: current.objectRef,
637
+ operationId: current.operationId,
638
+ });
639
+ if (!result.ok) return result;
640
+
641
+ // The remote read can overlap a stop, expiry or replacement. Only
642
+ // release bytes if the same committed snapshot still permits it.
643
+ const latest = await repository.getCurrentShareContent({
644
+ shareId,
645
+ namespace: owner.namespace,
646
+ ownerProfileId: owner.ownerProfileId,
647
+ now: now(),
648
+ allowExpired: options.allowExpired,
649
+ });
650
+ if (
651
+ latest.state !== 'active' ||
652
+ latest.version !== current.version ||
653
+ latest.objectRef !== current.objectRef ||
654
+ latest.operationId !== current.operationId ||
655
+ latest.contentVersion !== current.contentVersion
656
+ ) {
657
+ return { ok: false, error: 'NOT_FOUND' };
658
+ }
659
+ return result;
660
+ },
661
+
662
+ readOwnerRecovery: async (shareId, context) => {
663
+ const owner = parseOwnerContext(context);
664
+ // Owner recovery deliberately admits retained *expired* content so the
665
+ // owner can extend/edit it. Stopped, staging, content_missing and
666
+ // missing shares still fail closed here.
667
+ const current = await repository.getCurrentShareContent({
668
+ shareId,
669
+ namespace: owner.namespace,
670
+ ownerProfileId: owner.ownerProfileId,
671
+ now: now(),
672
+ allowExpired: true,
673
+ });
674
+
675
+ if (current.state !== 'active') {
676
+ return { ok: false, error: 'NOT_FOUND' };
677
+ }
678
+
679
+ const result = await client.readRecovery({
680
+ namespace: current.namespace,
681
+ ownerProfileId: current.ownerProfileId,
682
+ shareId: current.shareId,
683
+ contentVersion: current.contentVersion,
684
+ objectId: current.objectRef,
685
+ operationId: current.operationId,
686
+ });
687
+
688
+ if (!result.ok) return result;
689
+
690
+ // The remote read can overlap a stop/replacement. Only release the
691
+ // recovery for the same immutable committed tuple that authorized it.
692
+ const latest = await repository.getCurrentShareContent({
693
+ shareId,
694
+ namespace: owner.namespace,
695
+ ownerProfileId: owner.ownerProfileId,
696
+ now: now(),
697
+ allowExpired: true,
698
+ });
699
+ if (
700
+ latest.state !== 'active' ||
701
+ latest.version !== current.version ||
702
+ latest.objectRef !== current.objectRef ||
703
+ latest.operationId !== current.operationId ||
704
+ latest.contentVersion !== current.contentVersion
705
+ ) {
706
+ return { ok: false, error: 'NOT_FOUND' };
707
+ }
708
+
709
+ return result;
710
+ },
711
+ };
712
+ };
713
+
714
+ export type { PendingShareContentOperation };