@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
@@ -1,4 +1,5 @@
1
1
  import { getAdminRole, getEmptyRole } from '@accesslayer/role/read';
2
+ import type { Transaction } from 'neo4j-driver';
2
3
  import { CredentialInstance, BoostInstance, AppStoreListingInstance } from '@models';
3
4
  import { neogma } from '@instance';
4
5
  import { ProfileType } from 'types/profile';
@@ -6,9 +7,14 @@ import { clearDidWebCacheForChildProfileManagers } from './update';
6
7
 
7
8
  export const createBoostInstanceOfRelationship = async (
8
9
  credential: CredentialInstance,
9
- boost: BoostInstance
10
+ boost: BoostInstance,
11
+ transaction?: Transaction
10
12
  ): Promise<void> => {
11
- await credential.relateTo({ alias: 'instanceOf', where: { id: boost.id } });
13
+ await credential.relateTo({
14
+ alias: 'instanceOf',
15
+ where: { id: boost.id },
16
+ session: transaction,
17
+ });
12
18
  };
13
19
 
14
20
  export const createReceivedCredentialRelationship = async (
@@ -3,12 +3,16 @@ import { QueryBuilder, BindParam } from 'neogma';
3
3
  import { ConsentFlowContract } from '@models';
4
4
  import { ConsentFlowContract as ConsentFlowContractType } from '@learncard/types';
5
5
  import { v4 as uuid } from 'uuid';
6
- import { getContractById } from './read';
6
+ import { TRPCError } from '@trpc/server';
7
7
  import { DbContractType } from 'types/consentflowcontract';
8
- import { flattenObject } from '@helpers/objects.helpers';
8
+ import { flattenObject, inflateObject } from '@helpers/objects.helpers';
9
9
 
10
10
  export const createConsentFlowContract = async ({
11
11
  contract,
12
+ ownerProfileId,
13
+ recipientIds = [],
14
+ writerIds = [],
15
+ autoboosts = [],
12
16
  name,
13
17
  subtitle = '',
14
18
  description = '',
@@ -20,6 +24,10 @@ export const createConsentFlowContract = async ({
20
24
  expiresAt,
21
25
  }: {
22
26
  contract: ConsentFlowContractType;
27
+ ownerProfileId: string;
28
+ recipientIds?: string[];
29
+ writerIds?: string[];
30
+ autoboosts?: { id: string; signingAuthorityEndpoint: string; signingAuthorityName: string }[];
23
31
  name: string;
24
32
  subtitle?: string;
25
33
  description?: string;
@@ -31,9 +39,35 @@ export const createConsentFlowContract = async ({
31
39
  expiresAt?: string;
32
40
  }): Promise<DbContractType> => {
33
41
  const id = uuid();
42
+ const createdAt = new Date().toISOString();
43
+ const uniqueRecipientIds = [...new Set(recipientIds)].filter(id => id !== ownerProfileId);
44
+ const uniqueWriterIds = [...new Set(writerIds)];
45
+ const autoBoostsById = new Map<string, (typeof autoboosts)[number]>();
46
+ for (const config of autoboosts) {
47
+ const existing = autoBoostsById.get(config.id);
48
+ if (
49
+ existing &&
50
+ (existing.signingAuthorityEndpoint !== config.signingAuthorityEndpoint ||
51
+ existing.signingAuthorityName !== config.signingAuthorityName)
52
+ ) {
53
+ throw new TRPCError({
54
+ code: 'BAD_REQUEST',
55
+ message: `Conflicting signing authorities for autoboost: ${config.id}`,
56
+ });
57
+ }
58
+ autoBoostsById.set(config.id, config);
59
+ }
60
+ // One configured template produces one issuance per consent/update.
61
+ const uniqueAutoBoosts = [...autoBoostsById.values()];
34
62
 
35
63
  const query = new QueryBuilder(
36
64
  new BindParam({
65
+ ownerProfileId,
66
+ recipientIds: uniqueRecipientIds,
67
+ writerIds: uniqueWriterIds,
68
+ boostIds: uniqueAutoBoosts.map(boost => boost.id),
69
+ autoboosts: uniqueAutoBoosts,
70
+ createdAt,
37
71
  params: flattenObject({
38
72
  id,
39
73
  name,
@@ -45,16 +79,55 @@ export const createConsentFlowContract = async ({
45
79
  frontDoorBoostUri,
46
80
  image,
47
81
  contract,
48
- createdAt: new Date().toISOString(),
49
- updatedAt: new Date().toISOString(),
82
+ createdAt,
83
+ updatedAt: createdAt,
84
+ audienceVersion: uniqueRecipientIds.length,
50
85
  expiresAt,
51
86
  }),
52
87
  })
53
88
  )
89
+ // Resolve every target before writing; a missing target produces no writes.
90
+ // One Cypher statement also rolls back the entire configuration on a write failure.
91
+ .raw(
92
+ `
93
+ MATCH (owner:Profile {profileId: $ownerProfileId})
94
+ OPTIONAL MATCH (recipient:Profile) WHERE recipient.profileId IN $recipientIds
95
+ WITH owner, collect(DISTINCT recipient) AS recipients
96
+ WHERE size(recipients) = size($recipientIds)
97
+ OPTIONAL MATCH (writer:Profile) WHERE writer.profileId IN $writerIds
98
+ WITH owner, recipients, collect(DISTINCT writer) AS writers
99
+ WHERE size(writers) = size($writerIds)
100
+ OPTIONAL MATCH (boost:Boost) WHERE boost.id IN $boostIds
101
+ WITH owner, recipients, writers, collect(DISTINCT boost) AS boosts
102
+ WHERE size(boosts) = size($boostIds)
103
+ `
104
+ )
54
105
  .create({ model: ConsentFlowContract, identifier: 'contract' })
55
- .set('contract += $params');
106
+ .set('contract += $params')
107
+ .raw(
108
+ `
109
+ CREATE (contract)-[:CREATED_BY]->(owner)
110
+ FOREACH (recipient IN recipients |
111
+ CREATE (contract)-[:SHARES_DATA_WITH {addedAt: $createdAt}]->(recipient))
112
+ FOREACH (writer IN writers | CREATE (contract)-[:CAN_WRITE]->(writer))
113
+ FOREACH (config IN $autoboosts |
114
+ FOREACH (boost IN [target IN boosts WHERE target.id = config.id] |
115
+ CREATE (contract)-[:AUTO_RECEIVE {
116
+ signingAuthorityEndpoint: config.signingAuthorityEndpoint,
117
+ signingAuthorityName: config.signingAuthorityName,
118
+ issuer: $ownerProfileId
119
+ }]->(boost)))
120
+ `
121
+ )
122
+ .return('contract');
56
123
 
57
- await query.run();
58
-
59
- return (await getContractById(id))!;
124
+ const result = await query.run();
125
+ const created = result.records[0]?.toObject().contract;
126
+ if (!created) {
127
+ throw new TRPCError({
128
+ code: 'BAD_REQUEST',
129
+ message: 'Could not find all contract configuration targets',
130
+ });
131
+ }
132
+ return inflateObject<DbContractType>(created.properties);
60
133
  };
@@ -1,9 +1,11 @@
1
+ import { neogma } from '@instance';
1
2
  import { QueryBuilder, BindParam } from 'neogma';
2
3
 
3
4
  import { getIdFromUri } from '@helpers/uri.helpers';
4
5
  import { ConsentFlowContract, ConsentFlowInstance, Profile } from '@models';
5
6
  import { DbContractType } from 'types/consentflowcontract';
6
7
  import { inflateObject } from '@helpers/objects.helpers';
8
+ import type { ContractRequestFields } from '@learncard/types';
7
9
  import { ProfileType } from 'types/profile';
8
10
 
9
11
  export const getContractById = async (id: string): Promise<DbContractType | null> => {
@@ -37,21 +39,63 @@ export const getConsentFlowContractById = async (
37
39
  return record.toObject().contract;
38
40
  };
39
41
 
40
- export const getRequestedForList = async (id: string) => {
41
- const result = await new QueryBuilder()
42
+ const requestFields = (properties: ContractRequestFields): ContractRequestFields => ({
43
+ ...(properties.requestId ? { requestId: properties.requestId } : {}),
44
+ ...(properties.requestedBy ? { requestedBy: properties.requestedBy } : {}),
45
+ ...(properties.externalReferenceId
46
+ ? { externalReferenceId: properties.externalReferenceId }
47
+ : {}),
48
+ ...(properties.requestedAt ? { requestedAt: properties.requestedAt } : {}),
49
+ ...(properties.message ? { message: properties.message } : {}),
50
+ });
51
+
52
+ /** Referral management is distinct from permission to receive consented data. */
53
+ export const getContractRequestAccess = async (
54
+ contractId: string,
55
+ profileId: string
56
+ ): Promise<{ isOwner: boolean; isManager: boolean; isRecipient: boolean }> => {
57
+ const result = await neogma.queryRunner.run(
58
+ `MATCH (contract:ConsentFlowContract {id:$contractId})
59
+ RETURN EXISTS {
60
+ MATCH (contract)-[:CREATED_BY]->(:Profile {profileId:$profileId})
61
+ } AS isOwner, EXISTS {
62
+ MATCH (contract)-[:CREATED_BY|CAN_WRITE]->(:Profile {profileId:$profileId})
63
+ } AS isManager, EXISTS {
64
+ MATCH (contract)-[:CREATED_BY|SHARES_DATA_WITH]->(:Profile {profileId:$profileId})
65
+ } AS isRecipient`,
66
+ { contractId, profileId }
67
+ );
68
+ const access = result.records[0];
69
+ return {
70
+ isOwner: access?.get('isOwner') === true,
71
+ isManager: access?.get('isManager') === true,
72
+ isRecipient: access?.get('isRecipient') === true,
73
+ };
74
+ };
75
+
76
+ const requestManagerWhere = `EXISTS {
77
+ MATCH (c)-[:CREATED_BY|CAN_WRITE]->(:Profile {profileId:$viewerProfileId})
78
+ }`;
79
+ const requestingRecipientWhere = `r.requestedBy = $viewerProfileId AND EXISTS {
80
+ MATCH (c)-[:SHARES_DATA_WITH]->(:Profile {profileId:$viewerProfileId})
81
+ }`;
82
+
83
+ export const getRequestedForList = async (id: string, viewerProfileId: string) => {
84
+ const query = new QueryBuilder(new BindParam({ viewerProfileId }))
42
85
  .match({
43
86
  model: ConsentFlowContract,
44
87
  identifier: 'c',
45
88
  where: { id },
46
89
  })
47
90
  .match('(c)-[r:REQUESTED_FOR]->(p:Profile)')
48
- .return(['p', 'r'])
49
- .run();
91
+ .where(`${requestManagerWhere} OR (${requestingRecipientWhere})`);
92
+ const result = await query.return(['p', 'r']).run();
50
93
 
51
94
  return result.records.map(rec => {
52
95
  const { p, r } = rec.toObject();
53
96
 
54
97
  return {
98
+ ...requestFields(r.properties),
55
99
  profile: p.properties,
56
100
  status: r.properties?.status ?? null,
57
101
  readStatus: r.properties?.readStatus ?? null,
@@ -82,7 +126,9 @@ export const getSharedInsightsRequestsForTargetProfile = async (
82
126
  { identifier: 'writer', model: Profile },
83
127
  ],
84
128
  })
85
- .where("writer.profileId <> $targetProfileId AND r.status IN ['pending', 'accepted']")
129
+ .where(
130
+ "writer.profileId <> $targetProfileId AND r.requestId IS NULL AND r.status IN ['pending', 'accepted']"
131
+ )
86
132
  .return(['writer', 'r', 'c'])
87
133
  .run();
88
134
 
@@ -129,6 +175,7 @@ export const getRequestedForByStatus = async (
129
175
  const { p, r } = rec.toObject();
130
176
 
131
177
  return {
178
+ ...requestFields(r.properties),
132
179
  profile: p.properties,
133
180
  status: r.properties?.status ?? null,
134
181
  readStatus: r.properties?.readStatus ?? null,
@@ -136,22 +183,29 @@ export const getRequestedForByStatus = async (
136
183
  });
137
184
  };
138
185
 
139
- export const getRequestedForForUser = async (contractId: string, requesterProfileId: string) => {
140
- const result = await new QueryBuilder(new BindParam({ requesterProfileId }))
186
+ export const getRequestedForForUser = async (
187
+ contractId: string,
188
+ requesterProfileId: string,
189
+ viewerProfileId: string
190
+ ) => {
191
+ const query = new QueryBuilder(new BindParam({ requesterProfileId, viewerProfileId }))
141
192
  .match({
142
193
  model: ConsentFlowContract,
143
194
  identifier: 'c',
144
195
  where: { id: contractId },
145
196
  })
146
197
  .match('(c)-[r:REQUESTED_FOR]->(p:Profile)')
147
- .where('p.profileId = $requesterProfileId')
148
- .return(['p', 'r'])
149
- .run();
198
+ .where(
199
+ `p.profileId = $requesterProfileId AND
200
+ (p.profileId = $viewerProfileId OR ${requestManagerWhere} OR (${requestingRecipientWhere}))`
201
+ );
202
+ const result = await query.return(['p', 'r']).run();
150
203
 
151
204
  return result.records.map(rec => {
152
205
  const { p, r } = rec.toObject();
153
206
 
154
207
  return {
208
+ ...requestFields(r.properties),
155
209
  profile: p.properties,
156
210
  status: r.properties?.status ?? null,
157
211
  readStatus: r.properties?.readStatus ?? null,
@@ -175,9 +229,29 @@ export const getAllRequestsForTargetProfile = async (targetProfileId: string) =>
175
229
 
176
230
  return {
177
231
  contract: c.properties,
232
+ ...requestFields(r.properties),
178
233
  profile: p.properties,
179
234
  status: r.properties?.status ?? null,
180
235
  readStatus: r.properties?.readStatus ?? null,
181
236
  };
182
237
  });
183
238
  };
239
+
240
+ export type StoredContractRequest = ContractRequestFields & {
241
+ status?: 'pending' | 'accepted' | 'denied' | 'cancelled' | null;
242
+ readStatus?: 'unseen' | 'seen' | null;
243
+ };
244
+
245
+ export const getStoredContractRequest = async (
246
+ contractId: string,
247
+ targetProfileId: string
248
+ ): Promise<StoredContractRequest | null> => {
249
+ const result = await neogma.queryRunner.run(
250
+ `
251
+ MATCH (:ConsentFlowContract {id: $contractId})-[request:REQUESTED_FOR]->(:Profile {profileId: $targetProfileId})
252
+ RETURN request
253
+ `,
254
+ { contractId, targetProfileId }
255
+ );
256
+ return result.records[0]?.get('request').properties ?? null;
257
+ };
@@ -1,8 +1,9 @@
1
+ import { termsReferralSnapshotCypher } from '@helpers/consent-referral.helpers';
2
+ import { appendConsentEvent, tryDispatchContractEvent } from '@helpers/contract-events.helpers';
1
3
  import {
2
4
  ConsentFlowTerms as ConsentFlowTermsType,
3
5
  ConsentFlowTransaction,
4
6
  ConsentFlowGuardianApproval,
5
- LCNNotificationTypeEnumValidator,
6
7
  LCNProfile,
7
8
  UnsignedVC,
8
9
  VC,
@@ -19,10 +20,7 @@ import { DbContractType, FlatDbTermsType } from 'types/consentflowcontract';
19
20
  import { BoostType } from 'types/boost';
20
21
  import { flattenObject, inflateObject } from '@helpers/objects.helpers';
21
22
  import { convertQueryResultToPropertiesObjectArray } from '@helpers/neo4j.helpers';
22
- import { reconsentTerms, upsertRequestedForRelationship } from './update';
23
- import { addNotificationToQueue } from '@helpers/notifications.helpers';
24
- import { getNotificationMessage } from '@helpers/notificationMessages';
25
- import { resolveRecipientLocale } from '@helpers/getRecipientLocale.helpers';
23
+ import { reconsentTerms } from './update';
26
24
  import { getBoostUri, sendBoost } from '@helpers/boost.helpers';
27
25
  import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
28
26
  import { getDidWeb } from '@helpers/did.helpers';
@@ -31,7 +29,13 @@ import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.h
31
29
  import { getProfileByProfileId, getProfilesByProfileIds } from '@accesslayer/profile/read';
32
30
  import { cloneDeep } from 'lodash';
33
31
  import { injectObv3AlignmentsIntoCredentialForBoost } from '@services/skills-provider/inject';
34
- import { constructUri } from '@helpers/uri.helpers';
32
+ import {
33
+ lockContractAudience,
34
+ audienceVersionWhere,
35
+ consentMutationWhere,
36
+ assertAudienceMutation,
37
+ runAudienceMutation,
38
+ } from './recipients';
35
39
 
36
40
  export const setCreatorForContract = async (contract: DbContractType, profile: LCNProfile) => {
37
41
  return ConsentFlowContract.relateTo({
@@ -78,12 +82,18 @@ export const consentToContract = async (
78
82
  expiresAt,
79
83
  oneTime,
80
84
  guardianApproval,
85
+ audienceVersion,
86
+ expectedRequestId,
87
+ smartResumeFingerprint,
81
88
  }: {
82
89
  terms: ConsentFlowTermsType;
83
90
  expiresAt?: string;
84
91
  liveSyncing?: boolean;
85
92
  oneTime?: boolean;
86
93
  guardianApproval?: ConsentFlowGuardianApproval;
94
+ audienceVersion?: number;
95
+ expectedRequestId?: string;
96
+ smartResumeFingerprint?: string;
87
97
  },
88
98
  domain: string
89
99
  ) => {
@@ -125,7 +135,15 @@ export const consentToContract = async (
125
135
  if (existing.length > 0) {
126
136
  return reconsentTerms(
127
137
  { terms: inflateObject(existing[0]!.terms), consenter, contract, contractOwner },
128
- { terms, expiresAt, oneTime, guardianApproval },
138
+ {
139
+ terms,
140
+ expiresAt,
141
+ oneTime,
142
+ guardianApproval,
143
+ audienceVersion,
144
+ smartResumeFingerprint,
145
+ expectedRequestId,
146
+ },
129
147
  domain
130
148
  );
131
149
  }
@@ -140,55 +158,95 @@ export const consentToContract = async (
140
158
 
141
159
  const termsId = uuid();
142
160
 
143
- const result = await new QueryBuilder(
144
- new BindParam({
145
- params: flattenObject({ terms, ...(guardianApproval ? { guardianApproval } : {}) }),
146
- })
147
- )
148
- .match({
149
- multiple: [
150
- {
151
- model: Profile,
152
- where: { profileId: consenter.profileId },
153
- identifier: 'profile',
154
- },
155
- { model: ConsentFlowContract, where: { id: contract.id }, identifier: 'contract' },
156
- ],
157
- })
158
- .create({
159
- related: [
160
- { identifier: 'profile' },
161
- ConsentFlowTerms.getRelationshipByAlias('createdBy'),
162
- {
163
- model: ConsentFlowTerms,
164
- properties: {
165
- id: termsId,
166
- status: oneTime ? 'stale' : 'live',
167
- createdAt: transaction.date,
168
- updatedAt: transaction.date,
169
- ...(expiresAt ? { expiresAt } : {}),
170
- ...(oneTime ? { oneTime } : {}),
171
- },
172
- identifier: 'terms',
173
- },
174
- ConsentFlowTerms.getRelationshipByAlias('consentsTo'),
175
- { identifier: 'contract' },
176
- ],
177
- })
178
- .create({
179
- related: [
180
- {
181
- identifier: 'transaction',
182
- model: ConsentFlowTransactionModel,
183
- properties: transaction,
184
- },
185
- ConsentFlowTransactionModel.getRelationshipByAlias('isFor'),
186
- { identifier: 'terms' },
187
- ],
188
- })
189
- .set('terms += $params')
190
- .set('transaction += $params')
191
- .run();
161
+ const result = await runAudienceMutation(
162
+ appendConsentEvent(
163
+ lockContractAudience(
164
+ new QueryBuilder(
165
+ new BindParam({
166
+ params: flattenObject({
167
+ terms,
168
+ ...(smartResumeFingerprint
169
+ ? {
170
+ smartResumeFingerprint,
171
+ smartResumePublicationStatus: 'pending',
172
+ smartResumeMutationVersion: 0,
173
+ }
174
+ : {}),
175
+ ...(guardianApproval ? { guardianApproval } : {}),
176
+ }),
177
+ audienceVersion: audienceVersion ?? null,
178
+ expectedRequestId: expectedRequestId ?? null,
179
+ consenterProfileId: consenter.profileId,
180
+ })
181
+ ),
182
+ contract.id
183
+ )
184
+ .where(`${audienceVersionWhere} AND ${consentMutationWhere}`)
185
+ .set('contract.hasConsented = true')
186
+ .with('contract')
187
+ .match({
188
+ multiple: [
189
+ {
190
+ model: Profile,
191
+ where: { profileId: consenter.profileId },
192
+ identifier: 'profile',
193
+ },
194
+ {
195
+ model: ConsentFlowContract,
196
+ where: { id: contract.id },
197
+ identifier: 'contract',
198
+ },
199
+ ],
200
+ })
201
+ .where(
202
+ 'NOT EXISTS { MATCH (profile)-[:CREATED_BY]->(:ConsentFlowTerms)-[:CONSENTS_TO]->(contract) }'
203
+ )
204
+ .create({
205
+ related: [
206
+ { identifier: 'profile' },
207
+ ConsentFlowTerms.getRelationshipByAlias('createdBy'),
208
+ {
209
+ model: ConsentFlowTerms,
210
+ properties: {
211
+ id: termsId,
212
+ status: oneTime ? 'stale' : 'live',
213
+ createdAt: transaction.date,
214
+ updatedAt: transaction.date,
215
+ ...(expiresAt ? { expiresAt } : {}),
216
+ ...(oneTime ? { oneTime } : {}),
217
+ },
218
+ identifier: 'terms',
219
+ },
220
+ ConsentFlowTerms.getRelationshipByAlias('consentsTo'),
221
+ { identifier: 'contract' },
222
+ ],
223
+ })
224
+ .create({
225
+ related: [
226
+ {
227
+ identifier: 'transaction',
228
+ model: ConsentFlowTransactionModel,
229
+ properties: transaction,
230
+ },
231
+ ConsentFlowTransactionModel.getRelationshipByAlias('isFor'),
232
+ { identifier: 'terms' },
233
+ ],
234
+ })
235
+ .set('terms += $params')
236
+ .set('transaction += $params')
237
+ .with('terms, contract, transaction'),
238
+ {
239
+ consenterProfileId: consenter.profileId,
240
+ transaction,
241
+ domain,
242
+ messageKey:
243
+ contract.name === 'AI Insights'
244
+ ? 'consentFlowInsightsShared'
245
+ : 'consentFlowTransactionCreated',
246
+ }
247
+ ).return('terms.id AS id')
248
+ );
249
+ assertAudienceMutation(result.records.length);
192
250
 
193
251
  const autoBoosts = await ConsentFlowContract.findRelationships({
194
252
  alias: 'autoReceive',
@@ -288,6 +346,7 @@ export const consentToContract = async (
288
346
  { identifier: 'terms' },
289
347
  ],
290
348
  })
349
+ .raw(termsReferralSnapshotCypher('boostTransaction'))
291
350
  .return('terms')
292
351
  .run();
293
352
 
@@ -311,43 +370,7 @@ export const consentToContract = async (
311
370
  );
312
371
  }
313
372
 
314
- // TODO: Improve notification handling for consent flow contracts
315
- // TODO: Replace hardcoded string matching with contract type identification
316
- // This should check a contract.type field instead of contract.name
317
- const isAiInsights = contract.name === 'AI Insights';
318
-
319
- const notificationMessage = getNotificationMessage(
320
- isAiInsights ? 'consentFlowInsightsShared' : 'consentFlowTransactionCreated',
321
- resolveRecipientLocale(contractOwner),
322
- { consenter: consenter.displayName, contractName: contract.name }
323
- );
324
-
325
- const notificationMetadata = isAiInsights
326
- ? {
327
- type: 'AI Insight',
328
- contractId: contract?.id,
329
- contractUri: constructUri('contract', contract.id, domain),
330
- }
331
- : {};
332
-
333
- await addNotificationToQueue({
334
- type: LCNNotificationTypeEnumValidator.enum.CONSENT_FLOW_TRANSACTION,
335
- from: consenter,
336
- to: contractOwner,
337
- message: notificationMessage,
338
- data: { transaction, metadata: notificationMetadata },
339
- });
340
-
341
- try {
342
- await upsertRequestedForRelationship(
343
- contract.id,
344
- consenter.profileId,
345
- 'accepted',
346
- 'unseen'
347
- );
348
- } catch {
349
- console.log('Unable to update contract request status');
350
- }
373
+ await tryDispatchContractEvent(transaction.id);
351
374
 
352
375
  return result.summary.counters.containsUpdates();
353
376
  };
@@ -21,6 +21,7 @@ export const removeRequestedForRelationship = async (
21
21
  identifier: 'profile',
22
22
  })
23
23
  .match({ optional: true, literal: '(contract)-[r:REQUESTED_FOR]->(profile)' })
24
+ .where('r.requestId IS NULL')
24
25
  .with('r')
25
26
  .delete('r')
26
27
  .return('r')