@learncard/network-brain-service 3.19.1 → 4.0.1

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 (315) hide show
  1. package/.env.example +44 -111
  2. package/AGENTS.md +52 -0
  3. package/CHANGELOG.md +120 -0
  4. package/README.md +9 -2
  5. package/bootstrapLambda.test.ts +72 -0
  6. package/bundle.mjs +9 -1
  7. package/config/config.dev.json +29 -0
  8. package/config/config.example.json +29 -0
  9. package/config/config.json +1 -0
  10. package/config/config.production.json +29 -0
  11. package/config/config.scouts.dev.json +24 -0
  12. package/config/config.scouts.production.json +24 -0
  13. package/contractEventsLambda.ts +18 -0
  14. package/contractEventsLambdaApp.ts +19 -0
  15. package/didWebLambda.ts +13 -18
  16. package/didWebLambdaApp.ts +22 -0
  17. package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
  18. package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
  19. package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
  20. package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
  21. package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
  22. package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
  23. package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
  24. package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
  25. package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
  26. package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
  27. package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
  28. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  29. package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
  30. package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
  31. package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
  32. package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
  33. package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
  34. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  35. package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
  36. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  37. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
  38. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  39. package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
  40. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
  41. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  42. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
  43. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
  44. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
  45. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
  47. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  48. package/dist/accesslayer/credential/create.d.ts +2 -1
  49. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  50. package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
  51. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  52. package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
  53. package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
  54. package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
  55. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  56. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -1
  57. package/dist/accesslayer/share-link/attachment.d.ts +37 -0
  58. package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
  59. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -1
  60. package/dist/accesslayer/share-link/helpers.d.ts +3 -0
  61. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -1
  62. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -1
  63. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -1
  64. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -1
  65. package/dist/accesslayer/share-link/types.d.ts +8 -3
  66. package/dist/accesslayer/share-link/types.d.ts.map +1 -1
  67. package/dist/cache/app-store.caches.d.ts +49 -0
  68. package/dist/cache/app-store.caches.d.ts.map +1 -1
  69. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  70. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  71. package/dist/config/environment.d.ts +5 -0
  72. package/dist/config/environment.d.ts.map +1 -1
  73. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  74. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  75. package/dist/config/stageConfig.d.ts +112 -0
  76. package/dist/config/stageConfig.d.ts.map +1 -0
  77. package/dist/config/stageConfig.test.d.ts +2 -0
  78. package/dist/config/stageConfig.test.d.ts.map +1 -0
  79. package/dist/didkit_wasm_bg.wasm +0 -0
  80. package/dist/docker-entry.d.ts +1 -1
  81. package/dist/docker-entry.d.ts.map +1 -1
  82. package/dist/helpers/app-manifest.helpers.d.ts +5 -0
  83. package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
  84. package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
  85. package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
  86. package/dist/helpers/consent-referral.helpers.d.ts +2 -0
  87. package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
  88. package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
  89. package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
  90. package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
  91. package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
  92. package/dist/helpers/contract-events.helpers.d.ts +25 -0
  93. package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
  94. package/dist/helpers/contract-requests.helpers.d.ts +19 -0
  95. package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
  96. package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
  97. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  98. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  99. package/dist/helpers/inline-template.helpers.d.ts +45 -0
  100. package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
  101. package/dist/helpers/notificationMessages.d.ts +1 -1
  102. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  103. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  104. package/dist/helpers/notifications.helpers.d.ts +6 -0
  105. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  106. package/dist/helpers/profile.helpers.d.ts +1 -0
  107. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  108. package/dist/helpers/share-link-attachment-access.d.ts +4 -0
  109. package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
  110. package/dist/helpers/share-link-attachment.d.ts +18 -0
  111. package/dist/helpers/share-link-attachment.d.ts.map +1 -0
  112. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
  113. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
  114. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -1
  115. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -1
  116. package/dist/helpers/share-link-maintenance/runner.d.ts +1 -0
  117. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -1
  118. package/dist/helpers/share-link-maintenance/runtime.d.ts +2 -0
  119. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -1
  120. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -1
  121. package/dist/helpers/share-link-maintenance/types.d.ts +4 -1
  122. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -1
  123. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -1
  124. package/dist/helpers/share-link-policy/production.d.ts +3 -5
  125. package/dist/helpers/share-link-policy/production.d.ts.map +1 -1
  126. package/dist/helpers/share-link-policy/resolver.d.ts +1 -1
  127. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -1
  128. package/dist/helpers/share-link-policy/types.d.ts +8 -2
  129. package/dist/helpers/share-link-policy/types.d.ts.map +1 -1
  130. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  131. package/dist/helpers/smartResume.helpers.d.ts +3 -0
  132. package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
  133. package/dist/index.js +2 -2
  134. package/dist/models/AppManifestVersion.d.ts +20 -0
  135. package/dist/models/AppManifestVersion.d.ts.map +1 -0
  136. package/dist/models/AppStoreListing.d.ts +13 -0
  137. package/dist/models/AppStoreListing.d.ts.map +1 -1
  138. package/dist/models/ConsentFlowContract.d.ts +13 -2
  139. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  140. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  141. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  142. package/dist/models/Integration.d.ts +2 -0
  143. package/dist/models/Integration.d.ts.map +1 -1
  144. package/dist/models/ShareLink.d.ts +2 -0
  145. package/dist/models/ShareLink.d.ts.map +1 -1
  146. package/dist/models/index.d.ts +1 -0
  147. package/dist/models/index.d.ts.map +1 -1
  148. package/dist/models/share-link-constraints.d.ts.map +1 -1
  149. package/dist/openapi.d.ts +1 -1
  150. package/dist/openapi.d.ts.map +1 -1
  151. package/dist/publish-metafile.json +1 -0
  152. package/dist/routes/app-store.d.ts +619 -0
  153. package/dist/routes/app-store.d.ts.map +1 -1
  154. package/dist/routes/boosts.d.ts +2 -2
  155. package/dist/routes/boosts.d.ts.map +1 -1
  156. package/dist/routes/contracts.d.ts +146 -3
  157. package/dist/routes/contracts.d.ts.map +1 -1
  158. package/dist/routes/credentials.d.ts +34 -0
  159. package/dist/routes/credentials.d.ts.map +1 -1
  160. package/dist/routes/inbox.d.ts +52 -2
  161. package/dist/routes/inbox.d.ts.map +1 -1
  162. package/dist/routes/index.d.ts.map +1 -1
  163. package/dist/routes/profile-manager.d.ts.map +1 -1
  164. package/dist/routes/profiles.d.ts.map +1 -1
  165. package/dist/routes/public-share-links.d.ts +55 -0
  166. package/dist/routes/public-share-links.d.ts.map +1 -1
  167. package/dist/routes/share-links.d.ts +175 -14
  168. package/dist/routes/share-links.d.ts.map +1 -1
  169. package/dist/routes/test.d.ts +5 -0
  170. package/dist/routes/test.d.ts.map +1 -1
  171. package/dist/routes/workflows.d.ts.map +1 -1
  172. package/dist/types/app-manifest.d.ts +339 -0
  173. package/dist/types/app-manifest.d.ts.map +1 -0
  174. package/dist/types/app-store-listing.d.ts +34 -0
  175. package/dist/types/app-store-listing.d.ts.map +1 -1
  176. package/dist/types/consentflowcontract.d.ts +30 -3
  177. package/dist/types/consentflowcontract.d.ts.map +1 -1
  178. package/dist/types/credential-refresh.d.ts +1 -1
  179. package/esbuildPlugins.cjs +1 -0
  180. package/lambda.ts +49 -195
  181. package/lambdaApp.ts +203 -0
  182. package/package.json +20 -19
  183. package/project.json +8 -1
  184. package/scripts/export-openapi.ts +20 -0
  185. package/scripts/offline-schema-environment.ts +18 -0
  186. package/scripts/verify-python-contracts.ts +72 -0
  187. package/serverless-local.yml +3 -6
  188. package/serverless.function-env.cjs +50 -0
  189. package/serverless.local.yml +0 -1
  190. package/serverless.yml +29 -74
  191. package/shareLinkMaintenanceLambda.ts +13 -44
  192. package/shareLinkMaintenanceLambdaApp.ts +50 -0
  193. package/src/accesslayer/app-manifest-version/create.ts +57 -0
  194. package/src/accesslayer/app-manifest-version/read.ts +129 -0
  195. package/src/accesslayer/app-manifest-version/update.ts +61 -0
  196. package/src/accesslayer/app-store-listing/read.ts +6 -0
  197. package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
  198. package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
  199. package/src/accesslayer/app-store-listing/update.ts +15 -0
  200. package/src/accesslayer/boost/relationships/create.ts +8 -2
  201. package/src/accesslayer/consentflowcontract/create.ts +81 -8
  202. package/src/accesslayer/consentflowcontract/read.ts +84 -10
  203. package/src/accesslayer/consentflowcontract/relationships/create.ts +116 -93
  204. package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
  205. package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
  206. package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
  207. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
  208. package/src/accesslayer/consentflowcontract/relationships/update.ts +240 -189
  209. package/src/accesslayer/credential/create.test.ts +2 -115
  210. package/src/accesslayer/credential/create.ts +11 -6
  211. package/src/accesslayer/credential/relationships/create.ts +17 -10
  212. package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
  213. package/src/accesslayer/inbox-credential/update.ts +4 -2
  214. package/src/accesslayer/share-link/abandon.ts +20 -0
  215. package/src/accesslayer/share-link/attachment.ts +360 -0
  216. package/src/accesslayer/share-link/finalize.ts +55 -7
  217. package/src/accesslayer/share-link/helpers.ts +33 -2
  218. package/src/accesslayer/share-link/recovery.ts +20 -0
  219. package/src/accesslayer/share-link/reserve.ts +85 -6
  220. package/src/accesslayer/share-link/revoke.ts +35 -0
  221. package/src/accesslayer/share-link/types.ts +9 -4
  222. package/src/cache/app-store.caches.ts +35 -0
  223. package/src/config/applyDockerStageConfig.ts +6 -0
  224. package/src/config/environment.test.ts +17 -12
  225. package/src/config/environment.ts +1 -0
  226. package/src/config/serverlessFunctionEnv.test.ts +105 -0
  227. package/src/config/stageConfig.test.ts +25 -0
  228. package/src/config/stageConfig.ts +24 -0
  229. package/src/docker-entry.ts +24 -0
  230. package/src/helpers/app-manifest.helpers.ts +185 -0
  231. package/src/helpers/claim-hooks.helpers.ts +20 -7
  232. package/src/helpers/consent-referral.helpers.ts +6 -0
  233. package/src/helpers/consent-scopes.helpers.ts +58 -0
  234. package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
  235. package/src/helpers/contract-events.helpers.ts +540 -0
  236. package/src/helpers/contract-requests.helpers.ts +211 -0
  237. package/src/helpers/finalize-inbox.helpers.ts +163 -44
  238. package/src/helpers/inbox.helpers.ts +51 -66
  239. package/src/helpers/inline-template.helpers.ts +560 -0
  240. package/src/helpers/notificationMessages.ts +59 -0
  241. package/src/helpers/notificationQueue.helpers.ts +23 -5
  242. package/src/helpers/notifications.helpers.ts +25 -1
  243. package/src/helpers/profile.helpers.ts +6 -0
  244. package/src/helpers/share-link-attachment-access.ts +87 -0
  245. package/src/helpers/share-link-attachment.ts +115 -0
  246. package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
  247. package/src/helpers/share-link-coordinator/coordinator.ts +15 -2
  248. package/src/helpers/share-link-coordinator/recovery-runner.ts +15 -0
  249. package/src/helpers/share-link-maintenance/runner.ts +28 -0
  250. package/src/helpers/share-link-maintenance/runtime.ts +6 -0
  251. package/src/helpers/share-link-maintenance/telemetry.ts +3 -0
  252. package/src/helpers/share-link-maintenance/types.ts +2 -0
  253. package/src/helpers/share-link-owner-projection.ts +2 -0
  254. package/src/helpers/share-link-policy/production.ts +37 -17
  255. package/src/helpers/share-link-policy/resolver.ts +16 -11
  256. package/src/helpers/share-link-policy/types.ts +15 -7
  257. package/src/helpers/signingAuthority.helpers.ts +23 -14
  258. package/src/helpers/smartResume.helpers.ts +123 -0
  259. package/src/models/AppManifestVersion.ts +43 -0
  260. package/src/models/AppStoreListing.ts +32 -0
  261. package/src/models/ConsentFlowContract.ts +34 -3
  262. package/src/models/ConsentFlowTerms.ts +10 -0
  263. package/src/models/ConsentFlowTransaction.ts +3 -0
  264. package/src/models/Integration.ts +2 -0
  265. package/src/models/ShareLink.ts +4 -0
  266. package/src/models/index.ts +23 -0
  267. package/src/models/share-link-constraints.ts +5 -0
  268. package/src/openapi.ts +44 -8
  269. package/src/routes/app-store.ts +1054 -27
  270. package/src/routes/boosts.ts +12 -14
  271. package/src/routes/contracts.ts +382 -262
  272. package/src/routes/inbox.ts +4 -2
  273. package/src/routes/index.ts +4 -1
  274. package/src/routes/profile-manager.ts +19 -0
  275. package/src/routes/profiles.ts +12 -0
  276. package/src/routes/public-share-links.ts +169 -0
  277. package/src/routes/share-links.ts +69 -0
  278. package/src/routes/workflows.ts +3 -2
  279. package/src/types/app-manifest.ts +28 -0
  280. package/src/types/app-store-listing.ts +13 -0
  281. package/src/types/consentflowcontract.ts +25 -4
  282. package/test/app-manifest.helpers.spec.ts +170 -0
  283. package/test/app-manifest.routes.spec.ts +299 -0
  284. package/test/app-store-listing-updates.spec.ts +236 -0
  285. package/test/app-store-listing.spec.ts +65 -3
  286. package/test/consent-data-boundaries.spec.ts +352 -0
  287. package/test/consent-recipients.spec.ts +513 -0
  288. package/test/consent-scopes.helpers.spec.ts +67 -0
  289. package/test/consentflow.spec.ts +63 -2
  290. package/test/contract-requests.spec.ts +1588 -0
  291. package/test/credential-storage.neo4j.spec.ts +122 -0
  292. package/test/guardian-gated-routes.spec.ts +36 -0
  293. package/test/inbox-security.e2e.ts +26 -10
  294. package/test/inbox.spec.ts +184 -8
  295. package/test/inline-template.helpers.spec.ts +49 -0
  296. package/test/managed-recipient-inbox.spec.ts +24 -2
  297. package/test/openapi.spec.ts +14 -73
  298. package/test/profiles.spec.ts +30 -0
  299. package/test/share-link-attachment.neo4j.spec.ts +262 -0
  300. package/test/share-link-attachment.unit.spec.ts +244 -0
  301. package/test/share-link-lifecycle.neo4j.spec.ts +145 -2
  302. package/test/share-link-maintenance.unit.spec.ts +32 -5
  303. package/test/share-link-owner-route.unit.spec.ts +70 -2
  304. package/test/share-link-policy.unit.spec.ts +18 -4
  305. package/test/share-link-production-policy.unit.spec.ts +58 -9
  306. package/test/share-link-public-route.unit.spec.ts +279 -0
  307. package/test/share-link-service-policy.neo4j.spec.ts +403 -0
  308. package/test/signing-authority-encryption.spec.ts +75 -0
  309. package/test/smart-resume-consent.spec.ts +233 -0
  310. package/test/smart-resume-upload.spec.ts +65 -0
  311. package/tsconfig.build.json +1 -1
  312. package/tsconfig.json +1 -0
  313. package/vitest.config.ts +13 -0
  314. package/vitest.inbox-e2e.config.ts +2 -0
  315. package/workerLambda.test.ts +100 -0
