@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,185 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { canonicalConsentScopeString, canonicalJsonString } from '@learncard/partner-connect-core';
4
+ import type { NormalizedConsentScopes } from '@learncard/partner-connect-core';
5
+ import type { AppManifest, AppManifestDiff, AppManifestTemplateRef } from '@learncard/types';
6
+
7
+ const sortStrings = <T extends string>(values: T[]): T[] =>
8
+ Array.from(new Set(values)).sort() as T[];
9
+
10
+ const normalizeTemplateRecord = (template: AppManifest['templates'][number]) => ({
11
+ alias: template.alias,
12
+ template: template.template,
13
+ version: template.version,
14
+ });
15
+
16
+ const normalizeConsentRecord = (
17
+ record: AppManifest['consentRequests'][number]
18
+ ): { scopes: NormalizedConsentScopes; reason?: string } => ({
19
+ scopes: {
20
+ read: {
21
+ credentialCategories: sortStrings(record.scopes.read.credentialCategories),
22
+ personalFields: sortStrings(record.scopes.read.personalFields),
23
+ },
24
+ write: {
25
+ credentialCategories: sortStrings(record.scopes.write.credentialCategories),
26
+ },
27
+ },
28
+ ...(record.reason ? { reason: record.reason } : {}),
29
+ });
30
+
31
+ const normalizeManifestForHash = (manifest: AppManifest): Record<string, unknown> => ({
32
+ manifestVersion: manifest.manifestVersion,
33
+ appUrl: manifest.appUrl,
34
+ ...(manifest.suggestedName ? { suggestedName: manifest.suggestedName } : {}),
35
+ ...(manifest.suggestedIconUrl ? { suggestedIconUrl: manifest.suggestedIconUrl } : {}),
36
+ permissions: sortStrings(manifest.permissions),
37
+ templates: [...manifest.templates]
38
+ .map(normalizeTemplateRecord)
39
+ .sort((a, b) => a.alias.localeCompare(b.alias) || a.version - b.version),
40
+ consentRequests: [...manifest.consentRequests]
41
+ .map(normalizeConsentRecord)
42
+ .sort((a, b) =>
43
+ canonicalConsentScopeString(a.scopes).localeCompare(
44
+ canonicalConsentScopeString(b.scopes)
45
+ )
46
+ ),
47
+ featuresLaunched: sortStrings(manifest.featuresLaunched),
48
+ counterKeys: sortStrings(manifest.counterKeys),
49
+ usedLearnerContext: manifest.usedLearnerContext,
50
+ usedNotifications: manifest.usedNotifications,
51
+ });
52
+
53
+ const buildStringDiff = (current: string[], next: string[]) => {
54
+ const currentSet = new Set(current);
55
+ const nextSet = new Set(next);
56
+
57
+ return {
58
+ added: sortStrings(next.filter(value => !currentSet.has(value))),
59
+ removed: sortStrings(current.filter(value => !nextSet.has(value))),
60
+ changed: [] as string[],
61
+ };
62
+ };
63
+
64
+ const templateSignature = (template: AppManifest['templates'][number]): string => {
65
+ return canonicalJsonString({ template: template.template, version: template.version });
66
+ };
67
+
68
+ const toTemplateRef = (template: AppManifest['templates'][number]): AppManifestTemplateRef => ({
69
+ alias: template.alias,
70
+ version: template.version,
71
+ });
72
+
73
+ const getConsentExpansionSurface = (
74
+ manifest: AppManifest
75
+ ): {
76
+ readCredentialCategories: Set<string>;
77
+ readPersonalFields: Set<string>;
78
+ writeCredentialCategories: Set<string>;
79
+ } => {
80
+ const readCredentialCategories = new Set<string>();
81
+ const readPersonalFields = new Set<string>();
82
+ const writeCredentialCategories = new Set<string>();
83
+
84
+ for (const request of manifest.consentRequests) {
85
+ request.scopes.read.credentialCategories.forEach(category =>
86
+ readCredentialCategories.add(category)
87
+ );
88
+ request.scopes.read.personalFields.forEach(field => readPersonalFields.add(field));
89
+ request.scopes.write.credentialCategories.forEach(category =>
90
+ writeCredentialCategories.add(category)
91
+ );
92
+ }
93
+
94
+ return {
95
+ readCredentialCategories,
96
+ readPersonalFields,
97
+ writeCredentialCategories,
98
+ };
99
+ };
100
+
101
+ const isConsentExpanded = (current: AppManifest, next: AppManifest): boolean => {
102
+ const currentSurface = getConsentExpansionSurface(current);
103
+ const nextSurface = getConsentExpansionSurface(next);
104
+
105
+ return (
106
+ [...nextSurface.readCredentialCategories].some(
107
+ category => !currentSurface.readCredentialCategories.has(category)
108
+ ) ||
109
+ [...nextSurface.readPersonalFields].some(
110
+ field => !currentSurface.readPersonalFields.has(field)
111
+ ) ||
112
+ [...nextSurface.writeCredentialCategories].some(
113
+ category => !currentSurface.writeCredentialCategories.has(category)
114
+ )
115
+ );
116
+ };
117
+
118
+ export const canonicalizeManifest = (manifest: AppManifest): string => {
119
+ return canonicalJsonString(normalizeManifestForHash(manifest));
120
+ };
121
+
122
+ export const hashManifest = (manifest: AppManifest): string => {
123
+ return createHash('sha256').update(canonicalizeManifest(manifest)).digest('hex');
124
+ };
125
+
126
+ export const diffManifests = (current: AppManifest, next: AppManifest): AppManifestDiff => {
127
+ const currentTemplates = new Map(current.templates.map(template => [template.alias, template]));
128
+ const nextTemplates = new Map(next.templates.map(template => [template.alias, template]));
129
+
130
+ const addedTemplates: AppManifestTemplateRef[] = [];
131
+ const removedTemplates: AppManifestTemplateRef[] = [];
132
+ const changedTemplates: AppManifestDiff['templates']['changed'] = [];
133
+
134
+ for (const [alias, template] of nextTemplates.entries()) {
135
+ const existing = currentTemplates.get(alias);
136
+
137
+ if (!existing) {
138
+ addedTemplates.push(toTemplateRef(template));
139
+ continue;
140
+ }
141
+
142
+ if (templateSignature(existing) !== templateSignature(template)) {
143
+ changedTemplates.push({
144
+ alias,
145
+ fromVersion: existing.version,
146
+ toVersion: template.version,
147
+ });
148
+ }
149
+ }
150
+
151
+ for (const [alias, template] of currentTemplates.entries()) {
152
+ if (!nextTemplates.has(alias)) {
153
+ removedTemplates.push(toTemplateRef(template));
154
+ }
155
+ }
156
+
157
+ const permissionDiff = buildStringDiff(current.permissions, next.permissions);
158
+ const consentScopeDiff = buildStringDiff(
159
+ current.consentRequests.map(record =>
160
+ canonicalConsentScopeString(normalizeConsentRecord(record).scopes)
161
+ ),
162
+ next.consentRequests.map(record =>
163
+ canonicalConsentScopeString(normalizeConsentRecord(record).scopes)
164
+ )
165
+ );
166
+
167
+ return {
168
+ permissions: permissionDiff,
169
+ templates: {
170
+ added: addedTemplates.sort(
171
+ (a, b) => a.alias.localeCompare(b.alias) || a.version - b.version
172
+ ),
173
+ removed: removedTemplates.sort(
174
+ (a, b) => a.alias.localeCompare(b.alias) || a.version - b.version
175
+ ),
176
+ changed: changedTemplates.sort(
177
+ (a, b) => a.alias.localeCompare(b.alias) || a.toVersion - b.toVersion
178
+ ),
179
+ },
180
+ consentScopes: consentScopeDiff,
181
+ featurePaths: buildStringDiff(current.featuresLaunched, next.featuresLaunched),
182
+ counterKeys: buildStringDiff(current.counterKeys, next.counterKeys),
183
+ requiresReview: permissionDiff.added.length > 0 || isConsentExpanded(current, next),
184
+ };
185
+ };
@@ -1,4 +1,5 @@
1
1
  import { QueryBuilder } from 'neogma';
