@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,286 @@
1
+ import {
2
+ SHARE_LINK_CLEANUP_DEFAULT_BATCH,
3
+ SHARE_LINK_CLEANUP_MAX_BATCH,
4
+ } from '../share-link-lifecycle';
5
+ import {
6
+ isTransientShareContentError,
7
+ type ShareContentClientErrorCode,
8
+ } from '../share-content-client/types';
9
+ import {
10
+ ShareLinkCoordinatorError,
11
+ type CleanupRunnerDependencies,
12
+ type CleanupRunSummary,
13
+ type ShareCleanupCategory,
14
+ } from './types';
15
+ import { budgetAllows, budgetHasUnitReserve, boundTransactionTimeout } from './budget-helpers';
16
+
17
+ const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
18
+
19
+ const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
20
+ typeof value === 'string' &&
21
+ value.length >= 1 &&
22
+ value.length <= maxLength &&
23
+ OPAQUE_IDENTIFIER_RE.test(value);
24
+
25
+ /**
26
+ * Validate the operator-configured scope before any graph or remote I/O. A
27
+ * missing or malformed namespace means the pass does nothing at all.
28
+ */
29
+ const assertRunnerScope = (dependencies: CleanupRunnerDependencies): void => {
30
+ if (!isOpaqueIdentifier(dependencies.claimant)) {
31
+ throw new ShareLinkCoordinatorError(
32
+ 'INVALID_INPUT',
33
+ 'claimant must be an opaque configured identifier'
34
+ );
35
+ }
36
+ if (!isOpaqueIdentifier(dependencies.namespace)) {
37
+ throw new ShareLinkCoordinatorError(
38
+ 'INVALID_INPUT',
39
+ 'namespace must be an opaque configured identifier'
40
+ );
41
+ }
42
+ };
43
+
44
+ const clampTotal = (limit: number | undefined): number => {
45
+ if (!Number.isSafeInteger(limit) || (limit as number) < 1)
46
+ return SHARE_LINK_CLEANUP_DEFAULT_BATCH;
47
+
48
+ return Math.min(limit as number, SHARE_LINK_CLEANUP_MAX_BATCH);
49
+ };
50
+
51
+ const clampBatchSize = (batchSize: number | undefined, total: number): number => {
52
+ if (!Number.isSafeInteger(batchSize) || (batchSize as number) < 1) return total;
53
+
54
+ return Math.min(batchSize as number, total);
55
+ };
56
+
57
+ const recordCategory = (
58
+ categories: Partial<Record<ShareCleanupCategory, number>>,
59
+ category: ShareCleanupCategory
60
+ ): void => {
61
+ categories[category] = (categories[category] ?? 0) + 1;
62
+ };
63
+
64
+ /**
65
+ * Cap the managed-transaction timeout by the time left in the pass so a graph
66
+ * unit cannot outlive a cooperative deadline. No budget means the configured
67
+ * repository default is used unchanged.
68
+ */
69
+ const boundTransactionTimeoutMs = (
70
+ configuredTimeoutMs: number | undefined,
71
+ budget: CleanupRunnerDependencies['budget']
72
+ ): number | undefined => boundTransactionTimeout(configuredTimeoutMs, budget);
73
+
74
+ /**
75
+ * Worst-case bounded cost of one whole cleanup unit:
76
+ * claim (graph) + delete (HTTP) + complete (graph).
77
+ * The HTTP allowance is fixed when the transport client is constructed, so it
78
+ * cannot be reduced per call; the graph calls are bounded per call below.
79
+ */
80
+ const cleanupUnitCostMs = (dependencies: CleanupRunnerDependencies): number =>
81
+ (dependencies.requestTimeoutMs ?? 0) + 2 * (dependencies.transactionTimeoutMs ?? 0);
82
+
83
+ /** Cost of the remote/complete tail once a job has already been claimed. */
84
+ const cleanupRemoteCostMs = (dependencies: CleanupRunnerDependencies): number =>
85
+ (dependencies.requestTimeoutMs ?? 0) + (dependencies.transactionTimeoutMs ?? 0);
86
+
87
+ type DeleteOutcome =
88
+ | { ok: true }
89
+ | { ok: false; transient: true; error: ShareContentClientErrorCode }
90
+ | { ok: false; category: ShareCleanupCategory };
91
+
92
+ type ClaimedCleanupJob = Awaited<
93
+ ReturnType<CleanupRunnerDependencies['repository']['claimCleanupJobs']>
94
+ >['jobs'][number];
95
+
96
+ /**
97
+ * Bounded, one-shot cleanup runner for durable C3 cleanup jobs.
98
+ *
99
+ * It is deliberately NOT a scheduler: nothing registers it, no interval is
100
+ * created, and no ShareLink routes or workers are enabled by importing it. The
101
+ * maintenance scheduler owns cadence.
102
+ *
103
+ * For every claimed job it sends the exact immutable binding —
104
+ * `{ namespace, ownerProfileId, shareId, contentVersion, objectId: objectRef,
105
+ * operationId }` — to LearnCloud `delete`. `operationId` is the ORIGINAL object
106
+ * operation id recorded by C3, never the latest metadata operation.
107
+ *
108
+ * A job is completed only on a proven LearnCloud deletion/tombstone response.
109
+ * Transient/unavailable responses are returned to the queue with C3's capped
110
+ * exponential backoff. Any other response leaves the job claimed (never
111
+ * completed) so the fenced claim expires and another worker can retry; this
112
+ * never pretends a deletion happened. The returned summary contains aggregate
113
+ * counts and fixed categories only — never an object ref or exception text.
114
+ */
115
+ export const runShareContentCleanupOnce = async (
116
+ dependencies: CleanupRunnerDependencies
117
+ ): Promise<CleanupRunSummary> => {
118
+ assertRunnerScope(dependencies);
119
+
120
+ const { repository, client } = dependencies;
121
+ const total = clampTotal(dependencies.limit);
122
+ const batchSize = clampBatchSize(dependencies.claimBatchSize, total);
123
+
124
+ const summary: CleanupRunSummary = {
125
+ claimed: 0,
126
+ completed: 0,
127
+ retried: 0,
128
+ claimLost: 0,
129
+ skipped: 0,
130
+ categories: {},
131
+ };
132
+
133
+ // Claim just-in-time so a deadline can never strand a whole claimed batch.
134
+ // The default (batch size >= total) keeps the reviewed single-claim
135
+ // behaviour; maintenance sets a small batch size and re-checks the budget
136
+ // before every claim.
137
+ const singleShot = batchSize >= total;
138
+
139
+ while (summary.claimed < total) {
140
+ // Whole-unit admission: a claim is only started when the remaining budget
141
+ // can also cover the delete and the completion (or the retry write).
142
+ if (!budgetAllows(dependencies.budget, cleanupUnitCostMs(dependencies))) break;
143
+
144
+ const remaining = total - summary.claimed;
145
+ const claimLimit = singleShot ? total : Math.min(batchSize, remaining);
146
+
147
+ let claim: Awaited<ReturnType<CleanupRunnerDependencies['repository']['claimCleanupJobs']>>;
148
+ try {
149
+ claim = await repository.claimCleanupJobs({
150
+ namespace: dependencies.namespace,
151
+ claimant: dependencies.claimant,
152
+ limit: claimLimit,
153
+ claimMs: dependencies.claimMs,
154
+ transactionTimeoutMs: boundTransactionTimeoutMs(
155
+ dependencies.transactionTimeoutMs,
156
+ dependencies.budget
157
+ ),
158
+ noInlineRetry: dependencies.noInlineRetry,
159
+ // Omit production time so the repository samples it after the lock.
160
+ now: dependencies.now?.(),
161
+ });
162
+ } catch {
163
+ // Could not confirm a claim; a later pass retries after the lease.
164
+ summary.claimLost += 1;
165
+ recordCategory(summary.categories, 'claim_error');
166
+ break;
167
+ }
168
+
169
+ if (claim.jobs.length === 0) break;
170
+
171
+ let stoppedBeforeRemote = false;
172
+
173
+ for (const job of claim.jobs) {
174
+ summary.claimed += 1;
175
+
176
+ // Recheck after the awaited claim, before the network call: the claim
177
+ // may have waited on a lock. If the tail no longer fits, leave the job
178
+ // fenced (its lease will lapse) and start no further call. Only the
179
+ // maintenance budget opts into this mid-unit reserve.
180
+ if (
181
+ budgetHasUnitReserve(dependencies.budget) &&
182
+ !budgetAllows(dependencies.budget, cleanupRemoteCostMs(dependencies))
183
+ ) {
184
+ recordCategory(summary.categories, 'budget_exhausted');
185
+ stoppedBeforeRemote = true;
186
+ break;
187
+ }
188
+
189
+ const deletion = await safeDelete(client, job);
190
+
191
+ if (deletion.ok) {
192
+ const completion = await safeComplete(repository, {
193
+ objectRef: job.objectRef,
194
+ claimToken: claim.claimToken,
195
+ outcome: 'completed',
196
+ transactionTimeoutMs: boundTransactionTimeoutMs(
197
+ dependencies.transactionTimeoutMs,
198
+ dependencies.budget
199
+ ),
200
+ noInlineRetry: dependencies.noInlineRetry,
201
+ now: dependencies.now?.(),
202
+ });
203
+
204
+ if (completion?.outcome === 'completed') summary.completed += 1;
205
+ else {
206
+ summary.claimLost += 1;
207
+ recordCategory(summary.categories, 'claim_lost');
208
+ }
209
+
210
+ continue;
211
+ }
212
+
213
+ if ('transient' in deletion) {
214
+ const completion = await safeComplete(repository, {
215
+ objectRef: job.objectRef,
216
+ claimToken: claim.claimToken,
217
+ outcome: 'retry',
218
+ errorMessage: deletion.error,
219
+ transactionTimeoutMs: boundTransactionTimeoutMs(
220
+ dependencies.transactionTimeoutMs,
221
+ dependencies.budget
222
+ ),
223
+ noInlineRetry: dependencies.noInlineRetry,
224
+ now: dependencies.now?.(),
225
+ });
226
+
227
+ if (completion?.outcome === 'retry') summary.retried += 1;
228
+ else {
229
+ summary.claimLost += 1;
230
+ recordCategory(summary.categories, 'claim_lost');
231
+ }
232
+
233
+ continue;
234
+ }
235
+
236
+ // Deterministic/authorization failure: never complete. Leave the claim to
237
+ // expire so the bounded fence lets another worker retry later.
238
+ summary.skipped += 1;
239
+ recordCategory(summary.categories, deletion.category);
240
+ }
241
+
242
+ if (singleShot || stoppedBeforeRemote) break;
243
+ }
244
+
245
+ return summary;
246
+ };
247
+
248
+ const safeDelete = async (
249
+ client: CleanupRunnerDependencies['client'],
250
+ job: ClaimedCleanupJob
251
+ ): Promise<DeleteOutcome> => {
252
+ let result: Awaited<ReturnType<CleanupRunnerDependencies['client']['delete']>>;
253
+
254
+ try {
255
+ result = await client.delete({
256
+ namespace: job.namespace,
257
+ ownerProfileId: job.ownerProfileId,
258
+ shareId: job.shareId,
259
+ contentVersion: job.contentVersion,
260
+ objectId: job.objectRef,
261
+ operationId: job.operationId,
262
+ });
263
+ } catch {
264
+ return { ok: false, category: 'client_error' };
265
+ }
266
+
267
+ if (result.ok) return { ok: true };
268
+ if (isTransientShareContentError(result.error)) {
269
+ return { ok: false, transient: true, error: result.error };
270
+ }
271
+
272
+ return { ok: false, category: result.error };
273
+ };
274
+
275
+ const safeComplete = async (
276
+ repository: CleanupRunnerDependencies['repository'],
277
+ input: Parameters<CleanupRunnerDependencies['repository']['completeCleanupJob']>[0]
278
+ ): Promise<Awaited<
279
+ ReturnType<CleanupRunnerDependencies['repository']['completeCleanupJob']>
280
+ > | null> => {
281
+ try {
282
+ return await repository.completeCleanupJob(input);
283
+ } catch {
284
+ return null;
285
+ }
286
+ };