@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,684 @@
1
+ import { randomBytes, createHash } from 'node:crypto';
2
+
3
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { v4 as uuid } from 'uuid';
5
+
6
+ import {
7
+ computeShareLinkPayloadHash,
8
+ computeShareLinkRequestHash,
9
+ } from '@helpers/share-link-lifecycle';
10
+ import {
11
+ createShareLinkCoordinator,
12
+ runShareContentCleanupOnce,
13
+ } from '@helpers/share-link-coordinator';
14
+ import type { ShareLinkLifecycleRepository } from '@helpers/share-link-coordinator';
15
+ import { neogma } from '@instance';
16
+
17
+ import {
18
+ abandonReservation,
19
+ claimCleanupJobs,
20
+ completeCleanupJob,
21
+ finalizeReservation,
22
+ getCurrentShareContent,
23
+ getCleanupJob,
24
+ getShareLink,
25
+ listCleanupJobsForShare,
26
+ reserveCreate,
27
+ reserveReplacement,
28
+ revokeShareLink,
29
+ } from '../src/accesslayer/share-link';
30
+ import type {
31
+ ShareContentClient,
32
+ ShareContentPutRequest,
33
+ } from '../src/helpers/share-content-client';
34
+ import { ensureShareLinkConstraints } from '../src/models/share-link-constraints';
35
+
36
+ /**
37
+ * LC-2187 coordinator orchestration against a REAL Neo4j C3 repository with a
38
+ * deterministic in-memory LearnCloud stand-in. The Neo4j lock/fence/cleanup
39
+ * semantics are real; the remote object store is explicitly a fake, so this is
40
+ * NOT the reviewed C4 HTTP endpoint integration.
41
+ */
42
+
43
+ import { clearLifecycleGraph } from './helpers/share-link-fixtures';
44
+
45
+ const NAMESPACE = 'test-namespace';
46
+ const OWNER = 'owner-1';
47
+ const LEASE_OWNER = 'coordinator-1';
48
+
49
+ const envelopeFor = (seed: string) => ({
50
+ v: 1 as const,
51
+ alg: 'A256GCM' as const,
52
+ iv: createHash('sha256').update(`iv:${seed}`).digest().subarray(0, 12).toString('base64url'),
53
+ ct: createHash('sha256').update(`ct:${seed}`).digest().toString('base64url'),
54
+ });
55
+
56
+ const recoveryFor = (seed: string) => ({
57
+ protected: Buffer.from(`protected:${seed}`).toString('base64url'),
58
+ iv: Buffer.from(`iv:${seed}`).toString('base64url'),
59
+ ciphertext: Buffer.from(`ciphertext:${seed}`).toString('base64url'),
60
+ tag: Buffer.from(`tag:${seed}`).toString('base64url'),
61
+ });
62
+
63
+ type StoredObject = ShareContentPutRequest & {
64
+ payloadHash: string;
65
+ contentHash: string;
66
+ ciphertextBytes: number;
67
+ recoveryBytes: number;
68
+ deleted: boolean;
69
+ };
70
+
71
+ const createFakeLearnCloud = (options?: {
72
+ statPayloadHashOverride?: string;
73
+ deleteErrorOnce?: 'UNAVAILABLE';
74
+ }) => {
75
+ const objects = new Map<string, StoredObject>();
76
+ const puts: ShareContentPutRequest[] = [];
77
+ const deletes: Array<{ objectId: string; operationId: string; contentVersion: number }> = [];
78
+ let failNextDelete = options?.deleteErrorOnce === 'UNAVAILABLE';
79
+
80
+ const summary = (object: StoredObject) => ({
81
+ kind: 'active' as const,
82
+ namespace: object.namespace,
83
+ ownerProfileId: object.ownerProfileId,
84
+ shareId: object.shareId,
85
+ contentVersion: object.contentVersion,
86
+ objectId: object.objectId,
87
+ operationId: object.operationId,
88
+ contentHash: object.contentHash,
89
+ payloadHash: options?.statPayloadHashOverride ?? object.payloadHash,
90
+ ciphertextBytes: object.ciphertextBytes,
91
+ recoveryBytes: object.recoveryBytes,
92
+ createdAt: '2026-09-20T00:00:00.000Z',
93
+ });
94
+
95
+ const client = {
96
+ put: vi.fn(async (request: ShareContentPutRequest) => {
97
+ const payloadHash = computeShareLinkPayloadHash({
98
+ envelope: request.envelope,
99
+ ownerEncryptedRecovery: request.ownerEncryptedRecovery,
100
+ });
101
+ const object: StoredObject = {
102
+ ...request,
103
+ payloadHash,
104
+ contentHash: createHash('sha256')
105
+ .update(`${request.objectId}:${payloadHash}`)
106
+ .digest('hex'),
107
+ ciphertextBytes: Buffer.from(request.envelope.ct, 'base64url').length,
108
+ recoveryBytes: Buffer.byteLength(
109
+ JSON.stringify(request.ownerEncryptedRecovery),
110
+ 'utf8'
111
+ ),
112
+ deleted: false,
113
+ };
114
+ objects.set(request.objectId, object);
115
+ puts.push(request);
116
+
117
+ return {
118
+ ok: true as const,
119
+ value: { status: 'created' as const, record: summary(object) },
120
+ };
121
+ }),
122
+ stat: vi.fn(async (request: { objectId: string }) => {
123
+ const object = objects.get(request.objectId);
124
+
125
+ if (!object) return { ok: false as const, error: 'NOT_FOUND' as const };
126
+
127
+ if (object.deleted) {
128
+ return {
129
+ ok: true as const,
130
+ value: {
131
+ kind: 'tombstone' as const,
132
+ namespace: object.namespace,
133
+ ownerProfileId: object.ownerProfileId,
134
+ shareId: object.shareId,
135
+ contentVersion: object.contentVersion,
136
+ objectId: object.objectId,
137
+ operationId: object.operationId,
138
+ deletedAt: '2026-09-20T00:00:00.000Z',
139
+ },
140
+ };
141
+ }
142
+
143
+ return { ok: true as const, value: summary(object) };
144
+ }),
145
+ delete: vi.fn(
146
+ async (request: { objectId: string; operationId: string; contentVersion: number }) => {
147
+ deletes.push({
148
+ objectId: request.objectId,
149
+ operationId: request.operationId,
150
+ contentVersion: request.contentVersion,
151
+ });
152
+
153
+ if (failNextDelete) {
154
+ failNextDelete = false;
155
+
156
+ return { ok: false as const, error: 'UNAVAILABLE' as const };
157
+ }
158
+
159
+ const object = objects.get(request.objectId);
160
+
161
+ if (object) object.deleted = true;
162
+
163
+ return {
164
+ ok: true as const,
165
+ value: {
166
+ kind: 'tombstone' as const,
167
+ namespace: NAMESPACE,
168
+ ownerProfileId: OWNER,
169
+ shareId: object?.shareId ?? '',
170
+ contentVersion: request.contentVersion,
171
+ objectId: request.objectId,
172
+ operationId: request.operationId,
173
+ deletedAt: '2026-09-20T00:00:00.000Z',
174
+ },
175
+ };
176
+ }
177
+ ),
178
+ get: vi.fn(),
179
+ readRecovery: vi.fn(),
180
+ };
181
+
182
+ return {
183
+ client: client as unknown as ShareContentClient,
184
+ objects,
185
+ puts,
186
+ deletes,
187
+ raw: client,
188
+ };
189
+ };
190
+
191
+ const repository: ShareLinkLifecycleRepository = {
192
+ reserveCreate,
193
+ reserveReplacement,
194
+ finalizeReservation,
195
+ abandonReservation,
196
+ revokeShareLink,
197
+ getShareLink,
198
+ getCurrentShareContent,
199
+ claimCleanupJobs,
200
+ completeCleanupJob,
201
+ };
202
+
203
+ const makeCreateRequest = (overrides: Record<string, unknown> = {}) => ({
204
+ id: randomBytes(16).toString('base64url'),
205
+ clientRequestId: uuid(),
206
+ title: 'Shared credentials',
207
+ selectedCount: 1,
208
+ contentVersion: 1,
209
+ envelope: envelopeFor('create'),
210
+ ownerEncryptedRecovery: recoveryFor('create'),
211
+ ...overrides,
212
+ });
213
+
214
+ const makeCoordinator = (
215
+ client: ShareContentClient,
216
+ repo: ShareLinkLifecycleRepository = repository,
217
+ now?: () => Date
218
+ ) =>
219
+ createShareLinkCoordinator({
220
+ repository: repo,
221
+ client,
222
+ leaseOwner: LEASE_OWNER,
223
+ leaseMs: 60_000,
224
+ now: now ?? (() => new Date()),
225
+ });
226
+
227
+ const context = { namespace: NAMESPACE, ownerProfileId: OWNER };
228
+
229
+ describe('share-link coordinator (real Neo4j, fake LearnCloud)', () => {
230
+ beforeAll(async () => {
231
+ await ensureShareLinkConstraints();
232
+ });
233
+
234
+ beforeEach(async () => {
235
+ await clearLifecycleGraph();
236
+ });
237
+
238
+ afterAll(async () => {
239
+ await clearLifecycleGraph();
240
+ });
241
+
242
+ it('creates a share: reserve, upload reserved ids, verify stat and finalize', async () => {
243
+ const learnCloud = createFakeLearnCloud();
244
+ const coordinator = makeCoordinator(learnCloud.client);
245
+ const request = makeCreateRequest();
246
+
247
+ const result = await coordinator.createShareLink(request, context);
248
+
249
+ expect(result.status).toBe('committed');
250
+ expect(learnCloud.raw.put).toHaveBeenCalledTimes(1);
251
+
252
+ const put = learnCloud.puts[0];
253
+ expect(put?.contentVersion).toBe(1);
254
+
255
+ const share = await getShareLink({ shareId: request.id });
256
+ expect(share?.status).toBe('active');
257
+ expect(share?.contentState).toBe('finalized');
258
+ expect(share?.activeObjectRef).toBe(put?.objectId);
259
+ expect(share?.activeObjectOperationId).toBe(put?.operationId);
260
+ expect(share?.activeContentHash).toBe(
261
+ computeShareLinkPayloadHash({
262
+ envelope: request.envelope,
263
+ ownerEncryptedRecovery: request.ownerEncryptedRecovery,
264
+ })
265
+ );
266
+ });
267
+
268
+ it('replays a committed create without a second upload', async () => {
269
+ const learnCloud = createFakeLearnCloud();
270
+ const coordinator = makeCoordinator(learnCloud.client);
271
+ const request = makeCreateRequest();
272
+
273
+ expect((await coordinator.createShareLink(request, context)).status).toBe('committed');
274
+ const replay = await coordinator.createShareLink(request, context);
275
+
276
+ expect(replay.status).toBe('replayed');
277
+ expect(learnCloud.raw.put).toHaveBeenCalledTimes(1);
278
+ });
279
+
280
+ it('queues the superseded object for cleanup on a content update', async () => {
281
+ const learnCloud = createFakeLearnCloud();
282
+ const coordinator = makeCoordinator(learnCloud.client);
283
+ const created = makeCreateRequest();
284
+ const first = await coordinator.createShareLink(created, context);
285
+ if (first.status !== 'committed') throw new Error('expected committed create');
286
+
287
+ const update = await coordinator.updateShareLink(
288
+ {
289
+ id: created.id,
290
+ expectedVersion: first.share.version,
291
+ clientRequestId: uuid(),
292
+ contentVersion: 2,
293
+ selectedCount: 1,
294
+ envelope: envelopeFor('update'),
295
+ ownerEncryptedRecovery: recoveryFor('update'),
296
+ },
297
+ context
298
+ );
299
+
300
+ expect(update.status).toBe('committed');
301
+ if (update.status !== 'committed') return;
302
+
303
+ expect(update.share.contentVersion).toBe(2);
304
+ expect(update.share.activeObjectRef).not.toBe(first.share.activeObjectRef);
305
+
306
+ const jobs = await listCleanupJobsForShare(created.id);
307
+ const superseded = jobs.find(job => job.objectRef === first.share.activeObjectRef);
308
+ expect(superseded).toMatchObject({
309
+ reason: 'superseded',
310
+ operationId: first.share.activeObjectOperationId,
311
+ });
312
+ });
313
+
314
+ it('metadata-only update uploads nothing and preserves the original object operation id', async () => {
315
+ const learnCloud = createFakeLearnCloud();
316
+ const coordinator = makeCoordinator(learnCloud.client);
317
+ const created = makeCreateRequest();
318
+ const first = await coordinator.createShareLink(created, context);
319
+ if (first.status !== 'committed') throw new Error('expected committed create');
320
+
321
+ const updated = await coordinator.updateShareLink(
322
+ {
323
+ id: created.id,
324
+ expectedVersion: first.share.version,
325
+ clientRequestId: uuid(),
326
+ title: 'Renamed share',
327
+ },
328
+ context
329
+ );
330
+
331
+ expect(updated.status).toBe('committed');
332
+ expect(learnCloud.raw.put).toHaveBeenCalledTimes(1);
333
+
334
+ const share = await getShareLink({ shareId: created.id });
335
+ expect(share?.activeObjectRef).toBe(first.share.activeObjectRef);
336
+ expect(share?.activeObjectOperationId).toBe(first.share.activeObjectOperationId);
337
+ expect(share?.title).toBe('Renamed share');
338
+
339
+ const content = await getCurrentShareContent({ shareId: created.id });
340
+ expect(content.state === 'active' && content.operationId).toBe(
341
+ first.share.activeObjectOperationId
342
+ );
343
+
344
+ // Only the latest metadata operation id advanced; the object binding did not.
345
+ expect(share?.lastOperationId).not.toBe(first.share.activeObjectOperationId);
346
+ });
347
+
348
+ it('keeps a failed finalize resumable and commits the already-uploaded object later', async () => {
349
+ const learnCloud = createFakeLearnCloud();
350
+ const failingRepository: ShareLinkLifecycleRepository = {
351
+ ...repository,
352
+ finalizeReservation: vi
353
+ .fn()
354
+ .mockRejectedValueOnce(new Error('neo4j unavailable'))
355
+ .mockImplementation(finalizeReservation),
356
+ };
357
+ const coordinator = makeCoordinator(learnCloud.client, failingRepository);
358
+ const request = makeCreateRequest();
359
+
360
+ const first = await coordinator.createShareLink(request, context);
361
+
362
+ expect(first.status).toBe('pending');
363
+ if (first.status !== 'pending') return;
364
+ expect(first.reason).toBe('finalize_unavailable');
365
+ expect(learnCloud.raw.put).toHaveBeenCalledTimes(1);
366
+
367
+ const beforeResume = await getShareLink({ shareId: request.id });
368
+ expect(beforeResume?.contentState).toBe('staging');
369
+
370
+ const retryCoordinator = makeCoordinator(learnCloud.client, repository);
371
+ const resumed = await retryCoordinator.resumePendingOperation({
372
+ reservation: first.reservation,
373
+ });
374
+
375
+ expect(resumed.status).toBe('committed');
376
+ expect(learnCloud.raw.put).toHaveBeenCalledTimes(1);
377
+
378
+ const share = await getShareLink({ shareId: request.id });
379
+ expect(share?.contentState).toBe('finalized');
380
+ expect(share?.activeObjectRef).toBe(learnCloud.puts[0]?.objectId);
381
+ });
382
+
383
+ it('never finalizes on a wrong stat hash and does not destroy the staged object', async () => {
384
+ const learnCloud = createFakeLearnCloud({ statPayloadHashOverride: 'e'.repeat(64) });
385
+ const coordinator = makeCoordinator(learnCloud.client);
386
+ const request = makeCreateRequest();
387
+
388
+ const result = await coordinator.createShareLink(request, context);
389
+
390
+ expect(result.status).toBe('pending');
391
+ if (result.status === 'pending') expect(result.reason).toBe('stat_mismatch');
392
+
393
+ const share = await getShareLink({ shareId: request.id });
394
+ expect(share?.contentState).toBe('staging');
395
+ expect(share?.activeObjectRef).toBeNull();
396
+ expect(await listCleanupJobsForShare(request.id)).toHaveLength(0);
397
+ });
398
+
399
+ it('revokes synchronously and queues cleanup without any LearnCloud delete', async () => {
400
+ const learnCloud = createFakeLearnCloud();
401
+ const coordinator = makeCoordinator(learnCloud.client);
402
+ const created = makeCreateRequest();
403
+ const first = await coordinator.createShareLink(created, context);
404
+ if (first.status !== 'committed') throw new Error('expected committed create');
405
+
406
+ const revoked = await coordinator.revokeShareLink(
407
+ { id: created.id, clientRequestId: uuid() },
408
+ context
409
+ );
410
+
411
+ expect(revoked.status).toBe('revoked');
412
+ expect(revoked.share.status).toBe('stopped');
413
+ expect(learnCloud.raw.delete).not.toHaveBeenCalled();
414
+
415
+ const jobs = await listCleanupJobsForShare(created.id);
416
+ const stopped = jobs.find(job => job.objectRef === first.share.activeObjectRef);
417
+ expect(stopped).toMatchObject({
418
+ reason: 'stopped',
419
+ operationId: first.share.activeObjectOperationId,
420
+ });
421
+ });
422
+
423
+ it('runs a bounded cleanup with retry and preserves original object identity after metadata', async () => {
424
+ const learnCloud = createFakeLearnCloud({ deleteErrorOnce: 'UNAVAILABLE' });
425
+ const coordinator = makeCoordinator(learnCloud.client);
426
+ const created = makeCreateRequest();
427
+ const first = await coordinator.createShareLink(created, context);
428
+ if (first.status !== 'committed') throw new Error('expected committed create');
429
+
430
+ // Metadata-only update: the visible object operation id must not change.
431
+ const updated = await coordinator.updateShareLink(
432
+ {
433
+ id: created.id,
434
+ expectedVersion: first.share.version,
435
+ clientRequestId: uuid(),
436
+ note: 'updated note',
437
+ },
438
+ context
439
+ );
440
+ expect(updated.status).toBe('committed');
441
+
442
+ await coordinator.revokeShareLink({ id: created.id, clientRequestId: uuid() }, context);
443
+
444
+ const base = Date.now();
445
+ const firstRun = await runShareContentCleanupOnce({
446
+ repository,
447
+ client: learnCloud.client,
448
+ claimant: 'cleaner-1',
449
+ namespace: NAMESPACE,
450
+ now: () => new Date(base),
451
+ });
452
+
453
+ expect(firstRun).toMatchObject({ claimed: 1, completed: 0, retried: 1 });
454
+ expect(learnCloud.deletes[0]).toMatchObject({
455
+ objectId: first.share.activeObjectRef,
456
+ operationId: first.share.activeObjectOperationId,
457
+ });
458
+
459
+ const queued = await getCleanupJob(first.share.activeObjectRef as string);
460
+ expect(queued?.status).toBe('queued');
461
+
462
+ const secondRun = await runShareContentCleanupOnce({
463
+ repository,
464
+ client: learnCloud.client,
465
+ claimant: 'cleaner-2',
466
+ namespace: NAMESPACE,
467
+ now: () => new Date(Date.parse(queued!.nextAttemptAt) + 1000),
468
+ });
469
+
470
+ expect(secondRun).toMatchObject({ claimed: 1, completed: 1 });
471
+ expect(learnCloud.deletes).toHaveLength(2);
472
+ expect(learnCloud.deletes[1]).toMatchObject({
473
+ objectId: first.share.activeObjectRef,
474
+ operationId: first.share.activeObjectOperationId,
475
+ });
476
+ expect((await getCleanupJob(first.share.activeObjectRef as string))?.status).toBe(
477
+ 'completed'
478
+ );
479
+ });
480
+
481
+ it('fences a stale worker after revoke and never finalizes it', async () => {
482
+ const learnCloud = createFakeLearnCloud();
483
+ const coordinator = makeCoordinator(learnCloud.client);
484
+ const created = makeCreateRequest();
485
+ const first = await coordinator.createShareLink(created, context);
486
+ if (first.status !== 'committed') throw new Error('expected committed create');
487
+
488
+ // Stage an in-flight replacement but never drive it.
489
+ const staged = await reserveReplacement({
490
+ namespace: NAMESPACE,
491
+ ownerProfileId: OWNER,
492
+ clientRequestId: uuid(),
493
+ shareId: created.id,
494
+ expectedVersion: first.share.version,
495
+ requestHash: createHash('sha256').update('stale').digest('hex'),
496
+ leaseOwner: 'stale-worker',
497
+ now: new Date(),
498
+ content: {
499
+ contentHash: createHash('sha256').update('stale-content').digest('hex'),
500
+ contentBytes: 32,
501
+ recoveryHash: createHash('sha256').update('stale-recovery').digest('hex'),
502
+ recoveryBytes: 20,
503
+ contentVersion: 2,
504
+ selectedCount: 1,
505
+ },
506
+ });
507
+ expect(staged.outcome).toBe('reserved');
508
+ if (staged.outcome !== 'reserved' || !staged.reservation.objectRef) return;
509
+
510
+ await revokeShareLink({
511
+ namespace: NAMESPACE,
512
+ ownerProfileId: OWNER,
513
+ shareId: created.id,
514
+ now: new Date(),
515
+ });
516
+
517
+ await expect(
518
+ finalizeReservation({
519
+ namespace: NAMESPACE,
520
+ ownerProfileId: OWNER,
521
+ shareId: created.id,
522
+ operationId: staged.reservation.operationId,
523
+ objectRef: staged.reservation.objectRef,
524
+ generation: staged.reservation.generation,
525
+ leaseOwner: staged.reservation.leaseOwner,
526
+ now: new Date(),
527
+ })
528
+ ).rejects.toMatchObject({ code: 'CONFLICT' });
529
+
530
+ const share = await getShareLink({ shareId: created.id });
531
+ expect(share?.status).toBe('stopped');
532
+ expect(share?.activeObjectRef).toBe(first.share.activeObjectRef);
533
+ });
534
+
535
+ it.each([false, true])(
536
+ 'retains original expiry across create retry (already abandoned: %s)',
537
+ async alreadyAbandoned => {
538
+ const learnCloud = createFakeLearnCloud();
539
+ let failFirstPut = true;
540
+ const client = {
541
+ ...learnCloud.client,
542
+ put: vi.fn(async (request: Parameters<ShareContentClient['put']>[0]) => {
543
+ if (failFirstPut) {
544
+ failFirstPut = false;
545
+
546
+ return { ok: false as const, error: 'UNAVAILABLE' as const };
547
+ }
548
+
549
+ return learnCloud.client.put(request);
550
+ }),
551
+ } as unknown as ShareContentClient;
552
+
553
+ const t0 = new Date('2026-09-21T00:00:00.000Z');
554
+ let clock = t0;
555
+ const coordinator = createShareLinkCoordinator({
556
+ repository,
557
+ client,
558
+ leaseOwner: LEASE_OWNER,
559
+ leaseMs: 1_000,
560
+ now: () => clock,
561
+ policyResolver: {
562
+ resolve: async () => ({
563
+ isMinor: false,
564
+ policyResolved: true,
565
+ defaultExpiryDays: 365 as const,
566
+ viewCountingEnabled: true,
567
+ }),
568
+ },
569
+ });
570
+ const request = makeCreateRequest();
571
+
572
+ // First attempt stays pending on a transient upload failure.
573
+ const pending = await coordinator.createShareLink(request, context);
574
+ expect(pending.status).toBe('pending');
575
+ if (alreadyAbandoned && pending.status === 'pending') {
576
+ await coordinator.abandonPendingOperation({ reservation: pending.reservation });
577
+ }
578
+
579
+ // Clock advances past the 1s lease; the same logical create is retried
580
+ // with a server-derived default that would otherwise be `t0 + 5s + 365d`.
581
+ clock = new Date(t0.getTime() + 5_000);
582
+ const retry = await coordinator.createShareLink(request, context);
583
+ expect(retry.status).toBe('committed');
584
+
585
+ const share = await getShareLink({ shareId: request.id });
586
+ expect(share?.expiresAt).toBe('2027-09-21T00:00:00.000Z');
587
+
588
+ // The abandoned first upload is queued for cleanup and a fresh immutable
589
+ // object identity was allocated for the replacement.
590
+ const jobs = await listCleanupJobsForShare(request.id);
591
+ expect(jobs.some(job => job.reason === 'abandoned')).toBe(true);
592
+ expect(learnCloud.puts).toHaveLength(1);
593
+ }
594
+ );
595
+
596
+ it('admits retained expired content only to owner recovery while the public guard stays denied', async () => {
597
+ const learnCloud = createFakeLearnCloud();
598
+ const coordinator = makeCoordinator(learnCloud.client);
599
+ const request = makeCreateRequest({ expiresAt: '2020-01-01T00:00:00.000Z' });
600
+
601
+ expect((await coordinator.createShareLink(request, context)).status).toBe('committed');
602
+
603
+ learnCloud.raw.readRecovery.mockResolvedValueOnce({
604
+ ok: true as const,
605
+ value: { ownerEncryptedRecovery: recoveryFor('owner-expired') },
606
+ });
607
+
608
+ const owner = await coordinator.readOwnerRecovery(request.id, context);
609
+ expect(owner.ok).toBe(true);
610
+
611
+ const publicRead = await coordinator.fetchShareContent(request.id, context);
612
+ expect(publicRead.ok).toBe(false);
613
+ if (!publicRead.ok) expect(publicRead.error).toBe('NOT_FOUND');
614
+
615
+ const activeRead = await coordinator.getActiveShareContent(request.id, context);
616
+ expect(activeRead.state).toBe('not_active');
617
+ if (activeRead.state === 'not_active') expect(activeRead.reason).toBe('expired');
618
+ });
619
+
620
+ it('withholds owner recovery when a revoke lands during the remote read', async () => {
621
+ const learnCloud = createFakeLearnCloud();
622
+ const coordinator = makeCoordinator(learnCloud.client);
623
+ const request = makeCreateRequest();
624
+
625
+ expect((await coordinator.createShareLink(request, context)).status).toBe('committed');
626
+
627
+ learnCloud.raw.readRecovery.mockImplementationOnce(async () => {
628
+ await revokeShareLink({
629
+ namespace: NAMESPACE,
630
+ ownerProfileId: OWNER,
631
+ shareId: request.id,
632
+ now: new Date(),
633
+ });
634
+
635
+ return {
636
+ ok: true as const,
637
+ value: { ownerEncryptedRecovery: recoveryFor('revoked-during-read') },
638
+ };
639
+ });
640
+
641
+ const owner = await coordinator.readOwnerRecovery(request.id, context);
642
+ expect(owner.ok).toBe(false);
643
+ });
644
+
645
+ it('withholds owner recovery when a replacement commits during the remote read', async () => {
646
+ const learnCloud = createFakeLearnCloud();
647
+ const coordinator = makeCoordinator(learnCloud.client);
648
+ const request = makeCreateRequest();
649
+
650
+ const created = await coordinator.createShareLink(request, context);
651
+ if (created.status !== 'committed') throw new Error('expected committed create');
652
+
653
+ learnCloud.raw.readRecovery.mockImplementationOnce(async () => {
654
+ const share = await getShareLink({ shareId: request.id });
655
+ if (!share) throw new Error('expected share');
656
+
657
+ const replacement = await reserveReplacement({
658
+ namespace: NAMESPACE,
659
+ ownerProfileId: OWNER,
660
+ clientRequestId: uuid(),
661
+ shareId: request.id,
662
+ expectedVersion: share.version,
663
+ requestHash: computeShareLinkRequestHash('update', {
664
+ id: request.id,
665
+ title: 'Metadata only',
666
+ }),
667
+ title: 'Metadata only',
668
+ leaseOwner: 'worker-2',
669
+ now: new Date(),
670
+ });
671
+ if (replacement.outcome !== 'reserved') throw new Error('expected reservation');
672
+
673
+ await finalizeReservation({ ...replacement.reservation, now: new Date() });
674
+
675
+ return {
676
+ ok: true as const,
677
+ value: { ownerEncryptedRecovery: recoveryFor('replaced-during-read') },
678
+ };
679
+ });
680
+
681
+ const owner = await coordinator.readOwnerRecovery(request.id, context);
682
+ expect(owner.ok).toBe(false);
683
+ });
684
+ });