@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,211 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { BindParam, QueryBuilder } from 'neogma';
3
+ import { TRPCError } from '@trpc/server';
4
+ import type { SendContractRequest } from '@learncard/types';
5
+ import {
6
+ getContractByUri,
7
+ getContractRequestAccess,
8
+ getStoredContractRequest,
9
+ } from '@accesslayer/consentflowcontract/read';
10
+ import { getProfileByProfileId } from '@accesslayer/profile/read';
11
+ import { getWritersForContract } from '@accesslayer/consentflowcontract/relationships/read';
12
+ import {
13
+ canReadContractData,
14
+ lockContractAudience,
15
+ runAudienceMutation,
16
+ } from '@accesslayer/consentflowcontract/relationships/recipients';
17
+ import { getProfileIdFromString } from './did.helpers';
18
+ import { userHasRequiredScopes } from './auth-grant.helpers';
19
+ import { enforceRateLimits } from './rateLimit.helpers';
20
+ import { requestEventCypher, tryDispatchContractEvent } from './contract-events.helpers';
21
+ import type { ProfileType } from 'types/profile';
22
+ import type { DbContractType } from 'types/consentflowcontract';
23
+
24
+ /** Resolve shared request inputs without granting writers access to consented data. */
25
+ export const resolveContractRequest = async (
26
+ contractUri: string,
27
+ targetIdentifier: string,
28
+ profile: ProfileType,
29
+ domain: string,
30
+ allowRecipients = true
31
+ ): Promise<{ contract: DbContractType; target: ProfileType }> => {
32
+ const contract = await getContractByUri(contractUri);
33
+ if (!contract) throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
34
+ if (!(await canManageContractRequests(contract, profile.profileId, allowRecipients)))
35
+ throw new TRPCError({
36
+ code: 'UNAUTHORIZED',
37
+ message: 'You do not have permission to send requests for this contract.',
38
+ });
39
+ const targetId = await getProfileIdFromString(targetIdentifier, domain);
40
+ const target = targetId ? await getProfileByProfileId(targetId) : null;
41
+ if (!target) throw new TRPCError({ code: 'NOT_FOUND', message: 'Target profile not found' });
42
+ return { contract, target };
43
+ };
44
+
45
+ export const canManageContractRequests = async (
46
+ contract: DbContractType,
47
+ profileId: string,
48
+ allowRecipients = false
49
+ ): Promise<boolean> => {
50
+ const access = await getContractRequestAccess(contract.id, profileId);
51
+ return access.isManager || (allowRecipients && access.isRecipient);
52
+ };
53
+
54
+ /** One request per contract/target in v1. Exact pending retries do not create another event. */
55
+ export const sendGenericContractRequest = async (
56
+ input: SendContractRequest,
57
+ profile: ProfileType,
58
+ domain: string
59
+ ): Promise<boolean> => {
60
+ const { contract, target } = await resolveContractRequest(
61
+ input.contractUri,
62
+ input.targetProfileId,
63
+ profile,
64
+ domain
65
+ );
66
+ if (target.profileId === profile.profileId)
67
+ throw new TRPCError({
68
+ code: 'BAD_REQUEST',
69
+ message: 'You cannot send a request to yourself.',
70
+ });
71
+ await enforceRateLimits([
72
+ {
73
+ key: `contract-request-rate:${contract.id}:${profile.profileId}`,
74
+ limit: 500,
75
+ windowSeconds: 3600,
76
+ description: '500 contract requests per hour',
77
+ },
78
+ ]);
79
+ const requestId = randomUUID();
80
+ const query = lockContractAudience(
81
+ new QueryBuilder(
82
+ new BindParam({
83
+ targetProfileId: target.profileId,
84
+ senderProfileId: profile.profileId,
85
+ requestId,
86
+ externalReferenceId: input.externalReferenceId ?? null,
87
+ message: input.message ?? null,
88
+ now: new Date().toISOString(),
89
+ domain,
90
+ eventFrom: profile.profileId,
91
+ eventKind: 'request_sent',
92
+ })
93
+ ),
94
+ contract.id
95
+ ).raw(`
96
+ MATCH (target:Profile {profileId: $targetProfileId})
97
+ WITH contract, target WHERE $targetProfileId <> $senderProfileId AND EXISTS {
98
+ MATCH (contract)-[:CREATED_BY|CAN_WRITE|SHARES_DATA_WITH]->(:Profile {profileId: $senderProfileId})
99
+ } AND CASE WHEN contract.expiresAt IS NULL OR trim(contract.expiresAt) = '' THEN true ELSE datetime(contract.expiresAt) > datetime($now) END
100
+ AND NOT EXISTS {
101
+ MATCH (target)-[:CREATED_BY]->(terms:ConsentFlowTerms)-[:CONSENTS_TO]->(contract)
102
+ WHERE (terms.status = 'live' OR (terms.status = 'stale' AND terms.oneTime = true)) AND
103
+ CASE WHEN terms.expiresAt IS NULL OR trim(terms.expiresAt) = '' THEN true ELSE datetime(terms.expiresAt) > datetime($now) END
104
+ }
105
+ OPTIONAL MATCH (contract)-[existing:REQUESTED_FOR]->(target)
106
+ WITH contract, target, existing WHERE existing IS NULL OR
107
+ (existing.status = 'pending' AND existing.requestId IS NOT NULL AND
108
+ existing.requestedBy = $senderProfileId AND
109
+ coalesce(existing.externalReferenceId, '') = coalesce($externalReferenceId, '') AND
110
+ coalesce(existing.message, '') = coalesce($message, ''))
111
+ MERGE (contract)-[request:REQUESTED_FOR]->(target)
112
+ ON CREATE SET request += {requestId: $requestId, requestedBy: $senderProfileId,
113
+ externalReferenceId: $externalReferenceId, message: $message,
114
+ requestedAt: $now, status: 'pending', readStatus: 'unseen'}
115
+ ${requestEventCypher}
116
+ `);
117
+ const result = await runAudienceMutation(query);
118
+ if (!result.records.length)
119
+ throw new TRPCError({
120
+ code: 'CONFLICT',
121
+ message:
122
+ 'Active consent or a conflicting/terminal request already exists, or the contract changed.',
123
+ });
124
+ await tryDispatchContractEvent(result.records[0]!.get('eventId'));
125
+ return true;
126
+ };
127
+
128
+ /** Target-only denial; cancellation also permits owner/writers and the requesting recipient. */
129
+ export const decideGenericContractRequest = async ({
130
+ contract,
131
+ targetProfileId,
132
+ actorProfileId,
133
+ status,
134
+ domain,
135
+ }: {
136
+ contract: DbContractType;
137
+ targetProfileId: string;
138
+ actorProfileId: string;
139
+ status: 'denied' | 'cancelled';
140
+ domain: string;
141
+ }): Promise<boolean> => {
142
+ const request = await getStoredContractRequest(contract.id, targetProfileId);
143
+ if (!request?.requestId)
144
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract request not found' });
145
+ const isTarget = actorProfileId === targetProfileId;
146
+ const writers = await getWritersForContract(contract);
147
+ const authorized =
148
+ isTarget ||
149
+ (status === 'cancelled' &&
150
+ (writers.some(profile => profile.profileId === actorProfileId) ||
151
+ (request.requestedBy === actorProfileId &&
152
+ (await canReadContractData(contract.id, actorProfileId)))));
153
+ if (!authorized)
154
+ throw new TRPCError({ code: 'UNAUTHORIZED', message: 'You cannot change this request' });
155
+ const result = await runAudienceMutation(
156
+ lockContractAudience(
157
+ new QueryBuilder(
158
+ new BindParam({
159
+ targetProfileId,
160
+ actorProfileId,
161
+ status,
162
+ domain,
163
+ now: new Date().toISOString(),
164
+ eventKind: status === 'denied' ? 'request_denied' : 'request_cancelled',
165
+ eventFrom: actorProfileId,
166
+ })
167
+ ),
168
+ contract.id
169
+ ).raw(`
170
+ MATCH (contract)-[request:REQUESTED_FOR]->(target:Profile {profileId: $targetProfileId})
171
+ WHERE request.requestId IS NOT NULL AND request.status IN ['pending', $status]
172
+ AND ($actorProfileId = $targetProfileId OR ($status = 'cancelled' AND
173
+ (EXISTS { MATCH (contract)-[:CREATED_BY|CAN_WRITE]->(:Profile {profileId: $actorProfileId}) } OR
174
+ (request.requestedBy = $actorProfileId AND EXISTS {
175
+ MATCH (contract)-[:SHARES_DATA_WITH]->(:Profile {profileId: $actorProfileId}) }))))
176
+ SET request.status = $status
177
+ ${requestEventCypher}
178
+ `)
179
+ );
180
+ if (!result.records.length)
181
+ throw new TRPCError({
182
+ code: 'CONFLICT',
183
+ message: 'Request already decided or permission changed',
184
+ });
185
+ await tryDispatchContractEvent(result.records[0]!.get('eventId'));
186
+ return true;
187
+ };
188
+
189
+ export const markGenericContractRequestSeen = async (
190
+ contractId: string,
191
+ targetProfileId: string
192
+ ): Promise<boolean> => {
193
+ const result = await runAudienceMutation(
194
+ lockContractAudience(new QueryBuilder(new BindParam({ targetProfileId })), contractId)
195
+ .raw(`MATCH (contract)-[request:REQUESTED_FOR]->(:Profile {profileId: $targetProfileId})
196
+ WHERE request.requestId IS NOT NULL
197
+ SET request.readStatus = 'seen' RETURN request`)
198
+ );
199
+ if (!result.records.length)
200
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Request not found' });
201
+ return true;
202
+ };
203
+
204
+ /** Legacy request mutations retain their scope behavior; attributed requests require write. */
205
+ export const assertGenericRequestWriteScope = (scope?: string): void => {
206
+ if (!userHasRequiredScopes(scope ?? '', 'contracts:write'))
207
+ throw new TRPCError({
208
+ code: 'UNAUTHORIZED',
209
+ message: 'This operation requires contracts:write scope',
210
+ });
211
+ };
@@ -3,7 +3,11 @@ import {
3
3
  LCNNotificationTypeEnumValidator,
4
4
  LCNInboxStatusEnumValidator,
5
5
  VC,
6
+ JWE,
6
7
  UnsignedVC,
8
+ VCValidator,
9
+ JWEValidator,
10
+ InboxCredentialType,
7
11
  } from '@learncard/types';
