@learncard/network-brain-service 4.1.2 → 5.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 (375) hide show
  1. package/.env.example +14 -0
  2. package/CHANGELOG.md +164 -0
  3. package/README.md +27 -1
  4. package/bootstrapLambda.test.ts +70 -32
  5. package/config/config.dev.json +1 -0
  6. package/config/config.example.json +2 -1
  7. package/config/config.production.json +1 -0
  8. package/config/config.scouts.dev.json +1 -0
  9. package/config/config.scouts.production.json +1 -0
  10. package/consentFlowDeadlineLambda.test.ts +108 -0
  11. package/consentFlowDeadlineLambda.ts +18 -0
  12. package/consentFlowDeadlineLambdaApp.ts +14 -0
  13. package/dist/accesslayer/boost/create.d.ts +8 -2
  14. package/dist/accesslayer/boost/create.d.ts.map +1 -1
  15. package/dist/accesslayer/campaign/audience.d.ts +4 -4
  16. package/dist/accesslayer/campaign/audience.d.ts.map +1 -1
  17. package/dist/accesslayer/campaign/create.d.ts +1 -1
  18. package/dist/accesslayer/campaign/create.d.ts.map +1 -1
  19. package/dist/accesslayer/campaign/delivery.d.ts +5 -0
  20. package/dist/accesslayer/campaign/delivery.d.ts.map +1 -0
  21. package/dist/accesslayer/campaign/dispatch.d.ts +5 -0
  22. package/dist/accesslayer/campaign/dispatch.d.ts.map +1 -0
  23. package/dist/accesslayer/campaign/dispatch.test.d.ts +2 -0
  24. package/dist/accesslayer/campaign/dispatch.test.d.ts.map +1 -0
  25. package/dist/accesslayer/campaign/email.d.ts +10 -0
  26. package/dist/accesslayer/campaign/email.d.ts.map +1 -0
  27. package/dist/accesslayer/campaign/email.test.d.ts +2 -0
  28. package/dist/accesslayer/campaign/email.test.d.ts.map +1 -0
  29. package/dist/accesslayer/campaign/preferences.d.ts +5 -0
  30. package/dist/accesslayer/campaign/preferences.d.ts.map +1 -0
  31. package/dist/accesslayer/campaign/read.d.ts.map +1 -1
  32. package/dist/accesslayer/campaign/test.d.ts +5 -0
  33. package/dist/accesslayer/campaign/test.d.ts.map +1 -0
  34. package/dist/accesslayer/campaign/test.test.d.ts +2 -0
  35. package/dist/accesslayer/campaign/test.test.d.ts.map +1 -0
  36. package/dist/accesslayer/campaign/update.d.ts.map +1 -1
  37. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  38. package/dist/accesslayer/consentflowcontract/delete.d.ts +1 -1
  39. package/dist/accesslayer/consentflowcontract/delete.d.ts.map +1 -1
  40. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  41. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +2 -1
  42. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  43. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +19 -4
  44. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  45. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +6 -1
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  47. package/dist/accesslayer/consentflowcontract/versions.d.ts +68 -0
  48. package/dist/accesslayer/consentflowcontract/versions.d.ts.map +1 -0
  49. package/dist/accesslayer/credential/create.d.ts +3 -1
  50. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  51. package/dist/accesslayer/credential/delete.d.ts +1 -0
  52. package/dist/accesslayer/credential/delete.d.ts.map +1 -1
  53. package/dist/accesslayer/credential/relationships/create.d.ts +15 -3
  54. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  55. package/dist/accesslayer/credential/relationships/read.d.ts +2 -0
  56. package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
  57. package/dist/accesslayer/credential/relationships/update.d.ts.map +1 -1
  58. package/dist/accesslayer/credential-refresh/read.d.ts +1 -0
  59. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  60. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  61. package/dist/accesslayer/inbox-credential/create.d.ts +1 -0
  62. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  63. package/dist/accesslayer/inbox-credential/refresh.d.ts +7 -0
  64. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -1
  65. package/dist/app.d.ts +11 -1
  66. package/dist/app.d.ts.map +1 -1
  67. package/dist/cache/app-store.caches.d.ts +61 -1
  68. package/dist/cache/app-store.caches.d.ts.map +1 -1
  69. package/dist/campaign-unsubscribe.d.ts +7 -0
  70. package/dist/campaign-unsubscribe.d.ts.map +1 -0
  71. package/dist/campaign-unsubscribe.test.d.ts +2 -0
  72. package/dist/campaign-unsubscribe.test.d.ts.map +1 -0
  73. package/dist/config/environment.d.ts +42 -0
  74. package/dist/config/environment.d.ts.map +1 -1
  75. package/dist/config/stageConfig.d.ts +4 -0
  76. package/dist/config/stageConfig.d.ts.map +1 -1
  77. package/dist/credential-refresh.d.ts.map +1 -1
  78. package/dist/helpers/boost.helpers.d.ts +4 -1
  79. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  80. package/dist/helpers/campaign-app-origin.helpers.d.ts +2 -0
  81. package/dist/helpers/campaign-app-origin.helpers.d.ts.map +1 -0
  82. package/dist/helpers/campaign-email.helpers.d.ts +13 -0
  83. package/dist/helpers/campaign-email.helpers.d.ts.map +1 -0
  84. package/dist/helpers/campaign-notifications.helpers.d.ts +7 -0
  85. package/dist/helpers/campaign-notifications.helpers.d.ts.map +1 -0
  86. package/dist/helpers/campaign-queue.helpers.d.ts +7 -0
  87. package/dist/helpers/campaign-queue.helpers.d.ts.map +1 -0
  88. package/dist/helpers/campaign-queue.helpers.test.d.ts +2 -0
  89. package/dist/helpers/campaign-queue.helpers.test.d.ts.map +1 -0
  90. package/dist/helpers/campaign-unsubscribe-page.d.ts +2 -0
  91. package/dist/helpers/campaign-unsubscribe-page.d.ts.map +1 -0
  92. package/dist/helpers/campaign-unsubscribe.helpers.d.ts +7 -0
  93. package/dist/helpers/campaign-unsubscribe.helpers.d.ts.map +1 -0
  94. package/dist/helpers/campaign-webhook.helpers.test.d.ts +2 -0
  95. package/dist/helpers/campaign-webhook.helpers.test.d.ts.map +1 -0
  96. package/dist/helpers/consentflow-deadline.helpers.d.ts +5 -0
  97. package/dist/helpers/consentflow-deadline.helpers.d.ts.map +1 -0
  98. package/dist/helpers/consentflow-legacy-migration.helpers.d.ts +2 -0
  99. package/dist/helpers/consentflow-legacy-migration.helpers.d.ts.map +1 -0
  100. package/dist/helpers/contract-events.helpers.d.ts.map +1 -1
  101. package/dist/helpers/contract.helpers.d.ts +1 -1
  102. package/dist/helpers/contract.helpers.d.ts.map +1 -1
  103. package/dist/helpers/credential-display.helpers.d.ts +5 -0
  104. package/dist/helpers/credential-display.helpers.d.ts.map +1 -0
  105. package/dist/helpers/credential-display.helpers.test.d.ts +2 -0
  106. package/dist/helpers/credential-display.helpers.test.d.ts.map +1 -0
  107. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -1
  108. package/dist/helpers/credential-refresh-materiality.helpers.d.ts +4 -0
  109. package/dist/helpers/credential-refresh-materiality.helpers.d.ts.map +1 -1
  110. package/dist/helpers/credential-refresh.helpers.d.ts +30 -1
  111. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  112. package/dist/helpers/credential-status-mutation.helpers.d.ts +5 -0
  113. package/dist/helpers/credential-status-mutation.helpers.d.ts.map +1 -0
  114. package/dist/helpers/credential-status-mutation.helpers.test.d.ts +2 -0
  115. package/dist/helpers/credential-status-mutation.helpers.test.d.ts.map +1 -0
  116. package/dist/helpers/credential.helpers.d.ts +1 -1
  117. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  118. package/dist/helpers/inbox-refresh.helpers.d.ts +4 -2
  119. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -1
  120. package/dist/helpers/inbox.helpers.d.ts +1 -0
  121. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  122. package/dist/helpers/learnCard.helpers.d.ts +1 -0
  123. package/dist/helpers/learnCard.helpers.d.ts.map +1 -1
  124. package/dist/helpers/managed-status.helpers.d.ts +7 -0
  125. package/dist/helpers/managed-status.helpers.d.ts.map +1 -0
  126. package/dist/helpers/managed-status.helpers.test.d.ts +2 -0
  127. package/dist/helpers/managed-status.helpers.test.d.ts.map +1 -0
  128. package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts +2 -0
  129. package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts.map +1 -0
  130. package/dist/helpers/notification-webhook-fetch.helpers.d.ts +13 -0
  131. package/dist/helpers/notification-webhook-fetch.helpers.d.ts.map +1 -0
  132. package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts +2 -0
  133. package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts.map +1 -0
  134. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  135. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  136. package/dist/helpers/refresh-send-intent.helpers.d.ts +13 -5
  137. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -1
  138. package/dist/helpers/refresh-send-intent.helpers.test.d.ts +2 -0
  139. package/dist/helpers/refresh-send-intent.helpers.test.d.ts.map +1 -0
  140. package/dist/helpers/send-contract.helpers.d.ts +11 -0
  141. package/dist/helpers/send-contract.helpers.d.ts.map +1 -0
  142. package/dist/helpers/status-list.helpers.d.ts +12 -1
  143. package/dist/helpers/status-list.helpers.d.ts.map +1 -1
  144. package/dist/index.js +2 -2
  145. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  146. package/dist/models/ConsentFlowContractVersion.d.ts +12 -0
  147. package/dist/models/ConsentFlowContractVersion.d.ts.map +1 -0
  148. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  149. package/dist/models/Credential.d.ts +2 -0
  150. package/dist/models/Credential.d.ts.map +1 -1
  151. package/dist/models/CredentialRefresh.d.ts +5 -0
  152. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  153. package/dist/models/Integration.d.ts +1 -1
  154. package/dist/models/Profile.d.ts.map +1 -1
  155. package/dist/models/campaign-constraints.d.ts.map +1 -1
  156. package/dist/models/index.d.ts +1 -0
  157. package/dist/models/index.d.ts.map +1 -1
  158. package/dist/openapi.d.ts.map +1 -1
  159. package/dist/publish-metafile.json +1 -1
  160. package/dist/routes/activity.d.ts +15 -5
  161. package/dist/routes/activity.d.ts.map +1 -1
  162. package/dist/routes/app-store.d.ts +15 -5
  163. package/dist/routes/app-store.d.ts.map +1 -1
  164. package/dist/routes/auth-grants.d.ts +12 -2
  165. package/dist/routes/auth-grants.d.ts.map +1 -1
  166. package/dist/routes/bench.d.ts +11 -1
  167. package/dist/routes/bench.d.ts.map +1 -1
  168. package/dist/routes/boosts.d.ts +70 -15
  169. package/dist/routes/boosts.d.ts.map +1 -1
  170. package/dist/routes/campaigns.d.ts +183 -51
  171. package/dist/routes/campaigns.d.ts.map +1 -1
  172. package/dist/routes/claim-hooks.d.ts +11 -1
  173. package/dist/routes/claim-hooks.d.ts.map +1 -1
  174. package/dist/routes/contact-methods.d.ts +11 -1
  175. package/dist/routes/contact-methods.d.ts.map +1 -1
  176. package/dist/routes/contracts.d.ts +374 -8
  177. package/dist/routes/contracts.d.ts.map +1 -1
  178. package/dist/routes/credential-input-privacy.test.d.ts +2 -0
  179. package/dist/routes/credential-input-privacy.test.d.ts.map +1 -0
  180. package/dist/routes/credential-refreshes.d.ts +210 -1
  181. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  182. package/dist/routes/credential-share-previews.d.ts +11 -1
  183. package/dist/routes/credential-share-previews.d.ts.map +1 -1
  184. package/dist/routes/credentials.d.ts +132 -3
  185. package/dist/routes/credentials.d.ts.map +1 -1
  186. package/dist/routes/did-metadata.d.ts +11 -1
  187. package/dist/routes/did-metadata.d.ts.map +1 -1
  188. package/dist/routes/federation.d.ts +11 -1
  189. package/dist/routes/federation.d.ts.map +1 -1
  190. package/dist/routes/inbox.d.ts +54 -19
  191. package/dist/routes/inbox.d.ts.map +1 -1
  192. package/dist/routes/index.d.ts +100 -1
  193. package/dist/routes/index.d.ts.map +1 -1
  194. package/dist/routes/integrations.d.ts +14 -4
  195. package/dist/routes/integrations.d.ts.map +1 -1
  196. package/dist/routes/presentations.d.ts +11 -1
  197. package/dist/routes/presentations.d.ts.map +1 -1
  198. package/dist/routes/profile-manager.d.ts +11 -1
  199. package/dist/routes/profile-manager.d.ts.map +1 -1
  200. package/dist/routes/profiles.d.ts +11 -1
  201. package/dist/routes/profiles.d.ts.map +1 -1
  202. package/dist/routes/public-share-links.d.ts +22 -2
  203. package/dist/routes/public-share-links.d.ts.map +1 -1
  204. package/dist/routes/share-links.d.ts +36 -16
  205. package/dist/routes/share-links.d.ts.map +1 -1
  206. package/dist/routes/skill-frameworks.d.ts +11 -1
  207. package/dist/routes/skill-frameworks.d.ts.map +1 -1
  208. package/dist/routes/skills.d.ts +11 -1
  209. package/dist/routes/skills.d.ts.map +1 -1
  210. package/dist/routes/storage.d.ts +37 -23
  211. package/dist/routes/storage.d.ts.map +1 -1
  212. package/dist/routes/test.d.ts +79 -3
  213. package/dist/routes/test.d.ts.map +1 -1
  214. package/dist/routes/utilities.d.ts +11 -1
  215. package/dist/routes/utilities.d.ts.map +1 -1
  216. package/dist/routes/workflows.d.ts +11 -1
  217. package/dist/routes/workflows.d.ts.map +1 -1
  218. package/dist/services/delivery/adapters/postmark.adapter.d.ts +3 -0
  219. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  220. package/dist/services/delivery/delivery.service.d.ts +10 -0
  221. package/dist/services/delivery/delivery.service.d.ts.map +1 -1
  222. package/dist/types/activity.d.ts +3 -3
  223. package/dist/types/app-manifest.d.ts +10 -10
  224. package/dist/types/campaign.d.ts +13 -1
  225. package/dist/types/campaign.d.ts.map +1 -1
  226. package/dist/types/consentflowcontract.d.ts +175 -7
  227. package/dist/types/consentflowcontract.d.ts.map +1 -1
  228. package/dist/types/credential-refresh.d.ts +27 -0
  229. package/dist/types/credential-refresh.d.ts.map +1 -1
  230. package/dist/types/credential.d.ts +5 -0
  231. package/dist/types/credential.d.ts.map +1 -1
  232. package/dist/types/integration.d.ts +3 -3
  233. package/lambda.ts +44 -14
  234. package/lambdaApp.ts +20 -0
  235. package/package.json +20 -19
  236. package/project.json +5 -0
  237. package/scripts/migrate-consent-flow-versions.ts +9 -0
  238. package/scripts/report-send-template-exposure.ts +47 -0
  239. package/scripts/verify-python-contracts.ts +16 -1
  240. package/serverless-local.yml +15 -2
  241. package/serverless.function-env.cjs +4 -0
  242. package/serverless.local.yml +12 -0
  243. package/serverless.yml +48 -3
  244. package/src/accesslayer/boost/create.ts +62 -7
  245. package/src/accesslayer/campaign/README.md +275 -6
  246. package/src/accesslayer/campaign/audience.ts +39 -7
  247. package/src/accesslayer/campaign/create.ts +4 -2
  248. package/src/accesslayer/campaign/delivery.ts +66 -0
  249. package/src/accesslayer/campaign/dispatch.test.ts +59 -0
  250. package/src/accesslayer/campaign/dispatch.ts +172 -0
  251. package/src/accesslayer/campaign/email.test.ts +73 -0
  252. package/src/accesslayer/campaign/email.ts +91 -0
  253. package/src/accesslayer/campaign/preferences.ts +53 -0
  254. package/src/accesslayer/campaign/read.ts +3 -1
  255. package/src/accesslayer/campaign/state.ts +1 -1
  256. package/src/accesslayer/campaign/test.test.ts +31 -0
  257. package/src/accesslayer/campaign/test.ts +75 -0
  258. package/src/accesslayer/campaign/update.ts +15 -4
  259. package/src/accesslayer/consentflowcontract/create.ts +20 -1
  260. package/src/accesslayer/consentflowcontract/delete.ts +121 -3
  261. package/src/accesslayer/consentflowcontract/read.ts +15 -9
  262. package/src/accesslayer/consentflowcontract/relationships/create.ts +70 -36
  263. package/src/accesslayer/consentflowcontract/relationships/read.ts +305 -73
  264. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +1 -1
  265. package/src/accesslayer/consentflowcontract/relationships/update.ts +269 -98
  266. package/src/accesslayer/consentflowcontract/versions.ts +790 -0
  267. package/src/accesslayer/credential/create.ts +33 -9
  268. package/src/accesslayer/credential/delete.ts +45 -0
  269. package/src/accesslayer/credential/relationships/create.ts +102 -47
  270. package/src/accesslayer/credential/relationships/update.ts +8 -116
  271. package/src/accesslayer/credential-refresh/read.ts +26 -2
  272. package/src/accesslayer/credential-refresh/update.ts +6 -2
  273. package/src/accesslayer/inbox-credential/create.ts +7 -1
  274. package/src/accesslayer/inbox-credential/refresh.ts +14 -4
  275. package/src/campaign-unsubscribe.test.ts +158 -0
  276. package/src/campaign-unsubscribe.ts +86 -0
  277. package/src/config/environment.test.ts +52 -0
  278. package/src/config/environment.ts +58 -0
  279. package/src/config/serverlessFunctionEnv.test.ts +59 -0
  280. package/src/credential-refresh.ts +1 -0
  281. package/src/docker-entry.ts +26 -1
  282. package/src/helpers/boost-storage.helpers.test.ts +107 -41
  283. package/src/helpers/boost.helpers.ts +88 -40
  284. package/src/helpers/campaign-app-origin.helpers.ts +25 -0
  285. package/src/helpers/campaign-email.helpers.ts +113 -0
  286. package/src/helpers/campaign-notifications.helpers.ts +127 -0
  287. package/src/helpers/campaign-queue.helpers.test.ts +442 -0
  288. package/src/helpers/campaign-queue.helpers.ts +122 -0
  289. package/src/helpers/campaign-unsubscribe-page.ts +107 -0
  290. package/src/helpers/campaign-unsubscribe.helpers.ts +56 -0
  291. package/src/helpers/campaign-webhook.helpers.test.ts +137 -0
  292. package/src/helpers/consentflow-deadline.helpers.ts +22 -0
  293. package/src/helpers/consentflow-legacy-migration.helpers.ts +179 -0
  294. package/src/helpers/contract-events.helpers.ts +35 -3
  295. package/src/helpers/contract-requests.helpers.ts +1 -1
  296. package/src/helpers/contract.helpers.ts +5 -1
  297. package/src/helpers/credential-display.helpers.test.ts +68 -0
  298. package/src/helpers/credential-display.helpers.ts +15 -0
  299. package/src/helpers/credential-refresh-email.helpers.ts +5 -12
  300. package/src/helpers/credential-refresh-materiality.helpers.test.ts +138 -1
  301. package/src/helpers/credential-refresh-materiality.helpers.ts +62 -1
  302. package/src/helpers/credential-refresh-proof.helpers.test.ts +1 -1
  303. package/src/helpers/credential-refresh-proof.helpers.ts +2 -2
  304. package/src/helpers/credential-refresh.helpers.ts +1166 -376
  305. package/src/helpers/credential-status-mutation.helpers.test.ts +155 -0
  306. package/src/helpers/credential-status-mutation.helpers.ts +166 -0
  307. package/src/helpers/credential.helpers.ts +19 -11
  308. package/src/helpers/finalize-inbox.helpers.ts +1 -1
  309. package/src/helpers/inbox-refresh.helpers.ts +34 -8
  310. package/src/helpers/inbox.helpers.ts +2 -0
  311. package/src/helpers/learnCard.helpers.ts +23 -0
  312. package/src/helpers/managed-status.helpers.test.ts +101 -0
  313. package/src/helpers/managed-status.helpers.ts +147 -0
  314. package/src/helpers/notification-delivery-boundary.helpers.test.ts +114 -0
  315. package/src/helpers/notification-webhook-fetch.helpers.test.ts +228 -0
  316. package/src/helpers/notification-webhook-fetch.helpers.ts +166 -0
  317. package/src/helpers/notificationQueue.helpers.ts +5 -0
  318. package/src/helpers/notifications.helpers.ts +19 -12
  319. package/src/helpers/refresh-send-intent.helpers.test.ts +60 -0
  320. package/src/helpers/refresh-send-intent.helpers.ts +81 -18
  321. package/src/helpers/send-contract.helpers.ts +36 -0
  322. package/src/helpers/status-list.helpers.test.ts +29 -12
  323. package/src/helpers/status-list.helpers.ts +329 -20
  324. package/src/models/ConsentFlowContract.ts +3 -0
  325. package/src/models/ConsentFlowContractVersion.ts +50 -0
  326. package/src/models/ConsentFlowTerms.ts +4 -0
  327. package/src/models/Credential.ts +2 -0
  328. package/src/models/CredentialRefresh.ts +2 -0
  329. package/src/models/Profile.ts +2 -0
  330. package/src/models/campaign-constraints.ts +13 -8
  331. package/src/models/index.ts +2 -0
  332. package/src/openapi.ts +21 -0
  333. package/src/routes/app-store.ts +2 -1
  334. package/src/routes/boosts.ts +219 -137
  335. package/src/routes/campaigns.ts +98 -19
  336. package/src/routes/contracts.ts +433 -115
  337. package/src/routes/credential-input-privacy.test.ts +92 -0
  338. package/src/routes/credential-refreshes.ts +273 -2
  339. package/src/routes/federation.ts +7 -2
  340. package/src/routes/inbox.ts +6 -1
  341. package/src/routes/index.ts +31 -2
  342. package/src/routes/workflows.ts +61 -46
  343. package/src/services/delivery/adapters/postmark.adapter.test.ts +70 -1
  344. package/src/services/delivery/adapters/postmark.adapter.ts +84 -11
  345. package/src/services/delivery/delivery.service.ts +4 -0
  346. package/src/types/campaign.ts +14 -1
  347. package/src/types/consentflowcontract.ts +43 -2
  348. package/src/types/credential-refresh.ts +11 -1
  349. package/src/types/credential.ts +2 -0
  350. package/test/boost-status-cleanup.spec.ts +123 -0
  351. package/test/campaigns-email.unit.spec.ts +268 -0
  352. package/test/campaigns.spec.ts +804 -30
  353. package/test/consent-autoboost-idempotency.spec.ts +432 -0
  354. package/test/consent-data-boundaries.spec.ts +322 -26
  355. package/test/consent-legacy-migration.spec.ts +315 -0
  356. package/test/consent-recipients.spec.ts +7 -7
  357. package/test/consent-version-updates.spec.ts +761 -0
  358. package/test/consentflow.spec.ts +698 -226
  359. package/test/contract-requests.spec.ts +55 -9
  360. package/test/credential-refresh-allocation.spec.ts +212 -14
  361. package/test/credential-refresh-email.spec.ts +6 -10
  362. package/test/credential-refresh-endpoint.spec.ts +3 -0
  363. package/test/credential-refresh-publication.spec.ts +52 -15
  364. package/test/credential-storage.neo4j.spec.ts +33 -6
  365. package/test/helpers/legacy-autoboost.ts +48 -0
  366. package/test/holder-mode-send.spec.ts +710 -0
  367. package/test/inbox-refresh.spec.ts +105 -0
  368. package/test/inbox-security.e2e.ts +18 -22
  369. package/test/managed-status-revocation.spec.ts +970 -0
  370. package/test/notifications-worker.spec.ts +31 -25
  371. package/test/refresh-send.spec.ts +334 -5
  372. package/test/smart-resume-consent.spec.ts +1 -1
  373. package/test/verification/campaigns.ts +40 -0
  374. package/vitest.campaigns.config.ts +2 -0
  375. package/vitest.config.ts +14 -0
