@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
@@ -0,0 +1,534 @@
1
+ import { vi, describe, it, expect, beforeAll, beforeEach } from 'vitest';
2
+ import type { UnsignedVC, VP } from '@learncard/types';
3
+ const mocks = vi.hoisted(() => ({ sign: vi.fn(), verify: vi.fn(), sendEmail: vi.fn() }));
4
+ vi.mock('@helpers/signingAuthority.helpers', async original => ({
5
+ ...(await original<Record<string, unknown>>()),
6
+ issueCredentialWithSigningAuthority: mocks.sign,
7
+ }));
8
+ // This suite tests DB races and lifecycle; real-signing E2E covers authority transport/proofs.
9
+ vi.mock('@helpers/credential-refresh-proof.helpers', () => ({
10
+ verifyManagedRefreshProof: mocks.verify,
11
+ }));
12
+ vi.mock('@services/delivery/delivery.factory', () => ({
13
+ getDeliveryService: () => ({ send: mocks.sendEmail }),
14
+ }));
15
+ vi.mock('@services/registry/registry.factory', () => ({
16
+ getRegistryService: () => ({ isTrusted: async () => true }),
17
+ }));
18
+ import { neogma } from '@instance';
19
+ import { getUser, getClient } from './helpers/getClient';
20
+ import { getContactMethodByValue } from '@accesslayer/contact-method/read';
21
+ import { createProfileContactMethodRelationship } from '@accesslayer/contact-method/relationships/create';
22
+ import { verifyContactMethod } from '@accesslayer/contact-method/update';
23
+ import { getProfileByProfileId } from '@accesslayer/profile/read';
24
+ import { getInboxCredentialById } from '@accesslayer/inbox-credential/read';
25
+ import {
26
+ updateInboxCredential,
27
+ expireInboxCredentials,
28
+ } from '@accesslayer/inbox-credential/update';
29
+ import { getCredentialRefresh, getCredentialRefreshHead } from '@accesslayer/credential-refresh';
30
+ import { finalizeInboxRefresh } from '@helpers/inbox-refresh.helpers';
31
+ import { decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
32
+ import * as notifications from '@helpers/notifications.helpers';
33
+
34
+ const DOMAIN = 'localhost%3A3000';
35
+ const SA = { endpoint: 'https://sa.example.com', name: 'inbox-refresh' };
36
+ let issuer: Awaited<ReturnType<typeof getUser>>;
37
+ let holder: Awaited<ReturnType<typeof getUser>>;
38
+ let stranger: Awaited<ReturnType<typeof getUser>>;
39
+ const template = (): UnsignedVC => ({
40
+ '@context': ['https://www.w3.org/ns/credentials/v2'],
41
+ type: ['VerifiableCredential'],
42
+ name: 'Provisional Results',
43
+ issuer: 'did:web:localhost%3A3000:users:inbox-refresh-issuer',
44
+ credentialSubject: {},
45
+ });
46
+ const issue = (overrides = {}) =>
47
+ issuer.clients.fullAuth.inbox.issue({
48
+ recipient: { type: 'email', value: 'unknown@example.com' },
49
+ credential: template(),
50
+ refresh: true,
51
+ configuration: { signingAuthority: SA, delivery: { suppress: true } },
52
+ ...overrides,
53
+ });
54
+ const queue = async (
55
+ issued: Awaited<ReturnType<typeof issue>>,
56
+ name: string,
57
+ idempotencyKey?: string
58
+ ) => {
59
+ const inbox = (await getInboxCredentialById(issued.issuanceId))!;
60
+ const body = JSON.parse(await decryptInboxCredential(inbox.credential));
61
+ return issuer.clients.fullAuth.credentialRefresh.publishCredentialRefresh({
62
+ refreshId: issued.refresh!.refreshId,
63
+ mode: 'signing-authority',
64
+ credential: { ...body, name },
65
+ signingAuthority: { type: 'LearnCardSigningAuthority', ...SA },
66
+ idempotencyKey,
67
+ });
68
+ };
69
+ const claim = async (issued: Awaited<ReturnType<typeof issue>>) =>
70
+ finalizeInboxRefresh({
71
+ inboxId: issued.issuanceId,
72
+ holderDid: holder.learnCard.id.did(),
73
+ holderProfile: await getProfileByProfileId('inbox-refresh-holder'),
74
+ domain: DOMAIN,
75
+ });
76
+ const replay = (refreshId: string, idempotencyKey: string) =>
77
+ issuer.clients.fullAuth.credentialRefresh.publishCredentialRefresh({
78
+ mode: 'signing-authority',
79
+ refreshId,
80
+ credential: template(),
81
+ signingAuthority: { type: 'LearnCardSigningAuthority', ...SA },
82
+ idempotencyKey,
83
+ });
84
+
85
+ beforeAll(async () => {
86
+ issuer = await getUser('e'.repeat(64));
87
+ holder = await getUser('9'.repeat(64));
88
+ stranger = await getUser('1'.repeat(64));
89
+ vi.spyOn(notifications, 'addNotificationToQueue').mockResolvedValue();
90
+ });
91
+ beforeEach(async () => {
92
+ mocks.sendEmail.mockReset().mockResolvedValue(undefined);
93
+ // @instance is the isolated Testcontainers database, never the user's local demo stack.
94
+ await neogma.queryRunner.run('MATCH (n) DETACH DELETE n');
95
+ await issuer.clients.fullAuth.profile.createProfile({ profileId: 'inbox-refresh-issuer' });
96
+ await holder.clients.fullAuth.profile.createProfile({ profileId: 'inbox-refresh-holder' });
97
+ await stranger.clients.fullAuth.profile.createProfile({ profileId: 'inbox-refresh-stranger' });
98
+ await issuer.clients.fullAuth.profile.registerSigningAuthority({
99
+ ...SA,
100
+ did: issuer.learnCard.id.did(),
101
+ });
102
+ mocks.sign.mockReset();
103
+ mocks.verify.mockReset();
104
+ mocks.sign.mockImplementation(async (_owner, body) => ({
105
+ kind: 'issued-credential',
106
+ credential: {
107
+ ...body,
108
+ proof: {
109
+ type: 'DataIntegrityProof',
110
+ cryptosuite: 'eddsa-rdfc-2022',
111
+ proofValue: 'test',
112
+ created: new Date().toISOString(),
113
+ proofPurpose: 'assertionMethod',
114
+ verificationMethod: issuer.learnCard.id.did(),
115
+ },
116
+ },
117
+ statusEntries: [],
118
+ }));
119
+ });
120
+
121
+ describe('managed Universal Inbox refresh', () => {
122
+ it.each(['email', 'phone'] as const)(
123
+ 'allocates for unknown %s, claims latest, then publishes normally',
124
+ async type => {
125
+ const issued = await issue({
126
+ recipient: {
127
+ type,
128
+ value: type === 'email' ? 'unknown@example.com' : '+12025550123',
129
+ },
130
+ });
131
+ expect(issued.status).toBe('PENDING');
132
+ expect(issued.refresh?.holderDid).toBeUndefined();
133
+ expect(mocks.sign).not.toHaveBeenCalled();
134
+ const id = issued.refresh!.refreshId;
135
+ expect(await getCredentialRefresh(id)).toMatchObject({
136
+ state: 'pending_holder',
137
+ currentVersion: 1,
138
+ });
139
+ expect((await getInboxCredentialById(issued.issuanceId))?.credential).toMatch(
140
+ /^lc-inbox-jwe:v1:/
141
+ );
142
+ expect(await getCredentialRefreshHead(id)).toBeNull();
143
+ expect(await queue(issued, 'Review complete', 'update-1')).toMatchObject({
144
+ version: 2,
145
+ notification: 'not-applicable',
146
+ });
147
+ expect(await queue(issued, 'Final grade A', 'update-2')).toMatchObject({ version: 3 });
148
+ const delivered = await claim(issued);
149
+ expect(delivered.credential.name).toBe('Final grade A');
150
+ expect(delivered.credential.refreshService).toMatchObject({
151
+ id: issued.refresh!.refreshService.id,
152
+ });
153
+ expect(await getCredentialRefresh(id)).toMatchObject({
154
+ state: 'active',
155
+ currentVersion: 3,
156
+ holderDid: holder.learnCard.id.did(),
157
+ });
158
+ expect((await getInboxCredentialById(issued.issuanceId))?.credential).toBeUndefined();
159
+ const head = (await getCredentialRefreshHead(id))!;
160
+ expect(head.version).toBe(3);
161
+ expect(
162
+ await holder.learnCard.invoke.decryptDagJwe(JSON.parse(head.credential))
163
+ ).toMatchObject({ name: 'Final grade A' });
164
+ const issuerRead = await issuer.learnCard.invoke
165
+ .decryptDagJwe(JSON.parse(head.credential))
166
+ .catch(() => null);
167
+ expect(issuerRead).toBeFalsy();
168
+ const published =
169
+ await issuer.clients.fullAuth.credentialRefresh.publishCredentialRefresh({
170
+ mode: 'signing-authority',
171
+ refreshId: id,
172
+ credential: {
173
+ ...delivered.credential,
174
+ proof: undefined,
175
+ name: 'Final grade A with honors',
176
+ },
177
+ signingAuthority: { type: 'LearnCardSigningAuthority', ...SA },
178
+ idempotencyKey: 'after-claim',
179
+ });
180
+ expect(published.version).toBe(4);
181
+ expect(await replay(id, 'update-1')).toMatchObject({ version: 2 });
182
+ const history =
183
+ await issuer.clients.fullAuth.credentialRefresh.getCredentialRefreshHistory({
184
+ refreshId: id,
185
+ });
186
+ expect(history.records.map(v => v.version)).toEqual([4, 3, 2, 1]);
187
+ }
188
+ );
189
+ it('reuses one issuance/allocation and rejects a changed keyed request', async () => {
190
+ const first = await issue({ idempotencyKey: 'issue-key' });
191
+ expect(await issue({ idempotencyKey: 'issue-key' })).toEqual(first);
192
+ await expect(
193
+ issue({ idempotencyKey: 'issue-key', credential: { ...template(), name: 'Different' } })
194
+ ).rejects.toMatchObject({ code: 'CONFLICT' });
195
+ expect(
196
+ Number(
197
+ (
198
+ await neogma.queryRunner.run(
199
+ 'MATCH (ic:InboxCredential) RETURN count(ic) AS count'
200
+ )
201
+ ).records[0]?.get('count')
202
+ )
203
+ ).toBe(1);
204
+ });
205
+ it('re-signs latest content when publication wins during claim', async () => {
206
+ const issued = await issue();
207
+ mocks.sign.mockImplementationOnce(async (_owner, body) => {
208
+ await queue(issued, 'Published while signing', 'racing-update');
209
+ return {
210
+ kind: 'issued-credential',
211
+ credential: {
212
+ ...body,
213
+ proof: {
214
+ type: 'test',
215
+ created: new Date().toISOString(),
216
+ proofPurpose: 'assertionMethod',
217
+ verificationMethod: issuer.learnCard.id.did(),
218
+ },
219
+ },
220
+ statusEntries: [],
221
+ };
222
+ });
223
+ expect((await claim(issued)).credential.name).toBe('Published while signing');
224
+ expect(mocks.sign).toHaveBeenCalledTimes(2);
225
+ });
226
+ it('binds only one claimant and refuses guardian-gated or expired claims', async () => {
227
+ const issued = await issue();
228
+ await updateInboxCredential(issued.issuanceId, { guardianStatus: 'AWAITING_GUARDIAN' });
229
+ await expect(claim(issued)).rejects.toMatchObject({ code: 'CONFLICT' });
230
+ await updateInboxCredential(issued.issuanceId, { guardianStatus: 'GUARDIAN_APPROVED' });
231
+ const results = await Promise.allSettled([
232
+ claim(issued),
233
+ finalizeInboxRefresh({
234
+ inboxId: issued.issuanceId,
235
+ holderDid: stranger.learnCard.id.did(),
236
+ holderProfile: await getProfileByProfileId('inbox-refresh-stranger'),
237
+ domain: DOMAIN,
238
+ }),
239
+ ]);
240
+ expect(results.filter(r => r.status === 'fulfilled')).toHaveLength(1);
241
+ const expired = await issue({ recipient: { type: 'email', value: 'expired@example.com' } });
242
+ await updateInboxCredential(expired.issuanceId, { expiresAt: '2000-01-01T00:00:00.000Z' });
243
+ await expect(claim(expired)).rejects.toMatchObject({ code: 'CONFLICT' });
244
+ await expect(queue(expired, 'Too late')).rejects.toMatchObject({ code: 'CONFLICT' });
245
+ await expireInboxCredentials();
246
+ expect((await getInboxCredentialById(expired.issuanceId))?.credential).toBeUndefined();
247
+ });
248
+ it('supports the profile finalize route and exposes a bound receipt to the issuer', async () => {
249
+ const issued = await issue();
250
+ await queue(issued, 'Latest at profile claim');
251
+ const contact = (await getContactMethodByValue('email', 'unknown@example.com'))!;
252
+ await createProfileContactMethodRelationship('inbox-refresh-holder', contact.id);
253
+ await verifyContactMethod(contact.id);
254
+ await updateInboxCredential(issued.issuanceId, { isAccepted: true });
255
+ const result = await holder.clients.fullAuth.inbox.finalize({});
256
+ expect(result).toMatchObject({ claimed: 1, errors: 0 });
257
+ expect(result.verifiableCredentials[0]?.name).toBe('Latest at profile claim');
258
+ const metadata = await issuer.clients.fullAuth.inbox.getInboxCredential({
259
+ credentialId: issued.issuanceId,
260
+ });
261
+ expect(metadata.refresh?.holderDid).toBe(holder.learnCard.id.did());
262
+ });
263
+
264
+ it('supports the claim-link exchange with the newest pending version', async () => {
265
+ const issued = await issue();
266
+ await queue(issued, 'Latest at link claim');
267
+ const localExchangeId = new URL(issued.claimUrl!).pathname.split('/').pop()!;
268
+ const start = await holder.clients.fullAuth.workflows.participateInExchange({
269
+ localWorkflowId: 'inbox-claim',
270
+ localExchangeId,
271
+ });
272
+ const vp = (await holder.learnCard.invoke.getDidAuthVp({
273
+ challenge: start.verifiablePresentationRequest?.challenge,
274
+ domain: start.verifiablePresentationRequest?.domain,
275
+ })) as VP;
276
+ const result = await holder.clients.fullAuth.workflows.participateInExchange({
277
+ localWorkflowId: 'inbox-claim',
278
+ localExchangeId,
279
+ verifiablePresentation: vp,
280
+ });
281
+ expect(result.verifiablePresentation?.verifiableCredential?.[0]).toMatchObject({
282
+ name: 'Latest at link claim',
283
+ });
284
+ expect((await getCredentialRefresh(issued.refresh!.refreshId))?.state).toBe('active');
285
+ });
286
+
287
+ it('auto-delivers to a verified existing recipient with refresh enabled', async () => {
288
+ await holder.clients.fullAuth.contactMethods.addContactMethod({
289
+ type: 'email',
290
+ value: 'known@example.com',
291
+ });
292
+ await verifyContactMethod(
293
+ (await getContactMethodByValue('email', 'known@example.com'))!.id
294
+ );
295
+ mocks.sendEmail.mockClear(); // Ignore contact-method verification delivery above.
296
+ const issued = await issue({ recipient: { type: 'email', value: 'known@example.com' } });
297
+ expect(issued).toMatchObject({
298
+ status: 'ISSUED',
299
+ recipientDid: holder.learnCard.id.did(),
300
+ refresh: { holderDid: holder.learnCard.id.did() },
301
+ });
302
+ expect((await getInboxCredentialById(issued.issuanceId))?.credential).toBeUndefined();
303
+ expect((await getCredentialRefresh(issued.refresh!.refreshId))?.state).toBe(
304
+ 'awaiting_claim'
305
+ );
306
+ expect(mocks.sendEmail).not.toHaveBeenCalled();
307
+ });
308
+
309
+ it('emails existing recipients once with an app link and preserves delivery if email fails', async () => {
310
+ await holder.clients.fullAuth.contactMethods.addContactMethod({
311
+ type: 'email',
312
+ value: 'known@example.com',
313
+ });
314
+ await verifyContactMethod(
315
+ (await getContactMethodByValue('email', 'known@example.com'))!.id
316
+ );
317
+ mocks.sendEmail.mockClear(); // Ignore contact-method verification delivery above.
318
+ mocks.sendEmail.mockRejectedValueOnce(new Error('Email provider unavailable'));
319
+ const input = {
320
+ recipient: { type: 'email' as const, value: 'known@example.com' },
321
+ idempotencyKey: 'known-email',
322
+ configuration: { signingAuthority: SA, delivery: { suppress: false } },
323
+ };
324
+ const issued = await issue(input);
325
+ expect(issued.status).toBe('ISSUED');
326
+ expect(mocks.sendEmail).toHaveBeenCalledTimes(1);
327
+ expect(mocks.sendEmail).toHaveBeenCalledWith(
328
+ expect.objectContaining({
329
+ contactMethod: input.recipient,
330
+ templateId: 'universal-inbox-claim',
331
+ templateModel: expect.objectContaining({
332
+ claimUrl: expect.stringMatching(/\/notifications$/),
333
+ }),
334
+ })
335
+ );
336
+ expect((await issue(input)).issuanceId).toBe(issued.issuanceId);
337
+ expect(mocks.sendEmail).toHaveBeenCalledTimes(1);
338
+ });
339
+
340
+ it('routes unified send to the same inbox lifecycle and reuses its boost on retry', async () => {
341
+ await issuer.clients.fullAuth.profile.setPrimarySigningAuthority(SA);
342
+ const input = {
343
+ type: 'boost' as const,
344
+ recipient: 'unknown@example.com',
345
+ template: { credential: template() },
346
+ refresh: true,
347
+ idempotencyKey: 'unified-key',
348
+ options: { suppressDelivery: true },
349
+ };
350
+ const first = await issuer.clients.fullAuth.boost.send(input);
351
+ expect(first.inbox?.refresh?.refreshId).toBeTruthy();
352
+ expect(await issuer.clients.fullAuth.boost.send(input)).toEqual(first);
353
+ expect(
354
+ Number(
355
+ (
356
+ await neogma.queryRunner.run('MATCH (b:Boost) RETURN count(b) AS count')
357
+ ).records[0]?.get('count')
358
+ )
359
+ ).toBe(1);
360
+ await expect(
361
+ issuer.clients.fullAuth.boost.send({ ...input, recipient: 'different@example.com' })
362
+ ).rejects.toMatchObject({ code: 'CONFLICT' });
363
+ });
364
+
365
+ it('retains escrow when signing fails and can retry the same claim', async () => {
366
+ const issued = await issue();
367
+ mocks.sign.mockRejectedValueOnce(new Error('Authority unavailable'));
368
+ await expect(claim(issued)).rejects.toThrow('Authority unavailable');
369
+ expect((await getInboxCredentialById(issued.issuanceId))?.credential).toMatch(
370
+ /^lc-inbox-jwe:v1:/
371
+ );
372
+ expect((await getCredentialRefresh(issued.refresh!.refreshId))?.holderDid).toBeUndefined();
373
+ expect((await claim(issued)).credential.name).toBe('Provisional Results');
374
+ });
375
+
376
+ it('keeps concurrent issue retries on a single aggregate and inbox record', async () => {
377
+ const results = await Promise.all([
378
+ issue({ idempotencyKey: 'concurrent' }),
379
+ issue({ idempotencyKey: 'concurrent' }),
380
+ ]);
381
+ expect(results[0].issuanceId).toBe(results[1].issuanceId);
382
+ expect(results[0].refresh).toEqual(results[1].refresh);
383
+ expect(
384
+ Number(
385
+ (
386
+ await neogma.queryRunner.run(
387
+ 'MATCH (r:CredentialRefresh) RETURN count(r) AS count'
388
+ )
389
+ ).records[0]?.get('count')
390
+ )
391
+ ).toBe(1);
392
+ });
393
+
394
+ it('returns a valid allocation receipt for VC1 without status entries', async () => {
395
+ const issued = await issue({
396
+ credential: {
397
+ ...template(),
398
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
399
+ issuanceDate: new Date().toISOString(),
400
+ },
401
+ });
402
+ expect(issued.refresh?.credentialStatus).toBeUndefined();
403
+ expect((await claim(issued)).credential.id).toBe(issued.refresh?.credentialId);
404
+ });
405
+
406
+ it('resumes notification delivery after an auto-delivery bind committed', async () => {
407
+ await holder.clients.fullAuth.contactMethods.addContactMethod({
408
+ type: 'email',
409
+ value: 'retry-known@example.com',
410
+ });
411
+ await verifyContactMethod(
412
+ (await getContactMethodByValue('email', 'retry-known@example.com'))!.id
413
+ );
414
+ vi.mocked(notifications.addNotificationToQueue).mockRejectedValueOnce(
415
+ new Error('Queue unavailable')
416
+ );
417
+ const options = {
418
+ recipient: { type: 'email', value: 'retry-known@example.com' },
419
+ idempotencyKey: 'known-retry',
420
+ };
421
+ await expect(issue(options)).rejects.toThrow();
422
+ const retried = await issue(options);
423
+ expect(retried).toMatchObject({
424
+ status: 'ISSUED',
425
+ recipientDid: holder.learnCard.id.did(),
426
+ });
427
+ expect(mocks.sign).toHaveBeenCalledTimes(1);
428
+ expect(
429
+ (await getCredentialRefresh(retried.refresh!.refreshId))?.initialNotificationSentAt
430
+ ).toBeTruthy();
431
+ });
432
+
433
+ it('concurrent pending publications with one key create one revision', async () => {
434
+ const issued = await issue();
435
+ const outcomes = await Promise.all([
436
+ queue(issued, 'Final', 'same-key'),
437
+ queue(issued, 'Final', 'same-key'),
438
+ ]);
439
+ expect(outcomes[0]).toEqual(outcomes[1]);
440
+ expect((await getCredentialRefresh(issued.refresh!.refreshId))?.currentVersion).toBe(2);
441
+ });
442
+
443
+ it('rejects changed pending identity and mechanisms without advancing the version', async () => {
444
+ const issued = await issue();
445
+ const inbox = (await getInboxCredentialById(issued.issuanceId))!;
446
+ const body = JSON.parse(await decryptInboxCredential(inbox.credential));
447
+ for (const patch of [
448
+ { id: 'urn:uuid:different' },
449
+ { issuer: stranger.learnCard.id.did() },
450
+ { credentialSubject: { id: stranger.learnCard.id.did() } },
451
+ { credentialStatus: undefined },
452
+ { refreshService: { ...body.refreshService, id: 'https://example.com/refresh' } },
453
+ { boostId: 'different-boost' },
454
+ ]) {
455
+ await expect(
456
+ issuer.clients.fullAuth.credentialRefresh.publishCredentialRefresh({
457
+ mode: 'signing-authority',
458
+ refreshId: issued.refresh!.refreshId,
459
+ credential: { ...body, ...patch },
460
+ signingAuthority: { type: 'LearnCardSigningAuthority', ...SA },
461
+ })
462
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
463
+ }
464
+ expect((await getCredentialRefresh(issued.refresh!.refreshId))?.currentVersion).toBe(1);
465
+ expect(mocks.sign).not.toHaveBeenCalled();
466
+ });
467
+
468
+ it('requires credentials:write in addition to inbox:write before allocation', async () => {
469
+ const limited = getClient({
470
+ did: issuer.learnCard.id.did(),
471
+ isChallengeValid: true,
472
+ scope: 'inbox:write',
473
+ });
474
+ await expect(
475
+ limited.inbox.issue({
476
+ recipient: { type: 'email', value: 'unknown@example.com' },
477
+ credential: template(),
478
+ refresh: true,
479
+ configuration: { signingAuthority: SA, delivery: { suppress: true } },
480
+ })
481
+ ).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
482
+ const result = await neogma.queryRunner.run(
483
+ 'MATCH (r:CredentialRefresh) RETURN count(r) AS count'
484
+ );
485
+ expect(Number(result.records[0]?.get('count'))).toBe(0);
486
+ });
487
+
488
+ it('preserves the effective time across claim for later publication validation', async () => {
489
+ const issued = await issue({
490
+ credential: { ...template(), validFrom: '2026-09-17T00:00:00.000Z' },
491
+ });
492
+ const delivered = await claim(issued);
493
+ expect((await getCredentialRefreshHead(issued.refresh!.refreshId))?.effectiveAt).toBe(
494
+ '2026-09-17T00:00:00.000Z'
495
+ );
496
+ await expect(
497
+ issuer.clients.fullAuth.credentialRefresh.publishCredentialRefresh({
498
+ mode: 'signing-authority',
499
+ refreshId: issued.refresh!.refreshId,
500
+ credential: {
501
+ ...delivered.credential,
502
+ proof: undefined,
503
+ validFrom: '2026-09-16T00:00:00.000Z',
504
+ },
505
+ signingAuthority: { type: 'LearnCardSigningAuthority', ...SA },
506
+ })
507
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
508
+ });
509
+
510
+ it('rejects another issuer and pre-signed pending content', async () => {
511
+ const issued = await issue();
512
+ await expect(
513
+ stranger.clients.fullAuth.credentialRefresh.publishCredentialRefresh({
514
+ mode: 'signing-authority',
515
+ refreshId: issued.refresh!.refreshId,
516
+ credential: template(),
517
+ signingAuthority: { type: 'LearnCardSigningAuthority', ...SA },
518
+ })
519
+ ).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
520
+ await expect(
521
+ issue({
522
+ credential: {
523
+ ...template(),
524
+ proof: {
525
+ type: 'test',
526
+ created: new Date().toISOString(),
527
+ proofPurpose: 'assertionMethod',
528
+ verificationMethod: issuer.learnCard.id.did(),
529
+ },
530
+ },
531
+ })
532
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
533
+ });
534
+ });
@@ -4,6 +4,7 @@ import { fastifyTRPCOpenApiPlugin } from 'trpc-to-openapi';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import type { IssueInboxCredentialType, UnsignedVC, VC } from '@learncard/types';
7
+ import { getBitstringStatusListEntries } from '@learncard/helpers';
7
8
 
8
9
  import { appRouter, createContext } from '../src/app';
9
10
  import { neogma } from '@instance';
@@ -32,6 +33,10 @@ import * as encryption from '@helpers/inbox-encryption.helpers';
32
33
  import * as notifications from '@helpers/notifications.helpers';
33
34
  import * as activity from '@helpers/activity.helpers';
34
35
  import * as credentialStorage from '@accesslayer/credential/create';
36
+ import {
37
+ appendBitstringStatusListEntries,
38
+ setCredentialBitstringStatus,
39
+ } from '@helpers/status-list.helpers';
35
40
  import { testUnsignedBoost } from './helpers/send';
36
41
  import { clrMinimal } from '../../../../packages/credential-library/src/fixtures/clr/minimal';
37
42
 
@@ -154,6 +159,43 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
154
159
  ]),
155
160
  });