@@ -0,0 +1,560 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ import { z } from 'zod';
4
+ import { TRPCError } from '@trpc/server';
5
+ import type { UnsignedVC } from '@learncard/types';
6
+ import type { InlineCredentialTemplate, VariableManifest } from '@learncard/partner-connect-core';
7
+ import {
8
+ canonicalJsonString,
9
+ compileInlineTemplate,
10
+ validateInlineTemplate,
11
+ validateTemplateData,
12
+ } from '@learncard/partner-connect-core';
13
+
14
+ import { getBoostUri } from '@helpers/boost.helpers';
15
+ import { getAppDidWeb, getDidWeb } from '@helpers/did.helpers';
16
+ import { getDidWebLearnCard, getLearnCard } from '@helpers/learnCard.helpers';
17
+ import { inflateObject } from '@helpers/objects.helpers';
18
+ import { createBoostForListing } from '@accesslayer/boost/create';
19
+ import { updateBoost } from '@accesslayer/boost/update';
20
+ import { removeBoostFromListing } from '@accesslayer/app-store-listing/relationships/delete';
21
+ import {
22
+ associateBoostWithListing,
23
+ associateListingWithSigningAuthority,
24
+ } from '@accesslayer/app-store-listing/relationships/create';
25
+ import { getBoostForListingByTemplateAlias } from '@accesslayer/app-store-listing/relationships/read';
26
+ import { upsertSigningAuthority } from '@accesslayer/signing-authority/create';
27
+ import { createUseSigningAuthorityRelationship } from '@accesslayer/signing-authority/relationships/create';
28
+ import {
29
+ getPrimarySigningAuthorityForListing,
30
+ getPrimarySigningAuthorityForIntegration,
31
+ getPrimarySigningAuthorityForUser,
32
+ } from '@accesslayer/signing-authority/relationships/read';
33
+ import {
34
+ invalidateBoostForListingCache,
35
+ invalidateListingSigningAuthorityCache,
36
+ } from '@cache/app-store.caches';
37
+ import { deleteDidDocForProfile } from '@cache/did-docs';
38
+ import { neogma } from '@instance';
39
+ import { environment } from '@environment';
40
+ import { Boost, type BoostInstance } from '@models';
41
+ import type { AppStoreListingType } from 'types/app-store-listing';
42
+ import type { IntegrationType } from 'types/integration';
43
+ import type { ProfileType, SigningAuthorityForUserType } from 'types/profile';
44
+
45
+ type InlineTemplateBoostMeta = {
46
+ integrationId?: string;
47
+ templateAlias?: string;
48
+ inlineTemplateHash?: string;
49
+ inlineTemplateVersion?: number;
50
+ inlineVariableManifest?: string;
51
+ source?: string;
52
+ supersededAt?: string;
53
+ supersededBy?: string;
54
+ };
55
+
56
+ type UpsertInlineTemplateBoostResult = {
57
+ boost: BoostInstance;
58
+ boostUri: string;
59
+ templateVersion: number;
60
+ manifest: VariableManifest;
61
+ };
62
+
63
+ const INLINE_TEMPLATE_SOURCE = 'partner-connect-inline';
64
+ const DEFAULT_SIGNING_AUTHORITY_ENDPOINT =
65
+ environment.SIGNING_AUTHORITY_ENDPOINT ?? 'http://localhost:5100/api';
66
+ const MAX_SIGNING_AUTHORITY_NAME_LENGTH = 15;
67
+ const SIGNING_AUTHORITY_PREFIX = 'app-';
68
+
69
+ const getBoostMeta = (boost: BoostInstance): InlineTemplateBoostMeta => {
70
+ const inflated = inflateObject<Record<string, unknown>>(
71
+ boost.getDataValues() as Record<string, unknown>
72
+ );
73
+ return (inflated.meta as InlineTemplateBoostMeta | undefined) ?? {};
74
+ };
75
+
76
+ export const readInlineTemplateBoostMeta = async (
77
+ boostOrId: BoostInstance | string
78
+ ): Promise<InlineTemplateBoostMeta> => {
79
+ const boostId = typeof boostOrId === 'string' ? boostOrId : boostOrId.id;
80
+ const inMemoryMeta = typeof boostOrId === 'string' ? undefined : getBoostMeta(boostOrId);
81
+
82
+ if (
83
+ inMemoryMeta?.inlineTemplateHash ||
84
+ typeof inMemoryMeta?.inlineTemplateVersion === 'number' ||
85
+ inMemoryMeta?.templateAlias
86
+ ) {
87
+ return inMemoryMeta;
88
+ }
89
+
90
+ const result = await neogma.queryRunner.run(
91
+ `MATCH (boost:Boost {id: $boostId})
92
+ RETURN boost.\`meta.integrationId\` AS integrationId,
93
+ boost.\`meta.templateAlias\` AS templateAlias,
94
+ boost.\`meta.inlineTemplateHash\` AS inlineTemplateHash,
95
+ boost.\`meta.inlineTemplateVersion\` AS inlineTemplateVersion,
96
+ boost.\`meta.inlineVariableManifest\` AS inlineVariableManifest,
97
+ boost.\`meta.source\` AS source,
98
+ boost.\`meta.supersededAt\` AS supersededAt,
99
+ boost.\`meta.supersededBy\` AS supersededBy`,
100
+ { boostId }
101
+ );
102
+
103
+ const record = result.records[0];
104
+
105
+ if (!record) {
106
+ return {};
107
+ }
108
+
109
+ const rawVersion = record.get('inlineTemplateVersion') as { toNumber?: () => number } | number;
110
+
111
+ return {
112
+ integrationId: record.get('integrationId') ?? undefined,
113
+ templateAlias: record.get('templateAlias') ?? undefined,
114
+ inlineTemplateHash: record.get('inlineTemplateHash') ?? undefined,
115
+ inlineTemplateVersion:
116
+ typeof rawVersion === 'number' ? rawVersion : rawVersion?.toNumber?.(),
117
+ inlineVariableManifest: record.get('inlineVariableManifest') ?? undefined,
118
+ source: record.get('source') ?? undefined,
119
+ supersededAt: record.get('supersededAt') ?? undefined,
120
+ supersededBy: record.get('supersededBy') ?? undefined,
121
+ };
122
+ };
123
+
124
+ const joinTemplateErrors = (errors: Array<{ message: string }>): string => {
125
+ return errors.map(error => error.message).join('; ');
126
+ };
127
+
128
+ export const getInlineTemplateVersion = (boost: BoostInstance): number | undefined => {
129
+ const version = getBoostMeta(boost).inlineTemplateVersion;
130
+ return typeof version === 'number' && Number.isFinite(version) ? version : undefined;
131
+ };
132
+
133
+ export const getInlineTemplateHashForBoost = (boost: BoostInstance): string | undefined => {
134
+ const inlineTemplateHash = getBoostMeta(boost).inlineTemplateHash;
135
+ return typeof inlineTemplateHash === 'string' ? inlineTemplateHash : undefined;
136
+ };
137
+
138
+ export const isInlineTemplateBoostVersionMatch = (
139
+ boost: BoostInstance,
140
+ templateHash: string,
141
+ templateVersion: number
142
+ ): boolean => {
143
+ return (
144
+ getInlineTemplateHashForBoost(boost) === templateHash &&
145
+ getInlineTemplateVersion(boost) === templateVersion
146
+ );
147
+ };
148
+
149
+ export const computeInlineTemplateHash = (
150
+ template: InlineCredentialTemplate
151
+ ): {
152
+ credentialTemplateJson: string;
153
+ variableManifest: VariableManifest;
154
+ category: string;
155
+ walletSkills?: unknown;
156
+ contentHash: string;
157
+ } => {
158
+ const compiled = compileInlineTemplate(template);
159
+ const contentHash = crypto
160
+ .createHash('sha256')
161
+ .update(
162
+ canonicalJsonString({
163
+ credentialTemplateJson: compiled.credentialTemplateJson,
164
+ category: compiled.category,
165
+ walletSkills: compiled.walletSkills,
166
+ })
167
+ )
168
+ .digest('hex');
169
+
170
+ return {
171
+ ...compiled,
172
+ contentHash,
173
+ };
174
+ };
175
+
176
+ export const validateInlineTemplateOrThrow = (template: InlineCredentialTemplate): void => {
177
+ const errors = validateInlineTemplate(template);
178
+
179
+ if (errors.length === 0) {
180
+ return;
181
+ }
182
+
183
+ throw new TRPCError({
184
+ code: 'BAD_REQUEST',
185
+ message: `TEMPLATE_INVALID: ${joinTemplateErrors(errors)}`,
186
+ });
187
+ };
188
+
189
+ export const validateInlineTemplateDataOrThrow = (
190
+ manifest: VariableManifest,
191
+ templateData: Record<string, unknown> | undefined
192
+ ): void => {
193
+ const errors = validateTemplateData(manifest, templateData);
194
+
195
+ if (errors.length === 0) {
196
+ return;
197
+ }
198
+
199
+ throw new TRPCError({
200
+ code: 'BAD_REQUEST',
201
+ message: `TEMPLATE_DATA_INVALID: ${joinTemplateErrors(errors)}`,
202
+ });
203
+ };
204
+
205
+ const ensureListingSigningAuthorityRelationship = async (
206
+ listing: AppStoreListingType,
207
+ signingAuthority: SigningAuthorityForUserType
208
+ ): Promise<void> => {
209
+ const listingSigningAuthority = await getPrimarySigningAuthorityForListing(listing);
210
+
211
+ if (
212
+ listingSigningAuthority &&
213
+ listingSigningAuthority.signingAuthority.endpoint ===
214
+ signingAuthority.signingAuthority.endpoint &&
215
+ listingSigningAuthority.relationship.name === signingAuthority.relationship.name &&
216
+ listingSigningAuthority.relationship.did === signingAuthority.relationship.did
217
+ ) {
218
+ return;
219
+ }
220
+
221
+ await associateListingWithSigningAuthority(
222
+ listing.listing_id,
223
+ signingAuthority.signingAuthority.endpoint,
224
+ {
225
+ name: signingAuthority.relationship.name,
226
+ did: signingAuthority.relationship.did,
227
+ isPrimary: true,
228
+ }
229
+ );
230
+
231
+ invalidateListingSigningAuthorityCache(listing.listing_id);
232
+ };
233
+
234
+ const buildManagedSigningAuthorityName = (listing: AppStoreListingType): string => {
235
+ const base = (listing.slug || listing.listing_id).toLowerCase().replace(/[^a-z0-9-]/g, '');
236
+ const trimmed = base.slice(
237
+ 0,
238
+ MAX_SIGNING_AUTHORITY_NAME_LENGTH - SIGNING_AUTHORITY_PREFIX.length
239
+ );
240
+ return `${SIGNING_AUTHORITY_PREFIX}${trimmed || 'app'}`.slice(
241
+ 0,
242
+ MAX_SIGNING_AUTHORITY_NAME_LENGTH
243
+ );
244
+ };
245
+
246
+ const ManagedSigningAuthorityResponseValidator = z.object({ did: z.string().min(1) });
247
+
248
+ /**
249
+ * Gets or creates the app's signing authority through LCA API, which owns key generation
250
+ * and encrypts the seed at rest. Brain authenticates as a trusted service DID.
251
+ */
252
+ const provisionManagedSigningAuthority = async (
253
+ endpoint: string,
254
+ ownerDid: string,
255
+ name: string
256
+ ): Promise<string> => {
257
+ // Tests mock signing-authority issuance, so derive a stable DID without calling LCA API.
258
+ if (environment.NODE_ENV === 'test') {
259
+ return (await getLearnCard('e'.repeat(64))).id.did();
260
+ }
261
+
262
+ try {
263
+ const learnCard = await getDidWebLearnCard();
264
+ const didJwt = await learnCard.invoke.getDidAuthVp({ proofFormat: 'jwt' });
265
+
266
+ const response = await fetch(`${endpoint}/signing-authority/managed`, {
267
+ method: 'POST',
268
+ headers: {
269
+ 'Content-Type': 'application/json',
270
+ 'Authorization': `Bearer ${didJwt}`,
271
+ },
272
+ body: JSON.stringify({ ownerDid, name }),
273
+ });
274
+
275
+ if (!response.ok) {
276
+ throw new Error(`LCA API responded with ${response.status}`);
277
+ }
278
+
279
+ return ManagedSigningAuthorityResponseValidator.parse(await response.json()).did;
280
+ } catch (error) {
281
+ const message = error instanceof Error ? error.message : String(error);
282
+ throw new TRPCError({
283
+ code: 'INTERNAL_SERVER_ERROR',
284
+ message: `Unable to auto-provision app signing authority: ${message}`,
285
+ cause: error,
286
+ });
287
+ }
288
+ };
289
+
290
+ export const ensureManagedSigningAuthorityForListing = async (
291
+ listing: AppStoreListingType,
292
+ integration: IntegrationType,
293
+ integrationOwner: ProfileType,
294
+ domain: string
295
+ ): Promise<SigningAuthorityForUserType> => {
296
+ const existingListingSa = await getPrimarySigningAuthorityForListing(listing);
297
+ if (existingListingSa) {
298
+ return existingListingSa;
299
+ }
300
+
301
+ const existingIntegrationSa = await getPrimarySigningAuthorityForIntegration(integration.id);
302
+ if (existingIntegrationSa) {
303
+ await ensureListingSigningAuthorityRelationship(listing, existingIntegrationSa);
304
+ return existingIntegrationSa;
305
+ }
306
+
307
+ const existingOwnerSa = await getPrimarySigningAuthorityForUser(integrationOwner);
308
+ if (existingOwnerSa) {
309
+ await ensureListingSigningAuthorityRelationship(listing, existingOwnerSa);
310
+ return existingOwnerSa;
311
+ }
312
+
313
+ const ownerDid = listing.slug
314
+ ? getAppDidWeb(domain, listing.slug)
315
+ : getDidWeb(domain, integrationOwner.profileId);
316
+ const name = buildManagedSigningAuthorityName(listing);
317
+ const did = await provisionManagedSigningAuthority(
318
+ DEFAULT_SIGNING_AUTHORITY_ENDPOINT,
319
+ ownerDid,
320
+ name
321
+ );
322
+ const signingAuthority = await upsertSigningAuthority(DEFAULT_SIGNING_AUTHORITY_ENDPOINT);
323
+
324
+ await createUseSigningAuthorityRelationship(
325
+ integrationOwner,
326
+ signingAuthority,
327
+ name,
328
+ did,
329
+ true
330
+ );
331
+ await associateListingWithSigningAuthority(
332
+ listing.listing_id,
333
+ DEFAULT_SIGNING_AUTHORITY_ENDPOINT,
334
+ {
335
+ name,
336
+ did,
337
+ isPrimary: true,
338
+ }
339
+ );
340
+ invalidateListingSigningAuthorityCache(listing.listing_id);
341
+ await deleteDidDocForProfile(integrationOwner.profileId);
342
+
343
+ return {
344
+ signingAuthority: { endpoint: DEFAULT_SIGNING_AUTHORITY_ENDPOINT },
345
+ relationship: { name, did, isPrimary: true },
346
+ } as SigningAuthorityForUserType;
347
+ };
348
+
349
+ const getReusableInlineTemplateBoost = async ({
350
+ integrationId,
351
+ templateAlias,
352
+ contentHash,
353
+ templateVersion,
354
+ }: {
355
+ integrationId: string;
356
+ templateAlias: string;
357
+ contentHash: string;
358
+ templateVersion: number;
359
+ }): Promise<BoostInstance | null> => {
360
+ const result = await neogma.queryRunner.run(
361
+ `MATCH (boost:Boost)
362
+ WHERE boost.\`meta.integrationId\` = $integrationId
363
+ AND boost.\`meta.templateAlias\` = $templateAlias
364
+ AND boost.\`meta.inlineTemplateHash\` = $contentHash
365
+ AND boost.\`meta.inlineTemplateVersion\` = $templateVersion
366
+ RETURN boost.id AS id
367
+ ORDER BY boost.id DESC
368
+ LIMIT 1`,
369
+ {
370
+ integrationId,
371
+ templateAlias,
372
+ contentHash,
373
+ templateVersion,
374
+ }
375
+ );
376
+
377
+ const id = result.records[0]?.get('id');
378
+
379
+ if (typeof id !== 'string') {
380
+ return null;
381
+ }
382
+
383
+ return Boost.findOne({ where: { id } });
384
+ };
385
+
386
+ export const upsertInlineTemplateBoostForListing = async ({
387
+ listing,
388
+ integration,
389
+ templateAlias,
390
+ template,
391
+ domain,
392
+ desiredVersion,
393
+ }: {
394
+ listing: AppStoreListingType;
395
+ integration: IntegrationType;
396
+ templateAlias: string;
397
+ template: InlineCredentialTemplate;
398
+ domain: string;
399
+ desiredVersion?: number;
400
+ }): Promise<UpsertInlineTemplateBoostResult> => {
401
+ validateInlineTemplateOrThrow(template);
402
+
403
+ const { credentialTemplateJson, variableManifest, category, walletSkills, contentHash } =
404
+ computeInlineTemplateHash(template);
405
+ const existing = await getBoostForListingByTemplateAlias(
406
+ listing.listing_id,
407
+ templateAlias,
408
+ domain
409
+ );
410
+
411
+ if (existing) {
412
+ const existingMeta = await readInlineTemplateBoostMeta(existing.boost);
413
+ const existingVersion =
414
+ typeof existingMeta.inlineTemplateVersion === 'number'
415
+ ? existingMeta.inlineTemplateVersion
416
+ : 1;
417
+ const targetVersion = desiredVersion ?? existingVersion;
418
+
419
+ if (existingMeta.inlineTemplateHash === contentHash && existingVersion === targetVersion) {
420
+ return {
421
+ boost: existing.boost,
422
+ boostUri: existing.boostUri,
423
+ templateVersion: targetVersion,
424
+ manifest: variableManifest,
425
+ };
426
+ }
427
+
428
+ const reusableBoost = await getReusableInlineTemplateBoost({
429
+ integrationId: integration.id,
430
+ templateAlias,
431
+ contentHash,
432
+ templateVersion: targetVersion,
433
+ });
434
+
435
+ if (reusableBoost) {
436
+ const reusableBoostUri = getBoostUri(reusableBoost.id, domain);
437
+
438
+ await updateBoost(existing.boost, {
439
+ meta: {
440
+ supersededAt: new Date().toISOString(),
441
+ supersededBy: reusableBoostUri,
442
+ },
443
+ });
444
+ await removeBoostFromListing(listing.listing_id, templateAlias);
445
+ await associateBoostWithListing(listing.listing_id, reusableBoostUri, templateAlias);
446
+ invalidateBoostForListingCache(listing.listing_id, templateAlias, domain);
447
+
448
+ return {
449
+ boost: reusableBoost,
450
+ boostUri: reusableBoostUri,
451
+ templateVersion: targetVersion,
452
+ manifest: variableManifest,
453
+ };
454
+ }
455
+
456
+ const nextVersion = desiredVersion ?? existingVersion + 1;
457
+
458
+ const newBoost = await createBoostForListing(
459
+ parseInlineTemplateJson(credentialTemplateJson) as UnsignedVC,
460
+ listing,
461
+ {
462
+ status: 'LIVE',
463
+ category,
464
+ meta: {
465
+ integrationId: integration.id,
466
+ templateAlias,
467
+ inlineTemplateHash: contentHash,
468
+ inlineTemplateVersion: nextVersion,
469
+ inlineVariableManifest: JSON.stringify(variableManifest),
470
+ inlineWalletSkills: walletSkills ? JSON.stringify(walletSkills) : undefined,
471
+ source: INLINE_TEMPLATE_SOURCE,
472
+ },
473
+ },
474
+ domain
475
+ );
476
+ const newBoostUri = getBoostUri(newBoost.id, domain);
477
+
478
+ await updateBoost(existing.boost, {
479
+ meta: {
480
+ supersededAt: new Date().toISOString(),
481
+ supersededBy: newBoostUri,
482
+ },
483
+ });
484
+ await removeBoostFromListing(listing.listing_id, templateAlias);
485
+ await associateBoostWithListing(listing.listing_id, newBoostUri, templateAlias);
486
+ invalidateBoostForListingCache(listing.listing_id, templateAlias, domain);
487
+
488
+ return {
489
+ boost: newBoost,
490
+ boostUri: newBoostUri,
491
+ templateVersion: nextVersion,
492
+ manifest: variableManifest,
493
+ };
494
+ }
495
+
496
+ const initialVersion = desiredVersion ?? 1;
497
+
498
+ const reusableBoost = await getReusableInlineTemplateBoost({
499
+ integrationId: integration.id,
500
+ templateAlias,
501
+ contentHash,
502
+ templateVersion: initialVersion,
503
+ });
504
+
505
+ if (reusableBoost) {
506
+ const reusableBoostUri = getBoostUri(reusableBoost.id, domain);
507
+
508
+ await associateBoostWithListing(listing.listing_id, reusableBoostUri, templateAlias);
509
+ invalidateBoostForListingCache(listing.listing_id, templateAlias, domain);
510
+
511
+ return {
512
+ boost: reusableBoost,
513
+ boostUri: reusableBoostUri,
514
+ templateVersion: initialVersion,
515
+ manifest: variableManifest,
516
+ };
517
+ }
518
+
519
+ const boost = await createBoostForListing(
520
+ parseInlineTemplateJson(credentialTemplateJson) as UnsignedVC,
521
+ listing,
522
+ {
523
+ status: 'LIVE',
524
+ category,
525
+ meta: {
526
+ integrationId: integration.id,
527
+ templateAlias,
528
+ inlineTemplateHash: contentHash,
529
+ inlineTemplateVersion: initialVersion,
530
+ inlineVariableManifest: JSON.stringify(variableManifest),
531
+ inlineWalletSkills: walletSkills ? JSON.stringify(walletSkills) : undefined,
532
+ source: INLINE_TEMPLATE_SOURCE,
533
+ },
534
+ },
535
+ domain
536
+ );
537
+ const boostUri = getBoostUri(boost.id, domain);
538
+
539
+ await associateBoostWithListing(listing.listing_id, boostUri, templateAlias);
540
+ invalidateBoostForListingCache(listing.listing_id, templateAlias, domain);
541
+
542
+ return {
543
+ boost,
544
+ boostUri,
545
+ templateVersion: initialVersion,
546
+ manifest: variableManifest,
547
+ };
548
+ };
549
+
550
+ const parseInlineTemplateJson = (templateJson: string): Record<string, unknown> => {
551
+ try {
552
+ return JSON.parse(templateJson) as Record<string, unknown>;
553
+ } catch (error) {
554
+ const message = error instanceof Error ? error.message : String(error);
555
+ throw new TRPCError({
556
+ code: 'INTERNAL_SERVER_ERROR',
557
+ message: `Compiled inline template produced invalid JSON: ${message}`,
558
+ });
559
+ }
560
+ };
@@ -38,6 +38,10 @@ export type NotificationMessageKey =
38
38
  | 'appListingWithdrawn'
