@learncard/network-brain-service 3.17.0 → 3.19.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 (274) hide show
  1. package/.env.example +4 -0
  2. package/AGENTS.md +177 -159
  3. package/CHANGELOG.md +148 -0
  4. package/README.md +26 -8
  5. package/compose.inbox.yml +7 -0
  6. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  7. package/dist/accesslayer/auth-grant/read.d.ts +1 -0
  8. package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
  9. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +3 -2
  10. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  11. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +1 -0
  12. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  13. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +5 -3
  14. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  15. package/dist/accesslayer/credential/create.d.ts +2 -1
  16. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  17. package/dist/accesslayer/credential/create.test.d.ts +2 -0
  18. package/dist/accesslayer/credential/create.test.d.ts.map +1 -0
  19. package/dist/accesslayer/credential/relationships/read.d.ts +1 -0
  20. package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
  21. package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
  22. package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
  23. package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
  24. package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
  25. package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
  26. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  27. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  28. package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
  29. package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
  30. package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
  31. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  32. package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
  33. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
  34. package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
  35. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  36. package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
  37. package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
  38. package/dist/cache/atomic-counter.test.d.ts +2 -0
  39. package/dist/cache/atomic-counter.test.d.ts.map +1 -0
  40. package/dist/cache/index.d.ts +1 -1
  41. package/dist/cache/index.d.ts.map +1 -1
  42. package/dist/config/environment.d.ts +46 -0
  43. package/dist/config/environment.d.ts.map +1 -1
  44. package/dist/credential-refresh.d.ts.map +1 -1
  45. package/dist/didkit_wasm_bg.wasm +0 -0
  46. package/dist/helpers/activity.helpers.d.ts +4 -0
  47. package/dist/helpers/activity.helpers.d.ts.map +1 -1
  48. package/dist/helpers/boost-storage.helpers.test.d.ts +2 -0
  49. package/dist/helpers/boost-storage.helpers.test.d.ts.map +1 -0
  50. package/dist/helpers/boost.helpers.d.ts +3 -7
  51. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  52. package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
  53. package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
  54. package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
  55. package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
  56. package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
  57. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
  58. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
  59. package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
  60. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
  61. package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
  62. package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
  63. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
  64. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
  65. package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
  66. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  67. package/dist/helpers/credential.helpers.d.ts +4 -2
  68. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  69. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  70. package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
  71. package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
  72. package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
  73. package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
  74. package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
  75. package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
  76. package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
  77. package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
  78. package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
  79. package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
  80. package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
  81. package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
  82. package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
  83. package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
  84. package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
  85. package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
  86. package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
  87. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
  88. package/dist/helpers/inbox.helpers.d.ts +9 -1
  89. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  90. package/dist/helpers/issuedCredentialStatus.helpers.d.ts +5 -0
  91. package/dist/helpers/issuedCredentialStatus.helpers.d.ts.map +1 -0
  92. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  93. package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
  94. package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
  95. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
  96. package/dist/helpers/shim.d.ts +1 -1
  97. package/dist/helpers/shim.d.ts.map +1 -1
  98. package/dist/helpers/signingAuthority.helpers.d.ts +3 -2
  99. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  100. package/dist/helpers/signingAuthority.helpers.test.d.ts +2 -0
  101. package/dist/helpers/signingAuthority.helpers.test.d.ts.map +1 -0
  102. package/dist/helpers/status-list.helpers.d.ts.map +1 -1
  103. package/dist/helpers/status-list.helpers.test.d.ts +2 -0
  104. package/dist/helpers/status-list.helpers.test.d.ts.map +1 -0
  105. package/dist/inbox-queue-local.d.ts +2 -0
  106. package/dist/inbox-queue-local.d.ts.map +1 -0
  107. package/dist/index.js +2 -2
  108. package/dist/models/AuthGrant.d.ts.map +1 -1
  109. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  110. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  111. package/dist/models/Credential.d.ts +1 -0
  112. package/dist/models/Credential.d.ts.map +1 -1
  113. package/dist/models/CredentialActivity.d.ts +1 -0
  114. package/dist/models/CredentialActivity.d.ts.map +1 -1
  115. package/dist/models/CredentialRefresh.d.ts +5 -2
  116. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  117. package/dist/models/InboxCredential.d.ts +1 -0
  118. package/dist/models/InboxCredential.d.ts.map +1 -1
  119. package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
  120. package/dist/openapi.d.ts.map +1 -1
  121. package/dist/routes/act-as.test.d.ts +2 -0
  122. package/dist/routes/act-as.test.d.ts.map +1 -0
  123. package/dist/routes/activity.d.ts +3 -0
  124. package/dist/routes/activity.d.ts.map +1 -1
  125. package/dist/routes/app-store.d.ts.map +1 -1
  126. package/dist/routes/auth-grants.d.ts +10 -0
  127. package/dist/routes/auth-grants.d.ts.map +1 -1
  128. package/dist/routes/boosts.d.ts +506 -12
  129. package/dist/routes/boosts.d.ts.map +1 -1
  130. package/dist/routes/contracts.d.ts +48 -2
  131. package/dist/routes/contracts.d.ts.map +1 -1
  132. package/dist/routes/credential-refreshes.d.ts +4 -4
  133. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  134. package/dist/routes/credentials.d.ts +8 -2
  135. package/dist/routes/credentials.d.ts.map +1 -1
  136. package/dist/routes/did-metadata.d.ts +8 -8
  137. package/dist/routes/federation.d.ts +2 -2
  138. package/dist/routes/inbox.d.ts +973 -31
  139. package/dist/routes/inbox.d.ts.map +1 -1
  140. package/dist/routes/index.d.ts +47 -127
  141. package/dist/routes/index.d.ts.map +1 -1
  142. package/dist/routes/presentations.d.ts +6 -6
  143. package/dist/routes/storage.d.ts +63 -63
  144. package/dist/routes/test.d.ts +6 -0
  145. package/dist/routes/test.d.ts.map +1 -1
  146. package/dist/routes/workflows.d.ts +14 -14
  147. package/dist/routes/workflows.d.ts.map +1 -1
  148. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  149. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
  150. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
  151. package/dist/services/registry/registry.factory.d.ts.map +1 -1
  152. package/dist/types/activity.d.ts +4 -0
  153. package/dist/types/activity.d.ts.map +1 -1
  154. package/dist/types/consentflowcontract.d.ts +22 -8
  155. package/dist/types/consentflowcontract.d.ts.map +1 -1
  156. package/dist/types/credential-refresh.d.ts +10 -1
  157. package/dist/types/credential-refresh.d.ts.map +1 -1
  158. package/dist/types/credential.d.ts +7 -0
  159. package/dist/types/credential.d.ts.map +1 -1
  160. package/dist/types/did-metadata.d.ts +2 -2
  161. package/dist/types/inbox-batch.d.ts +38 -0
  162. package/dist/types/inbox-batch.d.ts.map +1 -0
  163. package/inbox-e2e-setup.ts +53 -1
  164. package/inbox-elasticmq.conf +10 -0
  165. package/lambda.ts +61 -16
  166. package/package.json +18 -16
  167. package/scripts/setup-inbox-queues.ts +60 -0
  168. package/serverless-local.yml +2 -0
  169. package/serverless.yml +75 -0
  170. package/src/accesslayer/app-store-listing/relationships/read.ts +4 -3
  171. package/src/accesslayer/auth-grant/read.ts +2 -1
  172. package/src/accesslayer/consentflowcontract/relationships/create.ts +15 -5
  173. package/src/accesslayer/consentflowcontract/relationships/read.ts +161 -63
  174. package/src/accesslayer/consentflowcontract/relationships/update.ts +43 -10
  175. package/src/accesslayer/credential/create.test.ts +141 -0
  176. package/src/accesslayer/credential/create.ts +27 -3
  177. package/src/accesslayer/credential-activity/create.ts +1 -0
  178. package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
  179. package/src/accesslayer/credential-refresh/index.ts +1 -0
  180. package/src/accesslayer/credential-refresh/read.ts +8 -2
  181. package/src/accesslayer/credential-refresh/update.ts +4 -0
  182. package/src/accesslayer/inbox-batch/store.ts +469 -0
  183. package/src/accesslayer/inbox-credential/create.ts +51 -19
  184. package/src/accesslayer/inbox-credential/refresh.ts +221 -0
  185. package/src/accesslayer/inbox-credential/update.ts +1 -1
  186. package/src/cache/atomic-counter.test-helpers.ts +27 -0
  187. package/src/cache/atomic-counter.test.ts +6 -0
  188. package/src/cache/index.ts +25 -13
  189. package/src/config/environment.ts +26 -0
  190. package/src/credential-refresh.ts +5 -1
  191. package/src/docker-entry.ts +8 -0
  192. package/src/helpers/activity.helpers.ts +4 -0
  193. package/src/helpers/app-counter.helpers.ts +1 -1
  194. package/src/helpers/boost-storage.helpers.test.ts +242 -0
  195. package/src/helpers/boost.helpers.ts +58 -437
  196. package/src/helpers/contact-method.helpers.test.ts +43 -0
  197. package/src/helpers/contact-method.helpers.ts +3 -4
  198. package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
  199. package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
  200. package/src/helpers/credential-refresh-email.helpers.ts +201 -0
  201. package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
  202. package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
  203. package/src/helpers/credential-refresh.helpers.ts +385 -107
  204. package/src/helpers/credential.helpers.ts +6 -2
  205. package/src/helpers/finalize-inbox.helpers.ts +39 -22
  206. package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
  207. package/src/helpers/inbox-batch-http.helpers.ts +150 -0
  208. package/src/helpers/inbox-batch-status.helpers.ts +11 -0
  209. package/src/helpers/inbox-batch.helpers.test.ts +668 -0
  210. package/src/helpers/inbox-batch.helpers.ts +374 -0
  211. package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
  212. package/src/helpers/inbox-queue.helpers.test.ts +253 -0
  213. package/src/helpers/inbox-queue.helpers.ts +372 -0
  214. package/src/helpers/inbox-refresh.helpers.ts +432 -0
  215. package/src/helpers/inbox.helpers.ts +301 -50
  216. package/src/helpers/issuedCredentialStatus.helpers.ts +11 -0
  217. package/src/helpers/notifications.helpers.ts +4 -2
  218. package/src/helpers/rateLimit.helpers.ts +2 -5
  219. package/src/helpers/refresh-send-intent.helpers.ts +273 -0
  220. package/src/helpers/shim.ts +32 -31
  221. package/src/helpers/signingAuthority.helpers.test.ts +198 -0
  222. package/src/helpers/signingAuthority.helpers.ts +41 -18
  223. package/src/helpers/status-list.helpers.test.ts +77 -0
  224. package/src/helpers/status-list.helpers.ts +31 -8
  225. package/src/inbox-queue-local.ts +28 -0
  226. package/src/models/AuthGrant.ts +1 -0
  227. package/src/models/ConsentFlowTerms.ts +4 -0
  228. package/src/models/ConsentFlowTransaction.ts +4 -0
  229. package/src/models/Credential.ts +1 -0
  230. package/src/models/CredentialActivity.ts +3 -5
  231. package/src/models/CredentialRefresh.ts +4 -2
  232. package/src/models/InboxCredential.ts +2 -0
  233. package/src/models/credential-refresh-constraints.test.ts +8 -3
  234. package/src/models/credential-refresh-constraints.ts +5 -0
  235. package/src/openapi.ts +8 -0
  236. package/src/routes/act-as.test.ts +239 -0
  237. package/src/routes/app-store.ts +69 -52
  238. package/src/routes/auth-grants.ts +13 -11
  239. package/src/routes/boosts.ts +943 -120
  240. package/src/routes/contracts.ts +55 -7
  241. package/src/routes/credential-refreshes.test.ts +8 -2
  242. package/src/routes/credential-refreshes.ts +4 -1
  243. package/src/routes/credentials.ts +2 -1
  244. package/src/routes/inbox.ts +95 -49
  245. package/src/routes/index.ts +164 -88
  246. package/src/routes/skills.ts +1 -1
  247. package/src/routes/workflows.ts +80 -31
  248. package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
  249. package/src/services/delivery/adapters/postmark.adapter.ts +12 -0
  250. package/src/services/registry/registry.factory.ts +2 -1
  251. package/src/types/activity.ts +2 -0
  252. package/src/types/consentflowcontract.ts +21 -3
  253. package/src/types/credential-refresh.ts +21 -2
  254. package/src/types/credential.ts +13 -1
  255. package/src/types/inbox-batch.ts +46 -0
  256. package/test/app-store-listing.spec.ts +66 -30
  257. package/test/atomic-counter.e2e.ts +12 -0
  258. package/test/auth-grant.spec.ts +21 -21
  259. package/test/bitstring-status-list.spec.ts +12 -9
  260. package/test/consentflow.spec.ts +453 -26
  261. package/test/credential-refresh-email.spec.ts +663 -0
  262. package/test/credential-refresh-publication.spec.ts +13 -5
  263. package/test/guardian-gated-routes.spec.ts +337 -180
  264. package/test/helpers/inbox-e2e-environment.ts +6 -0
  265. package/test/inbox-batch.spec.ts +1338 -0
  266. package/test/inbox-refresh.spec.ts +534 -0
  267. package/test/inbox-security.e2e.ts +48 -3
  268. package/test/openapi.spec.ts +41 -1
  269. package/test/refresh-send.spec.ts +1799 -0
  270. package/test/registry.spec.ts +56 -11
  271. package/test/revoke-suspend-per-instance.spec.ts +9 -1
  272. package/vitest.config.ts +13 -0
  273. package/vitest.inbox-e2e.config.ts +5 -1
  274. package/vitest.integration.config.ts +2 -0
