@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
@@ -1,6 +1,9 @@
1
- import { environment } from '@environment';
1
+ import { environment, getCredentialRefreshRuntimeEnvironment } from '@environment';
2
2
  import { gzipSync, gunzipSync } from 'node:zlib';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import type { Transaction } from 'neo4j-driver';
3
5
  import { v4 as uuid } from 'uuid';
6
+ import { TRPCError } from '@trpc/server';
4
7
  import type {
5
8
  AllocatedBitstringStatusListEntry,
6
9
  BitstringStatusListEntry,
@@ -20,10 +23,17 @@ import {
20
23
  } from '@learncard/helpers';
21
24
 
22
25
  import { neogma } from '@instance';
23
- import { Credential } from '@models';
24
26
  import type { StatusListPurpose, StatusListType } from '@models';
25
27
  import { getDidWebLearnCard, getLearnCard } from '@helpers/learnCard.helpers';
26
28
 
29
+ /** Resolve original deliveries and versions through anchored refresh lookups. */
30
+ export const CREDENTIAL_STATUS_ROOT_QUERY = `MATCH (credential:Credential {id: $credentialId})
31
+ OPTIONAL MATCH (rootRefresh:CredentialRefresh)-[:ROOT]->(credential)
32
+ OPTIONAL MATCH (versionRefresh:CredentialRefresh {refreshId: credential.refreshId})
33
+ WITH credential, coalesce(rootRefresh, versionRefresh) AS refresh
34
+ OPTIONAL MATCH (refresh)-[:ROOT]->(managedRoot:Credential)
35
+ WITH credential, refresh, coalesce(managedRoot, credential) AS root`;
36
+
27
37
  const hasToNumber = (value: unknown): value is { toNumber: () => number } =>
28
38
  !!value &&
29
39
  typeof value === 'object' &&
@@ -35,6 +45,111 @@ const asNumber = (value: unknown): number => {
35
45
  return Number(value);
36
46
  };
37
47
 
48
+ /** Reserve local coordinates for an ordinary delivery under the same locks as managed binding. */
49
+ export const reserveCredentialStatusEntries = async (
50
+ entries: BitstringStatusListEntry[],
51
+ ownerProfileId: string,
52
+ credentialId: string,
53
+ transaction: Transaction,
54
+ options: { lenient?: boolean; credential?: VC | UnsignedVC } = {}
55
+ ): Promise<void> => {
56
+ const domain = getCredentialRefreshRuntimeEnvironment().DOMAIN_NAME || 'localhost%3A3000';
57
+ for (const entry of [...entries].sort((a, b) =>
58
+ a.statusListCredential.localeCompare(b.statusListCredential)
59
+ )) {
60
+ const result = await transaction.run(
61
+ `MATCH (list:BitstringStatusList {statusListCredential: $url})
62
+ SET list._statusBindingLock = true REMOVE list._statusBindingLock
63
+ WITH list
64
+ OPTIONAL MATCH (list)-[:HAS_STATUS_BINDING]->(binding:CredentialStatusBinding {index: $index})
65
+ RETURN list, binding.refreshId AS refreshId, binding.credentialId AS credentialId`,
66
+ { url: entry.statusListCredential, index: String(entry.statusListIndex) }
67
+ );
68
+ const row = result.records[0];
69
+ const list = row ? nodeToStatusList(row.get('list')) : null;
70
+ if (!list) {
71
+ if (
72
+ entry.statusListCredential.startsWith(
73
+ `${getStatusListBaseUrl(domain)}/status-lists/`
74
+ )
75
+ ) {
76
+ throw new TRPCError({
77
+ code: 'BAD_REQUEST',
78
+ message: 'Local status list does not exist',
79
+ });
80
+ }
81
+ continue; // External status lists are not managed by this network.
82
+ }
83
+ // Forwarding does not confer authority over another issuer's local list.
84
+ if (options.lenient && list.ownerProfileId !== ownerProfileId) continue;
85
+ const index = Number(entry.statusListIndex);
86
+ if (
87
+ entry.statusListCredential !== getStatusListCredentialUrl(list.id, domain) ||
88
+ list.ownerProfileId !== ownerProfileId ||
89
+ list.statusPurpose !== entry.statusPurpose ||
90
+ !Number.isSafeInteger(index) ||
91
+ index < 0 ||
92
+ index >= list.size ||
93
+ index >= list.nextIndex ||
94
+ (typeof entry.statusListIndex === 'string' && String(index) !== entry.statusListIndex)
95
+ ) {
96
+ throw new TRPCError({
97
+ code: 'BAD_REQUEST',
98
+ message: 'Invalid local credential status coordinates',
99
+ });
100
+ }
101
+ if (
102
+ row?.get('refreshId') ||
103
+ (row?.get('credentialId') && row.get('credentialId') !== credentialId)
104
+ ) {
105
+ const boundId: unknown = row?.get('credentialId');
106
+ if (
107
+ options.lenient &&
108
+ !row?.get('refreshId') &&
109
+ typeof boundId === 'string' &&
110
+ options.credential?.proof &&
111
+ (!Array.isArray(options.credential.proof) || options.credential.proof.length > 0)
112
+ ) {
113
+ const existing = await transaction.run(
114
+ 'MATCH (credential:Credential {id: $id}) RETURN credential.credential AS body',
115
+ { id: boundId }
116
+ );
117
+ const body: unknown = existing.records[0]?.get('body');
118
+ if (
119
+ typeof body === 'string' &&
120
+ isDeepStrictEqual(
121
+ JSON.parse(body),
122
+ JSON.parse(JSON.stringify(options.credential))
123
+ )
124
+ ) {
125
+ await transaction.run(
126
+ `MATCH (:BitstringStatusList {statusListCredential: $url})-[:HAS_STATUS_BINDING]->(binding:CredentialStatusBinding {index: $index})
127
+ WITH binding, coalesce(binding.credentialIds, [binding.credentialId]) AS ids
128
+ SET binding.credentialIds = CASE WHEN $credentialId IN ids THEN ids ELSE ids + $credentialId END`,
129
+ {
130
+ url: entry.statusListCredential,
131
+ index: String(entry.statusListIndex),
132
+ credentialId,
133
+ }
134
+ );
135
+ continue; // Retain every delivery under the list lock, including legacy bindings.
136
+ }
137
+ }
138
+ throw new TRPCError({
139
+ code: 'BAD_REQUEST',
140
+ message: 'Status list index is already bound to another credential',
141
+ });
142
+ }
143
+ await transaction.run(
144
+ `MATCH (list:BitstringStatusList {statusListCredential: $url})
145
+ MERGE (list)-[:HAS_STATUS_BINDING]->(binding:CredentialStatusBinding {index: $index})
146
+ SET binding.credentialId = $credentialId,
147
+ binding.credentialIds = coalesce(binding.credentialIds, [$credentialId])`,
148
+ { url: entry.statusListCredential, index: String(entry.statusListIndex), credentialId }
149
+ );
150
+ }
151
+ };
152
+
38
153
  const nodeToStatusList = (node: unknown): StatusListType | null => {
39
154
  const properties = (node as { properties?: Record<string, unknown> } | undefined)?.properties;
40
155
  if (!properties) return null;
@@ -338,15 +453,25 @@ export const appendBitstringStatusListEntries = async (
338
453
 
339
454
  export const setStatusListEntryBit = async (
340
455
  entry: BitstringStatusListEntry,
341
- value: boolean
456
+ value: boolean,
457
+ ownerProfileId: string,
458
+ transaction?: Transaction
342
459
  ): Promise<boolean> => {
343
460
  const statusListIndex = Number(entry.statusListIndex);
344
461
 
345
- if (!Number.isInteger(statusListIndex) || statusListIndex < 0) {
346
- throw new Error('Bitstring status list index is out of range');
462
+ if (
463
+ !Number.isSafeInteger(statusListIndex) ||
464
+ statusListIndex < 0 ||
465
+ (typeof entry.statusListIndex === 'string' &&
466
+ String(statusListIndex) !== entry.statusListIndex)
467
+ ) {
468
+ throw new TRPCError({
469
+ code: 'BAD_REQUEST',
470
+ message: 'Bitstring status list index is out of range',
471
+ });
347
472
  }
348
473
 
349
- return neogma.getTransaction(null, async tx => {
474
+ return neogma.getTransaction(transaction ?? null, async tx => {
350
475
  const record = (
351
476
  await tx.run(
352
477
  `MATCH (list:BitstringStatusList {statusListCredential: $statusListCredential})
@@ -364,8 +489,43 @@ export const setStatusListEntryBit = async (
364
489
  const list = record ? nodeToStatusList(record.get('list')) : null;
365
490
  if (!list) return false;
366
491
 
367
- if (statusListIndex >= list.size) {
368
- throw new Error('Bitstring status list index is out of range');
492
+ const url = new URL(entry.statusListCredential);
493
+ if (
494
+ entry.statusListCredential !==
495
+ getStatusListCredentialUrl(
496
+ list.id,
497
+ getCredentialRefreshRuntimeEnvironment().DOMAIN_NAME || 'localhost%3A3000'
498
+ ) ||
499
+ url.pathname !== `/status-lists/${list.id}` ||
500
+ url.search ||
501
+ url.hash ||
502
+ url.username ||
503
+ url.password
504
+ ) {
505
+ throw new TRPCError({
506
+ code: 'BAD_REQUEST',
507
+ message: 'Status list URL must be canonical and local',
508
+ });
509
+ }
510
+
511
+ if (list.ownerProfileId !== ownerProfileId) {
512
+ throw new TRPCError({
513
+ code: 'FORBIDDEN',
514
+ message: 'Status list owner does not match the credential',
515
+ });
516
+ }
517
+ if (list.statusPurpose !== entry.statusPurpose) {
518
+ throw new TRPCError({
519
+ code: 'BAD_REQUEST',
520
+ message: 'Status list purpose does not match the credential',
521
+ });
522
+ }
523
+
524
+ if (statusListIndex >= list.size || statusListIndex >= list.nextIndex) {
525
+ throw new TRPCError({
526
+ code: 'BAD_REQUEST',
527
+ message: 'Bitstring status list index is out of range',
528
+ });
369
529
  }
370
530
 
371
531
  const bitstring = decodeBitstring(list.encodedList, list.size);
@@ -397,18 +557,9 @@ export const setCredentialBitstringStatus = async (
397
557
  statusPurpose: BitstringStatusPurpose,
398
558
  value: boolean
399
559
  ): Promise<boolean> => {
400
- const credential = await Credential.findOne({ where: { id: credentialId } });
401
- if (!credential) return false;
402
-
403
- // Encrypted SA credentials retain only public status coordinates separately.
404
- // Legacy plaintext/wrapped credentials continue using their embedded entries.
405
560
  let statusEntries: BitstringStatusListEntry[];
406
561
  try {
407
- statusEntries = BitstringStatusListEntryValidator.array().parse(
408
- credential.statusEntries
409
- ? JSON.parse(credential.statusEntries)
410
- : getBitstringStatusListEntries(JSON.parse(credential.credential))
411
- );
562
+ statusEntries = await getStatusEntriesForCredential(credentialId);
412
563
  if (
413
564
  statusEntries.some(
414
565
  entry =>
@@ -424,14 +575,172 @@ export const setCredentialBitstringStatus = async (
424
575
  });
425
576
  return false;
426
577
  }
427
- const entries = statusEntries.filter(entry => entry.statusPurpose === statusPurpose);
578
+ if (statusEntries.length === 0) return false;
428
579
 
580
+ const owner = (
581
+ await neogma.queryRunner.run(
582
+ `${CREDENTIAL_STATUS_ROOT_QUERY}
583
+ OPTIONAL MATCH (issuer:Profile)-[:CREDENTIAL_SENT]->(root)
584
+ RETURN coalesce(refresh.issuerProfileId, root.statusOwnerProfileId, issuer.profileId) AS owner LIMIT 1`,
585
+ { credentialId }
586
+ )
587
+ ).records[0]?.get('owner');
588
+ if (typeof owner !== 'string') return false;
589
+ const entries = statusEntries.filter(entry => entry.statusPurpose === statusPurpose);
429
590
  if (entries.length === 0) return false;
430
591
 
431
- const updated = await Promise.all(entries.map(entry => setStatusListEntryBit(entry, value)));
592
+ const updated = await Promise.all(
593
+ entries.map(entry => setStatusListEntryBit(entry, value, owner))
594
+ );
432
595
  return updated.every(Boolean);
433
596
  };
434
597
 
598
+ /** Public coordinates are shared by every immutable version of a managed credential. */
599
+ export const getStatusEntriesForCredential = async (
600
+ credentialId: string,
601
+ transaction?: Transaction
602
+ ): Promise<BitstringStatusListEntry[]> => {
603
+ const query = `${CREDENTIAL_STATUS_ROOT_QUERY}
604
+ RETURN credential.statusEntries AS direct, credential.credential AS body,
605
+ refresh.statusEntries AS entries LIMIT 1`;
606
+ const result = transaction
607
+ ? await transaction.run(query, { credentialId })
608
+ : await neogma.queryRunner.run(query, { credentialId });
609
+ const row = result.records[0];
610
+ if (!row) return [];
611
+ const direct: unknown = row.get('direct');
612
+ const body: unknown = row.get('body');
613
+ const directEntries = BitstringStatusListEntryValidator.array().parse(
614
+ typeof direct === 'string'
615
+ ? JSON.parse(direct)
616
+ : typeof body === 'string'
617
+ ? getBitstringStatusListEntries(JSON.parse(body))
618
+ : []
619
+ );
620
+ if (directEntries.length > 0) return directEntries;
621
+ const entries: unknown = row.get('entries');
622
+ return typeof entries === 'string'
623
+ ? BitstringStatusListEntryValidator.array().parse(JSON.parse(entries))
624
+ : [];
625
+ };
626
+
627
+ /** Resolve lifecycle requests for any immutable version to its original delivery. */
628
+ export const getCredentialStatusRootId = async (credentialId: string): Promise<string> => {
629
+ const result = await neogma.queryRunner.run(
630
+ `${CREDENTIAL_STATUS_ROOT_QUERY}
631
+ RETURN root.id AS id LIMIT 1`,
632
+ { credentialId }
633
+ );
634
+ return String(result.records[0]?.get('id') ?? credentialId);
635
+ };
636
+
637
+ /** Own-host URLs must pass local validation, even with a wrong path, port or scheme. */
638
+ export const isNetworkStatusEntry = (
639
+ entry: BitstringStatusListEntry,
640
+ domain = getCredentialRefreshRuntimeEnvironment().DOMAIN_NAME || 'localhost%3A3000'
641
+ ): boolean => {
642
+ const base = getStatusListBaseUrl(domain);
643
+ let url: URL;
644
+ try {
645
+ url = new URL(entry.statusListCredential);
646
+ } catch (cause) {
647
+ throw new TRPCError({
648
+ code: 'BAD_REQUEST',
649
+ message: 'Invalid credential status URL',
650
+ cause,
651
+ });
652
+ }
653
+ return (
654
+ entry.statusListCredential.startsWith(`${base}/status-lists/`) ||
655
+ url.hostname === new URL(base).hostname
656
+ );
657
+ };
658
+
659
+ /** Issuer-facing information, separate from the signed refresh receipt. */
660
+ export const getStatusManagement = (
661
+ entries: BitstringStatusListEntry[],
662
+ domain: string
663
+ ): 'network' | 'external' | 'mixed' | 'none' => {
664
+ if (!entries.length) return 'none';
665
+ const local = entries.filter(entry => isNetworkStatusEntry(entry, domain)).length;
666
+ return local === entries.length ? 'network' : local === 0 ? 'external' : 'mixed';
667
+ };
668
+
669
+ /** Validate external shapes and local, issuer-owned, allocated coordinates. */
670
+ export const validateStatusEntries = async (
671
+ entries: BitstringStatusListEntry[],
672
+ ownerProfileId: string,
673
+ domain: string
674
+ ): Promise<void> => {
675
+ for (const entry of entries) {
676
+ if (!BitstringStatusListEntryValidator.safeParse(entry).success) {
677
+ throw new TRPCError({
678
+ code: 'BAD_REQUEST',
679
+ message: 'Invalid credential status entry',
680
+ });
681
+ }
682
+ if (!isNetworkStatusEntry(entry, domain)) {
683
+ const index = Number(entry.statusListIndex);
684
+ if (
685
+ new URL(entry.statusListCredential).protocol !== 'https:' ||
686
+ !Number.isSafeInteger(index) ||
687
+ index < 0 ||
688
+ (typeof entry.statusListIndex === 'string' && !/^\d+$/.test(entry.statusListIndex))
689
+ ) {
690
+ throw new TRPCError({
691
+ code: 'BAD_REQUEST',
692
+ message: 'Invalid external credential status entry',
693
+ });
694
+ }
695
+ continue;
696
+ }
697
+ const prefix = `${getStatusListBaseUrl(domain)}/status-lists/`;
698
+ const id = entry.statusListCredential.slice(prefix.length);
699
+ if (!entry.statusListCredential.startsWith(prefix)) {
700
+ throw new TRPCError({
701
+ code: 'BAD_REQUEST',
702
+ message: 'Status list URL must be canonical and local',
703
+ });
704
+ }
705
+ if (!/^[a-zA-Z0-9-]+$/.test(id)) {
706
+ throw new TRPCError({
707
+ code: 'BAD_REQUEST',
708
+ message: 'Status list URL must be canonical and local',
709
+ });
710
+ }
711
+ const list = await getStatusListById(id);
712
+ const index = Number(entry.statusListIndex);
713
+ if (
714
+ !list ||
715
+ list.statusListCredential !== entry.statusListCredential ||
716
+ list.statusPurpose !== entry.statusPurpose
717
+ ) {
718
+ throw new TRPCError({
719
+ code: 'BAD_REQUEST',
720
+ message: 'Status list URL or purpose does not match the credential',
721
+ });
722
+ }
723
+ if (list.ownerProfileId !== ownerProfileId) {
724
+ throw new TRPCError({
725
+ code: 'FORBIDDEN',
726
+ message: 'Status list owner does not match the credential',
727
+ });
728
+ }
729
+ if (
730
+ !Number.isSafeInteger(index) ||
731
+ index < 0 ||
732
+ index >= list.size ||
733
+ index >= list.nextIndex ||
734
+ (typeof entry.statusListIndex === 'string' && String(index) !== entry.statusListIndex)
735
+ ) {
736
+ throw new TRPCError({
737
+ code: 'BAD_REQUEST',
738
+ message: 'Status list index is out of range',
739
+ });
740
+ }
741
+ }
742
+ };
743
+
435
744
  export const getSignedStatusListCredential = async (id: string): Promise<VC | null> => {
436
745
  const list = await getStatusListById(id);
437
746
  if (!list) return null;
@@ -58,6 +58,9 @@ export const ConsentFlowContract = ModelFactory<FlatDbContractType, ConsentFlowR
58
58
  createdAt: { type: 'string', required: true },
59
59
  updatedAt: { type: 'string', required: true },
60
60
  expiresAt: { type: 'string', required: false },
61
+ currentVersion: { type: 'number', required: false },
62
+ status: { type: 'string', required: false },
63
+ latestUpdate: { type: 'string', required: false },
61
64
  audienceVersion: { type: 'number', required: false },
62
65
  hasConsented: { type: 'boolean', required: false },
63
66
  } as any,
@@ -0,0 +1,50 @@
1
+ import { ModelFactory, NeogmaInstance } from 'neogma';
2
+ import { neogma } from '@instance';
3
+ import type { DbContractVersionType } from 'types/consentflowcontract';
4
+
5
+ type StoredVersionProperties = Omit<
6
+ DbContractVersionType,
7
+ 'contract' | 'autoboosts' | 'writers'
8
+ > & {
9
+ contract: string;
10
+ autoboosts?: string;
11
+ writers?: string;
12
+ };
13
+ type ContractVersionRelationships = Record<string, never>;
14
+ export type ConsentFlowContractVersionInstance = NeogmaInstance<
15
+ StoredVersionProperties,
16
+ ContractVersionRelationships
17
+ >;
18
+
19
+ export const ConsentFlowContractVersion = ModelFactory<
20
+ StoredVersionProperties,
21
+ ContractVersionRelationships
22
+ >(
23
+ {
24
+ label: 'ConsentFlowContractVersion',
25
+ schema: {
26
+ id: { type: 'string', required: true },
27
+ version: { type: 'number', required: true },
28
+ createdAt: { type: 'string', required: true },
29
+ contract: { type: 'string', required: true },
30
+ name: { type: 'string', required: true },
31
+ subtitle: { type: 'string', required: false },
32
+ description: { type: 'string', required: false },
33
+ reasonForAccessing: { type: 'string', required: false },
34
+ image: { type: 'string', required: false },
35
+ needsGuardianConsent: { type: 'boolean', required: false },
36
+ redirectUrl: { type: 'string', required: false },
37
+ frontDoorBoostUri: { type: 'string', required: false },
38
+ expiresAt: { type: 'string', required: false },
39
+ autoboosts: { type: 'string', required: false },
40
+ writers: { type: 'string', required: false },
41
+ reason: { type: 'string', required: false },
42
+ kind: { type: 'string', required: false },
43
+ },
44
+ relationships: {},
45
+ primaryKeyField: 'id',
46
+ },
47
+ neogma
48
+ );
49
+
50
+ export default ConsentFlowContractVersion;
@@ -33,6 +33,10 @@ export const ConsentFlowTerms = ModelFactory<FlatDbTermsType, ConsentFlowTermsRe
33
33
  createdAt: { type: 'string', required: false },
34
34
  updatedAt: { type: 'string', required: false },
35
35
  mutationVersion: { type: 'number', required: false },
36
+ consentedVersion: { type: 'number', required: false },
37
+ lastReviewedVersion: { type: 'number', required: false },
38
+ reconsentDeadline: { type: 'string', required: false },
39
+ newOptionsUntil: { type: 'string', required: false },
36
40
  expiresAt: { type: 'string', required: false },
37
41
  oneTime: { type: 'boolean', required: false },
38
42
  deniedWriters: { type: 'string[]', required: false },
@@ -34,6 +34,8 @@ export const Credential = ModelFactory<CredentialType, CredentialRelationships>(
34
34
  id: { type: 'string', required: true, uniqueItems: true },
35
35
  credential: { type: 'string', required: true },
36
36
  statusEntries: { type: 'string', required: false },
37
+ statusOwnerProfileId: { type: 'string', required: false },
38
+ statusReservationMode: { type: 'string', enum: ['strict', 'lenient'], required: false },
37
39
  },
38
40
  primaryKeyField: 'id',
39
41
  },
@@ -54,6 +54,7 @@ export const CredentialRefresh = ModelFactory<
54
54
  materialDigest: { type: 'string', required: false },
55
55
  rootMaterialDigest: { type: 'string', required: false },
56
56
  credentialStatusDigest: { type: 'string', required: false },
57
+ statusEntries: { type: 'string', required: false },
57
58
  credentialSubjectDigest: { type: 'string', required: false },
58
59
  boostId: { type: 'string', required: false },
59
60
  signingMode: {
@@ -63,6 +64,7 @@ export const CredentialRefresh = ModelFactory<
63
64
  },
64
65
  idempotencyKey: { type: 'string', required: false },
65
66
  updateSummary: { type: 'string', required: false },
67
+ updateReason: { type: 'string', required: false },
66
68
  lastPublishedAt: { type: 'string', required: false },
67
69
  notificationWindowKey: { type: 'string', required: false },
68
70
  lastNotificationId: { type: 'string', required: false },
@@ -126,6 +126,8 @@ export const Profile: any = ModelFactory<FlatProfileType, ProfileRelationships>(
126
126
  notificationsWebhook: { type: 'string', required: false },
127
127
  highlightedCredentials: { type: 'array', items: { type: 'string' }, required: false },
128
128
  dob: { type: 'string', required: false },
129
+ announcementEmailOptOut: { type: 'boolean', required: false },
130
+ announcementPushOptOut: { type: 'boolean', required: false },
129
131
  country: { type: 'string', required: false },
130
132
  role: {
131
133
  type: 'string',
@@ -4,17 +4,22 @@ let readiness: Promise<void> | undefined;
4
4
 
5
5
  /** Await uniqueness before accepting creates, even during concurrent cold starts. */
6
6
  export const ensureCampaignConstraints = (): Promise<void> => {
7
- readiness ??= neogma.queryRunner
8
- .run(
9
- 'CREATE CONSTRAINT campaign_id_unique IF NOT EXISTS FOR (c:Campaign) REQUIRE c.id IS UNIQUE'
7
+ readiness ??= Promise.all(
8
+ [
9
+ 'CREATE CONSTRAINT campaign_id_unique IF NOT EXISTS FOR (c:Campaign) REQUIRE c.id IS UNIQUE',
10
+ 'CREATE CONSTRAINT campaign_unsubscribe_id_unique IF NOT EXISTS FOR (p:Profile) REQUIRE p.campaignUnsubscribeId IS UNIQUE',
11
+ ].map(query =>
12
+ neogma.queryRunner.run(query).catch((error: unknown) => {
13
+ if (
14
+ (error as { code?: string })?.code !==
15
+ 'Neo.ClientError.Schema.EquivalentSchemaRuleAlreadyExists'
16
+ )
17
+ throw error;
18
+ })
10
19
  )
20
+ )
11
21
  .then(() => undefined)
12
22
  .catch((error: unknown) => {
13
- if (
14
- (error as { code?: string })?.code ===
15
- 'Neo.ClientError.Schema.EquivalentSchemaRuleAlreadyExists'
16
- )
17
- return;
18
23
  readiness = undefined;
19
24
  throw error;
20
25
  });
@@ -183,6 +183,7 @@ const indexQueries = [
183
183
  'CREATE INDEX contract_updated_idx IF NOT EXISTS FOR (c:ConsentFlowContract) ON (c.updatedAt)',
184
184
  'CREATE INDEX terms_updated_idx IF NOT EXISTS FOR (t:ConsentFlowTerms) ON (t.updatedAt)',
185
185
  'CREATE INDEX terms_status_idx IF NOT EXISTS FOR (t:ConsentFlowTerms) ON (t.status)',
186
+ 'CREATE INDEX consent_autoboost_claim_identity_idx IF NOT EXISTS FOR (c:ConsentFlowAutoBoostClaim) ON (c.termsId, c.boostId)',
186
187
  'CREATE INDEX claim_hook_updated_idx IF NOT EXISTS FOR (c:ClaimHook) ON (c.updatedAt)',
187
188
  'CREATE INDEX integration_id_idx IF NOT EXISTS FOR (i:Integration) ON (i.id)',
188
189
  'CREATE INDEX integration_publishablekey_idx IF NOT EXISTS FOR (i:Integration) ON (i.publishableKey)',
@@ -306,6 +307,7 @@ export * from './DidMetadata';
306
307
  export * from './SigningAuthority';
307
308
  export * from './ConsentFlowTerms';
308
309
  export * from './ConsentFlowContract';
310
+ export * from './ConsentFlowContractVersion';
309
311
  export * from './ConsentFlowTransaction';
310
312
  export * from './ContactMethod';
311
313
  export * from './InboxCredential';
package/src/openapi.ts CHANGED
@@ -57,6 +57,7 @@ if (publishRequestBody && !('$ref' in publishRequestBody)) {
57
57
  refreshId: properties.refreshId,
58
58
  notifyHolder: properties.notifyHolder!,
59
59
  updateSummary: properties.updateSummary!,
60
+ updateReason: properties.updateReason!,
60
61
  idempotencyKey: properties.idempotencyKey!,
61
62
  };
62
63
  // Generator 7.25.0 loses the OpenAPI 3.1 closed-object flag. Keep the
@@ -96,6 +97,26 @@ if (publishRequestBody && !('$ref' in publishRequestBody)) {
96
97
  }
97
98
  }
98
99
 
100
+ // Preserve the existing Python allocation identity contract without changing
101
+ // None/default serialization for unrelated optional request fields.
102
+ const allocationBody = openApiDocument.paths?.['/credential-refresh/allocate']?.post?.requestBody;
103
+ const allocationSchema =
104
+ allocationBody && !('$ref' in allocationBody)
105
+ ? allocationBody.content?.['application/json']?.schema
106
+ : undefined;
107
+ const allocationHolder =
108
+ allocationSchema && !('$ref' in allocationSchema)
109
+ ? allocationSchema.properties?.holder
110
+ : undefined;
111
+ const allocationProfile =
112
+ allocationHolder && !('$ref' in allocationHolder)
113
+ ? allocationHolder.properties?.profileId
114
+ : undefined;
115
+ if (!allocationProfile || '$ref' in allocationProfile) {
116
+ throw new Error('Managed refresh allocation schema is missing its holder profile input');
117
+ }
118
+ Object.assign(allocationProfile, { 'x-python-preserve-explicit-null': true });
119
+
99
120
  const SCALAR_HTML = `<!DOCTYPE html>
100
121
  <html lang="en">
101
122
  <head>
@@ -1721,11 +1721,12 @@ const handleRequestLearnerContextEvent = async (
1721
1721
 
1722
1722
  // Use the same live/expiry/category/guardian evaluation as data-provider reads.
1723
1723
  // An app's contract is not interchangeable with another app's or AI's grant.
1724
- const [consent] = await getConsentedDataBetweenProfiles(
1724
+ const consentPage = await getConsentedDataBetweenProfiles(
1725
1725
  owner.profileId,
1726
1726
  profile.profileId,
1727
1727
  { query: { id: contractDetails.contract.id }, limit: 1, domain: ctx.domain }
1728
1728
  );
1729
+ const consent = consentPage.records[0];
1729
1730
  if (
1730
1731
  !consent ||
1731
1732
  consent.status !== 'live' ||