39
39
  | 'appListingApproved'
40
40
  | 'appListingRejected'
41
+ | 'contractRequestReceived'
42
+ | 'contractRequestAccepted'
43
+ | 'contractRequestDenied'
44
+ | 'contractRequestCancelled'
41
45
  | 'consentFlowViewRequest'
42
46
  | 'consentFlowInvite'
43
47
  | 'consentFlowShare'
@@ -149,6 +153,22 @@ const en: LocaleCatalog = {
149
153
  title: 'App Listing Needs Changes',
150
154
  body: '"{displayName}" was not approved. Please review and resubmit.',
151
155
  },
156
+ contractRequestReceived: {
157
+ title: 'Connection request',
158
+ body: '{referrer} would like to connect you with {contractOwner}.',
159
+ },
160
+ contractRequestAccepted: {
161
+ title: 'Connection request accepted',
162
+ body: '{name} accepted the request.',
163
+ },
164
+ contractRequestDenied: {
165
+ title: 'Connection request declined',
166
+ body: '{name} declined the request.',
167
+ },
168
+ contractRequestCancelled: {
169
+ title: 'Connection request cancelled',
170
+ body: '{name} cancelled the request.',
171
+ },
152
172
  consentFlowViewRequest: {
153
173
  title: 'AI Insights',
154
174
  body: '{name} has requested to view your insights.',
@@ -304,6 +324,22 @@ const es: LocaleCatalog = {
304
324
  title: 'La aplicación necesita cambios',
305
325
  body: '"{displayName}" no fue aprobada. Revísala y vuelve a enviarla.',
306
326
  },
327
+ contractRequestReceived: {
328
+ title: 'Solicitud de conexión',
329
+ body: '{referrer} quiere conectarte con {contractOwner}.',
330
+ },
331
+ contractRequestAccepted: {
332
+ title: 'Solicitud de conexión aceptada',
333
+ body: '{name} aceptó la solicitud.',
334
+ },
335
+ contractRequestDenied: {
336
+ title: 'Solicitud de conexión rechazada',
337
+ body: '{name} rechazó la solicitud.',
338
+ },
339
+ contractRequestCancelled: {
340
+ title: 'Solicitud de conexión cancelada',
341
+ body: '{name} canceló la solicitud.',
342
+ },
307
343
  consentFlowViewRequest: {
308
344
  title: 'AI Insights',
309
345
  body: '{name} ha solicitado ver tus análisis.',
@@ -459,6 +495,22 @@ const fr: LocaleCatalog = {
459
495
  title: "L'application nécessite des modifications",
460
496
  body: "« {displayName} » n'a pas été approuvée. Veuillez la réviser et la soumettre à nouveau.",
461
497
  },
498
+ contractRequestReceived: {
499
+ title: 'Demande de connexion',
500
+ body: '{referrer} souhaite vous mettre en relation avec {contractOwner}.',
501
+ },
502
+ contractRequestAccepted: {
503
+ title: 'Demande de connexion acceptée',
504
+ body: '{name} a accepté la demande.',
505
+ },
506
+ contractRequestDenied: {
507
+ title: 'Demande de connexion refusée',
508
+ body: '{name} a refusé la demande.',
509
+ },
510
+ contractRequestCancelled: {
511
+ title: 'Demande de connexion annulée',
512
+ body: '{name} a annulé la demande.',
513
+ },
462
514
  consentFlowViewRequest: {
463
515
  title: 'AI Insights',
464
516
  body: '{name} a demandé à consulter vos analyses.',
@@ -614,6 +666,13 @@ const ar: LocaleCatalog = {
614
666
  title: 'التطبيق يحتاج إلى تعديلات',
615
667
  body: 'لم تتم الموافقة على "{displayName}". يرجى مراجعته وإعادة إرساله.',
616
668
  },
669
+ contractRequestReceived: {
670
+ title: 'طلب اتصال',
671
+ body: 'يريد {referrer} توصيلك بـ {contractOwner}.',
672
+ },
673
+ contractRequestAccepted: { title: 'تم قبول طلب الاتصال', body: 'قبل {name} الطلب.' },
674
+ contractRequestDenied: { title: 'تم رفض طلب الاتصال', body: 'رفض {name} الطلب.' },
675
+ contractRequestCancelled: { title: 'تم إلغاء طلب الاتصال', body: 'ألغى {name} الطلب.' },
617
676
  consentFlowViewRequest: {
618
677
  title: 'رؤى الذكاء الاصطناعي',
619
678
  body: 'طلب {name} الاطلاع على رؤاك.',
@@ -1,14 +1,32 @@
1
- import { LCNNotificationValidator } from '@learncard/types';
1
+ import { authorizeContractNotification } from '@helpers/contract-events.helpers';
2
+ import { ConsentFlowWebhookMetadataValidator, LCNNotificationValidator } from '@learncard/types';
2
3
 
3
4
  import { acknowledgeConnectionPromptNotificationDelivery } from '@helpers/connectionPrompt.helpers';
4
- import { sendNotification } from '@helpers/notifications.helpers';
5
+ import {
6
+ PermanentNotificationDeliveryError,
7
+ sendNotification,
8
+ } from '@helpers/notifications.helpers';
5
9
 
6
10
  export const deliverQueuedNotification = async (body: string): Promise<void> => {
7
11
  const notification = await LCNNotificationValidator.parseAsync(JSON.parse(body));
8
12
 
9
- const stored = await sendNotification(notification, {
10
- propagateDirectWebhookTransportErrors: true,
11
- });
13
+ if (!(await authorizeContractNotification(notification))) return;
14
+
15
+ const isContractEvent = ConsentFlowWebhookMetadataValidator.safeParse(
16
+ notification.data?.metadata
17
+ ).success;
18
+ let stored: boolean | undefined;
19
+ try {
20
+ stored = await sendNotification(notification, {
21
+ propagateDirectWebhookTransportErrors: true,
22
+ throwOnPermanentFailure: isContractEvent,
23
+ });
24
+ } catch (error) {
25
+ // Acknowledge terminal contract rejections so SQS can discard its payload too.
26
+ // Other notification types keep their existing worker behavior.
27
+ if (isContractEvent && error instanceof PermanentNotificationDeliveryError) return;
28
+ throw error;
29
+ }
12
30
 
13
31
  if (!stored) throw new Error('Notification was not durably stored');
14
32