@learncard/network-brain-service 3.19.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/.env.example +44 -111
  2. package/AGENTS.md +52 -0
  3. package/CHANGELOG.md +120 -0
  4. package/README.md +9 -2
  5. package/bootstrapLambda.test.ts +72 -0
  6. package/bundle.mjs +9 -1
  7. package/config/config.dev.json +29 -0
  8. package/config/config.example.json +29 -0
  9. package/config/config.json +1 -0
  10. package/config/config.production.json +29 -0
  11. package/config/config.scouts.dev.json +24 -0
  12. package/config/config.scouts.production.json +24 -0
  13. package/contractEventsLambda.ts +18 -0
  14. package/contractEventsLambdaApp.ts +19 -0
  15. package/didWebLambda.ts +13 -18
  16. package/didWebLambdaApp.ts +22 -0
  17. package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
  18. package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
  19. package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
  20. package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
  21. package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
  22. package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
  23. package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
  24. package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
  25. package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
  26. package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
  27. package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
  28. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  29. package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
  30. package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
  31. package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
  32. package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
  33. package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
  34. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  35. package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
  36. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  37. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
  38. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  39. package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
  40. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
  41. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  42. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
  43. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
  44. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
  45. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
  47. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  48. package/dist/accesslayer/credential/create.d.ts +2 -1
  49. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  50. package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
  51. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  52. package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
  53. package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
  54. package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
  55. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  56. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -1
  57. package/dist/accesslayer/share-link/attachment.d.ts +37 -0
  58. package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
  59. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -1
  60. package/dist/accesslayer/share-link/helpers.d.ts +3 -0
  61. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -1
  62. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -1
  63. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -1
  64. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -1
  65. package/dist/accesslayer/share-link/types.d.ts +8 -3
  66. package/dist/accesslayer/share-link/types.d.ts.map +1 -1
  67. package/dist/cache/app-store.caches.d.ts +49 -0
  68. package/dist/cache/app-store.caches.d.ts.map +1 -1
  69. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  70. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  71. package/dist/config/environment.d.ts +5 -0
  72. package/dist/config/environment.d.ts.map +1 -1
  73. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  74. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  75. package/dist/config/stageConfig.d.ts +112 -0
  76. package/dist/config/stageConfig.d.ts.map +1 -0
  77. package/dist/config/stageConfig.test.d.ts +2 -0
  78. package/dist/config/stageConfig.test.d.ts.map +1 -0
  79. package/dist/didkit_wasm_bg.wasm +0 -0
  80. package/dist/docker-entry.d.ts +1 -1
  81. package/dist/docker-entry.d.ts.map +1 -1
  82. package/dist/helpers/app-manifest.helpers.d.ts +5 -0
  83. package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
  84. package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
  85. package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
  86. package/dist/helpers/consent-referral.helpers.d.ts +2 -0
  87. package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
  88. package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
  89. package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
  90. package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
  91. package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
  92. package/dist/helpers/contract-events.helpers.d.ts +25 -0
  93. package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
  94. package/dist/helpers/contract-requests.helpers.d.ts +19 -0
  95. package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
  96. package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
  97. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  98. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  99. package/dist/helpers/inline-template.helpers.d.ts +45 -0
  100. package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
  101. package/dist/helpers/notificationMessages.d.ts +1 -1
  102. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  103. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  104. package/dist/helpers/notifications.helpers.d.ts +6 -0
  105. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  106. package/dist/helpers/profile.helpers.d.ts +1 -0
  107. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  108. package/dist/helpers/share-link-attachment-access.d.ts +4 -0
  109. package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
  110. package/dist/helpers/share-link-attachment.d.ts +18 -0
  111. package/dist/helpers/share-link-attachment.d.ts.map +1 -0
  112. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
  113. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
  114. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -1
  115. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -1
  116. package/dist/helpers/share-link-maintenance/runner.d.ts +1 -0
  117. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -1
  118. package/dist/helpers/share-link-maintenance/runtime.d.ts +2 -0
  119. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -1
  120. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -1
  121. package/dist/helpers/share-link-maintenance/types.d.ts +4 -1
  122. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -1
  123. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -1
  124. package/dist/helpers/share-link-policy/production.d.ts +3 -5
  125. package/dist/helpers/share-link-policy/production.d.ts.map +1 -1
  126. package/dist/helpers/share-link-policy/resolver.d.ts +1 -1
  127. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -1
  128. package/dist/helpers/share-link-policy/types.d.ts +8 -2
  129. package/dist/helpers/share-link-policy/types.d.ts.map +1 -1
  130. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  131. package/dist/helpers/smartResume.helpers.d.ts +3 -0
  132. package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
  133. package/dist/index.js +2 -2
  134. package/dist/models/AppManifestVersion.d.ts +20 -0
  135. package/dist/models/AppManifestVersion.d.ts.map +1 -0
  136. package/dist/models/AppStoreListing.d.ts +13 -0
  137. package/dist/models/AppStoreListing.d.ts.map +1 -1
  138. package/dist/models/ConsentFlowContract.d.ts +13 -2
  139. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  140. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  141. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  142. package/dist/models/Integration.d.ts +2 -0
  143. package/dist/models/Integration.d.ts.map +1 -1
  144. package/dist/models/ShareLink.d.ts +2 -0
  145. package/dist/models/ShareLink.d.ts.map +1 -1
  146. package/dist/models/index.d.ts +1 -0
  147. package/dist/models/index.d.ts.map +1 -1
  148. package/dist/models/share-link-constraints.d.ts.map +1 -1
  149. package/dist/openapi.d.ts +1 -1
  150. package/dist/openapi.d.ts.map +1 -1
  151. package/dist/publish-metafile.json +1 -0
  152. package/dist/routes/app-store.d.ts +619 -0
  153. package/dist/routes/app-store.d.ts.map +1 -1
  154. package/dist/routes/boosts.d.ts +2 -2
  155. package/dist/routes/boosts.d.ts.map +1 -1
  156. package/dist/routes/contracts.d.ts +146 -3
  157. package/dist/routes/contracts.d.ts.map +1 -1
  158. package/dist/routes/credentials.d.ts +34 -0
  159. package/dist/routes/credentials.d.ts.map +1 -1
  160. package/dist/routes/inbox.d.ts +52 -2
  161. package/dist/routes/inbox.d.ts.map +1 -1
  162. package/dist/routes/index.d.ts.map +1 -1
  163. package/dist/routes/profile-manager.d.ts.map +1 -1
  164. package/dist/routes/profiles.d.ts.map +1 -1
  165. package/dist/routes/public-share-links.d.ts +55 -0
  166. package/dist/routes/public-share-links.d.ts.map +1 -1
  167. package/dist/routes/share-links.d.ts +175 -14
  168. package/dist/routes/share-links.d.ts.map +1 -1
  169. package/dist/routes/test.d.ts +5 -0
  170. package/dist/routes/test.d.ts.map +1 -1
  171. package/dist/routes/workflows.d.ts.map +1 -1
  172. package/dist/types/app-manifest.d.ts +339 -0
  173. package/dist/types/app-manifest.d.ts.map +1 -0
  174. package/dist/types/app-store-listing.d.ts +34 -0
  175. package/dist/types/app-store-listing.d.ts.map +1 -1
  176. package/dist/types/consentflowcontract.d.ts +30 -3
  177. package/dist/types/consentflowcontract.d.ts.map +1 -1
  178. package/dist/types/credential-refresh.d.ts +1 -1
  179. package/esbuildPlugins.cjs +1 -0
  180. package/lambda.ts +49 -195
  181. package/lambdaApp.ts +203 -0
  182. package/package.json +20 -19
  183. package/project.json +8 -1
  184. package/scripts/export-openapi.ts +20 -0
  185. package/scripts/offline-schema-environment.ts +18 -0
  186. package/scripts/verify-python-contracts.ts +72 -0
  187. package/serverless-local.yml +3 -6
  188. package/serverless.function-env.cjs +50 -0
  189. package/serverless.local.yml +0 -1
  190. package/serverless.yml +29 -74
  191. package/shareLinkMaintenanceLambda.ts +13 -44
  192. package/shareLinkMaintenanceLambdaApp.ts +50 -0
  193. package/src/accesslayer/app-manifest-version/create.ts +57 -0
  194. package/src/accesslayer/app-manifest-version/read.ts +129 -0
  195. package/src/accesslayer/app-manifest-version/update.ts +61 -0
  196. package/src/accesslayer/app-store-listing/read.ts +6 -0
  197. package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
  198. package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
  199. package/src/accesslayer/app-store-listing/update.ts +15 -0
  200. package/src/accesslayer/boost/relationships/create.ts +8 -2
  201. package/src/accesslayer/consentflowcontract/create.ts +81 -8
  202. package/src/accesslayer/consentflowcontract/read.ts +84 -10
  203. package/src/accesslayer/consentflowcontract/relationships/create.ts +116 -93
  204. package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
  205. package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
  206. package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
  207. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
  208. package/src/accesslayer/consentflowcontract/relationships/update.ts +240 -189
  209. package/src/accesslayer/credential/create.test.ts +2 -115
  210. package/src/accesslayer/credential/create.ts +11 -6
  211. package/src/accesslayer/credential/relationships/create.ts +17 -10
  212. package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
  213. package/src/accesslayer/inbox-credential/update.ts +4 -2
  214. package/src/accesslayer/share-link/abandon.ts +20 -0
  215. package/src/accesslayer/share-link/attachment.ts +360 -0
  216. package/src/accesslayer/share-link/finalize.ts +55 -7
  217. package/src/accesslayer/share-link/helpers.ts +33 -2
  218. package/src/accesslayer/share-link/recovery.ts +20 -0
  219. package/src/accesslayer/share-link/reserve.ts +85 -6
  220. package/src/accesslayer/share-link/revoke.ts +35 -0
  221. package/src/accesslayer/share-link/types.ts +9 -4
  222. package/src/cache/app-store.caches.ts +35 -0
  223. package/src/config/applyDockerStageConfig.ts +6 -0
  224. package/src/config/environment.test.ts +17 -12
  225. package/src/config/environment.ts +1 -0
  226. package/src/config/serverlessFunctionEnv.test.ts +105 -0
  227. package/src/config/stageConfig.test.ts +25 -0
  228. package/src/config/stageConfig.ts +24 -0
  229. package/src/docker-entry.ts +24 -0
  230. package/src/helpers/app-manifest.helpers.ts +185 -0
  231. package/src/helpers/claim-hooks.helpers.ts +20 -7
  232. package/src/helpers/consent-referral.helpers.ts +6 -0
  233. package/src/helpers/consent-scopes.helpers.ts +58 -0
  234. package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
  235. package/src/helpers/contract-events.helpers.ts +540 -0
  236. package/src/helpers/contract-requests.helpers.ts +211 -0
  237. package/src/helpers/finalize-inbox.helpers.ts +163 -44
  238. package/src/helpers/inbox.helpers.ts +51 -66
  239. package/src/helpers/inline-template.helpers.ts +560 -0
  240. package/src/helpers/notificationMessages.ts +59 -0
  241. package/src/helpers/notificationQueue.helpers.ts +23 -5
  242. package/src/helpers/notifications.helpers.ts +25 -1
  243. package/src/helpers/profile.helpers.ts +6 -0
  244. package/src/helpers/share-link-attachment-access.ts +87 -0
  245. package/src/helpers/share-link-attachment.ts +115 -0
  246. package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
  247. package/src/helpers/share-link-coordinator/coordinator.ts +15 -2
  248. package/src/helpers/share-link-coordinator/recovery-runner.ts +15 -0
  249. package/src/helpers/share-link-maintenance/runner.ts +28 -0
  250. package/src/helpers/share-link-maintenance/runtime.ts +6 -0
  251. package/src/helpers/share-link-maintenance/telemetry.ts +3 -0
  252. package/src/helpers/share-link-maintenance/types.ts +2 -0
  253. package/src/helpers/share-link-owner-projection.ts +2 -0
  254. package/src/helpers/share-link-policy/production.ts +37 -17
  255. package/src/helpers/share-link-policy/resolver.ts +16 -11
  256. package/src/helpers/share-link-policy/types.ts +15 -7
  257. package/src/helpers/signingAuthority.helpers.ts +23 -14
  258. package/src/helpers/smartResume.helpers.ts +123 -0
  259. package/src/models/AppManifestVersion.ts +43 -0
  260. package/src/models/AppStoreListing.ts +32 -0
  261. package/src/models/ConsentFlowContract.ts +34 -3
  262. package/src/models/ConsentFlowTerms.ts +10 -0
  263. package/src/models/ConsentFlowTransaction.ts +3 -0
  264. package/src/models/Integration.ts +2 -0
  265. package/src/models/ShareLink.ts +4 -0
  266. package/src/models/index.ts +23 -0
  267. package/src/models/share-link-constraints.ts +5 -0
  268. package/src/openapi.ts +44 -8
  269. package/src/routes/app-store.ts +1054 -27
  270. package/src/routes/boosts.ts +12 -14
  271. package/src/routes/contracts.ts +382 -262
  272. package/src/routes/inbox.ts +4 -2
  273. package/src/routes/index.ts +4 -1
  274. package/src/routes/profile-manager.ts +19 -0
  275. package/src/routes/profiles.ts +12 -0
  276. package/src/routes/public-share-links.ts +169 -0
  277. package/src/routes/share-links.ts +69 -0
  278. package/src/routes/workflows.ts +3 -2
  279. package/src/types/app-manifest.ts +28 -0
  280. package/src/types/app-store-listing.ts +13 -0
  281. package/src/types/consentflowcontract.ts +25 -4
  282. package/test/app-manifest.helpers.spec.ts +170 -0
  283. package/test/app-manifest.routes.spec.ts +299 -0
  284. package/test/app-store-listing-updates.spec.ts +236 -0
  285. package/test/app-store-listing.spec.ts +65 -3
  286. package/test/consent-data-boundaries.spec.ts +352 -0
  287. package/test/consent-recipients.spec.ts +513 -0
  288. package/test/consent-scopes.helpers.spec.ts +67 -0
  289. package/test/consentflow.spec.ts +63 -2
  290. package/test/contract-requests.spec.ts +1588 -0
  291. package/test/credential-storage.neo4j.spec.ts +122 -0
  292. package/test/guardian-gated-routes.spec.ts +36 -0
  293. package/test/inbox-security.e2e.ts +26 -10
  294. package/test/inbox.spec.ts +184 -8
  295. package/test/inline-template.helpers.spec.ts +49 -0
  296. package/test/managed-recipient-inbox.spec.ts +24 -2
  297. package/test/openapi.spec.ts +14 -73
  298. package/test/profiles.spec.ts +30 -0
  299. package/test/share-link-attachment.neo4j.spec.ts +262 -0
  300. package/test/share-link-attachment.unit.spec.ts +244 -0
  301. package/test/share-link-lifecycle.neo4j.spec.ts +145 -2
  302. package/test/share-link-maintenance.unit.spec.ts +32 -5
  303. package/test/share-link-owner-route.unit.spec.ts +70 -2
  304. package/test/share-link-policy.unit.spec.ts +18 -4
  305. package/test/share-link-production-policy.unit.spec.ts +58 -9
  306. package/test/share-link-public-route.unit.spec.ts +279 -0
  307. package/test/share-link-service-policy.neo4j.spec.ts +403 -0
  308. package/test/signing-authority-encryption.spec.ts +75 -0
  309. package/test/smart-resume-consent.spec.ts +233 -0
  310. package/test/smart-resume-upload.spec.ts +65 -0
  311. package/tsconfig.build.json +1 -1
  312. package/tsconfig.json +1 -0
  313. package/vitest.config.ts +13 -0
  314. package/vitest.inbox-e2e.config.ts +2 -0
  315. package/workerLambda.test.ts +100 -0
