@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,355 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
4
+ import { v4 as uuid } from 'uuid';
5
+
6
+ import { neogma } from '@instance';
7
+
8
+ import { claimCleanupJobs, completeCleanupJob, getCleanupJob } from '../src/accesslayer/share-link';
9
+ import { ensureShareLinkConstraints } from '../src/models/share-link-constraints';
10
+
11
+ /**
12
+ * LC-2187 namespace-scoped cleanup claim/complete against a REAL Neo4j
13
+ * repository.
14
+ *
15
+ * These specs prove the repository-level namespace filter, the post-lock claim
16
+ * fence and the post-lock completion clock. They use injected explicit clocks
17
+ * (never guessed sleeps) so the timing assertions are deterministic.
18
+ */
19
+
20
+ const NAMESPACE = 'test-namespace';
21
+ const OTHER_NAMESPACE = 'other.example';
22
+ const BASE = new Date('2026-09-20T12:00:00.000Z');
23
+
24
+ const iso = (offsetMs = 0): string => new Date(BASE.getTime() + offsetMs).toISOString();
25
+
26
+ type CreatedJob = {
27
+ objectRef: string;
28
+ operationId: string;
29
+ };
30
+
31
+ const createCleanupJob = async (options: {
32
+ namespace?: string;
33
+ status?: 'queued' | 'claimed';
34
+ nextAttemptAt?: string;
35
+ claimToken?: string | null;
36
+ claimExpiresAt?: string | null;
37
+ attempts?: number;
38
+ }): Promise<CreatedJob> => {
39
+ const objectRef = randomBytes(32).toString('base64url');
40
+ const operationId = uuid();
41
+
42
+ await neogma.queryRunner.run(
43
+ `CREATE (c:ShareContentCleanupJob {
44
+ objectRef: $objectRef,
45
+ operationId: $operationId,
46
+ namespace: $namespace,
47
+ ownerProfileId: 'owner-1',
48
+ shareId: $shareId,
49
+ contentVersion: 1,
50
+ reason: 'stopped',
51
+ status: $status,
52
+ attempts: $attempts,
53
+ nextAttemptAt: $nextAttemptAt,
54
+ claimToken: $claimToken,
55
+ claimedBy: $claimedBy,
56
+ claimExpiresAt: $claimExpiresAt,
57
+ lastError: null,
58
+ createdAt: $now,
59
+ updatedAt: $now,
60
+ completedAt: null
61
+ })`,
62
+ {
63
+ objectRef,
64
+ operationId,
65
+ namespace: options.namespace ?? NAMESPACE,
66
+ shareId: randomBytes(16).toString('base64url'),
67
+ status: options.status ?? 'queued',
68
+ attempts: options.attempts ?? 0,
69
+ nextAttemptAt: options.nextAttemptAt ?? iso(),
70
+ claimToken: options.claimToken ?? null,
71
+ claimedBy: options.claimToken ? 'cleaner-1' : null,
72
+ claimExpiresAt: options.claimExpiresAt ?? null,
73
+ now: iso(),
74
+ }
75
+ );
76
+
77
+ return { objectRef, operationId };
78
+ };
79
+
80
+ const clearCleanupJobs = async (): Promise<void> => {
81
+ await neogma.queryRunner.run('MATCH (c:ShareContentCleanupJob) DETACH DELETE c');
82
+ };
83
+
84
+ describe('namespace-scoped cleanup claim (real Neo4j)', () => {
85
+ beforeAll(async () => {
86
+ await ensureShareLinkConstraints();
87
+ });
88
+
89
+ beforeEach(async () => {
90
+ await clearCleanupJobs();
91
+ });
92
+
93
+ afterAll(async () => {
94
+ await clearCleanupJobs();
95
+ });
96
+
97
+ it('never claims a foreign namespace job', async () => {
98
+ const own = await createCleanupJob({ namespace: NAMESPACE });
99
+ const foreign = await createCleanupJob({ namespace: OTHER_NAMESPACE });
100
+
101
+ const claimed = await claimCleanupJobs({
102
+ namespace: NAMESPACE,
103
+ claimant: 'cleaner-1',
104
+ now: BASE,
105
+ });
106
+
107
+ expect(claimed.jobs.map(job => job.objectRef)).toEqual([own.objectRef]);
108
+ expect(claimed.jobs.map(job => job.objectRef)).not.toContain(foreign.objectRef);
109
+ expect((await getCleanupJob(foreign.objectRef))?.status).toBe('queued');
110
+ });
111
+
112
+ it('lets exactly one of two concurrent claims win', async () => {
113
+ const created = await createCleanupJob({ namespace: NAMESPACE });
114
+
115
+ const claims = await Promise.all([
116
+ claimCleanupJobs({ namespace: NAMESPACE, claimant: 'a', now: BASE }),
117
+ claimCleanupJobs({ namespace: NAMESPACE, claimant: 'b', now: BASE }),
118
+ ]);
119
+
120
+ const claimedJobs = claims.flatMap(claim => claim.jobs);
121
+ expect(claimedJobs).toHaveLength(1);
122
+ expect(claimedJobs[0]?.objectRef).toBe(created.objectRef);
123
+ });
124
+
125
+ it('computes the claim expiry from the explicit clock', async () => {
126
+ await createCleanupJob({ namespace: NAMESPACE });
127
+
128
+ const claimed = await claimCleanupJobs({
129
+ namespace: NAMESPACE,
130
+ claimant: 'cleaner-1',
131
+ claimMs: 5_000,
132
+ now: BASE,
133
+ });
134
+
135
+ expect(claimed.jobs[0]?.claimExpiresAt).toBe(iso(5_000));
136
+ });
137
+
138
+ it('rejects a missing namespace before touching the graph', async () => {
139
+ await expect(
140
+ claimCleanupJobs({ namespace: '', claimant: 'cleaner-1', now: BASE })
141
+ ).rejects.toMatchObject({ code: 'INVALID_INPUT' });
142
+ });
143
+ });
144
+
145
+ describe('post-lock cleanup completion fence (real Neo4j)', () => {
146
+ beforeAll(async () => {
147
+ await ensureShareLinkConstraints();
148
+ });
149
+
150
+ beforeEach(async () => {
151
+ await clearCleanupJobs();
152
+ });
153
+
154
+ afterAll(async () => {
155
+ await clearCleanupJobs();
156
+ });
157
+
158
+ it('refuses completion for a stale claim token', async () => {
159
+ const created = await createCleanupJob({
160
+ namespace: NAMESPACE,
161
+ status: 'claimed',
162
+ claimToken: 'token-1',
163
+ claimExpiresAt: iso(5_000),
164
+ });
165
+
166
+ const result = await completeCleanupJob({
167
+ objectRef: created.objectRef,
168
+ claimToken: 'not-the-token',
169
+ outcome: 'completed',
170
+ now: BASE,
171
+ });
172
+
173
+ expect(result).toEqual({ outcome: 'claim_lost' });
174
+ expect((await getCleanupJob(created.objectRef))?.status).toBe('claimed');
175
+ });
176
+
177
+ it('refuses completion once the claim has expired, even while the job lock was held', async () => {
178
+ const created = await createCleanupJob({
179
+ namespace: NAMESPACE,
180
+ status: 'claimed',
181
+ claimToken: 'token-1',
182
+ claimExpiresAt: iso(1_000),
183
+ });
184
+
185
+ const driver = neogma.queryRunner.getDriver();
186
+ const holderSession = driver.session();
187
+
188
+ let releaseLock!: () => void;
189
+ const lockGate = new Promise<void>(resolve => {
190
+ releaseLock = resolve;
191
+ });
192
+ let signalLocked!: () => void;
193
+ const lockHeld = new Promise<void>(resolve => {
194
+ signalLocked = resolve;
195
+ });
196
+
197
+ const holder = holderSession
198
+ .executeWrite(async tx => {
199
+ await tx.run(
200
+ `MATCH (c:ShareContentCleanupJob {objectRef: $objectRef})
201
+ SET c.lockTick = coalesce(c.lockTick, 0) + 1 RETURN c`,
202
+ { objectRef: created.objectRef }
203
+ );
204
+ signalLocked();
205
+ await lockGate;
206
+ })
207
+ .finally(() => holderSession.close());
208
+
209
+ await lockHeld;
210
+
211
+ // The completion transaction must wait for the held write lock, then read
212
+ // the (already lapsed) lease with the injectable post-lock clock.
213
+ const completion = completeCleanupJob({
214
+ objectRef: created.objectRef,
215
+ claimToken: 'token-1',
216
+ outcome: 'completed',
217
+ now: new Date(BASE.getTime() + 5_000),
218
+ });
219
+
220
+ releaseLock();
221
+
222
+ const [result] = await Promise.all([completion, holder]);
223
+
224
+ expect(result).toEqual({ outcome: 'claim_lost' });
225
+ expect((await getCleanupJob(created.objectRef))?.status).toBe('claimed');
226
+ });
227
+
228
+ it('bounds a maintenance completion under a held lock without an inline retry', async () => {
229
+ const created = await createCleanupJob({
230
+ namespace: NAMESPACE,
231
+ status: 'claimed',
232
+ claimToken: 'token-1',
233
+ claimExpiresAt: iso(60_000),
234
+ });
235
+
236
+ const driver = neogma.queryRunner.getDriver();
237
+ const holderSession = driver.session();
238
+
239
+ let releaseLock!: () => void;
240
+ const lockGate = new Promise<void>(resolve => {
241
+ releaseLock = resolve;
242
+ });
243
+ let signalLocked!: () => void;
244
+ const lockHeld = new Promise<void>(resolve => {
245
+ signalLocked = resolve;
246
+ });
247
+
248
+ const holder = holderSession
249
+ .executeWrite(async tx => {
250
+ await tx.run(
251
+ `MATCH (c:ShareContentCleanupJob {objectRef: $objectRef})
252
+ SET c.lockTick = coalesce(c.lockTick, 0) + 1 RETURN c`,
253
+ { objectRef: created.objectRef }
254
+ );
255
+ signalLocked();
256
+ await lockGate;
257
+ })
258
+ .finally(() => holderSession.close());
259
+
260
+ await lockHeld;
261
+
262
+ // Guard so the test can never hang even if the driver ignored the bound.
263
+ const releaseTimer = setTimeout(() => releaseLock(), 5_000);
264
+
265
+ try {
266
+ const startedAt = Date.now();
267
+ const completion = completeCleanupJob({
268
+ objectRef: created.objectRef,
269
+ claimToken: 'token-1',
270
+ outcome: 'completed',
271
+ transactionTimeoutMs: 250,
272
+ noInlineRetry: true,
273
+ now: BASE,
274
+ });
275
+
276
+ await expect(completion).rejects.toBeTruthy();
277
+
278
+ // A bounded attempt fails fast rather than waiting out the lock.
279
+ expect(Date.now() - startedAt).toBeLessThan(3_000);
280
+ } finally {
281
+ clearTimeout(releaseTimer);
282
+ releaseLock();
283
+ await holder;
284
+ }
285
+
286
+ // The aborted attempt must not have completed the job.
287
+ expect((await getCleanupJob(created.objectRef))?.status).toBe('claimed');
288
+
289
+ // Ordinary managed completion (no maintenance bound) still succeeds.
290
+ const after = await completeCleanupJob({
291
+ objectRef: created.objectRef,
292
+ claimToken: 'token-1',
293
+ outcome: 'completed',
294
+ now: BASE,
295
+ });
296
+
297
+ expect(after.outcome).toBe('completed');
298
+ });
299
+
300
+ it('returns a transient failure to the queue with backoff and preserves identity', async () => {
301
+ const created = await createCleanupJob({
302
+ namespace: NAMESPACE,
303
+ status: 'claimed',
304
+ claimToken: 'token-1',
305
+ claimExpiresAt: iso(5_000),
306
+ });
307
+
308
+ const result = await completeCleanupJob({
309
+ objectRef: created.objectRef,
310
+ claimToken: 'token-1',
311
+ outcome: 'retry',
312
+ errorMessage: 'throttled',
313
+ now: BASE,
314
+ });
315
+
316
+ expect(result.outcome).toBe('retry');
317
+ if (result.outcome === 'retry') {
318
+ expect(result.job.status).toBe('queued');
319
+ expect(result.job.attempts).toBe(1);
320
+ expect(Date.parse(result.job.nextAttemptAt)).toBeGreaterThan(BASE.getTime());
321
+ expect(result.job.operationId).toBe(created.operationId);
322
+ }
323
+
324
+ const stored = await getCleanupJob(created.objectRef);
325
+ expect(stored?.operationId).toBe(created.operationId);
326
+ expect(stored?.status).toBe('queued');
327
+ });
328
+
329
+ it('reclaims after lease expiry and fences the old claim token', async () => {
330
+ const created = await createCleanupJob({
331
+ namespace: NAMESPACE,
332
+ status: 'claimed',
333
+ claimToken: 'old-token',
334
+ claimExpiresAt: iso(1_000),
335
+ });
336
+
337
+ const reclaimed = await claimCleanupJobs({
338
+ namespace: NAMESPACE,
339
+ claimant: 'cleaner-2',
340
+ now: new Date(BASE.getTime() + 2_000),
341
+ });
342
+
343
+ expect(reclaimed.jobs.map(job => job.objectRef)).toEqual([created.objectRef]);
344
+ expect(reclaimed.claimToken).not.toBe('old-token');
345
+
346
+ const stale = await completeCleanupJob({
347
+ objectRef: created.objectRef,
348
+ claimToken: 'old-token',
349
+ outcome: 'completed',
350
+ now: new Date(BASE.getTime() + 2_000),
351
+ });
352
+
353
+ expect(stale).toEqual({ outcome: 'claim_lost' });
354
+ });
355
+ });