@learncard/network-brain-service 3.19.1 → 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 (312) hide show
  1. package/.env.example +44 -111
  2. package/AGENTS.md +52 -0
  3. package/CHANGELOG.md +109 -0
  4. package/README.md +9 -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/share-link/abandon.d.ts.map +1 -1
  56. package/dist/accesslayer/share-link/attachment.d.ts +37 -0
  57. package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
  58. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -1
  59. package/dist/accesslayer/share-link/helpers.d.ts +3 -0
  60. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -1
  61. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -1
  62. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -1
  63. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -1
  64. package/dist/accesslayer/share-link/types.d.ts +8 -3
  65. package/dist/accesslayer/share-link/types.d.ts.map +1 -1
  66. package/dist/cache/app-store.caches.d.ts +49 -0
  67. package/dist/cache/app-store.caches.d.ts.map +1 -1
  68. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  69. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  70. package/dist/config/environment.d.ts +5 -0
  71. package/dist/config/environment.d.ts.map +1 -1
  72. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  73. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  74. package/dist/config/stageConfig.d.ts +112 -0
  75. package/dist/config/stageConfig.d.ts.map +1 -0
  76. package/dist/config/stageConfig.test.d.ts +2 -0
  77. package/dist/config/stageConfig.test.d.ts.map +1 -0
  78. package/dist/didkit_wasm_bg.wasm +0 -0
  79. package/dist/docker-entry.d.ts +1 -1
  80. package/dist/docker-entry.d.ts.map +1 -1
  81. package/dist/helpers/app-manifest.helpers.d.ts +5 -0
  82. package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
  83. package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
  84. package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
  85. package/dist/helpers/consent-referral.helpers.d.ts +2 -0
  86. package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
  87. package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
  88. package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
  89. package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
  90. package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
  91. package/dist/helpers/contract-events.helpers.d.ts +25 -0
  92. package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
  93. package/dist/helpers/contract-requests.helpers.d.ts +19 -0
  94. package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
  95. package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
  96. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  97. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  98. package/dist/helpers/inline-template.helpers.d.ts +45 -0
  99. package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
  100. package/dist/helpers/notificationMessages.d.ts +1 -1
  101. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  102. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  103. package/dist/helpers/notifications.helpers.d.ts +6 -0
  104. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  105. package/dist/helpers/profile.helpers.d.ts +1 -0
  106. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  107. package/dist/helpers/share-link-attachment-access.d.ts +4 -0
  108. package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
  109. package/dist/helpers/share-link-attachment.d.ts +18 -0
  110. package/dist/helpers/share-link-attachment.d.ts.map +1 -0
  111. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
  112. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
  113. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -1
  114. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -1
  115. package/dist/helpers/share-link-maintenance/runner.d.ts +1 -0
  116. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -1
  117. package/dist/helpers/share-link-maintenance/runtime.d.ts +2 -0
  118. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -1
  119. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -1
  120. package/dist/helpers/share-link-maintenance/types.d.ts +4 -1
  121. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -1
  122. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -1
  123. package/dist/helpers/share-link-policy/production.d.ts +3 -5
  124. package/dist/helpers/share-link-policy/production.d.ts.map +1 -1
  125. package/dist/helpers/share-link-policy/resolver.d.ts +1 -1
  126. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -1
  127. package/dist/helpers/share-link-policy/types.d.ts +8 -2
  128. package/dist/helpers/share-link-policy/types.d.ts.map +1 -1
  129. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  130. package/dist/helpers/smartResume.helpers.d.ts +3 -0
  131. package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
  132. package/dist/index.js +2 -2
  133. package/dist/models/AppManifestVersion.d.ts +20 -0
  134. package/dist/models/AppManifestVersion.d.ts.map +1 -0
  135. package/dist/models/AppStoreListing.d.ts +13 -0
  136. package/dist/models/AppStoreListing.d.ts.map +1 -1
  137. package/dist/models/ConsentFlowContract.d.ts +13 -2
  138. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  139. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  140. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  141. package/dist/models/Integration.d.ts +2 -0
  142. package/dist/models/Integration.d.ts.map +1 -1
  143. package/dist/models/ShareLink.d.ts +2 -0
  144. package/dist/models/ShareLink.d.ts.map +1 -1
  145. package/dist/models/index.d.ts +1 -0
  146. package/dist/models/index.d.ts.map +1 -1
  147. package/dist/models/share-link-constraints.d.ts.map +1 -1
  148. package/dist/openapi.d.ts +1 -1
  149. package/dist/openapi.d.ts.map +1 -1
  150. package/dist/routes/app-store.d.ts +619 -0
  151. package/dist/routes/app-store.d.ts.map +1 -1
  152. package/dist/routes/boosts.d.ts +2 -2
  153. package/dist/routes/boosts.d.ts.map +1 -1
  154. package/dist/routes/contracts.d.ts +146 -3
  155. package/dist/routes/contracts.d.ts.map +1 -1
  156. package/dist/routes/credentials.d.ts +34 -0
  157. package/dist/routes/credentials.d.ts.map +1 -1
  158. package/dist/routes/inbox.d.ts +52 -2
  159. package/dist/routes/inbox.d.ts.map +1 -1
  160. package/dist/routes/index.d.ts.map +1 -1
  161. package/dist/routes/profile-manager.d.ts.map +1 -1
  162. package/dist/routes/profiles.d.ts.map +1 -1
  163. package/dist/routes/public-share-links.d.ts +55 -0
  164. package/dist/routes/public-share-links.d.ts.map +1 -1
  165. package/dist/routes/share-links.d.ts +175 -14
  166. package/dist/routes/share-links.d.ts.map +1 -1
  167. package/dist/routes/test.d.ts +5 -0
  168. package/dist/routes/test.d.ts.map +1 -1
  169. package/dist/routes/workflows.d.ts.map +1 -1
  170. package/dist/types/app-manifest.d.ts +339 -0
  171. package/dist/types/app-manifest.d.ts.map +1 -0
  172. package/dist/types/app-store-listing.d.ts +34 -0
  173. package/dist/types/app-store-listing.d.ts.map +1 -1
  174. package/dist/types/consentflowcontract.d.ts +30 -3
  175. package/dist/types/consentflowcontract.d.ts.map +1 -1
  176. package/dist/types/credential-refresh.d.ts +1 -1
  177. package/esbuildPlugins.cjs +1 -0
  178. package/lambda.ts +49 -195
  179. package/lambdaApp.ts +203 -0
  180. package/package.json +20 -18
  181. package/scripts/export-openapi.ts +20 -0
  182. package/scripts/offline-schema-environment.ts +18 -0
  183. package/scripts/verify-python-contracts.ts +72 -0
  184. package/serverless-local.yml +3 -6
  185. package/serverless.function-env.cjs +50 -0
  186. package/serverless.local.yml +0 -1
  187. package/serverless.yml +27 -74
  188. package/shareLinkMaintenanceLambda.ts +13 -44
  189. package/shareLinkMaintenanceLambdaApp.ts +50 -0
  190. package/src/accesslayer/app-manifest-version/create.ts +57 -0
  191. package/src/accesslayer/app-manifest-version/read.ts +129 -0
  192. package/src/accesslayer/app-manifest-version/update.ts +61 -0
  193. package/src/accesslayer/app-store-listing/read.ts +6 -0
  194. package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
  195. package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
  196. package/src/accesslayer/app-store-listing/update.ts +15 -0
  197. package/src/accesslayer/boost/relationships/create.ts +8 -2
  198. package/src/accesslayer/consentflowcontract/create.ts +81 -8
  199. package/src/accesslayer/consentflowcontract/read.ts +84 -10
  200. package/src/accesslayer/consentflowcontract/relationships/create.ts +116 -93
  201. package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
  202. package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
  203. package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
  204. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
  205. package/src/accesslayer/consentflowcontract/relationships/update.ts +240 -189
  206. package/src/accesslayer/credential/create.test.ts +2 -115
  207. package/src/accesslayer/credential/create.ts +11 -6
  208. package/src/accesslayer/credential/relationships/create.ts +17 -10
  209. package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
  210. package/src/accesslayer/inbox-credential/update.ts +4 -2
  211. package/src/accesslayer/share-link/abandon.ts +20 -0
  212. package/src/accesslayer/share-link/attachment.ts +360 -0
  213. package/src/accesslayer/share-link/finalize.ts +55 -7
  214. package/src/accesslayer/share-link/helpers.ts +33 -2
  215. package/src/accesslayer/share-link/recovery.ts +20 -0
  216. package/src/accesslayer/share-link/reserve.ts +85 -6
  217. package/src/accesslayer/share-link/revoke.ts +35 -0
  218. package/src/accesslayer/share-link/types.ts +9 -4
  219. package/src/cache/app-store.caches.ts +35 -0
  220. package/src/config/applyDockerStageConfig.ts +6 -0
  221. package/src/config/environment.test.ts +17 -12
  222. package/src/config/environment.ts +1 -0
  223. package/src/config/serverlessFunctionEnv.test.ts +105 -0
  224. package/src/config/stageConfig.test.ts +25 -0
  225. package/src/config/stageConfig.ts +24 -0
  226. package/src/docker-entry.ts +24 -0
  227. package/src/helpers/app-manifest.helpers.ts +185 -0
  228. package/src/helpers/claim-hooks.helpers.ts +20 -7
  229. package/src/helpers/consent-referral.helpers.ts +6 -0
  230. package/src/helpers/consent-scopes.helpers.ts +58 -0
  231. package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
  232. package/src/helpers/contract-events.helpers.ts +540 -0
  233. package/src/helpers/contract-requests.helpers.ts +211 -0
  234. package/src/helpers/finalize-inbox.helpers.ts +163 -44
  235. package/src/helpers/inbox.helpers.ts +51 -66
  236. package/src/helpers/inline-template.helpers.ts +560 -0
  237. package/src/helpers/notificationMessages.ts +59 -0
  238. package/src/helpers/notificationQueue.helpers.ts +23 -5
  239. package/src/helpers/notifications.helpers.ts +25 -1
  240. package/src/helpers/profile.helpers.ts +6 -0
  241. package/src/helpers/share-link-attachment-access.ts +87 -0
  242. package/src/helpers/share-link-attachment.ts +115 -0
  243. package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
  244. package/src/helpers/share-link-coordinator/coordinator.ts +15 -2
  245. package/src/helpers/share-link-coordinator/recovery-runner.ts +15 -0
  246. package/src/helpers/share-link-maintenance/runner.ts +28 -0
  247. package/src/helpers/share-link-maintenance/runtime.ts +6 -0
  248. package/src/helpers/share-link-maintenance/telemetry.ts +3 -0
  249. package/src/helpers/share-link-maintenance/types.ts +2 -0
  250. package/src/helpers/share-link-owner-projection.ts +2 -0
  251. package/src/helpers/share-link-policy/production.ts +37 -17
  252. package/src/helpers/share-link-policy/resolver.ts +16 -11
  253. package/src/helpers/share-link-policy/types.ts +15 -7
  254. package/src/helpers/signingAuthority.helpers.ts +23 -14
  255. package/src/helpers/smartResume.helpers.ts +123 -0
  256. package/src/models/AppManifestVersion.ts +43 -0
  257. package/src/models/AppStoreListing.ts +32 -0
  258. package/src/models/ConsentFlowContract.ts +34 -3
  259. package/src/models/ConsentFlowTerms.ts +10 -0
  260. package/src/models/ConsentFlowTransaction.ts +3 -0
  261. package/src/models/Integration.ts +2 -0
  262. package/src/models/ShareLink.ts +4 -0
  263. package/src/models/index.ts +23 -0
  264. package/src/models/share-link-constraints.ts +5 -0
  265. package/src/openapi.ts +44 -8
  266. package/src/routes/app-store.ts +1054 -27
  267. package/src/routes/boosts.ts +12 -14
  268. package/src/routes/contracts.ts +382 -262
  269. package/src/routes/inbox.ts +4 -2
  270. package/src/routes/index.ts +4 -1
  271. package/src/routes/profile-manager.ts +19 -0
  272. package/src/routes/profiles.ts +12 -0
  273. package/src/routes/public-share-links.ts +169 -0
  274. package/src/routes/share-links.ts +69 -0
  275. package/src/routes/workflows.ts +3 -2
  276. package/src/types/app-manifest.ts +28 -0
  277. package/src/types/app-store-listing.ts +13 -0
  278. package/src/types/consentflowcontract.ts +25 -4
  279. package/test/app-manifest.helpers.spec.ts +170 -0
  280. package/test/app-manifest.routes.spec.ts +299 -0
  281. package/test/app-store-listing-updates.spec.ts +236 -0
  282. package/test/app-store-listing.spec.ts +65 -3
  283. package/test/consent-data-boundaries.spec.ts +352 -0
  284. package/test/consent-recipients.spec.ts +513 -0
  285. package/test/consent-scopes.helpers.spec.ts +67 -0
  286. package/test/consentflow.spec.ts +63 -2
  287. package/test/contract-requests.spec.ts +1588 -0
  288. package/test/credential-storage.neo4j.spec.ts +122 -0
  289. package/test/guardian-gated-routes.spec.ts +36 -0
  290. package/test/inbox-security.e2e.ts +26 -10
  291. package/test/inbox.spec.ts +184 -8
  292. package/test/inline-template.helpers.spec.ts +49 -0
  293. package/test/managed-recipient-inbox.spec.ts +24 -2
  294. package/test/openapi.spec.ts +14 -73
  295. package/test/profiles.spec.ts +30 -0
  296. package/test/share-link-attachment.neo4j.spec.ts +262 -0
  297. package/test/share-link-attachment.unit.spec.ts +244 -0
  298. package/test/share-link-lifecycle.neo4j.spec.ts +145 -2
  299. package/test/share-link-maintenance.unit.spec.ts +32 -5
  300. package/test/share-link-owner-route.unit.spec.ts +70 -2
  301. package/test/share-link-policy.unit.spec.ts +18 -4
  302. package/test/share-link-production-policy.unit.spec.ts +58 -9
  303. package/test/share-link-public-route.unit.spec.ts +279 -0
  304. package/test/share-link-service-policy.neo4j.spec.ts +403 -0
  305. package/test/signing-authority-encryption.spec.ts +75 -0
  306. package/test/smart-resume-consent.spec.ts +233 -0
  307. package/test/smart-resume-upload.spec.ts +65 -0
  308. package/tsconfig.build.json +1 -1
  309. package/tsconfig.json +1 -0
  310. package/vitest.config.ts +13 -0
  311. package/vitest.inbox-e2e.config.ts +2 -0
  312. package/workerLambda.test.ts +100 -0
