@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,236 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { AppManifestVersion, AppStoreListing, Integration, Profile } from '@models';
4
+
5
+ import { getUser } from './helpers/getClient';
6
+ import { seedListedApp } from './helpers/app-store.helpers';
7
+
8
+ import type { AppManifest } from '@learncard/types';
9
+
10
+ let owner: Awaited<ReturnType<typeof getUser>>;
11
+ let stranger: Awaited<ReturnType<typeof getUser>>;
12
+ let admin: Awaited<ReturnType<typeof getUser>>;
13
+
14
+ const cleanup = async () => {
15
+ await AppManifestVersion.delete({ detach: true, where: {} });
16
+ await AppStoreListing.delete({ detach: true, where: {} });
17
+ await Integration.delete({ detach: true, where: {} });
18
+ await Profile.delete({ detach: true, where: {} });
19
+ };
20
+
21
+ const manifest = (permissions: string[]): AppManifest => ({
22
+ manifestVersion: 1,
23
+ appUrl: 'https://app.example.com',
24
+ permissions,
25
+ templates: [],
26
+ consentRequests: [],
27
+ featuresLaunched: [],
28
+ counterKeys: [],
29
+ usedLearnerContext: false,
30
+ usedNotifications: false,
31
+ firstCapturedAt: '2026-01-01T00:00:00.000Z',
32
+ lastUpdatedAt: '2026-01-01T00:00:00.000Z',
33
+ });
34
+
35
+ describe('updates to live apps', () => {
36
+ beforeAll(async () => {
37
+ owner = await getUser('a'.repeat(64));
38
+ stranger = await getUser('b'.repeat(64));
39
+ admin = await getUser('d'.repeat(64));
40
+ });
41
+
42
+ beforeEach(async () => {
43
+ await cleanup();
44
+ await owner.clients.fullAuth.profile.createProfile({ profileId: 'listing-owner' });
45
+ await stranger.clients.fullAuth.profile.createProfile({ profileId: 'stranger' });
46
+ await admin.clients.fullAuth.profile.createProfile({ profileId: 'app-store-admin' });
47
+ });
48
+
49
+ afterAll(cleanup);
50
+
51
+ const getOwned = async (listingId: string) =>
52
+ owner.clients.fullAuth.appStore.getListing({ listingId });
53
+
54
+ it('holds edits to a live app until they are approved', async () => {
55
+ const { listing } = await seedListedApp('listing-owner');
56
+
57
+ await owner.clients.fullAuth.appStore.updateListing({
58
+ listingId: listing.listing_id,
59
+ updates: { tagline: 'A brand new tagline', screenshots: ['https://example.com/s.png'] },
60
+ });
61
+
62
+ const publicListing = await stranger.clients.fullAuth.appStore.getPublicListing({
63
+ listingId: listing.listing_id,
64
+ });
65
+ expect(publicListing?.tagline).toBe('A test application');
66
+ expect(publicListing).not.toHaveProperty('pending_update');
67
+
68
+ const owned = await getOwned(listing.listing_id);
69
+ expect(owned?.tagline).toBe('A test application');
70
+ expect(owned?.pending_update).toMatchObject({
71
+ status: 'DRAFT',
72
+ changes: { tagline: 'A brand new tagline', screenshots: ['https://example.com/s.png'] },
73
+ });
74
+ });
75
+
76
+ it('applies an approved update to the live listing', async () => {
77
+ const { listing } = await seedListedApp('listing-owner');
78
+ const listingId = listing.listing_id;
79
+
80
+ await owner.clients.fullAuth.appStore.updateListing({
81
+ listingId,
82
+ updates: { tagline: 'Approved tagline' },
83
+ });
84
+ await owner.clients.fullAuth.appStore.submitListingUpdate({ listingId });
85
+
86
+ expect((await getOwned(listingId))?.pending_update?.status).toBe('PENDING_REVIEW');
87
+
88
+ await admin.clients.fullAuth.appStore.adminReviewListingUpdate({
89
+ listingId,
90
+ approve: true,
91
+ });
92
+
93
+ const owned = await getOwned(listingId);
94
+ expect(owned?.tagline).toBe('Approved tagline');
95
+ expect(owned?.app_listing_status).toBe('LISTED');
96
+ expect(owned?.pending_update).toBeUndefined();
97
+ });
98
+
99
+ it('keeps rejected changes editable without touching the live listing', async () => {
100
+ const { listing } = await seedListedApp('listing-owner');
101
+ const listingId = listing.listing_id;
102
+
103
+ await owner.clients.fullAuth.appStore.updateListing({
104
+ listingId,
105
+ updates: { tagline: 'Rejected tagline' },
106
+ });
107
+ await owner.clients.fullAuth.appStore.submitListingUpdate({ listingId });
108
+ await admin.clients.fullAuth.appStore.adminReviewListingUpdate({
109
+ listingId,
110
+ approve: false,
111
+ });
112
+
113
+ const owned = await getOwned(listingId);
114
+ expect(owned?.tagline).toBe('A test application');
115
+ expect(owned?.pending_update).toMatchObject({
116
+ status: 'DRAFT',
117
+ changes: { tagline: 'Rejected tagline' },
118
+ });
119
+ });
120
+
121
+ it('blocks edits while an update is in review, until it is withdrawn', async () => {
122
+ const { listing } = await seedListedApp('listing-owner');
123
+ const listingId = listing.listing_id;
124
+
125
+ await owner.clients.fullAuth.appStore.updateListing({
126
+ listingId,
127
+ updates: { tagline: 'First' },
128
+ });
129
+ await owner.clients.fullAuth.appStore.submitListingUpdate({ listingId });
130
+
131
+ await expect(
132
+ owner.clients.fullAuth.appStore.updateListing({
133
+ listingId,
134
+ updates: { tagline: 'Second' },
135
+ })
136
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
137
+
138
+ await owner.clients.fullAuth.appStore.withdrawListingUpdate({ listingId });
139
+ await owner.clients.fullAuth.appStore.updateListing({
140
+ listingId,
141
+ updates: { tagline: 'Second' },
142
+ });
143
+
144
+ expect((await getOwned(listingId))?.pending_update).toMatchObject({
145
+ status: 'DRAFT',
146
+ changes: { tagline: 'Second' },
147
+ });
148
+ });
149
+
150
+ it('discards unsubmitted changes', async () => {
151
+ const { listing } = await seedListedApp('listing-owner');
152
+ const listingId = listing.listing_id;
153
+
154
+ await owner.clients.fullAuth.appStore.updateListing({
155
+ listingId,
156
+ updates: { tagline: 'Never mind' },
157
+ });
158
+ await owner.clients.fullAuth.appStore.discardListingUpdate({ listingId });
159
+
160
+ expect((await getOwned(listingId))?.pending_update).toBeUndefined();
161
+ });
162
+
163
+ it('holds new capabilities for a live app instead of switching them on', async () => {
164
+ const { listing, integration } = await seedListedApp('listing-owner');
165
+ const listingId = listing.listing_id;
166
+
167
+ const first = await owner.clients.fullAuth.appStore.submitAppManifest({
168
+ integrationId: integration.id,
169
+ manifest: manifest(['request_identity']),
170
+ });
171
+ const applied = await owner.clients.fullAuth.appStore.applyManifestVersion({
172
+ integrationId: integration.id,
173
+ version: first.version,
174
+ listingId,
175
+ });
176
+
177
+ expect(applied).toMatchObject({ applied: false, pendingReview: true });
178
+ expect((await getOwned(listingId))?.pending_update?.manifest_version).toBe(first.version);
179
+
180
+ await owner.clients.fullAuth.appStore.submitListingUpdate({ listingId });
181
+ await admin.clients.fullAuth.appStore.adminReviewListingUpdate({
182
+ listingId,
183
+ approve: true,
184
+ });
185
+
186
+ const versions = await owner.clients.fullAuth.appStore.getManifestVersions({
187
+ integrationId: integration.id,
188
+ });
189
+ expect(versions.records[0]).toMatchObject({ version: first.version, status: 'active' });
190
+ });
191
+
192
+ it('refuses to submit a localhost address in an update', async () => {
193
+ const { listing } = await seedListedApp('listing-owner');
194
+ const listingId = listing.listing_id;
195
+
196
+ await owner.clients.fullAuth.appStore.updateListing({
197
+ listingId,
198
+ updates: {
199
+ launch_type: 'EMBEDDED_IFRAME',
200
+ launch_config_json: JSON.stringify({ url: 'http://localhost:4321' }),
201
+ },
202
+ });
203
+
204
+ await expect(
205
+ owner.clients.fullAuth.appStore.submitListingUpdate({ listingId })
206
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
207
+ });
208
+
209
+ it('only lets the owner manage updates and only admins review them', async () => {
210
+ const { listing } = await seedListedApp('listing-owner');
211
+ const listingId = listing.listing_id;
212
+
213
+ await owner.clients.fullAuth.appStore.updateListing({
214
+ listingId,
215
+ updates: { tagline: 'Mine' },
216
+ });
217
+
218
+ await expect(
219
+ stranger.clients.fullAuth.appStore.submitListingUpdate({ listingId })
220
+ ).rejects.toBeDefined();
221
+ await expect(
222
+ stranger.clients.fullAuth.appStore.discardListingUpdate({ listingId })
223
+ ).rejects.toBeDefined();
224
+
225
+ await owner.clients.fullAuth.appStore.submitListingUpdate({ listingId });
226
+ await expect(
227
+ owner.clients.fullAuth.appStore.adminReviewListingUpdate({ listingId, approve: true })
228
+ ).rejects.toBeDefined();
229
+
230
+ const pendingForAdmin = await admin.clients.fullAuth.appStore.adminGetAllListings({
231
+ pendingUpdatesOnly: true,
232
+ });
233
+ expect(pendingForAdmin.records.map(record => record.listing_id)).toEqual([listingId]);
234
+ expect(pendingForAdmin.records[0]?.pending_update?.changes.tagline).toBe('Mine');
235
+ });
236
+ });
@@ -893,6 +893,65 @@ describe('AppStoreListing', () => {
893
893
  const after = await readAppStoreListingById(listingId);
894
894
  expect(after).toBeNull();
895
895
  });
896
+
897
+ it('deletes an ARCHIVED (rejected) listing', async () => {
898
+ const integrationId = await seedIntegrationViaRouter(userA);
899
+ const listingId = await seedListingViaRouter(userA, integrationId);
900
+ await setListingStatus(listingId, 'ARCHIVED');
901
+
902
+ const ok = await userA.clients.fullAuth.appStore.deleteListing({ listingId });
903
+ expect(ok).toBe(true);
904
+
905
+ const after = await readAppStoreListingById(listingId);
906
+ expect(after).toBeNull();
907
+ });
908
+
909
+ it('refuses to delete a listing that is in review', async () => {
910
+ const integrationId = await seedIntegrationViaRouter(userA);
911
+ const listingId = await seedListingViaRouter(userA, integrationId);
912
+ await userA.clients.fullAuth.appStore.submitForReview({ listingId });
913
+
914
+ await expect(
915
+ userA.clients.fullAuth.appStore.deleteListing({ listingId })
916
+ ).rejects.toMatchObject({
917
+ code: 'PRECONDITION_FAILED',
918
+ message: 'Only draft apps can be deleted. Withdraw it from review first.',
919
+ });
920
+
921
+ const after = await readAppStoreListingById(listingId);
922
+ expect(after?.app_listing_status).toBe('PENDING_REVIEW');
923
+ });
924
+
925
+ it('refuses to delete a live listing', async () => {
926
+ const integrationId = await seedIntegrationViaRouter(userA);
927
+ const listingId = await seedListingViaRouter(userA, integrationId);
928
+ await setListingStatus(listingId, 'LISTED');
929
+
930
+ await expect(
931
+ userA.clients.fullAuth.appStore.deleteListing({ listingId })
932
+ ).rejects.toMatchObject({
933
+ code: 'PRECONDITION_FAILED',
934
+ message: "Live apps can't be deleted. Remove it from the store first.",
935
+ });
936
+
937
+ const after = await readAppStoreListingById(listingId);
938
+ expect(after?.app_listing_status).toBe('LISTED');
939
+ });
940
+
941
+ it('refuses to delete a live listing even with a pending update in review', async () => {
942
+ const integrationId = await seedIntegrationViaRouter(userA);
943
+ const listingId = await seedListingViaRouter(userA, integrationId);
944
+ await setListingStatus(listingId, 'LISTED');
945
+ await userA.clients.fullAuth.appStore.updateListing({
946
+ listingId,
947
+ updates: { tagline: 'A new tagline' },
948
+ });
949
+ await userA.clients.fullAuth.appStore.submitListingUpdate({ listingId });
950
+
951
+ await expect(
952
+ userA.clients.fullAuth.appStore.deleteListing({ listingId })
953
+ ).rejects.toMatchObject({ code: 'PRECONDITION_FAILED' });
954
+ });
896
955
  });
