@learncard/network-brain-service 3.19.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/.env.example +47 -82
  2. package/AGENTS.md +52 -0
  3. package/CHANGELOG.md +147 -0
  4. package/README.md +51 -2
  5. package/bootstrapLambda.test.ts +72 -0
  6. package/config/config.dev.json +29 -0
  7. package/config/config.example.json +29 -0
  8. package/config/config.json +1 -0
  9. package/config/config.production.json +29 -0
  10. package/config/config.scouts.dev.json +24 -0
  11. package/config/config.scouts.production.json +24 -0
  12. package/contractEventsLambda.ts +18 -0
  13. package/contractEventsLambdaApp.ts +19 -0
  14. package/didWebLambda.ts +13 -18
  15. package/didWebLambdaApp.ts +22 -0
  16. package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
  17. package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
  18. package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
  19. package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
  20. package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
  21. package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
  22. package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
  23. package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
  24. package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
  25. package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
  26. package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
  27. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
  29. package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
  30. package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
  31. package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
  32. package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
  33. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  34. package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
  35. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  36. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
  37. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  38. package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
  39. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
  40. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  41. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
  42. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
  43. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
  44. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
  45. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  47. package/dist/accesslayer/credential/create.d.ts +2 -1
  48. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  49. package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
  50. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  51. package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
  52. package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
  53. package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
  54. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  55. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  56. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  57. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  58. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  59. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  60. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  61. package/dist/accesslayer/share-link/attachment.d.ts +37 -0
  62. package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
  63. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  64. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  65. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  66. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  67. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  68. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  69. package/dist/accesslayer/share-link/helpers.d.ts +59 -0
  70. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  71. package/dist/accesslayer/share-link/index.d.ts +12 -0
  72. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  73. package/dist/accesslayer/share-link/list.d.ts +5 -0
  74. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  75. package/dist/accesslayer/share-link/read.d.ts +5 -0
  76. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  77. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  78. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  79. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  80. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  81. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  82. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  83. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  84. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  85. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  86. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  87. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  88. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  89. package/dist/accesslayer/share-link/types.d.ts +375 -0
  90. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  91. package/dist/app.d.ts +4 -0
  92. package/dist/app.d.ts.map +1 -1
  93. package/dist/cache/app-store.caches.d.ts +49 -0
  94. package/dist/cache/app-store.caches.d.ts.map +1 -1
  95. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  96. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  97. package/dist/config/environment.d.ts +14 -0
  98. package/dist/config/environment.d.ts.map +1 -1
  99. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  100. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  101. package/dist/config/stageConfig.d.ts +112 -0
  102. package/dist/config/stageConfig.d.ts.map +1 -0
  103. package/dist/config/stageConfig.test.d.ts +2 -0
  104. package/dist/config/stageConfig.test.d.ts.map +1 -0
  105. package/dist/constants/auth-grant.d.ts +2 -0
  106. package/dist/constants/auth-grant.d.ts.map +1 -1
  107. package/dist/didkit_wasm_bg.wasm +0 -0
  108. package/dist/docker-entry.d.ts +1 -1
  109. package/dist/docker-entry.d.ts.map +1 -1
  110. package/dist/helpers/app-manifest.helpers.d.ts +5 -0
  111. package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
  112. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  113. package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
  114. package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
  115. package/dist/helpers/consent-referral.helpers.d.ts +2 -0
  116. package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
  117. package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
  118. package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
  119. package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
  120. package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
  121. package/dist/helpers/contract-events.helpers.d.ts +25 -0
  122. package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
  123. package/dist/helpers/contract-requests.helpers.d.ts +19 -0
  124. package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
  125. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  126. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  127. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  128. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  129. package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
  130. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  131. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  132. package/dist/helpers/inline-template.helpers.d.ts +45 -0
  133. package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
  134. package/dist/helpers/notificationMessages.d.ts +1 -1
  135. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  136. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  137. package/dist/helpers/notifications.helpers.d.ts +6 -0
  138. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  139. package/dist/helpers/presentation-metadata.d.ts +4 -0
  140. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  141. package/dist/helpers/presentation.helpers.d.ts +1 -1
  142. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  143. package/dist/helpers/profile.helpers.d.ts +2 -0
  144. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  145. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  146. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  147. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  148. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  149. package/dist/helpers/share-content-client/client.d.ts +5 -0
  150. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  151. package/dist/helpers/share-content-client/config.d.ts +11 -0
  152. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  153. package/dist/helpers/share-content-client/index.d.ts +6 -0
  154. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  155. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  156. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  157. package/dist/helpers/share-content-client/types.d.ts +145 -0
  158. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  159. package/dist/helpers/share-link-attachment-access.d.ts +4 -0
  160. package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
  161. package/dist/helpers/share-link-attachment.d.ts +18 -0
  162. package/dist/helpers/share-link-attachment.d.ts.map +1 -0
  163. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
  164. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
  165. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  166. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  167. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  168. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  169. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  170. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  171. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  172. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  173. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  174. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  175. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  176. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  177. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  178. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  179. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  180. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  181. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  182. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  183. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  184. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  185. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  186. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  187. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  188. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  189. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  190. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  191. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  192. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  193. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  194. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  195. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  196. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  197. package/dist/helpers/share-link-maintenance/runner.d.ts +20 -0
  198. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  199. package/dist/helpers/share-link-maintenance/runtime.d.ts +38 -0
  200. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  201. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  202. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  203. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  204. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  205. package/dist/helpers/share-link-maintenance/types.d.ts +56 -0
  206. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  207. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  208. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  209. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  210. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  211. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  212. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  213. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  214. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  215. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  216. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  217. package/dist/helpers/share-link-passcode.d.ts +6 -0
  218. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  219. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  220. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  221. package/dist/helpers/share-link-policy/production.d.ts +11 -0
  222. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  223. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  224. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  225. package/dist/helpers/share-link-policy/types.d.ts +22 -0
  226. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  227. package/dist/helpers/share-link-public/config.d.ts +13 -0
  228. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  229. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  230. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  231. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  232. package/dist/helpers/smartResume.helpers.d.ts +3 -0
  233. package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
  234. package/dist/index.js +2 -2
  235. package/dist/models/AppManifestVersion.d.ts +20 -0
  236. package/dist/models/AppManifestVersion.d.ts.map +1 -0
  237. package/dist/models/AppStoreListing.d.ts +13 -0
  238. package/dist/models/AppStoreListing.d.ts.map +1 -1
  239. package/dist/models/ConsentFlowContract.d.ts +13 -2
  240. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  241. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  242. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  243. package/dist/models/Integration.d.ts +2 -0
  244. package/dist/models/Integration.d.ts.map +1 -1
  245. package/dist/models/Presentation.d.ts +2 -0
  246. package/dist/models/Presentation.d.ts.map +1 -1
  247. package/dist/models/Profile.d.ts +2 -0
  248. package/dist/models/Profile.d.ts.map +1 -1
  249. package/dist/models/ShareLink.d.ts +46 -0
  250. package/dist/models/ShareLink.d.ts.map +1 -0
  251. package/dist/models/ShareViewReceipt.d.ts +22 -0
  252. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  253. package/dist/models/index.d.ts +2 -0
  254. package/dist/models/index.d.ts.map +1 -1
  255. package/dist/models/share-link-constraints.d.ts +3 -0
  256. package/dist/models/share-link-constraints.d.ts.map +1 -0
  257. package/dist/openapi.d.ts +1 -1
  258. package/dist/openapi.d.ts.map +1 -1
  259. package/dist/public-share-link-http.d.ts +4 -0
  260. package/dist/public-share-link-http.d.ts.map +1 -0
  261. package/dist/routes/app-store.d.ts +619 -0
  262. package/dist/routes/app-store.d.ts.map +1 -1
  263. package/dist/routes/boosts.d.ts +2 -2
  264. package/dist/routes/boosts.d.ts.map +1 -1
  265. package/dist/routes/contracts.d.ts +148 -5
  266. package/dist/routes/contracts.d.ts.map +1 -1
  267. package/dist/routes/credentials.d.ts +35 -1
  268. package/dist/routes/credentials.d.ts.map +1 -1
  269. package/dist/routes/inbox.d.ts +52 -2
  270. package/dist/routes/inbox.d.ts.map +1 -1
  271. package/dist/routes/index.d.ts +153 -0
  272. package/dist/routes/index.d.ts.map +1 -1
  273. package/dist/routes/presentations.d.ts +1 -0
  274. package/dist/routes/presentations.d.ts.map +1 -1
  275. package/dist/routes/profile-manager.d.ts.map +1 -1
  276. package/dist/routes/profiles.d.ts.map +1 -1
  277. package/dist/routes/public-share-links.d.ts +285 -0
  278. package/dist/routes/public-share-links.d.ts.map +1 -0
  279. package/dist/routes/share-links.d.ts +1005 -0
  280. package/dist/routes/share-links.d.ts.map +1 -0
  281. package/dist/routes/test.d.ts +5 -0
  282. package/dist/routes/test.d.ts.map +1 -1
  283. package/dist/routes/workflows.d.ts.map +1 -1
  284. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  285. package/dist/types/app-manifest.d.ts +339 -0
  286. package/dist/types/app-manifest.d.ts.map +1 -0
  287. package/dist/types/app-store-listing.d.ts +34 -0
  288. package/dist/types/app-store-listing.d.ts.map +1 -1
  289. package/dist/types/consentflowcontract.d.ts +31 -4
  290. package/dist/types/consentflowcontract.d.ts.map +1 -1
  291. package/dist/types/credential-refresh.d.ts +1 -1
  292. package/esbuildPlugins.cjs +1 -0
  293. package/lambda.ts +49 -192
  294. package/lambdaApp.ts +203 -0
  295. package/package.json +25 -17
  296. package/scripts/export-openapi.ts +20 -0
  297. package/scripts/offline-schema-environment.ts +18 -0
  298. package/scripts/seed-demo-persona.ts +425 -0
  299. package/scripts/verify-python-contracts.ts +72 -0
  300. package/serverless-local.yml +3 -6
  301. package/serverless.function-env.cjs +50 -0
  302. package/serverless.local.yml +0 -1
  303. package/serverless.yml +44 -55
  304. package/shareLinkMaintenanceLambda.ts +18 -0
  305. package/shareLinkMaintenanceLambdaApp.ts +50 -0
  306. package/src/accesslayer/app-manifest-version/create.ts +57 -0
  307. package/src/accesslayer/app-manifest-version/read.ts +129 -0
  308. package/src/accesslayer/app-manifest-version/update.ts +61 -0
  309. package/src/accesslayer/app-store-listing/read.ts +6 -0
  310. package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
  311. package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
  312. package/src/accesslayer/app-store-listing/update.ts +15 -0
  313. package/src/accesslayer/boost/relationships/create.ts +8 -2
  314. package/src/accesslayer/consentflowcontract/create.ts +81 -8
  315. package/src/accesslayer/consentflowcontract/read.ts +84 -10
  316. package/src/accesslayer/consentflowcontract/relationships/create.ts +125 -109
  317. package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
  318. package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
  319. package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
  320. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
  321. package/src/accesslayer/consentflowcontract/relationships/update.ts +255 -217
  322. package/src/accesslayer/credential/create.test.ts +2 -115
  323. package/src/accesslayer/credential/create.ts +11 -6
  324. package/src/accesslayer/credential/relationships/create.ts +17 -10
  325. package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
  326. package/src/accesslayer/inbox-credential/update.ts +4 -2
  327. package/src/accesslayer/presentation/read.ts +57 -25
  328. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  329. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  330. package/src/accesslayer/share-link/abandon.ts +120 -0
  331. package/src/accesslayer/share-link/attachment.ts +360 -0
  332. package/src/accesslayer/share-link/cleanup.ts +271 -0
  333. package/src/accesslayer/share-link/errors.ts +32 -0
  334. package/src/accesslayer/share-link/finalize.ts +313 -0
  335. package/src/accesslayer/share-link/helpers.ts +450 -0
  336. package/src/accesslayer/share-link/index.ts +11 -0
  337. package/src/accesslayer/share-link/list.ts +162 -0
  338. package/src/accesslayer/share-link/read.ts +107 -0
  339. package/src/accesslayer/share-link/receipt.ts +548 -0
  340. package/src/accesslayer/share-link/recovery.ts +565 -0
  341. package/src/accesslayer/share-link/reserve.ts +763 -0
  342. package/src/accesslayer/share-link/revoke.ts +232 -0
  343. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  344. package/src/accesslayer/share-link/transaction.ts +126 -0
  345. package/src/accesslayer/share-link/types.ts +606 -0
  346. package/src/app.ts +6 -0
  347. package/src/cache/app-store.caches.ts +35 -0
  348. package/src/config/applyDockerStageConfig.ts +6 -0
  349. package/src/config/environment.test.ts +17 -12
  350. package/src/config/environment.ts +15 -0
  351. package/src/config/serverlessFunctionEnv.test.ts +105 -0
  352. package/src/config/stageConfig.test.ts +25 -0
  353. package/src/config/stageConfig.ts +24 -0
  354. package/src/constants/auth-grant.ts +5 -0
  355. package/src/docker-entry.ts +81 -0
  356. package/src/helpers/app-manifest.helpers.ts +185 -0
  357. package/src/helpers/boost.helpers.ts +4 -31
  358. package/src/helpers/claim-hooks.helpers.ts +20 -7
  359. package/src/helpers/consent-referral.helpers.ts +6 -0
  360. package/src/helpers/consent-scopes.helpers.ts +58 -0
  361. package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
  362. package/src/helpers/contract-events.helpers.ts +540 -0
  363. package/src/helpers/contract-requests.helpers.ts +211 -0
  364. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  365. package/src/helpers/credentialSubject.helpers.ts +40 -0
  366. package/src/helpers/finalize-inbox.helpers.ts +163 -44
  367. package/src/helpers/inbox.helpers.ts +58 -70
  368. package/src/helpers/inline-template.helpers.ts +560 -0
  369. package/src/helpers/notificationMessages.ts +77 -1
  370. package/src/helpers/notificationQueue.helpers.ts +23 -5
  371. package/src/helpers/notifications.helpers.ts +25 -1
  372. package/src/helpers/presentation-metadata.ts +41 -0
  373. package/src/helpers/presentation.helpers.ts +27 -20
  374. package/src/helpers/profile.helpers.ts +13 -0
  375. package/src/helpers/share-content-client/adapters.ts +55 -0
  376. package/src/helpers/share-content-client/canonical.ts +25 -0
  377. package/src/helpers/share-content-client/client.ts +440 -0
  378. package/src/helpers/share-content-client/config.ts +189 -0
  379. package/src/helpers/share-content-client/index.ts +27 -0
  380. package/src/helpers/share-content-client/responses.ts +273 -0
  381. package/src/helpers/share-content-client/types.ts +253 -0
  382. package/src/helpers/share-link-attachment-access.ts +87 -0
  383. package/src/helpers/share-link-attachment.ts +115 -0
  384. package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
  385. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  386. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  387. package/src/helpers/share-link-coordinator/coordinator.ts +727 -0
  388. package/src/helpers/share-link-coordinator/index.ts +8 -0
  389. package/src/helpers/share-link-coordinator/recovery-runner.ts +443 -0
  390. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  391. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  392. package/src/helpers/share-link-coordinator/types.ts +353 -0
  393. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  394. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  395. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  396. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  397. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  398. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  399. package/src/helpers/share-link-maintenance/config.ts +312 -0
  400. package/src/helpers/share-link-maintenance/index.ts +24 -0
  401. package/src/helpers/share-link-maintenance/runner.ts +251 -0
  402. package/src/helpers/share-link-maintenance/runtime.ts +340 -0
  403. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  404. package/src/helpers/share-link-maintenance/telemetry.ts +73 -0
  405. package/src/helpers/share-link-maintenance/types.ts +91 -0
  406. package/src/helpers/share-link-owner/config.ts +48 -0
  407. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  408. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  409. package/src/helpers/share-link-owner-projection.ts +54 -0
  410. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  411. package/src/helpers/share-link-passcode.ts +47 -0
  412. package/src/helpers/share-link-policy/index.ts +7 -0
  413. package/src/helpers/share-link-policy/production.ts +108 -0
  414. package/src/helpers/share-link-policy/resolver.ts +113 -0
  415. package/src/helpers/share-link-policy/types.ts +56 -0
  416. package/src/helpers/share-link-public/config.ts +41 -0
  417. package/src/helpers/share-link-view-notification.ts +18 -0
  418. package/src/helpers/signingAuthority.helpers.ts +23 -14
  419. package/src/helpers/smartResume.helpers.ts +123 -0
  420. package/src/models/AppManifestVersion.ts +43 -0
  421. package/src/models/AppStoreListing.ts +32 -0
  422. package/src/models/ConsentFlowContract.ts +34 -3
  423. package/src/models/ConsentFlowTerms.ts +10 -0
  424. package/src/models/ConsentFlowTransaction.ts +3 -0
  425. package/src/models/Integration.ts +2 -0
  426. package/src/models/Presentation.ts +2 -2
  427. package/src/models/Profile.ts +3 -2
  428. package/src/models/ShareLink.ts +134 -0
  429. package/src/models/ShareViewReceipt.ts +69 -0
  430. package/src/models/index.ts +25 -0
  431. package/src/models/share-link-constraints.ts +98 -0
  432. package/src/openapi.ts +44 -8
  433. package/src/public-share-link-http.ts +49 -0
  434. package/src/routes/app-store.ts +1056 -38
  435. package/src/routes/boosts.ts +14 -25
  436. package/src/routes/contracts.ts +384 -273
  437. package/src/routes/inbox.ts +4 -2
  438. package/src/routes/index.ts +142 -44
  439. package/src/routes/presentations.ts +19 -3
  440. package/src/routes/profile-manager.ts +29 -2
  441. package/src/routes/profiles.ts +29 -6
  442. package/src/routes/public-share-links.ts +983 -0
  443. package/src/routes/share-links.ts +709 -0
  444. package/src/routes/workflows.ts +3 -2
  445. package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
  446. package/src/types/app-manifest.ts +28 -0
  447. package/src/types/app-store-listing.ts +13 -0
  448. package/src/types/consentflowcontract.ts +25 -4
  449. package/test/app-manifest.helpers.spec.ts +170 -0
  450. package/test/app-manifest.routes.spec.ts +299 -0
  451. package/test/app-store-listing-updates.spec.ts +236 -0
  452. package/test/app-store-listing.spec.ts +65 -3
  453. package/test/consent-data-boundaries.spec.ts +352 -0
  454. package/test/consent-recipients.spec.ts +513 -0
  455. package/test/consent-scopes.helpers.spec.ts +67 -0
  456. package/test/consentflow.spec.ts +63 -2
  457. package/test/contract-requests.spec.ts +1588 -0
  458. package/test/credential-storage.neo4j.spec.ts +122 -0
  459. package/test/dids.spec.ts +10 -1
  460. package/test/guardian-gated-routes.spec.ts +49 -0
  461. package/test/helpers/share-link-fixtures.ts +94 -0
  462. package/test/inbox-security.e2e.ts +26 -10
  463. package/test/inbox.spec.ts +184 -8
  464. package/test/inline-template.helpers.spec.ts +49 -0
  465. package/test/managed-recipient-inbox.spec.ts +152 -0
  466. package/test/notificationMessages.spec.ts +13 -0
  467. package/test/openapi.spec.ts +14 -53
  468. package/test/presentations.spec.ts +54 -6
  469. package/test/profiles.spec.ts +97 -1
  470. package/test/share-content-client.contract.spec.ts +365 -0
  471. package/test/share-content-client.unit.spec.ts +462 -0
  472. package/test/share-link-abuse.unit.spec.ts +65 -0
  473. package/test/share-link-attachment.neo4j.spec.ts +262 -0
  474. package/test/share-link-attachment.unit.spec.ts +244 -0
  475. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  476. package/test/share-link-coordinator.unit.spec.ts +990 -0
  477. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  478. package/test/share-link-lifecycle.neo4j.spec.ts +1411 -0
  479. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  480. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  481. package/test/share-link-list.neo4j.spec.ts +324 -0
  482. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  483. package/test/share-link-maintenance.unit.spec.ts +1140 -0
  484. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  485. package/test/share-link-owner-route.unit.spec.ts +820 -0
  486. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  487. package/test/share-link-passcode.unit.spec.ts +29 -0
  488. package/test/share-link-policy.unit.spec.ts +149 -0
  489. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  490. package/test/share-link-production-policy.unit.spec.ts +126 -0
  491. package/test/share-link-public-config.unit.spec.ts +64 -0
  492. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  493. package/test/share-link-public-route.unit.spec.ts +1079 -0
  494. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  495. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  496. package/test/share-link-recovery.unit.spec.ts +726 -0
  497. package/test/share-link-service-policy.neo4j.spec.ts +403 -0
  498. package/test/signing-authority-encryption.spec.ts +75 -0
  499. package/test/smart-resume-consent.spec.ts +233 -0
  500. package/test/smart-resume-upload.spec.ts +65 -0
  501. package/tsconfig.build.json +1 -1
  502. package/tsconfig.json +1 -0
  503. package/vitest.config.ts +14 -0
  504. package/vitest.inbox-e2e.config.ts +2 -0
  505. package/vitest.share-links-neo4j.config.ts +19 -0
  506. package/vitest.share-links-unit.config.ts +28 -0
  507. package/workerLambda.test.ts +100 -0
