@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,19 +1,12 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import type { JWE, VC } from '@learncard/types';
2
+ import type { JWE } from '@learncard/types';
3
3
  import type { IssuedCredential } from '../../types/credential';
4
4
 
5
- const mocks = vi.hoisted(() => ({ createOne: vi.fn(async input => input) }));
5
+ const mocks = vi.hoisted(() => ({ createOne: vi.fn() }));
6
6
  vi.mock('@models', () => ({ Credential: { createOne: mocks.createOne } }));
7
7
 
8
8
  import { storeCredential } from './create';
9
9
 
10
- const statusEntry = {
11
- id: 'https://network.example/status/1#42',
12
- type: 'BitstringStatusListEntry' as const,
13
- statusPurpose: 'revocation' as const,
14
- statusListIndex: '42',
15
- statusListCredential: 'https://network.example/status/1',
16
- };
17
10
  const createJwe = (): JWE => ({
18
11
  protected: 'header',
19
12
  iv: 'iv',
@@ -21,121 +14,15 @@ const createJwe = (): JWE => ({
21
14
  tag: 'tag',
22
15
  recipients: [],
23
16
  });
24
- const createVc = (): VC => ({
25
- '@context': ['https://www.w3.org/2018/credentials/v1'],
26
- type: ['VerifiableCredential'],
27
- issuer: 'did:example:issuer',
28
- issuanceDate: '2026-01-01T00:00:00Z',
29
- credentialSubject: { id: 'did:example:subject' },
30
- proof: {
31
- type: 'Ed25519Signature2020',
32
- created: '2026-01-01T00:00:00Z',
33
- proofPurpose: 'assertionMethod',
34
- verificationMethod: 'did:example:issuer#key-1',
35
- proofValue: 'existing-signature',
36
- },
37
- });
38
17
 
39
18
  describe('credential status storage', () => {
40
19
  beforeEach(() => {
41
- vi.restoreAllMocks();
42
20
  vi.clearAllMocks();
43
21
  });
44
22
 
45
- it('persists public status coordinates after serializing an issuance result', async () => {
46
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
47
- const jwe = createJwe();
48
- const issued: IssuedCredential = {
49
- kind: 'issued-credential',
50
- credential: jwe,
51
- statusEntries: [statusEntry],
52
- };
53
- await storeCredential(JSON.parse(JSON.stringify(issued)));
54
- expect(mocks.createOne).toHaveBeenCalledWith(
55
- expect.objectContaining({
56
- credential: JSON.stringify(jwe),
57
- statusEntries: JSON.stringify([statusEntry]),
58
- })
59
- );
60
- expect(warn).not.toHaveBeenCalled();
61
- });
62
-
63
- it('warns when a client-encrypted JWE has no metadata, without logging its payload', async () => {
64
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
65
- const original = createJwe();
66
- const jwe = { ...original };
67
- await storeCredential(jwe);
68
- expect(warn).toHaveBeenCalledExactlyOnceWith(
69
- expect.stringContaining('Encrypted credential has no status metadata'),
70
- { credentialId: expect.any(String) }
71
- );
72
- expect(mocks.createOne.mock.calls[0]![0].statusEntries).toBeUndefined();
73
- expect(mocks.createOne.mock.calls[0]![0].credential).toBe(JSON.stringify(jwe));
74
- });
75
-
76
23
  it('rejects internal issuance results that lost their status metadata before writing', async () => {
77
24
  const issued = { kind: 'issued-credential', credential: createJwe() } as IssuedCredential;
78
25
  await expect(storeCredential(issued)).rejects.toThrow();
79
26
  expect(mocks.createOne).not.toHaveBeenCalled();
80
27
  });
81
-
82
- it.each([undefined, []])(
83
- 'warns for a pre-signed plaintext VC with missing or empty status (%j) and preserves its signature',
84
- async credentialStatus => {
85
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
86
- const credential = { ...createVc(), credentialStatus };
87
- const stored = await storeCredential(credential);
88
- expect(warn).toHaveBeenCalledExactlyOnceWith(
89
- expect.stringContaining('Plaintext credential has no status metadata'),
90
- { credentialId: stored.id }
91
- );
92
- expect(stored.credential).toBe(JSON.stringify(credential));
93
- expect(stored.statusEntries).toBeUndefined();
94
- }
95
- );
96
-
97
- it.each([statusEntry, [statusEntry]])(
98
- 'does not warn for a plaintext VC with embedded status entries (%j)',
99
- async credentialStatus => {
100
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
101
- const credential = { ...createVc(), credentialStatus };
102
- const stored = await storeCredential(credential);
103
- expect(warn).not.toHaveBeenCalled();
104
- expect(stored.credential).toBe(JSON.stringify(credential));
105
- }
106
- );
107
-
108
- it.each([
109
- ['Encrypted', createJwe()],
110
- ['Plaintext', createVc()],
111
- ] as const)(
112
- 'warns for %s VC v1 issuance with empty status metadata',
113
- async (kind, credential) => {
114
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
115
- const stored = await storeCredential({
116
- kind: 'issued-credential',
117
- credential,
118
- statusEntries: [],
119
- });
120
- expect(warn).toHaveBeenCalledExactlyOnceWith(
121
- expect.stringContaining(`${kind} credential has no status metadata`),
122
- { credentialId: stored.id }
123
- );
124
- expect(stored.credential).toBe(JSON.stringify(credential));
125
- expect(stored.statusEntries).toBeUndefined();
126
- }
127
- );
128
-
129
- it('does not treat a wire JWE with extra envelope-like fields as trusted issuance metadata', async () => {
130
- vi.spyOn(console, 'warn').mockImplementation(() => {});
131
- const jwe = {
132
- ...createJwe(),
133
- kind: 'issued-credential',
134
- credential: createJwe(),
135
- statusEntries: [statusEntry],
136
- };
137
- await storeCredential(jwe);
138
- expect(mocks.createOne.mock.calls[0]![0].statusEntries).toBeUndefined();
139
- expect(mocks.createOne.mock.calls[0]![0].credential).toBe(JSON.stringify(jwe));
140
- });
141
28
  });
@@ -1,4 +1,5 @@
1
1
  import { BitstringStatusListEntryValidator, UnsignedVC, VC, JWE } from '@learncard/types';
2
+ import type { Transaction } from 'neo4j-driver';
2
3
  import { v4 as uuid } from 'uuid';
3
4
  import { getBitstringStatusListEntries, isEncrypted } from '@learncard/helpers';
4
5
  import { isIssuedCredential } from '@helpers/issuedCredentialStatus.helpers';
@@ -7,7 +8,8 @@ import type { IssuedCredential } from 'types/credential';
7
8
  import { Credential, CredentialInstance } from '@models';
8
9
 
9
10
  export const storeCredential = async (
10
- input: UnsignedVC | VC | JWE | IssuedCredential
11
+ input: UnsignedVC | VC | JWE | IssuedCredential,
12
+ transaction?: Transaction
11
13
  ): Promise<CredentialInstance> => {
12
14
  const id = uuid();
13
15
 
@@ -28,9 +30,12 @@ export const storeCredential = async (
28
30
  { credentialId: id }
29
31
  );
30
32
  }
31
- return Credential.createOne({
32
- id,
33
- credential: JSON.stringify(credential),
34
- ...(statusEntries?.length ? { statusEntries: JSON.stringify(statusEntries) } : {}),
35
- });
33
+ return Credential.createOne(
34
+ {
35
+ id,
36
+ credential: JSON.stringify(credential),
37
+ ...(statusEntries?.length ? { statusEntries: JSON.stringify(statusEntries) } : {}),
38
+ },
39
+ transaction ? { session: transaction } : undefined
40
+ );
36
41
  };
@@ -1,4 +1,6 @@
1
+ import { termsReferralSnapshotCypher } from '@helpers/consent-referral.helpers';
1
2
  import { QueryBuilder, BindParam } from 'neogma';
3
+ import type { Transaction } from 'neo4j-driver';
2
4
  import { v4 as uuid } from 'uuid';
3
5
 
4
6
  import {
@@ -24,7 +26,8 @@ export const createSentCredentialRelationship = async (
24
26
  credential: CredentialInstance,
25
27
  metadata?: Record<string, unknown>,
26
28
  activityId?: string,
27
- integrationId?: string
29
+ integrationId?: string,
30
+ transaction?: Transaction
28
31
  ): Promise<void> => {
29
32
  const properties = flattenObject({
30
33
  to: to.profileId,
@@ -57,12 +60,10 @@ export const createSentCredentialRelationship = async (
57
60
  ],
58
61
  })
59
62
  .create(
60
- `(profile)-[r:${
61
- Profile.getRelationshipByAlias('credentialSent').name
62
- }]->(credential)`
63
+ `(profile)-[r:${Profile.getRelationshipByAlias('credentialSent').name}]->(credential)`
63
64
  )
64
65
  .set('r = $params')
65
- .run();
66
+ .run(transaction);
66
67
 
67
68
  // If the issuer is an AppStoreListing, additionally write the
68
69
  // AppStoreListing -> CREDENTIAL_SENT -> Credential edge so that
@@ -91,7 +92,7 @@ export const createSentCredentialRelationship = async (
91
92
  }]->(credential)`
92
93
  )
93
94
  .set('r = $params')
94
- .run();
95
+ .run(transaction);
95
96
  }
96
97
  };
97
98
 
@@ -139,7 +140,8 @@ export const createReceivedCredentialRelationship = async (
139
140
  to: ProfileType,
140
141
  from: ProfileType | { listing_id: string },
141
142
  credential: CredentialInstance,
142
- metadata?: Record<string, unknown>
143
+ metadata?: Record<string, unknown>,
144
+ transaction?: Transaction
143
145
  ): Promise<void> => {
144
146
  const fromId = 'profileId' in from ? from.profileId : from.listing_id;
145
147
  const properties = flattenObject({
@@ -165,12 +167,13 @@ export const createReceivedCredentialRelationship = async (
165
167
  }]->(profile)`
166
168
  )
167
169
  .set('r = $params')
168
- .run();
170
+ .run(transaction);
169
171
  };