@@ -0,0 +1,50 @@
1
+ const { functionEnvironment, pickNonEmpty } = require('@learncard/service-config/function-env.cjs');
2
+
3
+ // User-approved brain-service deployment credentials. In bundle mode these are delivered
4
+ // through the runtime-secrets bundle and omitted here; in fallback mode (no bundle) the
5
+ // existing per-value GitHub environment secrets are retained. Only genuine credentials
6
+ // belong in this list — non-secret allowlists, tuning flags and per-stage values are
7
+ // delivered through checked-in stage config at cold start, never forwarded here.
8
+ const PROVIDER_SECRETS = [
9
+ 'SEED',
10
+ 'NEO4J_URI',
11
+ 'NEO4J_USERNAME',
12
+ 'NEO4J_PASSWORD',
13
+ 'POSTMARK_API_KEY',
14
+ 'MESSAGEBIRD_AUTH_TOKEN',
15
+ 'SENTRY_DSN',
16
+ 'POSTHOG_API_KEY',
17
+ 'SKILL_EMBEDDING_GOOGLE_API_KEY',
18
+ 'SKILLS_PROVIDER_API_KEY',
19
+ 'TWILIO_ACCOUNT_SID',
20
+ 'TWILIO_AUTH_TOKEN',
21
+ 'SMART_RESUME_CLIENT_ID',
22
+ 'SMART_RESUME_ACCESS_KEY',
23
+ 'SMART_RESUME_CONTRACT_URI',
24
+ 'CREDENTIAL_REFRESH_DIGEST_SECRET',
25
+ 'SHARE_LINK_REQUEST_HASH_SECRET',
26
+ 'APP_STORE_ADMIN_PROFILE_IDS',
27
+ 'LOGIN_PROVIDER_DID',
28
+ 'NOTIFICATIONS_SERVICE_WEBHOOK_URL',
29
+ ];
30
+
31
+ // Serverless v3 supplies CLI options to file functions and recursively resolves the strings
32
+ // it returns, so CloudFormation intrinsics (Fn::GetAtt) and construct references survive.
33
+ // Every brain-service function shares the one generated execution role, so the bundle pointer
34
+ // and the credential fallbacks sit together on the provider. Bundle mode emits only infra plus
35
+ // the pointer; fallback mode retains the per-value GitHub secrets. Non-secret per-stage tuning
36
+ // and allowlists are applied from checked-in stage config at cold start, never forwarded here.
37
+ exports.provider = ({ options = {}, env = process.env } = {}) => ({
38
+ LAMBDA_STAGE: options.stage || 'dev',
39
+ // Non-secret product selector for the shared stage files (config.<tenant>.<stage>.json).
40
+ CONFIG_TENANT: env.CONFIG_TENANT || 'learncard',
41
+ PORT: String(options.httpPort || '3000'),
42
+ REDIS_HOST: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Address'] },
43
+ REDIS_PORT: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Port'] },
44
+ NOTIFICATIONS_QUEUE_URL: '${construct:notifications-queue.queueUrl}',
45
+ INBOX_QUEUE_URL: '${construct:inbox-queue.queueUrl}',
46
+ // The deploy commit, emitted only when set so an empty string never counts toward the
47
+ // 4KB per-function environment limit.
48
+ ...pickNonEmpty(['GIT_SHA'], env),
49
+ ...functionEnvironment({ always: ['RUNTIME_SECRETS_ID'], fallback: PROVIDER_SECRETS, env }),
50
+ });
@@ -64,7 +64,6 @@ provider:
64
64
  'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Address]