@@ -37,6 +37,7 @@ import {
37
37
  SigningAuthority,
38
38
  } from '@models';
39
39
  import { testUnsignedBoost } from './helpers/send';
40
+ import { seedLegacyAutoBoost } from './helpers/legacy-autoboost';
40
41
  import { BindParam, QueryBuilder } from 'neogma';
41
42
  import {
42
43
  getContractTermsByUri,
@@ -44,14 +45,33 @@ import {
44
45
  hasGuardianApprovalHistory,
45
46
  } from '@accesslayer/consentflowcontract/relationships/read';
46
47
  import { consentToContract } from '@accesslayer/consentflowcontract/relationships/create';
47
- import { updateTerms } from '@accesslayer/consentflowcontract/relationships/update';
48
48
 
49
+ import {
50
+ getConsentFlowContractVersionRecord,
51
+ updateConsentFlowContractVersion,
52
+ } from '@accesslayer/consentflowcontract/versions';
53
+ import { neogma } from '@instance';
54
+ import { migrateLegacyConsentFlowContracts } from '../src/helpers/consentflow-legacy-migration.helpers';
55
+ import { constructUri, getIdFromUri } from '@helpers/uri.helpers';
56
+ import type { ConsentFlowTerms as ConsentFlowTermsType } from '@learncard/types';
49
57
  const noAuthClient = getClient();
50
58
  let userA: Awaited<ReturnType<typeof getUser>>;
51
59
  let userB: Awaited<ReturnType<typeof getUser>>;
52
60
  let userC: Awaited<ReturnType<typeof getUser>>;
53
61
  let userD: Awaited<ReturnType<typeof getUser>>;
54
62
 
63
+ const seedStoredContractExpiry = async (
64
+ contractId: string,
65
+ version: number,
66
+ expiresAt: string | null
67
+ ) => {
68
+ await neogma.queryRunner.run(
69
+ `MATCH (contract:ConsentFlowContract {id:$contractId})-[:HAS_VERSION]->(accepted:ConsentFlowContractVersion {version:$version})
70
+ SET contract.expiresAt = $expiresAt, accepted.expiresAt = $expiresAt`,
71
+ { contractId, version, expiresAt }
72
+ );
73
+ };
74
+
55
75
  describe('Consent Flow Contracts', () => {
56
76
  beforeAll(async () => {
57
77
  userA = await getUser();
@@ -112,6 +132,90 @@ describe('Consent Flow Contracts', () => {
112
132
  expect(result).toBeDefined();
113
133
  expect(typeof result).toBe('string');
114
134
  });
135
+ it('rejects a signing result when consent is suspended while issuance is in flight', async () => {
136
+ await userA.clients.fullAuth.profile.registerSigningAuthority({
137
+ endpoint: 'http://localhost:5000/api',
138
+ name: 'mysa',
139
+ did: 'did:key:z6MkitsQTk2GDNYXAFckVcQHtC68S9j9ruVFYWrixM6RG5Mw',
140
+ });
141
+ const boostUri = await userA.clients.fullAuth.boost.createBoost({
142
+ credential: testUnsignedBoost,
143
+ category: 'Achievement',
144
+ name: 'Delayed signing boost',
145
+ });
146
+ const contractUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
147
+ contract: normalContract,
148
+ name: 'Delayed signing contract',
149
+ });
150
+ const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
151
+ contractUri,
152
+ terms: normalFullTerms,
153
+ });
154
+ const userBProfile = await userB.clients.fullAuth.profile.getProfile();
155
+ let announceStarted!: () => void;
156
+ let releaseSigning!: () => void;
157
+ const started = new Promise<void>(resolve => {
158
+ announceStarted = resolve;
159
+ });
160
+ const signingGate = new Promise<void>(resolve => {
161
+ releaseSigning = resolve;
162
+ });
163
+ const issueCredential = signingAuthorityMocks.issueCredential.getMockImplementation();
164
+ if (!issueCredential) throw new Error('Real signer was not initialized');
165
+ signingAuthorityMocks.issueCredential.mockImplementation(async (...args) => {
166
+ announceStarted();
167
+ await signingGate;
168
+ return issueCredential(...args);
169
+ });
170
+
171
+ try {
172
+ const write =
173
+ userA.clients.fullAuth.contracts.writeCredentialToContractViaSigningAuthority({
174
+ did: userBProfile!.did,
175
+ contractUri,
176
+ boostUri,
177
+ signingAuthority: { endpoint: 'http://localhost:5000/api', name: 'MySA' },
178
+ });
179
+ await started;
180
+ const changedContract = {
181
+ ...normalContract,
182
+ read: {
183
+ ...normalContract.read,
184
+ credentials: {
185
+ ...normalContract.read.credentials,
186
+ categories: {
187
+ ...normalContract.read.credentials.categories,
188
+ Transcript: { required: true },
189
+ },
190
+ },
191
+ },
192
+ };
193
+ await userA.clients.fullAuth.contracts.updateConsentFlowContract({
194
+ uri: contractUri,
195
+ changes: { contract: changedContract },
196
+ reason: 'We need transcripts to personalize your progress report.',
197
+ });
198
+ releaseSigning();
199
+ await expect(write).rejects.toMatchObject({
200
+ code: 'FORBIDDEN',
201
+ });
202
+ const terms = await getContractTermsByUri(termsUri);
203
+ expect(terms).toBeDefined();
204
+ const transactions = await getTransactionsForTerms(terms!.terms.id, { limit: 100 });
205
+ expect(transactions.some(transaction => transaction.action === 'write')).toBe(
206
+ false
207
+ );
208
+ const deliveries = await userB.clients.fullAuth.contracts.getCredentialsForContract(
209
+ {
210
+ termsUri,
211
+ }
212
+ );
213
+ expect(deliveries.records).toEqual([]);
214
+ } finally {
215
+ releaseSigning();
216
+ signingAuthorityMocks.issueCredential.mockImplementation(issueCredential);
217
+ }
218
+ });
115
219
  });