@@ -1,9 +1,22 @@
1
+ import { SendContractRequestValidator, ContractRequestFieldsValidator } from '@learncard/types';
2
+ import {
3
+ sendGenericContractRequest,
4
+ decideGenericContractRequest,
5
+ markGenericContractRequestSeen,
6
+ assertGenericRequestWriteScope,
7
+ canManageContractRequests,
8
+ resolveContractRequest,
9
+ } from '@helpers/contract-requests.helpers';
1
10
  import { environment } from '@environment';
11
+ import {
12
+ smartResumeFingerprint,
13
+ publishSmartResume,
14
+ } from '@accesslayer/consentflowcontract/relationships/smartResume';
15
+ import { uploadSmartResume } from '@helpers/smartResume.helpers';
2
16
  import { TRPCError } from '@trpc/server';
3
17
  import { z } from 'zod';
4
18
 
5
19
  import { t, profileRoute, openRoute, guardianGatedRoute } from '@routes';
6
- import { ConsentFlowContract } from '@models';
7
20
 
8
21
  import {
9
22
  ConsentFlowContractValidator,
@@ -47,10 +60,11 @@ import {
47
60
  isProfileConsentFlowContractAdmin,
48
61
  getWritersForContract,
49
62
  } from '@accesslayer/consentflowcontract/relationships/read';
50
- import { constructUri, getIdFromUri, resolveUri } from '@helpers/uri.helpers';
63
+ import { inflateObject } from '@helpers/objects.helpers';
64
+ import { constructUri, getIdFromUri } from '@helpers/uri.helpers';
51
65
  import {
52
66
  getContractByUri,
53
- getConsentFlowContractById,
67
+ getStoredContractRequest,
54
68
  getRequestedForList,
55
69
  getRequestedForForUser,
56
70
  getContractById,
@@ -64,17 +78,12 @@ import { areTermsValid } from '@helpers/contract.helpers';
64
78
  import { updateDidForProfile, getProfileIdFromString } from '@helpers/did.helpers';
65
79
  import {
66
80
  syncCredentialsToContract,
67
- updateRequestedForStatusIfExists,
68
81
  updateTerms,
69
82
  pruneDeletedUrisFromConsentTerms,
70
83
  upsertRequestedForRelationship,
71
84
  withdrawTerms,
72
85
  } from '@accesslayer/consentflowcontract/relationships/update';
73
- import {
74
- consentToContract,
75
- setAutoBoostForContract,
76
- setCreatorForContract,
77
- } from '@accesslayer/consentflowcontract/relationships/create';
86
+ import { consentToContract } from '@accesslayer/consentflowcontract/relationships/create';
78
87
  import { getProfileByDid, getProfileByProfileId } from '@accesslayer/profile/read';
79
88
  import { sendBoost, isDraftBoost } from '@helpers/boost.helpers';
80
89
  import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
@@ -105,6 +114,24 @@ import { addNotificationToQueue } from '@helpers/notifications.helpers';
105
114
  import { getNotificationMessage } from '@helpers/notificationMessages';
106
115
  import { resolveRecipientLocale } from '@helpers/getRecipientLocale.helpers';
107
116
  import { ProfileType } from 'types/profile';
117
+ import {
118
+ getRecipientsForContract,
119
+ addRecipientToContract,
120
+ removeRecipientFromContract,
121
+ canReadContractData,
122
+ } from '@accesslayer/consentflowcontract/relationships/recipients';
123
+
124
+ const publicContractRecipient = (domain: string, recipient: ProfileType) => {
125
+ const profile = updateDidForProfile(domain, recipient);
126
+ return { ...sanitizeProfileForTier(profile, 'unauthenticated'), did: profile.did };
127
+ };
128
+
129
+ const resolveRecipientProfileId = async (identifier: string, domain: string): Promise<string> => {
130
+ const profileId = await getProfileIdFromString(identifier, domain);
131
+ if (!profileId)
132
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid recipient identifier' });
133
+ return profileId;
134
+ };
108
135
 
109
136
  export const contractsRouter = t.router({
110
137
  createConsentFlowContract: profileRoute
@@ -133,6 +160,7 @@ export const contractsRouter = t.router({
133
160
  expiresAt: z.string().optional(),
134
161
  autoboosts: z.array(AutoBoostConfigValidator).optional(),
135
162
  writers: z.array(z.string()).optional(),
163
+ recipients: z.array(z.string()).max(50).optional(),
136
164
  })
137
165
  )
138
166
  .output(z.string())
@@ -150,26 +178,31 @@ export const contractsRouter = t.router({
150
178
  expiresAt,
151
179
  autoboosts,
152
180
  writers,
181
+ recipients,
153
182
  } = input;
154
183
 
155
- // Create ConsentFlow instance
156
- const createdContract = await createConsentFlowContract({
157
- contract,
158
- name,
159
- subtitle,
160
- description,
161
- reasonForAccessing,
162
- needsGuardianConsent,
163
- redirectUrl,
164
- frontDoorBoostUri,
165
- image,
166
- expiresAt,
167
- });
184
+ const recipientIds = [
185
+ ...new Set(
186
+ await Promise.all(
187
+ (recipients ?? []).map(id => resolveRecipientProfileId(id, ctx.domain))
188
+ )
189
+ ),
190
+ ].filter(id => id !== ctx.user.profile.profileId);
191
+ const recipientProfiles = await getProfilesByProfileIds(recipientIds);
192
+ if (recipientProfiles.length !== recipientIds.length) {
193
+ throw new TRPCError({
194
+ code: 'BAD_REQUEST',
195
+ message: 'Could not find all recipient profiles',
196
+ });
197
+ }
168
198
 
169
- // Get profile by profileId
170
- await setCreatorForContract(createdContract, ctx.user.profile);
199
+ const preparedAutoBoosts: {
200
+ id: string;
201
+ signingAuthorityEndpoint: string;
202
+ signingAuthorityName: string;
203
+ }[] = [];
171
204
 
172
- // Add specified writers
205
+ // Validate specified writers before creating the contract.
173
206
  if (writers && writers.length > 0) {
174
207
  const writerProfiles = await getProfilesByProfileIds(writers);
175
208
 
@@ -184,17 +217,6 @@ export const contractsRouter = t.router({
184
217
  )}`,
185
218
  });
186
219
  }
187
-
188
- // Create CAN_WRITE relationship for each writer using static relateTo
189
- for (const writerProfile of writerProfiles) {
190
- await ConsentFlowContract.relateTo({
191
- alias: 'canWrite',
192
- where: {
193
- source: { id: createdContract.id }, // Specify source contract by id
194
- target: { profileId: writerProfile.profileId }, // Specify target profile by profileId
195
- },
196
- });
197
- }
198
220
  }
199
221
 
200
222
  if (autoboosts && autoboosts.length > 0) {
@@ -243,18 +265,94 @@ export const contractsRouter = t.router({
243
265
  });
244
266
  }
245
267
 
246
- await setAutoBoostForContract(
247
- createdContract,
248
- boost,
249
- normalizedSigningAuthority,
250
- ctx.user.profile.profileId
251
- );
268
+ preparedAutoBoosts.push({
269
+ id: boost.id,
270
+ signingAuthorityEndpoint: normalizedSigningAuthority.endpoint,
271
+ signingAuthorityName: normalizedSigningAuthority.name,
272
+ });
252
273
  }
253
274
  }
254
275
 
276
+ // All validation precedes the single atomic creation statement.
277
+ const createdContract = await createConsentFlowContract({
278
+ contract,
279
+ ownerProfileId: ctx.user.profile.profileId,
280
+ recipientIds,
281
+ writerIds: writers,
282
+ autoboosts: preparedAutoBoosts,
283
+ name,
284
+ subtitle,
285
+ description,
286
+ reasonForAccessing,
287
+ needsGuardianConsent,
288
+ redirectUrl,
289
+ frontDoorBoostUri,
290
+ image,
291
+ expiresAt,
292
+ });
293
+
255
294
  return constructUri('contract', createdContract.id, ctx.domain);
256
295
  }),
257
296
 
297
+ addContractRecipient: profileRoute
298
+ .meta({
299
+ requiredScope: 'contracts:write',
300
+ openapi: {
301
+ protect: true,
302
+ method: 'POST',
303
+ path: '/consent-flow-contract/recipient/add',
304
+ tags: ['Contracts'],
305
+ summary: 'Add a data recipient before first consent',
306
+ },
307
+ })
308
+ .input(z.object({ contractUri: z.string(), recipient: z.string() }))
309
+ .output(z.boolean())
310
+ .mutation(async ({ ctx, input }) => {
311
+ const contract = await getContractByUri(input.contractUri);
312
+ if (!contract)
313
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
314
+ if (!(await isProfileConsentFlowContractAdmin(ctx.user.profile, contract)))
315
+ throw new TRPCError({
316
+ code: 'UNAUTHORIZED',
317
+ message: 'Profile does not own contract',
318
+ });
319
+ const profileId = await resolveRecipientProfileId(input.recipient, ctx.domain);
320
+ if (!(await getProfileByProfileId(profileId)))
321
+ throw new TRPCError({
322
+ code: 'BAD_REQUEST',
323
+ message: 'Could not find recipient profile',
324
+ });
325
+ return addRecipientToContract(contract.id, profileId);
326
+ }),
327
+
328
+ removeContractRecipient: profileRoute
329
+ .meta({
330
+ requiredScope: 'contracts:write',
331
+ openapi: {
332
+ protect: true,
333
+ method: 'POST',
334
+ path: '/consent-flow-contract/recipient/remove',
335
+ tags: ['Contracts'],
336
+ summary: 'Remove a data recipient',
337
+ },
338
+ })
339
+ .input(z.object({ contractUri: z.string(), recipient: z.string() }))
340
+ .output(z.boolean())
341
+ .mutation(async ({ ctx, input }) => {
342
+ const contract = await getContractByUri(input.contractUri);
343
+ if (!contract)
344
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
345
+ if (!(await isProfileConsentFlowContractAdmin(ctx.user.profile, contract)))
346
+ throw new TRPCError({
347
+ code: 'UNAUTHORIZED',
348
+ message: 'Profile does not own contract',
349
+ });
350
+ return removeRecipientFromContract(
351
+ contract.id,
352
+ await resolveRecipientProfileId(input.recipient, ctx.domain)
353
+ );
354
+ }),
355
+
258
356
  getConsentFlowContract: openRoute
259
357
  .meta({
260
358
  openapi: {
@@ -280,6 +378,7 @@ export const contractsRouter = t.router({
280
378
  }
281
379
 
282
380
  const autoboosts = await getAutoBoostsForContract(result.contract.id, ctx.domain);
381
+ const recipients = await getRecipientsForContract(result.contract.id);
283
382
 
284
383
  return {
285
384
  owner: updateDidForProfile(ctx.domain, result.contractOwner),
@@ -297,6 +396,10 @@ export const contractsRouter = t.router({
297
396
  uri: constructUri('contract', result.contract.id, ctx.domain),
298
397
  ...(result.contract.expiresAt ? { expiresAt: result.contract.expiresAt } : {}),
299
398
  autoBoosts: autoboosts,
399
+ recipients: recipients.map(recipient =>
400
+ publicContractRecipient(ctx.domain, recipient)
401
+ ),
402
+ audienceVersion: Number(result.contract.audienceVersion ?? 0),
300
403
  };
301
404
  }),
302
405
 
@@ -426,16 +529,16 @@ export const contractsRouter = t.router({
426
529
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
427
530
  }
428
531
 
429
- if (!(await isProfileConsentFlowContractAdmin(profile, contract))) {
532
+ if (!(await canReadContractData(contract.id, profile.profileId))) {
430
533
  throw new TRPCError({
431
534
  code: 'UNAUTHORIZED',
432
- message: 'Profile does not own contract',
535
+ message: 'Profile is not in the contract data audience',
433
536
  });
434
537
  }
435
538
 
436
539
  const contractId = getIdFromUri(uri);
437
540
 
438
- const results = await getConsentedDataForContract(contractId, {
541
+ const results = await getConsentedDataForContract(contractId, profile.profileId, {
439
542
  query,
440
543
  limit: limit + 1,
441
544
  cursor,
@@ -892,6 +995,8 @@ export const contractsRouter = t.router({
892
995
  contractUri: z.string(),
893
996
  expiresAt: z.string().optional(),
894
997
  oneTime: z.boolean().optional(),
998
+ audienceVersion: z.number().int().nonnegative().optional(),
999
+ expectedRequestId: z.string().min(1).optional(),
895
1000
  recipientToken: z.string().optional(), // SmartResume recipientToken needed for API call
896
1001
  })
897
1002
  )
@@ -907,7 +1012,15 @@ export const contractsRouter = t.router({
907
1012
  });
908
1013
  }
909
1014
 
910
- const { terms, contractUri, expiresAt, oneTime, recipientToken } = input;
1015
+ const {
1016
+ terms,
1017
+ contractUri,
1018
+ expiresAt,
1019
+ oneTime,
1020
+ recipientToken,
1021
+ audienceVersion,
1022
+ expectedRequestId,
1023
+ } = input;
911
1024
 
912
1025
  const contractDetails = await getContractDetailsByUri(contractUri);
913
1026
 
@@ -915,7 +1028,37 @@ export const contractsRouter = t.router({
915
1028
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
916
1029
  }
917
1030
 
918
- if (await hasProfileConsentedToContract(profile, contractDetails.contract)) {
1031
+ if (
1032
+ contractDetails.contract.expiresAt?.trim() &&
1033
+ (!Number.isFinite(Date.parse(contractDetails.contract.expiresAt)) ||
1034
+ Date.parse(contractDetails.contract.expiresAt) <= Date.now())
1035
+ ) {
1036
+ throw new TRPCError({ code: 'CONFLICT', message: 'This contract has expired.' });
1037
+ }
1038
+ const isSmartResume =
1039
+ contractUri === environment.SMART_RESUME_CONTRACT_URI ||
1040
+ contractUri ===
1041
+ 'lc:network:network.learncard.com/trpc:contract:55b738f0-49f4-4b33-b6c1-afa99b605cd6';
1042
+ if (isSmartResume && !recipientToken) {
1043
+ throw new TRPCError({
1044
+ code: 'BAD_REQUEST',
1045
+ message: 'Missing recipientToken for SmartResume',
1046
+ });
1047
+ }
1048
+ const previousTerms = await getContractTermsForProfile(
1049
+ profile,
1050
+ contractDetails.contract
1051
+ );
1052
+ const retrySmartResume =
1053
+ isSmartResume &&
1054
+ Boolean(previousTerms?.smartResumeFingerprint) &&
1055
+ (previousTerms?.status === 'live' ||
1056
+ (previousTerms?.status === 'stale' && previousTerms.oneTime));
1057
+
1058
+ if (
1059
+ !retrySmartResume &&
1060
+ (await hasProfileConsentedToContract(profile, contractDetails.contract))
1061
+ ) {
919
1062
  try {
920
1063
  await upsertRequestedForRelationship(
921
1064
  contractDetails.contract.id,
@@ -930,10 +1073,6 @@ export const contractsRouter = t.router({
930
1073
  }
931
1074
 
932
1075
  if (!guardianIdentity) {
933
- const previousTerms = await getContractTermsForProfile(
934
- profile,
935
- contractDetails.contract
936
- );
937
1076
  if (previousTerms && (await hasGuardianApprovalHistory(previousTerms))) {
938
1077
  throw new TRPCError({
939
1078
  code: 'FORBIDDEN',
@@ -954,156 +1093,46 @@ export const contractsRouter = t.router({
954
1093
  );
955
1094
  }
956
1095
 
957
- let redirectUrl: string | undefined;
958
- // SmartResume handling
959
- const isSmartResume =
960
- contractUri === environment.SMART_RESUME_CONTRACT_URI ||
961
- contractUri ===
962
- 'lc:network:network.learncard.com/trpc:contract:55b738f0-49f4-4b33-b6c1-afa99b605cd6'; // hardcode for quick fix purposes
963
- if (isSmartResume) {
964
- if (!recipientToken) {
965
- throw new Error('Missing recipientToken for SmartResume');
966
- }
967
-
968
- const isProduction = !environment.IS_OFFLINE;
969
-
970
- const srUrl = isProduction
971
- ? 'https://my.smartresume.com/'
972
- : 'https://mystage.smartresume.com/';
973
- const clientId = environment.SMART_RESUME_CLIENT_ID;
974
- const accessKey = environment.SMART_RESUME_ACCESS_KEY;
975
-
976
- const accessTokenResponse = (await fetch(`${srUrl}api/v1/token`, {
977
- method: 'POST',
978
- headers: {
979
- 'Content-Type': 'application/x-www-form-urlencoded',
980
- Authorization: `Basic ${btoa(`${clientId}:${accessKey}`)}`,
981
- },
982
- body: new URLSearchParams({
983
- grant_type: 'client_credentials',
984
- scope: 'delete readonly replace',
985
- }),
986
- }).then(res => res.json())) as { access_token?: string };
987
-
988
- const accessToken = accessTokenResponse.access_token;
989
- if (!accessToken) throw new Error('Missing access_token for SmartResume');
990
-
991
- const parsedTerms = ConsentFlowTermsValidator.parse(terms);
992
-
993
- const categories = parsedTerms.read.credentials.categories;
994
- const categoryValues = Object.values(categories) as Array<{ shared?: string[] }>;
995
-
996
- const allSharedCredentialUris = [
997
- // filter out duplicates
998
- ...new Set(categoryValues.flatMap(({ shared }) => shared ?? [])),
999
- ];
1000
-
1001
- const resolvedCredentials = await Promise.all(
1002
- allSharedCredentialUris.map(async uri => {
1003
- try {
1004
- return await resolveUri(uri);
1005
- } catch (error) {
1006
- console.error(`Error resolving URI ${uri}:`, error);
1007
- return undefined;
1008
- }
1009
- })
1010
- );
1011
-
1012
- type ResolvedCredential = {
1013
- issuer?: string | { id: string };
1014
- id?: string;
1015
- boostCredential?: Record<string, unknown>;
1016
- } & Record<string, unknown>;
1017
-
1018
- const credentials = resolvedCredentials
1019
- .filter(
1020
- (cred): cred is ResolvedCredential =>
1021
- typeof cred === 'object' && cred !== null
1022
- )
1023
- .map(cred =>
1024
- cred.boostCredential && typeof cred.boostCredential === 'object'
1025
- ? ({ ...cred.boostCredential, id: cred.id } as ResolvedCredential) // unwrap credential, preserve id
1026
- : cred
1027
- );
1028
-
1029
- const transformedCredentials = credentials.map(cred => {
1030
- const issuer =
1031
- typeof cred.issuer === 'string'
1032
- ? { id: cred.issuer }
1033
- : cred.issuer || { id: '' };
1034
-
1035
- return {
1036
- ...cred,
1037
- issuer,
1038
- };
1039
- });
1040
-
1041
- const { name, email } = parsedTerms.read.personal;
1042
-
1043
- const body = JSON.stringify({
1044
- '@context': [
1045
- 'https://www.w3.org/ns/credentials/v2',
1046
- 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
1047
- 'https://w3id.org/security/suites/ed25519-2020/v1',
1048
- ],
1049
- recipienttoken: recipientToken,
1050
- recipient: {
1051
- id: ctx.user.did,
1052
- givenName: name && name !== 'Anonymous' ? name : '',
1053
- familyName: '', // this is neecessary in order for givenName to be respected
1054
- email: email && email !== 'anonymous@hidden.com' ? email : '',
1096
+ const fingerprint = isSmartResume
1097
+ ? smartResumeFingerprint({
1098
+ terms,
1099
+ expiresAt,
1100
+ oneTime,
1101
+ audienceVersion,
1102
+ recipientToken,
1103
+ expectedRequestId,
1104
+ })
1105
+ : undefined;
1106
+ // A completed one-time snapshot can be followed by a new reviewed decision.
1107
+ // Identical submissions still replay the completed upload instead.
1108
+ const newDecisionAfterOneTime =
1109
+ retrySmartResume &&
1110
+ previousTerms?.status === 'stale' &&
1111
+ previousTerms.oneTime &&
1112
+ previousTerms.smartResumePublicationStatus === 'succeeded' &&
1113
+ fingerprint !== previousTerms.smartResumeFingerprint;
1114
+ if (!retrySmartResume || newDecisionAfterOneTime)
1115
+ await consentToContract(
1116
+ profile,
1117
+ contractDetails,
1118
+ {
1119
+ terms,
1120
+ expiresAt,
1121
+ oneTime,
1122
+ audienceVersion,
1123
+ expectedRequestId,
1124
+ smartResumeFingerprint: fingerprint,
1125
+ guardianApproval: guardianIdentity
1126
+ ? {
1127
+ guardianProfileId: guardianIdentity.profileId,
1128
+ guardianDid: guardianIdentity.did,
1129
+ approvedAt: new Date().toISOString(),
1130
+ contractUpdatedAt: contractDetails.contract.updatedAt,
1131
+ }
1132
+ : undefined,
1055
1133
  },
1056
- credentials: transformedCredentials,
1057
- });
1058
-
1059
- try {
1060
- const response = await fetch(`${srUrl}api/v1/credentials`, {
1061
- method: 'POST',
1062
- headers: {
1063
- Authorization: `Bearer ${accessToken}`,
1064
- 'Content-Type': 'application/json',
1065
- },
1066
- body,
1067
- });
1068
-
1069
- if (!response.ok) {
1070
- throw new Error(`Error (${response.status}): ${await response.text()}`);
1071
- }
1072
-
1073
- const result = (await response.json()) as { redirect_url?: string };
1074
- redirectUrl = result.redirect_url;
1075
- } catch (error) {
1076
- console.error('Error uploading credentials to SmartResume:', error);
1077
- throw error;
1078
- }
1079
- }
1080
-
1081
- await consentToContract(
1082
- profile,
1083
- contractDetails,
1084
- {
1085
- terms,
1086
- expiresAt,
1087
- oneTime,
1088
- guardianApproval: guardianIdentity
1089
- ? {
1090
- guardianProfileId: guardianIdentity.profileId,
1091
- guardianDid: guardianIdentity.did,
1092
- approvedAt: new Date().toISOString(),
1093
- contractUpdatedAt: contractDetails.contract.updatedAt,
1094
- }
1095
- : undefined,
1096
- },
1097
- ctx.domain
1098
- );
1099
-
1100
- try {
1101
- await updateRequestedForStatusIfExists(
1102
- contractDetails.contract.id,
1103
- profile.profileId,
1104
- 'accepted'
1134
+ ctx.domain
1105
1135
  );
1106
- } catch {}
1107
1136
 
1108
1137
  const relationship = await getContractTermsForProfile(
1109
1138
  profile,
@@ -1117,6 +1146,18 @@ export const contractsRouter = t.router({
1117
1146
  });
1118
1147
  }
1119
1148
 
1149
+ const redirectUrl = isSmartResume
1150
+ ? await publishSmartResume({
1151
+ contractId: contractDetails.contract.id,
1152
+ profileId: profile.profileId,
1153
+ termsId: relationship.id,
1154
+ fingerprint: fingerprint!,
1155
+ audienceVersion,
1156
+ expectedRequestId,
1157
+ upload: acceptedTerms =>
1158
+ uploadSmartResume(acceptedTerms, ctx.user.did, recipientToken!),
1159
+ })
1160
+ : undefined;
1120
1161
  return { termsUri: constructUri('terms', relationship.id, ctx.domain), redirectUrl };
1121
1162
  }),
1122
1163
 
@@ -1178,9 +1219,14 @@ export const contractsRouter = t.router({
1178
1219
  ? { expiresAt: record.contract.expiresAt }
1179
1220
  : {}),
1180
1221
  autoBoosts: record.autoBoosts,
1222
+ audienceVersion: Number(record.contract.audienceVersion ?? 0),
1223
+ recipients: record.recipients.map(recipient =>
1224
+ publicContractRecipient(ctx.domain, recipient)
1225
+ ),
1181
1226
  },
1182
1227
  uri: constructUri('terms', record.terms.id, ctx.domain),
1183
1228
  terms: record.terms.terms,
1229
+ ...(record.terms.referral ? { referral: record.terms.referral } : {}),
1184
1230
  ...(record.terms.expiresAt ? { expiresAt: record.terms.expiresAt } : {}),
1185
1231
  ...(record.terms.oneTime ? { oneTime: record.terms.oneTime } : {}),
1186
1232
  consenter: updateDidForProfile(ctx.domain, profile),
@@ -1207,6 +1253,7 @@ export const contractsRouter = t.router({
1207
1253
  terms: ConsentFlowTermsValidator,
1208
1254
  expiresAt: z.string().optional(),
1209
1255
  oneTime: z.boolean().optional(),
1256
+ audienceVersion: z.number().int().nonnegative().optional(),
1210
1257
  })
1211
1258
  )
1212
1259
  .output(z.boolean())
@@ -1222,7 +1269,7 @@ export const contractsRouter = t.router({
1222
1269
  });
1223
1270
  }
1224
1271
 
1225
- const { uri, terms, expiresAt, oneTime } = input;
1272
+ const { uri, terms, expiresAt, oneTime, audienceVersion } = input;
1226
1273
 
1227
1274
  const decodedUri = decodeURIComponent(uri);
1228
1275
  const relationship = await getContractTermsByUri(decodedUri);
@@ -1271,6 +1318,7 @@ export const contractsRouter = t.router({
1271
1318
  terms,
1272
1319
  expiresAt,
1273
1320
  oneTime,
1321
+ audienceVersion,
1274
1322
  guardianApproval: guardianIdentity
1275
1323
  ? {
1276
1324
  guardianProfileId: guardianIdentity.profileId,
@@ -1396,7 +1444,7 @@ export const contractsRouter = t.router({
1396
1444
  });
1397
1445
  }
1398
1446
 
1399
- await Promise.all([withdrawTerms(relationship), deleteStorageForUri(uri)]);
1447
+ await Promise.all([withdrawTerms(relationship, ctx.domain), deleteStorageForUri(uri)]);
1400
1448
 
1401
1449
  return true;
1402
1450
  }),
@@ -1528,13 +1576,14 @@ export const contractsRouter = t.router({
1528
1576
  .input(
1529
1577
  z.object({
1530
1578
  termsUri: z.string(),
1579
+ audienceVersion: z.number().int().nonnegative().optional(),
1531
1580
  categories: z.record(z.string(), z.string().array()),
1532
1581
  })
1533
1582
  )
1534
1583
  .output(z.boolean())
1535
1584
  .mutation(async ({ ctx, input }) => {
1536
1585
  const { profile } = ctx.user;
1537
- const { termsUri, categories } = input;
1586
+ const { termsUri, categories, audienceVersion } = input;
1538
1587
 
1539
1588
  // Verify the terms exist and belong to this profile
1540
1589
  const relationship = await getContractTermsByUri(termsUri);
@@ -1604,7 +1653,12 @@ export const contractsRouter = t.router({
1604
1653
  }
1605
1654
 
1606
1655
  // Sync the credentials
1607
- const result = await syncCredentialsToContract(relationship, categories);
1656
+ const result = await syncCredentialsToContract(
1657
+ relationship,
1658
+ categories,
1659
+ audienceVersion,
1660
+ ctx.domain
1661
+ );
1608
1662
 
1609
1663
  return result;
1610
1664
  }),
@@ -1824,6 +1878,49 @@ export const contractsRouter = t.router({
1824
1878
  return true;
1825
1879
  }),
1826
1880
 
1881
+ sendContractRequest: profileRoute
1882
+ .meta({
1883
+ requiredScope: 'contracts:write',
1884
+ openapi: {
1885
+ protect: true,
1886
+ method: 'POST',
1887
+ path: '/consent-flow-contracts/request',
1888
+ tags: ['Contracts'],
1889
+ summary: 'Send an attributed contract request',
1890
+ },
1891
+ })
1892
+ .input(SendContractRequestValidator)
1893
+ .output(z.boolean())
1894
+ .mutation(async ({ ctx, input }) =>
1895
+ sendGenericContractRequest(input, ctx.user.profile, ctx.domain)
1896
+ ),
1897
+
1898
+ denyContractRequest: profileRoute
1899
+ .meta({
1900
+ requiredScope: 'contracts:write',
1901
+ openapi: {
1902
+ protect: true,
1903
+ method: 'POST',
1904
+ path: '/consent-flow-contracts/deny-request',
1905
+ tags: ['Contracts'],
1906
+ summary: 'Deny a pending contract request as its target',
1907
+ },
1908
+ })
1909
+ .input(z.object({ contractUri: z.string() }))
1910
+ .output(z.boolean())
1911
+ .mutation(async ({ ctx, input }) => {
1912
+ const contract = await getContractByUri(input.contractUri);
1913
+ if (!contract)
1914
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
1915
+ return decideGenericContractRequest({
1916
+ contract,
1917
+ targetProfileId: ctx.user.profile.profileId,
1918
+ actorProfileId: ctx.user.profile.profileId,
1919
+ status: 'denied',
1920
+ domain: ctx.domain,
1921
+ });
1922
+ }),
1923
+
1827
1924
  sendAiInsightsContractRequest: profileRoute
1828
1925
  .meta({
1829
1926
  openapi: {
@@ -1848,43 +1945,14 @@ export const contractsRouter = t.router({
1848
1945
  const { profile } = ctx.user;
1849
1946
  const { contractUri, targetProfileId, shareLink } = input;
1850
1947
 
1851
- const resolvedTargetProfileId = await getProfileIdFromString(
1948
+ const { contract: contractByUri, target: targetProfile } = await resolveContractRequest(
1949
+ contractUri,
1852
1950
  targetProfileId,
1853
- ctx.domain
1951
+ profile,
1952
+ ctx.domain,
1953
+ false
1854
1954
  );
1855
- if (!resolvedTargetProfileId) {
1856
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found' });
1857
- }
1858
-
1859
- const contractByUri = await getContractByUri(contractUri);
1860
-
1861
- if (!contractByUri)
1862
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
1863
-
1864
- const contract = await getConsentFlowContractById(contractByUri.id); // model instance
1865
-
1866
- if (!contract) {
1867
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
1868
- }
1869
-
1870
- const writers = await getWritersForContract(contractByUri);
1871
- const isAuthorized = writers.some(writer => writer.profileId === profile.profileId);
1872
-
1873
- if (!isAuthorized) {
1874
- throw new TRPCError({
1875
- code: 'UNAUTHORIZED',
1876
- message: 'You do not have permission to send requests for this contract.',
1877
- });
1878
- }
1879
-
1880
- const targetProfile = await getProfileByProfileId(resolvedTargetProfileId);
1881
-
1882
- if (!targetProfile) {
1883
- throw new TRPCError({
1884
- code: 'NOT_FOUND',
1885
- message: 'Target profile not found.',
1886
- });
1887
- }
1955
+ const resolvedTargetProfileId = targetProfile.profileId;
1888
1956
 
1889
1957
  try {
1890
1958
  await upsertRequestedForRelationship(
@@ -1894,6 +1962,7 @@ export const contractsRouter = t.router({
1894
1962
  null
1895
1963
  );
1896
1964
  } catch (error) {
1965
+ if (error instanceof TRPCError && error.code === 'CONFLICT') throw error;
1897
1966
  throw new TRPCError({
1898
1967
  code: 'BAD_REQUEST',
1899
1968
  message: 'Unable to send request',
@@ -2026,9 +2095,9 @@ export const contractsRouter = t.router({
2026
2095
  )
2027
2096
  .output(
2028
2097
  z.array(
2029
- z.object({
2098
+ ContractRequestFieldsValidator.extend({
2030
2099
  profile: LCNProfileValidator,
2031
- status: z.enum(['pending', 'accepted', 'denied']).nullable(),
2100
+ status: z.enum(['pending', 'accepted', 'denied', 'cancelled']).nullable(),
2032
2101
  readStatus: z.enum(['unseen', 'seen']).nullable().optional(),
2033
2102
  })
2034
2103
  )
@@ -2041,17 +2110,19 @@ export const contractsRouter = t.router({
2041
2110
 
2042
2111
  if (!contractByUri)
2043
2112
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
2044
- const writers = await getWritersForContract(contractByUri);
2045
- const isAuthorized = writers.some(w => w.profileId === profile.profileId);
2113
+ const isAuthorized = await canManageContractRequests(contractByUri, profile.profileId);
2046
2114
 
2047
- if (!isAuthorized) {
2115
+ if (
2116
+ !isAuthorized &&
2117
+ !(await canReadContractData(contractByUri.id, profile.profileId))
2118
+ ) {
2048
2119
  throw new TRPCError({
2049
2120
  code: 'UNAUTHORIZED',
2050
2121
  message: 'You do not have permissions to view requests made for this contract.',
2051
2122
  });
2052
2123
  }
2053
2124
 
2054
- const requests = await getRequestedForList(contractByUri.id);
2125
+ const requests = await getRequestedForList(contractByUri.id, profile.profileId);
2055
2126
 
2056
2127
  return Promise.all(
2057
2128
  requests.map(async request => {
@@ -2086,13 +2157,11 @@ export const contractsRouter = t.router({
2086
2157
  })
2087
2158
  )
2088
2159
  .output(
2089
- z
2090
- .object({
2091
- profile: LCNProfileValidator,
2092
- status: z.enum(['pending', 'accepted', 'denied']).nullable(),
2093
- readStatus: z.enum(['unseen', 'seen']).nullable().optional(),
2094
- })
2095
- .nullable()
2160
+ ContractRequestFieldsValidator.extend({
2161
+ profile: LCNProfileValidator,
2162
+ status: z.enum(['pending', 'accepted', 'denied', 'cancelled']).nullable(),
2163
+ readStatus: z.enum(['unseen', 'seen']).nullable().optional(),
2164
+ }).nullable()
2096
2165
  )
2097
2166
  .query(async ({ ctx, input }) => {
2098
2167
  const { profile } = ctx.user;
@@ -2119,10 +2188,9 @@ export const contractsRouter = t.router({
2119
2188
  const isCheckingOwnStatus = profile.profileId === resolvedTargetProfileId;
2120
2189
 
2121
2190
  if (!isCheckingOwnStatus) {
2122
- const writers = await getWritersForContract(contract);
2123
- const isAuthorized = writers.some(w => w.profileId === profile.profileId);
2191
+ const isAuthorized = await canManageContractRequests(contract, profile.profileId);
2124
2192
 
2125
- if (!isAuthorized) {
2193
+ if (!isAuthorized && !(await canReadContractData(contract.id, profile.profileId))) {
2126
2194
  throw new TRPCError({
2127
2195
  code: 'UNAUTHORIZED',
2128
2196
  message:
@@ -2131,7 +2199,11 @@ export const contractsRouter = t.router({
2131
2199
  }
2132
2200
  }
2133
2201
 
2134
- const requests = await getRequestedForForUser(contract.id, resolvedTargetProfileId);
2202
+ const requests = await getRequestedForForUser(
2203
+ contract.id,
2204
+ resolvedTargetProfileId,
2205
+ profile.profileId
2206
+ );
2135
2207
 
2136
2208
  if (!requests?.[0]) return null;
2137
2209
 
@@ -2154,7 +2226,7 @@ export const contractsRouter = t.router({
2154
2226
  tags: ['Contracts'],
2155
2227
  summary: 'Marks a contract request as seen',
2156
2228
  description:
2157
- 'Updates the read status of a contract request to "seen" for the specified target profile. Only contract writers are authorized to perform this action.',
2229
+ 'Updates the read status of a contract request to "seen" for the specified target profile. Targets can mark their existing requests seen. Legacy writer behavior is retained for legacy requests.',
2158
2230
  },
2159
2231
  })
2160
2232
  .input(
@@ -2181,6 +2253,31 @@ export const contractsRouter = t.router({
2181
2253
  if (!contractByUri)
2182
2254
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
2183
2255
 
2256
+ const request = await getStoredContractRequest(
2257
+ contractByUri.id,
2258
+ resolvedTargetProfileId
2259
+ );
2260
+ if (request?.requestId) {
2261
+ assertGenericRequestWriteScope(ctx.user.scope);
2262
+ if (profile.profileId !== resolvedTargetProfileId)
2263
+ throw new TRPCError({
2264
+ code: 'UNAUTHORIZED',
2265
+ message: 'Only the target can mark this request seen',
2266
+ });
2267
+ return markGenericContractRequestSeen(contractByUri.id, resolvedTargetProfileId);
2268
+ }
2269
+ if (profile.profileId === resolvedTargetProfileId) {
2270
+ if (!request)
2271
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Request not found' });
2272
+ await upsertRequestedForRelationship(
2273
+ contractByUri.id,
2274
+ resolvedTargetProfileId,
2275
+ undefined,
2276
+ 'seen'
2277
+ );
2278
+ return true;
2279
+ }
2280
+
2184
2281
  const writers = await getWritersForContract(contractByUri);
2185
2282
  const isAuthorized = writers.some(w => w.profileId === profile.profileId);
2186
2283
 
@@ -2217,7 +2314,7 @@ export const contractsRouter = t.router({
2217
2314
  tags: ['Contracts'],
2218
2315
  summary: 'Cancels/removes a contract request',
2219
2316
  description:
2220
- 'Removes a REQUESTED_FOR relationship, cancelling the request sent to the specified target profile. Only contract writers are authorized to perform this action.',
2317
+ 'Cancels a pending generic request while retaining its history. Owner/writers, the requesting recipient, or the target may cancel. Legacy requests retain deletion behavior.',
2221
2318
  },
2222
2319
  })
2223
2320
  .input(
@@ -2244,6 +2341,21 @@ export const contractsRouter = t.router({
2244
2341
  if (!contractByUri)
2245
2342
  throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
2246
2343
 
2344
+ const request = await getStoredContractRequest(
2345
+ contractByUri.id,
2346
+ resolvedTargetProfileId
2347
+ );
2348
+ if (request?.requestId) {
2349
+ assertGenericRequestWriteScope(ctx.user.scope);
2350
+ return decideGenericContractRequest({
2351
+ contract: contractByUri,
2352
+ targetProfileId: resolvedTargetProfileId,
2353
+ actorProfileId: profile.profileId,
2354
+ status: 'cancelled',
2355
+ domain: ctx.domain,
2356
+ });
2357
+ }
2358
+
2247
2359
  const isDenyingRequest = profile.profileId === resolvedTargetProfileId;
2248
2360
 
2249
2361
  if (!isDenyingRequest) {
@@ -2302,10 +2414,15 @@ export const contractsRouter = t.router({
2302
2414
  )
2303
2415
  .output(
2304
2416
  z.array(
2305
- z.object({
2306
- contract: ConsentFlowContractValidator.extend({ uri: z.string() }),
2417
+ ContractRequestFieldsValidator.extend({
2418
+ contract: ConsentFlowContractValidator.extend({
2419
+ uri: z.string(),
2420
+ name: z.string().optional(),
2421
+ image: z.string().optional(),
2422
+ description: z.string().optional(),
2423
+ }),
2307
2424
  profile: LCNProfileValidator,
2308
- status: z.enum(['pending', 'accepted', 'denied']).nullable(),
2425
+ status: z.enum(['pending', 'accepted', 'denied', 'cancelled']).nullable(),
2309
2426
  readStatus: z.enum(['unseen', 'seen']).nullable().optional(),
2310
2427
  })
2311
2428
  )
@@ -2345,7 +2462,10 @@ export const contractsRouter = t.router({
2345
2462
  sanitizeProfileForTier(updatedProfile, 'authenticated')
2346
2463
  ),
2347
2464
  contract: {
2348
- ...request.contract,
2465
+ ...inflateObject(request.contract).contract,
2466
+ name: request.contract.name,
2467
+ image: request.contract.image,
2468
+ description: request.contract.description,
2349
2469
  uri: constructUri('contract', request.contract.id, ctx.domain),
2350
2470
  },
2351
2471
  };
@@ -2372,7 +2492,7 @@ export const contractsRouter = t.router({
2372
2492
  )
2373
2493
  .output(
2374
2494
  z.array(
2375
- z.object({
2495
+ ContractRequestFieldsValidator.extend({
2376
2496
  profile: LCNProfileValidator,
2377
2497
  status: z.enum(['pending', 'accepted', 'denied']).nullable(),
2378
2498
  readStatus: z.enum(['unseen', 'seen']).nullable().optional(),