156
161
 
162
+ const signedStatusCredential = async (): Promise<VC> =>
163
+ issuer.learnCard.invoke.issueCredential(
164
+ await appendBitstringStatusListEntries(
165
+ {
166
+ '@context': [
167
+ 'https://www.w3.org/ns/credentials/v2',
168
+ { privateNote: 'https://schema.org/description' },
169
+ ],
170
+ type: ['VerifiableCredential'],
171
+ id: `urn:uuid:${randomUUID()}`,
172
+ validFrom: new Date().toISOString(),
173
+ issuer: issuer.learnCard.id.did(),
174
+ credentialSubject: {
175
+ id: recipient.learnCard.id.did(),
176
+ privateNote: marker,
177
+ },
178
+ },
179
+ 'escrow-issuer',
180
+ 'localhost%3A3000'
181
+ )
182
+ );
183
+
184
+ const expectStoredStatus = async (credential: VC): Promise<void> => {
185
+ const entries = getBitstringStatusListEntries(credential);
186
+ expect(entries).toHaveLength(2);
187
+ const stored = await neogma.queryRunner.run(
188
+ 'MATCH (n:Credential) RETURN n.id AS id, n.statusEntries AS statusEntries'
189
+ );
190
+ expect(stored.records).toHaveLength(1);
191
+ expect(JSON.parse(stored.records[0]!.get('statusEntries'))).toEqual(entries);
192
+ for (const purpose of ['revocation', 'suspension'] as const) {
193
+ expect(
194
+ await setCredentialBitstringStatus(stored.records[0]!.get('id'), purpose, true)
195
+ ).toBe(true);
196
+ }
197
+ };
198
+
157
199
  const verifyRecipientContact = async (email: string): Promise<void> => {
158
200
  await recipient.clients.fullAuth.contactMethods.addContactMethod({
159
201
  type: 'email',
@@ -255,7 +297,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
255
297
  name: 'Inbox concurrency test',
256
298
  category: 'Achievement',
257
299
  });
258
- const credential = await signedCredential();
300
+ const credential = await signedStatusCredential();
259
301
  const issued = await issue({
260
302
  credential,
261
303
  recipient: { type: 'email', value: 'boost-claim@example.test' },
@@ -296,6 +338,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
296
338
  'MATCH (n:Credential) RETURN n.credential AS payload'
297
339
  );
298
340
  expect(stored.records).toHaveLength(outcome === 'success' ? 1 : 0);
341
+ if (outcome === 'success') await expectStoredStatus(credential);
299
342
  const audit = await neogma.queryRunner.run(
300
343
  'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(n:InboxCredential {id: $id}) RETURN count(r) AS count',
301
344
  { id: issued.issuanceId }
@@ -612,7 +655,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
612
655
 
613
656
  it('encrypts ordinary credential storage on auto-delivery and wipes escrow', async () => {
614
657
  await verifyRecipientContact('existing@example.test');
615
- const credential = await signedCredential();
658
+ const credential = await signedStatusCredential();
616
659
  const escrowEncrypt = vi.spyOn(encryption, 'encryptInboxCredential');
617
660
  const issued = await issue({
618
661
  credential,
@@ -632,6 +675,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
632
675
  const stored = JSON.parse(deliveries.records[0]!.get('payload'));
633
676
  expect(await recipient.learnCard.invoke.decryptDagJwe(stored)).toEqual(credential);
634
677
  expect(await issuer.learnCard.invoke.decryptDagJwe(stored)).toEqual(credential);
678
+ await expectStoredStatus(credential);
635
679
  await expectNoPlaintext();
636
680
  });
637
681
 
@@ -656,7 +700,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
656
700
  isVerified: true,
657
701
  });
658
702
  const client = getVerifiedContactMethodClient({ contactMethod });
659
- const credential = await signedCredential();
703
+ const credential = await signedStatusCredential();
660
704
  const claimed = await client.inbox.claim({
661
705
  credential,
662
706
  configuration: { publishableKey: integration.publishableKey!, expiresInDays: 3 },
@@ -701,6 +745,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
701
745
  JSON.parse(stored.records[0]!.get('payload'))
702
746
  )
703
747
  ).toEqual(credential);
748
+ await expectStoredStatus(credential);
704
749
  await expectNoPlaintext();
705
750
  });
706
751