116
220
 
117
221
  describe('createConsentFlowContract', () => {
@@ -156,6 +260,75 @@ describe('Consent Flow Contracts', () => {
156
260
  })
157
261
  ).resolves.not.toThrow();
158
262
  });
263
+ it('persists immutable versions and leaves dry runs untouched', async () => {
264
+ const uri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
265
+ contract: minimalContract,
266
+ name: 'Versioned test contract',
267
+ });
268
+ const id = getIdFromUri(uri);
269
+ const initial = await getConsentFlowContractVersionRecord(id, 1);
270
+ expect(initial?.name).toBe('Versioned test contract');
271
+
272
+ const changes = {
273
+ contract: {
274
+ write: {
275
+ credentials: {
276
+ categories: {
277
+ OptionalAddition: { required: false, defaultEnabled: true },
278
+ },
279
+ },
280
+ },
281
+ },
282
+ };
283
+ const preview = await updateConsentFlowContractVersion(
284
+ { uri, changes, dryRun: true },
285
+ 'usera',
286
+ 'localhost'
287
+ );
288
+ expect(preview).toMatchObject({ version: 2, kind: 'expanded_optional' });
289
+ expect(await getConsentFlowContractVersionRecord(id, 2)).toBeNull();
290
+
291
+ const updated = await updateConsentFlowContractVersion(
292
+ { uri, changes },
293
+ 'usera',
294
+ 'localhost'
295
+ );
296
+ expect(updated.version).toBe(2);
297
+ expect((await getConsentFlowContractVersionRecord(id, 1))?.contract).toEqual(
298
+ initial?.contract
299
+ );
300
+ expect(
301
+ (await getConsentFlowContractVersionRecord(id, 2))?.contract.write.credentials
302
+ .categories
303
+ ).toHaveProperty('OptionalAddition');
304
+ });
305
+ it('migrates a legacy contract snapshot idempotently from persisted Neo4j state', async () => {
306
+ const uri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
307
+ contract: normalContract,
308
+ name: 'Legacy migration contract',
309
+ });
310
+ const id = getIdFromUri(uri);
311
+ await neogma.queryRunner.run(
312
+ `MATCH (contract:ConsentFlowContract {id:$id})-[relationship:HAS_VERSION]->(version:ConsentFlowContractVersion)
313
+ WITH contract, relationship, version
314
+ DELETE relationship, version
315
+ WITH contract
316
+ SET contract.currentVersion=null`,
317
+ { id }
318
+ );
319
+
320
+ await migrateLegacyConsentFlowContracts();
321
+ const migrated = await getConsentFlowContractVersionRecord(id, 1);
322
+ expect(migrated?.contract).toEqual(normalContract);
323
+ await migrateLegacyConsentFlowContracts();
324
+
325
+ const count = await neogma.queryRunner.run(
326
+ `MATCH (:ConsentFlowContract {id:$id})-[:HAS_VERSION]->(version:ConsentFlowContractVersion)
327
+ RETURN count(version) AS count`,
328
+ { id }
329
+ );
330
+ expect(Number(count.records[0]?.get('count'))).toBe(1);
331
+ });
159
332
 
