@learncard/network-brain-service 3.19.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 (301) hide show
  1. package/.env.example +34 -2
  2. package/CHANGELOG.md +38 -0
  3. package/README.md +42 -0
  4. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  5. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  6. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  7. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  8. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  9. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  10. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  11. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  12. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  13. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  14. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  15. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  16. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  17. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  18. package/dist/accesslayer/share-link/helpers.d.ts +56 -0
  19. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  20. package/dist/accesslayer/share-link/index.d.ts +12 -0
  21. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  22. package/dist/accesslayer/share-link/list.d.ts +5 -0
  23. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  24. package/dist/accesslayer/share-link/read.d.ts +5 -0
  25. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  26. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  27. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  28. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  29. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  30. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  31. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  32. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  33. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  34. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  35. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  36. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  37. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  38. package/dist/accesslayer/share-link/types.d.ts +370 -0
  39. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  40. package/dist/app.d.ts +4 -0
  41. package/dist/app.d.ts.map +1 -1
  42. package/dist/config/environment.d.ts +9 -0
  43. package/dist/config/environment.d.ts.map +1 -1
  44. package/dist/constants/auth-grant.d.ts +2 -0
  45. package/dist/constants/auth-grant.d.ts.map +1 -1
  46. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  47. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  48. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  49. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  50. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  51. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  52. package/dist/helpers/notificationMessages.d.ts +1 -1
  53. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  54. package/dist/helpers/presentation-metadata.d.ts +4 -0
  55. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  56. package/dist/helpers/presentation.helpers.d.ts +1 -1
  57. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  58. package/dist/helpers/profile.helpers.d.ts +1 -0
  59. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  60. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  61. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  62. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  63. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  64. package/dist/helpers/share-content-client/client.d.ts +5 -0
  65. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  66. package/dist/helpers/share-content-client/config.d.ts +11 -0
  67. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  68. package/dist/helpers/share-content-client/index.d.ts +6 -0
  69. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  70. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  71. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  72. package/dist/helpers/share-content-client/types.d.ts +145 -0
  73. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  74. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  75. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  76. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  77. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  78. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  79. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  80. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  81. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  82. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  83. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  84. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  85. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  86. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  87. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  88. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  89. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  90. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  91. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  92. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  93. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  94. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  95. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  96. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  97. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  98. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  99. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  100. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  101. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  102. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  103. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  104. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  105. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  106. package/dist/helpers/share-link-maintenance/runner.d.ts +19 -0
  107. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  108. package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -0
  109. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  110. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  111. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  112. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  113. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  114. package/dist/helpers/share-link-maintenance/types.d.ts +53 -0
  115. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  116. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  117. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  118. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  119. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  120. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  121. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  122. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  123. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  124. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  125. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  126. package/dist/helpers/share-link-passcode.d.ts +6 -0
  127. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  128. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  129. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  130. package/dist/helpers/share-link-policy/production.d.ts +13 -0
  131. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  132. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  133. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  134. package/dist/helpers/share-link-policy/types.d.ts +16 -0
  135. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  136. package/dist/helpers/share-link-public/config.d.ts +13 -0
  137. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  138. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  139. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  140. package/dist/index.js +2 -2
  141. package/dist/models/Presentation.d.ts +2 -0
  142. package/dist/models/Presentation.d.ts.map +1 -1
  143. package/dist/models/Profile.d.ts +2 -0
  144. package/dist/models/Profile.d.ts.map +1 -1
  145. package/dist/models/ShareLink.d.ts +44 -0
  146. package/dist/models/ShareLink.d.ts.map +1 -0
  147. package/dist/models/ShareViewReceipt.d.ts +22 -0
  148. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  149. package/dist/models/index.d.ts +1 -0
  150. package/dist/models/index.d.ts.map +1 -1
  151. package/dist/models/share-link-constraints.d.ts +3 -0
  152. package/dist/models/share-link-constraints.d.ts.map +1 -0
  153. package/dist/public-share-link-http.d.ts +4 -0
  154. package/dist/public-share-link-http.d.ts.map +1 -0
  155. package/dist/routes/app-store.d.ts.map +1 -1
  156. package/dist/routes/boosts.d.ts.map +1 -1
  157. package/dist/routes/contracts.d.ts +2 -2
  158. package/dist/routes/contracts.d.ts.map +1 -1
  159. package/dist/routes/credentials.d.ts +1 -1
  160. package/dist/routes/index.d.ts +153 -0
  161. package/dist/routes/index.d.ts.map +1 -1
  162. package/dist/routes/presentations.d.ts +1 -0
  163. package/dist/routes/presentations.d.ts.map +1 -1
  164. package/dist/routes/profile-manager.d.ts.map +1 -1
  165. package/dist/routes/profiles.d.ts.map +1 -1
  166. package/dist/routes/public-share-links.d.ts +230 -0
  167. package/dist/routes/public-share-links.d.ts.map +1 -0
  168. package/dist/routes/share-links.d.ts +844 -0
  169. package/dist/routes/share-links.d.ts.map +1 -0
  170. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  171. package/dist/types/consentflowcontract.d.ts +1 -1
  172. package/lambda.ts +4 -1
  173. package/package.json +22 -16
  174. package/scripts/seed-demo-persona.ts +425 -0
  175. package/serverless.yml +36 -0
  176. package/shareLinkMaintenanceLambda.ts +49 -0
  177. package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
  178. package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
  179. package/src/accesslayer/presentation/read.ts +57 -25
  180. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  181. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  182. package/src/accesslayer/share-link/abandon.ts +100 -0
  183. package/src/accesslayer/share-link/cleanup.ts +271 -0
  184. package/src/accesslayer/share-link/errors.ts +32 -0
  185. package/src/accesslayer/share-link/finalize.ts +265 -0
  186. package/src/accesslayer/share-link/helpers.ts +419 -0
  187. package/src/accesslayer/share-link/index.ts +11 -0
  188. package/src/accesslayer/share-link/list.ts +162 -0
  189. package/src/accesslayer/share-link/read.ts +107 -0
  190. package/src/accesslayer/share-link/receipt.ts +548 -0
  191. package/src/accesslayer/share-link/recovery.ts +545 -0
  192. package/src/accesslayer/share-link/reserve.ts +684 -0
  193. package/src/accesslayer/share-link/revoke.ts +197 -0
  194. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  195. package/src/accesslayer/share-link/transaction.ts +126 -0
  196. package/src/accesslayer/share-link/types.ts +601 -0
  197. package/src/app.ts +6 -0
  198. package/src/config/environment.ts +14 -0
  199. package/src/constants/auth-grant.ts +5 -0
  200. package/src/docker-entry.ts +57 -0
  201. package/src/helpers/boost.helpers.ts +4 -31
  202. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  203. package/src/helpers/credentialSubject.helpers.ts +40 -0
  204. package/src/helpers/inbox.helpers.ts +7 -4
  205. package/src/helpers/notificationMessages.ts +18 -1
  206. package/src/helpers/presentation-metadata.ts +41 -0
  207. package/src/helpers/presentation.helpers.ts +27 -20
  208. package/src/helpers/profile.helpers.ts +7 -0
  209. package/src/helpers/share-content-client/adapters.ts +55 -0
  210. package/src/helpers/share-content-client/canonical.ts +25 -0
  211. package/src/helpers/share-content-client/client.ts +440 -0
  212. package/src/helpers/share-content-client/config.ts +189 -0
  213. package/src/helpers/share-content-client/index.ts +27 -0
  214. package/src/helpers/share-content-client/responses.ts +273 -0
  215. package/src/helpers/share-content-client/types.ts +253 -0
  216. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  217. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  218. package/src/helpers/share-link-coordinator/coordinator.ts +714 -0
  219. package/src/helpers/share-link-coordinator/index.ts +8 -0
  220. package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -0
  221. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  222. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  223. package/src/helpers/share-link-coordinator/types.ts +353 -0
  224. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  225. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  226. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  227. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  228. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  229. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  230. package/src/helpers/share-link-maintenance/config.ts +312 -0
  231. package/src/helpers/share-link-maintenance/index.ts +24 -0
  232. package/src/helpers/share-link-maintenance/runner.ts +223 -0
  233. package/src/helpers/share-link-maintenance/runtime.ts +334 -0
  234. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  235. package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
  236. package/src/helpers/share-link-maintenance/types.ts +89 -0
  237. package/src/helpers/share-link-owner/config.ts +48 -0
  238. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  239. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  240. package/src/helpers/share-link-owner-projection.ts +52 -0
  241. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  242. package/src/helpers/share-link-passcode.ts +47 -0
  243. package/src/helpers/share-link-policy/index.ts +7 -0
  244. package/src/helpers/share-link-policy/production.ts +88 -0
  245. package/src/helpers/share-link-policy/resolver.ts +108 -0
  246. package/src/helpers/share-link-policy/types.ts +48 -0
  247. package/src/helpers/share-link-public/config.ts +41 -0
  248. package/src/helpers/share-link-view-notification.ts +18 -0
  249. package/src/models/Presentation.ts +2 -2
  250. package/src/models/Profile.ts +3 -2
  251. package/src/models/ShareLink.ts +130 -0
  252. package/src/models/ShareViewReceipt.ts +69 -0
  253. package/src/models/index.ts +2 -0
  254. package/src/models/share-link-constraints.ts +93 -0
  255. package/src/public-share-link-http.ts +49 -0
  256. package/src/routes/app-store.ts +2 -11
  257. package/src/routes/boosts.ts +2 -11
  258. package/src/routes/contracts.ts +2 -11
  259. package/src/routes/index.ts +139 -44
  260. package/src/routes/presentations.ts +19 -3
  261. package/src/routes/profile-manager.ts +10 -2
  262. package/src/routes/profiles.ts +17 -6
  263. package/src/routes/public-share-links.ts +814 -0
  264. package/src/routes/share-links.ts +640 -0
  265. package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
  266. package/test/dids.spec.ts +10 -1
  267. package/test/guardian-gated-routes.spec.ts +13 -0
  268. package/test/helpers/share-link-fixtures.ts +94 -0
  269. package/test/managed-recipient-inbox.spec.ts +130 -0
  270. package/test/notificationMessages.spec.ts +13 -0
  271. package/test/openapi.spec.ts +20 -0
  272. package/test/presentations.spec.ts +54 -6
  273. package/test/profiles.spec.ts +67 -1
  274. package/test/share-content-client.contract.spec.ts +365 -0
  275. package/test/share-content-client.unit.spec.ts +462 -0
  276. package/test/share-link-abuse.unit.spec.ts +65 -0
  277. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  278. package/test/share-link-coordinator.unit.spec.ts +990 -0
  279. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  280. package/test/share-link-lifecycle.neo4j.spec.ts +1268 -0
  281. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  282. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  283. package/test/share-link-list.neo4j.spec.ts +324 -0
  284. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  285. package/test/share-link-maintenance.unit.spec.ts +1113 -0
  286. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  287. package/test/share-link-owner-route.unit.spec.ts +752 -0
  288. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  289. package/test/share-link-passcode.unit.spec.ts +29 -0
  290. package/test/share-link-policy.unit.spec.ts +135 -0
  291. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  292. package/test/share-link-production-policy.unit.spec.ts +77 -0
  293. package/test/share-link-public-config.unit.spec.ts +64 -0
  294. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  295. package/test/share-link-public-route.unit.spec.ts +800 -0
  296. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  297. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  298. package/test/share-link-recovery.unit.spec.ts +726 -0
  299. package/vitest.config.ts +1 -0
  300. package/vitest.share-links-neo4j.config.ts +19 -0
  301. package/vitest.share-links-unit.config.ts +28 -0