8
12
  import { ProfileType } from 'types/profile';
9
13
  import { getContactMethodsForProfile } from '@accesslayer/contact-method/read';
@@ -21,6 +25,113 @@ import { getLearnCard, getEmptyLearnCard } from '@helpers/learnCard.helpers';
21
25
  import { logCredentialClaimed, logCredentialFailed } from '@helpers/activity.helpers';
22
26
  import { handleConnectionPromptsForCredentialClaim } from '@helpers/connectionPrompt.helpers';
23
27
  import { decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
28
+ import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
29
+ import { getBitstringStatusListEntries } from '@learncard/helpers';
30
+ import type { IssuedCredential } from 'types/credential';
31
+ import { getBoostByUri } from '@accesslayer/boost/read';
32
+ import { storeCredential } from '@accesslayer/credential/create';
33
+ import { createBoostInstanceOfRelationship } from '@accesslayer/boost/relationships/create';
34
+ import {
35
+ createSentCredentialRelationship,
36
+ createReceivedCredentialRelationship,
37
+ setDefaultClaimedRole,
38
+ } from '@accesslayer/credential/relationships/create';
39
+ import { neogma } from '@instance';
40
+ import { processClaimHooks } from '@helpers/claim-hooks.helpers';
41
+ import { clearDidWebCacheForChildProfileManagers } from '@accesslayer/boost/relationships/update';
42
+
43
+ /** Graph-only completion: a failed write retains pending escrow for the next attempt. */
44
+ const finalizeAcceptedDelivery = async (
45
+ inboxCredential: InboxCredentialType,
46
+ issued: IssuedCredential,
47
+ recipient: ProfileType,
48
+ sender: ProfileType
49
+ ): Promise<boolean> => {
50
+ const boost = inboxCredential.boostUri
51
+ ? await getBoostByUri(inboxCredential.boostUri)
52
+ : undefined;
53
+ const session = neogma.driver.session();
54
+ const transaction = session.beginTransaction({ timeout: 30_000 });
55
+ // Signing and status-index allocation happen before this bounded graph transaction;
56
+ // a retry or lost claim race may leave unused indices, intentionally, rather than
57
+ // hold Neo4j locks across external signing.
58
+ try {
59
+ const finalized = await finalizeAndWipeInboxCredential(
60
+ inboxCredential.id,
61
+ { recipientDid: recipient.did, credential: JWEValidator.parse(issued.credential) },
62
+ {},
63
+ transaction
64
+ );
65
+ if (!finalized) {
66
+ // Another claim or expiry won the lock. It is not a new issuance failure.
67
+ await transaction.rollback();
68
+ return false;
69
+ }
70
+ const instance = await storeCredential(issued, transaction);
71
+ if (boost) await createBoostInstanceOfRelationship(instance, boost, transaction);
72
+ await createSentCredentialRelationship(
73
+ { type: 'profile', profile: sender },
74
+ recipient,
75
+ instance,
76
+ undefined,
77
+ inboxCredential.activityId,
78
+ inboxCredential.integrationId,
79
+ transaction
80
+ );
81
+ await createReceivedCredentialRelationship(
82
+ recipient,
83
+ sender,
84
+ instance,
85
+ undefined,
86
+ transaction
87
+ );
88
+ await processClaimHooks(recipient, instance, transaction);
89
+ await setDefaultClaimedRole(recipient, instance, transaction);
90
+ if (
91
+ !(await createClaimedRelationship(
92
+ recipient.profileId,
93
+ inboxCredential.id,
94
+ 'finalize',
95
+ transaction
96
+ ))
97
+ ) {
98
+ throw new Error('Inbox claim recipient no longer exists');
99
+ }
100
+ await transaction.commit();
101
+ } catch (error) {
102
+ await transaction
103
+ .rollback()
104
+ .catch(rollbackError =>
105
+ console.error(
106
+ 'Failed to roll back inbox finalization',
107
+ inboxCredential.id,
108
+ rollbackError
109
+ )
110
+ );
111
+ throw error;
112
+ } finally {
113
+ // Closing the session cannot turn a committed delivery into a pending failure.
114
+ await session
115
+ .close()
116
+ .catch(error =>
117
+ console.error(
118
+ 'Failed to close inbox finalization session',
119
+ inboxCredential.id,
120
+ error
121
+ )
122
+ );
123
+ }
124
+ if (boost) {
125
+ await clearDidWebCacheForChildProfileManagers(boost.id).catch(error =>
126
+ console.error(
127
+ 'Could not clear did:web cache after inbox claim',
128
+ inboxCredential.id,
129
+ error
130
+ )
131
+ );
132
+ }
133
+ return true;
134
+ };
24
135
 
25
136
  export async function finalizeInboxCredentialsForProfile(
26
137
  profile: ProfileType,
@@ -30,8 +141,8 @@ export async function finalizeInboxCredentialsForProfile(
30
141
  claimed: number;
31
142
  errors: number;
32
143
  guardianPending: number;
33
- verifiableCredentials: VC[];
34
- deliveries: { id: string; credential: VC }[];
144
+ verifiableCredentials: (VC | JWE)[];
145
+ deliveries: { id: string; credential: VC | JWE }[];
35
146
  }> {
36
147
  const contactMethods = await getContactMethodsForProfile(profile.did);
37
148
  const verifiedContacts = contactMethods.filter(cm => cm.isVerified);
@@ -48,8 +159,8 @@ export async function finalizeInboxCredentialsForProfile(
48
159
  lcDid = lc.id.did();
49
160
  } catch {}
50
161
 
51
- const verifiableCredentials: VC[] = [];
52
- const deliveries: { id: string; credential: VC }[] = [];
162
+ const verifiableCredentials: (VC | JWE)[] = [];
163
+ const deliveries: { id: string; credential: VC | JWE }[] = [];
53
164
 
54
165
  for (const cm of verifiedContacts) {
55
166
  const pending = await getAcceptedPendingInboxCredentialsForContactMethodId(cm.id);
@@ -79,7 +190,8 @@ export async function finalizeInboxCredentialsForProfile(
79
190
  }
80
191
 
81
192
  try {
82
- let finalCredential: VC;
193
+ let finalCredential: VC | JWE;
194
+ let issued: IssuedCredential | undefined;
83
195
  const credentialPayload = inboxCredential.refreshId
84
196
  ? undefined
85
197
  : await decryptInboxCredential(inboxCredential.credential);
@@ -102,29 +214,16 @@ export async function finalizeInboxCredentialsForProfile(
102
214
  if (!endpoint || !name)
103
215
  throw new Error('Inbox credential missing signing authority info');
104
216
 
105
- const issuerProfile = await getProfileByDid(inboxCredential.issuerDid);
106
- if (!issuerProfile) throw new Error('Issuer profile not found');
217
+ if (!senderProfile) throw new Error('Issuer profile not found');
107
218
 
108
219
  const signingAuthorityForUser = await getSigningAuthorityForUserByName(
109
- issuerProfile,
220
+ senderProfile,
110
221
  endpoint,
111
222
  name
112
223
  );
113
224
  if (!signingAuthorityForUser) throw new Error('Signing authority not found');
114
225
 
115
- // Set subject DID to the authenticated user's DID
116
- if (Array.isArray(unsignedCredential.credentialSubject)) {
117
- unsignedCredential.credentialSubject =
118
- unsignedCredential.credentialSubject.map(sub => ({
119
- ...sub,
120
- id: (sub as any).did || (sub as any).id || profile.did,
121
- }));
122
- } else {
123
- (unsignedCredential.credentialSubject as any).id =
124
- (unsignedCredential as any).credentialSubject?.did ||
125
- (unsignedCredential as any).credentialSubject?.id ||
126
- profile.did;
127
- }
226
+ setCredentialSubjectIds(unsignedCredential, profile.did);
128
227
 
129
228
  // Set issuer from signing authority
130
229
  unsignedCredential.issuer = signingAuthorityForUser.relationship.did;
@@ -136,36 +235,56 @@ export async function finalizeInboxCredentialsForProfile(
136
235
  ? getAppDidWeb(domain, listingSlug)
137
236
  : undefined;
138
237
 
139
- finalCredential = (
140
- await issueCredentialWithSigningAuthority(
141
- { type: 'profile', profile: issuerProfile },
142
- unsignedCredential,
143
- signingAuthorityForUser,
144
- domain,
145
- false,
146
- ownerDidOverride
147
- )
148
- ).credential as VC;
238
+ issued = await issueCredentialWithSigningAuthority(
239
+ { type: 'profile', profile: senderProfile },
240
+ unsignedCredential,
241
+ signingAuthorityForUser,
242
+ domain,
243
+ undefined,
244
+ ownerDidOverride,
245
+ undefined,
246
+ // Preserve issuer did:key access alongside the managed/app did:web reader.
247
+ [senderProfile.did]
248
+ );
249
+ finalCredential = issued.credential;
149
250
  } else {
150
251
  finalCredential = JSON.parse(credentialPayload!) as VC;
151
252
  }
152
253
 
153
254
  if (!inboxCredential.refreshId) {
154
- // The seeded encryption plugin adds the service DID; use explicit recipients.
155
- const learnCard = await getEmptyLearnCard();
156
- const recoveryCredential = await learnCard.invoke.createDagJwe(
157
- finalCredential,
158
- [profile.did]
159
- );
160
- const finalized = await finalizeAndWipeInboxCredential(inboxCredential.id, {
161
- recipientDid: profile.did,
162
- credential: recoveryCredential,
163
- });
164
- if (!finalized) throw new Error('Inbox credential is no longer pending');
255
+ if (!senderProfile) throw new Error('Issuer profile not found');
256
+ if (!issued) {
257
+ const signedCredential = VCValidator.parse(finalCredential);
258
+ const learnCard = await getEmptyLearnCard();
259
+ issued = {
260
+ kind: 'issued-credential',
261
+ credential: await learnCard.invoke.createDagJwe(signedCredential, [
262
+ profile.did,
263
+ inboxCredential.issuerDid,
264
+ ]),
265
+ statusEntries: getBitstringStatusListEntries(signedCredential),
266
+ };
267
+ }
268
+ if (
269
+ !(await finalizeAcceptedDelivery(
270
+ inboxCredential,
271
+ issued,
272
+ profile,
273
+ senderProfile
274
+ ))
275
+ )
276
+ continue;
277
+ finalCredential = issued.credential;
165
278
  }
166
279
 
167
- // Only write a claim audit edge once the record is actually finalized.
168
- await createClaimedRelationship(profile.profileId, inboxCredential.id, 'finalize');
280
+ // Managed refresh retains its separate lifecycle; ordinary claims audit in the transaction.
281
+ if (inboxCredential.refreshId) {
282
+ await createClaimedRelationship(
283
+ profile.profileId,
284
+ inboxCredential.id,
285
+ 'finalize'
286
+ );
287
+ }
169
288
 
170
289
  if (
171
290
  senderProfile &&
@@ -28,6 +28,7 @@ import {
28
28
 
29
29
  import { ProfileType, SigningAuthorityForUserType } from 'types/profile';
30
30
  import type { IssuedCredential } from 'types/credential';
31
+ import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
31
32
  import { getBitstringStatusListEntries } from '@learncard/helpers';
32
33
  import { createInboxCredential } from '@accesslayer/inbox-credential/create';
33
34
  import { Context } from '@routes';
@@ -111,51 +112,48 @@ export const claimIntoInbox = async (
111
112
  );
112
113
 
113
114
  if (existingProfile) {
114
- // Auto-deliver to existing user
115
- let finalCredential: VC;
116
-
115
+ let issued: IssuedCredential;
117
116
  if (isSigned) {
118
- finalCredential = credential as VC;
117
+ const signedCredential = credential as VC;
118
+ const learnCard = await getEmptyLearnCard();
119
+ issued = {
120
+ kind: 'issued-credential',
121
+ credential: await learnCard.invoke.createDagJwe(signedCredential, [
122
+ existingProfile.did,
123
+ issuerProfile.did,
124
+ ]),
125
+ statusEntries: getBitstringStatusListEntries(signedCredential),
126
+ };
119
127
  } else {
120
- // For app-based SAs (listings), use the app did:web as ownerDid
128
+ const unsignedCredential = credential as UnsignedVC;
129
+ setCredentialSubjectIds(unsignedCredential, existingProfile.did);
130
+ // Listings sign as the app, but the issuing profile must remain a reader.
121
131
  const ownerDidOverride = listingSlug
122
132
  ? getAppDidWeb(ctx.domain, listingSlug)
123
133
  : undefined;
124
-
125
- finalCredential = (
126
- await issueCredentialWithSigningAuthority(
127
- { type: 'profile', profile: issuerProfile },
128
- credential as UnsignedVC,
129
- signingAuthorityForUser,
130
- ctx.domain,
131
- false, // don't encrypt
132
- ownerDidOverride
133
- )
134
- ).credential as VC;
134
+ issued = await issueCredentialWithSigningAuthority(
135
+ { type: 'profile', profile: issuerProfile },
136
+ unsignedCredential,
137
+ signingAuthorityForUser,
138
+ ctx.domain,
139
+ undefined,
140
+ ownerDidOverride,
141
+ undefined,
142
+ [issuerProfile.did]
143
+ );
135
144
  }
136
-
137
- // Use the explicit-recipient API; the seeded encryption plugin also adds the service DID.
138
- const learnCard = await getEmptyLearnCard();
139
- const encryptedDelivery = await learnCard.invoke.createDagJwe(finalCredential, [
140
- existingProfile.did,
141
- issuerProfile.did,
142
- ]);
143
145
  await sendCredential(
144
146
  issuerProfile,
145
147
  existingProfile,
146
- {
147
- kind: 'issued-credential',
148
- credential: encryptedDelivery,
149
- statusEntries: getBitstringStatusListEntries(finalCredential),
150
- },
148
+ issued,
151
149
  ctx.domain,
152
150
  undefined,
153
151
  activityId,
154
152
  integrationId
155
153
  );
156
- // Record successful delivery without creating temporary service-readable escrow.
157
154
  const finalizedInboxCredential = await createInboxCredential({
158
- credential: JSON.stringify(finalCredential),
155
+ // Delivered audit rows never retain a credential payload.
156
+ credential: '',
159
157
  isSigned: true,
160
158
  delivered: true,
161
159
  isAccepted: true,
@@ -166,7 +164,6 @@ export const claimIntoInbox = async (
166
164
  activityId,
167
165
  expiresInDays,
168
166
  });
169
-
170
167
  return {
171
168
  status: LCNInboxStatusEnumValidator.enum.ISSUED,
172
169
  inboxCredential: finalizedInboxCredential,
@@ -333,50 +330,39 @@ export const issueToInbox = async (
333
330
  const recipientIsManaged = recipientManagers.length > 0;
334
331
 
335
332
  if (existingProfile && !guardianEmail && !recipientIsManaged && !configuration.refresh) {
336
- // Auto-deliver to existing user
337
- let finalCredential: VC;
338
-
333
+ let issued: IssuedCredential;
339
334
  if (isSigned) {
340
- finalCredential = credential as VC;
335
+ const signedCredential = credential as VC;
336
+ const learnCard = await getEmptyLearnCard();
337
+ issued = {
338
+ kind: 'issued-credential',
339
+ credential: await learnCard.invoke.createDagJwe(signedCredential, [
340
+ existingProfile.did,
341
+ issuerProfile.did,
342
+ ]),
343
+ statusEntries: getBitstringStatusListEntries(signedCredential),
344
+ };
341
345
  } else {
342
- // Sign the credential using signing authority
343
346
  const signingAuthorityForUser = await getSigningAuthorityForUserByName(
344
347
  issuerProfile,
345
348
  signingAuthority!.endpoint,
346
349
  signingAuthority!.name
347
350
  );
348
-
349
351
  if (!signingAuthorityForUser) {
350
352
  throw new InboxIssuancePreflightError({
351
353
  code: 'NOT_FOUND',
352
354
  message: 'Signing authority not found for issuer',
353
355
  });
354
356
  }
355
-
356
- finalCredential = (
357
- await issueCredentialWithSigningAuthority(
358
- { type: 'profile', profile: issuerProfile },
359
- credential as UnsignedVC,
360
- signingAuthorityForUser,
361
- ctx.domain,
362
- false // don't encrypt
363
- )
364
- ).credential as VC;
357
+ const unsignedCredential = credential as UnsignedVC;
358
+ setCredentialSubjectIds(unsignedCredential, existingProfile.did);
359
+ issued = await issueCredentialWithSigningAuthority(
360
+ { type: 'profile', profile: issuerProfile },
361
+ unsignedCredential,
362
+ signingAuthorityForUser,
363
+ ctx.domain
364
+ );
365
365
  }
366
-
367
- // Send credential using appropriate helper (sendBoost handles boost tracking)
368
- // Pass activityId and integrationId so they're stored on the relationship for CLAIMED chaining
369
- const learnCard = await getEmptyLearnCard();
370
- const encryptedDelivery = await learnCard.invoke.createDagJwe(finalCredential, [
371
- existingProfile.did,
372
- issuerProfile.did,
373
- ]);
374
- // Carry only public status coordinates alongside the newly encrypted payload.
375
- const delivery: IssuedCredential = {
376
- kind: 'issued-credential',
377
- credential: encryptedDelivery,
378
- statusEntries: getBitstringStatusListEntries(finalCredential),
379
- };
380
366
  const boost = boostUri ? await getBoostByUri(boostUri) : undefined;
381
367
  await checkpointDelivery();
382
368
  if (boostUri && boost) {
@@ -384,27 +370,26 @@ export const issueToInbox = async (
384
370
  from: { type: 'profile', profile: issuerProfile },
385
371
  to: existingProfile,
386
372
  boost,
387
- credential: delivery,
373
+ credential: issued,
388
374
  domain: ctx.domain,
389
375
  activityId,
390
376
  integrationId,
391
377
  });
392
378
  } else {
393
- // Fall back to ordinary delivery when no boost exists for the supplied URI.
394
379
  await sendCredential(
395
380
  issuerProfile,
396
381
  existingProfile,
397
- delivery,
382
+ issued,
398
383
  ctx.domain,
399
384
  undefined,
400
385
  activityId,
401
386
  integrationId
402
387
  );
403
388
  }
404
-
405
389
  // Record successful delivery without creating temporary service-readable escrow.
406
390
  const finalizedInboxCredential = await createInboxCredential({
407
- credential: JSON.stringify(finalCredential),
391
+ // Delivered audit rows never retain a credential payload.
392
+ credential: '',
408
393
  isSigned: true,
409
394
  delivered: true,
410
395
  recipient,