160
333
  it('should allow creating a contract with an auto-boost', async () => {
161
334
  await userA.clients.fullAuth.profile.registerSigningAuthority({
@@ -612,6 +785,278 @@ describe('Consent Flow Contracts', () => {
612
785
  });
613
786
  });
614
787
 
788
+ describe('versioned consent-flow updates', () => {
789
+ let uri: string;
790
+ let termsUri: string;
791
+
792
+ beforeEach(async () => {
793
+ await Profile.delete({ detach: true, where: {} });
794
+ await ConsentFlowContract.delete({ detach: true, where: {} });
795
+ await ConsentFlowTransaction.delete({ detach: true, where: {} });
796
+ await ConsentFlowTerms.delete({ detach: true, where: {} });
797
+ await userA.clients.fullAuth.profile.createProfile({ profileId: 'usera' });
798
+ await userB.clients.fullAuth.profile.createProfile({ profileId: 'userb' });
799
+ uri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
800
+ contract: normalContract,
801
+ name: 'Versioned contract',
802
+ });
803
+ ({ termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
804
+ contractUri: uri,
805
+ terms: normalFullTerms,
806
+ }));
807
+ });
808
+
809
+ it('keeps empty requests readable through optional additions and removal of the last category', async () => {
810
+ const emptyContract = {
811
+ read: { personal: {}, credentials: { categories: {} } },
812
+ write: { personal: {}, credentials: { categories: {} } },
813
+ };
814
+ const emptyTerms = {
815
+ read: { personal: {}, credentials: { categories: {} } },
816
+ write: { personal: {}, credentials: { categories: {} } },
817
+ };
818
+ const emptyUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
819
+ name: 'Empty request',
820
+ contract: emptyContract,
821
+ });
822
+ expect(
823
+ (await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri: emptyUri }))
824
+ .contract
825
+ ).toEqual(emptyContract);
826
+ const { termsUri: emptyTermsUri } =
827
+ await userB.clients.fullAuth.contracts.consentToContract({
828
+ contractUri: emptyUri,
829
+ terms: emptyTerms,
830
+ });
831
+ await userA.clients.fullAuth.contracts.updateConsentFlowContract({
832
+ uri: emptyUri,
833
+ changes: {
834
+ contract: {
835
+ write: {
836
+ credentials: { categories: { Certificate: { required: false } } },
837
+ },
838
+ },
839
+ },
840
+ });
841
+ const optional = await userB.clients.fullAuth.contracts.getConsentedContracts();
842
+ expect(
843
+ optional.records.find(record => record.uri === emptyTermsUri)?.terms.write
844
+ .credentials.categories.Certificate
845
+ ).toBe(false);
846
+ const removal = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
847
+ uri: emptyUri,
848
+ changes: {
849
+ contract: { write: { credentials: { categories: { Certificate: null } } } },
850
+ },
851
+ });
852
+ expect(removal.kind).toBe('relaxed');
853
+ expect(
854
+ (await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri: emptyUri }))
855
+ .contract
856
+ ).toEqual(emptyContract);
857
+ expect(
858
+ (await userA.clients.fullAuth.contracts.getConsentFlowContracts()).records
859
+ ).toEqual(
860
+ expect.arrayContaining([expect.objectContaining({ uri: emptyUri, version: 3 })])
861
+ );
862
+ const records = await userB.clients.fullAuth.contracts.getConsentedContracts();
863
+ expect(records.records.find(record => record.uri === emptyTermsUri)).toMatchObject({
864
+ status: 'live',
865
+ consentedVersion: 3,
866
+ terms: emptyTerms,
867
+ });
868
+ expect(
869
+ await userA.clients.fullAuth.contracts.verifyConsent({
870
+ uri: emptyUri,
871
+ profileId: 'userb',
872
+ })
873
+ ).toBe(true);
874
+ });
875
+
876
+ it('previews without writing, requires a reason for expanded access, and blocks stale acceptance', async () => {
877
+ const changedContract = {
878
+ ...normalContract,
879
+ read: {
880
+ ...normalContract.read,
881
+ credentials: {
882
+ ...normalContract.read.credentials,
883
+ categories: {
884
+ ...normalContract.read.credentials.categories,
885
+ Transcript: { required: true },
886
+ },
887
+ },
888
+ },
889
+ };
890
+ const preview = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
891
+ uri,
892
+ changes: { contract: changedContract },
893
+ reason: 'We need transcripts to personalize your progress report.',
894
+ dryRun: true,
895
+ });
896
+ expect(preview.kind).toBe('requires_consent');
897
+ expect(preview.version).toBe(2);
898
+ expect(
899
+ await userA.clients.fullAuth.contracts.getContractVersions({ uri })
900
+ ).toHaveLength(1);
901
+
902
+ await expect(
903
+ userA.clients.fullAuth.contracts.updateConsentFlowContract({
904
+ uri,
905
+ changes: { contract: changedContract },
906
+ })
907
+ ).rejects.toMatchObject({
908
+ code: 'BAD_REQUEST',
909
+ message: 'A reason is required when asking users for more access',
910
+ });
911
+ expect(
912
+ await userA.clients.fullAuth.contracts.getContractVersions({ uri })
913
+ ).toHaveLength(1);
914
+
915
+ const result = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
916
+ uri,
917
+ changes: { contract: changedContract },
918
+ reason: 'We need transcripts to personalize your progress report.',
919
+ });
920
+ expect(result.version).toBe(2);
921
+ expect(result.affectedUsers).toBe(1);
922
+ const status = await userB.clients.fullAuth.contracts.getConsentStatus({
923
+ uri,
924
+ profileId: 'userb',
925
+ });
926
+ expect(status.status).toBe('needs_reconsent');
927
+ expect(status.consentedVersion).toBe(1);
928
+ expect(status.currentVersion).toBe(2);
929
+ expect(status.reviewUrl).toContain('/consent-flow/review?uri=');
930
+ expect(
931
+ await userB.clients.fullAuth.contracts.verifyConsent({ uri, profileId: 'userb' })
932
+ ).toBe(false);
933
+ const retainedTerms = await getContractTermsByUri(termsUri);
934
+ expect(retainedTerms?.terms.terms).toEqual(normalFullTerms);
935
+ expect(retainedTerms?.terms.consentedVersion).toBe(1);
936
+ const retainedData = await userA.clients.fullAuth.contracts.getConsentedDataForContract(
937
+ { uri }
938
+ );
939
+ expect(retainedData.records[0]?.credentials.categories.Achievement).toEqual(
940
+ normalFullTerms.read.credentials.categories.Achievement.shared
941
+ );
942
+ expect(retainedData.records[0]?.credentials.categories.Transcript).toBeUndefined();
943
+ await expect(
944
+ userB.clients.fullAuth.contracts.syncCredentialsToContract({
945
+ termsUri,
946
+ categories: { Achievement: ['credential:attempted-write'] },
947
+ })
948
+ ).rejects.toMatchObject({
949
+ code: 'FORBIDDEN',
950
+ message: 'User has not yet approved the latest version of this contract',
951
+ });
952
+ const signedCredential = await userA.learnCard.invoke.issueCredential({
953
+ ...testUnsignedBoost,
954
+ issuer: userA.learnCard.id.did(),
955
+ });
956
+ await expect(
957
+ userA.clients.fullAuth.contracts.writeCredentialToContract({
958
+ did: userB.learnCard.id.did(),
959
+ contractUri: uri,
960
+ boostUri: 'boost:pending-write',
961
+ credential: signedCredential,
962
+ })
963
+ ).rejects.toMatchObject({
964
+ code: 'FORBIDDEN',
965
+ message: 'User has not yet approved the latest version of this contract',
966
+ });
967
+ await expect(
968
+ userA.clients.fullAuth.contracts.writeCredentialToContractViaSigningAuthority({
969
+ did: userB.learnCard.id.did(),
970
+ contractUri: uri,
971
+ boostUri: 'boost:pending-write',
972
+ credential: testUnsignedBoost,
973
+ signingAuthority: { endpoint: 'https://issuer.example', name: 'issuer' },
974
+ })
975
+ ).rejects.toMatchObject({
976
+ code: 'FORBIDDEN',
977
+ message: 'User has not yet approved the latest version of this contract',
978
+ });
979
+
980
+ await expect(
981
+ userB.clients.fullAuth.contracts.updateConsentedContractTerms({
982
+ uri: termsUri,
983
+ terms: normalFullTerms,
984
+ acceptVersion: 1,
985
+ })
986
+ ).rejects.toMatchObject({ code: 'CONFLICT' });
987
+ await userA.clients.fullAuth.contracts.updateConsentFlowContract({
988
+ uri,
989
+ changes: { subtitle: 'Updated display information' },
990
+ });
991
+ await expect(
992
+ userB.clients.fullAuth.contracts.updateConsentedContractTerms({
993
+ uri: termsUri,
994
+ terms: normalFullTerms,
995
+ acceptVersion: 2,
996
+ })
997
+ ).rejects.toMatchObject({ code: 'CONFLICT' });
998
+ const stillPending = await userB.clients.fullAuth.contracts.getConsentStatus({
999
+ uri,
1000
+ profileId: 'userb',
1001
+ });
1002
+ expect(stillPending.status).toBe('needs_reconsent');
1003
+ expect(stillPending.consentedVersion).toBe(1);
1004
+ expect(stillPending.currentVersion).toBe(3);
1005
+ const acceptedTerms: ConsentFlowTermsType = structuredClone(normalFullTerms);
1006
+ acceptedTerms.read.credentials.categories.Transcript = { sharing: true, shared: [] };
1007
+ await expect(
1008
+ userB.clients.fullAuth.contracts.updateConsentedContractTerms({
1009
+ uri: termsUri,
1010
+ terms: acceptedTerms,
1011
+ acceptVersion: 3,
1012
+ })
1013
+ ).resolves.toBe(true);
1014
+ expect((await getContractTermsByUri(termsUri))?.terms.consentedVersion).toBe(3);
1015
+ });
1016
+
1017
+ it('migrates relaxed changes by pruning removed grants without requiring reapproval', async () => {
1018
+ const changedContract = {
1019
+ read: { credentials: { categories: { ID: null } } },
1020
+ };
1021
+ const result = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
1022
+ uri,
1023
+ changes: { contract: changedContract },
1024
+ });
1025
+ expect(result.kind).toBe('relaxed');
1026
+ const status = await userB.clients.fullAuth.contracts.getConsentStatus({
1027
+ uri,
1028
+ profileId: 'userb',
1029
+ });
1030
+ expect(status.status).toBe('live');
1031
+ expect(status.consentedVersion).toBe(2);
1032
+ const terms = await getContractTermsByUri(termsUri);
1033
+ expect(terms?.terms.terms.read.credentials.categories.ID).toBeUndefined();
1034
+ });
1035
+
1036
+ it('keeps a newly introduced optional write field off even when defaultEnabled is true', async () => {
1037
+ const changedContract = {
1038
+ ...normalContract,
1039
+ write: {
1040
+ ...normalContract.write,
1041
+ personal: {
1042
+ ...normalContract.write.personal,
1043
+ email: { required: false, defaultEnabled: true },
1044
+ },
1045
+ },
1046
+ };
1047
+ const result = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
1048
+ uri,
1049
+ changes: { contract: changedContract },
1050
+ });
1051
+ expect(result.kind).toBe('expanded_optional');
1052
+
1053
+ const terms = await getContractTermsByUri(termsUri);
1054
+ expect(terms?.terms.status).toBe('live');
1055
+ expect(terms?.terms.consentedVersion).toBe(2);
1056
+ expect(terms?.terms.terms.write.personal.email).not.toBe(true);
1057
+ });
1058
+ });
1059
+
615
1060
  describe('deleteConsentFlowContract', () => {
616
1061
  let uri: string;
617
1062
 
@@ -643,35 +1088,51 @@ describe('Consent Flow Contracts', () => {
643
1088
  ).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
644
1089
  });
645
1090
 
646
- it('should allow you to delete contracts', async () => {
1091
+ it('should allow you to retire contracts while retaining their details', async () => {
647
1092
  const contracts = await userA.clients.fullAuth.contracts.getConsentFlowContracts();
648
-
649
1093
  expect(contracts.records).toHaveLength(1);
650
1094
 
651
1095
  await expect(
652
1096
  userA.clients.fullAuth.contracts.deleteConsentFlowContract({ uri })
653
- ).resolves.not.toThrow();
1097
+ ).resolves.toBe(true);
654
1098
 
655
- const newContracts = await userA.clients.fullAuth.contracts.getConsentFlowContracts();
656
-
657
- expect(newContracts.records).toHaveLength(0);
1099
+ const retired = await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri });
1100
+ expect(retired.status).toBe('retired');
1101
+ expect(retired.uri).toBe(uri);
658
1102
  });