2
+ import type { Transaction } from 'neo4j-driver';
2
3
 
3
4
  import { CredentialInstance, Credential, Boost, Profile, ClaimHook, Role } from '@models';
4
5
  import { ProfileType } from 'types/profile';
@@ -8,7 +9,8 @@ import { getAdminRole } from '@accesslayer/role/read';
8
9
 
9
10
  const processPermissionsClaimHooks = async (
10
11
  profile: ProfileType,
11
- credential: CredentialInstance
12
+ credential: CredentialInstance,
13
+ transaction?: Transaction
12
14
  ): Promise<void> => {
13
15
  await new QueryBuilder()
14
16
  .match({
@@ -43,12 +45,13 @@ const processPermissionsClaimHooks = async (
43
45
  { identifier: 'targetBoost' },
44
46
  ],
45
47
  })
46
- .run();
48
+ .run(transaction);
47
49
  };
48
50
 
49
51
  const processAutoConnectClaimHooks = async (
50
52
  profile: ProfileType,
51
- credential: CredentialInstance
53
+ credential: CredentialInstance,
54
+ transaction?: Transaction
52
55
  ): Promise<void> => {
53
56
  // For any AUTO_CONNECT claim hooks attached to the boost this credential is an instance of,
54
57
  // create an explicit AUTO_CONNECT_RECIPIENT relationship from the target boost to the claiming profile.
@@ -76,12 +79,13 @@ const processAutoConnectClaimHooks = async (
76
79
  Boost.getRelationshipByAlias('autoConnectRecipient').name
77
80
  }]->(profile)`
78
81
  )
79
- .run();
82
+ .run(transaction);
80
83
  };
81
84
 
82
85
  const processAdminClaimHooks = async (
83
86
  profile: ProfileType,
84
- credential: CredentialInstance
87
+ credential: CredentialInstance,
88
+ transaction?: Transaction
85
89
  ): Promise<void> => {
86
90
  const adminRole = await getAdminRole();
87
91
  await new QueryBuilder()
@@ -115,13 +119,22 @@ const processAdminClaimHooks = async (
115
119
  { identifier: 'targetBoost' },
116
120
  ],
117
121
  })
118
- .run();
122
+ .run(transaction);
119
123
  };
120
124
 
121
125
  export const processClaimHooks = async (
122
126
  profile: ProfileType,
123
- credential: CredentialInstance
127
+ credential: CredentialInstance,
128
+ transaction?: Transaction
124
129
  ): Promise<void> => {
130
+ if (transaction) {
131
+ // Keep graph effects in the caller's transaction; cache invalidation follows commit.
132
+ await processPermissionsClaimHooks(profile, credential, transaction);
133
+ await processAdminClaimHooks(profile, credential, transaction);
134
+ await processAutoConnectClaimHooks(profile, credential, transaction);
135
+ return;
136
+ }
137
+
125
138
  await Promise.all([
126
139
  processPermissionsClaimHooks(profile, credential),
127
140
  processAdminClaimHooks(profile, credential),
@@ -0,0 +1,6 @@
1
+ /** Internal aliases only: capture durable attribution on issuance/maintenance transactions. */
2
+ export const termsReferralSnapshotCypher = (transactionAlias = 'transaction'): string => `
3
+ SET ${transactionAlias}.\`referral.requestId\` = terms.\`referral.requestId\`,
4
+ ${transactionAlias}.\`referral.requestedBy\` = terms.\`referral.requestedBy\`,
5
+ ${transactionAlias}.\`referral.externalReferenceId\` = terms.\`referral.externalReferenceId\`
6
+ `;
@@ -0,0 +1,58 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import {
4
+ canonicalConsentScopeString,
5
+ normalizeConsentRequest,
6
+ } from '@learncard/partner-connect-core';
7
+ import type { ConsentRequest, NormalizedConsentScopes } from '@learncard/partner-connect-core';
8
+ import type { ConsentFlowContract } from '@learncard/types';
9
+
10
+ const CONSENT_CONTRACT_NAME_MAX_LENGTH = 100;
11
+
12
+ const toToggle = (): { required: false; defaultEnabled: true } => ({
13
+ required: false,
14
+ defaultEnabled: true,
15
+ });
16
+
17
+ export const normalizeConsentScopes = (request: ConsentRequest): NormalizedConsentScopes => {
18
+ return normalizeConsentRequest(request);
19
+ };
20
+
21
+ export const computeConsentScopeHash = (scopes: NormalizedConsentScopes): string => {
22
+ return createHash('sha256').update(canonicalConsentScopeString(scopes)).digest('hex');
23
+ };
24
+
25
+ export const buildConsentFlowContractFromScopes = (
26
+ scopes: NormalizedConsentScopes
27
+ ): ConsentFlowContract => {
28
+ return {
29
+ read: {
30
+ credentials: {
31
+ categories: Object.fromEntries(
32
+ scopes.read.credentialCategories.map(category => [category, toToggle()])
33
+ ),
34
+ },
35
+ personal: Object.fromEntries(
36
+ scopes.read.personalFields.map(field => [field, toToggle()])
37
+ ),
38
+ },
39
+ write: {
40
+ credentials: {
41
+ categories: Object.fromEntries(
42
+ scopes.write.credentialCategories.map(category => [category, toToggle()])
43
+ ),
44
+ },
45
+ personal: {},
46
+ },
47
+ };
48
+ };
49
+
50
+ export const buildScopedConsentContractName = (listingDisplayName: string): string => {
51
+ const candidate = `${listingDisplayName} — Data Sharing`;
52
+
53
+ if (candidate.length <= CONSENT_CONTRACT_NAME_MAX_LENGTH) {
54
+ return candidate;
55
+ }
56
+
57
+ return candidate.slice(0, CONSENT_CONTRACT_NAME_MAX_LENGTH).trimEnd();
58
+ };
@@ -0,0 +1,43 @@
1
+ import { neogma } from '@instance';
2
+
3
+ export const contractEventMaintenanceSchema = [
4
+ 'CREATE CONSTRAINT consent_event_migration_id IF NOT EXISTS FOR (m:ConsentFlowEventMigration) REQUIRE m.id IS UNIQUE',
5
+ 'CREATE INDEX consent_event_cleanup IF NOT EXISTS FOR (e:ConsentFlowEvent) ON (e.cleanupPending)',
6
+ 'CREATE INDEX consent_delivery_due IF NOT EXISTS FOR (d:ConsentFlowEventDelivery) ON (d.state, d.nextAttemptAt)',
7
+ 'CREATE INDEX consent_delivery_age IF NOT EXISTS FOR (d:ConsentFlowEventDelivery) ON (d.state, d.eventCreatedAt)',
8
+ 'CREATE INDEX consent_delivery_attempts IF NOT EXISTS FOR (d:ConsentFlowEventDelivery) ON (d.state, d.attempts)',
9
+ ];
10
+
11
+ let schemaReady: Promise<void> | undefined;
12
+
13
+ /** One-time backfill for pre-indexed outbox records; the unique marker serializes workers. */
14
+ export const ensureContractEventMaintenance = async (): Promise<void> => {
15
+ schemaReady ??= (async (): Promise<void> => {
16
+ for (const query of contractEventMaintenanceSchema) await neogma.queryRunner.run(query);
17
+ })().catch(error => {
18
+ schemaReady = undefined;
19
+ throw error;
20
+ });
21
+ await schemaReady;
22
+ await neogma.queryRunner.run(`
23
+ MERGE (migration:ConsentFlowEventMigration {id: 'indexed-outbox-v1'})
24
+ SET migration.lock = coalesce(migration.lock, 0) + 1
25
+ WITH migration WHERE migration.completedAt IS NULL
26
+ CALL {
27
+ WITH migration
28
+ MATCH (event:ConsentFlowEvent)
29
+ WHERE event.payload IS NOT NULL OR event.message IS NOT NULL
30
+ SET event.cleanupPending = true
31
+ RETURN count(event) AS marked
32
+ }
33
+ CALL {
34
+ WITH migration
35
+ MATCH (event:ConsentFlowEvent)-[:HAS_DELIVERY]->(delivery:ConsentFlowEventDelivery)
36
+ WHERE delivery.state = 'pending'
37
+ SET delivery.eventCreatedAt = event.createdAt,
38
+ delivery.attempts = coalesce(delivery.attempts, 0)
39
+ RETURN count(delivery) AS updated
40
+ }
41
+ SET migration.completedAt = toString(datetime())
42
+ `);
43
+ };