65
65
  REDIS_PORT:
66
66
  'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Port]
67
- APP_BRAND: ${env:APP_BRAND, 'scouts'}
68
67
  tracing:
69
68
  apiGateway: true
70
69
  lambda: true
package/serverless.yml CHANGED
@@ -138,80 +138,20 @@ provider:
138
138
  timeout: 29
139
139
  stage: ${opt:stage, "dev"}
140
140
  region: ${opt:region, "us-east-1"}
141
- environment:
142
- PORT: ${env:PORT, 3000}
143
- INBOX_DELETE_EXPIRED_RECORDS: ${env:INBOX_DELETE_EXPIRED_RECORDS, "false"}
144
- LAMBDA_STAGE: ${opt:stage, "dev"}
145
- INBOX_BATCH_ITEMS_PER_HOUR: ${env:INBOX_BATCH_ITEMS_PER_HOUR, '10000'}
146
- SEED: ${env:SEED}
147
- DOMAIN_NAME: ${env:DOMAIN_NAME}
148
- CLIENT_APP_DOMAIN_NAME: ${env:CLIENT_APP_DOMAIN_NAME}
149
- NOTIFICATIONS_SERVICE_WEBHOOK_URL: ${env:NOTIFICATIONS_SERVICE_WEBHOOK_URL}
150
- NEO4J_URI: ${env:NEO4J_URI}
151
- NEO4J_USERNAME: ${env:NEO4J_USERNAME}
152
- NEO4J_PASSWORD: ${env:NEO4J_PASSWORD}
153
- POSTMARK_API_KEY: ${env:POSTMARK_API_KEY}
154
- POSTMARK_FROM_EMAIL: ${env:POSTMARK_FROM_EMAIL}
155
- MESSAGEBIRD_AUTH_TOKEN: ${env:MESSAGEBIRD_AUTH_TOKEN}
156
- MESSAGEBIRD_ORIGINATOR: ${env:MESSAGEBIRD_ORIGINATOR}
157
- TRUSTED_ISSUERS_WHITELIST: ${env:TRUSTED_ISSUERS_WHITELIST, ""}
158
- TRUSTED_BRAIN_SERVICES: ${env:TRUSTED_BRAIN_SERVICES, ""}
159
- BRAIN_SERVICE_REGISTRY_URL: ${env:BRAIN_SERVICE_REGISTRY_URL, ""}
160
- DCC_KNOWN_REGISTRIES_URL: ${env:DCC_KNOWN_REGISTRIES_URL, ""}
161
- SENTRY_DSN: ${env:SENTRY_DSN}
162
- SENTRY_ENV: ${env:SENTRY_ENV}
163
- SENTRY_AUTH_TOKEN: ${env:SENTRY_AUTH_TOKEN}
164
- SENTRY_ORG: ${env:SENTRY_ORG}
165
- SENTRY_PROJECT: ${env:SENTRY_PROJECT}
166
- REDIS_HOST:
167
- 'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Address]
168
- REDIS_PORT:
169
- 'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Port]
170
- NOTIFICATIONS_QUEUE_URL: ${construct:notifications-queue.queueUrl}
171
- INBOX_QUEUE_URL: ${construct:inbox-queue.queueUrl}
172
- SKILL_EMBEDDING_GOOGLE_API_KEY: ${env:SKILL_EMBEDDING_GOOGLE_API_KEY, ""}
173
- SKILL_EMBEDDING_GOOGLE_MODEL: ${env:SKILL_EMBEDDING_GOOGLE_MODEL, "gemini-embedding-001"}
174
- SKILL_EMBEDDING_BATCH_SIZE: ${env:SKILL_EMBEDDING_BATCH_SIZE, "25"}
175
- SKILL_EMBEDDING_BACKFILL_ON_STARTUP: ${env:SKILL_EMBEDDING_BACKFILL_ON_STARTUP, "false"}
176
- SKILL_EMBEDDING_BACKFILL_LOCK_TTL_SECONDS: ${env:SKILL_EMBEDDING_BACKFILL_LOCK_TTL_SECONDS, "900"}
177
- SKILL_EMBEDDING_BACKFILL_PAGE_SIZE: ${env:SKILL_EMBEDDING_BACKFILL_PAGE_SIZE, "100"}
178
- SKILL_EMBEDDING_BACKFILL_BATCH_SIZE: ${env:SKILL_EMBEDDING_BACKFILL_BATCH_SIZE, "50"}
179
- SMART_RESUME_CLIENT_ID: ${env:SMART_RESUME_CLIENT_ID, ""}
180
- SMART_RESUME_ACCESS_KEY: ${env:SMART_RESUME_ACCESS_KEY, ""}
181
- SMART_RESUME_CONTRACT_URI: ${env:SMART_RESUME_CONTRACT_URI, ""}
182
- LOGIN_PROVIDER_DID: ${env:LOGIN_PROVIDER_DID, ""}
183
- OIDC_TRUSTED_ISSUERS: ${env:OIDC_TRUSTED_ISSUERS, ""}
184
- OIDC_EXPECTED_AUDIENCE: ${env:OIDC_EXPECTED_AUDIENCE, ""}
185
- APP_STORE_ADMIN_PROFILE_IDS: ${env:APP_STORE_ADMIN_PROFILE_IDS, ""}
186
- APP_BRAND: ${env:APP_BRAND, 'scouts'}
187
- # LC-1644 perf bench (admin-only). Optional — if unset, the bench still runs
188
- # but emits no PostHog events. POSTHOG_HOST falls back to the helper's default
189
- # (https://us.i.posthog.com).
190
- POSTHOG_API_KEY: ${env:POSTHOG_API_KEY, ""}
191
- POSTHOG_HOST: ${env:POSTHOG_HOST, ""}
192
- GIT_SHA: ${env:GIT_SHA, ""}
193
- CREDENTIAL_REFRESH_ENABLED: ${env:CREDENTIAL_REFRESH_ENABLED, "false"}
194
- CREDENTIAL_REFRESH_DIGEST_SECRET: ${env:CREDENTIAL_REFRESH_DIGEST_SECRET, ""}
195
- CREDENTIAL_REFRESH_NOTIFICATION_WINDOW_HOURS: ${env:CREDENTIAL_REFRESH_NOTIFICATION_WINDOW_HOURS, "24"}
196
- # Stable HMAC key for passcode-bearing share-link request fingerprints.
197
- SHARE_LINK_REQUEST_HASH_SECRET: ${env:SHARE_LINK_REQUEST_HASH_SECRET, ""}
198
- # LC-2187 share-link service wiring and automatic maintenance. Every value is
199
- # bounded and re-validated fail-closed at runtime. Never read from the
200
- # scheduled event payload.
201
- SHARE_LINK_MAINTENANCE_NAMESPACE: ${env:SHARE_LINK_MAINTENANCE_NAMESPACE, ""}
202
- SHARE_LINK_MAINTENANCE_ORIGIN: ${env:SHARE_LINK_MAINTENANCE_ORIGIN, ""}
203
- SHARE_LINK_MAINTENANCE_AUDIENCE: ${env:SHARE_LINK_MAINTENANCE_AUDIENCE, ""}
204
- SHARE_LINK_MAINTENANCE_ALLOW_INSECURE_LOOPBACK: ${env:SHARE_LINK_MAINTENANCE_ALLOW_INSECURE_LOOPBACK, "false"}
205
- SHARE_LINK_MAINTENANCE_CLAIMANT: ${env:SHARE_LINK_MAINTENANCE_CLAIMANT, "brain-share-link-maintenance"}
206
- SHARE_LINK_MAINTENANCE_RECOVERY_LIMIT: ${env:SHARE_LINK_MAINTENANCE_RECOVERY_LIMIT, "25"}
207
- SHARE_LINK_MAINTENANCE_CLEANUP_LIMIT: ${env:SHARE_LINK_MAINTENANCE_CLEANUP_LIMIT, "25"}
208
- SHARE_LINK_MAINTENANCE_LEASE_MS: ${env:SHARE_LINK_MAINTENANCE_LEASE_MS, "120000"}
209
- SHARE_LINK_MAINTENANCE_CLAIM_MS: ${env:SHARE_LINK_MAINTENANCE_CLAIM_MS, "300000"}
210
- SHARE_LINK_MAINTENANCE_BUDGET_MS: ${env:SHARE_LINK_MAINTENANCE_BUDGET_MS, "60000"}
211
- SHARE_LINK_MAINTENANCE_INTERVAL_MS: ${env:SHARE_LINK_MAINTENANCE_INTERVAL_MS, "300000"}
212
- SHARE_LINK_MAINTENANCE_REQUEST_TIMEOUT_MS: ${env:SHARE_LINK_MAINTENANCE_REQUEST_TIMEOUT_MS, "10000"}
213
- SHARE_LINK_MAINTENANCE_FINALIZE_RESERVE_MS: ${env:SHARE_LINK_MAINTENANCE_FINALIZE_RESERVE_MS, "5000"}
214
- SHARE_LINK_MAINTENANCE_GRAPH_TIMEOUT_MS: ${env:SHARE_LINK_MAINTENANCE_GRAPH_TIMEOUT_MS, "15000"}
141
+ # Every function shares the default execution role. Grant it read access to the stage's
142
+ # runtime-secrets bundle only; the id itself is passed through RUNTIME_SECRETS_ID.
143
+ iam:
144
+ role:
145
+ statements:
146
+ - Effect: Allow
147
+ Action: secretsmanager:GetSecretValue
148
+ Resource:
149
+ Fn::Sub: arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:brain-service/${sls:stage}/runtime-secrets-*
150
+ # Bundle mode emits only infra (Redis/queue CF intrinsics, the bundle pointer and the
151
+ # deploy commit); fallback mode retains the existing GitHub credential secrets. Non-secret
152
+ # per-stage tuning, allowlists and OIDC/share-link config are applied from checked-in stage
153
+ # config at cold start, never forwarded here. LAMBDA_STAGE comes from the CLI stage option.
154
+ environment: ${file(./serverless.function-env.cjs):provider}
215
155
  tracing:
216
156
  apiGateway: true
217
157
  lambda: true
@@ -263,6 +203,19 @@ functions:
263
203
  rate: rate(1 day)
264
204
  enabled: true
265
205
 
206
+ # Retry durable consent notification intents once per minute.
207
+ contractEvents:
208
+ handler: contractEventsLambda.contractEventsHandler
209
+ timeout: 60
210
+ reservedConcurrency: 1
211
+ vpc:
212
+ securityGroupIds:
213
+ - 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
214
+ subnetIds:
215
+ - Ref: PrivateSubnetA
216
+ events:
217
+ - schedule: rate(1 minute)
218
+
266
219
  # LC-2187 share-link recovery + cleanup. Dedicated entrypoint (no lambda.ts
267
220
  # import-time work). Runs automatically with valid service configuration;
268
221
  # absent wiring remains inert and partial wiring fails closed. The cadence
@@ -1,49 +1,18 @@
1
- import type { Context } from 'aws-lambda';
1
+ import { bootstrapLambda } from '@learncard/service-config';
2
2
 
3
- import {
4
- createShareLinkMaintenanceRuntime,
5
- type ShareLinkMaintenanceRuntime,
6
- } from './src/helpers/share-link-maintenance';
3
+ import { base, stages } from './src/config/stageConfig';
7
4
 