897
956
 
898
957
  describe('submitForReview', () => {
@@ -2083,12 +2142,15 @@ describe('AppStoreListing', () => {
2083
2142
  let browseResults = await noAuthClient.appStore.browseListedApps();
2084
2143
  expect(browseResults.records.some(r => r.listing_id === listingId)).toBe(true);
2085
2144
 
2086
- // Delete the listing
2087
- await userA.clients.fullAuth.appStore.deleteListing({ listingId });
2145
+ await setListingStatus(listingId, 'ARCHIVED');
2088
2146
 
2089
- // Verify it no longer appears in browse
2090
2147
  browseResults = await noAuthClient.appStore.browseListedApps();
2091
2148
  expect(browseResults.records.some(r => r.listing_id === listingId)).toBe(false);
2149
+
2150
+ await userA.clients.fullAuth.appStore.deleteListing({ listingId });
2151
+
2152
+ const after = await readAppStoreListingById(listingId);
2153
+ expect(after).toBeNull();
2092
2154
  });
2093
2155
 
2094
2156
  it('unlisting an app removes it from public browse', async () => {
@@ -0,0 +1,352 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { randomBytes } from 'node:crypto';
3
+ import type { ConsentFlowDataQuery, ConsentFlowTermsInput } from '@learncard/types';
4
+ import { ConsentFlowTerms, ConsentFlowContract } from '@models';
5
+ import { getUser } from './helpers/getClient';
6
+ import { minimalContract, minimalTerms, normalContract, normalFullTerms } from './helpers/contract';
7
+
8
+ describe('consented data boundaries', () => {
9
+ let owner: Awaited<ReturnType<typeof getUser>>;
10
+ let learner: Awaited<ReturnType<typeof getUser>>;
11
+ let learnerDid: string;
12
+
13
+ beforeEach(async () => {
14
+ const suffix = randomBytes(5).toString('hex');
15
+ owner = await getUser(randomBytes(32).toString('hex'));
16
+ learner = await getUser(randomBytes(32).toString('hex'));
17
+ await owner.clients.fullAuth.profile.createProfile({
18
+ profileId: `boundary-owner-${suffix}`,
19
+ });
20
+ await learner.clients.fullAuth.profile.createProfile({
21
+ profileId: `boundary-user-${suffix}`,
22
+ });
23
+ learnerDid = (await learner.clients.fullAuth.profile.getProfile())!.did;
24
+ });
25
+
26
+ const consent = async ({
27
+ terms = normalFullTerms,
28
+ expiresAt,
29
+ contractExpiresAt,
30
+ oneTime,
31
+ minimal = false,
32
+ }: {
33
+ terms?: ConsentFlowTermsInput;
34
+ expiresAt?: string;
35
+ contractExpiresAt?: string;
36
+ oneTime?: boolean;
37
+ minimal?: boolean;
38
+ } = {}) => {
39
+ const contractUri = await owner.clients.fullAuth.contracts.createConsentFlowContract({
40
+ name: 'Synthetic boundary contract',
41
+ contract: minimal ? minimalContract : normalContract,
42
+ expiresAt: contractExpiresAt,
43
+ });
44
+ const { termsUri } = await learner.clients.fullAuth.contracts.consentToContract({
45
+ contractUri,
46
+ terms,
47
+ expiresAt,
48
+ oneTime,
49
+ });
50
+ return { contractUri, termsUri };
51
+ };
52
+
53
+ const readAll = async (
54
+ contractUri: string,
55
+ query: ConsentFlowDataQuery = {},
56
+ pagination: { limit?: number; cursor?: string } = {}
57
+ ) => [
58
+ await owner.clients.fullAuth.contracts.getConsentedDataForContract({
59
+ uri: contractUri,
60
+ query,
61
+ ...pagination,
62
+ }),
63
+ await owner.clients.fullAuth.contracts.getConsentedDataForDid({
64
+ did: learnerDid,
65
+ query: { ...query, id: contractUri.split(':').at(-1)! },
66
+ ...pagination,
67
+ }),
68
+ await owner.clients.fullAuth.contracts.getConsentedData({ query, ...pagination }),
69
+ ];
70
+
71
+ it('returns live unexpired personal fields and selected credentials on every route', async () => {
72
+ const { contractUri } = await consent({
73
+ expiresAt: '2100-01-01T00:00:00+05:30',
74
+ contractExpiresAt: '2100-01-01T00:00:00-05:00',
75
+ });
76
+ for (const response of await readAll(contractUri)) {
77
+ expect(response.records).toHaveLength(1);
78
+ expect(response.records[0]!.personal).toEqual(normalFullTerms.read.personal);
79
+ expect(JSON.stringify(response)).toContain('achievement1');
80
+ }
81
+ });
82
+
83
+ it.each([
84
+ [{ name: true }, 1],
85
+ [{ name: false }, 0],
86
+ [{ missing: false }, 1],
87
+ [{ name: true, missing: true }, 0],
88
+ ] as const)(
89
+ 'filters personal field presence without treating empty strings as absent: %j',
90
+ async (personal, count) => {
91
+ const terms = structuredClone(normalFullTerms);
92
+ terms.read.personal = { ...terms.read.personal, name: '' };
93
+ const { contractUri } = await consent({ terms });
94
+ for (const response of await readAll(contractUri, { personal })) {
95
+ expect(response.records).toHaveLength(count);
96
+ }
97
+ }
98
+ );
99
+
100
+ it('treats legacy empty expiry strings as no expiry, including category expiry', async () => {
101
+ const terms = structuredClone(normalFullTerms);
102
+ terms.read.credentials.categories.Achievement!.shareUntil = '';
103
+ const { contractUri, termsUri } = await consent({ terms });
104
+ await learner.clients.fullAuth.contracts.updateConsentedContractTerms({
105
+ uri: termsUri,
106
+ terms,
107
+ expiresAt: '',
108
+ });
109
+ for (const response of await readAll(contractUri, {
110
+ credentials: { categories: { Achievement: true } },
111
+ })) {
112
+ expect(response.records).toHaveLength(1);
113
+ }
114
+ await learner.clients.fullAuth.contracts.syncCredentialsToContract({
115
+ termsUri,
116
+ categories: { Achievement: ['urn:blank-expiry:copy'] },
117
+ });
118
+ });
119
+
120
+ it.each([true, false, undefined] as const)(
121
+ 'preserves anonymize presence filters for stored value %s',
122
+ async anonymize => {
123
+ const terms = structuredClone(normalFullTerms);
124
+ if (anonymize === undefined) delete terms.read.anonymize;
125
+ else terms.read.anonymize = anonymize;
126
+ const { contractUri } = await consent({ terms });
127
+ for (const filter of [true, false, undefined]) {
128
+ // The per-DID query does not expose anonymize; check its two supported routes.
129
+ const query = filter === undefined ? {} : { anonymize: filter };
130
+ for (const response of [
131
+ await owner.clients.fullAuth.contracts.getConsentedDataForContract({
132
+ uri: contractUri,
133
+ query,
134
+ limit: 1,
135
+ }),
136
+ await owner.clients.fullAuth.contracts.getConsentedData({ query, limit: 1 }),
137
+ ]) {
138
+ expect(response.records).toHaveLength(
139
+ filter === undefined || filter === (anonymize !== undefined) ? 1 : 0
140
+ );
141
+ expect(response.hasMore).toBe(false);
142
+ }
143
+ }
144
+ }
145
+ );
146
+
147
+ it.each(['terms', 'contract'] as const)(
148
+ 'allows locked sync when %s expiry contains only whitespace',
149
+ async kind => {
150
+ const { contractUri, termsUri } = await consent();
151
+ const model = kind === 'terms' ? ConsentFlowTerms : ConsentFlowContract;
152
+ const uri = kind === 'terms' ? termsUri : contractUri;
153
+ await model.update({ expiresAt: ' ' }, { where: { id: uri.split(':').at(-1)! } });
154
+ await expect(
155
+ learner.clients.fullAuth.contracts.syncCredentialsToContract({
156
+ termsUri,
157
+ categories: { Achievement: ['urn:whitespace-expiry:copy'] },
158
+ })
159
+ ).resolves.toBe(true);
160
+ }
161
+ );
162
+
163
+ it('excludes withdrawn consent while retaining its audit history', async () => {
164
+ const { contractUri, termsUri } = await consent();
165
+ await learner.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
166
+ const history = await learner.clients.fullAuth.contracts.getConsentedContracts();
167
+ expect(history.records.find(record => record.uri === termsUri)?.status).toBe('withdrawn');
168
+ for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
169
+ });
170
+
171
+ it.each(['terms', 'contract'] as const)('excludes expired %s on every route', async kind => {
172
+ const past = '2000-01-01T00:00:00.000Z';
173
+ const { contractUri } = await consent({
174
+ expiresAt: kind === 'terms' ? past : undefined,
175
+ });
176
+ // Seed an existing consent that subsequently expires, rather than consent to an expired contract.
177
+ if (kind === 'contract')
178
+ await ConsentFlowContract.update(
179
+ { expiresAt: past },
180
+ { where: { id: contractUri.split(':').at(-1)! } }
181
+ );
182
+ for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
183
+ });
184
+
185
+ it('preserves one-time snapshots without permitting ongoing sync, then honors withdrawal', async () => {
186
+ const { contractUri, termsUri } = await consent({ oneTime: true });
187
+ const history = await learner.clients.fullAuth.contracts.getConsentedContracts();
188
+ expect(history.records.find(record => record.uri === termsUri)?.status).toBe('stale');
189
+ for (const response of await readAll(contractUri)) {
190
+ expect(response.records).toHaveLength(1);
191
+ expect(JSON.stringify(response)).toContain('achievement1');
192
+ }
193
+ await expect(
194
+ learner.clients.fullAuth.contracts.syncCredentialsToContract({
195
+ termsUri,
196
+ categories: { Achievement: ['urn:synthetic:new'] },
197
+ })
198
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
199
+ await learner.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
200
+ for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
201
+ });
202
+
203
+ it('excludes stale consent without the explicit one-time flag', async () => {
204
+ const { contractUri, termsUri } = await consent();
205
+ await ConsentFlowTerms.update(
206
+ { status: 'stale' },
207
+ { where: { id: termsUri.split(':').at(-1)! } }
208
+ );
209
+ for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
210
+ });
211
+
212
+ it('also expires one-time snapshots', async () => {
213
+ const { contractUri } = await consent({ oneTime: true, expiresAt: '2000-01-01T00:00:00Z' });
214
+ for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
215
+ });
216
+
217
+ it.each(['disabled', 'unspecified', 'expired'] as const)(
218
+ 'excludes %s category URIs and filters by permitted data on every route',
219
+ async kind => {
220
+ const terms = structuredClone(normalFullTerms);
221
+ terms.read.credentials.categories.Achievement = {
222
+ shared: ['urn:synthetic:blocked'],
223
+ sharing: kind === 'expired' ? true : kind === 'disabled' ? false : undefined,
224
+ shareUntil: kind === 'expired' ? '2000-01-01T00:00:00Z' : undefined,
225
+ };
226
+ const { contractUri } = await consent({ terms });
227
+ for (const response of await readAll(contractUri)) {
228
+ expect(response.records).toHaveLength(1);
229
+ expect(response.records[0]!.personal).toEqual(normalFullTerms.read.personal);
230
+ expect(JSON.stringify(response)).not.toContain('urn:synthetic:blocked');
231
+ expect(JSON.stringify(response)).toContain('id1');
232
+ }
233
+ for (const response of await readAll(contractUri, {
234
+ credentials: { categories: { Achievement: true } },
235
+ })) {
236
+ expect(response.records).toEqual([]);
237
+ }
238
+ for (const response of await readAll(contractUri, {
239
+ credentials: { categories: { Achievement: false } },
240
+ })) {
241
+ expect(response.records).toHaveLength(1);
242
+ expect(JSON.stringify(response)).not.toContain('urn:synthetic:blocked');
243
+ }
244
+ }
245
+ );
246
+
247
+ it('returns an empty credential collection for valid terms without categories', async () => {
248
+ const { contractUri } = await consent({ terms: minimalTerms, minimal: true });
249
+ const [byContract, byDid, aggregate] = await readAll(contractUri);
250
+ expect(byContract!.records[0]!.credentials).toEqual({ categories: {} });
251
+ expect(aggregate!.records[0]!.credentials).toEqual({ categories: {} });
252
+ expect(byDid!.records[0]!.credentials).toEqual([]);
253
+ expect(byDid!.records[0]!.personal).toEqual(minimalTerms.read.personal);
254
+ });
255
+
256
+ it('hides saved URIs when credential sharing is disabled globally', async () => {
257
+ const terms = structuredClone(normalFullTerms);
258
+ terms.read.credentials.sharing = false;
259
+ const { contractUri } = await consent({ terms });
260
+ for (const response of await readAll(contractUri)) {
261
+ expect(response.records).toHaveLength(1);
262
+ expect(JSON.stringify(response)).not.toContain('achievement1');
263
+ expect(JSON.stringify(response)).not.toContain('id1');
264
+ }
265
+ for (const response of await readAll(contractUri, {
266
+ credentials: { categories: { Achievement: true } },
267
+ })) {
268
+ expect(response.records).toEqual([]);
269
+ }
270
+ for (const response of await readAll(contractUri, {
271
+ credentials: { categories: { Achievement: false } },
272
+ })) {
273
+ expect(response.records).toHaveLength(1);
274
+ }
275
+ });
276
+
277
+ it.each(['terms', 'contract'] as const)(
278
+ 'excludes malformed legacy %s expiry without failing the query',
279
+ async kind => {
280
+ const { contractUri, termsUri } = await consent();
281
+ const model = kind === 'terms' ? ConsentFlowTerms : ConsentFlowContract;
282
+ await model.update(
283
+ { expiresAt: 'invalid-legacy-date' },
284
+ { where: { id: (kind === 'terms' ? termsUri : contractUri).split(':').at(-1)! } }
285
+ );
286
+ for (const response of await readAll(contractUri)) expect(response.records).toEqual([]);
287
+ }
288
+ );
289
+
290
+ it('projects only requested categories consistently', async () => {
291
+ const { contractUri } = await consent();
292
+ for (const response of await readAll(contractUri, {
293
+ credentials: { categories: { Achievement: true } },
294
+ })) {
295
+ expect(JSON.stringify(response)).toContain('achievement1');
296
+ expect(JSON.stringify(response)).not.toContain('id1');
297
+ }
298
+ });
299
+
300
+ it('applies eligibility before pagination and accepts a cursor on every route', async () => {
301
+ const { contractUri, termsUri } = await consent();
302
+ const secondLearner = await getUser(randomBytes(32).toString('hex'));
303
+ await secondLearner.clients.fullAuth.profile.createProfile({
304
+ profileId: `boundary-second-${randomBytes(5).toString('hex')}`,
305
+ });
306
+ await secondLearner.clients.fullAuth.contracts.consentToContract({
307
+ contractUri,
308
+ terms: normalFullTerms,
309
+ expiresAt: '2000-01-01T00:00:00Z',
310
+ });
311
+ // Put the expired row first, so filtering after LIMIT would hide the live row.
312
+ await ConsentFlowTerms.update(
313
+ { updatedAt: '2001-01-01T00:00:00Z' },
314
+ {
315
+ where: { id: termsUri.split(':').at(-1)! },
316
+ }
317
+ );
318
+ for (const response of await readAll(contractUri, {}, { limit: 1 })) {
319
+ expect(response.records).toHaveLength(1);
320
+ expect(response.hasMore).toBe(false);
321
+ }
322
+ for (const response of await readAll(
323
+ contractUri,
324
+ {},
325
+ { cursor: '2001-01-01T00:00:00Z', limit: 1 }
326
+ )) {
327
+ expect(response.records).toEqual([]);
328
+ }
329
+ });
330
+
331
+ it('paginates per-DID data across contracts after excluding expired consent', async () => {
332
+ const { termsUri } = await consent();
333
+ await ConsentFlowTerms.update(
334
+ { updatedAt: '2001-01-01T00:00:00Z' },
335
+ { where: { id: termsUri.split(':').at(-1)! } }
336
+ );
337
+ await consent({ expiresAt: '2000-01-01T00:00:00Z' });
338
+ const response = await owner.clients.fullAuth.contracts.getConsentedDataForDid({
339
+ did: learnerDid,
340
+ limit: 1,
341
+ });
342
+ expect(response.records).toHaveLength(1);
343
+ expect(response.hasMore).toBe(false);
344
+ expect(response.cursor).toBe('2001-01-01T00:00:00Z');
345
+ const next = await owner.clients.fullAuth.contracts.getConsentedDataForDid({
346
+ did: learnerDid,
347
+ limit: 1,
348
+ cursor: response.cursor,
349
+ });
350
+ expect(next.records).toEqual([]);
351
+ });
352
+ });