@learncard/network-brain-service 3.18.0 → 3.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/.env.example +38 -2
  2. package/CHANGELOG.md +118 -0
  3. package/README.md +42 -0
  4. package/compose.inbox.yml +7 -0
  5. package/dist/accesslayer/auth-grant/read.d.ts +1 -0
  6. package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
  7. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  8. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  9. package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
  10. package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
  11. package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
  12. package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
  13. package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
  14. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  15. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  16. package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
  17. package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
  18. package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
  19. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  20. package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
  21. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
  22. package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
  23. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  24. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  25. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  26. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  27. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  29. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  30. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  31. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  32. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  33. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  34. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  35. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  36. package/dist/accesslayer/share-link/helpers.d.ts +56 -0
  37. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  38. package/dist/accesslayer/share-link/index.d.ts +12 -0
  39. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  40. package/dist/accesslayer/share-link/list.d.ts +5 -0
  41. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  42. package/dist/accesslayer/share-link/read.d.ts +5 -0
  43. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  44. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  45. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  46. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  47. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  48. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  49. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  50. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  51. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  52. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  53. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  54. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  55. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  56. package/dist/accesslayer/share-link/types.d.ts +370 -0
  57. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  58. package/dist/app.d.ts +4 -0
  59. package/dist/app.d.ts.map +1 -1
  60. package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
  61. package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
  62. package/dist/cache/atomic-counter.test.d.ts +2 -0
  63. package/dist/cache/atomic-counter.test.d.ts.map +1 -0
  64. package/dist/cache/index.d.ts +1 -1
  65. package/dist/cache/index.d.ts.map +1 -1
  66. package/dist/config/environment.d.ts +55 -0
  67. package/dist/config/environment.d.ts.map +1 -1
  68. package/dist/constants/auth-grant.d.ts +2 -0
  69. package/dist/constants/auth-grant.d.ts.map +1 -1
  70. package/dist/credential-refresh.d.ts.map +1 -1
  71. package/dist/helpers/activity.helpers.d.ts +4 -0
  72. package/dist/helpers/activity.helpers.d.ts.map +1 -1
  73. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  74. package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
  75. package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
  76. package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
  77. package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
  78. package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
  79. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
  80. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
  81. package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
  82. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
  83. package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
  84. package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
  85. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
  86. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
  87. package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
  88. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  89. package/dist/helpers/credential.helpers.d.ts +2 -1
  90. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  91. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  92. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  93. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  94. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  95. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  96. package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
  97. package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
  98. package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
  99. package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
  100. package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
  101. package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
  102. package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
  103. package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
  104. package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
  105. package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
  106. package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
  107. package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
  108. package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
  109. package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
  110. package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
  111. package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
  112. package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
  113. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
  114. package/dist/helpers/inbox.helpers.d.ts +9 -1
  115. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  116. package/dist/helpers/notificationMessages.d.ts +1 -1
  117. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  118. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  119. package/dist/helpers/presentation-metadata.d.ts +4 -0
  120. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  121. package/dist/helpers/presentation.helpers.d.ts +1 -1
  122. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  123. package/dist/helpers/profile.helpers.d.ts +1 -0
  124. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  125. package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
  126. package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
  127. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
  128. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  129. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  130. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  131. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  132. package/dist/helpers/share-content-client/client.d.ts +5 -0
  133. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  134. package/dist/helpers/share-content-client/config.d.ts +11 -0
  135. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  136. package/dist/helpers/share-content-client/index.d.ts +6 -0
  137. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  138. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  139. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  140. package/dist/helpers/share-content-client/types.d.ts +145 -0
  141. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  142. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  143. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  144. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  145. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  146. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  147. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  148. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  149. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  150. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  151. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  152. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  153. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  154. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  155. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  156. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  157. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  158. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  159. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  160. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  161. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  162. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  163. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  164. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  165. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  166. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  167. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  168. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  169. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  170. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  171. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  172. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  173. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  174. package/dist/helpers/share-link-maintenance/runner.d.ts +19 -0
  175. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  176. package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -0
  177. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  178. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  179. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  180. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  181. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  182. package/dist/helpers/share-link-maintenance/types.d.ts +53 -0
  183. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  184. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  185. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  186. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  187. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  188. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  189. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  190. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  191. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  192. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  193. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  194. package/dist/helpers/share-link-passcode.d.ts +6 -0
  195. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  196. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  197. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  198. package/dist/helpers/share-link-policy/production.d.ts +13 -0
  199. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  200. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  201. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  202. package/dist/helpers/share-link-policy/types.d.ts +16 -0
  203. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  204. package/dist/helpers/share-link-public/config.d.ts +13 -0
  205. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  206. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  207. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  208. package/dist/helpers/shim.d.ts +1 -1
  209. package/dist/helpers/shim.d.ts.map +1 -1
  210. package/dist/inbox-queue-local.d.ts +2 -0
  211. package/dist/inbox-queue-local.d.ts.map +1 -0
  212. package/dist/index.js +2 -2
  213. package/dist/models/AuthGrant.d.ts.map +1 -1
  214. package/dist/models/CredentialActivity.d.ts +1 -0
  215. package/dist/models/CredentialActivity.d.ts.map +1 -1
  216. package/dist/models/CredentialRefresh.d.ts +5 -2
  217. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  218. package/dist/models/InboxCredential.d.ts +1 -0
  219. package/dist/models/InboxCredential.d.ts.map +1 -1
  220. package/dist/models/Presentation.d.ts +2 -0
  221. package/dist/models/Presentation.d.ts.map +1 -1
  222. package/dist/models/Profile.d.ts +2 -0
  223. package/dist/models/Profile.d.ts.map +1 -1
  224. package/dist/models/ShareLink.d.ts +44 -0
  225. package/dist/models/ShareLink.d.ts.map +1 -0
  226. package/dist/models/ShareViewReceipt.d.ts +22 -0
  227. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  228. package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
  229. package/dist/models/index.d.ts +1 -0
  230. package/dist/models/index.d.ts.map +1 -1
  231. package/dist/models/share-link-constraints.d.ts +3 -0
  232. package/dist/models/share-link-constraints.d.ts.map +1 -0
  233. package/dist/openapi.d.ts.map +1 -1
  234. package/dist/public-share-link-http.d.ts +4 -0
  235. package/dist/public-share-link-http.d.ts.map +1 -0
  236. package/dist/routes/act-as.test.d.ts +2 -0
  237. package/dist/routes/act-as.test.d.ts.map +1 -0
  238. package/dist/routes/activity.d.ts +3 -0
  239. package/dist/routes/activity.d.ts.map +1 -1
  240. package/dist/routes/app-store.d.ts.map +1 -1
  241. package/dist/routes/auth-grants.d.ts +10 -0
  242. package/dist/routes/auth-grants.d.ts.map +1 -1
  243. package/dist/routes/boosts.d.ts +494 -0
  244. package/dist/routes/boosts.d.ts.map +1 -1
  245. package/dist/routes/contracts.d.ts +2 -2
  246. package/dist/routes/contracts.d.ts.map +1 -1
  247. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  248. package/dist/routes/credentials.d.ts +1 -1
  249. package/dist/routes/credentials.d.ts.map +1 -1
  250. package/dist/routes/inbox.d.ts +953 -11
  251. package/dist/routes/inbox.d.ts.map +1 -1
  252. package/dist/routes/index.d.ts +154 -85
  253. package/dist/routes/index.d.ts.map +1 -1
  254. package/dist/routes/presentations.d.ts +1 -0
  255. package/dist/routes/presentations.d.ts.map +1 -1
  256. package/dist/routes/profile-manager.d.ts.map +1 -1
  257. package/dist/routes/profiles.d.ts.map +1 -1
  258. package/dist/routes/public-share-links.d.ts +230 -0
  259. package/dist/routes/public-share-links.d.ts.map +1 -0
  260. package/dist/routes/share-links.d.ts +844 -0
  261. package/dist/routes/share-links.d.ts.map +1 -0
  262. package/dist/routes/workflows.d.ts.map +1 -1
  263. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  264. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
  265. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
  266. package/dist/services/registry/registry.factory.d.ts.map +1 -1
  267. package/dist/types/activity.d.ts +4 -0
  268. package/dist/types/activity.d.ts.map +1 -1
  269. package/dist/types/consentflowcontract.d.ts +1 -1
  270. package/dist/types/credential-refresh.d.ts +10 -1
  271. package/dist/types/credential-refresh.d.ts.map +1 -1
  272. package/dist/types/inbox-batch.d.ts +38 -0
  273. package/dist/types/inbox-batch.d.ts.map +1 -0
  274. package/inbox-e2e-setup.ts +53 -1
  275. package/inbox-elasticmq.conf +10 -0
  276. package/lambda.ts +64 -16
  277. package/package.json +24 -16
  278. package/scripts/seed-demo-persona.ts +425 -0
  279. package/scripts/setup-inbox-queues.ts +60 -0
  280. package/serverless-local.yml +2 -0
  281. package/serverless.yml +111 -0
  282. package/shareLinkMaintenanceLambda.ts +49 -0
  283. package/src/accesslayer/auth-grant/read.ts +2 -1
  284. package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
  285. package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
  286. package/src/accesslayer/credential-activity/create.ts +1 -0
  287. package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
  288. package/src/accesslayer/credential-refresh/index.ts +1 -0
  289. package/src/accesslayer/credential-refresh/read.ts +8 -2
  290. package/src/accesslayer/credential-refresh/update.ts +4 -0
  291. package/src/accesslayer/inbox-batch/store.ts +469 -0
  292. package/src/accesslayer/inbox-credential/create.ts +51 -19
  293. package/src/accesslayer/inbox-credential/refresh.ts +221 -0
  294. package/src/accesslayer/inbox-credential/update.ts +1 -1
  295. package/src/accesslayer/presentation/read.ts +57 -25
  296. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  297. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  298. package/src/accesslayer/share-link/abandon.ts +100 -0
  299. package/src/accesslayer/share-link/cleanup.ts +271 -0
  300. package/src/accesslayer/share-link/errors.ts +32 -0
  301. package/src/accesslayer/share-link/finalize.ts +265 -0
  302. package/src/accesslayer/share-link/helpers.ts +419 -0
  303. package/src/accesslayer/share-link/index.ts +11 -0
  304. package/src/accesslayer/share-link/list.ts +162 -0
  305. package/src/accesslayer/share-link/read.ts +107 -0
  306. package/src/accesslayer/share-link/receipt.ts +548 -0
  307. package/src/accesslayer/share-link/recovery.ts +545 -0
  308. package/src/accesslayer/share-link/reserve.ts +684 -0
  309. package/src/accesslayer/share-link/revoke.ts +197 -0
  310. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  311. package/src/accesslayer/share-link/transaction.ts +126 -0
  312. package/src/accesslayer/share-link/types.ts +601 -0
  313. package/src/app.ts +6 -0
  314. package/src/cache/atomic-counter.test-helpers.ts +27 -0
  315. package/src/cache/atomic-counter.test.ts +6 -0
  316. package/src/cache/index.ts +25 -13
  317. package/src/config/environment.ts +40 -0
  318. package/src/constants/auth-grant.ts +5 -0
  319. package/src/credential-refresh.ts +5 -1
  320. package/src/docker-entry.ts +65 -0
  321. package/src/helpers/activity.helpers.ts +4 -0
  322. package/src/helpers/app-counter.helpers.ts +1 -1
  323. package/src/helpers/boost.helpers.ts +4 -31
  324. package/src/helpers/contact-method.helpers.test.ts +43 -0
  325. package/src/helpers/contact-method.helpers.ts +3 -4
  326. package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
  327. package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
  328. package/src/helpers/credential-refresh-email.helpers.ts +201 -0
  329. package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
  330. package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
  331. package/src/helpers/credential-refresh.helpers.ts +375 -99
  332. package/src/helpers/credential.helpers.ts +4 -1
  333. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  334. package/src/helpers/credentialSubject.helpers.ts +40 -0
  335. package/src/helpers/finalize-inbox.helpers.ts +29 -14
  336. package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
  337. package/src/helpers/inbox-batch-http.helpers.ts +150 -0
  338. package/src/helpers/inbox-batch-status.helpers.ts +11 -0
  339. package/src/helpers/inbox-batch.helpers.test.ts +668 -0
  340. package/src/helpers/inbox-batch.helpers.ts +374 -0
  341. package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
  342. package/src/helpers/inbox-queue.helpers.test.ts +253 -0
  343. package/src/helpers/inbox-queue.helpers.ts +372 -0
  344. package/src/helpers/inbox-refresh.helpers.ts +432 -0
  345. package/src/helpers/inbox.helpers.ts +275 -36
  346. package/src/helpers/notificationMessages.ts +18 -1
  347. package/src/helpers/notifications.helpers.ts +4 -2
  348. package/src/helpers/presentation-metadata.ts +41 -0
  349. package/src/helpers/presentation.helpers.ts +27 -20
  350. package/src/helpers/profile.helpers.ts +7 -0
  351. package/src/helpers/rateLimit.helpers.ts +2 -5
  352. package/src/helpers/refresh-send-intent.helpers.ts +273 -0
  353. package/src/helpers/share-content-client/adapters.ts +55 -0
  354. package/src/helpers/share-content-client/canonical.ts +25 -0
  355. package/src/helpers/share-content-client/client.ts +440 -0
  356. package/src/helpers/share-content-client/config.ts +189 -0
  357. package/src/helpers/share-content-client/index.ts +27 -0
  358. package/src/helpers/share-content-client/responses.ts +273 -0
  359. package/src/helpers/share-content-client/types.ts +253 -0
  360. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  361. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  362. package/src/helpers/share-link-coordinator/coordinator.ts +714 -0
  363. package/src/helpers/share-link-coordinator/index.ts +8 -0
  364. package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -0
  365. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  366. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  367. package/src/helpers/share-link-coordinator/types.ts +353 -0
  368. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  369. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  370. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  371. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  372. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  373. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  374. package/src/helpers/share-link-maintenance/config.ts +312 -0
  375. package/src/helpers/share-link-maintenance/index.ts +24 -0
  376. package/src/helpers/share-link-maintenance/runner.ts +223 -0
  377. package/src/helpers/share-link-maintenance/runtime.ts +334 -0
  378. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  379. package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
  380. package/src/helpers/share-link-maintenance/types.ts +89 -0
  381. package/src/helpers/share-link-owner/config.ts +48 -0
  382. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  383. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  384. package/src/helpers/share-link-owner-projection.ts +52 -0
  385. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  386. package/src/helpers/share-link-passcode.ts +47 -0
  387. package/src/helpers/share-link-policy/index.ts +7 -0
  388. package/src/helpers/share-link-policy/production.ts +88 -0
  389. package/src/helpers/share-link-policy/resolver.ts +108 -0
  390. package/src/helpers/share-link-policy/types.ts +48 -0
  391. package/src/helpers/share-link-public/config.ts +41 -0
  392. package/src/helpers/share-link-view-notification.ts +18 -0
  393. package/src/helpers/shim.ts +32 -31
  394. package/src/inbox-queue-local.ts +28 -0
  395. package/src/models/AuthGrant.ts +1 -0
  396. package/src/models/CredentialActivity.ts +3 -5
  397. package/src/models/CredentialRefresh.ts +4 -2
  398. package/src/models/InboxCredential.ts +2 -0
  399. package/src/models/Presentation.ts +2 -2
  400. package/src/models/Profile.ts +3 -2
  401. package/src/models/ShareLink.ts +130 -0
  402. package/src/models/ShareViewReceipt.ts +69 -0
  403. package/src/models/credential-refresh-constraints.test.ts +8 -3
  404. package/src/models/credential-refresh-constraints.ts +5 -0
  405. package/src/models/index.ts +2 -0
  406. package/src/models/share-link-constraints.ts +93 -0
  407. package/src/openapi.ts +8 -0
  408. package/src/public-share-link-http.ts +49 -0
  409. package/src/routes/act-as.test.ts +239 -0
  410. package/src/routes/app-store.ts +4 -13
  411. package/src/routes/auth-grants.ts +13 -11
  412. package/src/routes/boosts.ts +916 -103
  413. package/src/routes/contracts.ts +2 -11
  414. package/src/routes/credential-refreshes.test.ts +8 -2
  415. package/src/routes/credential-refreshes.ts +4 -1
  416. package/src/routes/credentials.ts +2 -1
  417. package/src/routes/inbox.ts +95 -49
  418. package/src/routes/index.ts +216 -46
  419. package/src/routes/presentations.ts +19 -3
  420. package/src/routes/profile-manager.ts +10 -2
  421. package/src/routes/profiles.ts +17 -6
  422. package/src/routes/public-share-links.ts +814 -0
  423. package/src/routes/share-links.ts +640 -0
  424. package/src/routes/skills.ts +1 -1
  425. package/src/routes/workflows.ts +43 -24
  426. package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
  427. package/src/services/delivery/adapters/postmark.adapter.ts +16 -1
  428. package/src/services/registry/registry.factory.ts +2 -1
  429. package/src/types/activity.ts +2 -0
  430. package/src/types/credential-refresh.ts +21 -2
  431. package/src/types/inbox-batch.ts +46 -0
  432. package/test/atomic-counter.e2e.ts +12 -0
  433. package/test/auth-grant.spec.ts +21 -21
  434. package/test/credential-refresh-email.spec.ts +663 -0
  435. package/test/credential-refresh-publication.spec.ts +2 -0
  436. package/test/dids.spec.ts +10 -1
  437. package/test/guardian-gated-routes.spec.ts +13 -0
  438. package/test/helpers/inbox-e2e-environment.ts +6 -0
  439. package/test/helpers/share-link-fixtures.ts +94 -0
  440. package/test/inbox-batch.spec.ts +1338 -0
  441. package/test/inbox-refresh.spec.ts +534 -0
  442. package/test/managed-recipient-inbox.spec.ts +130 -0
  443. package/test/notificationMessages.spec.ts +13 -0
  444. package/test/openapi.spec.ts +61 -1
  445. package/test/presentations.spec.ts +54 -6
  446. package/test/profiles.spec.ts +67 -1
  447. package/test/refresh-send.spec.ts +1799 -0
  448. package/test/registry.spec.ts +56 -11
  449. package/test/share-content-client.contract.spec.ts +365 -0
  450. package/test/share-content-client.unit.spec.ts +462 -0
  451. package/test/share-link-abuse.unit.spec.ts +65 -0
  452. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  453. package/test/share-link-coordinator.unit.spec.ts +990 -0
  454. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  455. package/test/share-link-lifecycle.neo4j.spec.ts +1268 -0
  456. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  457. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  458. package/test/share-link-list.neo4j.spec.ts +324 -0
  459. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  460. package/test/share-link-maintenance.unit.spec.ts +1113 -0
  461. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  462. package/test/share-link-owner-route.unit.spec.ts +752 -0
  463. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  464. package/test/share-link-passcode.unit.spec.ts +29 -0
  465. package/test/share-link-policy.unit.spec.ts +135 -0
  466. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  467. package/test/share-link-production-policy.unit.spec.ts +77 -0
  468. package/test/share-link-public-config.unit.spec.ts +64 -0
  469. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  470. package/test/share-link-public-route.unit.spec.ts +800 -0
  471. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  472. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  473. package/test/share-link-recovery.unit.spec.ts +726 -0
  474. package/vitest.config.ts +10 -0
  475. package/vitest.inbox-e2e.config.ts +5 -1
  476. package/vitest.integration.config.ts +2 -0
  477. package/vitest.share-links-neo4j.config.ts +19 -0
  478. package/vitest.share-links-unit.config.ts +28 -0
