@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,122 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { VCValidator, type BitstringStatusListEntry } from '@learncard/types';
3
+ import { Credential, Profile, StatusList } from '@models';
4
+ import { storeCredential } from '../src/accesslayer/credential/create';
5
+ import { getCredentialById } from '../src/accesslayer/credential/read';
6
+ import { setCredentialBitstringStatus, decodeBitstring } from '../src/helpers/status-list.helpers';
7
+ import { getUser } from './helpers/getClient';
8
+ import { app as statusListsApp } from '../src/status-lists';
9
+
10
+ let issuer: Awaited<ReturnType<typeof getUser>>;
11
+ const credentialIds: string[] = [];
12
+
13
+ const isStatusBitSet = async (entry: BitstringStatusListEntry): Promise<boolean> => {
14
+ const id = entry.statusListCredential.split('/').pop();
15
+ if (!id) throw new Error('Missing status list id');
16
+ const response = await statusListsApp.inject({ method: 'GET', url: `/status-lists/${id}` });
17
+ expect(response.statusCode).toBe(200);
18
+ const list = JSON.parse(response.body);
19
+ const bytes = decodeBitstring(list.credentialSubject.encodedList, 131_072);
20
+ const index = Number(entry.statusListIndex);
21
+ return ((bytes[Math.floor(index / 8)] ?? 0) & (1 << (index % 8))) !== 0;
22
+ };
23
+
24
+ describe('credential status storage in Neo4j', () => {
25
+ beforeAll(async () => {
26
+ issuer = await getUser();
27
+ await issuer.clients.fullAuth.profile.createProfile({ profileId: 'storage-issuer' });
28
+ await statusListsApp.ready();
29
+ });
30
+ afterAll(async () => {
31
+ for (const id of credentialIds) await Credential.delete({ where: { id } });
32
+ await Profile.delete({ detach: true, where: { profileId: 'storage-issuer' } });
33
+ await StatusList.delete({ detach: true, where: { ownerProfileId: 'storage-issuer' } });
34
+ });
35
+
36
+ it('persists trusted public status descriptors for revocation consumers without decrypting the payload', async () => {
37
+ const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
38
+ statusPurposes: ['revocation'],
39
+ });
40
+ if (!entry) throw new Error('Expected allocated revocation entry');
41
+ const signed = await issuer.learnCard.invoke.issueCredential({
42
+ ...(await issuer.learnCard.invoke.getTestVc()),
43
+ credentialStatus: entry,
44
+ });
45
+ const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
46
+ const stored = await storeCredential({
47
+ kind: 'issued-credential',
48
+ credential: jwe,
49
+ statusEntries: [entry],
50
+ });
51
+ credentialIds.push(stored.id);
52
+
53
+ const row = await getCredentialById(stored.id);
54
+ expect(row?.credential).toBe(JSON.stringify(jwe));
55
+ expect(JSON.parse(row?.statusEntries ?? 'null')).toEqual([entry]);
56
+ expect(await isStatusBitSet(entry)).toBe(false);
57
+ expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(true);
58
+ expect(await isStatusBitSet(entry)).toBe(true);
59
+ });
60
+
61
+ it.each(['embedded', 'absent'] as const)(
62
+ 'preserves a valid signed wire VC and proof with %s status',
63
+ async statusCase => {
64
+ const unsigned = await issuer.learnCard.invoke.getTestVc();
65
+ if (statusCase === 'embedded') {
66
+ const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
67
+ statusPurposes: ['revocation'],
68
+ });
69
+ unsigned.credentialStatus = entry;
70
+ }
71
+ const signed = await issuer.learnCard.invoke.issueCredential(unsigned);
72
+ const stored = await storeCredential(signed);
73
+ credentialIds.push(stored.id);
74
+ const row = await getCredentialById(stored.id);
75
+ const recovered = VCValidator.parse(JSON.parse(row?.credential ?? 'null'));
76
+ expect(recovered).toEqual(signed);
77
+ expect(
78
+ await issuer.learnCard.invoke.verifyCredential(recovered, { checks: ['proof'] })
79
+ ).toMatchObject({
80
+ checks: expect.arrayContaining(['proof']),
81
+ warnings: [],
82
+ errors: [],
83
+ });
84
+ }
85
+ );
86
+
87
+ it('does not promote wire JWE metadata to trusted status descriptors', async () => {
88
+ const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
89
+ statusPurposes: ['revocation'],
90
+ });
91
+ if (!entry) throw new Error('Expected allocated revocation entry');
92
+ const signed = await issuer.learnCard.invoke.issueCredential({
93
+ ...(await issuer.learnCard.invoke.getTestVc()),
94
+ credentialStatus: entry,
95
+ });
96
+ const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
97
+ const wire = { ...jwe, kind: 'issued-credential', statusEntries: [entry] };
98
+ const stored = await storeCredential(wire);
99
+ credentialIds.push(stored.id);
100
+ const row = await getCredentialById(stored.id);
101
+ expect(row?.credential).toBe(JSON.stringify(wire));
102
+ expect(row?.statusEntries).toBeUndefined();
103
+ expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
104
+ expect(await isStatusBitSet(entry)).toBe(false);
105
+ });
106
+
107
+ it('does not persist or apply an empty trusted status descriptor set', async () => {
108
+ const signed = await issuer.learnCard.invoke.issueCredential(
109
+ await issuer.learnCard.invoke.getTestVc()
110
+ );
111
+ const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
112
+ const stored = await storeCredential({
113
+ kind: 'issued-credential',
114
+ credential: jwe,
115
+ statusEntries: [],
116
+ });
117
+ credentialIds.push(stored.id);
118
+ const row = await getCredentialById(stored.id);
119
+ expect(row?.statusEntries).toBeUndefined();
120
+ expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
121
+ });
122
+ });
package/test/dids.spec.ts CHANGED
@@ -168,7 +168,7 @@ describe('did:web profile document generation with signing authorities', () => {
168
168
  await deleteDidDocForProfile(profileId);
169
169
  });