170
172
 
171
173
  export const setDefaultClaimedRole = async (
172
174
  profile: ProfileType,
173
- credential: CredentialInstance
175
+ credential: CredentialInstance,
176
+ transaction?: Transaction
174
177
  ): Promise<void> => {
175
178
  await new QueryBuilder()
176
179
  .match({
@@ -200,7 +203,10 @@ export const setDefaultClaimedRole = async (
200
203
  { identifier: 'boost' },
201
204
  ],
202
205
  })
203
- .run();
206
+ .run(transaction);
207
+
208
+ // An enclosing transaction invalidates caches only after its writes commit.
209
+ if (transaction) return;
204
210
 
205
211
  try {
206
212
  const boostId = await getBoostIdForCredentialInstance(credential);
@@ -239,6 +245,7 @@ export const createCredentialIssuedViaContractRelationship = async (
239
245
  { identifier: 'terms' },
240
246
  ],
241
247
  })
248
+ .raw(termsReferralSnapshotCypher())
242
249
  .with('transaction')
243
250
  .match({
244
251
  model: Credential,
@@ -1,4 +1,5 @@
1
1
  import { QueryBuilder, BindParam } from 'neogma';
2
+ import type { Transaction } from 'neo4j-driver';
2
3
  import { Profile, InboxCredential } from '@models';
3
4
 
4
5
  export const createDeliveredRelationship = async (
@@ -8,19 +9,23 @@ export const createDeliveredRelationship = async (
8
9
  deliveryMethod: string = 'direct'
9
10
  ): Promise<boolean> => {
10
11
  const timestamp = new Date().toISOString();
11
-
12
- const result = await new QueryBuilder(new BindParam({
13
- profileId,
14
- inboxCredentialId,
15
- timestamp,
16
- recipientDid,
17
- deliveryMethod
18
- }))
12
+
13
+ const result = await new QueryBuilder(
14
+ new BindParam({
15
+ profileId,
16
+ inboxCredentialId,
17
+ timestamp,
18
+ recipientDid,
19
+ deliveryMethod,
20
+ })
21
+ )
19
22
  .match({ model: Profile, identifier: 'profile' })
20
23
  .where('profile.profileId = $profileId')
21
24
  .match({ model: InboxCredential, identifier: 'inboxCredential' })
22
25
  .where('inboxCredential.id = $inboxCredentialId')
23
- .create('(profile)-[:DELIVERED_INBOX_CREDENTIAL { timestamp: $timestamp, recipientDid: $recipientDid, deliveryMethod: $deliveryMethod }]->(inboxCredential)')
26
+ .create(
27
+ '(profile)-[:DELIVERED_INBOX_CREDENTIAL { timestamp: $timestamp, recipientDid: $recipientDid, deliveryMethod: $deliveryMethod }]->(inboxCredential)'
28
+ )
24
29
  .return('profile')
25
30
  .run();
26
31
 
@@ -30,23 +35,28 @@ export const createDeliveredRelationship = async (
30
35
  export const createClaimedRelationship = async (
31
36
  profileId: string,
32
37
  inboxCredentialId: string,
33
- claimToken: string
38
+ claimToken: string,
39
+ transaction?: Transaction
34
40
  ): Promise<boolean> => {
35
41
  const timestamp = new Date().toISOString();
36
-
37
- const result = await new QueryBuilder(new BindParam({
38
- profileId,
39
- inboxCredentialId,
40
- timestamp,
41
- claimToken
42
- }))
43
- .match({ model: Profile, identifier: 'profile'})
42
+
43
+ const result = await new QueryBuilder(
44
+ new BindParam({
45
+ profileId,
46
+ inboxCredentialId,
47
+ timestamp,
48
+ claimToken,
49
+ })
50
+ )
51
+ .match({ model: Profile, identifier: 'profile' })
44
52
  .where('profile.profileId = $profileId')
45
- .match({ model: InboxCredential, identifier: 'inboxCredential'})
53
+ .match({ model: InboxCredential, identifier: 'inboxCredential' })
46
54
  .where('inboxCredential.id = $inboxCredentialId')
47
- .create('(profile)-[:CLAIMED_INBOX_CREDENTIAL { timestamp: $timestamp, claimToken: $claimToken }]->(inboxCredential)')
55
+ .create(
56
+ '(profile)-[:CLAIMED_INBOX_CREDENTIAL { timestamp: $timestamp, claimToken: $claimToken }]->(inboxCredential)'
57
+ )
48
58
  .return('profile')
49
- .run();
59
+ .run(transaction);
50
60
 
51
61
  return result.records.length > 0;
52
62
  };
@@ -58,19 +68,23 @@ export const createEmailSentRelationship = async (
58
68
  token: string
59
69
  ): Promise<boolean> => {
60
70
  const timestamp = new Date().toISOString();
61
-
62
- const result = await new QueryBuilder(new BindParam({
63
- profileDid,
64
- inboxCredentialId,
65
- timestamp,
66
- emailAddress,
67
- token
68
- }))
71
+
72
+ const result = await new QueryBuilder(
73
+ new BindParam({
74
+ profileDid,
75
+ inboxCredentialId,
76
+ timestamp,
77
+ emailAddress,
78
+ token,
79
+ })
80
+ )
69
81
  .match({ model: Profile, identifier: 'profile' })
70
82
  .where('profile.did = $profileDid')
71
83
  .match({ model: InboxCredential, identifier: 'inboxCredential' })
72
84
  .where('inboxCredential.id = $inboxCredentialId')
73
- .create('(profile)-[:SENT_EMAIL { timestamp: $timestamp, emailAddress: $emailAddress, token: $token }]->(inboxCredential)')
85
+ .create(
86
+ '(profile)-[:SENT_EMAIL { timestamp: $timestamp, emailAddress: $emailAddress, token: $token }]->(inboxCredential)'
87
+ )
74
88
  .return('profile')
75
89
  .run();
76
90
 
@@ -85,20 +99,24 @@ export const createWebhookSentRelationship = async (
85
99
  response?: string
86
100
  ): Promise<boolean> => {
87
101
  const timestamp = new Date().toISOString();
88
-
89
- const result = await new QueryBuilder(new BindParam({
90
- profileDid,
91
- inboxCredentialId,
92
- timestamp,
93
- url,
94
- status,
95
- response: response || null
96
- }))
102
+
103
+ const result = await new QueryBuilder(
104
+ new BindParam({
105
+ profileDid,
106
+ inboxCredentialId,
107
+ timestamp,
108
+ url,
109
+ status,
110
+ response: response || null,
111
+ })
112
+ )
97
113
  .match({ model: Profile, identifier: 'profile' })
98
114
  .where('profile.did = $profileDid')
99
115
  .match({ model: InboxCredential, identifier: 'inboxCredential' })
100
116
  .where('inboxCredential.id = $inboxCredentialId')
101
- .create('(profile)-[:SENT_WEBHOOK { timestamp: $timestamp, url: $url, status: $status, response: $response }]->(inboxCredential)')
117
+ .create(
118
+ '(profile)-[:SENT_WEBHOOK { timestamp: $timestamp, url: $url, status: $status, response: $response }]->(inboxCredential)'
119
+ )
102
120
  .return('profile')
103
121
  .run();
104
122
 
@@ -110,19 +128,23 @@ export const createExpiredRelationship = async (
110
128
  inboxCredentialId: string
111
129
  ): Promise<boolean> => {
112
130
  const timestamp = new Date().toISOString();
113
-
114
- const result = await new QueryBuilder(new BindParam({
115
- profileDid,
116
- inboxCredentialId,
117
- timestamp
118
- }))
131
+
132
+ const result = await new QueryBuilder(
133
+ new BindParam({
134
+ profileDid,
135
+ inboxCredentialId,
136
+ timestamp,
137
+ })
138
+ )
119
139
  .match({ model: Profile, identifier: 'profile' })
120
140
  .where('profile.did = $profileDid')
121
141
  .match({ model: InboxCredential, identifier: 'inboxCredential' })
122
142
  .where('inboxCredential.id = $inboxCredentialId')
123
- .create('(profile)-[:EXPIRED_INBOX_CREDENTIAL { timestamp: $timestamp }]->(inboxCredential)')
143
+ .create(
144
+ '(profile)-[:EXPIRED_INBOX_CREDENTIAL { timestamp: $timestamp }]->(inboxCredential)'
145
+ )
124
146
  .return('profile')
125
147
  .run();
126
148
 
127
149
  return result.records.length > 0;
128
- };
150
+ };
@@ -1,4 +1,5 @@
1
1
  import { QueryBuilder, BindParam } from 'neogma';
2
+ import type { Transaction } from 'neo4j-driver';
2
3
  import { InboxCredential } from '@models';
3
4
  import { InboxCredentialType } from '@learncard/types';
4
5
  import { flattenObject, inflateObject } from '@helpers/objects.helpers';
@@ -31,7 +32,8 @@ export const updateInboxCredential = async (
31
32
  export const finalizeAndWipeInboxCredential = async (
32
33
  id: string,
33
34
  delivery: InboxDelivery,
34
- { isAccepted = true }: { isAccepted?: boolean } = {}
35
+ { isAccepted = true }: { isAccepted?: boolean } = {},
36
+ transaction?: Transaction
35
37
  ): Promise<InboxCredentialType | null> => {
36
38
  const result = await new QueryBuilder(
37
39
  new BindParam({
@@ -64,7 +66,7 @@ export const finalizeAndWipeInboxCredential = async (
64
66
  )
65
67
  .return('inboxCredential')
66
68
  .limit(1)
67
- .run();
69
+ .run(transaction);
68
70
 
69
71
  const inboxCredential = result.records[0]?.get('inboxCredential')?.properties;
70
72
  return inboxCredential
@@ -1,5 +1,6 @@
1
1
  import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
2
2
  import { failShareLink } from './errors';
3
+ import { queueShareAttachmentCleanup } from './attachment';
3
4
  import {
4
5
  deleteReservation,
5
6
  enqueueCleanupJob,
@@ -70,6 +71,25 @@ export const abandonReservation = async (
70
71
  }
71
72
 
72
73
  let cleanupQueuedFor: string | null = null;
74
+ if (
75
+ reservation.attachmentId &&
76
+ reservation.attachmentChunkCount &&
77
+ reservation.contentVersion
78
+ ) {
79
+ await queueShareAttachmentCleanup(
80
+ tx,
81
+ {
82
+ namespace: reservation.namespace,
83
+ ownerProfileId: reservation.ownerProfileId,
84
+ shareId: reservation.shareId,
85
+ contentVersion: reservation.contentVersion,
86
+ attachmentId: reservation.attachmentId,
87
+ chunkCount: reservation.attachmentChunkCount,
88
+ },
89
+ 'abandoned',
90
+ nowIso
91
+ );
92
+ }
73
93
 
74
94
  if (reservation.objectRef) {
75
95
  await enqueueCleanupJob(tx, {