659
1103
 
660
- it('should remove contract from profiles that have consented to it', async () => {
661
- await userB.clients.fullAuth.contracts.consentToContract({
1104
+ it('withdraws consent while retaining terms and contract history', async () => {
1105
+ const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
662
1106
  terms: minimalTerms,
663
1107
  contractUri: uri,
664
1108
  });
665
-
666
1109
  const contracts = await userB.clients.fullAuth.contracts.getConsentedContracts();
667
-
668
1110
  expect(contracts.records).toHaveLength(1);
669
1111
 
670
1112
  await userA.clients.fullAuth.contracts.deleteConsentFlowContract({ uri });
671
1113
 
672
1114
  const newContracts = await userB.clients.fullAuth.contracts.getConsentedContracts();
673
-
674
- expect(newContracts.records).toHaveLength(0);
1115
+ expect(newContracts.records.find(record => record.uri === termsUri)?.status).toBe(
1116
+ 'withdrawn'
1117
+ );
1118
+ const retainedTerms = await getContractTermsByUri(termsUri);
1119
+ expect(retainedTerms?.terms.status).toBe('withdrawn');
1120
+ expect(
1121
+ (await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri })).status
1122
+ ).toBe('retired');
1123
+ await expect(
1124
+ userB.clients.fullAuth.contracts.updateConsentedContractTerms({
1125
+ uri: termsUri,
1126
+ terms: minimalTerms,
1127
+ acceptVersion: 1,
1128
+ })
1129
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
1130
+ const history = await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
1131
+ uri: termsUri,
1132
+ });
1133
+ expect(history.records.some(transaction => transaction.action === 'withdraw')).toBe(
1134
+ true
1135
+ );
675
1136
  });
676
1137
  });
677
1138
 
@@ -1033,7 +1494,7 @@ describe('Consent Flow Contracts', () => {
1033
1494
  expect(withdrawnData.records).toEqual([]);
1034
1495
  });
1035
1496
 
1036
- it('returns one-time selected data while stale, but not after withdrawal', async () => {
1497
+ it('returns one-time selected data while consumed, but not after withdrawal', async () => {
1037
1498
  const initial = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
1038
1499
  did: userBDid,
1039
1500
  });
@@ -1049,14 +1510,14 @@ describe('Consent Flow Contracts', () => {
1049
1510
  terms: selectedTerms,
1050
1511
  oneTime: true,
1051
1512
  });
1052
- expect((await getContractTermsByUri(termsUri))?.terms.status).toBe('stale');
1513
+ expect((await getContractTermsByUri(termsUri))?.terms.status).toBe('consumed');
1053
1514
  const shared = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
1054
1515
  did: userBDid,
1055
1516
  });
