@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
@@ -2,10 +2,20 @@ import { TRPCError } from '@trpc/server';
2
2
  import { v4 as uuid } from 'uuid';
3
3
  import { z } from 'zod';
4
4
  import {
5
+ PendingListingUpdateValidator,
6
+ AppManifestDiffValidator,
7
+ AppManifestValidator,
8
+ AppManifestVersionValidator,
5
9
  AppEventValidator,
6
10
  LCNNotificationTypeEnumValidator,
7
11
  SendNotificationEventValidator,
8
12
  } from '@learncard/types';
13
+ import { mergeCapturedManifests } from '@learncard/partner-connect-core';
14
+ import type {
15
+ ConsentRequest,
16
+ InlineCredentialTemplate,
17
+ NormalizedConsentScopes,
18
+ } from '@learncard/partner-connect-core';
9
19
  import type { UnsignedVC } from '@learncard/types';
10
20
  import { isVC2Format, checkAppInstallEligibility, calculateAgeFromDob } from '@learncard/helpers';
11
21
  import type { ProfileType } from 'types/profile';
@@ -18,18 +28,28 @@ import { addNotificationToQueue } from '@helpers/notifications.helpers';
18
28
  import { getNotificationMessage } from '@helpers/notificationMessages';
19
29
  import { resolveRecipientLocale } from '@helpers/getRecipientLocale.helpers';
20
30
  import { PerfTracker } from '@helpers/perf';
31
+ import {
32
+ buildConsentFlowContractFromScopes,
33
+ buildScopedConsentContractName,
34
+ computeConsentScopeHash,
35
+ normalizeConsentScopes,
36
+ } from '@helpers/consent-scopes.helpers';
37
+ import { diffManifests, hashManifest } from '@helpers/app-manifest.helpers';
21
38
  import cache from '@cache';
22
39
  import {
23
40
  getBoostForListingCached,
41
+ getConsentContractForListingByScopeHashCached,
24
42
  readAppStoreListingByIdCached,
25
43
  getIntegrationForListingCached,
26
44
  getOwnerProfileForIntegrationCached,
27
45
  getPrimarySigningAuthorityForListingCached,
28
46
  getPrimarySigningAuthorityForIntegrationCached,
47
+ invalidateBoostForListingCache,
48
+ invalidateConsentContractForListingByScopeHashCache,
29
49
  } from '@cache/app-store.caches';
30
50
  import { logCredentialSent } from '@helpers/activity.helpers';
31
51
  import { getCredentialUri } from '@helpers/credential.helpers';
32
- import { resolveUri } from '@helpers/uri.helpers';
52
+ import { constructUri, resolveUri } from '@helpers/uri.helpers';
33
53
  import { inflateObject } from '@helpers/objects.helpers';
34
54
  import { getAvailableAppSlug } from '@helpers/slug.helpers';