@@ -0,0 +1,419 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
+
3
+ import type { ShareLinkOperationKind } from '@helpers/share-link-lifecycle';
4
+
5
+ import type { ShareContentState, ShareLinkRecord, ShareLinkStatus } from '../../models/ShareLink';
6
+ import type { Neo4jQueryResult, ShareLinkTransaction } from './transaction';
7
+ import type {
8
+ ShareContentCleanupJobRecord,
9
+ ShareLinkOperationRecord,
10
+ ShareLinkOperationStatus,
11
+ ShareLinkReservationRecord,
12
+ } from './types';
13
+
14
+ type Neo4jNodeLike = {
15
+ properties?: Record<string, unknown>;
16
+ };
17
+
18
+ /** Converts a neo4j-driver Integer (or nested array) to plain JS values. */
19
+ export const normalizeNeo4jValue = (value: unknown): unknown => {
20
+ if (value === undefined || value === null) return null;
21
+
22
+ if (typeof value === 'object') {
23
+ const maybeInteger = value as { toNumber?: () => number };
24
+
25
+ if (typeof maybeInteger.toNumber === 'function') {
26
+ return maybeInteger.toNumber();
27
+ }
28
+ }
29
+
30
+ if (Array.isArray(value)) return value.map(normalizeNeo4jValue);
31
+
32
+ return value;
33
+ };
34
+
35
+ export const normalizeProperties = (props: Record<string, unknown>): Record<string, unknown> =>
36
+ Object.fromEntries(
37
+ Object.entries(props).map(([key, value]) => [key, normalizeNeo4jValue(value)])
38
+ );
39
+
40
+ /** Reads `rowIndex`'s node under `key` and returns normalized properties, or null. */
41
+ export const readNodeProperties = (
42
+ result: Neo4jQueryResult,
43
+ key: string,
44
+ rowIndex = 0
45
+ ): Record<string, unknown> | null => {
46
+ const value = result.records[rowIndex]?.get(key);
47
+
48
+ if (!value || typeof value !== 'object') return null;
49
+
50
+ const props = (value as Neo4jNodeLike).properties;
51
+
52
+ return props ? normalizeProperties(props) : null;
53
+ };
54
+
55
+ const SHARE_LINK_STATUSES: readonly ShareLinkStatus[] = ['pending', 'active', 'stopped'];
56
+ const SHARE_CONTENT_STATES: readonly ShareContentState[] = [
57
+ 'staging',
58
+ 'finalized',
59
+ 'content_missing',
60
+ ];
61
+ const OPERATION_KINDS: readonly ShareLinkOperationKind[] = ['create', 'update', 'revoke'];
62
+ const OPERATION_STATUSES: readonly ShareLinkOperationStatus[] = [
63
+ 'in_progress',
64
+ 'committed',
65
+ 'abandoned',
66
+ ];
67
+ const CLEANUP_REASONS = ['superseded', 'abandoned', 'stopped'] as const;
68
+ const CLEANUP_STATUSES = ['queued', 'claimed', 'completed'] as const;
69
+
70
+ const isOneOf = <T extends string>(value: unknown, allowed: readonly T[]): value is T =>
71
+ typeof value === 'string' && (allowed as readonly string[]).includes(value);
72
+
73
+ const asString = (value: unknown, fallback = ''): string =>
74
+ typeof value === 'string' ? value : fallback;
75
+
76
+ const asNullableString = (value: unknown): string | null =>
77
+ typeof value === 'string' ? value : null;
78
+
79
+ const asNumber = (value: unknown, fallback: number): number =>
80
+ typeof value === 'number' && Number.isFinite(value) ? value : fallback;
81
+
82
+ const asNullableNumber = (value: unknown): number | null =>
83
+ typeof value === 'number' && Number.isFinite(value) ? value : null;
84
+
85
+ const asBoolean = (value: unknown, fallback: boolean): boolean =>
86
+ typeof value === 'boolean' ? value : fallback;
87
+
88
+ const asNullableBoolean = (value: unknown): boolean | null =>
89
+ typeof value === 'boolean' ? value : null;
90
+
91
+ export const toShareLinkRecord = (props: Record<string, unknown>): ShareLinkRecord => ({
92
+ id: asString(props.id),
93
+ namespace: asString(props.namespace),
94
+ ownerProfileId: asString(props.ownerProfileId),
95
+ version: asNumber(props.version, 1),
96
+ contentVersion: asNumber(props.contentVersion, 1),
97
+ generation: asNumber(props.generation, 1),
98
+ status: isOneOf(props.status, SHARE_LINK_STATUSES) ? props.status : 'pending',
99
+ contentState: isOneOf(props.contentState, SHARE_CONTENT_STATES)
100
+ ? props.contentState
101
+ : 'staging',
102
+ activeObjectRef: asNullableString(props.activeObjectRef),
103
+ activeObjectOperationId: asNullableString(props.activeObjectOperationId),
104
+ activeContentHash: asNullableString(props.activeContentHash),
105
+ activeContentBytes: asNullableNumber(props.activeContentBytes),
106
+ activeRecoveryHash: asNullableString(props.activeRecoveryHash),
107
+ lastOperationId: asNullableString(props.lastOperationId),
108
+ createdByClientRequestId: asNullableString(props.createdByClientRequestId),
109
+ title: asString(props.title),
110
+ note: asNullableString(props.note),
111
+ selectedCount: asNumber(props.selectedCount, 1),
112
+ expiresAt: asNullableString(props.expiresAt),
113
+ stoppedAt: asNullableString(props.stoppedAt),
114
+ viewCount: asNumber(props.viewCount, 0),
115
+ lastViewedAt: asNullableString(props.lastViewedAt),
116
+ passcodeHash: asNullableString(props.passcodeHash),
117
+ notifyOnView: asBoolean(props.notifyOnView, false),
118
+ minorPolicyIsMinor: asNullableBoolean(props.minorPolicyIsMinor),
119
+ minorPolicyResolved: asBoolean(props.minorPolicyResolved, false),
120
+ minorPolicyDefaultExpiryDays: asNumber(props.minorPolicyDefaultExpiryDays, 30),
121
+ minorPolicyViewCountingEnabled: asBoolean(props.minorPolicyViewCountingEnabled, false),
122
+ createdAt: asString(props.createdAt),
123
+ updatedAt: asString(props.updatedAt),
124
+ });
125
+
126
+ export const toShareLinkReservationRecord = (
127
+ props: Record<string, unknown>
128
+ ): ShareLinkReservationRecord => ({
129
+ shareId: asString(props.shareId),
130
+ namespace: asString(props.namespace),
131
+ ownerProfileId: asString(props.ownerProfileId),
132
+ opKind: isOneOf(props.opKind, OPERATION_KINDS) ? props.opKind : 'create',
133
+ clientRequestId: asString(props.clientRequestId),
134
+ requestHash: asString(props.requestHash),
135
+ operationId: asString(props.operationId),
136
+ objectRef: asNullableString(props.objectRef),
137
+ contentVersion: asNullableNumber(props.contentVersion),
138
+ baseVersion: asNumber(props.baseVersion, 1),
139
+ baseContentVersion: asNumber(props.baseContentVersion, 1),
140
+ contentHash: asNullableString(props.contentHash),
141
+ contentBytes: asNullableNumber(props.contentBytes),
142
+ recoveryHash: asNullableString(props.recoveryHash),
143
+ recoveryBytes: asNullableNumber(props.recoveryBytes),
144
+ title: asString(props.title),
145
+ note: asNullableString(props.note),
146
+ expiresAt: asNullableString(props.expiresAt),
147
+ selectedCount: asNumber(props.selectedCount, 1),
148
+ passcodeHash: asNullableString(props.passcodeHash),
149
+ notifyOnView: asBoolean(props.notifyOnView, false),
150
+ policy: {
151
+ isMinor: asNullableBoolean(props.policyIsMinor),
152
+ policyResolved: asBoolean(props.policyResolved, false),
153
+ defaultExpiryDays: asNumber(props.policyDefaultExpiryDays, 30) === 365 ? 365 : 30,
154
+ viewCountingEnabled: asBoolean(props.policyViewCountingEnabled, false),
155
+ },
156
+ generation: asNumber(props.generation, 1),
157
+ leaseOwner: asString(props.leaseOwner),
158
+ leaseExpiresAt: asString(props.leaseExpiresAt),
159
+ createdAt: asString(props.createdAt),
160
+ updatedAt: asString(props.updatedAt),
161
+ });
162
+
163
+ export const toShareLinkOperationRecord = (
164
+ props: Record<string, unknown>
165
+ ): ShareLinkOperationRecord => ({
166
+ namespace: asString(props.namespace),
167
+ ownerProfileId: asString(props.ownerProfileId),
168
+ opKind: isOneOf(props.opKind, OPERATION_KINDS) ? props.opKind : 'create',
169
+ clientRequestId: asString(props.clientRequestId),
170
+ operationId: asString(props.operationId),
171
+ shareId: asString(props.shareId),
172
+ requestHash: asString(props.requestHash),
173
+ status: isOneOf(props.status, OPERATION_STATUSES) ? props.status : 'in_progress',
174
+ resultJson: asNullableString(props.resultJson),
175
+ resultVersion: asNullableNumber(props.resultVersion),
176
+ createdAt: asString(props.createdAt),
177
+ updatedAt: asString(props.updatedAt),
178
+ pruneAfter: asString(props.pruneAfter),
179
+ });
180
+
181
+ export const toShareContentCleanupJobRecord = (
182
+ props: Record<string, unknown>
183
+ ): ShareContentCleanupJobRecord => ({
184
+ objectRef: asString(props.objectRef),
185
+ operationId: asString(props.operationId),
186
+ namespace: asString(props.namespace),
187
+ ownerProfileId: asString(props.ownerProfileId),
188
+ shareId: asString(props.shareId),
189
+ contentVersion: asNumber(props.contentVersion, 0),
190
+ reason: isOneOf(props.reason, CLEANUP_REASONS) ? props.reason : 'superseded',
191
+ status: isOneOf(props.status, CLEANUP_STATUSES) ? props.status : 'queued',
192
+ attempts: asNumber(props.attempts, 0),
193
+ nextAttemptAt: asString(props.nextAttemptAt),
194
+ claimToken: asNullableString(props.claimToken),
195
+ claimedBy: asNullableString(props.claimedBy),
196
+ claimExpiresAt: asNullableString(props.claimExpiresAt),
197
+ lastError: asNullableString(props.lastError),
198
+ createdAt: asString(props.createdAt),
199
+ updatedAt: asString(props.updatedAt),
200
+ completedAt: asNullableString(props.completedAt),
201
+ });
202
+
203
+ /** 32-byte opaque immutable LearnCloud object id (43-char base64url). */
204
+ export const generateObjectRef = (): string => randomBytes(32).toString('base64url');
205
+
206
+ /** Opaque, unique operation id used to bind a reservation to a worker's finalize. */
207
+ export const generateOperationId = (): string => randomUUID();
208
+
209
+ /** Opaque cleanup claim fence; a stale claimant can no longer complete the job. */
210
+ export const generateClaimToken = (): string => randomUUID();
211
+
212
+ export type ShareLinkOperationKey = {
213
+ namespace: string;
214
+ ownerProfileId: string;
215
+ opKind: ShareLinkOperationKind;
216
+ clientRequestId: string;
217
+ };
218
+
219
+ /**
220
+ * Acquires the share write lock.
221
+ *
222
+ * The property write is deliberate: once it commits, every other lifecycle
223
+ * transaction that targets this share blocks until this transaction ends, so a
224
+ * caller always reads status/version/reservation state under the lock.
225
+ */
226
+ export const lockShare = async (
227
+ tx: ShareLinkTransaction,
228
+ shareId: string
229
+ ): Promise<Record<string, unknown> | null> => {
230
+ const result = await tx.run(
231
+ `MATCH (s:ShareLink {id: $shareId})
232
+ SET s.lockTick = coalesce(s.lockTick, 0) + 1
233
+ RETURN s`,
234
+ { shareId }
235
+ );
236
+
237
+ return readNodeProperties(result, 's');
238
+ };
239
+
240
+ export const readShareById = async (
241
+ tx: ShareLinkTransaction,
242
+ shareId: string
243
+ ): Promise<Record<string, unknown> | null> => {
244
+ const result = await tx.run('MATCH (s:ShareLink {id: $shareId}) RETURN s LIMIT 1', {
245
+ shareId,
246
+ });
247
+
248
+ return readNodeProperties(result, 's');
249
+ };
250
+
251
+ export const readReservation = async (
252
+ tx: ShareLinkTransaction,
253
+ shareId: string
254
+ ): Promise<Record<string, unknown> | null> => {
255
+ const result = await tx.run(
256
+ 'MATCH (r:ShareLinkReservation {shareId: $shareId}) RETURN r LIMIT 1',
257
+ { shareId }
258
+ );
259
+
260
+ return readNodeProperties(result, 'r');
261
+ };
262
+
263
+ export const readOperation = async (
264
+ tx: ShareLinkTransaction,
265
+ key: ShareLinkOperationKey
266
+ ): Promise<Record<string, unknown> | null> => {
267
+ const result = await tx.run(
268
+ `MATCH (o:ShareLinkOperation {
269
+ namespace: $namespace,
270
+ ownerProfileId: $ownerProfileId,
271
+ opKind: $opKind,
272
+ clientRequestId: $clientRequestId
273
+ })
274
+ RETURN o LIMIT 1`,
275
+ key
276
+ );
277
+
278
+ return readNodeProperties(result, 'o');
279
+ };
280
+
281
+ export const readOperationByOperationId = async (
282
+ tx: ShareLinkTransaction,
283
+ operationId: string
284
+ ): Promise<Record<string, unknown> | null> => {
285
+ const result = await tx.run(
286
+ 'MATCH (o:ShareLinkOperation {operationId: $operationId}) RETURN o LIMIT 1',
287
+ { operationId }
288
+ );
289
+
290
+ return readNodeProperties(result, 'o');
291
+ };
292
+
293
+ export type WriteOperationInput = ShareLinkOperationKey & {
294
+ operationId: string;
295
+ shareId: string;
296
+ requestHash: string;
297
+ now: string;
298
+ pruneAfter: string;
299
+ };
300
+
301
+ /**
302
+ * Creates or re-arms the idempotency record for an in-flight operation.
303
+ *
304
+ * The caller has already validated the request hash for an existing record, so
305
+ * overwriting the stored hash here is only a re-drive of an abandoned record.
306
+ */
307
+ export const writeOperationInProgress = async (
308
+ tx: ShareLinkTransaction,
309
+ input: WriteOperationInput
310
+ ): Promise<void> => {
311
+ await tx.run(
312
+ `MERGE (o:ShareLinkOperation {
313
+ namespace: $namespace,
314
+ ownerProfileId: $ownerProfileId,
315
+ opKind: $opKind,
316
+ clientRequestId: $clientRequestId
317
+ })
318
+ ON CREATE SET o.createdAt = $now, o.pruneAfter = $pruneAfter
319
+ SET o.operationId = $operationId,
320
+ o.shareId = $shareId,
321
+ o.requestHash = $requestHash,
322
+ o.status = 'in_progress',
323
+ o.resultJson = null,
324
+ o.resultVersion = null,
325
+ o.updatedAt = $now`,
326
+ input
327
+ );
328
+ };
329
+
330
+ export const markOperationCommitted = async (
331
+ tx: ShareLinkTransaction,
332
+ input: ShareLinkOperationKey & {
333
+ operationId: string;
334
+ resultJson: string;
335
+ resultVersion: number;
336
+ now: string;
337
+ pruneAfter: string;
338
+ }
339
+ ): Promise<void> => {
340
+ await tx.run(
341
+ `MATCH (o:ShareLinkOperation {
342
+ namespace: $namespace,
343
+ ownerProfileId: $ownerProfileId,
344
+ opKind: $opKind,
345
+ clientRequestId: $clientRequestId
346
+ })
347
+ SET o.operationId = $operationId,
348
+ o.status = 'committed',
349
+ o.resultJson = $resultJson,
350
+ o.resultVersion = $resultVersion,
351
+ o.updatedAt = $now,
352
+ o.pruneAfter = $pruneAfter`,
353
+ input
354
+ );
355
+ };
356
+
357
+ export const markOperationAbandoned = async (
358
+ tx: ShareLinkTransaction,
359
+ key: ShareLinkOperationKey & { now: string }
360
+ ): Promise<void> => {
361
+ await tx.run(
362
+ `MATCH (o:ShareLinkOperation {
363
+ namespace: $namespace,
364
+ ownerProfileId: $ownerProfileId,
365
+ opKind: $opKind,
366
+ clientRequestId: $clientRequestId
367
+ })
368
+ SET o.status = 'abandoned', o.updatedAt = $now`,
369
+ key
370
+ );
371
+ };
372
+
373
+ export const deleteReservation = async (
374
+ tx: ShareLinkTransaction,
375
+ shareId: string
376
+ ): Promise<void> => {
377
+ await tx.run('MATCH (r:ShareLinkReservation {shareId: $shareId}) DELETE r', { shareId });
378
+ };
379
+
380
+ export type EnqueueCleanupJobInput = {
381
+ objectRef: string;
382
+ operationId: string;
383
+ namespace: string;
384
+ ownerProfileId: string;
385
+ shareId: string;
386
+ contentVersion: number;
387
+ reason: 'superseded' | 'abandoned' | 'stopped';
388
+ now: string;
389
+ };
390
+
391
+ /**
392
+ * Durable, exact-object cleanup enqueue. `MERGE` on the unique `objectRef` makes
393
+ * a retry idempotent; the repository never deletes LearnCloud content itself.
394
+ */
395
+ export const enqueueCleanupJob = async (
396
+ tx: ShareLinkTransaction,
397
+ input: EnqueueCleanupJobInput
398
+ ): Promise<void> => {
399
+ await tx.run(
400
+ `MERGE (c:ShareContentCleanupJob {objectRef: $objectRef})
401
+ ON CREATE SET c.namespace = $namespace,
402
+ c.ownerProfileId = $ownerProfileId,
403
+ c.shareId = $shareId,
404
+ c.contentVersion = $contentVersion,
405
+ c.operationId = $operationId,
406
+ c.reason = $reason,
407
+ c.status = 'queued',
408
+ c.attempts = 0,
409
+ c.nextAttemptAt = $now,
410
+ c.claimToken = null,
411
+ c.claimedBy = null,
412
+ c.claimExpiresAt = null,
413
+ c.lastError = null,
414
+ c.completedAt = null,
415
+ c.createdAt = $now,
416
+ c.updatedAt = $now`,
417
+ input
418
+ );
419
+ };
@@ -0,0 +1,11 @@
1
+ export * from './types';
2
+ export * from './errors';
3
+ export * from './reserve';
4
+ export * from './finalize';
5
+ export * from './abandon';
6
+ export * from './revoke';
7
+ export * from './read';
8
+ export * from './list';
9
+ export * from './cleanup';
10
+ export * from './recovery';
11
+ export * from './receipt';
@@ -0,0 +1,162 @@
1
+ import { SHARE_LINK_ID_BYTES, isCanonicalBase64Url } from '@learncard/types';
2
+
3
+ import { neogma } from '@instance';
4
+
5
+ import { readNodeProperties, toShareLinkRecord } from './helpers';
6
+ import type { ListShareLinksInput, ListShareLinksResult, ShareLinkListCursor } from './types';
7
+ import type { ShareLinkRecord } from '../../models/ShareLink';
8
+
9
+ /**
10
+ * LC-2187 bounded owner-list repository.
11
+ *
12
+ * Contract:
13
+ * - Every query is scoped by the trusted `namespace` AND `ownerProfileId`; the
14
+ * caller can never widen either through the cursor or any other field.
15
+ * - Keyset pagination over the immutable `(createdAt, id)` key. `createdAt` is
16
+ * never mutated for a share and `id` is globally unique, so the total order is
17
+ * stable and equal-timestamp records neither duplicate nor skip across pages.
18
+ * There is no OFFSET and no unbounded fetch/sort in memory.
19
+ * - The cursor is a canonical, versioned encoding of exactly the two ordering
20
+ * fields. It is an ORDERING HINT only: it never carries namespace, owner,
21
+ * status or a query clause, and the current authenticated scope is always
22
+ * applied independently. A cursor minted for another owner therefore cannot
23
+ * reveal that owner's records; it can only advance the caller's own page.
24
+ * - `limit + 1` rows are fetched so `hasMore`/`nextCursor` are decided without a
25
+ * count query. Limits are defensively bounded again here.
26
+ * - Pending (`staging`), active, retained-expired and stopped records are all
27
+ * listed by owner. Listing is read-only: it never triggers cleanup, never
28
+ * contacts public content, and never mutates a share.
29
+ *
30
+ * Concurrent insert/delete is deliberately not a snapshot: a page reflects the
31
+ * graph at read time, so a new share may appear and a deleted share may vanish
32
+ * between pages. Only the immutable ordering key is guaranteed stable.
33
+ */
34
+
35
+ const CURSOR_VERSION = 1;
36
+ const CURSOR_PREFIX = `v${CURSOR_VERSION}.`;
37
+ const MAX_CURSOR_LENGTH = 512;
38
+ const MAX_LIST_LIMIT = 50;
39
+ const DEFAULT_LIST_LIMIT = 25;
40
+
41
+ const isCanonicalIsoInstant = (value: string): boolean => {
42
+ const parsed = Date.parse(value);
43
+
44
+ if (!Number.isFinite(parsed)) return false;
45
+
46
+ try {
47
+ return new Date(parsed).toISOString() === value;
48
+ } catch {
49
+ return false;
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Canonical, versioned cursor over exactly `(createdAt, id)`.
55
+ *
56
+ * Encoding: `v1.` + base64url(UTF-8 JSON array `[createdAt, id]`). The version
57
+ * prefix lets a future encoding be rejected rather than mis-parsed, and the
58
+ * ordered-fields-only shape guarantees no database node, extra clause or private
59
+ * field can be smuggled through the cursor.
60
+ */
61
+ export const encodeShareLinkListCursor = (cursor: ShareLinkListCursor): string =>
62
+ `${CURSOR_PREFIX}${Buffer.from(JSON.stringify([cursor.createdAt, cursor.id]), 'utf8').toString(
63
+ 'base64url'
64
+ )}`;
65
+
66
+ /**
67
+ * Strictly decodes a caller cursor. Returns `null` for anything malformed,
68
+ * oversized, non-canonical or impossible so the route can answer a fixed safe
69
+ * BAD_REQUEST without touching the graph.
70
+ */
71
+ export const decodeShareLinkListCursor = (raw: unknown): ShareLinkListCursor | null => {
72
+ if (typeof raw !== 'string') return null;
73
+ if (raw.length === 0 || raw.length > MAX_CURSOR_LENGTH) return null;
74
+ if (!raw.startsWith(CURSOR_PREFIX)) return null;
75
+
76
+ const encoded = raw.slice(CURSOR_PREFIX.length);
77
+
78
+ if (encoded.length === 0) return null;
79
+
80
+ let json: string;
81
+ try {
82
+ const bytes = Buffer.from(encoded, 'base64url');
83
+
84
+ // Reject non-canonical base64url (padding/unused-bit variants) so one
85
+ // logical cursor has exactly one spelling.
86
+ if (bytes.toString('base64url') !== encoded) return null;
87
+
88
+ json = bytes.toString('utf8');
89
+ } catch {
90
+ return null;
91
+ }
92
+
93
+ let parsed: unknown;
94
+ try {
95
+ parsed = JSON.parse(json);
96
+ } catch {
97
+ return null;
98
+ }
99
+
100
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
101
+
102
+ const [createdAt, id] = parsed;
103
+
104
+ if (typeof createdAt !== 'string' || !isCanonicalIsoInstant(createdAt)) return null;
105
+ if (!isCanonicalBase64Url(id, SHARE_LINK_ID_BYTES)) return null;
106
+
107
+ return { createdAt, id };
108
+ };
109
+
110
+ const boundLimit = (limit: number): number => {
111
+ if (!Number.isFinite(limit)) return DEFAULT_LIST_LIMIT;
112
+
113
+ return Math.min(Math.max(Math.trunc(limit), 1), MAX_LIST_LIMIT);
114
+ };
115
+
116
+ /**
117
+ * Reads one bounded page of the caller's own shares, newest first. A decoded
118
+ * cursor is the only accepted cursor form; the route decodes/validates the raw
119
+ * caller string before this point.
120
+ */
121
+ export const listShareLinks = async (input: ListShareLinksInput): Promise<ListShareLinksResult> => {
122
+ const limit = boundLimit(input.limit);
123
+ const cursor = input.cursor ?? null;
124
+
125
+ const result = await neogma.queryRunner.run(
126
+ `MATCH (s:ShareLink {namespace: $namespace, ownerProfileId: $ownerProfileId})
127
+ WHERE $cursorCreatedAt IS NULL
128
+ OR s.createdAt < $cursorCreatedAt
129
+ OR (s.createdAt = $cursorCreatedAt AND s.id < $cursorId)
130
+ RETURN s
131
+ ORDER BY s.createdAt DESC, s.id DESC
132
+ LIMIT toInteger($limit)`,
133
+ {
134
+ namespace: input.namespace,
135
+ ownerProfileId: input.ownerProfileId,
136
+ cursorCreatedAt: cursor?.createdAt ?? null,
137
+ cursorId: cursor?.id ?? null,
138
+ limit: limit + 1,
139
+ }
140
+ );
141
+
142
+ const fetched: ShareLinkRecord[] = [];
143
+
144
+ for (let index = 0; index < result.records.length; index += 1) {
145
+ const props = readNodeProperties(result, 's', index);
146
+
147
+ if (props) fetched.push(toShareLinkRecord(props));
148
+ }
149
+
150
+ const hasMore = fetched.length > limit;
151
+ const records = hasMore ? fetched.slice(0, limit) : fetched;
152
+ const last = records[records.length - 1];
153
+
154
+ return {
155
+ records,
156
+ hasMore,
157
+ nextCursor:
158
+ hasMore && last
159
+ ? encodeShareLinkListCursor({ createdAt: last.createdAt, id: last.id })
160
+ : null,
161
+ };
162
+ };
@@ -0,0 +1,107 @@
1
+ import { neogma } from '@instance';
2
+
3
+ import { readNodeProperties, toShareLinkRecord } from './helpers';
4
+ import type {
5
+ CurrentShareContentResult,
6
+ GetCurrentShareContentInput,
7
+ GetShareLinkInput,
8
+ } from './types';
9
+ import type { ShareLinkRecord } from '../../models/ShareLink';
10
+
11
+ /**
12
+ * Reads one committed share snapshot by opaque id, optionally constrained to the
13
+ * caller's namespace and owner. Cross-namespace/cross-owner reads return null so a
14
+ * route can answer without an existence oracle.
15
+ */
16
+ export const getShareLink = async (input: GetShareLinkInput): Promise<ShareLinkRecord | null> => {
17
+ const result = await neogma.queryRunner.run(
18
+ `MATCH (s:ShareLink {id: $shareId})
19
+ WHERE ($namespace IS NULL OR s.namespace = $namespace)
20
+ AND ($ownerProfileId IS NULL OR s.ownerProfileId = $ownerProfileId)
21
+ RETURN s LIMIT 1`,
22
+ {
23
+ shareId: input.shareId,
24
+ namespace: input.namespace ?? null,
25
+ ownerProfileId: input.ownerProfileId ?? null,
26
+ }
27
+ );
28
+
29
+ const props = readNodeProperties(result, 's');
30
+
31
+ return props ? toShareLinkRecord(props) : null;
32
+ };
33
+
34
+ /**
35
+ * Returns the single committed content reference that may be served, or a typed
36
+ * not-active/missing state.
37
+ *
38
+ * The expiry check is synchronous and read-only: an expired share is reported
39
+ * `not_active` while its bytes are retained, so a metadata-only expiry extension
40
+ * can later make it serve again. This method never deletes content and never
41
+ * mutates the graph — it is the read primitive a later Brain proxy route can use.
42
+ */
43
+ export const getCurrentShareContent = async (
44
+ input: GetCurrentShareContentInput
45
+ ): Promise<CurrentShareContentResult> => {
46
+ const share = await getShareLink(input);
47
+
48
+ if (!share) return { state: 'missing' };
49
+
50
+ const base = {
51
+ shareId: share.id,
52
+ namespace: share.namespace,
53
+ ownerProfileId: share.ownerProfileId,
54
+ };
55
+
56
+ if (share.status === 'stopped') {
57
+ return {
58
+ state: 'not_active',
59
+ ...base,
60
+ status: share.status,
61
+ contentState: share.contentState,
62
+ reason: 'stopped',
63
+ };
64
+ }
65
+
66
+ if (
67
+ share.status !== 'active' ||
68
+ share.contentState !== 'finalized' ||
69
+ !share.activeObjectRef ||
70
+ !share.activeObjectOperationId
71
+ ) {
72
+ return {
73
+ state: 'not_active',
74
+ ...base,
75
+ status: share.status,
76
+ contentState: share.contentState,
77
+ reason: share.contentState === 'content_missing' ? 'content_missing' : 'staging',
78
+ };
79
+ }
80
+
81
+ const now = input.now ?? new Date();
82
+
83
+ if (!input.allowExpired && share.expiresAt && Date.parse(share.expiresAt) <= now.getTime()) {
84
+ return {
85
+ state: 'not_active',
86
+ ...base,
87
+ status: share.status,
88
+ contentState: share.contentState,
89
+ reason: 'expired',
90
+ };
91
+ }
92
+
93
+ return {
94
+ state: 'active',
95
+ ...base,
96
+ version: share.version,
97
+ contentVersion: share.contentVersion,
98
+ objectRef: share.activeObjectRef,
99
+ operationId: share.activeObjectOperationId,
100
+ contentHash: share.activeContentHash,
101
+ contentBytes: share.activeContentBytes,
102
+ recoveryHash: share.activeRecoveryHash,
103
+ selectedCount: share.selectedCount,
104
+ expiresAt: share.expiresAt,
105
+ updatedAt: share.updatedAt,
106
+ };
107
+ };