1056
1517
  expect(shared.records).toEqual([
1057
1518
  expect.objectContaining({
1058
1519
  termsUri,
1059
- status: 'stale',
1520
+ status: 'consumed',
1060
1521
  credentials: [{ category: 'Achievement', uri: 'achievement2' }],
1061
1522
  personal: selectedTerms.read.personal,
1062
1523
  }),
@@ -1088,12 +1549,20 @@ describe('Consent Flow Contracts', () => {
1088
1549
  did: userBDid,
1089
1550
  })
1090
1551
  ).records[0]?.status
1091
- ).toBe('stale');
1092
- const model = target === 'terms' ? ConsentFlowTerms : ConsentFlowContract;
1093
- await new QueryBuilder()
1094
- .match({ model, identifier: 'record', where: { id: relationship[target].id } })
1095
- .set("record.expiresAt = '2000-01-01T00:00:00.000Z'")
1096
- .run();
1552
+ ).toBe('consumed');
1553
+ const expiredAt = '2000-01-01T00:00:00.000Z';
1554
+ if (target === 'contract') {
1555
+ await seedStoredContractExpiry(
1556
+ relationship.contract.id,
1557
+ relationship.terms.consentedVersion ?? 1,
1558
+ expiredAt
1559
+ );
1560
+ } else {
1561
+ await ConsentFlowTerms.update(
1562
+ { expiresAt: expiredAt },
1563
+ { where: { id: relationship.terms.id } }
1564
+ );
1565
+ }
1097
1566
  expect(
1098
1567
  (
1099
1568
  await userA.clients.fullAuth.contracts.getConsentedDataForDid({
@@ -1104,13 +1573,13 @@ describe('Consent Flow Contracts', () => {
1104
1573
  }
1105
1574
  );
1106
1575
 
1107
- it('does not treat other stale terms as a one-time grant', async () => {
1576
+ it('does not treat other consumed terms as a one-time grant', async () => {
1108
1577
  const initial = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
1109
1578
  did: userBDid,
1110
1579
  });
1111
1580
  const relationship = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
1112
1581
  await ConsentFlowTerms.update(
1113
- { status: 'stale' },
1582
+ { status: 'consumed' },
1114
1583
  { where: { id: relationship.terms.id } }
1115
1584
  );
1116
1585
  expect(
@@ -1259,8 +1728,6 @@ describe('Consent Flow Contracts', () => {
1259
1728
  did: userBDid,
1260
1729
  });
1261
1730
  const relationship = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
1262
- const model = target === 'terms' ? ConsentFlowTerms : ConsentFlowContract;
1263
- const id = relationship[target].id;
1264
1731
 
1265
1732
  for (const [expiresAt, active] of [
1266
1733
  [null, true],
@@ -1270,10 +1737,18 @@ describe('Consent Flow Contracts', () => {
1270
1737
  ['2000-01-01T00:00:00.000Z', false],
1271
1738
  ['not-a-date', false],
1272
1739
  ] as const) {
1273
- await new QueryBuilder(new BindParam({ expiresAt }))
1274
- .match({ model, identifier: 'record', where: { id } })
1275
- .set('record.expiresAt = $expiresAt')
1276
- .run();
1740
+ if (target === 'contract') {
1741
+ await seedStoredContractExpiry(
1742
+ relationship.contract.id,
1743
+ relationship.terms.consentedVersion ?? 1,
1744
+ expiresAt
1745
+ );
1746
+ } else {
1747
+ await ConsentFlowTerms.update(
1748
+ { expiresAt },
1749
+ { where: { id: relationship.terms.id } }
1750
+ );
1751
+ }
1277
1752
  const data = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
1278
1753
  did: userBDid,
1279
1754
  });
@@ -1292,28 +1767,38 @@ describe('Consent Flow Contracts', () => {
1292
1767
  });
1293
1768
  const original = initial.records[0]!;
1294
1769
  const relationship = (await getContractTermsByUri(original.termsUri))!;
1295
- await ConsentFlowContract.update(
1296
- {
1770
+ const changed = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
1771
+ uri: constructUri('contract', relationship.contract.id, 'localhost%3A3000'),
1772
+ changes: {
1297
1773
  reasonForAccessing: 'Personalized learning',
1298
1774
  expiresAt: '2999-01-01T00:00:00.000Z',
1299
1775
  },
1300
- { where: { id: relationship.contract.id } }
1301
- );
1776
+ reason: 'The contract metadata was updated.',
1777
+ });
1302
1778
  await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
1303
1779
  uri: original.termsUri,
1304
1780
  terms: normalAchievementOnlyTerms,
1781
+ acceptVersion: changed.version,
1305
1782
  });
1306
1783
  const updated = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
1307
1784
  did: userBDid,
1308
1785
  });
1309
1786
  expect(updated.records[0]).toMatchObject({
1310
1787
  createdAt: original.createdAt,
1311
- contractUpdatedAt: relationship.contract.updatedAt,
1788
+ contractUpdatedAt: (await getConsentFlowContractVersionRecord(
1789
+ relationship.contract.id,
1790
+ changed.version
1791
+ ))!.createdAt,
1312
1792
  contractExpiresAt: '2999-01-01T00:00:00.000Z',
1313
1793
  reasonForAccessing: 'Personalized learning',
1314
1794
  guardian: { required: false, approved: false },
1315
1795
  terms: normalAchievementOnlyTerms,
1316
1796
  });
1797
+ expect(
1798
+ (await getTransactionsForTerms(relationship.terms.id, { limit: 100 })).find(
1799
+ transaction => transaction.action === 'consent'
1800
+ )?.date
1801
+ ).toBe(original.createdAt);
1317
1802
  expect(Date.parse(updated.records[0]!.date)).toBeGreaterThanOrEqual(
1318
1803
  Date.parse(original.date)
1319
1804
  );
@@ -1405,23 +1890,24 @@ describe('Consent Flow Contracts', () => {
1405
1890
  transactions.find(transaction => transaction.guardianApproval)?.guardianApproval
1406
1891
  ).toEqual(approval);
1407
1892
 
1408
- await ConsentFlowContract.update(
1409
- { updatedAt: '2999-01-01T00:00:00.000Z' },
1410
- { where: { id: relationship.contract.id } }
1411
- );
1412
- const revised = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
1413
- did: userBDid,
1414
- });
1415
- expect(revised.records[0]?.guardian).toEqual({
1416
- required: true,
1417
- approved: false,
1418
- approval,
1893
+ const changedContract = {
1894
+ ...normalContract,
1895
+ read: {
1896
+ ...normalContract.read,
1897
+ credentials: {
1898
+ ...normalContract.read.credentials,
1899
+ categories: {
1900
+ ...normalContract.read.credentials.categories,
1901
+ Transcript: { required: true },
1902
+ },
1903
+ },
1904
+ },
1905
+ };
1906
+ await userA.clients.fullAuth.contracts.updateConsentFlowContract({
1907
+ uri: constructUri('contract', relationship.contract.id, 'localhost%3A3000'),
1908
+ changes: { contract: changedContract },
1909
+ reason: 'We need transcripts to personalize your progress report.',
1419
1910
  });
1420
-
1421
- await ConsentFlowContract.update(
1422
- { updatedAt: relationship.contract.updatedAt },
1423
- { where: { id: relationship.contract.id } }
1424
- );
1425
1911
  await Profile.delete({ detach: true, where: { profileId: 'userc' } });
1426
1912
  const unmanaged = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
1427
1913
  did: userBDid,
@@ -1433,7 +1919,25 @@ describe('Consent Flow Contracts', () => {
1433
1919
  });
1434
1920
 
1435
1921
  const current = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
1436
- await updateTerms(current, { terms: normalAchievementOnlyTerms }, 'localhost%3A3000');
1922
+ const acceptedTerms: ConsentFlowTermsType = structuredClone(normalAchievementOnlyTerms);
1923
+ acceptedTerms.read.credentials.categories.Transcript = { sharing: true, shared: [] };
1924
+ await expect(
1925
+ userB.clients.fullAuth.contracts.updateConsentedContractTerms({
1926
+ uri: initial.records[0]!.termsUri,
1927
+ terms: acceptedTerms,
1928
+ acceptVersion: current.contract.currentVersion,
1929
+ })
1930
+ ).rejects.toMatchObject({
1931
+ code: 'FORBIDDEN',
1932
+ message: 'This consent requires approval from a current guardian',
1933
+ });
1934
+ // Legacy metadata may be absent; the original guardian approval must remain auditable.
1935
+ await neogma.queryRunner.run(
1936
+ `MATCH (terms:ConsentFlowTerms {id:$id})
1937
+ REMOVE terms.\`guardianApproval.guardianProfileId\`, terms.\`guardianApproval.guardianDid\`,
1938
+ terms.\`guardianApproval.approvedAt\`, terms.\`guardianApproval.contractUpdatedAt\``,
1939
+ { id: relationship.terms.id }
1940
+ );
1437
1941
  const cleared = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
1438
1942
  expect(cleared.terms.guardianApproval).toBeUndefined();
1439
1943
  expect(await hasGuardianApprovalHistory(cleared.terms)).toBe(true);
@@ -1442,9 +1946,6 @@ describe('Consent Flow Contracts', () => {
1442
1946
  });
1443
1947
  expect(historical.records[0]?.guardian).toEqual({ required: true, approved: false });
1444
1948
  const history = await getTransactionsForTerms(relationship.terms.id, { limit: 10 });
1445
- expect(
1446
- history.find(transaction => transaction.action === 'update')?.guardianApproval
1447
- ).toBeUndefined();
1448
1949
  expect(
1449
1950
  history.find(transaction => transaction.guardianApproval)?.guardianApproval
1450
1951
  ).toEqual(approval);
@@ -1902,69 +2403,58 @@ describe('Consent Flow Contracts', () => {
1902
2403
  ).rejects.toMatchObject({ code: 'CONFLICT' });
1903
2404
  });
1904
2405
 
1905
- it('should auto-issue boosts when consenting to a contract with auto-boosts', async () => {
1906
- await userA.clients.fullAuth.profile.registerSigningAuthority({
1907
- endpoint: 'http://localhost:5000/api',
1908
- name: 'mysa',
1909
- did: 'did:key:z6MkitsQTk2GDNYXAFckVcQHtC68S9j9ruVFYWrixM6RG5Mw',
1910
- });
1911
-
1912
- const sa = await userA.clients.fullAuth.profile.signingAuthority({
1913
- endpoint: 'http://localhost:5000/api',
1914
- name: 'mysa',
1915
- });
1916
-
1917
- const signingAuthority = {
1918
- endpoint: sa!.signingAuthority.endpoint,
1919
- name: sa!.relationship.name,
1920
- };
1921
-
1922
- // Create a boost
1923
- const boostUri = await userA.clients.fullAuth.boost.createBoost({
1924
- credential: testUnsignedBoost,
1925
- category: 'Achievement',
1926
- name: 'Auto-issued Boost',
1927
- });
1928
-
1929
- // Create a contract with the auto-boost
1930
- const contractUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
1931
- contract: normalContract,
1932
- name: 'Auto Boost Contract',
1933
- autoboosts: [
1934
- {
1935
- boostUri,
1936
- signingAuthority,
1937
- },
1938
- ],
1939
- });
1940
-
1941
- // Consent to the contract
1942
- const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
1943
- contractUri,
1944
- terms: normalFullTerms,
1945
- });
1946
-
1947
- // Check that a credential was automatically issued
1948
- const result = await userB.clients.fullAuth.contracts.getCredentialsForContract({
1949
- termsUri,
1950
- });
2406
+ it.each([false, true])(
2407
+ 'auto-issues boosts for contracts with autoboosts when oneTime=%s',
2408
+ async oneTime => {
2409
+ await userA.clients.fullAuth.profile.registerSigningAuthority({
2410
+ endpoint: 'http://localhost:5000/api',
2411
+ name: 'mysa',
2412
+ did: 'did:key:z6MkitsQTk2GDNYXAFckVcQHtC68S9j9ruVFYWrixM6RG5Mw',
2413
+ });
1951
2414
 
1952
- // Should have one auto-issued credential
1953
- expect(result.records).toHaveLength(1);
1954
- expect(result.records[0]?.boostUri).toContain(boostUri.split(':').pop());
2415
+ const sa = await userA.clients.fullAuth.profile.signingAuthority({
2416
+ endpoint: 'http://localhost:5000/api',
2417
+ name: 'mysa',
2418
+ });
2419
+ const signingAuthority = {
2420
+ endpoint: sa!.signingAuthority.endpoint,
2421
+ name: sa!.relationship.name,
2422
+ };
1955
2423
 
1956
- // Check that the transaction history includes an auto-boost transaction
1957
- const transactions = await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
1958
- uri: termsUri,
1959
- });
2424
+ const boostUri = await userA.clients.fullAuth.boost.createBoost({
2425
+ credential: testUnsignedBoost,
2426
+ category: 'Achievement',
2427
+ name: 'Auto-issued Boost',
2428
+ });
2429
+ const contractUri =
2430
+ await userA.clients.fullAuth.contracts.createConsentFlowContract({
2431
+ contract: normalContract,
2432
+ name: 'Auto Boost Contract',
2433
+ autoboosts: [{ boostUri, signingAuthority }],
2434
+ });
1960
2435
 
1961
- // Should have at least 2 transactions (consent + auto-boost)
1962
- expect(transactions.records.length).toBeGreaterThanOrEqual(2);
2436
+ const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
2437
+ contractUri,
2438
+ terms: normalFullTerms,
2439
+ oneTime,
2440
+ });
2441
+ const result = await userB.clients.fullAuth.contracts.getCredentialsForContract({
2442
+ termsUri,
2443
+ });
2444
+ expect(result.records).toHaveLength(1);
2445
+ expect(result.records[0]?.boostUri).toContain(boostUri.split(':').pop());
1963
2446
 
