@learncard/network-brain-service 3.19.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/.env.example +47 -82
  2. package/AGENTS.md +52 -0
  3. package/CHANGELOG.md +147 -0
  4. package/README.md +51 -2
  5. package/bootstrapLambda.test.ts +72 -0
  6. package/config/config.dev.json +29 -0
  7. package/config/config.example.json +29 -0
  8. package/config/config.json +1 -0
  9. package/config/config.production.json +29 -0
  10. package/config/config.scouts.dev.json +24 -0
  11. package/config/config.scouts.production.json +24 -0
  12. package/contractEventsLambda.ts +18 -0
  13. package/contractEventsLambdaApp.ts +19 -0
  14. package/didWebLambda.ts +13 -18
  15. package/didWebLambdaApp.ts +22 -0
  16. package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
  17. package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
  18. package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
  19. package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
  20. package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
  21. package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
  22. package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
  23. package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
  24. package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
  25. package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
  26. package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
  27. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
  29. package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
  30. package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
  31. package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
  32. package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
  33. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  34. package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
  35. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  36. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
  37. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  38. package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
  39. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
  40. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  41. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
  42. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
  43. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
  44. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
  45. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  47. package/dist/accesslayer/credential/create.d.ts +2 -1
  48. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  49. package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
  50. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  51. package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
  52. package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
  53. package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
  54. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  55. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  56. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  57. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  58. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  59. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  60. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  61. package/dist/accesslayer/share-link/attachment.d.ts +37 -0
  62. package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
  63. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  64. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  65. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  66. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  67. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  68. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  69. package/dist/accesslayer/share-link/helpers.d.ts +59 -0
  70. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  71. package/dist/accesslayer/share-link/index.d.ts +12 -0
  72. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  73. package/dist/accesslayer/share-link/list.d.ts +5 -0
  74. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  75. package/dist/accesslayer/share-link/read.d.ts +5 -0
  76. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  77. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  78. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  79. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  80. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  81. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  82. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  83. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  84. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  85. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  86. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  87. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  88. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  89. package/dist/accesslayer/share-link/types.d.ts +375 -0
  90. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  91. package/dist/app.d.ts +4 -0
  92. package/dist/app.d.ts.map +1 -1
  93. package/dist/cache/app-store.caches.d.ts +49 -0
  94. package/dist/cache/app-store.caches.d.ts.map +1 -1
  95. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  96. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  97. package/dist/config/environment.d.ts +14 -0
  98. package/dist/config/environment.d.ts.map +1 -1
  99. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  100. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  101. package/dist/config/stageConfig.d.ts +112 -0
  102. package/dist/config/stageConfig.d.ts.map +1 -0
  103. package/dist/config/stageConfig.test.d.ts +2 -0
  104. package/dist/config/stageConfig.test.d.ts.map +1 -0
  105. package/dist/constants/auth-grant.d.ts +2 -0
  106. package/dist/constants/auth-grant.d.ts.map +1 -1
  107. package/dist/didkit_wasm_bg.wasm +0 -0
  108. package/dist/docker-entry.d.ts +1 -1
  109. package/dist/docker-entry.d.ts.map +1 -1
  110. package/dist/helpers/app-manifest.helpers.d.ts +5 -0
  111. package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
  112. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  113. package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
  114. package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
  115. package/dist/helpers/consent-referral.helpers.d.ts +2 -0
  116. package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
  117. package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
  118. package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
  119. package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
  120. package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
  121. package/dist/helpers/contract-events.helpers.d.ts +25 -0
  122. package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
  123. package/dist/helpers/contract-requests.helpers.d.ts +19 -0
  124. package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
  125. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  126. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  127. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  128. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  129. package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
  130. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  131. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  132. package/dist/helpers/inline-template.helpers.d.ts +45 -0
  133. package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
  134. package/dist/helpers/notificationMessages.d.ts +1 -1
  135. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  136. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  137. package/dist/helpers/notifications.helpers.d.ts +6 -0
  138. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  139. package/dist/helpers/presentation-metadata.d.ts +4 -0
  140. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  141. package/dist/helpers/presentation.helpers.d.ts +1 -1
  142. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  143. package/dist/helpers/profile.helpers.d.ts +2 -0
  144. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  145. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  146. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  147. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  148. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  149. package/dist/helpers/share-content-client/client.d.ts +5 -0
  150. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  151. package/dist/helpers/share-content-client/config.d.ts +11 -0
  152. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  153. package/dist/helpers/share-content-client/index.d.ts +6 -0
  154. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  155. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  156. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  157. package/dist/helpers/share-content-client/types.d.ts +145 -0
  158. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  159. package/dist/helpers/share-link-attachment-access.d.ts +4 -0
  160. package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
  161. package/dist/helpers/share-link-attachment.d.ts +18 -0
  162. package/dist/helpers/share-link-attachment.d.ts.map +1 -0
  163. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
  164. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
  165. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  166. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  167. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  168. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  169. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  170. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  171. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  172. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  173. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  174. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  175. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  176. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  177. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  178. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  179. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  180. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  181. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  182. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  183. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  184. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  185. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  186. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  187. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  188. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  189. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  190. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  191. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  192. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  193. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  194. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  195. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  196. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  197. package/dist/helpers/share-link-maintenance/runner.d.ts +20 -0
  198. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  199. package/dist/helpers/share-link-maintenance/runtime.d.ts +38 -0
  200. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  201. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  202. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  203. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  204. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  205. package/dist/helpers/share-link-maintenance/types.d.ts +56 -0
  206. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  207. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  208. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  209. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  210. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  211. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  212. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  213. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  214. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  215. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  216. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  217. package/dist/helpers/share-link-passcode.d.ts +6 -0
  218. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  219. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  220. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  221. package/dist/helpers/share-link-policy/production.d.ts +11 -0
  222. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  223. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  224. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  225. package/dist/helpers/share-link-policy/types.d.ts +22 -0
  226. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  227. package/dist/helpers/share-link-public/config.d.ts +13 -0
  228. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  229. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  230. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  231. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  232. package/dist/helpers/smartResume.helpers.d.ts +3 -0
  233. package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
  234. package/dist/index.js +2 -2
  235. package/dist/models/AppManifestVersion.d.ts +20 -0
  236. package/dist/models/AppManifestVersion.d.ts.map +1 -0
  237. package/dist/models/AppStoreListing.d.ts +13 -0
  238. package/dist/models/AppStoreListing.d.ts.map +1 -1
  239. package/dist/models/ConsentFlowContract.d.ts +13 -2
  240. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  241. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  242. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  243. package/dist/models/Integration.d.ts +2 -0
  244. package/dist/models/Integration.d.ts.map +1 -1
  245. package/dist/models/Presentation.d.ts +2 -0
  246. package/dist/models/Presentation.d.ts.map +1 -1
  247. package/dist/models/Profile.d.ts +2 -0
  248. package/dist/models/Profile.d.ts.map +1 -1
  249. package/dist/models/ShareLink.d.ts +46 -0
  250. package/dist/models/ShareLink.d.ts.map +1 -0
  251. package/dist/models/ShareViewReceipt.d.ts +22 -0
  252. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  253. package/dist/models/index.d.ts +2 -0
  254. package/dist/models/index.d.ts.map +1 -1
  255. package/dist/models/share-link-constraints.d.ts +3 -0
  256. package/dist/models/share-link-constraints.d.ts.map +1 -0
  257. package/dist/openapi.d.ts +1 -1
  258. package/dist/openapi.d.ts.map +1 -1
  259. package/dist/public-share-link-http.d.ts +4 -0
  260. package/dist/public-share-link-http.d.ts.map +1 -0
  261. package/dist/routes/app-store.d.ts +619 -0
  262. package/dist/routes/app-store.d.ts.map +1 -1
  263. package/dist/routes/boosts.d.ts +2 -2
  264. package/dist/routes/boosts.d.ts.map +1 -1
  265. package/dist/routes/contracts.d.ts +148 -5
  266. package/dist/routes/contracts.d.ts.map +1 -1
  267. package/dist/routes/credentials.d.ts +35 -1
  268. package/dist/routes/credentials.d.ts.map +1 -1
  269. package/dist/routes/inbox.d.ts +52 -2
  270. package/dist/routes/inbox.d.ts.map +1 -1
  271. package/dist/routes/index.d.ts +153 -0
  272. package/dist/routes/index.d.ts.map +1 -1
  273. package/dist/routes/presentations.d.ts +1 -0
  274. package/dist/routes/presentations.d.ts.map +1 -1
  275. package/dist/routes/profile-manager.d.ts.map +1 -1
  276. package/dist/routes/profiles.d.ts.map +1 -1
  277. package/dist/routes/public-share-links.d.ts +285 -0
  278. package/dist/routes/public-share-links.d.ts.map +1 -0
  279. package/dist/routes/share-links.d.ts +1005 -0
  280. package/dist/routes/share-links.d.ts.map +1 -0
  281. package/dist/routes/test.d.ts +5 -0
  282. package/dist/routes/test.d.ts.map +1 -1
  283. package/dist/routes/workflows.d.ts.map +1 -1
  284. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  285. package/dist/types/app-manifest.d.ts +339 -0
  286. package/dist/types/app-manifest.d.ts.map +1 -0
  287. package/dist/types/app-store-listing.d.ts +34 -0
  288. package/dist/types/app-store-listing.d.ts.map +1 -1
  289. package/dist/types/consentflowcontract.d.ts +31 -4
  290. package/dist/types/consentflowcontract.d.ts.map +1 -1
  291. package/dist/types/credential-refresh.d.ts +1 -1
  292. package/esbuildPlugins.cjs +1 -0
  293. package/lambda.ts +49 -192
  294. package/lambdaApp.ts +203 -0
  295. package/package.json +25 -17
  296. package/scripts/export-openapi.ts +20 -0
  297. package/scripts/offline-schema-environment.ts +18 -0
  298. package/scripts/seed-demo-persona.ts +425 -0
  299. package/scripts/verify-python-contracts.ts +72 -0
  300. package/serverless-local.yml +3 -6
  301. package/serverless.function-env.cjs +50 -0
  302. package/serverless.local.yml +0 -1
  303. package/serverless.yml +44 -55
  304. package/shareLinkMaintenanceLambda.ts +18 -0
  305. package/shareLinkMaintenanceLambdaApp.ts +50 -0
  306. package/src/accesslayer/app-manifest-version/create.ts +57 -0
  307. package/src/accesslayer/app-manifest-version/read.ts +129 -0
  308. package/src/accesslayer/app-manifest-version/update.ts +61 -0
  309. package/src/accesslayer/app-store-listing/read.ts +6 -0
  310. package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
  311. package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
  312. package/src/accesslayer/app-store-listing/update.ts +15 -0
  313. package/src/accesslayer/boost/relationships/create.ts +8 -2
  314. package/src/accesslayer/consentflowcontract/create.ts +81 -8
  315. package/src/accesslayer/consentflowcontract/read.ts +84 -10
  316. package/src/accesslayer/consentflowcontract/relationships/create.ts +125 -109
  317. package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
  318. package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
  319. package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
  320. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
  321. package/src/accesslayer/consentflowcontract/relationships/update.ts +255 -217
  322. package/src/accesslayer/credential/create.test.ts +2 -115
  323. package/src/accesslayer/credential/create.ts +11 -6
  324. package/src/accesslayer/credential/relationships/create.ts +17 -10
  325. package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
  326. package/src/accesslayer/inbox-credential/update.ts +4 -2
  327. package/src/accesslayer/presentation/read.ts +57 -25
  328. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  329. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  330. package/src/accesslayer/share-link/abandon.ts +120 -0
  331. package/src/accesslayer/share-link/attachment.ts +360 -0
  332. package/src/accesslayer/share-link/cleanup.ts +271 -0
  333. package/src/accesslayer/share-link/errors.ts +32 -0
  334. package/src/accesslayer/share-link/finalize.ts +313 -0
  335. package/src/accesslayer/share-link/helpers.ts +450 -0
  336. package/src/accesslayer/share-link/index.ts +11 -0
  337. package/src/accesslayer/share-link/list.ts +162 -0
  338. package/src/accesslayer/share-link/read.ts +107 -0
  339. package/src/accesslayer/share-link/receipt.ts +548 -0
  340. package/src/accesslayer/share-link/recovery.ts +565 -0
  341. package/src/accesslayer/share-link/reserve.ts +763 -0
  342. package/src/accesslayer/share-link/revoke.ts +232 -0
  343. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  344. package/src/accesslayer/share-link/transaction.ts +126 -0
  345. package/src/accesslayer/share-link/types.ts +606 -0
  346. package/src/app.ts +6 -0
  347. package/src/cache/app-store.caches.ts +35 -0
  348. package/src/config/applyDockerStageConfig.ts +6 -0
  349. package/src/config/environment.test.ts +17 -12
  350. package/src/config/environment.ts +15 -0
  351. package/src/config/serverlessFunctionEnv.test.ts +105 -0
  352. package/src/config/stageConfig.test.ts +25 -0
  353. package/src/config/stageConfig.ts +24 -0
  354. package/src/constants/auth-grant.ts +5 -0
  355. package/src/docker-entry.ts +81 -0
  356. package/src/helpers/app-manifest.helpers.ts +185 -0
  357. package/src/helpers/boost.helpers.ts +4 -31
  358. package/src/helpers/claim-hooks.helpers.ts +20 -7
  359. package/src/helpers/consent-referral.helpers.ts +6 -0
  360. package/src/helpers/consent-scopes.helpers.ts +58 -0
  361. package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
  362. package/src/helpers/contract-events.helpers.ts +540 -0
  363. package/src/helpers/contract-requests.helpers.ts +211 -0
  364. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  365. package/src/helpers/credentialSubject.helpers.ts +40 -0
  366. package/src/helpers/finalize-inbox.helpers.ts +163 -44
  367. package/src/helpers/inbox.helpers.ts +58 -70
  368. package/src/helpers/inline-template.helpers.ts +560 -0
  369. package/src/helpers/notificationMessages.ts +77 -1
  370. package/src/helpers/notificationQueue.helpers.ts +23 -5
  371. package/src/helpers/notifications.helpers.ts +25 -1
  372. package/src/helpers/presentation-metadata.ts +41 -0
  373. package/src/helpers/presentation.helpers.ts +27 -20
  374. package/src/helpers/profile.helpers.ts +13 -0
  375. package/src/helpers/share-content-client/adapters.ts +55 -0
  376. package/src/helpers/share-content-client/canonical.ts +25 -0
  377. package/src/helpers/share-content-client/client.ts +440 -0
  378. package/src/helpers/share-content-client/config.ts +189 -0
  379. package/src/helpers/share-content-client/index.ts +27 -0
  380. package/src/helpers/share-content-client/responses.ts +273 -0
  381. package/src/helpers/share-content-client/types.ts +253 -0
  382. package/src/helpers/share-link-attachment-access.ts +87 -0
  383. package/src/helpers/share-link-attachment.ts +115 -0
  384. package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
  385. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  386. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  387. package/src/helpers/share-link-coordinator/coordinator.ts +727 -0
  388. package/src/helpers/share-link-coordinator/index.ts +8 -0
  389. package/src/helpers/share-link-coordinator/recovery-runner.ts +443 -0
  390. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  391. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  392. package/src/helpers/share-link-coordinator/types.ts +353 -0
  393. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  394. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  395. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  396. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  397. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  398. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  399. package/src/helpers/share-link-maintenance/config.ts +312 -0
  400. package/src/helpers/share-link-maintenance/index.ts +24 -0
  401. package/src/helpers/share-link-maintenance/runner.ts +251 -0
  402. package/src/helpers/share-link-maintenance/runtime.ts +340 -0
  403. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  404. package/src/helpers/share-link-maintenance/telemetry.ts +73 -0
  405. package/src/helpers/share-link-maintenance/types.ts +91 -0
  406. package/src/helpers/share-link-owner/config.ts +48 -0
  407. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  408. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  409. package/src/helpers/share-link-owner-projection.ts +54 -0
  410. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  411. package/src/helpers/share-link-passcode.ts +47 -0
  412. package/src/helpers/share-link-policy/index.ts +7 -0
  413. package/src/helpers/share-link-policy/production.ts +108 -0
  414. package/src/helpers/share-link-policy/resolver.ts +113 -0
  415. package/src/helpers/share-link-policy/types.ts +56 -0
  416. package/src/helpers/share-link-public/config.ts +41 -0
  417. package/src/helpers/share-link-view-notification.ts +18 -0
  418. package/src/helpers/signingAuthority.helpers.ts +23 -14
  419. package/src/helpers/smartResume.helpers.ts +123 -0
  420. package/src/models/AppManifestVersion.ts +43 -0
  421. package/src/models/AppStoreListing.ts +32 -0
  422. package/src/models/ConsentFlowContract.ts +34 -3
  423. package/src/models/ConsentFlowTerms.ts +10 -0
  424. package/src/models/ConsentFlowTransaction.ts +3 -0
  425. package/src/models/Integration.ts +2 -0
  426. package/src/models/Presentation.ts +2 -2
  427. package/src/models/Profile.ts +3 -2
  428. package/src/models/ShareLink.ts +134 -0
  429. package/src/models/ShareViewReceipt.ts +69 -0
  430. package/src/models/index.ts +25 -0
  431. package/src/models/share-link-constraints.ts +98 -0
  432. package/src/openapi.ts +44 -8
  433. package/src/public-share-link-http.ts +49 -0
  434. package/src/routes/app-store.ts +1056 -38
  435. package/src/routes/boosts.ts +14 -25
  436. package/src/routes/contracts.ts +384 -273
  437. package/src/routes/inbox.ts +4 -2
  438. package/src/routes/index.ts +142 -44
  439. package/src/routes/presentations.ts +19 -3
  440. package/src/routes/profile-manager.ts +29 -2
  441. package/src/routes/profiles.ts +29 -6
  442. package/src/routes/public-share-links.ts +983 -0
  443. package/src/routes/share-links.ts +709 -0
  444. package/src/routes/workflows.ts +3 -2
  445. package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
  446. package/src/types/app-manifest.ts +28 -0
  447. package/src/types/app-store-listing.ts +13 -0
  448. package/src/types/consentflowcontract.ts +25 -4
  449. package/test/app-manifest.helpers.spec.ts +170 -0
  450. package/test/app-manifest.routes.spec.ts +299 -0
  451. package/test/app-store-listing-updates.spec.ts +236 -0
  452. package/test/app-store-listing.spec.ts +65 -3
  453. package/test/consent-data-boundaries.spec.ts +352 -0
  454. package/test/consent-recipients.spec.ts +513 -0
  455. package/test/consent-scopes.helpers.spec.ts +67 -0
  456. package/test/consentflow.spec.ts +63 -2
  457. package/test/contract-requests.spec.ts +1588 -0
  458. package/test/credential-storage.neo4j.spec.ts +122 -0
  459. package/test/dids.spec.ts +10 -1
  460. package/test/guardian-gated-routes.spec.ts +49 -0
  461. package/test/helpers/share-link-fixtures.ts +94 -0
  462. package/test/inbox-security.e2e.ts +26 -10
  463. package/test/inbox.spec.ts +184 -8
  464. package/test/inline-template.helpers.spec.ts +49 -0
  465. package/test/managed-recipient-inbox.spec.ts +152 -0
  466. package/test/notificationMessages.spec.ts +13 -0
  467. package/test/openapi.spec.ts +14 -53
  468. package/test/presentations.spec.ts +54 -6
  469. package/test/profiles.spec.ts +97 -1
  470. package/test/share-content-client.contract.spec.ts +365 -0
  471. package/test/share-content-client.unit.spec.ts +462 -0
  472. package/test/share-link-abuse.unit.spec.ts +65 -0
  473. package/test/share-link-attachment.neo4j.spec.ts +262 -0
  474. package/test/share-link-attachment.unit.spec.ts +244 -0
  475. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  476. package/test/share-link-coordinator.unit.spec.ts +990 -0
  477. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  478. package/test/share-link-lifecycle.neo4j.spec.ts +1411 -0
  479. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  480. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  481. package/test/share-link-list.neo4j.spec.ts +324 -0
  482. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  483. package/test/share-link-maintenance.unit.spec.ts +1140 -0
  484. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  485. package/test/share-link-owner-route.unit.spec.ts +820 -0
  486. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  487. package/test/share-link-passcode.unit.spec.ts +29 -0
  488. package/test/share-link-policy.unit.spec.ts +149 -0
  489. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  490. package/test/share-link-production-policy.unit.spec.ts +126 -0
  491. package/test/share-link-public-config.unit.spec.ts +64 -0
  492. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  493. package/test/share-link-public-route.unit.spec.ts +1079 -0
  494. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  495. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  496. package/test/share-link-recovery.unit.spec.ts +726 -0
  497. package/test/share-link-service-policy.neo4j.spec.ts +403 -0
  498. package/test/signing-authority-encryption.spec.ts +75 -0
  499. package/test/smart-resume-consent.spec.ts +233 -0
  500. package/test/smart-resume-upload.spec.ts +65 -0
  501. package/tsconfig.build.json +1 -1
  502. package/tsconfig.json +1 -0
  503. package/vitest.config.ts +14 -0
  504. package/vitest.inbox-e2e.config.ts +2 -0
  505. package/vitest.share-links-neo4j.config.ts +19 -0
  506. package/vitest.share-links-unit.config.ts +28 -0
  507. package/workerLambda.test.ts +100 -0
