@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,432 @@
1
+ import { randomUUID, createHmac } from 'node:crypto';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { UnsignedVCValidator, VCValidator } from '@learncard/types';
4
+ import type {
5
+ InboxCredentialRefreshReceipt,
6
+ PublishCredentialRefreshInput,
7
+ PublishCredentialRefreshResult,
8
+ VC,
9
+ } from '@learncard/types';
10
+ import {
11
+ canonicalizeCredentialJson,
12
+ injectManagedRefreshService,
13
+ getManagedRefreshServices,
14
+ getCredentialEffectiveTime,
15
+ getCredentialIssuerId,
16
+ getBitstringStatusListEntries,
17
+ } from '@learncard/helpers';
18
+ import { getCredentialRefreshRuntimeEnvironment } from '@environment';
19
+ import { AUTH_GRANT_NO_ACCESS_SCOPE } from '../constants/auth-grant';
20
+ import { ensureCredentialRefreshConstraints } from '../models/credential-refresh-constraints';
21
+ import { getCredentialRefresh, generateRefreshId } from '@accesslayer/credential-refresh';
22
+ import { getInboxCredentialById } from '@accesslayer/inbox-credential/read';
23
+ import {
24
+ advancePendingInboxRefresh,
25
+ bindPendingInboxRefresh,
26
+ getInboxRefreshIssue,
27
+ getPendingInboxPublication,
28
+ getInboxRefreshSnapshot,
29
+ } from '@accesslayer/inbox-credential/refresh';
30
+ import { getProfileByDid, getProfileByProfileId } from '@accesslayer/profile/read';
31
+ import { getProfilesThatManageAProfile } from '@accesslayer/profile/relationships/read';
32
+ import { getSigningAuthorityForUserByName } from '@accesslayer/signing-authority/relationships/read';
33
+ import { getBoostByUri } from '@accesslayer/boost/read';
34
+ import { INBOX_DELIVERY_RETENTION_DAYS } from 'types/inbox-delivery';
35
+ import type { ProfileType } from 'types/profile';
36
+ import type { CredentialRefreshRecord } from 'types/credential-refresh';
37
+ import { userHasRequiredScopes } from './auth-grant.helpers';
38
+ import { getDidWeb } from './did.helpers';
39
+ import { encryptInboxCredential, decryptInboxCredential } from './inbox-encryption.helpers';
40
+ import {
41
+ getCredentialRefreshServiceUrl,
42
+ computeRefreshEtag,
43
+ getManagedCredentialUri,
44
+ getInitialRefreshRoot,
45
+ sendInitialCredentialNotificationOnce,
46
+ } from './credential-refresh.helpers';
47
+ import {
48
+ getCredentialRefreshDigestSecret,
49
+ computeCredentialMaterialDigest,
50
+ computeCredentialStatusDigest,
51
+ computeCredentialSubjectDigest,
52
+ } from './credential-refresh-materiality.helpers';
53
+ import { appendBitstringStatusListEntries } from './status-list.helpers';
54
+ import { issueCredentialWithSigningAuthority } from './signingAuthority.helpers';
55
+ import { verifyManagedRefreshProof } from './credential-refresh-proof.helpers';
56
+ import { createDagJweForRecipients, getLearnCard } from './learnCard.helpers';
57
+ import { isRelationshipBlocked } from './connection.helpers';
58
+
59
+ export const assertInboxRefreshEnabled = async (scope?: string): Promise<void> => {
60
+ if (!getCredentialRefreshRuntimeEnvironment().CREDENTIAL_REFRESH_ENABLED)
61
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Credential refresh is not available' });
62
+ if (!userHasRequiredScopes(scope ?? AUTH_GRANT_NO_ACCESS_SCOPE, 'credentials:write'))
63
+ throw new TRPCError({
64
+ code: 'UNAUTHORIZED',
65
+ message: 'This operation requires credentials:write scope',
66
+ });
67
+ await ensureCredentialRefreshConstraints();
68
+ };
69
+
70
+ export const inboxRefreshRequestDigest = (request: unknown): string =>
71
+ createHmac('sha256', getCredentialRefreshDigestSecret())
72
+ .update('inbox-refresh-issue:v1\0')
73
+ .update(canonicalizeCredentialJson(request))
74
+ .digest('base64url');
75
+
76
+ export const getInboxRefreshIssueKey = (issuerProfileId: string, key: string): string =>
77
+ `${issuerProfileId}:${key}`;
78
+
79
+ export const getInboxRefreshReplay = async (
80
+ issuerProfileId: string,
81
+ key: string | undefined,
82
+ requestDigest: string
83
+ ) => {
84
+ if (!key) return null;
85
+ const existing = await getInboxRefreshIssue(getInboxRefreshIssueKey(issuerProfileId, key));
86
+ if (existing && existing.requestDigest !== requestDigest)
87
+ throw new TRPCError({
88
+ code: 'CONFLICT',
89
+ message: 'This idempotencyKey was used for a different inbox issuance.',
90
+ });
91
+ return existing;
92
+ };
93
+
94
+ const serviceFor = (refreshId: string, domain: string) => ({
95
+ id: getCredentialRefreshServiceUrl(refreshId, domain),
96
+ type: 'LearnCardCredentialRefresh2026' as const,
97
+ authorization: { type: 'LearnCardDIDAuth' as const },
98
+ });
99
+
100
+ /** Metadata only: status coordinates are public mechanisms, never credential claims. */
101
+ export const getInboxRefreshReceipt = async (
102
+ refreshId: string,
103
+ domain: string
104
+ ): Promise<InboxCredentialRefreshReceipt> => {
105
+ const aggregate = await getCredentialRefresh(refreshId);
106
+ if (!aggregate)
107
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Credential refresh not found' });
108
+ const status = aggregate.inboxCredentialStatus
109
+ ? JSON.parse(aggregate.inboxCredentialStatus)
110
+ : undefined;
111
+ return {
112
+ refreshId,
113
+ refreshService: serviceFor(refreshId, domain),
114
+ credentialId: aggregate.credentialId,
115
+ issuerDid: aggregate.issuerDid,
116
+ ...(aggregate.holderDid ? { holderDid: aggregate.holderDid } : {}),
117
+ ...(status ? { credentialStatus: status } : {}),
118
+ };
119
+ };
120
+
121
+ export const prepareInboxRefresh = async (
122
+ credential: unknown,
123
+ issuer: ProfileType,
124
+ domain: string
125
+ ) => {
126
+ const body = UnsignedVCValidator.parse(credential);
127
+ if (
128
+ body.proof ||
129
+ !body.type.includes('VerifiableCredential') ||
130
+ body.type.includes('VerifiablePresentation') ||
131
+ Array.isArray(body.credentialSubject)
132
+ ) {
133
+ throw new TRPCError({
134
+ code: 'BAD_REQUEST',
135
+ message:
136
+ 'Inbox refresh requires an unsigned credential with a single subject and a signing authority.',
137
+ });
138
+ }
139
+ if (getManagedRefreshServices(body).length)
140
+ throw new TRPCError({
141
+ code: 'BAD_REQUEST',
142
+ message:
143
+ 'Inbox refresh allocates its own refresh service; omit an existing managed service.',
144
+ });
145
+ const refreshId = generateRefreshId();
146
+ const issuerDid = getDidWeb(domain, issuer.profileId);
147
+ // Inbox is explicitly addressed by verified contact; the eventual claimant binds the subject.
148
+ const { id: _id, did: _did, ...subject } = body.credentialSubject;
149
+ const prepared = injectManagedRefreshService(
150
+ await appendBitstringStatusListEntries(
151
+ {
152
+ ...body,
153
+ id: body.id || `urn:uuid:${randomUUID()}`,
154
+ issuer: issuerDid,
155
+ credentialSubject: subject,
156
+ },
157
+ issuer.profileId,
158
+ domain
159
+ ),
160
+ serviceFor(refreshId, domain)
161
+ );
162
+ const now = new Date().toISOString();
163
+ return {
164
+ credential: prepared,
165
+ aggregate: {
166
+ refreshId,
167
+ issuerProfileId: issuer.profileId,
168
+ issuerDid,
169
+ credentialId: prepared.id!,
170
+ state: 'pending_holder',
171
+ currentVersion: 1,
172
+ createdAt: now,
173
+ updatedAt: now,
174
+ credentialStatusDigest: computeCredentialStatusDigest(prepared.credentialStatus),
175
+ inboxCredentialStatus: JSON.stringify(prepared.credentialStatus ?? null),
176
+ materialDigest: computeCredentialMaterialDigest(prepared),
177
+ },
178
+ };
179
+ };
180
+
181
+ /** Pending publications remain unsigned; finalization adds the verified holder before signing. */
182
+ export const publishPendingInboxRefresh = async (
183
+ aggregate: CredentialRefreshRecord,
184
+ issuer: ProfileType,
185
+ input: PublishCredentialRefreshInput,
186
+ domain: string
187
+ ): Promise<PublishCredentialRefreshResult> => {
188
+ const replay = await getPendingInboxPublication(aggregate.refreshId, input.idempotencyKey);
189
+ if (replay) return replay;
190
+ if (input.mode !== 'signing-authority')
191
+ throw new TRPCError({
192
+ code: 'BAD_REQUEST',
193
+ message:
194
+ 'Pending inbox refresh requires unsigned content in signing-authority mode; the holder is bound at claim.',
195
+ });
196
+ const inbox = await getInboxCredentialById(aggregate.inboxCredentialId!);
197
+ if (!inbox || inbox.currentStatus !== 'PENDING' || Date.parse(inbox.expiresAt) <= Date.now())
198
+ throw new TRPCError({ code: 'CONFLICT', message: 'Inbox credential is no longer pending' });
199
+ const authority = inbox.signingAuthority;
200
+ if (
201
+ !authority?.endpoint ||
202
+ !authority.name ||
203
+ input.signingAuthority.endpoint !== authority.endpoint ||
204
+ String(input.signingAuthority.name).toLowerCase() !== authority.name.toLowerCase() ||
205
+ !(await getSigningAuthorityForUserByName(issuer, authority.endpoint, authority.name))
206
+ )
207
+ throw new TRPCError({
208
+ code: 'UNAUTHORIZED',
209
+ message: 'Publication must use the inbox issuance signing authority',
210
+ });
211
+ const body = input.credential;
212
+ const previous = UnsignedVCValidator.parse(
213
+ JSON.parse(await decryptInboxCredential(inbox.credential))
214
+ );
215
+ if (
216
+ body.proof ||
217
+ Array.isArray(body.credentialSubject) ||
218
+ body.id !== aggregate.credentialId ||
219
+ getCredentialIssuerId(body) !== aggregate.issuerDid ||
220
+ computeCredentialSubjectDigest(body.credentialSubject) !==
221
+ computeCredentialSubjectDigest(previous.credentialSubject) ||
222
+ body.boostId !== previous.boostId ||
223
+ computeCredentialStatusDigest(body.credentialStatus) !== aggregate.credentialStatusDigest ||
224
+ canonicalizeCredentialJson(body.refreshService) !==
225
+ canonicalizeCredentialJson(previous.refreshService)
226
+ )
227
+ throw new TRPCError({
228
+ code: 'BAD_REQUEST',
229
+ message:
230
+ 'Pending update must preserve credential identity, issuer, subject, boost, status and refresh service.',
231
+ });
232
+ const previousTime = getCredentialEffectiveTime(previous);
233
+ const nextTime = getCredentialEffectiveTime(body);
234
+ if (previousTime !== undefined && nextTime !== undefined && nextTime < previousTime)
235
+ throw new TRPCError({
236
+ code: 'BAD_REQUEST',
237
+ message: 'Credential effective time is older than the current version',
238
+ });
239
+ const prepared = injectManagedRefreshService(body, serviceFor(aggregate.refreshId, domain));
240
+ const result = await advancePendingInboxRefresh({
241
+ refreshId: aggregate.refreshId,
242
+ expectedVersion: aggregate.currentVersion,
243
+ credential: await encryptInboxCredential(JSON.stringify(prepared)),
244
+ credentialName: typeof prepared.name === 'string' ? prepared.name : undefined,
245
+ materialDigest: computeCredentialMaterialDigest(prepared),
246
+ idempotencyKey: input.idempotencyKey,
247
+ updateSummary: input.updateSummary,
248
+ });
249
+ if (!result)
250
+ throw new TRPCError({
251
+ code: 'CONFLICT',
252
+ message: 'Inbox refresh changed during publication; retry with current state.',
253
+ });
254
+ return result;
255
+ };
256
+
257
+ /** Claim/finalize share this path. A racing publish forces a re-read and a fresh signature. */
258
+ export const finalizeInboxRefresh = async (params: {
259
+ inboxId: string;
260
+ holderDid: string;
261
+ holderProfile?: ProfileType | null;
262
+ domain: string;
263
+ accepted?: boolean;
264
+ }): Promise<{ credential: VC; uri: string }> => {
265
+ const { inboxId, holderDid, holderProfile, domain, accepted = true } = params;
266
+ if (!getCredentialRefreshRuntimeEnvironment().CREDENTIAL_REFRESH_ENABLED)
267
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Credential refresh is not available' });
268
+ for (let attempt = 0; attempt < 3; attempt++) {
269
+ const snapshot = await getInboxRefreshSnapshot(inboxId);
270
+ const inbox = snapshot?.inbox;
271
+ if (
272
+ !inbox?.refreshId ||
273
+ inbox.currentStatus !== 'PENDING' ||
274
+ Date.parse(inbox.expiresAt) <= Date.now() ||
275
+ (inbox.guardianStatus && inbox.guardianStatus !== 'GUARDIAN_APPROVED')
276
+ )
277
+ throw new TRPCError({
278
+ code: 'CONFLICT',
279
+ message: 'Inbox credential is no longer eligible for claim',
280
+ });
281
+ const aggregate = snapshot?.aggregate;
282
+ if (!aggregate || aggregate.state !== 'pending_holder')
283
+ throw new TRPCError({ code: 'CONFLICT', message: 'Inbox credential is already bound' });
284
+ const issuer = await getProfileByDid(inbox.issuerDid);
285
+ if (!issuer || (holderProfile && (await isRelationshipBlocked(issuer, holderProfile))))
286
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found' });
287
+ const authority =
288
+ inbox.signingAuthority?.endpoint && inbox.signingAuthority?.name
289
+ ? await getSigningAuthorityForUserByName(
290
+ issuer,
291
+ inbox.signingAuthority.endpoint,
292
+ inbox.signingAuthority.name
293
+ )
294
+ : null;
295
+ if (!authority)
296
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Signing authority not found' });
297
+ const template = UnsignedVCValidator.parse(
298
+ JSON.parse(await decryptInboxCredential(inbox.credential))
299
+ );
300
+ if (Array.isArray(template.credentialSubject))
301
+ throw new TRPCError({
302
+ code: 'BAD_REQUEST',
303
+ message: 'Inbox refresh requires one subject',
304
+ });
305
+ const unsigned = injectManagedRefreshService(
306
+ {
307
+ ...template,
308
+ issuer: aggregate.issuerDid,
309
+ credentialSubject: { ...template.credentialSubject, id: holderDid },
310
+ },
311
+ serviceFor(aggregate.refreshId, domain)
312
+ );
313
+ const credential = VCValidator.parse(
314
+ (
315
+ await issueCredentialWithSigningAuthority(
316
+ { type: 'profile', profile: issuer },
317
+ unsigned,
318
+ authority,
319
+ domain,
320
+ false,
321
+ undefined,
322
+ false
323
+ )
324
+ ).credential
325
+ );
326
+ if (
327
+ credential.id !== aggregate.credentialId ||
328
+ getCredentialIssuerId(credential) !== aggregate.issuerDid ||
329
+ computeCredentialSubjectDigest(credential.credentialSubject) !==
330
+ computeCredentialSubjectDigest(unsigned.credentialSubject) ||
331
+ computeCredentialStatusDigest(credential.credentialStatus) !==
332
+ aggregate.credentialStatusDigest ||
333
+ canonicalizeCredentialJson(credential.refreshService) !==
334
+ canonicalizeCredentialJson(unsigned.refreshService) ||
335
+ computeCredentialMaterialDigest(credential) !==
336
+ computeCredentialMaterialDigest(unsigned)
337
+ )
338
+ throw new TRPCError({
339
+ code: 'BAD_REQUEST',
340
+ message: 'Signing authority changed the prepared inbox credential',
341
+ });
342
+ await verifyManagedRefreshProof(
343
+ (await getLearnCard()).invoke,
344
+ credential,
345
+ aggregate.issuerDid
346
+ );
347
+ const managers = holderProfile
348
+ ? await getProfilesThatManageAProfile(holderProfile.profileId)
349
+ : [];
350
+ const encrypted = JSON.stringify(
351
+ await createDagJweForRecipients(credential, [
352
+ ...new Set([holderProfile?.did ?? holderDid, ...managers.map(p => p.did)]),
353
+ ])
354
+ );
355
+ const boost = inbox.boostUri ? await getBoostByUri(inbox.boostUri) : null;
356
+ if (inbox.boostUri && !boost)
357
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Boost not found' });
358
+ const id = randomUUID();
359
+ const now = new Date().toISOString();
360
+ const materialDigest = computeCredentialMaterialDigest(credential);
361
+ const etag = computeRefreshEtag(encrypted);
362
+ const effectiveTime = getCredentialEffectiveTime(credential);
363
+ const bound = await bindPendingInboxRefresh({
364
+ inboxId,
365
+ refreshId: aggregate.refreshId,
366
+ expectedVersion: aggregate.currentVersion,
367
+ issuerProfileId: issuer.profileId,
368
+ holderDid,
369
+ holderProfileId: holderProfile?.profileId,
370
+ boostId: boost?.id,
371
+ accepted,
372
+ deliveryCredential: encrypted,
373
+ deliveryExpiresAt: new Date(
374
+ Date.now() + INBOX_DELIVERY_RETENTION_DAYS * 86400000
375
+ ).toISOString(),
376
+ root: {
377
+ id,
378
+ credential: encrypted,
379
+ statusEntries: JSON.stringify(getBitstringStatusListEntries(credential)),
380
+ refreshId: aggregate.refreshId,
381
+ version: aggregate.currentVersion,
382
+ refreshVersionKey: `${aggregate.refreshId}:${aggregate.currentVersion}`,
383
+ publishedAt: aggregate.lastPublishedAt ?? now,
384
+ signingMode: 'signing-authority',
385
+ ...(effectiveTime === undefined
386
+ ? {}
387
+ : { effectiveAt: new Date(effectiveTime).toISOString() }),
388
+ ...(aggregate.updateSummary ? { updateSummary: aggregate.updateSummary } : {}),
389
+ etag,
390
+ },
391
+ aggregate: {
392
+ etag,
393
+ initialNotificationSuppressed:
394
+ accepted || issuer.profileId === holderProfile?.profileId,
395
+ materialDigest,
396
+ rootMaterialDigest: materialDigest,
397
+ credentialSubjectDigest: computeCredentialSubjectDigest(
398
+ credential.credentialSubject
399
+ ),
400
+ ...(boost ? { boostId: boost.id } : {}),
401
+ },
402
+ });
403
+ if (bound) return { credential, uri: getManagedCredentialUri(id, domain) };
404
+ }
405
+ throw new TRPCError({
406
+ code: 'CONFLICT',
407
+ message: 'Inbox refresh changed during claim; retry.',
408
+ });
409
+ };
410
+
411
+ /** Resume the initial notification after binding committed but delivery failed. No re-signing. */
412
+ export const resumeInboxRefreshDelivery = async (
413
+ refreshId: string,
414
+ domain: string
415
+ ): Promise<void> => {
416
+ const aggregate = await getCredentialRefresh(refreshId);
417
+ if (!aggregate?.holderProfileId || aggregate.initialNotificationSuppressed) return;
418
+ const root = await getInitialRefreshRoot(refreshId);
419
+ if (!root) return;
420
+ const [issuerProfile, holderProfile] = await Promise.all([
421
+ getProfileByProfileId(aggregate.issuerProfileId),
422
+ getProfileByProfileId(aggregate.holderProfileId),
423
+ ]);
424
+ if (!issuerProfile || !holderProfile) return;
425
+ await sendInitialCredentialNotificationOnce({
426
+ refreshId,
427
+ uri: getManagedCredentialUri(root.rootId, domain),
428
+ issuerProfile,
429
+ holderProfile,
430
+ initialNotificationSuppressed: false,
431
+ });
432
+ };