1964
- // At least one transaction should be an auto-boost transaction
1965
- const hasAutoBoostTx = transactions.records.some(tx => tx.action === 'write');
1966
- expect(hasAutoBoostTx).toBeTruthy();
1967
- });
2447
+ const transactions =
2448
+ await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
2449
+ uri: termsUri,
2450
+ });
2451
+ expect(transactions.records.length).toBeGreaterThanOrEqual(2);
2452
+ expect(transactions.records.some(tx => tx.action === 'write')).toBeTruthy();
2453
+ expect((await getContractTermsByUri(termsUri))?.terms.status).toBe(
2454
+ oneTime ? 'consumed' : 'live'
2455
+ );
2456
+ }
2457
+ );
1968
2458
 
1969
2459
  describe('first-consent auto-boost issuance dates', () => {
1970
2460
  // A malformed date seen on a production Boost template; DIDKit refuses to sign it.
@@ -2131,7 +2621,7 @@ describe('Consent Flow Contracts', () => {
2131
2621
  }
2132
2622
  });
2133
2623
 
2134
- it('should auto-issue boosts when updating terms for a contract with auto-boosts', async () => {
2624
+ it('does not reissue the same autoboost when terms are updated repeatedly', async () => {
2135
2625
  await userA.clients.fullAuth.profile.registerSigningAuthority({
2136
2626
  endpoint: 'http://localhost:5000/api',
2137
2627
  name: 'mysa',
@@ -2187,30 +2677,34 @@ describe('Consent Flow Contracts', () => {
2187
2677
  },
2188
2678
  });
2189
2679
 
2190
- // Check that another credential was automatically issued
2680
+ // Updating terms cannot cause the same automatic boost to be issued again.
2191
2681
  const updatedResult = await userB.clients.fullAuth.contracts.getCredentialsForContract({
2192
2682
  termsUri,
2193
2683
  });
2194
-
2195
- // Should have one more credential than before
2196
- expect(updatedResult.records.length).toBeGreaterThan(initialCount);
2197
-
2198
- // The latest credential should be from the same boost
2684
+ expect(updatedResult.records).toHaveLength(initialCount);
2199
2685
  expect(updatedResult.records[0]?.boostUri).toContain(boostUri.split(':').pop());
2200
2686
 
2201
- // Check transaction history
2202
- const transactions = await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
2687
+ await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
2203
2688
  uri: termsUri,
2689
+ terms: {
2690
+ ...normalFullTerms,
2691
+ read: { ...normalFullTerms.read, personal: { name: 'Updated again' } },
2692
+ },
2204
2693
  });
2694
+ const repeatedResult = await userB.clients.fullAuth.contracts.getCredentialsForContract(
2695
+ {
2696
+ termsUri,
2697
+ }
2698
+ );
2699
+ expect(repeatedResult.records).toHaveLength(initialCount);
2205
2700
 
2206
- // Should have at least 3 transactions now (consent + auto-boost + update + auto-boost)
2207
- expect(transactions.records.length).toBeGreaterThanOrEqual(4);
2208
-
2209
- // Should have at least two auto-boost transactions
2701
+ const transactions = await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
2702
+ uri: termsUri,
2703
+ });
2210
2704
  const autoBoostTxCount = transactions.records.filter(
2211
2705
  tx => tx.action === 'write'
2212
2706
  ).length;
2213
- expect(autoBoostTxCount).toBeGreaterThanOrEqual(2);
2707
+ expect(autoBoostTxCount).toBe(1);
2214
2708
  });
2215
2709
 