@@ -0,0 +1,727 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import {
4
+ CreateShareLinkInputValidator,
5
+ ShareLinkIdValidator,
6
+ UpdateShareLinkInputValidator,
7
+ decodedBase64UrlByteLength,
8
+ utf8ByteLength,
9
+ } from '@learncard/types';
10
+
11
+ import {
12
+ canonicalizeJson,
13
+ computeShareLinkPayloadHash,
14
+ computeShareLinkRequestHash,
15
+ } from '@helpers/share-link-lifecycle';
16
+ import {
17
+ createShareLinkPolicyResolver,
18
+ resolveShareLinkExpiry,
19
+ } from '@helpers/share-link-policy/resolver';
20
+ import type { ShareLinkPolicyResolver } from '@helpers/share-link-policy/types';
21
+ import { hashSharePasscode } from '@helpers/share-link-passcode';
22
+
23
+ import { isShareLinkRepositoryError } from '../../accesslayer/share-link/errors';
24
+ import type { ShareLinkReservationRecord } from '../../accesslayer/share-link';
25
+ import { isTransientShareContentError } from '../share-content-client/types';
26
+ import type {
27
+ ShareContentClientErrorCode,
28
+ ShareContentEnvelope,
29
+ } from '../share-content-client/types';
30
+ import { verifyShareContentActiveStat as verifyActiveStat } from './stat';
31
+ import { verifyShareAttachmentStats } from './attachment-stat';
32
+ import {
33
+ ShareLinkCoordinatorError,
34
+ type PendingShareContentOperation,
35
+ type ShareContentPayload,
36
+ type ShareLinkCommitResult,
37
+ type ShareLinkCoordinator,
38
+ type ShareLinkCoordinatorErrorCode,
39
+ type ShareLinkCoordinatorDependencies,
40
+ type ShareLinkResumeResult,
41
+ type ShareOwnerContext,
42
+ } from './types';
43
+
44
+ const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
45
+
46
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
47
+ typeof value === 'object' &&
48
+ value !== null &&
49
+ !Array.isArray(value) &&
50
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
51
+
52
+ const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
53
+ typeof value === 'string' &&
54
+ value.length >= 1 &&
55
+ value.length <= maxLength &&
56
+ OPAQUE_IDENTIFIER_RE.test(value);
57
+
58
+ function fail(code: ShareLinkCoordinatorErrorCode, message: string): never {
59
+ throw new ShareLinkCoordinatorError(code, message);
60
+ }
61
+
62
+ const mapRepositoryErrorCode = (code: string): ShareLinkCoordinatorErrorCode => {
63
+ switch (code) {
64
+ case 'NOT_FOUND':
65
+ case 'CONFLICT':
66
+ case 'OPERATION_IN_FLIGHT':
67
+ case 'LEASE_EXPIRED':
68
+ case 'STALE_GENERATION':
69
+ case 'PRECONDITION_FAILED':
70
+ case 'INVALID_INPUT':
71
+ return code;
72
+ default:
73
+ return 'UNEXPECTED';
74
+ }
75
+ };
76
+
77
+ const toCoordinatorError = (error: unknown): ShareLinkCoordinatorError => {
78
+ if (isShareLinkRepositoryError(error)) {
79
+ return new ShareLinkCoordinatorError(
80
+ mapRepositoryErrorCode(error.code),
81
+ 'share-link lifecycle operation failed'
82
+ );
83
+ }
84
+
85
+ return new ShareLinkCoordinatorError('UNEXPECTED', 'share-link lifecycle operation failed');
86
+ };
87
+
88
+ const parseOwnerContext = (context: ShareOwnerContext): ShareOwnerContext => {
89
+ if (!isPlainObject(context)) fail('INVALID_INPUT', 'owner context must be an object');
90
+ if (!isOpaqueIdentifier(context.namespace)) {
91
+ fail('INVALID_INPUT', 'namespace must be an opaque configured identifier');
92
+ }
93
+ if (!isOpaqueIdentifier(context.ownerProfileId)) {
94
+ fail('INVALID_INPUT', 'ownerProfileId must be an opaque identifier');
95
+ }
96
+
97
+ return { namespace: context.namespace, ownerProfileId: context.ownerProfileId };
98
+ };
99
+
100
+ const isRetryableClientError = (code: ShareContentClientErrorCode): boolean =>
101
+ isTransientShareContentError(code) ||
102
+ code === 'MALFORMED_RESPONSE' ||
103
+ code === 'RESPONSE_TOO_LARGE';
104
+
105
+ /** Preserve the real cause for deterministic client failures instead of a generic conflict. */
106
+ const mapClientErrorCode = (code: ShareContentClientErrorCode): ShareLinkCoordinatorErrorCode => {
107
+ switch (code) {
108
+ case 'UNAUTHORIZED':
109
+ return 'UNAUTHORIZED';
110
+ case 'SIGNING_FAILED':
111
+ return 'SIGNING_FAILED';
112
+ case 'PAYLOAD_TOO_LARGE':
113
+ return 'PAYLOAD_TOO_LARGE';
114
+ case 'NOT_FOUND':
115
+ return 'NOT_FOUND';
116
+ case 'CONFLICT':
117
+ return 'CONFLICT';
118
+ case 'INVALID_INPUT':
119
+ return 'INVALID_INPUT';
120
+ default:
121
+ return 'UNEXPECTED';
122
+ }
123
+ };
124
+
125
+ const measurementFor = (
126
+ envelope: ShareContentEnvelope,
127
+ ownerEncryptedRecovery: Record<string, unknown>
128
+ ): { contentBytes: number; recoveryBytes: number; recoveryHash: string } => {
129
+ const contentBytes = decodedBase64UrlByteLength(envelope.ct);
130
+
131
+ if (contentBytes === null) fail('INVALID_INPUT', 'envelope ciphertext must be base64url');
132
+
133
+ const recoveryBytes = utf8ByteLength(JSON.stringify(ownerEncryptedRecovery));
134
+ const recoveryHash = createHash('sha256')
135
+ .update(canonicalizeJson(ownerEncryptedRecovery))
136
+ .digest('hex');
137
+
138
+ return { contentBytes, recoveryBytes, recoveryHash };
139
+ };
140
+
141
+ const contentBindingFor = (
142
+ envelope: ShareContentEnvelope,
143
+ ownerEncryptedRecovery: Record<string, unknown>
144
+ ): {
145
+ contentHash: string;
146
+ contentBytes: number;
147
+ recoveryHash: string;
148
+ recoveryBytes: number;
149
+ } => ({
150
+ contentHash: computeShareLinkPayloadHash({ envelope, ownerEncryptedRecovery }),
151
+ ...measurementFor(envelope, ownerEncryptedRecovery),
152
+ });
153
+
154
+ export const createShareLinkCoordinator = (
155
+ dependencies: ShareLinkCoordinatorDependencies
156
+ ): ShareLinkCoordinator => {
157
+ const { repository, client } = dependencies;
158
+ const now = (): Date => dependencies.now?.() ?? new Date();
159
+
160
+ // Fail-closed fallback policy: no views, 30 days, age unknown. Production
161
+ // wiring injects the authoritative source; a missing resolver must never
162
+ // behave as a known unmanaged adult.
163
+ const policyResolver: ShareLinkPolicyResolver =
164
+ dependencies.policyResolver ??
165
+ createShareLinkPolicyResolver({
166
+ resolveOwner: async () => ({
167
+ age: 'unknown',
168
+ isManaged: false,
169
+ isServiceProfile: false,
170
+ }),
171
+ });
172
+
173
+ const finalizeOrPending = async (
174
+ reservation: ShareLinkReservationRecord,
175
+ objectRef: string | null
176
+ ): Promise<ShareLinkCommitResult> => {
177
+ try {
178
+ if (!(await verifyShareAttachmentStats(client, reservation))) {
179
+ return { status: 'pending', reservation, reason: 'stat_missing' };
180
+ }
181
+ const finalized = await repository.finalizeReservation({
182
+ verifiedAttachmentId:
183
+ reservation.contentVersion !== null
184
+ ? (reservation.attachmentId ?? undefined)
185
+ : undefined,
186
+ namespace: reservation.namespace,
187
+ ownerProfileId: reservation.ownerProfileId,
188
+ shareId: reservation.shareId,
189
+ operationId: reservation.operationId,
190
+ objectRef,
191
+ generation: reservation.generation,
192
+ leaseOwner: reservation.leaseOwner,
193
+ verifiedContentHash:
194
+ objectRef === null || reservation.contentHash === null
195
+ ? undefined
196
+ : reservation.contentHash,
197
+ now: now(),
198
+ });
199
+
200
+ return finalized.outcome === 'finalized'
201
+ ? { status: 'committed', share: finalized.share }
202
+ : { status: 'replayed', share: finalized.share, recorded: finalized.share };
203
+ } catch (error) {
204
+ if (isShareLinkRepositoryError(error)) throw toCoordinatorError(error);
205
+
206
+ // An unexpected failure may have committed the swap but lost the
207
+ // response. Keep the reservation resumable; a stateless retry will
208
+ // re-stat and finalize idempotently.
209
+ return {
210
+ status: 'pending',
211
+ reservation,
212
+ reason: 'finalize_unavailable',
213
+ };
214
+ }
215
+ };
216
+
217
+ const abandonBestEffort = async (reservation: ShareLinkReservationRecord): Promise<void> => {
218
+ try {
219
+ await repository.abandonReservation({
220
+ namespace: reservation.namespace,
221
+ ownerProfileId: reservation.ownerProfileId,
222
+ shareId: reservation.shareId,
223
+ operationId: reservation.operationId,
224
+ generation: reservation.generation,
225
+ leaseOwner: reservation.leaseOwner,
226
+ now: now(),
227
+ });
228
+ } catch {
229
+ // The reservation lease still bounds a later supersede; never hide the
230
+ // original failure that triggered the abandon.
231
+ }
232
+ };
233
+
234
+ const driveContentReservation = async (
235
+ reservation: ShareLinkReservationRecord,
236
+ payload: ShareContentPayload
237
+ ): Promise<ShareLinkCommitResult> => {
238
+ if (reservation.objectRef === null || reservation.contentVersion === null) {
239
+ fail('PRECONDITION_FAILED', 'content reservation is missing its object binding');
240
+ }
241
+
242
+ const tuple = {
243
+ namespace: reservation.namespace,
244
+ ownerProfileId: reservation.ownerProfileId,
245
+ shareId: reservation.shareId,
246
+ contentVersion: reservation.contentVersion,
247
+ objectId: reservation.objectRef,
248
+ operationId: reservation.operationId,
249
+ };
250
+
251
+ const upload = await client.put({
252
+ ...tuple,
253
+ envelope: payload.envelope,
254
+ ownerEncryptedRecovery: payload.ownerEncryptedRecovery,
255
+ });
256
+
257
+ if (!upload.ok) {
258
+ if (isRetryableClientError(upload.error)) {
259
+ return { status: 'pending', reservation, reason: 'upload_unavailable' };
260
+ }
261
+
262
+ // Deterministic rejection: release the reservation and never hide the
263
+ // failure behind a pending state. The exact object is queued for
264
+ // tracked cleanup by abandon.
265
+ await abandonBestEffort(reservation);
266
+ fail(mapClientErrorCode(upload.error), 'LearnCloud rejected the content upload');
267
+ }
268
+
269
+ const stat = await client.stat(tuple);
270
+
271
+ if (!stat.ok) {
272
+ if (stat.error === 'NOT_FOUND') {
273
+ return { status: 'pending', reservation, reason: 'stat_missing' };
274
+ }
275
+ if (isRetryableClientError(stat.error)) {
276
+ return { status: 'pending', reservation, reason: 'stat_unavailable' };
277
+ }
278
+
279
+ // Do not finalize on a failed stat, and do not destroy the staged
280
+ // object on an ambiguous response.
281
+ fail(mapClientErrorCode(stat.error), 'LearnCloud stat failed');
282
+ }
283
+
284
+ if (!verifyActiveStat(stat.value, reservation)) {
285
+ if (stat.value.kind === 'tombstone') {
286
+ await abandonBestEffort(reservation);
287
+ fail('CONFLICT', 'the reserved object is a permanent tombstone');
288
+ }
289
+
290
+ return { status: 'pending', reservation, reason: 'stat_mismatch' };
291
+ }
292
+
293
+ return finalizeOrPending(reservation, reservation.objectRef);
294
+ };
295
+
296
+ const handleReserveResult = async (
297
+ result: Awaited<
298
+ ReturnType<ShareLinkCoordinatorDependencies['repository']['reserveCreate']>
299
+ >,
300
+ payload?: ShareContentPayload
301
+ ): Promise<ShareLinkCommitResult> => {
302
+ if (result.outcome === 'already_committed') {
303
+ return { status: 'replayed', share: result.current, recorded: result.recorded };
304
+ }
305
+
306
+ if (!payload || result.reservation.objectRef === null) {
307
+ return finalizeOrPending(result.reservation, null);
308
+ }
309
+
310
+ return driveContentReservation(result.reservation, payload);
311
+ };
312
+
313
+ return {
314
+ createShareLink: async (request, context) => {
315
+ const owner = parseOwnerContext(context);
316
+ const parsed = CreateShareLinkInputValidator.safeParse(request);
317
+
318
+ if (!parsed.success) fail('INVALID_INPUT', 'invalid create share-link request');
319
+
320
+ const value = parsed.data;
321
+ const content = contentBindingFor(value.envelope, value.ownerEncryptedRecovery);
322
+ // Caller intent only: `expiresAt` remains exactly as supplied
323
+ // (including omitted) so a later-clock retry of an omitted-expiry
324
+ // create replays the original operation instead of deriving a new one.
325
+ const requestHash = computeShareLinkRequestHash(
326
+ 'create',
327
+ value as unknown as Record<string, unknown>
328
+ );
329
+ const policy = await policyResolver.resolve(owner.ownerProfileId);
330
+ const effectiveExpiresAt = resolveShareLinkExpiry(policy, value.expiresAt, now());
331
+ const passcodeHash = value.passcode ? await hashSharePasscode(value.passcode) : null;
332
+
333
+ let reserved;
334
+
335
+ try {
336
+ reserved = await repository.reserveCreate({
337
+ namespace: owner.namespace,
338
+ ownerProfileId: owner.ownerProfileId,
339
+ clientRequestId: value.clientRequestId,
340
+ shareId: value.id,
341
+ title: value.title,
342
+ note: value.note ?? null,
343
+ expiresAt: effectiveExpiresAt,
344
+ selectedCount: value.selectedCount,
345
+ passcodeHash,
346
+ // Notification consent cannot override the same trusted
347
+ // policy that disables view counting for minors/unknown age.
348
+ notifyOnView: value.notifyOnView && policy.viewCountingEnabled,
349
+ content,
350
+ attachment: value.attachment,
351
+ requestHash,
352
+ policy,
353
+ leaseOwner: dependencies.leaseOwner,
354
+ leaseMs: dependencies.leaseMs,
355
+ now: now(),
356
+ });
357
+ } catch (error) {
358
+ throw toCoordinatorError(error);
359
+ }
360
+
361
+ return handleReserveResult(reserved, {
362
+ envelope: value.envelope,
363
+ ownerEncryptedRecovery: value.ownerEncryptedRecovery,
364
+ });
365
+ },
366
+
367
+ updateShareLink: async (request, context) => {
368
+ const owner = parseOwnerContext(context);
369
+ const parsed = UpdateShareLinkInputValidator.safeParse(request);
370
+
371
+ if (!parsed.success) fail('INVALID_INPUT', 'invalid update share-link request');
372
+
373
+ const value = parsed.data;
374
+ const hasContent = value.envelope !== undefined;
375
+ const requestHash = computeShareLinkRequestHash(
376
+ 'update',
377
+ value as unknown as Record<string, unknown>
378
+ );
379
+ // Policy is re-resolved on every mutation, even a metadata-only one,
380
+ // so a profile that became managed stops accumulating views going
381
+ // forward. An omitted `expiresAt` leaves the existing expiry intact.
382
+ const policy = await policyResolver.resolve(owner.ownerProfileId);
383
+ const passcodeHash =
384
+ value.passcode === undefined
385
+ ? undefined
386
+ : value.passcode === null
387
+ ? null
388
+ : await hashSharePasscode(value.passcode);
389
+
390
+ let reserved;
391
+
392
+ try {
393
+ reserved = await repository.reserveReplacement({
394
+ namespace: owner.namespace,
395
+ ownerProfileId: owner.ownerProfileId,
396
+ clientRequestId: value.clientRequestId,
397
+ shareId: value.id,
398
+ expectedVersion: value.expectedVersion,
399
+ requestHash,
400
+ leaseOwner: dependencies.leaseOwner,
401
+ leaseMs: dependencies.leaseMs,
402
+ now: now(),
403
+ ...(hasContent
404
+ ? {
405
+ content: {
406
+ ...contentBindingFor(
407
+ value.envelope!,
408
+ value.ownerEncryptedRecovery!
409
+ ),
410
+ contentVersion: value.contentVersion!,
411
+ selectedCount: value.selectedCount!,
412
+ attachment: value.attachment,
413
+ },
414
+ }
415
+ : {}),
416
+ ...(value.title !== undefined ? { title: value.title } : {}),
417
+ ...(value.note !== undefined ? { note: value.note } : {}),
418
+ ...(value.expiresAt !== undefined ? { expiresAt: value.expiresAt } : {}),
419
+ ...(passcodeHash !== undefined ? { passcodeHash } : {}),
420
+ ...(value.notifyOnView !== undefined
421
+ ? { notifyOnView: value.notifyOnView && policy.viewCountingEnabled }
422
+ : {}),
423
+ policy,
424
+ });
425
+ } catch (error) {
426
+ throw toCoordinatorError(error);
427
+ }
428
+
429
+ if (reserved.outcome === 'already_committed') {
430
+ return {
431
+ status: 'replayed',
432
+ share: reserved.current,
433
+ recorded: reserved.recorded,
434
+ };
435
+ }
436
+
437
+ if (!hasContent || reserved.reservation.objectRef === null) {
438
+ // Metadata-only revision: never uploads, preserves the original
439
+ // object operation id via the C3 finalize path.
440
+ return finalizeOrPending(reserved.reservation, null);
441
+ }
442
+
443
+ return driveContentReservation(reserved.reservation, {
444
+ envelope: value.envelope!,
445
+ ownerEncryptedRecovery: value.ownerEncryptedRecovery!,
446
+ });
447
+ },
448
+
449
+ revokeShareLink: async (request, context) => {
450
+ const owner = parseOwnerContext(context);
451
+
452
+ if (!isPlainObject(request)) fail('INVALID_INPUT', 'invalid revoke request');
453
+
454
+ const allowedKeys = ['id', 'expectedVersion', 'clientRequestId'];
455
+
456
+ if (Object.keys(request).some(key => !allowedKeys.includes(key))) {
457
+ fail('INVALID_INPUT', 'invalid revoke request');
458
+ }
459
+ if (!ShareLinkIdValidator.safeParse(request.id).success) {
460
+ fail('INVALID_INPUT', 'invalid share id');
461
+ }
462
+ if (
463
+ request.expectedVersion !== undefined &&
464
+ (!Number.isSafeInteger(request.expectedVersion) ||
465
+ (request.expectedVersion as number) < 1)
466
+ ) {
467
+ fail('INVALID_INPUT', 'invalid expectedVersion');
468
+ }
469
+ if (
470
+ request.clientRequestId !== undefined &&
471
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
472
+ String(request.clientRequestId)
473
+ )
474
+ ) {
475
+ fail('INVALID_INPUT', 'invalid clientRequestId');
476
+ }
477
+
478
+ const clientRequestId = request.clientRequestId as string | undefined;
479
+ const requestHash = clientRequestId
480
+ ? computeShareLinkRequestHash('revoke', {
481
+ id: request.id,
482
+ expectedVersion: request.expectedVersion,
483
+ })
484
+ : undefined;
485
+
486
+ try {
487
+ const revoked = await repository.revokeShareLink({
488
+ namespace: owner.namespace,
489
+ ownerProfileId: owner.ownerProfileId,
490
+ shareId: request.id as string,
491
+ expectedVersion: request.expectedVersion as number | undefined,
492
+ clientRequestId,
493
+ requestHash,
494
+ now: now(),
495
+ });
496
+
497
+ return {
498
+ status: revoked.outcome,
499
+ share: revoked.share,
500
+ cleanupQueuedFor: revoked.cleanupQueuedFor,
501
+ };
502
+ } catch (error) {
503
+ throw toCoordinatorError(error);
504
+ }
505
+ },
506
+
507
+ resumePendingOperation: async (pending, content): Promise<ShareLinkResumeResult> => {
508
+ const reservation = pending.reservation;
509
+
510
+ if (reservation.objectRef === null || reservation.contentVersion === null) {
511
+ const finalized = await finalizeOrPending(reservation, null);
512
+
513
+ return finalized.status === 'pending'
514
+ ? { status: 'pending', reservation, reason: finalized.reason }
515
+ : finalized;
516
+ }
517
+
518
+ if (content) {
519
+ const result = await driveContentReservation(reservation, content);
520
+
521
+ return result.status === 'pending'
522
+ ? { status: 'pending', reservation, reason: result.reason }
523
+ : result;
524
+ }
525
+
526
+ const tuple = {
527
+ namespace: reservation.namespace,
528
+ ownerProfileId: reservation.ownerProfileId,
529
+ shareId: reservation.shareId,
530
+ contentVersion: reservation.contentVersion,
531
+ objectId: reservation.objectRef,
532
+ operationId: reservation.operationId,
533
+ };
534
+
535
+ const stat = await client.stat(tuple);
536
+
537
+ if (stat.ok) {
538
+ if (verifyActiveStat(stat.value, reservation)) {
539
+ const finalized = await finalizeOrPending(reservation, reservation.objectRef);
540
+
541
+ return finalized.status === 'pending'
542
+ ? { status: 'pending', reservation, reason: finalized.reason }
543
+ : finalized;
544
+ }
545
+
546
+ if (stat.value.kind === 'tombstone') {
547
+ const abandoned = await repository.abandonReservation({
548
+ namespace: reservation.namespace,
549
+ ownerProfileId: reservation.ownerProfileId,
550
+ shareId: reservation.shareId,
551
+ operationId: reservation.operationId,
552
+ generation: reservation.generation,
553
+ leaseOwner: reservation.leaseOwner,
554
+ now: now(),
555
+ });
556
+
557
+ return {
558
+ status: 'abandoned',
559
+ cleanupQueuedFor:
560
+ abandoned.outcome === 'abandoned' ? abandoned.cleanupQueuedFor : null,
561
+ };
562
+ }
563
+
564
+ // Active but divergent: never finalize and never destroy bytes.
565
+ return { status: 'pending', reservation, reason: 'stat_mismatch' };
566
+ }
567
+
568
+ if (stat.error === 'NOT_FOUND') {
569
+ const abandoned = await repository.abandonReservation({
570
+ namespace: reservation.namespace,
571
+ ownerProfileId: reservation.ownerProfileId,
572
+ shareId: reservation.shareId,
573
+ operationId: reservation.operationId,
574
+ generation: reservation.generation,
575
+ leaseOwner: reservation.leaseOwner,
576
+ now: now(),
577
+ });
578
+
579
+ return {
580
+ status: 'abandoned',
581
+ cleanupQueuedFor:
582
+ abandoned.outcome === 'abandoned' ? abandoned.cleanupQueuedFor : null,
583
+ };
584
+ }
585
+
586
+ if (isRetryableClientError(stat.error)) {
587
+ return { status: 'pending', reservation, reason: 'stat_unavailable' };
588
+ }
589
+
590
+ fail(mapClientErrorCode(stat.error), 'LearnCloud stat failed during recovery');
591
+ },
592
+
593
+ abandonPendingOperation: async pending =>
594
+ repository.abandonReservation({
595
+ namespace: pending.reservation.namespace,
596
+ ownerProfileId: pending.reservation.ownerProfileId,
597
+ shareId: pending.reservation.shareId,
598
+ operationId: pending.reservation.operationId,
599
+ generation: pending.reservation.generation,
600
+ leaseOwner: pending.reservation.leaseOwner,
601
+ now: now(),
602
+ }),
603
+
604
+ getShareLink: async (shareId, context) => {
605
+ const owner = parseOwnerContext(context);
606
+
607
+ return repository.getShareLink({
608
+ shareId,
609
+ namespace: owner.namespace,
610
+ ownerProfileId: owner.ownerProfileId,
611
+ });
612
+ },
613
+
614
+ getActiveShareContent: async (shareId, context) => {
615
+ const owner = parseOwnerContext(context);
616
+
617
+ return repository.getCurrentShareContent({
618
+ shareId,
619
+ namespace: owner.namespace,
620
+ ownerProfileId: owner.ownerProfileId,
621
+ now: now(),
622
+ });
623
+ },
624
+
625
+ fetchShareContent: async (shareId, context, options = {}) => {
626
+ const owner = parseOwnerContext(context);
627
+ const current = await repository.getCurrentShareContent({
628
+ shareId,
629
+ namespace: owner.namespace,
630
+ ownerProfileId: owner.ownerProfileId,
631
+ now: now(),
632
+ allowExpired: options.allowExpired,
633
+ });
634
+
635
+ if (current.state !== 'active') {
636
+ return {
637
+ ok: false,
638
+ error: 'NOT_FOUND',
639
+ };
640
+ }
641
+
642
+ // Use the object's ORIGINAL operation id, never the latest metadata
643
+ // operation id, for read/stat/delete of an immutable object.
644
+ const result = await client.get({
645
+ namespace: current.namespace,
646
+ ownerProfileId: current.ownerProfileId,
647
+ shareId: current.shareId,
648
+ contentVersion: current.contentVersion,
649
+ objectId: current.objectRef,
650
+ operationId: current.operationId,
651
+ });
652
+ if (!result.ok) return result;
653
+
654
+ // The remote read can overlap a stop, expiry or replacement. Only
655
+ // release bytes if the same committed snapshot still permits it.
656
+ const latest = await repository.getCurrentShareContent({
657
+ shareId,
658
+ namespace: owner.namespace,
659
+ ownerProfileId: owner.ownerProfileId,
660
+ now: now(),
661
+ allowExpired: options.allowExpired,
662
+ });
663
+ if (
664
+ latest.state !== 'active' ||
665
+ latest.version !== current.version ||
666
+ latest.objectRef !== current.objectRef ||
667
+ latest.operationId !== current.operationId ||
668
+ latest.contentVersion !== current.contentVersion
669
+ ) {
670
+ return { ok: false, error: 'NOT_FOUND' };
671
+ }
672
+ return result;
673
+ },
674
+
675
+ readOwnerRecovery: async (shareId, context) => {
676
+ const owner = parseOwnerContext(context);
677
+ // Owner recovery deliberately admits retained *expired* content so the
678
+ // owner can extend/edit it. Stopped, staging, content_missing and
679
+ // missing shares still fail closed here.
680
+ const current = await repository.getCurrentShareContent({
681
+ shareId,
682
+ namespace: owner.namespace,
683
+ ownerProfileId: owner.ownerProfileId,
684
+ now: now(),
685
+ allowExpired: true,
686
+ });
687
+
688
+ if (current.state !== 'active') {
689
+ return { ok: false, error: 'NOT_FOUND' };
690
+ }
691
+
692
+ const result = await client.readRecovery({
693
+ namespace: current.namespace,
694
+ ownerProfileId: current.ownerProfileId,
695
+ shareId: current.shareId,
696
+ contentVersion: current.contentVersion,
697
+ objectId: current.objectRef,
698
+ operationId: current.operationId,
699
+ });
700
+
701
+ if (!result.ok) return result;
702
+
703
+ // The remote read can overlap a stop/replacement. Only release the
704
+ // recovery for the same immutable committed tuple that authorized it.
705
+ const latest = await repository.getCurrentShareContent({
706
+ shareId,
707
+ namespace: owner.namespace,
708
+ ownerProfileId: owner.ownerProfileId,
709
+ now: now(),
710
+ allowExpired: true,
711
+ });
712
+ if (
713
+ latest.state !== 'active' ||
714
+ latest.version !== current.version ||
715
+ latest.objectRef !== current.objectRef ||
716
+ latest.operationId !== current.operationId ||
717
+ latest.contentVersion !== current.contentVersion
718
+ ) {
719
+ return { ok: false, error: 'NOT_FOUND' };
720
+ }
721
+
722
+ return result;
723
+ },
724
+ };
725
+ };
726
+
727
+ export type { PendingShareContentOperation };