8
- /**
9
- * Dedicated LC-2187 share-link maintenance Lambda entrypoint.
10
- *
11
- * It deliberately does NOT import `lambda.ts`, so no unrelated embedding
12
- * backfill, tRPC router, Sentry input capture or inbox handler runs at import
13
- * time. The only side effect is resolving the service and maintenance
14
- * configuration; when it is absent or malformed the runtime is inert and the
15
- * handler performs no graph, remote or signing work.
16
- *
17
- * The scheduled event payload is ignored entirely: it can never override the
18
- * namespace, origin, audience or any other configuration, and it is never
19
- * logged.
20
- */
5
+ type ShareLinkMaintenanceApp = typeof import('./shareLinkMaintenanceLambdaApp');
21
6
 
22
- let runtime: ShareLinkMaintenanceRuntime | null = null;
23
-
24
- const getRuntime = (): ShareLinkMaintenanceRuntime => {
25
- if (runtime === null) {
26
- runtime = createShareLinkMaintenanceRuntime({
27
- rawEnvironment: process.env as Record<string, unknown>,
28
- });
29
- }
30
-
31
- return runtime;
32
- };
7
+ const getApplication = bootstrapLambda<ShareLinkMaintenanceApp>({
8
+ base,
9
+ stages,
10
+ stage: process.env.AWS_LAMBDA_FUNCTION_NAME
11
+ ? process.env.LAMBDA_STAGE
12
+ : process.env.CONFIG_STAGE,
13
+ importApp: () => import('./shareLinkMaintenanceLambdaApp'),
14
+ });
33
15
 