2216
2710
  it('should support multiple auto-boosts per contract', async () => {
@@ -2394,7 +2888,7 @@ describe('Consent Flow Contracts', () => {
2394
2888
  expect(terms.deniedWriters).toEqual(['userc']);
2395
2889
  });
2396
2890
 
2397
- it('should not auto-issue boosts from denied writers', async () => {
2891
+ it('keeps owner autoboosts active when consent denies a writer', async () => {
2398
2892
  await userA.clients.fullAuth.profile.registerSigningAuthority({
2399
2893
  endpoint: 'http://localhost:5000/api',
2400
2894
  name: 'mysa',
@@ -2448,9 +2942,10 @@ describe('Consent Flow Contracts', () => {
2448
2942
  autoboosts: [{ boostUri, signingAuthority }],
2449
2943
  });
2450
2944
 
2451
- await userC.clients.fullAuth.contracts.addAutoBoostsToContract({
2452
- contractUri,
2453
- autoboosts: [{ boostUri: boostUri2, signingAuthority: signingAuthority2 }],
2945
+ await seedLegacyAutoBoost(contractUri, {
2946
+ boostUri: boostUri2,
2947
+ signingAuthority: signingAuthority2,
2948
+ issuerProfileId: 'userc',
2454
2949
  });
2455
2950
 
2456
2951
  // Consent to the contract
@@ -2481,7 +2976,7 @@ describe('Consent Flow Contracts', () => {
2481
2976
  expect(hasAutoBoostTx).toBeTruthy();
2482
2977
  });
2483
2978
 
2484
- it('should handle re-consenting to a contract, filtering auto-issued boosts based on new deniedWriters', async () => {
2979
+ it('retains legacy delegated deliveries across withdrawal and reconsent', async () => {
2485
2980
  // Setup: userA (owner) and userC (writer) can issue boosts
2486
2981
  // User A's SA
2487
2982
  await userA.clients.fullAuth.profile.registerSigningAuthority({
@@ -2532,43 +3027,25 @@ describe('Consent Flow Contracts', () => {
2532
3027
  },
2533
3028
  ],
2534
3029
  });
2535
- await userC.clients.fullAuth.contracts.addAutoBoostsToContract({
3030
+ await seedLegacyAutoBoost(contractUri, {
3031
+ boostUri: boostC_Uri,
3032
+ signingAuthority: {
3033
+ endpoint: saC_Details.signingAuthority.endpoint,
3034
+ name: saC_Details.relationship.name,
3035
+ },
3036
+ issuerProfileId: 'userc',
3037
+ });
3038
+ const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
2536
3039
  contractUri,
2537
- autoboosts: [
2538
- {
2539
- boostUri: boostC_Uri,
2540
- signingAuthority: {
2541
- endpoint: saC_Details.signingAuthority.endpoint,
2542
- name: saC_Details.relationship.name,
2543
- },
2544
- },
2545
- ],
3040
+ terms: normalFullTerms,
2546
3041
  });
2547
-
2548
- // 1. Initial consent by userB (no denied writers)
2549
- const { termsUri: initialTermsUri } =
2550
- await userB.clients.fullAuth.contracts.consentToContract({
2551
- contractUri,
2552
- terms: normalFullTerms, // No deniedWriters
2553
- });
2554
3042
  const credentialsAfterInitialConsent =
2555
- await userB.clients.fullAuth.contracts.getCredentialsForContract({
2556
- termsUri: initialTermsUri,
2557
- });
2558
- // Should have boosts from both A and C
2559
- expect(credentialsAfterInitialConsent.records).toHaveLength(2);
2560
- expect(
2561
- credentialsAfterInitialConsent.records.some(cred =>
2562
- cred.boostUri?.includes(boostA_Uri.split(':').pop()!)
2563
- )
2564
- ).toBeTruthy();
3043
+ await userB.clients.fullAuth.contracts.getCredentialsForContract({ termsUri });
2565
3044
  expect(
2566
- credentialsAfterInitialConsent.records.some(cred =>
2567
- cred.boostUri?.includes(boostC_Uri.split(':').pop()!)
2568
- )
2569
- ).toBeTruthy();
3045
+ credentialsAfterInitialConsent.records.map(credential => credential.boostUri).sort()
3046
+ ).toEqual([boostA_Uri, boostC_Uri].sort());
2570
3047
 
2571
- await userB.clients.fullAuth.contracts.withdrawConsent({ uri: initialTermsUri });
3048
+ await userB.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
2572
3049
 
2573
3050
  // 2. Re-consent by userB, denying userC
2574
3051
  const { termsUri: reconsentTermsUri } =
@@ -2583,18 +3060,10 @@ describe('Consent Flow Contracts', () => {
2583
3060
  termsUri: reconsentTermsUri,
2584
3061
  });
2585
3062
 
2586
- // Should now only have the boost from userA
2587
- expect(credentialsAfterReconsent.records).toHaveLength(3);
3063
+ // Withdrawing or denying future writes does not erase already delivered credentials.
2588
3064
  expect(
2589
- credentialsAfterReconsent.records.filter(cred =>
2590
- cred.boostUri?.includes(boostA_Uri.split(':').pop()!)
2591
- ).length
2592
- ).toBe(2);
2593
- expect(
2594
- credentialsAfterReconsent.records.filter(cred =>
2595
- cred.boostUri?.includes(boostC_Uri.split(':').pop()!)
2596
- ).length
2597
- ).toBe(1);
3065
+ credentialsAfterReconsent.records.map(credential => credential.boostUri).sort()
3066
+ ).toEqual([boostA_Uri, boostC_Uri].sort());
2598
3067
  });
2599
3068
 
2600
3069
  it('should handle updating terms for a contract, filtering auto-issued boosts based on new deniedWriters', async () => {
@@ -2648,17 +3117,13 @@ describe('Consent Flow Contracts', () => {
2648
3117
  },
2649
3118
  ],
2650
3119
  });
2651
- await userC.clients.fullAuth.contracts.addAutoBoostsToContract({
2652
- contractUri,
2653
- autoboosts: [
2654
- {
2655
- boostUri: boostC_Uri,
2656
- signingAuthority: {
2657
- endpoint: saC_Details.signingAuthority.endpoint,
2658
- name: saC_Details.relationship.name,
2659
- },
2660
- },
2661
- ],
3120
+ await seedLegacyAutoBoost(contractUri, {
3121
+ boostUri: boostC_Uri,
3122
+ signingAuthority: {
3123
+ endpoint: saC_Details.signingAuthority.endpoint,
3124
+ name: saC_Details.relationship.name,
3125
+ },
3126
+ issuerProfileId: 'userc',
2662
3127
  });
2663
3128
 
2664
3129
  // 1. Initial consent by userB (no denied writers)
@@ -2668,7 +3133,7 @@ describe('Consent Flow Contracts', () => {
2668
3133
  });
2669
3134
  const credentialsAfterInitialConsent =
2670
3135
  await userB.clients.fullAuth.contracts.getCredentialsForContract({ termsUri });
2671
- // Should have boosts from both A and C
3136
+ // Both owner and designated-writer configurations are active for new consent.
2672
3137
  expect(credentialsAfterInitialConsent.records).toHaveLength(2);
2673
3138
 
2674
3139
  // 2. Update terms by userB, denying userC
@@ -2684,18 +3149,8 @@ describe('Consent Flow Contracts', () => {
2684
3149
  termsUri,
2685
3150
  });
2686
3151
 
2687
- // Should now only have the boost from userA
2688
- expect(credentialsAfterUpdate.records).toHaveLength(3);
2689
- expect(
2690
- credentialsAfterUpdate.records.filter(cred =>
2691
- cred.boostUri?.includes(boostA_Uri.split(':').pop()!)
2692
- ).length
2693
- ).toBe(2);
2694
- expect(
2695
- credentialsAfterUpdate.records.filter(cred =>
2696
- cred.boostUri?.includes(boostC_Uri.split(':').pop()!)
2697
- ).length
2698
- ).toBe(1);
3152
+ // Updating terms must not re-issue already delivered automatic credentials.
3153
+ expect(credentialsAfterUpdate.records).toHaveLength(2);
2699
3154
  });
2700
3155
  });
2701
3156
 
@@ -3484,7 +3939,7 @@ describe('Consent Flow Contracts', () => {
3484
3939
 
3485
3940
  beforeEach(async () => {
3486
3941
  vi.useFakeTimers();
3487
- vi.setSystemTime(new Date(2024, 3, 21));
3942
+ vi.setSystemTime(new Date(2999, 3, 21));
3488
3943
 
3489
3944
  await Profile.delete({ detach: true, where: {} });
3490
3945
  await ConsentFlowContract.delete({ detach: true, where: {} });
@@ -3496,7 +3951,7 @@ describe('Consent Flow Contracts', () => {
3496
3951
  contractUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
3497
3952
  contract: minimalContract,
3498
3953
  name: 'a',
3499
- expiresAt: '2999-01-01T00:00:00.000Z', // New consent uses Neo4j's real clock.
3954
+ expiresAt: '3000-01-01T00:00:00.000Z', // New consent uses Neo4j's real clock.
3500
3955
  });
3501
3956
  const { termsUri: _termsUri } =
3502
3957
  await userB.clients.fullAuth.contracts.consentToContract({
@@ -3505,10 +3960,18 @@ describe('Consent Flow Contracts', () => {
3505
3960
  });
3506
3961
  termsUri = _termsUri;
3507
3962
  // Verification's fake JS clock can still inspect an existing historical consent.
3508
- await ConsentFlowContract.update(
3509
- { expiresAt: new Date(Date.UTC(2024, 3, 25)).toISOString() },
3510
- { where: { id: contractUri.split(':').at(-1)! } }
3511
- );
3963
+ const expiryVersion = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
3964
+ uri: contractUri,
3965
+ changes: { expiresAt: new Date(Date.UTC(2999, 3, 25)).toISOString() },
3966
+ reason: 'The contract expiry was updated.',
3967
+ });
3968
+ if (expiryVersion.kind === 'requires_consent') {
3969
+ await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
3970
+ uri: termsUri,
3971
+ terms: minimalTerms,
3972
+ acceptVersion: expiryVersion.version,
3973
+ });
3974
+ }
3512
3975
  });
3513
3976
 
3514
3977
  afterAll(async () => {
@@ -3522,22 +3985,22 @@ describe('Consent Flow Contracts', () => {
3522
3985
  await ConsentFlowTransaction.delete({ detach: true, where: {} });
3523
3986
  });
3524
3987
 
3525
- it('should not allow you to verify consent without full auth', async () => {
3988
+ it('returns consent state for public and authenticated callers', async () => {
3526
3989
  await expect(
3527
3990
  noAuthClient.contracts.verifyConsent({ uri: contractUri, profileId: 'userb' })
3528
- ).resolves.not.toThrow();
3991
+ ).resolves.toBe(true);
3529
3992
  await expect(
3530
3993
  userA.clients.partialAuth.contracts.verifyConsent({
3531
3994
  uri: contractUri,
3532
3995
  profileId: 'userb',
3533
3996
  })
3534
- ).resolves.not.toThrow();
3997
+ ).resolves.toBe(true);
3535
3998
  await expect(
3536
3999
  userA.clients.fullAuth.contracts.verifyConsent({
3537
4000
  uri: contractUri,
3538
4001
  profileId: 'userb',
3539
4002
  })
3540
- ).resolves.not.toThrow();
4003
+ ).resolves.toBe(true);
3541
4004
  });
3542
4005
 
3543
4006
  it('should actually verify consent', async () => {
@@ -3578,18 +4041,27 @@ describe('Consent Flow Contracts', () => {
3578
4041
  {
3579
4042
  contract: minimalContract,
3580
4043
  name: 'b',
3581
- expiresAt: '2999-01-01T00:00:00.000Z',
4044
+ expiresAt: '3000-01-01T00:00:00.000Z',
3582
4045
  }
3583
4046
  );
3584
4047
 
3585
- await userB.clients.fullAuth.contracts.consentToContract({
3586
- contractUri: newContractUri,
3587
- terms: minimalTerms,
4048
+ const { termsUri: newTermsUri } =
4049
+ await userB.clients.fullAuth.contracts.consentToContract({
4050
+ contractUri: newContractUri,
4051
+ terms: minimalTerms,
4052
+ });
4053
+ const expiryVersion = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
4054
+ uri: newContractUri,
4055
+ changes: { expiresAt: new Date(2999, 4, 19).toISOString() },
4056
+ reason: 'The contract expiry was updated.',
3588
4057
  });
3589
- await ConsentFlowContract.update(
3590
- { expiresAt: new Date(2024, 4, 19).toISOString() },
3591
- { where: { id: newContractUri.split(':').at(-1)! } }
3592
- );
4058
+ if (expiryVersion.kind === 'requires_consent') {
4059
+ await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
4060
+ uri: newTermsUri,
4061
+ terms: minimalTerms,
4062
+ acceptVersion: expiryVersion.version,
4063
+ });
4064
+ }
3593
4065
 
3594
4066
  expect(
3595
4067
  await userA.clients.fullAuth.contracts.verifyConsent({
@@ -3598,7 +4070,7 @@ describe('Consent Flow Contracts', () => {
3598
4070
  })
3599
4071
  ).toBeTruthy();
3600
4072
 
3601
- vi.setSystemTime(new Date(2024, 5, 21));
4073
+ vi.setSystemTime(new Date(2999, 5, 21));
3602
4074
 
3603
4075
  expect(
3604
4076
  await userA.clients.fullAuth.contracts.verifyConsent({
@@ -3619,7 +4091,7 @@ describe('Consent Flow Contracts', () => {
3619
4091
  await userB.clients.fullAuth.contracts.consentToContract({
3620
4092
  contractUri: newContractUri,
3621
4093
  terms: minimalTerms,
3622
- expiresAt: new Date(2024, 4, 19).toISOString(),
4094
+ expiresAt: new Date(2999, 4, 19).toISOString(),
3623
4095
  });
3624
4096
 
3625
4097
  expect(
@@ -3629,7 +4101,7 @@ describe('Consent Flow Contracts', () => {
3629
4101
  })
3630
4102
  ).toBeTruthy();
3631
4103
 
3632
- vi.setSystemTime(new Date(2024, 5, 21));
4104
+ vi.setSystemTime(new Date(2999, 5, 21));
3633
4105
 
3634
4106
  expect(
3635
4107
  await userA.clients.fullAuth.contracts.verifyConsent({
@@ -4641,10 +5113,7 @@ describe('Consent Flow Contracts', () => {
4641
5113
  Achievement: ['credential:123'],
4642
5114
  },
4643
5115
  })
4644
- ).rejects.toMatchObject({
4645
- code: 'FORBIDDEN',
4646
- message: expect.stringContaining('withdrawn'),
4647
- });
5116
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
4648
5117
  });
4649
5118
 
4650
5119
  it('should deduplicate credentials when syncing', async () => {
@@ -5208,7 +5677,10 @@ describe('Consent Flow Contracts', () => {
5208
5677
  it('should remove all specified autoboosts even if some in the list do not exist on the contract', async () => {
5209
5678
  await userA.clients.fullAuth.contracts.removeAutoBoostsFromContract({
5210
5679
  contractUri,
5211
- boostUris: [boost1Uri, 'urn:lc:boost:zNonExistentBoostForRemoval'],
5680
+ boostUris: [
5681
+ boost1Uri,
5682
+ constructUri('boost', 'not-on-this-contract', 'localhost%3A3000'),
5683
+ ],
5212
5684
  });
5213
5685
  const contract = await userA.clients.fullAuth.contracts.getConsentFlowContract({
5214
5686
  uri: contractUri,