@@ -0,0 +1,935 @@
1
+ import { randomBytes } 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 { computeLeaseExpiry, computeShareLinkRequestHash } from '@helpers/share-link-lifecycle';
7
+ import {
8
+ recoverShareLinkOperation,
9
+ runShareLinkRecoveryOnce,
10
+ } from '@helpers/share-link-coordinator';
11
+ import { neogma } from '@instance';
12
+
13
+ import {
14
+ abandonRecoveredReservation,
15
+ claimRecoverableReservation,
16
+ discoverRecoverableReservations,
17
+ finalizeReservation,
18
+ getShareLink,
19
+ listCleanupJobsForShare,
20
+ readShareLinkRecoveryTarget,
21
+ reserveCreate,
22
+ reserveReplacement,
23
+ revokeShareLink,
24
+ } from '../src/accesslayer/share-link';
25
+ import type { ShareLinkRecoveryKey } from '../src/accesslayer/share-link';
26
+ import { ensureShareLinkConstraints } from '../src/models/share-link-constraints';
27
+ import type { ShareContentClient } from '../src/helpers/share-content-client';
28
+
29
+ /**
30
+ * LC-2187 durable reservation recovery against a REAL Neo4j repository.
31
+ *
32
+ * These specs prove the persisted-state claim/reclaim fences, not remote
33
+ * storage. The coordinator's stat verification is covered by the DB-free
34
+ * recovery unit spec; the reviewed C4 HTTP endpoint remains separate evidence.
35
+ */
36
+
37
+ import {
38
+ nextShareId,
39
+ contentBinding,
40
+ clearLifecycleGraph,
41
+ reserveShareFixture,
42
+ type ReservedCreate,
43
+ } from './helpers/share-link-fixtures';
44
+
45
+ const NAMESPACE = 'test-namespace';
46
+ const OTHER_NAMESPACE = 'other.example';
47
+ const OWNER = 'owner-1';
48
+ const BASE = new Date('2026-09-20T12:00:00.000Z');
49
+ const AFTER_EXPIRY = new Date(BASE.getTime() + 5_000);
50
+
51
+ const readReservationProps = async (shareId: string): Promise<Record<string, unknown> | null> => {
52
+ const result = await neogma.queryRunner.run(
53
+ 'MATCH (r:ShareLinkReservation {shareId: $shareId}) RETURN r LIMIT 1',
54
+ { shareId }
55
+ );
56
+
57
+ return (
58
+ (result.records[0]?.get('r') as { properties?: Record<string, unknown> } | undefined)
59
+ ?.properties ?? null
60
+ );
61
+ };
62
+
63
+ const reserveNewShare = async (options?: {
64
+ namespace?: string;
65
+ ownerProfileId?: string;
66
+ shareId?: string;
67
+ clientRequestId?: string;
68
+ requestHash?: string;
69
+ leaseMs?: number;
70
+ now?: Date;
71
+ title?: string;
72
+ }): Promise<ReservedCreate> => {
73
+ return reserveShareFixture({
74
+ ...options,
75
+ namespace: options?.namespace ?? NAMESPACE,
76
+ ownerProfileId: options?.ownerProfileId ?? OWNER,
77
+ now: options?.now ?? BASE,
78
+ });
79
+ };
80
+ const commitNewShare = async (options?: {
81
+ namespace?: string;
82
+ ownerProfileId?: string;
83
+ shareId?: string;
84
+ now?: Date;
85
+ }) => {
86
+ const created = await reserveNewShare({ ...options, now: options?.now ?? BASE });
87
+
88
+ const finalized = await finalizeReservation({
89
+ namespace: created.namespace,
90
+ ownerProfileId: created.ownerProfileId,
91
+ shareId: created.shareId,
92
+ operationId: created.operationId,
93
+ objectRef: created.objectRef,
94
+ generation: created.generation,
95
+ leaseOwner: created.leaseOwner,
96
+ now: options?.now ?? BASE,
97
+ });
98
+
99
+ if (finalized.outcome !== 'finalized') {
100
+ throw new Error(`expected finalized create, got ${finalized.outcome}`);
101
+ }
102
+
103
+ return { ...created, committed: finalized.share };
104
+ };
105
+
106
+ const keyFor = (
107
+ created: Pick<ReservedCreate, 'namespace' | 'ownerProfileId' | 'shareId' | 'operationId'>
108
+ ): ShareLinkRecoveryKey => ({
109
+ namespace: created.namespace,
110
+ ownerProfileId: created.ownerProfileId,
111
+ shareId: created.shareId,
112
+ operationId: created.operationId,
113
+ });
114
+
115
+ const reserveReplacementContent = async (options: {
116
+ created: { namespace: string; ownerProfileId: string; shareId: string; objectRef: string };
117
+ seed: string;
118
+ contentVersion: number;
119
+ leaseMs?: number;
120
+ now?: Date;
121
+ clientRequestId?: string;
122
+ }) => {
123
+ const content = contentBinding(options.seed);
124
+ const clientRequestId = options.clientRequestId ?? uuid();
125
+ const requestHash = computeShareLinkRequestHash('update', {
126
+ id: options.created.shareId,
127
+ ...content,
128
+ contentVersion: options.contentVersion,
129
+ selectedCount: 1,
130
+ });
131
+
132
+ const result = await reserveReplacement({
133
+ namespace: options.created.namespace,
134
+ ownerProfileId: options.created.ownerProfileId,
135
+ clientRequestId,
136
+ shareId: options.created.shareId,
137
+ expectedVersion: 2,
138
+ requestHash,
139
+ leaseOwner: 'worker-2',
140
+ leaseMs: options.leaseMs,
141
+ now: options.now ?? BASE,
142
+ content: {
143
+ ...content,
144
+ contentVersion: options.contentVersion,
145
+ selectedCount: 1,
146
+ },
147
+ });
148
+
149
+ if (result.outcome !== 'reserved' || !result.reservation.objectRef) {
150
+ throw new Error(`expected reserved replacement, got ${result.outcome}`);
151
+ }
152
+
153
+ return { clientRequestId, requestHash, reservation: result.reservation };
154
+ };
155
+
156
+ const recoveryRepository = {
157
+ discoverRecoverableReservations,
158
+ readShareLinkRecoveryTarget,
159
+ claimRecoverableReservation,
160
+ abandonRecoveredReservation,
161
+ finalizeReservation,
162
+ };
163
+
164
+ const fakeStat = (overrides: Record<string, unknown> = {}) =>
165
+ vi.fn(async () => ({
166
+ ok: true as const,
167
+ value: {
168
+ kind: 'active' as const,
169
+ namespace: NAMESPACE,
170
+ ownerProfileId: OWNER,
171
+ shareId: '',
172
+ contentVersion: 1,
173
+ objectId: '',
174
+ operationId: '',
175
+ contentHash: 'f'.repeat(64),
176
+ payloadHash: '',
177
+ ciphertextBytes: 128,
178
+ recoveryBytes: 256,
179
+ createdAt: '2026-09-20T12:00:00.000Z',
180
+ ...overrides,
181
+ },
182
+ }));
183
+
184
+ const recoveryDependencies = (stat: ShareContentClient['stat']) => ({
185
+ repository: recoveryRepository,
186
+ client: { stat } as Pick<ShareContentClient, 'stat'>,
187
+ claimant: 'recovery-1',
188
+ namespace: NAMESPACE,
189
+ leaseMs: 60_000,
190
+ now: () => AFTER_EXPIRY,
191
+ });
192
+
193
+ describe('share-link recovery runner (real Neo4j, LABELED fake LearnCloud)', () => {
194
+ beforeAll(async () => {
195
+ await ensureShareLinkConstraints();
196
+ });
197
+
198
+ beforeEach(async () => {
199
+ await clearLifecycleGraph();
200
+ });
201
+
202
+ afterAll(async () => {
203
+ await clearLifecycleGraph();
204
+ });
205
+
206
+ it('finalizes a put-before-finalize reservation via bounded discovery + claim + stat', async () => {
207
+ const created = await reserveNewShare({ leaseMs: 1_000 });
208
+ const stat = fakeStat({
209
+ shareId: created.shareId,
210
+ objectId: created.objectRef,
211
+ operationId: created.operationId,
212
+ payloadHash: contentBinding(created.shareId).contentHash,
213
+ });
214
+
215
+ const summary = await runShareLinkRecoveryOnce(
216
+ recoveryDependencies(stat as unknown as ShareContentClient['stat'])
217
+ );
218
+
219
+ expect(summary).toMatchObject({
220
+ discovered: 1,
221
+ claimed: 1,
222
+ finalized: 1,
223
+ categories: { finalized: 1 },
224
+ });
225
+
226
+ const share = await getShareLink({ shareId: created.shareId });
227
+ expect(share?.contentState).toBe('finalized');
228
+ expect(share?.activeObjectRef).toBe(created.objectRef);
229
+ expect(share?.activeObjectOperationId).toBe(created.operationId);
230
+ });
231
+
232
+ it('abandons a missing object and queues exact-object cleanup', async () => {
233
+ const created = await reserveNewShare({ leaseMs: 1_000 });
234
+ const stat = vi.fn(async () => ({ ok: false as const, error: 'NOT_FOUND' as const }));
235
+
236
+ const summary = await runShareLinkRecoveryOnce(
237
+ recoveryDependencies(stat as unknown as ShareContentClient['stat'])
238
+ );
239
+
240
+ expect(summary).toMatchObject({
241
+ discovered: 1,
242
+ claimed: 1,
243
+ abandoned: 1,
244
+ categories: { abandoned_missing: 1 },
245
+ });
246
+
247
+ const jobs = await listCleanupJobsForShare(created.shareId);
248
+ expect(jobs.map(job => job.objectRef)).toContain(created.objectRef);
249
+ });
250
+
251
+ it('abandons a permanent tombstone under the live recovery fence', async () => {
252
+ const created = await reserveNewShare({ leaseMs: 1_000 });
253
+ const stat = vi.fn(async () => ({
254
+ ok: true as const,
255
+ value: {
256
+ kind: 'tombstone' as const,
257
+ namespace: NAMESPACE,
258
+ ownerProfileId: OWNER,
259
+ shareId: created.shareId,
260
+ contentVersion: 1,
261
+ objectId: created.objectRef,
262
+ operationId: created.operationId,
263
+ deletedAt: '2026-09-20T12:00:00.000Z',
264
+ },
265
+ }));
266
+
267
+ const summary = await runShareLinkRecoveryOnce(
268
+ recoveryDependencies(stat as unknown as ShareContentClient['stat'])
269
+ );
270
+
271
+ expect(summary).toMatchObject({ abandoned: 1, categories: { abandoned_tombstone: 1 } });
272
+ });
273
+
274
+ it('observes committed state after finalize response loss without any storage I/O', async () => {
275
+ const committed = await commitNewShare();
276
+ const stat = vi.fn();
277
+
278
+ const result = await recoverShareLinkOperation(
279
+ recoveryDependencies(stat as unknown as ShareContentClient['stat']),
280
+ keyFor(committed)
281
+ );
282
+
283
+ expect(result).toEqual({
284
+ status: 'committed',
285
+ share: expect.objectContaining({ activeObjectRef: committed.objectRef }),
286
+ });
287
+ expect(stat).not.toHaveBeenCalled();
288
+ });
289
+
290
+ it('recovers a scoped live reservation to a committed share', async () => {
291
+ const created = await reserveNewShare({ leaseMs: 1_000 });
292
+ const stat = fakeStat({
293
+ shareId: created.shareId,
294
+ objectId: created.objectRef,
295
+ operationId: created.operationId,
296
+ payloadHash: contentBinding(created.shareId).contentHash,
297
+ });
298
+
299
+ const result = await recoverShareLinkOperation(
300
+ recoveryDependencies(stat as unknown as ShareContentClient['stat']),
301
+ keyFor(created)
302
+ );
303
+
304
+ expect(result.status).toBe('committed');
305
+ expect(stat).toHaveBeenCalledTimes(1);
306
+ });
307
+ });
308
+
309
+ describe('share-link durable reservation recovery (Neo4j)', () => {
310
+ beforeAll(async () => {
311
+ await ensureShareLinkConstraints();
312
+ });
313
+
314
+ beforeEach(async () => {
315
+ await clearLifecycleGraph();
316
+ });
317
+
318
+ afterAll(async () => {
319
+ await clearLifecycleGraph();
320
+ });
321
+
322
+ it.each(['generation', 'version', 'status'])(
323
+ 'does not normalize a malformed share %s into recovery eligibility',
324
+ async field => {
325
+ const created = await reserveNewShare({ leaseMs: 1_000 });
326
+ await neogma.queryRunner.run('MATCH (s:ShareLink {id: $id}) SET s += $props', {
327
+ id: created.shareId,
328
+ props: { [field]: 'corrupt' },
329
+ });
330
+ const result = await claimRecoverableReservation({
331
+ ...keyFor(created),
332
+ leaseOwner: 'review',
333
+ now: AFTER_EXPIRY,
334
+ });
335
+ expect(result).toMatchObject({ outcome: 'not_claimable', reason: 'malformed_binding' });
336
+ expect(await listCleanupJobsForShare(created.shareId)).toEqual([]);
337
+ }
338
+ );
339
+
340
+ it('rejects a reservation whose request hash differs from its operation', async () => {
341
+ const created = await reserveNewShare({ leaseMs: 1_000 });
342
+ await neogma.queryRunner.run(
343
+ 'MATCH (r:ShareLinkReservation {shareId: $id}) SET r.requestHash = $hash',
344
+ {
345
+ id: created.shareId,
346
+ hash: 'a'.repeat(64),
347
+ }
348
+ );
349
+ expect(
350
+ await claimRecoverableReservation({
351
+ ...keyFor(created),
352
+ leaseOwner: 'review',
353
+ now: AFTER_EXPIRY,
354
+ })
355
+ ).toMatchObject({ outcome: 'not_claimable', reason: 'binding_mismatch' });
356
+ });
357
+
358
+ it('refuses recovered abandonment after the share generation changes independently', async () => {
359
+ const created = await reserveNewShare({ leaseMs: 1_000 });
360
+ const claim = await claimRecoverableReservation({
361
+ ...keyFor(created),
362
+ leaseOwner: 'review',
363
+ now: AFTER_EXPIRY,
364
+ });
365
+ expect(claim.outcome).toBe('claimed');
366
+ if (claim.outcome !== 'claimed') throw new Error('claim failed');
367
+ await neogma.queryRunner.run(
368
+ 'MATCH (s:ShareLink {id: $id}) SET s.generation = s.generation + 1',
369
+ { id: created.shareId }
370
+ );
371
+ expect(
372
+ await abandonRecoveredReservation({
373
+ ...keyFor(created),
374
+ leaseOwner: 'review',
375
+ generation: claim.reservation.generation,
376
+ now: AFTER_EXPIRY,
377
+ })
378
+ ).toMatchObject({ outcome: 'not_claimable', reason: 'stale_generation' });
379
+ expect(await listCleanupJobsForShare(created.shareId)).toEqual([]);
380
+ });
381
+
382
+ it.each([
383
+ { selectedCount: 0 },
384
+ { title: null },
385
+ {
386
+ objectRef: null,
387
+ contentVersion: null,
388
+ contentHash: null,
389
+ contentBytes: null,
390
+ recoveryHash: null,
391
+ recoveryBytes: null,
392
+ },
393
+ ])('rejects malformed persisted creation intent %j', async props => {
394
+ const created = await reserveNewShare({ leaseMs: 1_000 });
395
+ await neogma.queryRunner.run(
396
+ 'MATCH (r:ShareLinkReservation {shareId: $id}) SET r += $props',
397
+ { id: created.shareId, props }
398
+ );
399
+ expect(
400
+ await claimRecoverableReservation({
401
+ ...keyFor(created),
402
+ leaseOwner: 'review',
403
+ now: AFTER_EXPIRY,
404
+ })
405
+ ).toMatchObject({ outcome: 'not_claimable', reason: 'malformed_binding' });
406
+ });
407
+
408
+ it('discovers and claims only an expired in-flight reservation, preserving the tuple', async () => {
409
+ const created = await reserveNewShare({ leaseMs: 1_000 });
410
+ const key = keyFor(created);
411
+
412
+ const target = await readShareLinkRecoveryTarget(key);
413
+ expect(target.state).toBe('reservation');
414
+ if (target.state !== 'reservation') return;
415
+ expect(target.reservation.operationId).toBe(created.operationId);
416
+ expect(target.reservation.objectRef).toBe(created.objectRef);
417
+
418
+ const notYet = await discoverRecoverableReservations({
419
+ namespace: NAMESPACE,
420
+ now: BASE,
421
+ });
422
+ expect(notYet.keys).toHaveLength(0);
423
+
424
+ const discovery = await discoverRecoverableReservations({
425
+ namespace: NAMESPACE,
426
+ now: AFTER_EXPIRY,
427
+ });
428
+ expect(discovery.keys).toEqual([key]);
429
+
430
+ const claimed = await claimRecoverableReservation({
431
+ ...key,
432
+ leaseOwner: 'recovery-1',
433
+ leaseMs: 60_000,
434
+ now: AFTER_EXPIRY,
435
+ });
436
+
437
+ expect(claimed.outcome).toBe('claimed');
438
+ if (claimed.outcome !== 'claimed') return;
439
+
440
+ expect(claimed.reservation.generation).toBe(created.generation + 1);
441
+ expect(claimed.share.generation).toBe(created.generation + 1);
442
+ expect(claimed.reservation.operationId).toBe(created.operationId);
443
+ expect(claimed.reservation.objectRef).toBe(created.objectRef);
444
+ expect(claimed.reservation.contentHash).toBe(contentBinding(created.shareId).contentHash);
445
+ expect(claimed.reservation.requestHash).toBe(created.requestHash);
446
+ expect(claimed.reservation.leaseOwner).toBe('recovery-1');
447
+ expect(claimed.reservation.leaseExpiresAt).toBe(computeLeaseExpiry(AFTER_EXPIRY, 60_000));
448
+
449
+ const finalized = await finalizeReservation({
450
+ namespace: claimed.reservation.namespace,
451
+ ownerProfileId: claimed.reservation.ownerProfileId,
452
+ shareId: claimed.reservation.shareId,
453
+ operationId: claimed.reservation.operationId,
454
+ objectRef: claimed.reservation.objectRef,
455
+ generation: claimed.reservation.generation,
456
+ leaseOwner: claimed.reservation.leaseOwner,
457
+ now: AFTER_EXPIRY,
458
+ });
459
+
460
+ expect(finalized.outcome).toBe('finalized');
461
+ if (finalized.outcome !== 'finalized') return;
462
+ expect(finalized.share.activeObjectRef).toBe(created.objectRef);
463
+ expect(finalized.share.activeObjectOperationId).toBe(created.operationId);
464
+ });
465
+
466
+ it('exposes committed current state for a response-loss retry without a live reservation', async () => {
467
+ const committed = await commitNewShare();
468
+
469
+ const target = await readShareLinkRecoveryTarget(keyFor(committed));
470
+ expect(target.state).toBe('committed');
471
+ if (target.state !== 'committed') return;
472
+ expect(target.share.version).toBe(committed.committed.version);
473
+ expect(target.share.activeObjectRef).toBe(committed.objectRef);
474
+
475
+ const discovery = await discoverRecoverableReservations({
476
+ namespace: NAMESPACE,
477
+ now: AFTER_EXPIRY,
478
+ });
479
+ expect(discovery.keys).toHaveLength(0);
480
+ });
481
+
482
+ it('requires an explicit namespace and bounds the discovery batch', async () => {
483
+ const first = await reserveNewShare({ leaseMs: 1_000 });
484
+ const second = await reserveNewShare({ leaseMs: 1_000, namespace: OTHER_NAMESPACE });
485
+ const third = await reserveNewShare({ leaseMs: 1_000 });
486
+
487
+ const scoped = await discoverRecoverableReservations({
488
+ namespace: NAMESPACE,
489
+ now: AFTER_EXPIRY,
490
+ });
491
+ expect(scoped.keys).toHaveLength(2);
492
+ expect(scoped.keys.every(key => key.namespace === NAMESPACE)).toBe(true);
493
+
494
+ const limited = await discoverRecoverableReservations({
495
+ namespace: NAMESPACE,
496
+ limit: 1,
497
+ now: AFTER_EXPIRY,
498
+ });
499
+ expect(limited.keys).toHaveLength(1);
500
+
501
+ const other = await discoverRecoverableReservations({
502
+ namespace: OTHER_NAMESPACE,
503
+ now: AFTER_EXPIRY,
504
+ });
505
+ expect(other.keys).toHaveLength(1);
506
+ expect(other.keys[0]?.shareId).toBe(second.shareId);
507
+
508
+ // The active first/third leases are not selected at BASE.
509
+ const active = await discoverRecoverableReservations({
510
+ namespace: NAMESPACE,
511
+ now: BASE,
512
+ });
513
+ expect(active.keys).toHaveLength(0);
514
+ expect(first.shareId).not.toBe(second.shareId);
515
+ expect(third.shareId).not.toBe(first.shareId);
516
+ });
517
+
518
+ it('lets exactly one of two concurrent claims win', async () => {
519
+ const created = await reserveNewShare({ leaseMs: 1_000 });
520
+ const key = keyFor(created);
521
+
522
+ const [first, second] = await Promise.all([
523
+ claimRecoverableReservation({
524
+ ...key,
525
+ leaseOwner: 'recovery-1',
526
+ leaseMs: 60_000,
527
+ now: AFTER_EXPIRY,
528
+ }),
529
+ claimRecoverableReservation({
530
+ ...key,
531
+ leaseOwner: 'recovery-2',
532
+ leaseMs: 60_000,
533
+ now: AFTER_EXPIRY,
534
+ }),
535
+ ]);
536
+
537
+ const outcomes = [first.outcome, second.outcome].sort();
538
+ expect(outcomes).toEqual(['claimed', 'not_claimable']);
539
+
540
+ const share = await getShareLink({ shareId: created.shareId });
541
+ expect(share?.generation).toBe(created.generation + 1);
542
+
543
+ const reservation = await readReservationProps(created.shareId);
544
+ expect(reservation?.generation).toBe(created.generation + 1);
545
+ });
546
+
547
+ it('reclaims after lease expiry and fences the superseded generation', async () => {
548
+ const created = await reserveNewShare({ leaseMs: 1_000 });
549
+ const key = keyFor(created);
550
+
551
+ const first = await claimRecoverableReservation({
552
+ ...key,
553
+ leaseOwner: 'recovery-1',
554
+ leaseMs: 1_000,
555
+ now: AFTER_EXPIRY,
556
+ });
557
+ expect(first.outcome).toBe('claimed');
558
+ if (first.outcome !== 'claimed') return;
559
+
560
+ const reclaimAt = new Date(AFTER_EXPIRY.getTime() + 5_000);
561
+ const second = await claimRecoverableReservation({
562
+ ...key,
563
+ leaseOwner: 'recovery-2',
564
+ leaseMs: 60_000,
565
+ now: reclaimAt,
566
+ });
567
+ expect(second.outcome).toBe('claimed');
568
+ if (second.outcome !== 'claimed') return;
569
+ expect(second.reservation.generation).toBe(first.reservation.generation + 1);
570
+
571
+ await expect(
572
+ finalizeReservation({
573
+ namespace: first.reservation.namespace,
574
+ ownerProfileId: first.reservation.ownerProfileId,
575
+ shareId: first.reservation.shareId,
576
+ operationId: first.reservation.operationId,
577
+ objectRef: first.reservation.objectRef,
578
+ generation: first.reservation.generation,
579
+ leaseOwner: first.reservation.leaseOwner,
580
+ now: reclaimAt,
581
+ })
582
+ ).rejects.toMatchObject({ code: 'CONFLICT' });
583
+
584
+ const finalized = await finalizeReservation({
585
+ namespace: second.reservation.namespace,
586
+ ownerProfileId: second.reservation.ownerProfileId,
587
+ shareId: second.reservation.shareId,
588
+ operationId: second.reservation.operationId,
589
+ objectRef: second.reservation.objectRef,
590
+ generation: second.reservation.generation,
591
+ leaseOwner: second.reservation.leaseOwner,
592
+ now: reclaimAt,
593
+ });
594
+ expect(finalized.outcome).toBe('finalized');
595
+ });
596
+
597
+ it('fences a reused claimant string with a stale generation', async () => {
598
+ const created = await reserveNewShare({ leaseMs: 1_000 });
599
+ const key = keyFor(created);
600
+
601
+ const first = await claimRecoverableReservation({
602
+ ...key,
603
+ leaseOwner: 'reused-claimant',
604
+ leaseMs: 1_000,
605
+ now: AFTER_EXPIRY,
606
+ });
607
+ expect(first.outcome).toBe('claimed');
608
+ if (first.outcome !== 'claimed') return;
609
+
610
+ const reclaimAt = new Date(AFTER_EXPIRY.getTime() + 5_000);
611
+ const second = await claimRecoverableReservation({
612
+ ...key,
613
+ leaseOwner: 'reused-claimant',
614
+ leaseMs: 60_000,
615
+ now: reclaimAt,
616
+ });
617
+ expect(second.outcome).toBe('claimed');
618
+ if (second.outcome !== 'claimed') return;
619
+
620
+ const staleAbandon = await abandonRecoveredReservation({
621
+ ...key,
622
+ generation: first.reservation.generation,
623
+ leaseOwner: 'reused-claimant',
624
+ now: reclaimAt,
625
+ });
626
+ expect(staleAbandon.outcome).toBe('not_claimable');
627
+ if (staleAbandon.outcome === 'not_claimable') {
628
+ expect(staleAbandon.reason).toBe('stale_generation');
629
+ }
630
+
631
+ await expect(
632
+ finalizeReservation({
633
+ namespace: first.reservation.namespace,
634
+ ownerProfileId: first.reservation.ownerProfileId,
635
+ shareId: first.reservation.shareId,
636
+ operationId: first.reservation.operationId,
637
+ objectRef: first.reservation.objectRef,
638
+ generation: first.reservation.generation,
639
+ leaseOwner: first.reservation.leaseOwner,
640
+ now: reclaimAt,
641
+ })
642
+ ).rejects.toMatchObject({ code: 'CONFLICT' });
643
+
644
+ // The reclaimed reservation is still owned by the live generation.
645
+ const stillOwned = await readReservationProps(created.shareId);
646
+ expect(stillOwned?.generation).toBe(second.reservation.generation);
647
+
648
+ const freshAbandon = await abandonRecoveredReservation({
649
+ ...key,
650
+ generation: second.reservation.generation,
651
+ leaseOwner: 'reused-claimant',
652
+ now: reclaimAt,
653
+ });
654
+ expect(freshAbandon.outcome).toBe('abandoned');
655
+ });
656
+
657
+ it('blocks recovery-worker abandonment once its lease has lapsed', async () => {
658
+ const created = await reserveNewShare({ leaseMs: 1_000 });
659
+ const key = keyFor(created);
660
+
661
+ const claimed = await claimRecoverableReservation({
662
+ ...key,
663
+ leaseOwner: 'recovery-1',
664
+ leaseMs: 1_000,
665
+ now: AFTER_EXPIRY,
666
+ });
667
+ expect(claimed.outcome).toBe('claimed');
668
+ if (claimed.outcome !== 'claimed') return;
669
+
670
+ const lapsedAt = new Date(AFTER_EXPIRY.getTime() + 5_000);
671
+ const lapsed = await abandonRecoveredReservation({
672
+ ...key,
673
+ generation: claimed.reservation.generation,
674
+ leaseOwner: 'recovery-1',
675
+ now: lapsedAt,
676
+ });
677
+
678
+ expect(lapsed.outcome).toBe('not_claimable');
679
+ if (lapsed.outcome === 'not_claimable') {
680
+ expect(lapsed.reason).toBe('lease_lapsed');
681
+ }
682
+
683
+ // The reservation was not destroyed by the lapsed worker.
684
+ const stillPresent = await readReservationProps(created.shareId);
685
+ expect(stillPresent?.leaseOwner).toBe('recovery-1');
686
+ expect(await listCleanupJobsForShare(created.shareId)).toHaveLength(0);
687
+
688
+ // A fresh claim renews the fence, after which a fenced abandon works.
689
+ const reclaimed = await claimRecoverableReservation({
690
+ ...key,
691
+ leaseOwner: 'recovery-2',
692
+ leaseMs: 60_000,
693
+ now: lapsedAt,
694
+ });
695
+ expect(reclaimed.outcome).toBe('claimed');
696
+ if (reclaimed.outcome !== 'claimed') return;
697
+
698
+ const abandoned = await abandonRecoveredReservation({
699
+ ...key,
700
+ generation: reclaimed.reservation.generation,
701
+ leaseOwner: 'recovery-2',
702
+ now: lapsedAt,
703
+ });
704
+ expect(abandoned.outcome).toBe('abandoned');
705
+ expect(await listCleanupJobsForShare(created.shareId)).toHaveLength(1);
706
+ });
707
+
708
+ it('returns absent for a wrong namespace, owner or operation with no state change', async () => {
709
+ const created = await reserveNewShare({ leaseMs: 1_000 });
710
+ const key = keyFor(created);
711
+ const before = await readReservationProps(created.shareId);
712
+
713
+ expect(
714
+ (await readShareLinkRecoveryTarget({ ...key, namespace: OTHER_NAMESPACE })).state
715
+ ).toBe('absent');
716
+ expect(
717
+ (await readShareLinkRecoveryTarget({ ...key, ownerProfileId: 'other-owner' })).state
718
+ ).toBe('absent');
719
+ expect((await readShareLinkRecoveryTarget({ ...key, operationId: uuid() })).state).toBe(
720
+ 'absent'
721
+ );
722
+
723
+ expect(
724
+ await claimRecoverableReservation({
725
+ ...key,
726
+ namespace: OTHER_NAMESPACE,
727
+ leaseOwner: 'recovery-1',
728
+ now: AFTER_EXPIRY,
729
+ })
730
+ ).toEqual({ outcome: 'not_claimable', reason: 'absent' });
731
+ expect(
732
+ await claimRecoverableReservation({
733
+ ...key,
734
+ ownerProfileId: 'other-owner',
735
+ leaseOwner: 'recovery-1',
736
+ now: AFTER_EXPIRY,
737
+ })
738
+ ).toEqual({ outcome: 'not_claimable', reason: 'absent' });
739
+ expect(
740
+ await claimRecoverableReservation({
741
+ ...key,
742
+ operationId: uuid(),
743
+ leaseOwner: 'recovery-1',
744
+ now: AFTER_EXPIRY,
745
+ })
746
+ ).toEqual({ outcome: 'not_claimable', reason: 'binding_mismatch' });
747
+
748
+ expect(await readReservationProps(created.shareId)).toEqual(before);
749
+ });
750
+
751
+ it('fails closed on a malformed persisted reservation binding', async () => {
752
+ const created = await reserveNewShare({ leaseMs: 1_000 });
753
+ const key = keyFor(created);
754
+
755
+ await neogma.queryRunner.run(
756
+ 'MATCH (r:ShareLinkReservation {shareId: $shareId}) REMOVE r.generation',
757
+ { shareId: created.shareId }
758
+ );
759
+
760
+ const discovery = await discoverRecoverableReservations({
761
+ namespace: NAMESPACE,
762
+ now: AFTER_EXPIRY,
763
+ });
764
+ expect(discovery.keys).toHaveLength(0);
765
+
766
+ expect(
767
+ await claimRecoverableReservation({
768
+ ...key,
769
+ leaseOwner: 'recovery-1',
770
+ leaseMs: 60_000,
771
+ now: AFTER_EXPIRY,
772
+ })
773
+ ).toEqual({ outcome: 'not_claimable', reason: 'malformed_binding' });
774
+
775
+ expect(await readReservationProps(created.shareId)).not.toHaveProperty('generation');
776
+ expect(await listCleanupJobsForShare(created.shareId)).toHaveLength(0);
777
+ expect((await getShareLink({ shareId: created.shareId }))?.generation).toBe(
778
+ created.generation
779
+ );
780
+ });
781
+
782
+ it('recovers a metadata-only restart without changing the visible object tuple', async () => {
783
+ const committed = await commitNewShare();
784
+ const replacement = await reserveReplacement({
785
+ namespace: committed.namespace,
786
+ ownerProfileId: committed.ownerProfileId,
787
+ clientRequestId: uuid(),
788
+ shareId: committed.shareId,
789
+ expectedVersion: committed.committed.version,
790
+ requestHash: computeShareLinkRequestHash('update', {
791
+ id: committed.shareId,
792
+ title: 'Renamed share',
793
+ }),
794
+ leaseOwner: 'worker-2',
795
+ leaseMs: 1_000,
796
+ now: BASE,
797
+ title: 'Renamed share',
798
+ });
799
+ if (replacement.outcome !== 'reserved') throw new Error('expected reserved update');
800
+
801
+ const key: ShareLinkRecoveryKey = {
802
+ namespace: replacement.reservation.namespace,
803
+ ownerProfileId: replacement.reservation.ownerProfileId,
804
+ shareId: replacement.reservation.shareId,
805
+ operationId: replacement.reservation.operationId,
806
+ };
807
+
808
+ const claimed = await claimRecoverableReservation({
809
+ ...key,
810
+ leaseOwner: 'recovery-1',
811
+ leaseMs: 60_000,
812
+ now: AFTER_EXPIRY,
813
+ });
814
+ expect(claimed.outcome).toBe('claimed');
815
+ if (claimed.outcome !== 'claimed') return;
816
+
817
+ expect(claimed.reservation.objectRef).toBeNull();
818
+ expect(claimed.reservation.contentVersion).toBeNull();
819
+
820
+ const finalized = await finalizeReservation({
821
+ namespace: claimed.reservation.namespace,
822
+ ownerProfileId: claimed.reservation.ownerProfileId,
823
+ shareId: claimed.reservation.shareId,
824
+ operationId: claimed.reservation.operationId,
825
+ objectRef: null,
826
+ generation: claimed.reservation.generation,
827
+ leaseOwner: claimed.reservation.leaseOwner,
828
+ now: AFTER_EXPIRY,
829
+ });
830
+
831
+ expect(finalized.outcome).toBe('finalized');
832
+ if (finalized.outcome !== 'finalized') return;
833
+ expect(finalized.share.title).toBe('Renamed share');
834
+ expect(finalized.share.activeObjectRef).toBe(committed.objectRef);
835
+ expect(finalized.share.activeObjectOperationId).toBe(committed.operationId);
836
+ expect(finalized.share.contentVersion).toBe(1);
837
+ });
838
+
839
+ it('does not resurrect work after a concurrent revoke', async () => {
840
+ const committed = await commitNewShare();
841
+ const replacement = await reserveReplacementContent({
842
+ created: committed,
843
+ seed: 'revoked-replacement',
844
+ contentVersion: 2,
845
+ leaseMs: 1_000,
846
+ });
847
+
848
+ await revokeShareLink({
849
+ namespace: committed.namespace,
850
+ ownerProfileId: committed.ownerProfileId,
851
+ shareId: committed.shareId,
852
+ now: AFTER_EXPIRY,
853
+ });
854
+
855
+ const replacementKey: ShareLinkRecoveryKey = {
856
+ namespace: committed.namespace,
857
+ ownerProfileId: committed.ownerProfileId,
858
+ shareId: committed.shareId,
859
+ operationId: replacement.reservation.operationId,
860
+ };
861
+
862
+ const discovery = await discoverRecoverableReservations({
863
+ namespace: NAMESPACE,
864
+ now: AFTER_EXPIRY,
865
+ });
866
+ expect(discovery.keys).toHaveLength(0);
867
+ expect((await readShareLinkRecoveryTarget(replacementKey)).state).toBe('absent');
868
+
869
+ const claim = await claimRecoverableReservation({
870
+ ...replacementKey,
871
+ leaseOwner: 'recovery-1',
872
+ leaseMs: 60_000,
873
+ now: AFTER_EXPIRY,
874
+ });
875
+ expect(claim.outcome).toBe('not_claimable');
876
+ if (claim.outcome === 'not_claimable') {
877
+ expect(claim.reason).toBe('absent');
878
+ }
879
+
880
+ const share = await getShareLink({ shareId: committed.shareId });
881
+ expect(share?.status).toBe('stopped');
882
+ expect(share?.activeObjectRef).toBe(committed.objectRef);
883
+
884
+ const jobs = await listCleanupJobsForShare(committed.shareId);
885
+ const refs = jobs.map(job => job.objectRef);
886
+ expect(refs).toContain(committed.objectRef);
887
+ expect(refs).toContain(replacement.reservation.objectRef);
888
+ });
889
+
890
+ it('allocates a fresh object when the abandoned request is driven again', async () => {
891
+ const created = await reserveNewShare({ leaseMs: 1_000 });
892
+ const key = keyFor(created);
893
+
894
+ const claimed = await claimRecoverableReservation({
895
+ ...key,
896
+ leaseOwner: 'recovery-1',
897
+ leaseMs: 60_000,
898
+ now: AFTER_EXPIRY,
899
+ });
900
+ expect(claimed.outcome).toBe('claimed');
901
+ if (claimed.outcome !== 'claimed') return;
902
+
903
+ const abandoned = await abandonRecoveredReservation({
904
+ ...key,
905
+ generation: claimed.reservation.generation,
906
+ leaseOwner: 'recovery-1',
907
+ now: AFTER_EXPIRY,
908
+ });
909
+ expect(abandoned.outcome).toBe('abandoned');
910
+
911
+ const replayed = await reserveCreate({
912
+ namespace: created.namespace,
913
+ ownerProfileId: created.ownerProfileId,
914
+ clientRequestId: created.clientRequestId,
915
+ shareId: created.shareId,
916
+ title: 'Shared credentials',
917
+ note: null,
918
+ expiresAt: null,
919
+ selectedCount: 1,
920
+ content: contentBinding(created.shareId),
921
+ requestHash: created.requestHash,
922
+ leaseOwner: 'worker-1',
923
+ leaseMs: 60_000,
924
+ now: new Date(AFTER_EXPIRY.getTime() + 1_000),
925
+ });
926
+
927
+ expect(replayed.outcome).toBe('reserved');
928
+ if (replayed.outcome !== 'reserved' || !replayed.reservation.objectRef) return;
929
+ expect(replayed.reservation.objectRef).not.toBe(created.objectRef);
930
+ expect(replayed.reservation.operationId).not.toBe(created.operationId);
931
+
932
+ const jobs = await listCleanupJobsForShare(created.shareId);
933
+ expect(jobs.map(job => job.objectRef)).toContain(created.objectRef);
934
+ });
935
+ });