34
16
  export const shareLinkMaintenanceHandler = async (
35
- _event: unknown,
36
- context?: Pick<Context, 'getRemainingTimeInMillis'>
37
- ): Promise<{ status: string }> => {
38
- const hasRemainingTime = typeof context?.getRemainingTimeInMillis === 'function';
39
-
40
- const summary = await getRuntime().runOnce({
41
- // Preserve the LIVE Lambda remaining-time source: the runtime samples it
42
- // once, before dependency initialization, and anchors the invocation
43
- // budget to that instant.
44
- remainingTimeMs: hasRemainingTime ? () => context!.getRemainingTimeInMillis() : undefined,
45
- });
46
-
47
- // The runtime already emitted one aggregate-only allowlisted event.
48
- return { status: summary.status };
49
- };
17
+ ...args: Parameters<ShareLinkMaintenanceApp['shareLinkMaintenanceHandler']>
18
+ ) => (await getApplication()).shareLinkMaintenanceHandler(...args);
@@ -0,0 +1,50 @@
1
+ import type { Context } from 'aws-lambda';
2
+
3
+ import {
4
+ createShareLinkMaintenanceRuntime,
5
+ type ShareLinkMaintenanceRuntime,
6
+ } from './src/helpers/share-link-maintenance';
7
+
8
+ /**
9
+ * Dedicated LC-2187 share-link maintenance application module.
10
+ *
11
+ * It is imported lazily by the thin bootstrap entrypoint, after checked-in
12
+ * stage config has been applied, so no unrelated embedding backfill, tRPC
13
+ * router, Sentry input capture or inbox handler runs at import time. The only
14
+ * side effect is resolving the service and maintenance configuration; when it
15
+ * is absent or malformed the runtime is inert and the handler performs no
16
+ * graph, remote or signing work.
17
+ *
18
+ * The scheduled event payload is ignored entirely: it can never override the
19
+ * namespace, origin, audience or any other configuration, and it is never
20
+ * logged.
21
+ */
22
+
23
+ let runtime: ShareLinkMaintenanceRuntime | null = null;
24
+
25
+ const getRuntime = (): ShareLinkMaintenanceRuntime => {
26
+ if (runtime === null) {
27
+ runtime = createShareLinkMaintenanceRuntime({
28
+ rawEnvironment: process.env as Record<string, unknown>,
29
+ });
30
+ }
31
+
32
+ return runtime;
33
+ };
34
+
35
+ export const shareLinkMaintenanceHandler = async (
36
+ _event: unknown,
37
+ context?: Pick<Context, 'getRemainingTimeInMillis'>
38
+ ): Promise<{ status: string }> => {
39
+ const hasRemainingTime = typeof context?.getRemainingTimeInMillis === 'function';
40
+
41
+ const summary = await getRuntime().runOnce({
42
+ // Preserve the LIVE Lambda remaining-time source: the runtime samples it
43
+ // once, before dependency initialization, and anchors the invocation
44
+ // budget to that instant.
45
+ remainingTimeMs: hasRemainingTime ? () => context!.getRemainingTimeInMillis() : undefined,
46
+ });
47
+
48
+ // The runtime already emitted one aggregate-only allowlisted event.
49
+ return { status: summary.status };
50
+ };
@@ -0,0 +1,57 @@
1
+ import { BindParam, QueryBuilder } from 'neogma';
2
+ import { v4 as uuid } from 'uuid';
3
+
4
+ import { Integration, AppManifestVersion } from '@models';
5
+ import type { AppManifest } from '@learncard/types';
6
+
7
+ import type { AppManifestVersionType } from 'types/app-manifest';
8
+
9
+ import { readAppManifestVersionById } from './read';
10
+
11
+ export const createAppManifestVersion = async ({
12
+ integrationId,
13
+ version,
14
+ manifestHash,
15
+ manifest,
16
+ status = 'draft',
17
+ }: {
18
+ integrationId: string;
19
+ version: number;
20
+ manifestHash: string;
21
+ manifest: AppManifest;
22
+ status?: 'draft' | 'active' | 'superseded';
23
+ }): Promise<AppManifestVersionType> => {
24
+ const id = uuid();
25
+ const createdAt = new Date().toISOString();
26
+
27
+ await new QueryBuilder(
28
+ new BindParam({
29
+ integrationId,
30
+ params: {
31
+ id,
32
+ version,
33
+ manifestHash,
34
+ manifestJson: JSON.stringify(manifest),
35
+ status,
36
+ createdAt,
37
+ },
38
+ })
39
+ )
40
+ .match({ model: Integration, identifier: 'integration', where: { id: integrationId } })
41
+ .create({ model: AppManifestVersion, identifier: 'manifestVersion' })
42
+ .set('manifestVersion += $params')
43
+ .create(
44
+ `(integration)-[:${
45
+ Integration.getRelationshipByAlias('hasManifestVersion').name
46
+ }]->(manifestVersion)`
47
+ )
48
+ .run();
49
+
50
+ const created = await readAppManifestVersionById(id);
51
+
52
+ if (!created) {
53
+ throw new Error('Failed to create app manifest version');
54
+ }
55
+
56
+ return created;
57
+ };
@@ -0,0 +1,129 @@
1
+ import { neogma } from '@instance';
2
+
3
+ import { AppManifestVersionValidator } from '@learncard/types';
4
+
5
+ import type { AppManifestVersionType } from 'types/app-manifest';
6
+
7
+ const parseManifestVersionRecord = (
8
+ properties: Record<string, unknown>
9
+ ): AppManifestVersionType => {
10
+ const manifestJson = properties.manifestJson;
11
+ const rawVersion = properties.version as { toNumber?: () => number } | number;
12
+
13
+ if (typeof manifestJson !== 'string') {
14
+ throw new Error('AppManifestVersion.manifestJson is missing');
15
+ }
16
+
17
+ return AppManifestVersionValidator.parse({
18
+ id: properties.id,
19
+ version: typeof rawVersion === 'number' ? rawVersion : rawVersion?.toNumber?.(),
20
+ manifestHash: properties.manifestHash,
21
+ manifest: JSON.parse(manifestJson),
22
+ status: properties.status,
23
+ createdAt: properties.createdAt,
24
+ activatedAt: properties.activatedAt,
25
+ });
26
+ };
27
+
28
+ export const readAppManifestVersionById = async (
29
+ id: string
30
+ ): Promise<AppManifestVersionType | null> => {
31
+ const result = await neogma.queryRunner.run(
32
+ `MATCH (manifestVersion:AppManifestVersion {id: $id})
33
+ RETURN manifestVersion
34
+ LIMIT 1`,
35
+ { id }
36
+ );
37
+
38
+ const properties = result.records[0]?.get('manifestVersion')?.properties;
39
+
40
+ if (!properties) return null;
41
+
42
+ return parseManifestVersionRecord(properties as Record<string, unknown>);
43
+ };
44
+
45
+ export const getManifestVersionForIntegration = async (
46
+ integrationId: string,
47
+ version: number
48
+ ): Promise<AppManifestVersionType | null> => {
49
+ const result = await neogma.queryRunner.run(
50
+ `MATCH (:Integration {id: $integrationId})-[:HAS_MANIFEST_VERSION]->(manifestVersion:AppManifestVersion {version: $version})
51
+ RETURN manifestVersion
52
+ LIMIT 1`,
53
+ { integrationId, version }
54
+ );
55
+
56
+ const properties = result.records[0]?.get('manifestVersion')?.properties;
57
+
58
+ if (!properties) return null;
59
+
60
+ return parseManifestVersionRecord(properties as Record<string, unknown>);
61
+ };
62
+
63
+ export const getLatestManifestVersionForIntegration = async (
64
+ integrationId: string
65
+ ): Promise<AppManifestVersionType | null> => {
66
+ const result = await neogma.queryRunner.run(
67
+ `MATCH (:Integration {id: $integrationId})-[:HAS_MANIFEST_VERSION]->(manifestVersion:AppManifestVersion)
68
+ RETURN manifestVersion
69
+ ORDER BY manifestVersion.version DESC
70
+ LIMIT 1`,
71
+ { integrationId }
72
+ );
73
+
74
+ const properties = result.records[0]?.get('manifestVersion')?.properties;
75
+
76
+ if (!properties) return null;
77
+
78
+ return parseManifestVersionRecord(properties as Record<string, unknown>);
79
+ };
80
+
81
+ export const getActiveManifestVersionForIntegration = async (
82
+ integrationId: string
83
+ ): Promise<AppManifestVersionType | null> => {
84
+ const result = await neogma.queryRunner.run(
85
+ `MATCH (:Integration {id: $integrationId})-[:HAS_MANIFEST_VERSION]->(manifestVersion:AppManifestVersion {status: 'active'})
86
+ RETURN manifestVersion
87
+ ORDER BY manifestVersion.activatedAt DESC, manifestVersion.version DESC
88
+ LIMIT 1`,
89
+ { integrationId }
90
+ );
91
+
92
+ const properties = result.records[0]?.get('manifestVersion')?.properties;
93
+
94
+ if (!properties) return null;
95
+
96
+ return parseManifestVersionRecord(properties as Record<string, unknown>);
97
+ };
98
+
99
+ export const getManifestVersionsForIntegration = async (
100
+ integrationId: string,
101
+ { limit, cursor }: { limit: number; cursor?: string }
102
+ ): Promise<AppManifestVersionType[]> => {
103
+ const result = await neogma.queryRunner.run(
104
+ `MATCH (:Integration {id: $integrationId})-[:HAS_MANIFEST_VERSION]->(manifestVersion:AppManifestVersion)
105
+ ${cursor ? 'WHERE manifestVersion.version < toInteger($cursor)' : ''}
106
+ RETURN manifestVersion
107
+ ORDER BY manifestVersion.version DESC
108
+ LIMIT toInteger($limit)`,
109
+ { integrationId, cursor: cursor ?? null, limit }
110
+ );
111
+
112
+ return result.records.map(record => {
113
+ const properties = record.get('manifestVersion')?.properties as Record<string, unknown>;
114
+ return parseManifestVersionRecord(properties);
115
+ });
116
+ };
117
+
118
+ export const getListingIdForManifestVersion = async (id: string): Promise<string | null> => {
119
+ const result = await neogma.queryRunner.run(
120
+ `MATCH (listing:AppStoreListing)-[:USES_MANIFEST_VERSION]->(:AppManifestVersion {id: $id})
121
+ RETURN listing.listing_id AS listingId
122
+ LIMIT 1`,
123
+ { id }
124
+ );
125
+
126
+ const listingId = result.records[0]?.get('listingId');
127
+
128
+ return typeof listingId === 'string' ? listingId : null;
129
+ };
@@ -0,0 +1,61 @@
1
+ import { neogma } from '@instance';
2
+
3
+ export const updateAppManifestVersion = async (
4
+ id: string,
5
+ updates: Partial<{
6
+ status: 'draft' | 'active' | 'superseded';
7
+ activatedAt: string | null;
8
+ }>
9
+ ): Promise<boolean> => {
10
+ const setClauses: string[] = [];
11
+ const params: Record<string, unknown> = { id };
12
+
13
+ if (typeof updates.status !== 'undefined') {
14
+ params.status = updates.status;
15
+ setClauses.push('manifestVersion.status = $status');
16
+ }
17
+ if (typeof updates.activatedAt !== 'undefined') {
18
+ params.activatedAt = updates.activatedAt;
19
+ setClauses.push('manifestVersion.activatedAt = $activatedAt');
20
+ }
21
+
22
+ if (setClauses.length === 0) return true;
23
+
24
+ const result = await neogma.queryRunner.run(
25
+ `MATCH (manifestVersion:AppManifestVersion {id: $id})
26
+ SET ${setClauses.join(', ')}`,
27
+ params
28
+ );
29
+
30
+ return result.summary.updateStatistics.containsUpdates();
31
+ };
32
+
33
+ export const markManifestVersionsSuperseded = async (
34
+ integrationId: string,
35
+ exceptId: string
36
+ ): Promise<boolean> => {
37
+ const result = await neogma.queryRunner.run(
38
+ `MATCH (:Integration {id: $integrationId})-[:HAS_MANIFEST_VERSION]->(manifestVersion:AppManifestVersion)
39
+ WHERE manifestVersion.id <> $exceptId AND manifestVersion.status = 'active'
40
+ SET manifestVersion.status = 'superseded'`,
41
+ { integrationId, exceptId }
42
+ );
43
+
44
+ return result.summary.updateStatistics.containsUpdates();
45
+ };
46
+
47
+ export const associateListingWithManifestVersion = async (
48
+ listingId: string,
49
+ manifestVersionId: string
50
+ ): Promise<boolean> => {
51
+ await neogma.queryRunner.run(
52
+ `MATCH (listing:AppStoreListing {listing_id: $listingId})
53
+ MATCH (manifestVersion:AppManifestVersion {id: $manifestVersionId})
54
+ OPTIONAL MATCH (listing)-[existing:USES_MANIFEST_VERSION]->(:AppManifestVersion)
55
+ DELETE existing
56
+ MERGE (listing)-[:USES_MANIFEST_VERSION]->(manifestVersion)`,
57
+ { listingId, manifestVersionId }
58
+ );
59
+
60
+ return true;
61
+ };
@@ -244,16 +244,22 @@ export const getListedAppsWithSubmitter = async ({
244
244
  status,
245
245
  includeAllStatuses = false,
246
246
  excludeDemoted = true,
247
+ pendingUpdatesOnly = false,
247
248
  }: {
248
249
  limit: number;
249
250
  cursor?: string;
250
251
  status?: string;
251
252
  includeAllStatuses?: boolean;
252
253
  excludeDemoted?: boolean;
254
+ pendingUpdatesOnly?: boolean;
253
255
  }): Promise<ListingWithSubmitter[]> => {
254
256
  const whereClauses: string[] = [];
255
257
  const params: Record<string, any> = { limit: int(limit) };
256
258
 
259
+ if (pendingUpdatesOnly) {
260
+ whereClauses.push("listing.pending_update_status = 'PENDING_REVIEW'");
261
+ }
262
+
257
263
  if (status) {
258
264
  whereClauses.push('listing.app_listing_status = $status');
259
265
  params.status = status;
@@ -96,6 +96,26 @@ export const associateBoostWithListing = async (
96
96
  return true;
97
97
  };
98
98
 
99
+ export const associateConsentContractWithListing = async (
100
+ listingId: string,
101
+ contractId: string,
102
+ scopeHash: string
103
+ ): Promise<boolean> => {
104
+ await AppStoreListing.relateTo({
105
+ alias: 'hasConsentContract',
106
+ where: {
107
+ source: { listing_id: listingId },
108
+ target: { id: contractId },
109
+ },
110
+ properties: {
111
+ scopeHash,
112
+ createdAt: new Date().toISOString(),
113
+ },
114
+ });
115
+
116
+ return true;
117
+ };
118
+
99
119
  export const associateListingWithSigningAuthority = async (
100
120
  listingId: string,
101
121
  signingAuthorityEndpoint: string,
@@ -6,6 +6,8 @@ import { Boost, BoostInstance } from '@models';
6
6
  import { int } from 'neo4j-driver';
7
7
  import { getBoostUri } from '@helpers/boost.helpers';
8
8
  import { parseIntegration } from '@helpers/integrations.helpers';
9
+ import { getContractById } from '@accesslayer/consentflowcontract/read';
10
+ import type { DbContractType } from 'types/consentflowcontract';
9
11
 
10
12
  export const getIntegrationForListing = async (
11
13
  listingIdOrSlug: string
@@ -93,7 +95,9 @@ export const getBoostForListingByTemplateAlias = async (
93
95
  ): Promise<{ boost: BoostInstance; templateAlias: string; boostUri: string } | null> => {
94
96
  const result = await neogma.queryRunner.run(
95
97
  `MATCH (listing:AppStoreListing {listing_id: $listingId})-[r:HAS_BOOST {templateAlias: $templateAlias}]->(b:Boost)
96
- RETURN b.id AS id, r.templateAlias AS templateAlias`,
98
+ RETURN b.id AS id, r.templateAlias AS templateAlias
99
+ ORDER BY r.createdAt DESC
100
+ LIMIT 1`,
97
101
  { listingId, templateAlias }
98
102
  );
99
103
 
@@ -146,6 +150,25 @@ export const hasTemplateAliasForListing = async (
146
150
  return result.records[0]?.get('exists') ?? false;
147
151
  };
148
152
 
153
+ export const getConsentContractForListingByScopeHash = async (
154
+ listingId: string,
155
+ scopeHash: string
156
+ ): Promise<DbContractType | null> => {
157
+ const result = await neogma.queryRunner.run(
158
+ `MATCH (listing:AppStoreListing {listing_id: $listingId})
159
+ -[:HAS_CONSENT_CONTRACT {scopeHash: $scopeHash}]->(contract:ConsentFlowContract)
160
+ RETURN contract.id AS id
161
+ ORDER BY contract.createdAt ASC
162
+ LIMIT 1`,
163
+ { listingId, scopeHash }
164
+ );
165
+
166
+ const id = result.records[0]?.get('id');
167
+ if (typeof id !== 'string') return null;
168
+
169
+ return getContractById(id);
170
+ };
171
+
149
172
  export const getCredentialsSentByListingToProfile = async (
150
173
  listingId: string,
151
174
  profileId: string,
@@ -1,6 +1,7 @@
1
1
  import { QueryBuilder, BindParam } from 'neogma';
2
2
 
3
3
  import { AppStoreListing } from '@models';
4
+ import { neogma } from '@instance';
4
5
  import {
5
6
  FlatAppStoreListingType,
6
7
  AppStoreListingType,
@@ -50,6 +51,12 @@ export const updateAppStoreListing = async (
50
51
  updatesToPersist.submitted_at = updates.submitted_at;
51
52
  if (typeof updates.contact_email !== 'undefined')
52
53
  updatesToPersist.contact_email = updates.contact_email;
54
+ if (typeof updates.pending_update_json !== 'undefined')
55
+ updatesToPersist.pending_update_json = updates.pending_update_json;
56
+ if (typeof updates.pending_update_status !== 'undefined')
57
+ updatesToPersist.pending_update_status = updates.pending_update_status;
58
+ if (typeof updates.pending_update_submitted_at !== 'undefined')
59
+ updatesToPersist.pending_update_submitted_at = updates.pending_update_submitted_at;
53
60
 
54
61
  const params: Partial<FlatAppStoreListingType> = updatesToPersist;
55
62
 
@@ -67,3 +74,11 @@ export const updateAppStoreListing = async (
67
74
 
68
75
  return result.summary.updateStatistics.containsUpdates();
69
76
  };
77
+
78
+ export const clearPendingListingUpdate = async (listingId: string): Promise<void> => {
79
+ await neogma.queryRunner.run(
80
+ `MATCH (listing:AppStoreListing {listing_id: $listingId})
81
+ REMOVE listing.pending_update_json, listing.pending_update_status, listing.pending_update_submitted_at`,
82
+ { listingId }
83
+ );
84
+ };