35
55
  import {
@@ -52,13 +72,17 @@ import {
52
72
  countInstalledAppsForProfile,
53
73
  checkIfProfileInstalledApp,
54
74
  } from '@accesslayer/app-store-listing/read';
55
- import { updateAppStoreListing } from '@accesslayer/app-store-listing/update';
75
+ import {
76
+ clearPendingListingUpdate,
77
+ updateAppStoreListing,
78
+ } from '@accesslayer/app-store-listing/update';
56
79
  import { deleteAppStoreListing } from '@accesslayer/app-store-listing/delete';
57
80
  import {
58
81
  associateListingWithIntegration,
59
82
  associateListingWithSubmitter,
60
83
  installAppForProfile,
61
84
  associateBoostWithListing,
85
+ associateConsentContractWithListing,
62
86
  } from '@accesslayer/app-store-listing/relationships/create';
63
87
  import {
64
88
  uninstallAppForProfile,
@@ -91,11 +115,13 @@ import {
91
115
  PromotionLevel,
92
116
  AppStoreListingValidator,
93
117
  AgeRating,
118
+ StoredPendingListingUpdateValidator,
94
119
  } from 'types/app-store-listing';
95
120
  import type {
96
121
  AppStoreListingCreateType,
97
122
  AppStoreListingType,
98
123
  AppStoreListingUpdateType,
124
+ StoredPendingListingUpdate,
99
125
  } from 'types/app-store-listing';
100
126
  import { getBoostByUri } from '@accesslayer/boost/read';
101
127
  import {
@@ -104,10 +130,32 @@ import {
104
130
  isDraftBoost,
105
131
  appendTemplateEvidenceToCredential,
106
132
  } from '@helpers/boost.helpers';
133
+ import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
107
134
  import { createBoostForListing } from '@accesslayer/boost/create';
135
+ import { createConsentFlowContract } from '@accesslayer/consentflowcontract/create';
136
+ import { createAppManifestVersion } from '@accesslayer/app-manifest-version/create';
137
+ import {
138
+ getActiveManifestVersionForIntegration,
139
+ getLatestManifestVersionForIntegration,
140
+ getManifestVersionForIntegration,
141
+ getManifestVersionsForIntegration,
142
+ } from '@accesslayer/app-manifest-version/read';
143
+ import {
144
+ associateListingWithManifestVersion,
145
+ markManifestVersionsSuperseded,
146
+ updateAppManifestVersion,
147
+ } from '@accesslayer/app-manifest-version/update';
108
148
  import { setBoostAsParent } from '@accesslayer/boost/relationships/create';
109
149
  import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
110
150
  import type { IssuedCredential } from '../types/credential';
151
+ import {
152
+ computeInlineTemplateHash,
153
+ ensureManagedSigningAuthorityForListing,
154
+ getInlineTemplateVersion,
155
+ readInlineTemplateBoostMeta,
156
+ upsertInlineTemplateBoostForListing,
157
+ validateInlineTemplateDataOrThrow,
158
+ } from '@helpers/inline-template.helpers';
111
159
  import {
112
160
  renderBoostTemplate,
113
161
  parseRenderedTemplate,
@@ -178,8 +226,14 @@ const isAllowedImageUrl = (url: string): boolean => {
178
226
  const isValidIframeUrl = (url: string): boolean => {
179
227
  try {
180
228
  const parsed = new URL(url);
181
- // Only allow https URLs for iframes
182
- if (parsed.protocol !== 'https:') return false;
229
+ // Only allow https URLs for iframes (except for local preview of draft listings)
230
+ if (parsed.protocol !== 'https:') {
231
+ const isLocalhost =
232
+ parsed.hostname === 'localhost' ||
233
+ parsed.hostname === '127.0.0.1' ||
234
+ parsed.hostname === '[::1]';
235
+ if (!isLocalhost) return false;
236
+ }
183
237
  // Block javascript:, data:, and vbscript: schemes (already handled by URL parsing, but be explicit)
184
238
  const lowerUrl = url.toLowerCase();
185
239
  if (
@@ -291,7 +345,14 @@ const iframeUrlRefinement = (
291
345
  if (config.iframeUrl && !isValidIframeUrl(config.iframeUrl)) {
292
346
  ctx.addIssue({
293
347
  code: z.ZodIssueCode.custom,
294
- message: 'Iframe URL must be a valid HTTPS URL',
348
+ message: 'Iframe URL must be a valid HTTPS URL (or localhost for testing)',
349
+ path: ['launch_config_json'],
350
+ });
351
+ }
352
+ if (config.url && !isValidIframeUrl(config.url)) {
353
+ ctx.addIssue({
354
+ code: z.ZodIssueCode.custom,
355
+ message: 'URL must be a valid HTTPS URL (or localhost for testing)',
295
356
  path: ['launch_config_json'],
296
357
  });
297
358
  }
@@ -301,12 +362,16 @@ const iframeUrlRefinement = (
301
362
  }
302
363
  };
303
364
 
365
+ type PendingStorageField =
366
+ 'pending_update_json' | 'pending_update_status' | 'pending_update_submitted_at';
367
+
304
368
  type AppStoreListingResponse<T extends AppStoreListingType> = Omit<
305
369
  T,
306
- 'highlights_json' | 'screenshots_json'
370
+ 'highlights_json' | 'screenshots_json' | PendingStorageField
307
371
  > & {
308
372
  highlights?: string[];
309
373
  screenshots?: string[];
374
+ pending_update?: z.infer<typeof PendingListingUpdateValidator>;
310
375
  };
311
376
 
312
377
  type ListingStorageInput<T extends Record<string, unknown>> = Omit<
@@ -320,18 +385,72 @@ type ListingStorageInput<T extends Record<string, unknown>> = Omit<
320
385
  // Helper to strip sensitive fields (contact_email) from listings for public responses
321
386
  const stripSensitiveFields = <T extends Record<string, unknown>>(
322
387
  listing: T
323
- ): Omit<T, 'contact_email'> => {
324
- const { contact_email, ...rest } = listing;
325
- return rest as Omit<T, 'contact_email'>;
388
+ ): Omit<T, 'contact_email' | 'pending_update'> => {
389
+ const { contact_email, pending_update, ...rest } = listing;
390
+ return rest as Omit<T, 'contact_email' | 'pending_update'>;
391
+ };
392
+
393
+ const parseJsonArray = (value: unknown): string[] | undefined => {
394
+ if (typeof value !== 'string' || !value) return undefined;
395
+ try {
396
+ const parsed: unknown = JSON.parse(value);
397
+ return Array.isArray(parsed) ? parsed.filter(item => typeof item === 'string') : [];
398
+ } catch {
399
+ return [];
400
+ }
401
+ };
402
+
403
+ const readPendingUpdate = (listing: AppStoreListingType): StoredPendingListingUpdate | null => {
404
+ if (!listing.pending_update_json) return null;
405
+ try {
406
+ const parsed = StoredPendingListingUpdateValidator.safeParse(
407
+ JSON.parse(listing.pending_update_json)
408
+ );
409
+ return parsed.success ? parsed.data : null;
410
+ } catch {
411
+ return null;
412
+ }
413
+ };
414
+
415
+ const toPendingUpdateResponse = (
416
+ listing: AppStoreListingType
417
+ ): z.infer<typeof PendingListingUpdateValidator> | undefined => {
418
+ const pending = readPendingUpdate(listing);
419
+ if (!pending || !listing.pending_update_status) return undefined;
420
+
421
+ const { highlights_json, screenshots_json, ...changes } = pending.changes;
422
+ const highlights = parseJsonArray(highlights_json);
423
+ const screenshots = parseJsonArray(screenshots_json);
424
+
425
+ return PendingListingUpdateValidator.parse({
426
+ status: listing.pending_update_status,
427
+ submitted_at: listing.pending_update_submitted_at,
428
+ changes: {
429
+ ...changes,
430
+ ...(highlights ? { highlights } : {}),
431
+ ...(screenshots ? { screenshots } : {}),
432
+ },
433
+ manifest_version: pending.manifestVersion,
434
+ });
326
435
  };
327
436
 
328
437
  // Helper to transform listing for API response (JSON strings -> arrays)
329
438
  const transformListingForResponse = <T extends AppStoreListingType & Record<string, unknown>>(
330
439
  listing: T
331
440
  ): AppStoreListingResponse<T> => {
332
- const { highlights_json, screenshots_json, ...rest } = listing;
441
+ const {
442
+ highlights_json,
443
+ screenshots_json,
444
+ pending_update_json,
445
+ pending_update_status,
446
+ pending_update_submitted_at,
447
+ ...rest
448
+ } = listing;
333
449
  const result = { ...rest } as AppStoreListingResponse<T>;
334
450
 
451
+ const pendingUpdate = toPendingUpdateResponse(listing);
452
+ if (pendingUpdate) result.pending_update = pendingUpdate;
453
+
335
454
  if (highlights_json) {
336
455
  try {
337
456
  result.highlights = JSON.parse(highlights_json);
@@ -441,9 +560,13 @@ const AppStoreListingResponseValidator = AppStoreListingValidator.extend({
441
560
  highlights: z.array(z.string()).optional(),
442
561
  screenshots: z.array(z.string()).optional(),
443
562
  submitter: AppStoreListingSubmitterValidator.optional(),
563
+ pending_update: PendingListingUpdateValidator.optional(),
444
564
  }).omit({
445
565
  highlights_json: true,
446
566
  screenshots_json: true,
567
+ pending_update_json: true,
568
+ pending_update_status: true,
569
+ pending_update_submitted_at: true,
447
570
  });
448
571
 
449
572
  const PaginatedAppStoreListingsValidator = z.object({
@@ -479,6 +602,45 @@ const PaginatedAppCredentialsValidator = z.object({
479
602
  totalCount: z.number(),
480
603
  });
481
604
 
605
+ const AppManifestVersionSummaryValidator = AppManifestVersionValidator.omit({ manifest: true });
606
+
607
+ const PaginatedAppManifestVersionsValidator = z.object({
608
+ hasMore: z.boolean(),
609
+ cursor: z.string().optional(),
610
+ records: z.array(AppManifestVersionSummaryValidator),
611
+ });
612
+
613
+ const SubmitAppManifestResponseValidator = z.object({
614
+ version: z.number().int().min(1),
615
+ manifestHash: z.string(),
616
+ diff: AppManifestDiffValidator.nullable(),
617
+ noop: z.boolean(),
618
+ });
619
+
620
+ const ApplyManifestVersionResponseValidator = z.object({
621
+ applied: z.boolean(),
622
+ pendingReview: z.boolean().optional(),
623
+ version: z.number().int().min(1),
624
+ reconciled: z.object({
625
+ templatesUpserted: z.number().int().min(0),
626
+ templatesSkipped: z.number().int().min(0),
627
+ contractsUpserted: z.number().int().min(0),
628
+ contractsSkipped: z.number().int().min(0),
629
+ signingAuthorityEnsured: z.boolean(),
630
+ }),
631
+ });
632
+
633
+ const toManifestVersionSummary = (
634
+ manifestVersion: z.infer<typeof AppManifestVersionValidator>
635
+ ): z.infer<typeof AppManifestVersionSummaryValidator> => ({
636
+ id: manifestVersion.id,
637
+ version: manifestVersion.version,
638
+ manifestHash: manifestVersion.manifestHash,
639
+ status: manifestVersion.status,
640
+ createdAt: manifestVersion.createdAt,
641
+ activatedAt: manifestVersion.activatedAt,
642
+ });
643
+
482
644
  // Helper to verify integration ownership
483
645
  const verifyIntegrationOwnership = async (integrationId: string, profileId: string) => {
484
646
  const associated = await isIntegrationAssociatedWithProfile(integrationId, profileId);
@@ -557,12 +719,17 @@ export const handleSendCredentialEvent = async (
557
719
  ): Promise<Record<string, unknown>> => {
558
720
  const perf = perfTracker ?? new PerfTracker('handleSendCredentialEvent');
559
721
 
560
- const templateAlias = event.templateAlias as string | undefined;
722
+ const templateAlias =
723
+ (event.templateAlias as string | undefined) ?? (event.alias as string | undefined);
724
+ const inlineTemplate = event.template as
725
+ import('@learncard/partner-connect-core').InlineCredentialTemplate | undefined;
561
726
  const templateData = event.templateData as Record<string, unknown> | undefined;
562
727
  const preventDuplicateClaim = Boolean(event.preventDuplicateClaim);
728
+ const isInlineTemplateEvent = Boolean(inlineTemplate);
729
+ let templateVersion: number | undefined = undefined;
563
730
 
564
731
  if (!templateAlias) {
565
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'templateAlias required' });
732
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'templateAlias or alias required' });
566
733
  }
567
734
 
568
735
  // LC-1644 Phase 1b: Fire the four input-only lookups in parallel. All depend only on
@@ -571,17 +738,16 @@ export const handleSendCredentialEvent = async (
571
738
  // parallel batching + 30s LRU cache on the first three below compounds to ~50-150ms
572
739
  // saved on warm lambda invocations. targetProfile is keyed by the requester's
573
740
  // profileId (varies per user) so it's not cached.
574
- const [boostResult, listing, integration, targetProfile] = await Promise.all([
575
- getBoostForListingCached(listingId, templateAlias, ctx.domain),
741
+ const [cachedBoostResult, listing, integration, targetProfile] = await Promise.all([
742
+ isInlineTemplateEvent
743
+ ? Promise.resolve(null)
744
+ : getBoostForListingCached(listingId, templateAlias, ctx.domain),
576
745
  readAppStoreListingByIdCached(listingId),
577
746
  getIntegrationForListingCached(listingId),
578
747
  getProfilesByProfileIds([profile.profileId]),
579
748
  ]);
580
749
  perf.mark('parallelReads');
581
750
 
582
- if (!boostResult) {
583
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Boost not found for this app' });
584
- }
585
751
  if (!listing) {
586
752
  throw new TRPCError({ code: 'NOT_FOUND', message: 'App Store Listing not found' });
587
753
  }
@@ -593,7 +759,30 @@ export const handleSendCredentialEvent = async (
593
759
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found' });
594
760
  }
595
761
 
762
+ const boostResult = isInlineTemplateEvent
763
+ ? await upsertInlineTemplateBoostForListing({
764
+ listing,
765
+ integration,
766
+ templateAlias,
767
+ template: inlineTemplate!,
768
+ domain: ctx.domain,
769
+ })
770
+ : cachedBoostResult;
771
+
772
+ if (!boostResult) {
773
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Boost not found for this app' });
774
+ }
775
+
776
+ if (isInlineTemplateEvent) {
777
+ const inlineBoostResult = boostResult as Awaited<
778
+ ReturnType<typeof upsertInlineTemplateBoostForListing>
779
+ >;
780
+ validateInlineTemplateDataOrThrow(inlineBoostResult.manifest, templateData);
781
+ templateVersion = inlineBoostResult.templateVersion;
782
+ }
783
+
596
784
  const { boost, boostUri } = boostResult;
785
+ templateVersion ??= getInlineTemplateVersion(boost);
597
786
 
598
787
  // NOTE: This is best-effort duplicate prevention, not a guarantee.
599
788
  // There is a race condition window between checking and creating the credential
@@ -617,6 +806,7 @@ export const handleSendCredentialEvent = async (
617
806
  receivedDate: existingCredential.receivedDate ?? existingCredential.sentDate,
618
807
  status: existingCredential.status,
619
808
  boostUri,
809
+ ...(templateVersion !== undefined ? { templateVersion } : {}),
620
810
  // credential not included — frontend skips modal when alreadyClaimed=true
621
811
  };
622
812
  }
@@ -651,17 +841,19 @@ export const handleSendCredentialEvent = async (
651
841
  listingSa || integrationSa
652
842
  ? undefined
653
843
  : await getPrimarySigningAuthorityForUser(integrationOwner);
654
- const sa = listingSa ?? integrationSa ?? ownerSa;
844
+ const sa =
845
+ listingSa ??
846
+ integrationSa ??
847
+ ownerSa ??
848
+ (await ensureManagedSigningAuthorityForListing(
849
+ listing,
850
+ integration,
851
+ integrationOwner,
852
+ ctx.domain
853
+ ));
655
854
 
656
855
  perf.mark('saResolve');
657
856
 
658
- if (!sa) {
659
- throw new TRPCError({
660
- code: 'NOT_FOUND',
661
- message: 'No signing authority configured for this app',
662
- });
663
- }
664
-
665
857
  // Build unsigned credential from boost template
666
858
  let unsignedVc: UnsignedVC;
667
859
 
@@ -692,17 +884,7 @@ export const handleSendCredentialEvent = async (
692
884
 
693
885
  const targetDid = getDidWeb(ctx.domain, target.profileId);
694
886
 
695
- if (Array.isArray(unsignedVc.credentialSubject)) {
696
- unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
697
- ...subject,
698
- id: targetDid,
699
- }));
700
- } else {
701
- unsignedVc.credentialSubject = {
702
- ...unsignedVc.credentialSubject,
703
- id: targetDid,
704
- };
705
- }
887
+ setCredentialSubjectIds(unsignedVc, targetDid);
706
888
 
707
889
  if (unsignedVc?.type?.includes('BoostCredential')) {
708
890
  unsignedVc.boostId = boostUri;
@@ -835,6 +1017,7 @@ export const handleSendCredentialEvent = async (
835
1017
  return {
836
1018
  credentialUri,
837
1019
  boostUri,
1020
+ ...(templateVersion !== undefined ? { templateVersion } : {}),
838
1021
  // LC-1644: pre-resolved (enriched) credential so the frontend modal
839
1022
  // can render the proper title + issuer info without shimmer or
840
1023
  // background re-fetch. See enrichment block above for details.
@@ -842,6 +1025,330 @@ export const handleSendCredentialEvent = async (
842
1025
  };
843
1026
  };
844
1027
 
1028
+ const normalizeConsentRequestOrThrow = (scopes: ConsentRequest): NormalizedConsentScopes => {
1029
+ try {
1030
+ return normalizeConsentScopes(scopes);
1031
+ } catch (error) {
1032
+ const message = error instanceof Error ? error.message : 'Invalid consent scopes';
1033
+
1034
+ throw new TRPCError({
1035
+ code: 'BAD_REQUEST',
1036
+ message: `CONSENT_SCOPES_INVALID: ${message}`,
1037
+ });
1038
+ }
1039
+ };
1040
+
1041
+ const handleUpsertConsentContractEvent = async (
1042
+ ctx: { domain: string },
1043
+ listing: AppStoreListingType,
1044
+ listingId: string,
1045
+ event: Record<string, unknown>
1046
+ ): Promise<{ contractUri: string; created: boolean }> => {
1047
+ const rawScopes = event.scopes as ConsentRequest;
1048
+ const normalizedScopes = normalizeConsentRequestOrThrow(rawScopes);
1049
+ return upsertConsentContractForListingScopes({
1050
+ ctx,
1051
+ listing,
1052
+ listingId,
1053
+ normalizedScopes,
1054
+ reason:
1055
+ typeof rawScopes.reason === 'string' && rawScopes.reason.trim().length > 0
1056
+ ? rawScopes.reason
1057
+ : undefined,
1058
+ });
1059
+ };
1060
+
1061
+ const upsertConsentContractForListingScopes = async ({
1062
+ ctx,
1063
+ listing,
1064
+ listingId,
1065
+ normalizedScopes,
1066
+ reason,
1067
+ }: {
1068
+ ctx: { domain: string };
1069
+ listing: AppStoreListingType;
1070
+ listingId: string;
1071
+ normalizedScopes: NormalizedConsentScopes;
1072
+ reason?: string;
1073
+ }): Promise<{ contractUri: string; created: boolean; scopeHash: string }> => {
1074
+ const scopeHash = computeConsentScopeHash(normalizedScopes);
1075
+ const existingContract = await getConsentContractForListingByScopeHashCached(
1076
+ listingId,
1077
+ scopeHash
1078
+ );
1079
+
1080
+ if (existingContract) {
1081
+ return {
1082
+ contractUri: constructUri('contract', existingContract.id, ctx.domain),
1083
+ created: false,
1084
+ scopeHash,
1085
+ };
1086
+ }
1087
+
1088
+ const integration = await getIntegrationForListingCached(listingId);
1089
+
1090
+ if (!integration) {
1091
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Integration not found' });
1092
+ }
1093
+
1094
+ const ownerProfile = await getOwnerProfileForIntegrationCached(integration.id);
1095
+
1096
+ if (!ownerProfile) {
1097
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Integration owner not found' });
1098
+ }
1099
+
1100
+ const createdContract = await createConsentFlowContract({
1101
+ contract: buildConsentFlowContractFromScopes(normalizedScopes),
1102
+ ownerProfileId: ownerProfile.profileId,
1103
+ name: buildScopedConsentContractName(listing.display_name),
1104
+ description: listing.tagline,
1105
+ reasonForAccessing: reason,
1106
+ });
1107
+
1108
+ await associateConsentContractWithListing(listingId, createdContract.id, scopeHash);
1109
+ invalidateConsentContractForListingByScopeHashCache(listingId, scopeHash);
1110
+
1111
+ return {
1112
+ contractUri: constructUri('contract', createdContract.id, ctx.domain),
1113
+ created: true,
1114
+ scopeHash,
1115
+ };
1116
+ };
1117
+
1118
+ const resolveManifestApplyListing = async ({
1119
+ integrationId,
1120
+ listingId,
1121
+ profileId,
1122
+ }: {
1123
+ integrationId: string;
1124
+ listingId: string | undefined;
1125
+ profileId: string;
1126
+ }): Promise<AppStoreListingType> => {
1127
+ if (listingId) {
1128
+ const { listing, integration } = await verifyListingOwnership(listingId, profileId);
1129
+
1130
+ if (integration.id !== integrationId) {
1131
+ throw new TRPCError({
1132
+ code: 'BAD_REQUEST',
1133
+ message: 'Listing does not belong to the provided Integration',
1134
+ });
1135
+ }
1136
+
1137
+ return listing;
1138
+ }
1139
+
1140
+ const listings = await getListingsForIntegration(integrationId, { limit: 2 });
1141
+
1142
+ if (listings.length === 0) {
1143
+ throw new TRPCError({
1144
+ code: 'NOT_FOUND',
1145
+ message: 'No App Store Listing found for this Integration',
1146
+ });
1147
+ }
1148
+
1149
+ if (listings.length > 1) {
1150
+ throw new TRPCError({
1151
+ code: 'BAD_REQUEST',
1152
+ message: 'listingId is required when an Integration has multiple listings',
1153
+ });
1154
+ }
1155
+
1156
+ return listings[0]!;
1157
+ };
1158
+
1159
+ type ManifestVersionRecord = NonNullable<
1160
+ Awaited<ReturnType<typeof getManifestVersionForIntegration>>
1161
+ >;
1162
+ type IntegrationRecord = Awaited<ReturnType<typeof verifyIntegrationOwnership>>;
1163
+ type OwnerProfileRecord = NonNullable<Awaited<ReturnType<typeof getOwnerProfileForIntegration>>>;
1164
+
1165
+ const reconcileManifestVersionForListing = async ({
1166
+ ctx,
1167
+ integration,
1168
+ integrationOwner,
1169
+ listing,
1170
+ manifestVersion,
1171
+ }: {
1172
+ ctx: { domain: string };
1173
+ integration: IntegrationRecord;
1174
+ integrationOwner: OwnerProfileRecord;
1175
+ listing: AppStoreListingType;
1176
+ manifestVersion: ManifestVersionRecord;
1177
+ }) => {
1178
+ let templatesUpserted = 0;
1179
+ let templatesSkipped = 0;
1180
+ let contractsUpserted = 0;
1181
+ let contractsSkipped = 0;
1182
+
1183
+ for (const templateRecord of manifestVersion.manifest.templates) {
1184
+ const inlineTemplate = templateRecord.template as InlineCredentialTemplate;
1185
+ const existingBoost = await getBoostForListingByTemplateAlias(
1186
+ listing.listing_id,
1187
+ templateRecord.alias,
1188
+ ctx.domain
1189
+ );
1190
+ const desiredTemplateHash = computeInlineTemplateHash(inlineTemplate).contentHash;
1191
+
1192
+ const existingBoostMeta = existingBoost
1193
+ ? await readInlineTemplateBoostMeta(existingBoost.boost)
1194
+ : undefined;
1195
+
1196
+ if (
1197
+ existingBoostMeta?.inlineTemplateHash === desiredTemplateHash &&
1198
+ existingBoostMeta.inlineTemplateVersion === templateRecord.version
1199
+ ) {
1200
+ templatesSkipped += 1;
1201
+ continue;
1202
+ }
1203
+
1204
+ await upsertInlineTemplateBoostForListing({
1205
+ listing,
1206
+ integration,
1207
+ templateAlias: templateRecord.alias,
1208
+ template: inlineTemplate,
1209
+ domain: ctx.domain,
1210
+ desiredVersion: templateRecord.version,
1211
+ });
1212
+
1213
+ templatesUpserted += 1;
1214
+
1215
+ invalidateBoostForListingCache(listing.listing_id, templateRecord.alias, ctx.domain);
1216
+ }
1217
+
1218
+ for (const consentRecord of manifestVersion.manifest.consentRequests) {
1219
+ const upsertedConsent = await upsertConsentContractForListingScopes({
1220
+ ctx,
1221
+ listing,
1222
+ listingId: listing.listing_id,
1223
+ normalizedScopes: consentRecord.scopes,
1224
+ reason: consentRecord.reason,
1225
+ });
1226
+
1227
+ if (upsertedConsent.created) {
1228
+ contractsUpserted += 1;
1229
+ } else {
1230
+ contractsSkipped += 1;
1231
+ }
1232
+
1233
+ invalidateConsentContractForListingByScopeHashCache(
1234
+ listing.listing_id,
1235
+ upsertedConsent.scopeHash
1236
+ );
1237
+ }
1238
+
1239
+ await markManifestVersionsSuperseded(integration.id, manifestVersion.id);
1240
+
1241
+ if (manifestVersion.status !== 'active' || !manifestVersion.activatedAt) {
1242
+ await updateAppManifestVersion(manifestVersion.id, {
1243
+ status: 'active',
1244
+ activatedAt: manifestVersion.activatedAt ?? new Date().toISOString(),
1245
+ });
1246
+ }
1247
+
1248
+ await associateListingWithManifestVersion(listing.listing_id, manifestVersion.id);
1249
+
1250
+ await ensureManagedSigningAuthorityForListing(
1251
+ listing,
1252
+ integration,
1253
+ integrationOwner,
1254
+ ctx.domain
1255
+ );
1256
+
1257
+ return {
1258
+ templatesUpserted,
1259
+ templatesSkipped,
1260
+ contractsUpserted,
1261
+ contractsSkipped,
1262
+ signingAuthorityEnsured: true,
1263
+ };
1264
+ };
1265
+
1266
+ const PENDING_UPDATE_IN_REVIEW_MESSAGE =
1267
+ 'An update for this app is already in review. Withdraw it to make more changes.';
1268
+
1269
+ /**
1270
+ * Records changes to a LISTED app as a pending update instead of applying them, so
1271
+ * nothing about a live app changes until an admin approves it.
1272
+ */
1273
+ const holdChangesForReview = async (
1274
+ listing: AppStoreListingType,
1275
+ {
1276
+ changes = {},
1277
+ manifestVersion,
1278
+ }: { changes?: Record<string, unknown>; manifestVersion?: number }
1279
+ ): Promise<boolean> => {
1280
+ if (listing.pending_update_status === 'PENDING_REVIEW') {
1281
+ throw new TRPCError({ code: 'BAD_REQUEST', message: PENDING_UPDATE_IN_REVIEW_MESSAGE });
1282
+ }
1283
+
1284
+ const existing = readPendingUpdate(listing);
1285
+ const next: StoredPendingListingUpdate = {
1286
+ changes: { ...existing?.changes, ...changes },
1287
+ manifestVersion: manifestVersion ?? existing?.manifestVersion,
1288
+ };
1289
+
1290
+ return updateAppStoreListing(listing, {
1291
+ pending_update_json: JSON.stringify(next),
1292
+ pending_update_status: 'DRAFT',
1293
+ });
1294
+ };
1295
+
1296
+ const assertLaunchConfigSubmittable = (launchType: string, launchConfigJson: string): void => {
1297
+ if (launchType !== 'EMBEDDED_IFRAME' || !launchConfigJson) return;
1298
+
1299
+ try {
1300
+ const config = JSON.parse(launchConfigJson);
1301
+ const urlToCheck = config.url || config.iframeUrl;
1302
+ if (!urlToCheck) return;
1303
+
1304
+ const parsed = new URL(urlToCheck);
1305
+ const isLocalhost =
1306
+ parsed.hostname === 'localhost' ||
1307
+ parsed.hostname === '127.0.0.1' ||
1308
+ parsed.hostname === '[::1]';
1309
+ if (isLocalhost || parsed.protocol !== 'https:') {
1310
+ throw new TRPCError({
1311
+ code: 'BAD_REQUEST',
1312
+ message:
1313
+ 'Cannot submit an app for review with a localhost or non-HTTPS URL. Please update the launch configuration with your production URL.',
1314
+ });
1315
+ }
1316
+ } catch (e) {
1317
+ if (e instanceof TRPCError) throw e;
1318
+ // Unparseable config is rejected by input validation.
1319
+ }
1320
+ };
1321
+
1322
+ const notifyAppStoreAdmins = async (
1323
+ from: Parameters<typeof addNotificationToQueue>[0]['from'],
1324
+ type: 'APP_LISTING_SUBMITTED' | 'APP_LISTING_WITHDRAWN',
1325
+ listing: AppStoreListingType,
1326
+ metadata: Record<string, unknown> = {}
1327
+ ): Promise<void> => {
1328
+ if (APP_STORE_ADMIN_PROFILE_IDS.length === 0) return;
1329
+
1330
+ const adminProfiles = await getProfilesByProfileIds(APP_STORE_ADMIN_PROFILE_IDS);
1331
+ for (const adminProfile of adminProfiles) {
1332
+ await addNotificationToQueue({
1333
+ type,
1334
+ to: adminProfile,
1335
+ from,
1336
+ message: getNotificationMessage(
1337
+ type === 'APP_LISTING_SUBMITTED' ? 'appListingSubmitted' : 'appListingWithdrawn',
1338
+ resolveRecipientLocale(adminProfile),
1339
+ { displayName: listing.display_name }
1340
+ ),
1341
+ data: {
1342
+ metadata: {
1343
+ listingId: listing.listing_id,
1344
+ listingName: listing.display_name,
1345
+ ...metadata,
1346
+ },
1347
+ },
1348
+ });
1349
+ }
1350
+ };
1351
+
845
1352
  const handleCheckCredentialEvent = async (
846
1353
  ctx: { domain: string },
847
1354
  profile: { profileId: string },
@@ -1839,6 +2346,274 @@ export const appStoreRouter = t.router({
1839
2346
  return countListingsForIntegration(input.integrationId);
1840
2347
  }),
1841
2348
 
2349
+ submitAppManifest: profileRoute
2350
+ .meta({
2351
+ openapi: {
2352
+ protect: true,
2353
+ method: 'POST',
2354
+ path: '/app-store/integration/{integrationId}/manifest/submit',
2355
+ tags: ['App Store'],
2356
+ summary: 'Submit App Manifest',
2357
+ description:
2358
+ 'Create a versioned app manifest draft for an integration, idempotently by manifest hash.',
2359
+ },
2360
+ requiredScope: 'app-store:write',
2361
+ })
2362
+ .input(
2363
+ z.object({
2364
+ integrationId: z.string(),
2365
+ manifest: AppManifestValidator,
2366
+ })
2367
+ )
2368
+ .output(SubmitAppManifestResponseValidator)
2369
+ .mutation(async ({ input, ctx }) => {
2370
+ await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
2371
+
2372
+ const latestManifestVersion = await getLatestManifestVersionForIntegration(
2373
+ input.integrationId
2374
+ );
2375
+ const activeManifestVersion = await getActiveManifestVersionForIntegration(
2376
+ input.integrationId
2377
+ );
2378
+ // Each capture is one run of the app; merging keeps what earlier runs saw
2379
+ // so an unexercised feature never shows up as removed.
2380
+ const manifest = latestManifestVersion
2381
+ ? mergeCapturedManifests(latestManifestVersion.manifest, input.manifest)
2382
+ : input.manifest;
2383
+ const manifestHash = hashManifest(manifest);
2384
+
2385
+ if (latestManifestVersion?.manifestHash === manifestHash) {
2386
+ return {
2387
+ version: latestManifestVersion.version,
2388
+ manifestHash,
2389
+ diff: null,
2390
+ noop: true,
2391
+ };
2392
+ }
2393
+
2394
+ const createdManifestVersion = await createAppManifestVersion({
2395
+ integrationId: input.integrationId,
2396
+ version: (latestManifestVersion?.version ?? 0) + 1,
2397
+ manifestHash,
2398
+ manifest,
2399
+ status: 'draft',
2400
+ });
2401
+
2402
+ return {
2403
+ version: createdManifestVersion.version,
2404
+ manifestHash,
2405
+ diff: activeManifestVersion
2406
+ ? diffManifests(activeManifestVersion.manifest, manifest)
2407
+ : null,
2408
+ noop: false,
2409
+ };
2410
+ }),
2411
+
2412
+ getManifestVersions: profileRoute
2413
+ .meta({
2414
+ openapi: {
2415
+ protect: true,
2416
+ method: 'GET',
2417
+ path: '/app-store/integration/{integrationId}/manifest/versions',
2418
+ tags: ['App Store'],
2419
+ summary: 'Get App Manifest Versions',
2420
+ description: 'List paginated app manifest versions for an integration.',
2421
+ },
2422
+ requiredScope: 'app-store:read',
2423
+ })
2424
+ .input(
2425
+ z.object({
2426
+ integrationId: z.string(),
2427
+ limit: z.number().int().min(1).max(100).optional(),
2428
+ cursor: z.string().optional(),
2429
+ })
2430
+ )
2431
+ .output(PaginatedAppManifestVersionsValidator)
2432
+ .query(async ({ input, ctx }) => {
2433
+ await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
2434
+
2435
+ const limit = input.limit ?? 25;
2436
+ const versions = await getManifestVersionsForIntegration(input.integrationId, {
2437
+ limit: limit + 1,
2438
+ cursor: input.cursor,
2439
+ });
2440
+
2441
+ const hasMore = versions.length > limit;
2442
+ const records = (hasMore ? versions.slice(0, limit) : versions).map(
2443
+ toManifestVersionSummary
2444
+ );
2445
+
2446
+ return {
2447
+ hasMore,
2448
+ cursor: hasMore ? String(records[records.length - 1]?.version) : undefined,
2449
+ records,
2450
+ };
2451
+ }),
2452
+
2453
+ getManifestVersion: profileRoute
2454
+ .meta({
2455
+ openapi: {
2456
+ protect: true,
2457
+ method: 'GET',
2458
+ path: '/app-store/integration/{integrationId}/manifest/{version}',
2459
+ tags: ['App Store'],
2460
+ summary: 'Get App Manifest Version',
2461
+ description: 'Get a full app manifest version for an integration.',
2462
+ },
2463
+ requiredScope: 'app-store:read',
2464
+ })
2465
+ .input(
2466
+ z.object({
2467
+ integrationId: z.string(),
2468
+ version: z.number().int().min(1),
2469
+ })
2470
+ )
2471
+ .output(AppManifestVersionValidator)
2472
+ .query(async ({ input, ctx }) => {
2473
+ await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
2474
+
2475
+ const manifestVersion = await getManifestVersionForIntegration(
2476
+ input.integrationId,
2477
+ input.version
2478
+ );
2479
+
2480
+ if (!manifestVersion) {
2481
+ throw new TRPCError({
2482
+ code: 'NOT_FOUND',
2483
+ message: 'App manifest version not found',
2484
+ });
2485
+ }
2486
+
2487
+ return manifestVersion;
2488
+ }),
2489
+
2490
+ getManifestDiff: profileRoute
2491
+ .meta({
2492
+ openapi: {
2493
+ protect: true,
2494
+ method: 'GET',
2495
+ path: '/app-store/integration/{integrationId}/manifest/diff/{toVersion}',
2496
+ tags: ['App Store'],
2497
+ summary: 'Get App Manifest Diff',
2498
+ description: 'Diff one app manifest version against another for an integration.',
2499
+ },
2500
+ requiredScope: 'app-store:read',
2501
+ })
2502
+ .input(
2503
+ z.object({
2504
+ integrationId: z.string(),
2505
+ fromVersion: z.number().int().min(1).optional(),
2506
+ toVersion: z.number().int().min(1),
2507
+ })
2508
+ )
2509
+ .output(AppManifestDiffValidator)
2510
+ .query(async ({ input, ctx }) => {
2511
+ await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
2512
+
2513
+ const toManifestVersion = await getManifestVersionForIntegration(
2514
+ input.integrationId,
2515
+ input.toVersion
2516
+ );
2517
+
2518
+ if (!toManifestVersion) {
2519
+ throw new TRPCError({
2520
+ code: 'NOT_FOUND',
2521
+ message: 'Target app manifest version not found',
2522
+ });
2523
+ }
2524
+
2525
+ const fromManifestVersion = input.fromVersion
2526
+ ? await getManifestVersionForIntegration(input.integrationId, input.fromVersion)
2527
+ : await getActiveManifestVersionForIntegration(input.integrationId);
2528
+
2529
+ if (!fromManifestVersion) {
2530
+ throw new TRPCError({
2531
+ code: 'NOT_FOUND',
2532
+ message: 'Source app manifest version not found',
2533
+ });
2534
+ }
2535
+
2536
+ return diffManifests(fromManifestVersion.manifest, toManifestVersion.manifest);
2537
+ }),
2538
+
2539
+ applyManifestVersion: profileRoute
2540
+ .meta({
2541
+ openapi: {
2542
+ protect: true,
2543
+ method: 'POST',
2544
+ path: '/app-store/integration/{integrationId}/manifest/{version}/apply',
2545
+ tags: ['App Store'],
2546
+ summary: 'Apply App Manifest Version',
2547
+ description:
2548
+ 'Mark an app manifest version active and idempotently reconcile derived listing entities.',
2549
+ },
2550
+ requiredScope: 'app-store:write',
2551
+ })
2552
+ .input(
2553
+ z.object({
2554
+ integrationId: z.string(),
2555
+ version: z.number().int().min(1),
2556
+ listingId: z.string().optional(),
2557
+ })
2558
+ )
2559
+ .output(ApplyManifestVersionResponseValidator)
2560
+ .mutation(async ({ input, ctx }) => {
2561
+ const integration = await verifyIntegrationOwnership(
2562
+ input.integrationId,
2563
+ ctx.user.profile.profileId
2564
+ );
2565
+ const manifestVersion = await getManifestVersionForIntegration(
2566
+ input.integrationId,
2567
+ input.version
2568
+ );
2569
+
2570
+ if (!manifestVersion) {
2571
+ throw new TRPCError({
2572
+ code: 'NOT_FOUND',
2573
+ message: 'App manifest version not found',
2574
+ });
2575
+ }
2576
+
2577
+ const listing = await resolveManifestApplyListing({
2578
+ integrationId: input.integrationId,
2579
+ listingId: input.listingId,
2580
+ profileId: ctx.user.profile.profileId,
2581
+ });
2582
+ const integrationOwner = await getOwnerProfileForIntegration(input.integrationId);
2583
+
2584
+ if (!integrationOwner) {
2585
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Integration owner not found' });
2586
+ }
2587
+
2588
+ if (listing.app_listing_status === 'LISTED' && manifestVersion.status !== 'active') {
2589
+ // A live app's new capabilities wait for review instead of taking effect.
2590
+ await holdChangesForReview(listing, { manifestVersion: manifestVersion.version });
2591
+
2592
+ return {
2593
+ applied: false,
2594
+ pendingReview: true,
2595
+ version: manifestVersion.version,
2596
+ reconciled: {
2597
+ templatesUpserted: 0,
2598
+ templatesSkipped: 0,
2599
+ contractsUpserted: 0,
2600
+ contractsSkipped: 0,
2601
+ signingAuthorityEnsured: false,
2602
+ },
2603
+ };
2604
+ }
2605
+
2606
+ const reconciled = await reconcileManifestVersionForListing({
2607
+ ctx,
2608
+ integration,
2609
+ integrationOwner,
2610
+ listing,
2611
+ manifestVersion,
2612
+ });
2613
+
2614
+ return { applied: true, version: manifestVersion.version, reconciled };
2615
+ }),
2616
+
1842
2617
  updateListing: profileRoute
1843
2618
  .meta({
1844
2619
  openapi: {
@@ -1870,6 +2645,10 @@ export const appStoreRouter = t.router({
1870
2645
  ...input.updates,
1871
2646
  });
1872
2647
 
2648
+ if (listing.app_listing_status === 'LISTED') {
2649
+ return holdChangesForReview(listing, { changes: storageUpdates });
2650
+ }
2651
+
1873
2652
  if (!listing.slug) {
1874
2653
  const displayName = storageUpdates.display_name ?? listing.display_name;
1875
2654
  storageUpdates.slug = await getAvailableAppSlug(displayName, listing.listing_id);
@@ -2025,6 +2804,8 @@ export const appStoreRouter = t.router({
2025
2804
  });
2026
2805
  }
2027
2806
 
2807
+ assertLaunchConfigSubmittable(listing.launch_type, listing.launch_config_json);
2808
+
2028
2809
  const submittedAt = new Date().toISOString();
2029
2810
 
2030
2811
  // Update status on the listing node
@@ -2125,6 +2906,125 @@ export const appStoreRouter = t.router({
2125
2906
  return result;
2126
2907
  }),
2127
2908
 
2909
+ submitListingUpdate: profileRoute
2910
+ .meta({
2911
+ openapi: {
2912
+ protect: true,
2913
+ method: 'POST',
2914
+ path: '/app-store/listing/{listingId}/update/submit',
2915
+ tags: ['App Store'],
2916
+ summary: 'Submit Listing Update for Review',
2917
+ description:
2918
+ 'Send pending changes to a LISTED app for admin review. The live listing is unchanged until approval.',
2919
+ },
2920
+ requiredScope: 'app-store:write',
2921
+ })
2922
+ .input(z.object({ listingId: z.string() }))
2923
+ .output(z.boolean())
2924
+ .mutation(async ({ input, ctx }) => {
2925
+ const { listing } = await verifyListingOwnership(
2926
+ input.listingId,
2927
+ ctx.user.profile.profileId
2928
+ );
2929
+ const pending = readPendingUpdate(listing);
2930
+
2931
+ if (listing.app_listing_status !== 'LISTED' || !pending) {
2932
+ throw new TRPCError({
2933
+ code: 'BAD_REQUEST',
2934
+ message: 'There are no changes to submit for this app.',
2935
+ });
2936
+ }
2937
+ if (listing.pending_update_status === 'PENDING_REVIEW') {
2938
+ throw new TRPCError({
2939
+ code: 'BAD_REQUEST',
2940
+ message: PENDING_UPDATE_IN_REVIEW_MESSAGE,
2941
+ });
2942
+ }
2943
+
2944
+ const nextLaunchType = pending.changes.launch_type ?? listing.launch_type;
2945
+ const nextLaunchConfig =
2946
+ pending.changes.launch_config_json ?? listing.launch_config_json;
2947
+ assertLaunchConfigSubmittable(String(nextLaunchType), String(nextLaunchConfig));
2948
+
2949
+ const result = await updateAppStoreListing(listing, {
2950
+ pending_update_status: 'PENDING_REVIEW',
2951
+ pending_update_submitted_at: new Date().toISOString(),
2952
+ });
2953
+
2954
+ await notifyAppStoreAdmins(ctx.user.profile, 'APP_LISTING_SUBMITTED', listing, {
2955
+ isUpdate: true,
2956
+ });
2957
+
2958
+ return result;
2959
+ }),
2960
+
2961
+ withdrawListingUpdate: profileRoute
2962
+ .meta({
2963
+ openapi: {
2964
+ protect: true,
2965
+ method: 'POST',
2966
+ path: '/app-store/listing/{listingId}/update/withdraw',
2967
+ tags: ['App Store'],
2968
+ summary: 'Withdraw Listing Update from Review',
2969
+ description: 'Return a submitted update to a LISTED app back to editable changes.',
2970
+ },
2971
+ requiredScope: 'app-store:write',
2972
+ })
2973
+ .input(z.object({ listingId: z.string() }))
2974
+ .output(z.boolean())
2975
+ .mutation(async ({ input, ctx }) => {
2976
+ const { listing } = await verifyListingOwnership(
2977
+ input.listingId,
2978
+ ctx.user.profile.profileId
2979
+ );
2980
+
2981
+ if (listing.pending_update_status !== 'PENDING_REVIEW') {
2982
+ throw new TRPCError({
2983
+ code: 'BAD_REQUEST',
2984
+ message: 'There is no update in review for this app.',
2985
+ });
2986
+ }
2987
+
2988
+ const result = await updateAppStoreListing(listing, { pending_update_status: 'DRAFT' });
2989
+
2990
+ await notifyAppStoreAdmins(ctx.user.profile, 'APP_LISTING_WITHDRAWN', listing, {
2991
+ isUpdate: true,
2992
+ });
2993
+
2994
+ return result;
2995
+ }),
2996
+
2997
+ discardListingUpdate: profileRoute
2998
+ .meta({
2999
+ openapi: {
3000
+ protect: true,
3001
+ method: 'POST',
3002
+ path: '/app-store/listing/{listingId}/update/discard',
3003
+ tags: ['App Store'],
3004
+ summary: 'Discard Listing Update',
3005
+ description: 'Throw away unsubmitted changes to a LISTED app.',
3006
+ },
3007
+ requiredScope: 'app-store:write',
3008
+ })
3009
+ .input(z.object({ listingId: z.string() }))
3010
+ .output(z.boolean())
3011
+ .mutation(async ({ input, ctx }) => {
3012
+ const { listing } = await verifyListingOwnership(
3013
+ input.listingId,
3014
+ ctx.user.profile.profileId
3015
+ );
3016
+
3017
+ if (listing.pending_update_status === 'PENDING_REVIEW') {
3018
+ throw new TRPCError({
3019
+ code: 'BAD_REQUEST',
3020
+ message: PENDING_UPDATE_IN_REVIEW_MESSAGE,
3021
+ });
3022
+ }
3023
+
3024
+ await clearPendingListingUpdate(listing.listing_id);
3025
+ return true;
3026
+ }),
3027
+
2128
3028
  deleteListing: profileRoute
2129
3029
  .meta({
2130
3030
  openapi: {
@@ -2133,14 +3033,34 @@ export const appStoreRouter = t.router({
2133
3033
  path: '/app-store/listing/{listingId}',
2134
3034
  tags: ['App Store'],
2135
3035
  summary: 'Delete App Store Listing',
2136
- description: 'Delete an App Store Listing',
3036
+ description:
3037
+ 'Delete an App Store Listing. Only draft or rejected listings may be deleted; in-review and live listings must be withdrawn or removed from the store first.',
2137
3038
  },
2138
3039
  requiredScope: 'app-store:delete',
2139
3040
  })
2140
3041
  .input(z.object({ listingId: z.string() }))
2141
3042
  .output(z.boolean())
2142
3043
  .mutation(async ({ input, ctx }) => {
2143
- await verifyListingOwnership(input.listingId, ctx.user.profile.profileId);
3044
+ const { listing } = await verifyListingOwnership(
3045
+ input.listingId,
3046
+ ctx.user.profile.profileId
3047
+ );
3048
+
3049
+ // ARCHIVED is how a rejected listing is represented (there is no
3050
+ // separate REJECTED status), so DRAFT and ARCHIVED are deletable.
3051
+ if (listing.app_listing_status === 'PENDING_REVIEW') {
3052
+ throw new TRPCError({
3053
+ code: 'PRECONDITION_FAILED',
3054
+ message: 'Only draft apps can be deleted. Withdraw it from review first.',
3055
+ });
3056
+ }
3057
+
3058
+ if (listing.app_listing_status === 'LISTED') {
3059
+ throw new TRPCError({
3060
+ code: 'PRECONDITION_FAILED',
3061
+ message: "Live apps can't be deleted. Remove it from the store first.",
3062
+ });
3063
+ }
2144
3064
 
2145
3065
  await deleteAppStoreListing(input.listingId);
2146
3066
 
@@ -2838,6 +3758,10 @@ export const appStoreRouter = t.router({
2838
3758
  );
2839
3759
  }
2840
3760
 
3761
+ if (eventType === 'upsert-consent-contract') {
3762
+ return handleUpsertConsentContractEvent(ctx, listing, resolvedListingId, event);
3763
+ }
3764
+
2841
3765
  if (eventType === 'send-ai-session-credential') {
2842
3766
  return handleSendAiSessionCredentialEvent(ctx, profile, resolvedListingId, event);
2843
3767
  }
@@ -2947,6 +3871,98 @@ export const appStoreRouter = t.router({
2947
3871
  return result;
2948
3872
  }),
2949
3873
 
3874
+ adminReviewListingUpdate: profileRoute
3875
+ .meta({
3876
+ openapi: {
3877
+ protect: true,
3878
+ method: 'POST',
3879
+ path: '/app-store/admin/listing/{listingId}/update/review',
3880
+ tags: ['App Store Admin'],
3881
+ summary: 'Review Listing Update (Admin)',
3882
+ description:
3883
+ 'Approve (apply to the live listing) or reject a submitted update to a LISTED app.',
3884
+ },
3885
+ requiredScope: 'app-store:admin',
3886
+ })
3887
+ .input(z.object({ listingId: z.string(), approve: z.boolean() }))
3888
+ .output(z.boolean())
3889
+ .mutation(async ({ input, ctx }) => {
3890
+ verifyAppStoreAdmin(ctx.user.profile.profileId);
3891
+
3892
+ const listing = await getListingOrThrow(input.listingId);
3893
+ const pending = readPendingUpdate(listing);
3894
+
3895
+ if (listing.pending_update_status !== 'PENDING_REVIEW' || !pending) {
3896
+ throw new TRPCError({
3897
+ code: 'BAD_REQUEST',
3898
+ message: 'This app has no update waiting for review.',
3899
+ });
3900
+ }
3901
+
3902
+ const integration = await getIntegrationForListing(input.listingId);
3903
+ const ownerProfile = integration
3904
+ ? await getOwnerProfileForIntegration(integration.id)
3905
+ : null;
3906
+
3907
+ if (input.approve) {
3908
+ if (pending.manifestVersion !== undefined) {
3909
+ if (!integration || !ownerProfile) {
3910
+ throw new TRPCError({
3911
+ code: 'NOT_FOUND',
3912
+ message: 'Integration for this listing was not found',
3913
+ });
3914
+ }
3915
+ const manifestVersion = await getManifestVersionForIntegration(
3916
+ integration.id,
3917
+ pending.manifestVersion
3918
+ );
3919
+ if (manifestVersion) {
3920
+ await reconcileManifestVersionForListing({
3921
+ ctx,
3922
+ integration,
3923
+ integrationOwner: ownerProfile,
3924
+ listing,
3925
+ manifestVersion,
3926
+ });
3927
+ }
3928
+ }
3929
+
3930
+ await updateAppStoreListing(listing, pending.changes as AppStoreListingUpdateType);
3931
+ await clearPendingListingUpdate(listing.listing_id);
3932
+ } else {
3933
+ await updateAppStoreListing(listing, { pending_update_status: 'DRAFT' });
3934
+ }
3935
+
3936
+ if (ownerProfile) {
3937
+ const approvedName =
3938
+ typeof pending.changes.display_name === 'string' && input.approve
3939
+ ? pending.changes.display_name
3940
+ : listing.display_name;
3941
+
3942
+ await addNotificationToQueue({
3943
+ type: input.approve
3944
+ ? LCNNotificationTypeEnumValidator.enum.APP_LISTING_APPROVED
3945
+ : LCNNotificationTypeEnumValidator.enum.APP_LISTING_REJECTED,
3946
+ to: ownerProfile,
3947
+ from: ctx.user.profile,
3948
+ message: getNotificationMessage(
3949
+ input.approve ? 'appListingApproved' : 'appListingRejected',
3950
+ resolveRecipientLocale(ownerProfile),
3951
+ { displayName: approvedName }
3952
+ ),
3953
+ data: {
3954
+ metadata: {
3955
+ listingId: listing.listing_id,
3956
+ listingName: approvedName,
3957
+ isUpdate: true,
3958
+ },
3959
+ },
3960
+ });
3961
+ }
3962
+
3963
+ return true;
3964
+ }),
3965
+
2950
3966
  adminUpdatePromotionLevel: profileRoute
2951
3967
  .meta({
2952
3968
  openapi: {
@@ -2992,6 +4008,7 @@ export const appStoreRouter = t.router({
2992
4008
  limit: z.number().optional(),
2993
4009
  cursor: z.string().optional(),
2994
4010
  status: AppListingStatus.optional(),
4011
+ pendingUpdatesOnly: z.boolean().optional(),
2995
4012
  })
2996
4013
  .optional()
2997
4014
  )
@@ -3009,6 +4026,7 @@ export const appStoreRouter = t.router({
3009
4026
  status: input?.status,
3010
4027
  includeAllStatuses: !input?.status, // Include all if no specific status filter
3011
4028
  excludeDemoted: false, // Admin can see demoted listings
4029
+ pendingUpdatesOnly: input?.pendingUpdatesOnly,
3012
4030
  });
3013
4031
 
3014
4032
  const hasMore = results.length > limit;