@@ -208,6 +208,8 @@ describe('Credential Refresh Publication', () => {
208
208
  beforeEach(async () => {
209
209
  await runQuery('MATCH (r:CredentialRefresh) DETACH DELETE r');
210
210
  await runQuery('MATCH (c:Credential) DETACH DELETE c');
211
+ // Other refresh suites allocate status lists in this shared test database.
212
+ await runQuery('MATCH (l:BitstringStatusList) DETACH DELETE l');
211
213
  await runQuery('MATCH (p:Profile) DETACH DELETE p');
212
214
 
213
215
  await issuer.clients.fullAuth.profile.createProfile({ profileId: ISSUER_PROFILE_ID });
@@ -216,8 +218,11 @@ describe('Credential Refresh Publication', () => {
216
218
 
217
219
  addNotificationToQueueSpy.mockReset();
218
220
  signingAuthorityMocks.issueCredential.mockImplementation(
219
- async (_issuer, credential: UnsignedVC) =>
220
- issuer.learnCard.invoke.issueCredential(credential)
221
+ async (_issuer, credential: UnsignedVC) => ({
222
+ kind: 'issued-credential',
223
+ credential: await issuer.learnCard.invoke.issueCredential(credential),
224
+ statusEntries: [],
225
+ })
221
226
  );
222
227
  });
223
228
 
@@ -930,11 +935,14 @@ describe('Credential Refresh Publication', () => {
930
935
  await registerIssuerSigningAuthority();
931
936
 
932
937
  signingAuthorityMocks.issueCredential.mockImplementationOnce(
933
- async (_issuer, credential: UnsignedVC) =>
934
- issuer.learnCard.invoke.issueCredential({
938
+ async (_issuer, credential: UnsignedVC) => ({
939
+ kind: 'issued-credential',
940
+ credential: await issuer.learnCard.invoke.issueCredential({
935
941
  ...credential,
936
942
  credentialSubject: { id: outsider.learnCard.id.did() },
937
- } as UnsignedVC)
943
+ } as UnsignedVC),
944
+ statusEntries: [],
945
+ })
938
946
  );
939
947
 
940
948
  await expect(
@@ -1,222 +1,379 @@
1
- import { vi, describe, it, expect, beforeEach, beforeAll } from 'vitest';
2
-
3
- vi.mock('@accesslayer/profile/relationships/read', () => ({
4
- isProfileManaged: vi.fn(),
5
- getProfilesThatManageAProfile: vi.fn(),
6
- }));
7
-
8
- vi.mock('@accesslayer/profile/read', () => ({
9
- getProfileByDid: vi.fn(),
10
- }));
11
-
12
- import { isProfileManaged, getProfilesThatManageAProfile } from '@accesslayer/profile/relationships/read';
13
- import { getProfileByDid } from '@accesslayer/profile/read';
14
- import { getLearnCard, SeedLearnCard } from '@helpers/learnCard.helpers';
1
+ import { vi, describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
2
+ import { Profile, ConsentFlowContract, ConsentFlowTerms, ConsentFlowTransaction } from '@models';
3
+ import { ConsentFlowTermsValidator } from '@learncard/types';
4
+ import {
5
+ getContractTermsByUri,
6
+ getTransactionsForTerms,
7
+ } from '@accesslayer/consentflowcontract/relationships/read';
15
8
  import { getDidWeb } from '@helpers/did.helpers';
9
+ import { getLearnCard, type SeedLearnCard } from '@helpers/learnCard.helpers';
10
+ import * as learnCardHelpers from '@helpers/learnCard.helpers';
16
11
  import { AUTH_GRANT_FULL_ACCESS_SCOPE } from 'src/constants/auth-grant';
17
- import { appRouter } from '../src/app';
18
-
19
- const mockIsProfileManaged = vi.mocked(isProfileManaged);
20
- const mockGetProfilesThatManageAProfile = vi.mocked(getProfilesThatManageAProfile);
21
- const mockGetProfileByDid = vi.mocked(getProfileByDid);
12
+ import { getClient } from './helpers/getClient';
13
+ import { minimalContract, minimalTerms, noTerms } from './helpers/contract';
22
14
 
23
15
  const DOMAIN = 'localhost%3A3000';
24
-
25
- let guardianLearnCard: SeedLearnCard;
26
- let childLearnCard: SeedLearnCard;
27
- let nonManagedLearnCard: SeedLearnCard;
28
-
29
- const mockGuardianProfile = {
30
- profileId: 'guardian-user',
31
- displayName: 'Guardian User',
32
- did: '',
16
+ let guardian: SeedLearnCard;
17
+ let child: SeedLearnCard;
18
+ let owner: SeedLearnCard;
19
+ let contractUri: string;
20
+
21
+ const approve = async (
22
+ signer = guardian,
23
+ expiresInSeconds = 300,
24
+ claims: Record<string, unknown> = {}
25
+ ): Promise<string> => {
26
+ // Preserve coverage for older clients that omit iat.
27
+ const token = await signer.invoke.getDidAuthVp({
28
+ proofFormat: 'jwt',
29
+ challenge: JSON.stringify({
30
+ iss: signer.id.did(),
31
+ sub: getDidWeb(DOMAIN, 'child-user'),
32
+ exp: Math.floor(Date.now() / 1000) + expiresInSeconds,
33
+ scope: 'guardian-approval',
34
+ ...claims,
35
+ }),
36
+ });
37
+ if (typeof token !== 'string') throw new Error('Expected a JWT presentation');
38
+ return token;
33
39
  };
34
40
 
35
- const mockChildProfile = {
36
- profileId: 'child-user',
37
- displayName: 'Child User',
38
- did: '',
39
- };
41
+ const callerFor = (signer: SeedLearnCard, guardianApproval?: string) =>
42
+ getClient({
43
+ did: signer.id.did(),
44
+ isChallengeValid: true,
45
+ scope: AUTH_GRANT_FULL_ACCESS_SCOPE,
46
+ guardianApproval,
47
+ });
40
48
 
41
- const mockNonManagedProfile = {
42
- profileId: 'non-managed-user',
43
- displayName: 'Non-Managed User',
44
- did: '',
45
- };
49
+ const childCaller = (guardianApproval?: string) => callerFor(child, guardianApproval);
46
50
 
47
- const generateGuardianApprovalToken = async (
48
- signerLearnCard: SeedLearnCard,
49
- childProfileId: string,
50
- options: { expired?: boolean; wrongScope?: boolean; wrongSubject?: boolean } = {}
51
- ): Promise<string> => {
52
- const now = Math.floor(Date.now() / 1000);
53
- const exp = options.expired ? now - 300 : now + 300;
54
- const scope = options.wrongScope ? 'invalid-scope' : 'guardian-approval';
55
- const subject = options.wrongSubject
56
- ? getDidWeb(DOMAIN, 'wrong-profile-id')
57
- : getDidWeb(DOMAIN, childProfileId);
58
-
59
- const vp = await signerLearnCard.invoke.issuePresentation(
60
- {
61
- '@context': ['https://www.w3.org/2018/credentials/v1'],
62
- type: ['VerifiablePresentation'],
63
- holder: signerLearnCard.id.did(),
51
+ const manageChild = async (): Promise<void> => {
52
+ await Profile.relateTo({
53
+ alias: 'managedBy',
54
+ where: {
55
+ source: { profileId: 'child-user' },
56
+ target: { profileId: 'guardian-user' },
64
57
  },
65
- {
66
- proofFormat: 'jwt',
67
- proofPurpose: 'authentication',
68
- challenge: JSON.stringify({ iss: signerLearnCard.id.did(), sub: subject, exp, scope }),
69
- }
70
- );
71
-
72
- if (typeof vp !== 'string') throw new Error('Failed to generate guardian approval token');
73
- return vp;
58
+ });
74
59
  };
75
60
 
76
- const createCaller = (did: string, guardianApproval?: string) => {
77
- return appRouter.createCaller({
78
- domain: DOMAIN,
79
- user: { did, isChallengeValid: true, scope: AUTH_GRANT_FULL_ACCESS_SCOPE },
80
- _guardianApprovalToken: guardianApproval,
81
- });
61
+ const clearConsentState = async (): Promise<void> => {
62
+ await ConsentFlowTransaction.delete({ detach: true, where: {} });
63
+ await ConsentFlowTerms.delete({ detach: true, where: {} });
64
+ await ConsentFlowContract.delete({ detach: true, where: {} });
65
+ await Profile.delete({ detach: true, where: {} });
82
66
  };
83
67
 
84
- describe('Guardian Gated Routes - Context Passing', () => {
68
+ describe('Guardian-approved consent mutations', () => {
85
69
  beforeAll(async () => {
86
- guardianLearnCard = await getLearnCard('a'.repeat(64));
87
- childLearnCard = await getLearnCard('b'.repeat(64));
88
- nonManagedLearnCard = await getLearnCard('c'.repeat(64));
89
-
90
- mockGuardianProfile.did = guardianLearnCard.id.did();
91
- mockChildProfile.did = childLearnCard.id.did();
92
- mockNonManagedProfile.did = nonManagedLearnCard.id.did();
70
+ guardian = await getLearnCard('a'.repeat(64));
71
+ child = await getLearnCard('b'.repeat(64));
72
+ owner = await getLearnCard('c'.repeat(64));
93
73
  });
94
74
 
95
- beforeEach(() => {
96
- vi.clearAllMocks();
75
+ beforeEach(async () => {
76
+ await clearConsentState();
77
+ await callerFor(guardian).profile.createProfile({ profileId: 'guardian-user' });
78
+ await childCaller().profile.createProfile({ profileId: 'child-user' });
79
+ await callerFor(owner).profile.createProfile({ profileId: 'contract-owner' });
80
+ contractUri = await callerFor(owner).contracts.createConsentFlowContract({
81
+ contract: minimalContract,
82
+ name: 'Guardian-approved data access',
83
+ });
97
84
  });
98
85
 
99
- describe('Non-managed profiles (isChildAccount: false)', () => {
100
- it('should pass through for non-managed profiles with isChildAccount=false', async () => {
101
- mockGetProfileByDid.mockResolvedValue(mockNonManagedProfile as any);
102
- mockIsProfileManaged.mockResolvedValue(false);
103
-
104
- const caller = createCaller(nonManagedLearnCard.id.did());
105
- const profile = await caller.profile.getProfile();
106
-
107
- expect(profile).toBeDefined();
86
+ afterAll(clearConsentState);
87
+
88
+ afterEach(() => vi.restoreAllMocks());
89
+
90
+ it.each([
91
+ { name: 'clock sixty seconds ahead', iat: 60, exp: 360, approved: true },
92
+ { name: 'clock sixty seconds behind', iat: -60, exp: 240, approved: true },
93
+ { name: 'legacy token with positive skew', iat: undefined, exp: 360, approved: true },
94
+ { name: 'iat beyond allowed skew', iat: 61, exp: 300, approved: false },
95
+ { name: 'legacy expiry beyond allowed skew', iat: undefined, exp: 361, approved: false },
96
+ { name: 'signed lifetime exceeds five minutes', iat: 59, exp: 360, approved: false },
97
+ { name: 'expiry reached exactly', iat: -300, exp: 0, approved: false },
98
+ { name: 'expiry before issuance', iat: 60, exp: 59, approved: false },
99
+ ])('enforces approval timing for $name', async ({ iat, exp, approved }) => {
100
+ await manageChild();
101
+ const now = Math.floor(Date.now() / 1000);
102
+ vi.spyOn(Date, 'now').mockReturnValue(now * 1000);
103
+ const token = await approve(guardian, exp, {
104
+ ...(iat === undefined ? {} : { iat: now + iat }),
108
105
  });
106
+ const consent = childCaller(token).contracts.consentToContract({
107
+ contractUri,
108
+ terms: minimalTerms,
109
+ });
110
+ if (approved) {
111
+ const { termsUri } = await consent;
112
+ expect((await getContractTermsByUri(termsUri))?.terms.guardianApproval).toMatchObject({
113
+ guardianProfileId: 'guardian-user',
114
+ guardianDid: guardian.id.did(),
115
+ });
116
+ } else {
117
+ await expect(consent).rejects.toMatchObject({ code: 'FORBIDDEN' });
118
+ expect(await ConsentFlowTerms.findMany({ where: {} })).toEqual([]);
119
+ }
109
120
  });
110
121
 
111
- describe('Managed profiles (isChildAccount: true)', () => {
112
- it('should pass through for managed profiles without guardian approval (hasGuardianApproval=false)', async () => {
113
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
114
- mockIsProfileManaged.mockResolvedValue(true);
115
- mockGetProfilesThatManageAProfile.mockResolvedValue([]);
122
+ it.each([
123
+ { exp: '2999999999' },
124
+ { exp: null },
125
+ { iat: '0' },
126
+ { iat: null },
127
+ { iss: 'did:example:unrelated-issuer' },
128
+ { sub: 'did:example:another-child' },
129
+ { scope: 'another-scope' },
130
+ ])('rejects malformed or mismatched signed claims %j', async claims => {
131
+ await manageChild();
132
+ await expect(
133
+ childCaller(await approve(guardian, 300, claims)).contracts.consentToContract({
134
+ contractUri,
135
+ terms: minimalTerms,
136
+ })
137
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
138
+ expect(await ConsentFlowTerms.findMany({ where: {} })).toEqual([]);
139
+ });
116
140
 
117
- const caller = createCaller(childLearnCard.id.did());
118
- const profile = await caller.profile.getProfile();
141
+ it.each(['exp', 'iat'] as const)('rejects non-finite signed %s', async claim => {
142
+ await manageChild();
143
+ const now = Math.floor(Date.now() / 1000);
144
+ // JSON numeric overflow is valid JSON, but parses to Infinity in JavaScript.
145
+ const challenge = `{
146
+ "iss": ${JSON.stringify(guardian.id.did())},
147
+ "sub": ${JSON.stringify(getDidWeb(DOMAIN, 'child-user'))},
148
+ "scope": "guardian-approval",
149
+ "exp": ${claim === 'exp' ? '1e400' : now + 300},
150
+ "iat": ${claim === 'iat' ? '1e400' : now}
151
+ }`;
152
+ const token = await guardian.invoke.getDidAuthVp({ proofFormat: 'jwt', challenge });
153
+ if (typeof token !== 'string') throw new Error('Expected a JWT presentation');
154
+ await expect(
155
+ childCaller(token).contracts.consentToContract({ contractUri, terms: minimalTerms })
156
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
157
+ expect(await ConsentFlowTerms.findMany({ where: {} })).toEqual([]);
158
+ });
119
159
 
120
- expect(profile).toBeDefined();
160
+ it.each([
161
+ { name: 'advisory warnings', errors: [], checks: ['JWS'], approved: true },
162
+ {
163
+ name: 'verification errors',
164
+ errors: ['invalid signature'],
165
+ checks: ['JWS'],
166
+ approved: false,
167
+ },
168
+ { name: 'missing JWS verification', errors: [], checks: [], approved: false },
169
+ ])('handles $name without disclosing verifier output', async ({ errors, checks, approved }) => {
170
+ await manageChild();
171
+ const token = await approve();
172
+ const verifier = await learnCardHelpers.getEmptyLearnCard();
173
+ // Keep the cryptographic verification real, then exercise its advisory/error result contract.
174
+ const verified = await verifier.invoke.verifyPresentation(token, { proofFormat: 'jwt' });
175
+ expect(verified.errors).toEqual([]);
176
+ expect(verified.checks).toContain('JWS');
177
+ vi.spyOn(learnCardHelpers, 'getEmptyLearnCard').mockResolvedValue({
178
+ ...verifier,
179
+ invoke: {
180
+ ...verifier.invoke,
181
+ verifyPresentation: vi.fn().mockResolvedValue({
182
+ ...verified,
183
+ errors,
184
+ checks,
185
+ warnings: [`advisory containing private data ${guardian.id.did()} ${token}`],
186
+ }),
187
+ },
121
188
  });
122
-
123
- it('should pass through for managed profiles with valid guardian approval (hasGuardianApproval=true)', async () => {
124
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
125
- mockIsProfileManaged.mockResolvedValue(true);
126
- mockGetProfilesThatManageAProfile.mockResolvedValue([mockGuardianProfile as any]);
127
-
128
- const token = await generateGuardianApprovalToken(guardianLearnCard, 'child-user');
129
- const caller = createCaller(childLearnCard.id.did(), token);
130
- const profile = await caller.profile.getProfile();
131
-
132
- expect(profile).toBeDefined();
189
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
190
+ const consent = childCaller(token).contracts.consentToContract({
191
+ contractUri,
192
+ terms: minimalTerms,
133
193
  });
194
+ if (approved) {
195
+ const { termsUri } = await consent;
196
+ expect((await getContractTermsByUri(termsUri))?.terms.guardianApproval).toMatchObject({
197
+ guardianProfileId: 'guardian-user',
198
+ });
199
+ } else {
200
+ await expect(consent).rejects.toMatchObject({ code: 'FORBIDDEN' });
201
+ expect(await ConsentFlowTerms.findMany({ where: {} })).toEqual([]);
202
+ }
203
+ expect(JSON.stringify(warn.mock.calls)).not.toContain(token);
204
+ expect(JSON.stringify(warn.mock.calls)).not.toContain(guardian.id.did());
205
+ expect(JSON.stringify(warn.mock.calls)).not.toContain('advisory containing private data');
206
+ });
134
207
 
135
- it('should pass through with hasGuardianApproval=false for expired token', async () => {
136
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
137
- mockIsProfileManaged.mockResolvedValue(true);
138
-
139
- const token = await generateGuardianApprovalToken(guardianLearnCard, 'child-user', { expired: true });
140
- const caller = createCaller(childLearnCard.id.did(), token);
141
- const profile = await caller.profile.getProfile();
142
-
143
- expect(profile).toBeDefined();
208
+ it('preserves adult consent without inventing guardian approval', async () => {
209
+ const { termsUri } = await childCaller().contracts.consentToContract({
210
+ contractUri,
211
+ terms: minimalTerms,
144
212
  });
213
+ const record = await getContractTermsByUri(termsUri);
214
+ expect(record?.terms.status).toBe('live');
215
+ expect(record?.terms.terms.read.personal).toEqual(minimalTerms.read.personal);
216
+ expect(record?.terms.guardianApproval).toBeUndefined();
217
+ });
145
218
 
146
- it('should pass through with hasGuardianApproval=false for wrong scope token', async () => {
147
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
148
- mockIsProfileManaged.mockResolvedValue(true);
149
-
150
- const token = await generateGuardianApprovalToken(guardianLearnCard, 'child-user', { wrongScope: true });
151
- const caller = createCaller(childLearnCard.id.did(), token);
152
- const profile = await caller.profile.getProfile();
219
+ it('does not create consent or an audit transaction without guardian approval', async () => {
220
+ await manageChild();
221
+ await expect(
222
+ childCaller().contracts.consentToContract({ contractUri, terms: minimalTerms })
223
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
224
+ expect(await ConsentFlowTerms.findMany({ where: {} })).toEqual([]);
225
+ expect(await ConsentFlowTransaction.findMany({ where: {} })).toEqual([]);
226
+ });
153
227
 
154
- expect(profile).toBeDefined();
228
+ it('records the verified guardian and contract version on consent and permission updates', async () => {
229
+ await manageChild();
230
+ const beforeApproval = Date.now();
231
+ const token = await approve();
232
+ const caller = childCaller(token);
233
+ const { termsUri } = await caller.contracts.consentToContract({
234
+ contractUri,
235
+ terms: minimalTerms,
155
236
  });
156
-
157
- it('should pass through with hasGuardianApproval=false for non-guardian signer', async () => {
158
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
159
- mockIsProfileManaged.mockResolvedValue(true);
160
- mockGetProfilesThatManageAProfile.mockResolvedValue([mockGuardianProfile as any]);
161
-
162
- const nonGuardianLearnCard = await getLearnCard('d'.repeat(64));
163
- const token = await generateGuardianApprovalToken(nonGuardianLearnCard, 'child-user');
164
- const caller = createCaller(childLearnCard.id.did(), token);
165
- const profile = await caller.profile.getProfile();
166
-
167
- expect(profile).toBeDefined();
237
+ const consent = await getContractTermsByUri(termsUri);
238
+ const approval = consent?.terms.guardianApproval;
239
+ expect(consent?.terms.status).toBe('live');
240
+ expect(consent?.terms.terms.read.personal).toEqual(minimalTerms.read.personal);
241
+ expect(approval).toEqual({
242
+ guardianProfileId: 'guardian-user',
243
+ guardianDid: guardian.id.did(),
244
+ approvedAt: expect.any(String),
245
+ contractUpdatedAt: consent?.contract.updatedAt,
168
246
  });
169
-
170
- it('should pass through with hasGuardianApproval=false for wrong subject token', async () => {
171
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
172
- mockIsProfileManaged.mockResolvedValue(true);
173
-
174
- const token = await generateGuardianApprovalToken(guardianLearnCard, 'child-user', { wrongSubject: true });
175
- const caller = createCaller(childLearnCard.id.did(), token);
176
- const profile = await caller.profile.getProfile();
177
-
178
- expect(profile).toBeDefined();
247
+ expect(Date.parse(approval!.approvedAt)).toBeGreaterThanOrEqual(beforeApproval);
248
+ expect(Date.parse(approval!.approvedAt)).toBeLessThanOrEqual(Date.now());
249
+ const consentTransactions = await getTransactionsForTerms(consent!.terms.id, { limit: 10 });
250
+ expect(consentTransactions).toEqual([
251
+ expect.objectContaining({ action: 'consent', guardianApproval: approval }),
252
+ ]);
253
+
254
+ await caller.contracts.updateConsentedContractTerms({
255
+ uri: termsUri,
256
+ terms: ConsentFlowTermsValidator.parse({}),
257
+ });
258
+ const updated = await getContractTermsByUri(termsUri);
259
+ const providerData = await callerFor(owner).contracts.getConsentedDataForDid({
260
+ did: child.id.did(),
179
261
  });
262
+ expect(providerData.records).toEqual([
263
+ expect.objectContaining({
264
+ termsUri,
265
+ personal: {},
266
+ credentials: [],
267
+ guardian: expect.objectContaining({ required: true, approved: true }),
268
+ }),
269
+ ]);
270
+ expect(updated?.terms.guardianApproval).toMatchObject({
271
+ guardianProfileId: 'guardian-user',
272
+ guardianDid: guardian.id.did(),
273
+ contractUpdatedAt: updated?.contract.updatedAt,
274
+ });
275
+ const transactions = await getTransactionsForTerms(updated!.terms.id, { limit: 10 });
276
+ expect(transactions).toHaveLength(2);
277
+ expect(transactions).toEqual(
278
+ expect.arrayContaining([
279
+ expect.objectContaining({ action: 'consent', guardianApproval: approval }),
280
+ expect.objectContaining({
281
+ action: 'update',
282
+ guardianApproval: updated?.terms.guardianApproval,
283
+ }),
284
+ ])
285
+ );
180
286
  });
181
287
 
182
- describe('finalize — credential-level guardian gating', () => {
183
- it('should succeed for managed child and return guardianPending count', async () => {
184
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
185
- mockIsProfileManaged.mockResolvedValue(true);
186
- mockGetProfilesThatManageAProfile.mockResolvedValue([mockGuardianProfile as any]);
187
-
188
- // No guardian approval token needed — finalize now uses profileRoute
189
- // Guardian gating happens at the credential level, not route level
190
- const caller = createCaller(childLearnCard.id.did());
191
-
192
- const result = await caller.inbox.finalize({});
193
- expect(result).toBeDefined();
194
- expect(result.processed).toBeGreaterThanOrEqual(0);
195
- expect(result.guardianPending).toBeGreaterThanOrEqual(0);
288
+ it('leaves consent unchanged when the approval window has expired', async () => {
289
+ await manageChild();
290
+ const { termsUri } = await childCaller(await approve()).contracts.consentToContract({
291
+ contractUri,
292
+ terms: minimalTerms,
196
293
  });
294
+ const before = await getContractTermsByUri(termsUri);
295
+ await expect(
296
+ childCaller(await approve(guardian, -1)).contracts.updateConsentedContractTerms({
297
+ uri: termsUri,
298
+ terms: noTerms,
299
+ })
300
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
301
+ expect((await getContractTermsByUri(termsUri))?.terms).toEqual(before?.terms);
302
+ const transactions = await getTransactionsForTerms(before!.terms.id, { limit: 10 });
303
+ expect(transactions).toHaveLength(1);
304
+ });
197
305
 
198
- it('should succeed for non-managed profile with guardianPending=0', async () => {
199
- mockGetProfileByDid.mockResolvedValue(mockNonManagedProfile as any);
200
- mockIsProfileManaged.mockResolvedValue(false);
201
-
202
- const caller = createCaller(nonManagedLearnCard.id.did());
306
+ it('requires a current authorized manager, not another valid presentation signer', async () => {
307
+ await manageChild();
308
+ await expect(
309
+ childCaller(await approve(owner)).contracts.consentToContract({
310
+ contractUri,
311
+ terms: minimalTerms,
312
+ })
313
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
314
+ expect(await ConsentFlowTerms.findMany({ where: {} })).toEqual([]);
315
+ });
203
316
 
204
- const result = await caller.inbox.finalize({});
205
- expect(result).toBeDefined();
206
- expect(result.processed).toBeGreaterThanOrEqual(0);
207
- expect(result.guardianPending).toBe(0);
317
+ it('requires a new guardian-confirmed update for legacy managed consent', async () => {
318
+ const { termsUri } = await childCaller().contracts.consentToContract({
319
+ contractUri,
320
+ terms: minimalTerms,
208
321
  });
322
+ await manageChild();
323
+ await expect(
324
+ childCaller().contracts.updateConsentedContractTerms({ uri: termsUri, terms: noTerms })
325
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
326
+ const legacy = await getContractTermsByUri(termsUri);
327
+ expect(legacy?.terms.terms.read.personal).toEqual(minimalTerms.read.personal);
328
+ expect(legacy?.terms.guardianApproval).toBeUndefined();
329
+
330
+ await childCaller(await approve()).contracts.updateConsentedContractTerms({
331
+ uri: termsUri,
332
+ terms: noTerms,
333
+ });
334
+ const approved = await getContractTermsByUri(termsUri);
335
+ expect(approved?.terms.terms.read.personal?.name).toBeUndefined();
336
+ expect(approved?.terms.guardianApproval?.guardianProfileId).toBe('guardian-user');
337
+ const transactions = await getTransactionsForTerms(approved!.terms.id, { limit: 10 });
338
+ expect(
339
+ transactions.find(transaction => transaction.action === 'consent')?.guardianApproval
340
+ ).toBeUndefined();
341
+ expect(
342
+ transactions.find(transaction => transaction.action === 'update')?.guardianApproval
343
+ ).toEqual(approved?.terms.guardianApproval);
344
+ });
209
345
 
210
- it('should include guardianPending in response schema', async () => {
211
- mockGetProfileByDid.mockResolvedValue(mockChildProfile as any);
212
- mockIsProfileManaged.mockResolvedValue(true);
213
- mockGetProfilesThatManageAProfile.mockResolvedValue([mockGuardianProfile as any]);
214
-
215
- const caller = createCaller(childLearnCard.id.did());
216
-
217
- const result = await caller.inbox.finalize({});
218
- expect(result).toHaveProperty('guardianPending');
219
- expect(typeof result.guardianPending).toBe('number');
346
+ it('cannot downgrade historical child consent when its last manager is removed', async () => {
347
+ await manageChild();
348
+ const { termsUri } = await childCaller(await approve()).contracts.consentToContract({
349
+ contractUri,
350
+ terms: minimalTerms,
351
+ });
352
+ const before = await getContractTermsByUri(termsUri);
353
+ await Profile.delete({ detach: true, where: { profileId: 'guardian-user' } });
354
+ await expect(
355
+ childCaller().contracts.updateConsentedContractTerms({ uri: termsUri, terms: noTerms })
356
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
357
+ expect((await getContractTermsByUri(termsUri))?.terms).toEqual(before?.terms);
358
+
359
+ await childCaller().contracts.withdrawConsent({ uri: termsUri });
360
+ await expect(
361
+ childCaller().contracts.consentToContract({ contractUri, terms: noTerms })
362
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
363
+ expect((await getContractTermsByUri(termsUri))?.terms.status).toBe('withdrawn');
364
+
365
+ // Withdrawal does not erase guardian history. Recovery needs current authority,
366
+ // not a reclassification as an adult based on the missing manager relationship.
367
+ await callerFor(guardian).profile.createProfile({ profileId: 'guardian-user' });
368
+ await manageChild();
369
+ const recovered = await childCaller(await approve()).contracts.consentToContract({
370
+ contractUri,
371
+ terms: minimalTerms,
372
+ });
373
+ expect(recovered.termsUri).toBe(termsUri);
374
+ expect((await getContractTermsByUri(termsUri))?.terms).toMatchObject({
375
+ status: 'live',
376
+ guardianApproval: { guardianProfileId: 'guardian-user' },
220
377
  });
221
378
  });
222
379
  });
@@ -4,11 +4,17 @@ declare module 'vitest' {
4
4
  export interface ProvidedContext {
5
5
  'inbox-redis-host': string;
6
6
  'inbox-redis-port': number;
7
+ 'inbox-queue-endpoint': string;
8
+ 'inbox-queue-url': string;
9
+ 'inbox-dead-letter-url': string;
7
10
  }
8
11
  }
9
12
 
10
13
  // Set before importing the app, which captures its validated environment once.
11
14
  process.env.REDIS_HOST = inject('inbox-redis-host');
15
+ process.env.INBOX_QUEUE_ENDPOINT = inject('inbox-queue-endpoint');
16
+ process.env.INBOX_QUEUE_URL = inject('inbox-queue-url');
17
+ process.env.INBOX_DEAD_LETTER_QUEUE_URL = inject('inbox-dead-letter-url');
12
18
  process.env.REDIS_PORT = String(inject('inbox-redis-port'));
13
19
  process.env.NEO4J_URI = inject('neo4j-uri');
14
20
  process.env.NEO4J_USERNAME = 'neo4j';