@@ -0,0 +1,8 @@
1
+ export * from './types';
2
+ export { createShareLinkCoordinator } from './coordinator';
3
+ export { runShareContentCleanupOnce } from './cleanup-runner';
4
+ export { recoverShareLinkOperation, runShareLinkRecoveryOnce } from './recovery-runner';
5
+
6
+ // Runtime wiring over the real C3 repository lives in
7
+ // `@helpers/share-link-coordinator/runtime` so unit tests and the coordinator
8
+ // barrel never load `@instance`/Neo4j.
@@ -0,0 +1,428 @@
1
+ import { isShareLinkRepositoryError } from '../../accesslayer/share-link/errors';
2
+ import type {
3
+ RecoveryClaimRejection,
4
+ ShareLinkReservationRecord,
5
+ } from '../../accesslayer/share-link';
6
+ import type { ShareLinkRecord } from '../../models/ShareLink';
7
+ import { isTransientShareContentError } from '../share-content-client/types';
8
+ import { budgetAllows, budgetHasUnitReserve, boundTransactionTimeout } from './budget-helpers';
9
+ import { verifyShareContentActiveStat } from './stat';
10
+ import {
11
+ ShareLinkCoordinatorError,
12
+ type RecoveryRunnerDependencies,
13
+ type ScopedShareRecoveryResult,
14
+ type ShareRecoveryCategory,
15
+ type ShareRecoveryRunSummary,
16
+ } from './types';
17
+
18
+ const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
19
+
20
+ const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
21
+ typeof value === 'string' &&
22
+ value.length >= 1 &&
23
+ value.length <= maxLength &&
24
+ OPAQUE_IDENTIFIER_RE.test(value);
25
+
26
+ const failInvalidInput = (message: string): never => {
27
+ throw new ShareLinkCoordinatorError('INVALID_INPUT', message);
28
+ };
29
+
30
+ const assertRunnerScope = (
31
+ dependencies: RecoveryRunnerDependencies,
32
+ key?: { namespace: string; ownerProfileId: string; shareId: string; operationId: string }
33
+ ): void => {
34
+ if (!isOpaqueIdentifier(dependencies.claimant)) {
35
+ failInvalidInput('claimant must be an opaque configured identifier');
36
+ }
37
+ if (!isOpaqueIdentifier(dependencies.namespace)) {
38
+ failInvalidInput('namespace must be an opaque configured identifier');
39
+ }
40
+ if (
41
+ key &&
42
+ (key.namespace !== dependencies.namespace ||
43
+ !isOpaqueIdentifier(key.namespace) ||
44
+ !isOpaqueIdentifier(key.ownerProfileId) ||
45
+ !isOpaqueIdentifier(key.shareId) ||
46
+ !isOpaqueIdentifier(key.operationId))
47
+ ) {
48
+ failInvalidInput('recovery key must be well-formed opaque identifiers');
49
+ }
50
+ };
51
+
52
+ const recordCategory = (
53
+ categories: Partial<Record<ShareRecoveryCategory, number>>,
54
+ category: ShareRecoveryCategory
55
+ ): void => {
56
+ categories[category] = (categories[category] ?? 0) + 1;
57
+ };
58
+
59
+ type RecoveryDriveOutcome =
60
+ | { kind: 'finalized'; share: ShareLinkRecord }
61
+ | { kind: 'abandoned'; category: ShareRecoveryCategory }
62
+ | { kind: 'deferred'; category: ShareRecoveryCategory }
63
+ | { kind: 'claim_lost'; reason: RecoveryClaimRejection };
64
+
65
+ /** Bounded graph cost of one recovery transaction (claim, finalize or abandon). */
66
+ const graphCostMs = (dependencies: RecoveryRunnerDependencies): number =>
67
+ dependencies.transactionTimeoutMs ?? 0;
68
+
69
+ /**
70
+ * Worst-case bounded cost of claiming and driving one reservation:
71
+ * claim (graph) + stat (HTTP) + finalize/abandon (graph).
72
+ * The HTTP allowance is fixed when the transport client is constructed, so it
73
+ * cannot be reduced per call; the graph calls are bounded per call below.
74
+ */
75
+ const recoveryUnitCostMs = (dependencies: RecoveryRunnerDependencies): number =>
76
+ (dependencies.requestTimeoutMs ?? 0) + 2 * graphCostMs(dependencies);
77
+
78
+ /** Cost of the stat/verify tail once a reservation has already been claimed. */
79
+ const recoveryRemoteCostMs = (dependencies: RecoveryRunnerDependencies): number =>
80
+ (dependencies.requestTimeoutMs ?? 0) + graphCostMs(dependencies);
81
+
82
+ const finalizeFailureReason = (error: unknown): RecoveryClaimRejection | null => {
83
+ if (!isShareLinkRepositoryError(error)) return null;
84
+
85
+ switch (error.code) {
86
+ case 'STALE_GENERATION':
87
+ return 'stale_generation';
88
+ case 'LEASE_EXPIRED':
89
+ return 'lease_lapsed';
90
+ case 'NOT_FOUND':
91
+ return 'absent';
92
+ case 'CONFLICT':
93
+ case 'PRECONDITION_FAILED':
94
+ return 'version_changed';
95
+ default:
96
+ return null;
97
+ }
98
+ };
99
+
100
+ const finalizeClaimedReservation = async (
101
+ reservation: ShareLinkReservationRecord,
102
+ dependencies: RecoveryRunnerDependencies
103
+ ): Promise<RecoveryDriveOutcome> => {
104
+ try {
105
+ const finalized = await dependencies.repository.finalizeReservation({
106
+ namespace: reservation.namespace,
107
+ ownerProfileId: reservation.ownerProfileId,
108
+ shareId: reservation.shareId,
109
+ operationId: reservation.operationId,
110
+ objectRef: reservation.objectRef,
111
+ generation: reservation.generation,
112
+ leaseOwner: reservation.leaseOwner,
113
+ verifiedContentHash:
114
+ reservation.objectRef === null || reservation.contentHash === null
115
+ ? undefined
116
+ : reservation.contentHash,
117
+ now: dependencies.now?.(),
118
+ transactionTimeoutMs: boundTransactionTimeout(
119
+ dependencies.transactionTimeoutMs,
120
+ dependencies.budget
121
+ ),
122
+ noInlineRetry: dependencies.noInlineRetry,
123
+ });
124
+
125
+ return { kind: 'finalized', share: finalized.share };
126
+ } catch (error) {
127
+ const reason = finalizeFailureReason(error);
128
+
129
+ // A typed fence failure means another worker may own the reservation now;
130
+ // an unexpected failure keeps the work recoverable until the lease lapses.
131
+ return reason
132
+ ? { kind: 'claim_lost', reason }
133
+ : { kind: 'deferred', category: 'deferred_finalize' };
134
+ }
135
+ };
136
+
137
+ const abandonClaimedReservation = async (
138
+ reservation: ShareLinkReservationRecord,
139
+ dependencies: RecoveryRunnerDependencies,
140
+ category: ShareRecoveryCategory
141
+ ): Promise<RecoveryDriveOutcome> => {
142
+ try {
143
+ const abandoned = await dependencies.repository.abandonRecoveredReservation({
144
+ namespace: reservation.namespace,
145
+ ownerProfileId: reservation.ownerProfileId,
146
+ shareId: reservation.shareId,
147
+ operationId: reservation.operationId,
148
+ generation: reservation.generation,
149
+ leaseOwner: reservation.leaseOwner,
150
+ now: dependencies.now?.(),
151
+ transactionTimeoutMs: boundTransactionTimeout(
152
+ dependencies.transactionTimeoutMs,
153
+ dependencies.budget
154
+ ),
155
+ noInlineRetry: dependencies.noInlineRetry,
156
+ });
157
+
158
+ if (abandoned.outcome === 'abandoned') return { kind: 'abandoned', category };
159
+ if (abandoned.outcome === 'already_finalized') {
160
+ return { kind: 'finalized', share: abandoned.share };
161
+ }
162
+ if (abandoned.outcome === 'not_claimable') {
163
+ return { kind: 'claim_lost', reason: abandoned.reason };
164
+ }
165
+
166
+ return { kind: 'claim_lost', reason: 'absent' };
167
+ } catch {
168
+ return { kind: 'deferred', category: 'deferred_finalize' };
169
+ }
170
+ };
171
+
172
+ /**
173
+ * Drives one claimed reservation to a finalize or a fenced abandon using the
174
+ * persisted immutable tuple. All remote stat I/O happens here, outside every
175
+ * graph transaction. A mismatch or dependency failure never destroys content.
176
+ */
177
+ const driveClaimedReservation = async (
178
+ reservation: ShareLinkReservationRecord,
179
+ dependencies: RecoveryRunnerDependencies
180
+ ): Promise<RecoveryDriveOutcome> => {
181
+ if (reservation.objectRef === null || reservation.contentVersion === null) {
182
+ // Metadata-only reservation: finalize without any storage I/O.
183
+ if (
184
+ budgetHasUnitReserve(dependencies.budget) &&
185
+ !budgetAllows(dependencies.budget, graphCostMs(dependencies))
186
+ ) {
187
+ return { kind: 'deferred', category: 'budget_exhausted' };
188
+ }
189
+
190
+ return finalizeClaimedReservation(reservation, dependencies);
191
+ }
192
+
193
+ if (
194
+ budgetHasUnitReserve(dependencies.budget) &&
195
+ !budgetAllows(dependencies.budget, recoveryRemoteCostMs(dependencies))
196
+ ) {
197
+ return { kind: 'deferred', category: 'budget_exhausted' };
198
+ }
199
+
200
+ const stat = await dependencies.client.stat({
201
+ namespace: reservation.namespace,
202
+ ownerProfileId: reservation.ownerProfileId,
203
+ shareId: reservation.shareId,
204
+ contentVersion: reservation.contentVersion,
205
+ objectId: reservation.objectRef,
206
+ operationId: reservation.operationId,
207
+ });
208
+
209
+ if (!stat.ok) {
210
+ if (stat.error === 'NOT_FOUND') {
211
+ if (
212
+ budgetHasUnitReserve(dependencies.budget) &&
213
+ !budgetAllows(dependencies.budget, graphCostMs(dependencies))
214
+ ) {
215
+ return { kind: 'deferred', category: 'budget_exhausted' };
216
+ }
217
+
218
+ return abandonClaimedReservation(reservation, dependencies, 'abandoned_missing');
219
+ }
220
+
221
+ return {
222
+ kind: 'deferred',
223
+ category: isTransientShareContentError(stat.error)
224
+ ? 'deferred_stat_transient'
225
+ : 'deferred_stat_error',
226
+ };
227
+ }
228
+
229
+ if (verifyShareContentActiveStat(stat.value, reservation)) {
230
+ if (
231
+ budgetHasUnitReserve(dependencies.budget) &&
232
+ !budgetAllows(dependencies.budget, graphCostMs(dependencies))
233
+ ) {
234
+ return { kind: 'deferred', category: 'budget_exhausted' };
235
+ }
236
+
237
+ return finalizeClaimedReservation(reservation, dependencies);
238
+ }
239
+
240
+ if (stat.value.kind === 'tombstone') {
241
+ if (
242
+ budgetHasUnitReserve(dependencies.budget) &&
243
+ !budgetAllows(dependencies.budget, graphCostMs(dependencies))
244
+ ) {
245
+ return { kind: 'deferred', category: 'budget_exhausted' };
246
+ }
247
+
248
+ return abandonClaimedReservation(reservation, dependencies, 'abandoned_tombstone');
249
+ }
250
+
251
+ return { kind: 'deferred', category: 'deferred_stat_mismatch' };
252
+ };
253
+
254
+ /**
255
+ * Wraps the remote drive so a throwing storage dependency is reported as a
256
+ * bounded deferred category. A thrown stat must never escape the recovery pass or
257
+ * be mistaken for a proven deletion.
258
+ */
259
+ const safeDriveClaimedReservation = async (
260
+ reservation: ShareLinkReservationRecord,
261
+ dependencies: RecoveryRunnerDependencies
262
+ ): Promise<RecoveryDriveOutcome> => {
263
+ try {
264
+ return await driveClaimedReservation(reservation, dependencies);
265
+ } catch {
266
+ return { kind: 'deferred', category: 'deferred_stat_error' };
267
+ }
268
+ };
269
+
270
+ /**
271
+ * One bounded, one-shot recovery pass.
272
+ *
273
+ * It performs ONE discovery pass for the explicit namespace and then services
274
+ * each persisted key at most once, so a contended or failed item is never
275
+ * re-selected within the same pass. Every per-item failure is reported as a
276
+ * bounded category; the summary carries no owner ids, share ids, object refs,
277
+ * titles, notes, payloads, keys or URIs. Failed work stays recoverable and can be
278
+ * retried after its lease lapses.
279
+ */
280
+ export const runShareLinkRecoveryOnce = async (
281
+ dependencies: RecoveryRunnerDependencies
282
+ ): Promise<ShareRecoveryRunSummary> => {
283
+ assertRunnerScope(dependencies);
284
+
285
+ const summary: ShareRecoveryRunSummary = {
286
+ discovered: 0,
287
+ claimed: 0,
288
+ finalized: 0,
289
+ abandoned: 0,
290
+ deferred: 0,
291
+ claimLost: 0,
292
+ anomalies: 0,
293
+ categories: {},
294
+ };
295
+
296
+ // Bound discovery itself: a bounded maintenance pass must not start a read
297
+ // it cannot cover. Ordinary callers omit the reserve and keep the reviewed
298
+ // behaviour.
299
+ if (
300
+ budgetHasUnitReserve(dependencies.budget) &&
301
+ !budgetAllows(dependencies.budget, graphCostMs(dependencies))
302
+ ) {
303
+ recordCategory(summary.categories, 'budget_exhausted');
304
+
305
+ return summary;
306
+ }
307
+
308
+ const discovery = await dependencies.repository.discoverRecoverableReservations({
309
+ namespace: dependencies.namespace,
310
+ limit: dependencies.limit,
311
+ now: dependencies.now?.(),
312
+ transactionTimeoutMs: boundTransactionTimeout(
313
+ dependencies.transactionTimeoutMs,
314
+ dependencies.budget
315
+ ),
316
+ noInlineRetry: dependencies.noInlineRetry,
317
+ });
318
+
319
+ summary.discovered = discovery.keys.length;
320
+
321
+ for (const key of discovery.keys) {
322
+ // Whole-unit admission before each claim so a discovered item is never
323
+ // claimed only to be left mid-flight; a later run retries.
324
+ if (!budgetAllows(dependencies.budget, recoveryUnitCostMs(dependencies))) break;
325
+
326
+ let claim: Awaited<
327
+ ReturnType<RecoveryRunnerDependencies['repository']['claimRecoverableReservation']>
328
+ >;
329
+
330
+ try {
331
+ claim = await dependencies.repository.claimRecoverableReservation({
332
+ ...key,
333
+ leaseOwner: dependencies.claimant,
334
+ leaseMs: dependencies.leaseMs,
335
+ now: dependencies.now?.(),
336
+ transactionTimeoutMs: boundTransactionTimeout(
337
+ dependencies.transactionTimeoutMs,
338
+ dependencies.budget
339
+ ),
340
+ noInlineRetry: dependencies.noInlineRetry,
341
+ });
342
+ } catch {
343
+ // Could not confirm a claim; a later pass retries after the lease.
344
+ summary.claimLost += 1;
345
+ recordCategory(summary.categories, 'claim_lost');
346
+ continue;
347
+ }
348
+
349
+ if (claim.outcome !== 'claimed') {
350
+ if (claim.reason === 'malformed_binding') {
351
+ summary.anomalies += 1;
352
+ recordCategory(summary.categories, 'malformed_binding');
353
+ } else {
354
+ summary.claimLost += 1;
355
+ recordCategory(summary.categories, 'claim_lost');
356
+ }
357
+ continue;
358
+ }
359
+
360
+ summary.claimed += 1;
361
+ const outcome = await safeDriveClaimedReservation(claim.reservation, dependencies);
362
+
363
+ switch (outcome.kind) {
364
+ case 'finalized':
365
+ summary.finalized += 1;
366
+ recordCategory(summary.categories, 'finalized');
367
+ break;
368
+ case 'abandoned':
369
+ summary.abandoned += 1;
370
+ recordCategory(summary.categories, outcome.category);
371
+ break;
372
+ case 'deferred':
373
+ summary.deferred += 1;
374
+ recordCategory(summary.categories, outcome.category);
375
+ break;
376
+ case 'claim_lost':
377
+ summary.claimLost += 1;
378
+ recordCategory(summary.categories, 'claim_lost');
379
+ break;
380
+ }
381
+ }
382
+
383
+ return summary;
384
+ };
385
+
386
+ /**
387
+ * Scoped exact-key recovery retry.
388
+ *
389
+ * The reservation is loaded and claimed server-side; the caller supplies only the
390
+ * exact key, never an object ref, hash, generation or lease. When the finalize
391
+ * already committed but its response was lost, the committed current state is
392
+ * returned without re-uploading or reconstructing the deleted reservation.
393
+ */
394
+ export const recoverShareLinkOperation = async (
395
+ dependencies: RecoveryRunnerDependencies,
396
+ key: { namespace: string; ownerProfileId: string; shareId: string; operationId: string }
397
+ ): Promise<ScopedShareRecoveryResult> => {
398
+ assertRunnerScope(dependencies, key);
399
+
400
+ const target = await dependencies.repository.readShareLinkRecoveryTarget(key);
401
+
402
+ if (target.state === 'committed') return { status: 'committed', share: target.share };
403
+ if (target.state === 'absent') return { status: 'absent' };
404
+
405
+ const claim = await dependencies.repository.claimRecoverableReservation({
406
+ ...key,
407
+ leaseOwner: dependencies.claimant,
408
+ leaseMs: dependencies.leaseMs,
409
+ now: dependencies.now?.(),
410
+ });
411
+
412
+ if (claim.outcome !== 'claimed') {
413
+ return { status: 'not_claimable', reason: claim.reason };
414
+ }
415
+
416
+ const outcome = await safeDriveClaimedReservation(claim.reservation, dependencies);
417
+
418
+ switch (outcome.kind) {
419
+ case 'finalized':
420
+ return { status: 'committed', share: outcome.share };
421
+ case 'abandoned':
422
+ return { status: 'abandoned' };
423
+ case 'deferred':
424
+ return { status: 'deferred', category: outcome.category };
425
+ case 'claim_lost':
426
+ return { status: 'not_claimable', reason: outcome.reason };
427
+ }
428
+ };
@@ -0,0 +1,88 @@
1
+ import {
2
+ abandonRecoveredReservation,
3
+ abandonReservation,
4
+ claimCleanupJobs,
5
+ claimRecoverableReservation,
6
+ completeCleanupJob,
7
+ discoverRecoverableReservations,
8
+ finalizeReservation,
9
+ getCurrentShareContent,
10
+ getShareLink,
11
+ readShareLinkRecoveryTarget,
12
+ reserveCreate,
13
+ reserveReplacement,
14
+ revokeShareLink,
15
+ } from '@accesslayer/share-link';
16
+ import { resolveCurrentShareLinkPolicy } from '@helpers/share-link-policy/production';
17
+
18
+ import { createShareLinkCoordinator } from './coordinator';
19
+ import type {
20
+ CleanupRunnerDependencies,
21
+ RecoveryRunnerDependencies,
22
+ ShareLinkCoordinator,
23
+ ShareLinkCoordinatorDependencies,
24
+ ShareLinkLifecycleRepository,
25
+ ShareLinkRecoveryRepository,
26
+ } from './types';
27
+
28
+ /**
29
+ * Production wiring for the coordinator over the reviewed C3 lifecycle
30
+ * repository. Kept out of the coordinator barrel so unit tests never import
31
+ * `@instance`/the Neo4j graph.
32
+ *
33
+ * The scheduler is deliberately absent: nothing here starts a timer or worker.
34
+ */
35
+ const runtimeRepository: ShareLinkLifecycleRepository = {
36
+ reserveCreate,
37
+ reserveReplacement,
38
+ finalizeReservation: input =>
39
+ finalizeReservation({ ...input, resolveCurrentPolicy: resolveCurrentShareLinkPolicy }),
40
+ abandonReservation,
41
+ revokeShareLink,
42
+ getShareLink,
43
+ getCurrentShareContent,
44
+ claimCleanupJobs,
45
+ completeCleanupJob,
46
+ };
47
+
48
+ /**
49
+ * Production wiring for durable reservation recovery. It is separate from the
50
+ * lifecycle repository so no runner can accidentally accept a serialized caller
51
+ * reservation as authority.
52
+ */
53
+ const runtimeRecoveryRepository: ShareLinkRecoveryRepository = {
54
+ discoverRecoverableReservations,
55
+ readShareLinkRecoveryTarget,
56
+ claimRecoverableReservation,
57
+ abandonRecoveredReservation,
58
+ finalizeReservation: input =>
59
+ finalizeReservation({ ...input, resolveCurrentPolicy: resolveCurrentShareLinkPolicy }),
60
+ };
61
+
62
+ export const createRuntimeShareLinkCoordinator = (
63
+ options: Omit<ShareLinkCoordinatorDependencies, 'repository'> & {
64
+ repository?: ShareLinkLifecycleRepository;
65
+ }
66
+ ): ShareLinkCoordinator =>
67
+ createShareLinkCoordinator({
68
+ ...options,
69
+ repository: options.repository ?? runtimeRepository,
70
+ });
71
+
72
+ export const createRuntimeCleanupRunnerDependencies = (
73
+ options: Omit<CleanupRunnerDependencies, 'repository'> & {
74
+ repository?: CleanupRunnerDependencies['repository'];
75
+ }
76
+ ): CleanupRunnerDependencies => ({
77
+ ...options,
78
+ repository: options.repository ?? runtimeRepository,
79
+ });
80
+
81
+ export const createRuntimeRecoveryRunnerDependencies = (
82
+ options: Omit<RecoveryRunnerDependencies, 'repository'> & {
83
+ repository?: ShareLinkRecoveryRepository;
84
+ }
85
+ ): RecoveryRunnerDependencies => ({
86
+ ...options,
87
+ repository: options.repository ?? runtimeRecoveryRepository,
88
+ });
@@ -0,0 +1,30 @@
1
+ import type { ShareLinkReservationRecord } from '../../accesslayer/share-link';
2
+ import type { ShareContentStatValue } from '../share-content-client/types';
3
+
4
+ /**
5
+ * True only when an exact-object `stat` proves the immutable reserved object is
6
+ * still active with the full expected tuple, payload hash and byte sizes.
7
+ *
8
+ * Both the create/update coordinator and the durable recovery runner use this
9
+ * predicate so a recovered finalize applies exactly the same acceptance rule: a
10
+ * mismatch is never finalized and never destroys the staged content.
11
+ */
12
+ export const verifyShareContentActiveStat = (
13
+ stat: ShareContentStatValue,
14
+ reservation: ShareLinkReservationRecord
15
+ ): boolean => {
16
+ if (stat.kind !== 'active') return false;
17
+ if (reservation.objectRef === null || reservation.contentVersion === null) return false;
18
+
19
+ return (
20
+ stat.namespace === reservation.namespace &&
21
+ stat.ownerProfileId === reservation.ownerProfileId &&
22
+ stat.shareId === reservation.shareId &&
23
+ stat.objectId === reservation.objectRef &&
24
+ stat.operationId === reservation.operationId &&
25
+ stat.contentVersion === reservation.contentVersion &&
26
+ stat.payloadHash === reservation.contentHash &&
27
+ (reservation.contentBytes === null || stat.ciphertextBytes === reservation.contentBytes) &&
28
+ (reservation.recoveryBytes === null || stat.recoveryBytes === reservation.recoveryBytes)
29
+ );
30
+ };