170
170
 
171
- it('never emits a fragment on a verificationMethod controller', async () => {
171
+ it('renders controller-owned verification and key-agreement methods', async () => {
172
172
  const profile = await getProfileByProfileId(profileId);
173
173
  const sa = await createSigningAuthority(SA_ENDPOINT);
174
174
  await createUseSigningAuthorityRelationship(profile!, sa, 'lca-sa', SA_DID, true);
@@ -195,6 +195,15 @@ describe('did:web profile document generation with signing authorities', () => {
195
195
  );
196
196
  expect(saVm).toBeDefined();
197
197
  expect((saVm as { controller: string }).controller).toBe(did);
198
+
199
+ const ownerKeyAgreement = (doc.keyAgreement ?? []).find(
200
+ method => typeof method === 'object' && method.controller === did
201
+ );
202
+ expect(ownerKeyAgreement).toMatchObject({
203
+ type: 'X25519KeyAgreementKey2019',
204
+ controller: did,
205
+ publicKeyBase58: expect.any(String),
206
+ });
198
207
  });
199
208
 
200
209
  it('de-duplicates repeated signing-authority relationships in the rendered document', async () => {
@@ -216,6 +216,21 @@ describe('Guardian-approved consent mutations', () => {
216
216
  expect(record?.terms.guardianApproval).toBeUndefined();
217
217
  });
218
218
 
219
+ it('preserves the manager-based gate for explicitly typed profiles without managers', async () => {
220
+ await Profile.update(
221
+ { type: 'child', isServiceProfile: true },
222
+ { where: { profileId: 'child-user' } }
223
+ );
224
+
225
+ const { termsUri } = await childCaller().contracts.consentToContract({
226
+ contractUri,
227
+ terms: minimalTerms,
228
+ });
229
+ const record = await getContractTermsByUri(termsUri);
230
+ expect(record?.terms.status).toBe('live');
231
+ expect(record?.terms.guardianApproval).toBeUndefined();
232
+ });
233
+
219
234
  it('does not create consent or an audit transaction without guardian approval', async () => {
220
235
  await manageChild();
221
236
  await expect(
@@ -225,6 +240,40 @@ describe('Guardian-approved consent mutations', () => {
225
240
  expect(await ConsentFlowTransaction.findMany({ where: {} })).toEqual([]);
226
241
  });
227
242
 
243
+ it('does not guardian-gate a managed service profile', async () => {
244
+ await Profile.update({ isServiceProfile: true }, { where: { profileId: 'child-user' } });
245
+ await manageChild();
246
+
247
+ const { termsUri } = await childCaller().contracts.consentToContract({
248
+ contractUri,
249
+ terms: minimalTerms,
250
+ });
251
+ const record = await getContractTermsByUri(termsUri);
252
+ expect(record?.terms.status).toBe('live');
253
+ expect(record?.terms.guardianApproval).toBeUndefined();
254
+ });
255
+
256
+ it('requires guardian approval for legacy service-flagged children', async () => {
257
+ await Profile.update(
258
+ { type: 'child', isServiceProfile: true },
259
+ { where: { profileId: 'child-user' } }
260
+ );
261
+ await manageChild();
262
+
263
+ await expect(
264
+ childCaller().contracts.consentToContract({ contractUri, terms: minimalTerms })
265
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
266
+
267
+ const { termsUri } = await childCaller(await approve()).contracts.consentToContract({
268
+ contractUri,
269
+ terms: minimalTerms,
270
+ });
271
+ expect((await getContractTermsByUri(termsUri))?.terms.guardianApproval).toMatchObject({
272
+ guardianProfileId: 'guardian-user',
273
+ guardianDid: guardian.id.did(),
274
+ });
275
+ });
276
+
228
277
  it('records the verified guardian and contract version on consent and permission updates', async () => {
229
278
  await manageChild();
230
279
  const beforeApproval = Date.now();
@@ -0,0 +1,94 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { v4 as uuid } from 'uuid';
3
+ import { neogma } from '@instance';
4
+ import { computeShareLinkRequestHash } from '@helpers/share-link-lifecycle';
5
+ import { reserveCreate } from '../../src/accesslayer/share-link';
6
+
7
+ export const nextShareId = (): string => randomBytes(16).toString('base64url');
8
+
9
+ export const contentBinding = (seed: string) => ({
10
+ contentHash: createHash('sha256').update(`content:${seed}`).digest('hex'),
11
+ contentBytes: 128,
12
+ recoveryHash: createHash('sha256').update(`recovery:${seed}`).digest('hex'),
13
+ recoveryBytes: 256,
14
+ });
15
+
16
+ export const clearLifecycleGraph = async (): Promise<void> => {
17
+ await neogma.queryRunner.run(
18
+ `MATCH (n)
19
+ WHERE n:ShareLink OR n:ShareLinkReservation OR n:ShareLinkOperation OR n:ShareContentCleanupJob
20
+ DETACH DELETE n`
21
+ );
22
+ };
23
+
24
+ export type ReservedCreate = {
25
+ namespace: string;
26
+ ownerProfileId: string;
27
+ shareId: string;
28
+ clientRequestId: string;
29
+ requestHash: string;
30
+ operationId: string;
31
+ objectRef: string;
32
+ generation: number;
33
+ leaseOwner: string;
34
+ };
35
+
36
+ export const reserveShareFixture = async (options: {
37
+ ownerProfileId: string;
38
+ shareId?: string;
39
+ expiresAt?: string | null;
40
+ selectedCount?: number;
41
+ title?: string;
42
+ leaseMs?: number;
43
+ now: Date;
44
+ clientRequestId?: string;
45
+ requestHash?: string;
46
+ namespace: string;
47
+ }): Promise<ReservedCreate> => {
48
+ const namespace = options.namespace;
49
+ const ownerProfileId = options.ownerProfileId;
50
+ const shareId = options.shareId ?? nextShareId();
51
+ const clientRequestId = options.clientRequestId ?? uuid();
52
+ const selectedCount = options.selectedCount ?? 1;
53
+ const content = contentBinding(shareId);
54
+ const requestHash =
55
+ options.requestHash ??
56
+ computeShareLinkRequestHash('create', {
57
+ id: shareId,
58
+ title: options.title ?? 'Shared credentials',
59
+ selectedCount,
60
+ ...content,
61
+ });
62
+
63
+ const result = await reserveCreate({
64
+ namespace,
65
+ ownerProfileId,
66
+ clientRequestId,
67
+ shareId,
68
+ title: options.title ?? 'Shared credentials',
69
+ note: null,
70
+ expiresAt: options.expiresAt ?? null,
71
+ selectedCount,
72
+ content,
73
+ requestHash,
74
+ leaseOwner: 'worker-1',
75
+ leaseMs: options.leaseMs,
76
+ now: options.now,
77
+ });
78
+
79
+ if (result.outcome !== 'reserved' || !result.reservation.objectRef) {
80
+ throw new Error(`expected reserved create, got ${result.outcome}`);
81
+ }
82
+
83
+ return {
84
+ namespace,
85
+ ownerProfileId,
86
+ shareId,
87
+ clientRequestId,
88
+ requestHash,
89
+ operationId: result.reservation.operationId,
90
+ objectRef: result.reservation.objectRef,
91
+ generation: result.reservation.generation,
92
+ leaseOwner: result.reservation.leaseOwner,
93
+ };
94
+ };
@@ -3,7 +3,12 @@ import Fastify, { type FastifyInstance } from 'fastify';
3
3
  import { fastifyTRPCOpenApiPlugin } from 'trpc-to-openapi';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { randomUUID } from 'node:crypto';
6
- import type { IssueInboxCredentialType, UnsignedVC, VC } from '@learncard/types';
6
+ import {
7
+ JWEValidator,
8
+ type IssueInboxCredentialType,
9
+ type UnsignedVC,
10
+ type VC,
11
+ } from '@learncard/types';
7
12
  import { getBitstringStatusListEntries } from '@learncard/helpers';
8
13
 
9
14
  import { appRouter, createContext } from '../src/app';
@@ -380,7 +385,8 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
380
385
  expect(result.deliveries).toEqual([
381
386
  { id: issued.issuanceId, credential: result.verifiableCredentials[0] },
382
387
  ]);
383
- expect(result.verifiableCredentials[0]).toMatchObject({
388
+ const encrypted = JWEValidator.parse(result.verifiableCredentials[0]);
389
+ expect(await recipient.learnCard.invoke.decryptDagJwe(encrypted)).toMatchObject({
384
390
  type: ['VerifiableCredential', 'ClrCredential'],
385
391
  proof: expect.anything(),
386
392
  credentialSubject: {
@@ -397,7 +403,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
397
403
  });
398
404
 
399
405
  it.each(['finalize', 'claim link', 'claim without profile'] as const)(
400
- 'recovers exactly one holder-only delivery after discarding the %s response',
406
+ 'recovers exactly one authorized delivery after discarding the %s response',
401
407
  async path => {
402
408
  const credential = await signedCredential();
403
409
  const issued = await issue({
@@ -439,11 +445,11 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
439
445
  expect(
440
446
  (await service.invoke.decryptDagJwe(recovery.credential).catch(() => null)) || null
441
447
  ).toBeNull();
442
- expect(
448
+ const issuerRead =
443
449
  (await issuer.learnCard.invoke
444
450
  .decryptDagJwe(recovery.credential)
445
- .catch(() => null)) || null
446
- ).toBeNull();
451
+ .catch(() => null)) || null;
452
+ expect(issuerRead).toEqual(path === 'finalize' ? credential : null);
447
453
  expect((await post('/api/inbox/deliveries', {})).status).toBe(401);
448
454
  expect(
449
455
  (await (await post('/api/inbox/deliveries', {}, issuer)).json()).records
@@ -510,9 +516,14 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
510
516
  await verifyRecipientContact('idless@example.test');
511
517
  const response = await post('/api/inbox/finalize', {}, recipient);
512
518
  expect(response.status).toBe(200);
513
- expect((await response.json()).deliveries).toEqual([
514
- { id: issued.issuanceId, credential },
515
- ]);
519
+ const { deliveries } = await response.json();
520
+ expect(deliveries).toHaveLength(1);
521
+ expect(deliveries[0].id).toBe(issued.issuanceId);
522
+ expect(
523
+ await recipient.learnCard.invoke.decryptDagJwe(
524
+ JWEValidator.parse(deliveries[0].credential)
525
+ )
526
+ ).toEqual(credential);
516
527
  } else {
517
528
  expect(await claim(issued.claimUrl!)).toEqual([credential]);
518
529
  }
@@ -893,7 +904,12 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
893
904
  expect(config).toMatch(
894
905
  /inboxMaintenance:\s+handler: lambda\.inboxMaintenanceHandler[\s\S]*?rate: rate\(1 day\)\s+enabled: true/
895
906
  );
896
- const handler = await readFile(new URL('../lambda.ts', import.meta.url), 'utf8');
907
+ // lambda.ts is a thin bootstrap entry that delegates to the lazily imported lambdaApp.ts.
908
+ const entry = await readFile(new URL('../lambda.ts', import.meta.url), 'utf8');
909
+ expect(entry).toMatch(
910
+ /export const inboxMaintenanceHandler[\s\S]*?\(await getApplication\(\)\)\.inboxMaintenanceHandler\(/
911
+ );
912
+ const handler = await readFile(new URL('../lambdaApp.ts', import.meta.url), 'utf8');
897
913
  expect(handler).toMatch(/inboxMaintenanceHandler[\s\S]*?await runInboxMaintenance\(\)/);
898
914
  });
899
915
  });
@@ -8,6 +8,7 @@ import {
8
8
  VC,
9
9
  VP,
10
10
  JWE,
11
+ JWEValidator,
11
12
  } from '@learncard/types';
12
13
  import { sendSpy, addNotificationToQueueSpy } from './helpers/spies';
13
14
  import * as notifications from '@helpers/notifications.helpers';
@@ -16,6 +17,9 @@ import { getProfileByProfileId } from '@accesslayer/profile/read';
16
17
  import { areProfilesConnected } from '@helpers/connection.helpers';
17
18
  import { updateInboxCredential } from '@accesslayer/inbox-credential/update';
18
19
  import { neogma } from '@instance';
20
+ import { Credential } from '@models';
21
+ import { getInboxCredentialById } from '@accesslayer/inbox-credential/read';
22
+ import * as credentialRelationships from '@accesslayer/credential/relationships/create';
19
23
 
20
24
  const noAuthClient = getClient();
21
25
  let userA: Awaited<ReturnType<typeof getUser>>;
@@ -1832,6 +1836,174 @@ describe('Universal Inbox', () => {
1832
1836
  await SigningAuthority.delete({ detach: true, where: {} });
1833
1837
  });
1834
1838
 
1839
+ const preparePendingBoost = async (email: string) => {
1840
+ await Profile.relateTo({
1841
+ alias: 'usesSigningAuthority',
1842
+ where: {
1843
+ source: { profileId: 'usera' },
1844
+ target: { endpoint: 'https://example.com' },
1845
+ },
1846
+ properties: { name: 'example', did: userA.learnCard.id.did() },
1847
+ });
1848
+ const boostUri = await userA.clients.fullAuth.boost.createBoost({
1849
+ credential: testUnsignedBoost,
1850
+ claimPermissions: { canRevoke: true },
1851
+ });
1852
+ const issued = await userA.clients.fullAuth.inbox.issue({
1853
+ templateUri: boostUri,
1854
+ recipient: { type: 'email', value: email },
1855
+ configuration: {
1856
+ signingAuthority: { endpoint: 'https://example.com', name: 'example' },
1857
+ webhookUrl: 'https://example.com/finalized',
1858
+ },
1859
+ });
1860
+ await updateInboxCredential(issued.issuanceId, { isAccepted: true });
1861
+ await userB.clients.fullAuth.contactMethods.addContactMethod({
1862
+ type: 'email',
1863
+ value: email,
1864
+ });
1865
+ await userB.clients.fullAuth.contactMethods.verifyContactMethod({
1866
+ token: sendSpy.mock.calls.at(-1)?.[0].templateModel.verificationToken,
1867
+ });
1868
+ addNotificationToQueueSpy.mockClear();
1869
+ return { boostUri, inboxId: issued.issuanceId };
1870
+ };
1871
+
1872
+ it('rolls back a failed received write and completes all delivery tracking on retry', async () => {
1873
+ const { boostUri, inboxId } = await preparePendingBoost('atomic-receipt@test.com');
1874
+ const escrow = (await getInboxCredentialById(inboxId))!.credential;
1875
+ const originalCredentialIds = (await Credential.findMany()).map(record => record.id);
1876
+ const failedWrite = vi
1877
+ .spyOn(credentialRelationships, 'createReceivedCredentialRelationship')
1878
+ .mockRejectedValueOnce(new Error('injected received write failure'));
1879
+ try {
1880
+ await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
1881
+ claimed: 0,
1882
+ errors: 1,
1883
+ deliveries: [],
1884
+ });
1885
+ } finally {
1886
+ failedWrite.mockRestore();
1887
+ }
1888
+ expect(await getInboxCredentialById(inboxId)).toMatchObject({
1889
+ currentStatus: 'PENDING',
1890
+ credential: escrow,
1891
+ });
1892
+ expect((await Credential.findMany()).map(record => record.id).sort()).toEqual(
1893
+ originalCredentialIds.sort()
1894
+ );
1895
+ expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toEqual([]);
1896
+ const auditBeforeRetry = await neogma.queryRunner.run(
1897
+ 'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
1898
+ { id: inboxId }
1899
+ );
1900
+ expect(auditBeforeRetry.records[0].get('count').toNumber()).toBe(0);
1901
+ expect(
1902
+ addNotificationToQueueSpy.mock.calls.some(
1903
+ ([notification]) =>
1904
+ notification.type ===
1905
+ LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR &&
1906
+ notification.data?.inbox?.status === 'PENDING'
1907
+ )
1908
+ ).toBe(true);
1909
+ addNotificationToQueueSpy.mockClear();
1910
+
1911
+ const retry = await userB.clients.fullAuth.inbox.finalize({});
1912
+ expect(retry).toMatchObject({ claimed: 1, errors: 0, deliveries: [{ id: inboxId }] });
1913
+ const finalized = await getInboxCredentialById(inboxId);
1914
+ expect(finalized?.currentStatus).toBe('ISSUED');
1915
+ expect(finalized?.credential).toBeUndefined();
1916
+ expect(
1917
+ await userA.clients.fullAuth.boost.getBoostRecipients({
1918
+ uri: boostUri,
1919
+ includeUnacceptedBoosts: false,
1920
+ })
1921
+ ).toHaveLength(1);
1922
+ expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toHaveLength(1);
1923
+ const auditAfterRetry = await neogma.queryRunner.run(
1924
+ 'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
1925
+ { id: inboxId }
1926
+ );
1927
+ expect(auditAfterRetry.records[0].get('count').toNumber()).toBe(1);
1928
+ expect(
1929
+ addNotificationToQueueSpy.mock.calls.some(
1930
+ ([notification]) =>
1931
+ notification.type === LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR
1932
+ )
1933
+ ).toBe(false);
1934
+ });
1935
+
1936
+ it('rolls back partial claim permissions and grants hooks and the default role on retry', async () => {
1937
+ const { boostUri, inboxId } = await preparePendingBoost('atomic-permissions@test.com');
1938
+ const targetUri = await userA.clients.fullAuth.boost.createBoost({
1939
+ credential: testUnsignedBoost,
1940
+ });
1941
+ await userA.clients.fullAuth.claimHook.createClaimHook({
1942
+ hook: {
1943
+ type: 'GRANT_PERMISSIONS',
1944
+ data: { claimUri: boostUri, targetUri, permissions: { canIssue: true } },
1945
+ },
1946
+ });
1947
+ const failedRole = vi
1948
+ .spyOn(credentialRelationships, 'setDefaultClaimedRole')
1949
+ .mockRejectedValueOnce(new Error('injected claimed-role failure'));
1950
+ try {
1951
+ await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
1952
+ claimed: 0,
1953
+ errors: 1,
1954
+ });
1955
+ } finally {
1956
+ failedRole.mockRestore();
1957
+ }
1958
+ expect(await getInboxCredentialById(inboxId)).toMatchObject({
1959
+ currentStatus: 'PENDING',
1960
+ });
1961
+ expect(
1962
+ (await userB.clients.fullAuth.boost.getBoostPermissions({ uri: targetUri }))
1963
+ .canIssue
1964
+ ).toBe(false);
1965
+ expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toEqual([]);
1966
+
1967
+ await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
1968
+ claimed: 1,
1969
+ errors: 0,
1970
+ });
1971
+ expect(
1972
+ (await userB.clients.fullAuth.boost.getBoostPermissions({ uri: targetUri }))
1973
+ .canIssue
1974
+ ).toBe(true);
1975
+ expect(
1976
+ (await userB.clients.fullAuth.boost.getBoostPermissions({ uri: boostUri }))
1977
+ .canRevoke
1978
+ ).toBe(true);
1979
+ const [received] = await userB.clients.fullAuth.credential.receivedCredentials({});
1980
+ expect(
1981
+ await userB.clients.fullAuth.credential.acceptCredential({ uri: received.uri })
1982
+ ).toBe(true);
1983
+ const roleCounts = await neogma.queryRunner.run(
1984
+ 'MATCH (:Profile {profileId: "userb"})-[r:HAS_ROLE]->(b:Boost) WHERE b.id IN $ids RETURN b.id AS id, count(r) AS count',
1985
+ { ids: [boostUri, targetUri].map(uri => uri.split(':').at(-1)) }
1986
+ );
1987
+ expect(roleCounts.records.map(record => record.get('count').toNumber())).toEqual([
1988
+ 1, 1,
1989
+ ]);
1990
+ });
1991
+
1992
+ it('commits only one accepted delivery and claim audit under concurrent finalization', async () => {
1993
+ const { inboxId } = await preparePendingBoost('atomic-concurrent@test.com');
1994
+ const results = await Promise.all(
1995
+ Array.from({ length: 3 }, () => userB.clients.fullAuth.inbox.finalize({}))
1996
+ );
1997
+ expect(results.reduce((count, result) => count + result.claimed, 0)).toBe(1);
1998
+ expect(results.reduce((count, result) => count + result.errors, 0)).toBe(0);
1999
+ expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toHaveLength(1);
2000
+ const audit = await neogma.queryRunner.run(
2001
+ 'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
2002
+ { id: inboxId }
2003
+ );
2004
+ expect(audit.records[0].get('count').toNumber()).toBe(1);
2005
+ });
2006
+
1835
2007
  it('should not allow finalize without full auth', async () => {
1836
2008
  await expect(noAuthClient.inbox.finalize({})).rejects.toMatchObject({
1837
2009
  code: 'UNAUTHORIZED',
@@ -1912,8 +2084,11 @@ describe('Universal Inbox', () => {
1912
2084
  expect(res.claimed).toBe(2);
1913
2085
  expect(res.errors).toBe(0);
1914
2086
  expect(res.verifiableCredentials).toHaveLength(2);
1915
- // Ensure returned VCs are signed
1916
- for (const vc of res.verifiableCredentials) {
2087
+ // The HTTP contract is encrypted; only the holder decrypts the signed VCs.
2088
+ for (const encrypted of res.verifiableCredentials) {
2089
+ const vc = await userB.learnCard.invoke.decryptDagJwe<VC>(
2090
+ JWEValidator.parse(encrypted)
2091
+ );
1917
2092
  expect(vc.proof).toBeDefined();
1918
2093
  }
1919
2094
 
@@ -2089,12 +2264,13 @@ describe('Universal Inbox', () => {
2089
2264
  const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
2090
2265
 
2091
2266
  try {
2092
- await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
2093
- processed: 1,
2094
- claimed: 1,
2095
- errors: 0,
2096
- verifiableCredentials: [expect.objectContaining({ proof: expect.anything() })],
2097
- });
2267
+ const result = await userB.clients.fullAuth.inbox.finalize({});
2268
+ expect(result).toMatchObject({ processed: 1, claimed: 1, errors: 0 });
2269
+ expect(
2270
+ await userB.learnCard.invoke.decryptDagJwe<VC>(
2271
+ JWEValidator.parse(result.verifiableCredentials[0])
2272
+ )
2273
+ ).toMatchObject({ proof: expect.anything() });
2098
2274
  } finally {
2099
2275
  queryRunnerSpy.mockRestore();
2100
2276
  consoleErrorSpy.mockRestore();
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ computeInlineTemplateHash,
5
+ validateInlineTemplateDataOrThrow,
6
+ validateInlineTemplateOrThrow,
7
+ } from '../src/helpers/inline-template.helpers';
8
+
9
+ describe('inline-template.helpers', () => {
10
+ it('throws developer-readable template validation errors', () => {
11
+ expect(() =>
12
+ validateInlineTemplateOrThrow({
13
+ name: '',
14
+ image: 'http://not-https.example.com/badge.png',
15
+ })
16
+ ).toThrow(/TEMPLATE_INVALID:/);
17
+ });
18
+
19
+ it('throws strict templateData validation errors with suggestions', () => {
20
+ const compiled = computeInlineTemplateHash({
21
+ name: 'Course Completion: {{courseName}}',
22
+ description: 'Awarded to {{learnerName}}',
23
+ });
24
+
25
+ expect(() =>
26
+ validateInlineTemplateDataOrThrow(compiled.variableManifest, {
27
+ courseNmae: 'Broken key',
28
+ learnerName: 'Ada',
29
+ })
30
+ ).toThrow(
31
+ /TEMPLATE_DATA_INVALID: .*templateData.courseName is required.*did you mean "courseName"/
32
+ );
33
+ });
34
+
35
+ it('hashes template content deterministically', () => {
36
+ const first = computeInlineTemplateHash({
37
+ name: 'Course Completion: {{courseName}}',
38
+ description: 'Awarded to {{learnerName}}',
39
+ walletSkills: [{ frameworkId: 'framework-1', id: 'skill-1' }],
40
+ });
41
+ const second = computeInlineTemplateHash({
42
+ name: 'Course Completion: {{courseName}}',
43
+ description: 'Awarded to {{learnerName}}',
44
+ walletSkills: [{ frameworkId: 'framework-1', id: 'skill-1' }],
45
+ });
46
+
47
+ expect(first.contentHash).toBe(second.contentHash);
48
+ });
49
+ });