@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
@@ -9,7 +9,7 @@ import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
9
9
  import { OpenApiMeta } from 'trpc-to-openapi';
10
10
  import jwtDecode from 'jwt-decode';
11
11
  import * as Sentry from '@sentry/serverless';
12
- import { AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX } from '@learncard/types';
12
+ import { ACT_AS_HEADER, AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX } from '@learncard/types';
13
13
  import { ContactMethodType } from '@learncard/types';
14
14
 
15
15
  import { RegExpTransformer } from '@learncard/helpers';
@@ -48,7 +48,11 @@ export type Context = {
48
48
  did: string;
49
49
  isChallengeValid: boolean;
50
50
  scope?: string;
51
+ isAuthGrant?: boolean;
52
+ actAsPolicy?: string;
53
+ onBehalfOf?: string;
51
54
  };
55
+ actAs?: string;
52
56
  contactMethod?: ContactMethodType;
53
57
  domain: string;
54
58
  tenant: ResolvedTenant;
@@ -84,6 +88,14 @@ export const createContext = async (
84
88
  | { req: { headers: Map<string, string> } }
85
89
  ): Promise<Context> => {
86
90
  const event = 'event' in options ? options.event : options.req;
91
+ const headerEntries =
92
+ 'get' in event.headers
93
+ ? Array.from(event.headers as Map<string, string>)
94
+ : Object.entries(event.headers);
95
+ const rawActAs = headerEntries.find(
96
+ ([name]) => name.toLowerCase() === ACT_AS_HEADER.toLowerCase()
97
+ )?.[1];
98
+ const actAs = Array.isArray(rawActAs) ? rawActAs.join(',') : rawActAs;
87
99
  const authHeader =
88
100
  'get' in event.headers
89
101
  ? (event.headers as Map<string, string>).get('authorization')
@@ -141,6 +153,7 @@ export const createContext = async (
141
153
  if (!challenge)
142
154
  return {
143
155
  user: { did, isChallengeValid: false, scope: AUTH_GRANT_NO_ACCESS_SCOPE },
156
+ actAs,
144
157
  domain,
145
158
  tenant,
146
159
  sourceIp,
@@ -148,6 +161,8 @@ export const createContext = async (
148
161
 
149
162
  let isChallengeValid = false;
150
163
  let scope = AUTH_GRANT_FULL_ACCESS_SCOPE;
164
+ let isAuthGrant = false;
165
+ let actAsPolicy: string | undefined;
151
166
 
152
167
  // If the user is using a provisional auth token for a contact method:
153
168
  if (challenge?.includes(CONTACT_METHOD_SESSION_PREFIX)) {
@@ -159,6 +174,7 @@ export const createContext = async (
159
174
  if (!contactMethod) throw new TRPCError({ code: 'NOT_FOUND' });
160
175
  return {
161
176
  contactMethod,
177
+ actAs,
162
178
  domain,
163
179
  tenant,
164
180
  sourceIp,
@@ -166,9 +182,14 @@ export const createContext = async (
166
182
  }
167
183
  // If the user is using a real auth grant i.e. an API Token.
168
184
  } else if (challenge?.includes(AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX)) {
169
- const { isChallengeValid: _isChallengeValid, scope: _scope } =
170
- await isAuthGrantChallengeValidForDID(challenge, did);
171
-
185
+ const {
186
+ isChallengeValid: _isChallengeValid,
187
+ scope: _scope,
188
+ actAs: policy,
189
+ } = await isAuthGrantChallengeValidForDID(challenge, did);
190
+
191
+ isAuthGrant = true;
192
+ actAsPolicy = policy;
172
193
  isChallengeValid = _isChallengeValid;
173
194
  scope = _scope;
174
195
  // If the user is using a real challenge signed by their private key.
@@ -182,7 +203,8 @@ export const createContext = async (
182
203
  Sentry.setUser({ id: did });
183
204
 
184
205
  return {
185
- user: { did, isChallengeValid, scope },
206
+ user: { did, isChallengeValid, scope, isAuthGrant, actAsPolicy },
207
+ actAs,
186
208
  domain,
187
209
  tenant,
188
210
  _guardianApprovalToken,
@@ -192,7 +214,7 @@ export const createContext = async (
192
214
  }
193
215
  }
194
216
 
195
- return { domain, tenant, _guardianApprovalToken, sourceIp };
217
+ return { domain, tenant, _guardianApprovalToken, sourceIp, actAs };
196
218
  };
197
219
 
198
220
  export const openRoute = t.procedure
@@ -245,6 +267,55 @@ export const resolveProfileFromContextDid = async (
245
267
  return getProfileByDid(did);
246
268
  };
247
269
 
270
+ type ActingUser = NonNullable<Context['user']> & { profile: ProfileType | null };
271
+
272
+ const resolveActAs = async (
273
+ user: ActingUser,
274
+ actAs: string | undefined,
275
+ domain: string
276
+ ): Promise<ActingUser> => {
277
+ const requested = actAs?.trim();
278
+ if (!requested) return user;
279
+
280
+ const { profile } = user;
281
+ const prefix = `did:web:${domain}:users:`;
282
+ const profileId = requested.startsWith(prefix) ? requested.slice(prefix.length) : requested;
283
+ if (!profileId || /[:/?#]/.test(profileId)) {
284
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid act-as profile identifier' });
285
+ }
286
+
287
+ // Two DB lookups per request when the act-as header is present (target profile,
288
+ // then its managers) — acceptable at backend issuance volume; not worth caching.
289
+ const target = await getProfileByProfileId(profileId);
290
+ if (!target)
291
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Act-as target profile not found' });
292
+
293
+ const managers = await getProfilesThatManageAProfile(target.profileId);
294
+ if (!profile || !managers.some(manager => manager.profileId === profile.profileId)) {
295
+ throw new TRPCError({
296
+ code: 'FORBIDDEN',
297
+ message: `You do not manage profile "${profileId}".`,
298
+ });
299
+ }
300
+
301
+ const policy = user.actAsPolicy;
302
+ const permitted =
303
+ policy === '*' ||
304
+ policy
305
+ ?.split(',')
306
+ .map(id => id.trim())
307
+ .includes(target.profileId);
308
+ if (user.isAuthGrant && !permitted) {
309
+ throw new TRPCError({
310
+ code: 'FORBIDDEN',
311
+ message: `This API token may not act as "${profileId}". Grant actAs on the token.`,
312
+ });
313
+ }
314
+
315
+ Sentry.setUser({ id: target.profileId, username: target.displayName });
316
+ return { ...user, profile: target, did: target.did, onBehalfOf: profile.profileId };
317
+ };
318
+
248
319
  export const didRoute = openRoute.use(async ({ ctx, next }) => {
249
320
  if (!ctx.user?.did) {
250
321
  throw new TRPCError({ code: 'UNAUTHORIZED' });
@@ -254,7 +325,11 @@ export const didRoute = openRoute.use(async ({ ctx, next }) => {
254
325
 
255
326
  if (profile) Sentry.setUser({ id: profile.profileId, username: profile.displayName });
256
327
 
257
- return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
328
+ const user = ctx.user.isChallengeValid
329
+ ? await resolveActAs({ ...ctx.user, profile }, ctx.actAs, ctx.domain)
330
+ : { ...ctx.user, profile };
331
+
332
+ return next({ ctx: { ...ctx, user } });
258
333
  });
259
334
 
260
335
  export const didAndChallengeRoute = didRoute.use(({ ctx, next }) => {
@@ -373,99 +448,100 @@ export const verifiedContactRoute = openRoute.use(async ({ ctx, next }) => {
373
448
  export type GuardianApprovalToken = {
374
449
  iss: string;
375
450
  sub: string;
376
- iat: number;
451
+ iat?: number;
377
452
  exp: number;
378
453
  scope: string;
379
454
  };
380
455
 
456
+ // Match the existing guardian UI's five-minute approval window.
457
+ const GUARDIAN_APPROVAL_MAX_TTL_SECONDS = 5 * 60;
458
+ const GUARDIAN_APPROVAL_CLOCK_SKEW_SECONDS = 60;
459
+
381
460
  export const guardianGatedRoute = profileRoute.use(async ({ ctx, next }) => {
382
461
  const { profile } = ctx.user;
383
462
  const guardianApprovalToken = ctx._guardianApprovalToken;
384
-
385
463
  const isChildAccount = await isProfileManaged(profile.profileId);
464
+ let guardianIdentity: { profileId: string; did: string } | undefined;
386
465
 
387
- if (!isChildAccount) {
388
- return next({
389
- ctx: { ...ctx, isChildAccount: false, hasGuardianApproval: false },
390
- });
391
- }
392
-
393
- if (!guardianApprovalToken) {
394
- return next({
395
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
396
- });
397
- }
398
-
399
- try {
400
- const learnCard = await getEmptyLearnCard();
401
-
402
- const result = await learnCard.invoke.verifyPresentation(guardianApprovalToken, {
403
- proofFormat: 'jwt',
404
- });
405
-
406
- if (result.errors.length > 0 || !result.checks.includes('JWS')) {
407
- return next({
408
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
409
- });
410
- }
411
-
412
- const jwtPayload = jwtDecode<{ vp?: { proof?: { challenge?: string } }; nonce?: string }>(
413
- guardianApprovalToken
414
- );
415
-
416
- const challengeStr = jwtPayload.vp?.proof?.challenge ?? jwtPayload.nonce;
417
- if (!challengeStr) {
418
- return next({
419
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
420
- });
421
- }
422
-
423
- let guardianClaims: GuardianApprovalToken;
466
+ if (isChildAccount && guardianApprovalToken) {
424
467
  try {
425
- guardianClaims = JSON.parse(challengeStr);
426
- } catch {
427
- return next({
428
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
429
- });
430
- }
431
-
432
- if (!guardianClaims.exp || guardianClaims.exp * 1000 < Date.now()) {
433
- return next({
434
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
435
- });
436
- }
437
-
438
- if (guardianClaims.scope !== 'guardian-approval') {
439
- return next({
440
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
441
- });
442
- }
443
-
444
- const childDidWeb = getDidWeb(ctx.domain, profile.profileId);
445
- if (guardianClaims.sub !== childDidWeb) {
446
- return next({
447
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
468
+ const learnCard = await getEmptyLearnCard();
469
+ const result = await learnCard.invoke.verifyPresentation(guardianApprovalToken, {
470
+ proofFormat: 'jwt',
448
471
  });
449
- }
450
-
451
- const managers = await getProfilesThatManageAProfile(profile.profileId);
452
- const guardianProfile = managers.find(manager => {
453
- const managerDidWeb = getDidWeb(ctx.domain, manager.profileId);
454
- return guardianClaims.iss === managerDidWeb || guardianClaims.iss === manager.did;
455
- });
456
472
 
457
- if (!guardianProfile) {
458
- return next({
459
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
460
- });
473
+ // Verifier warnings are advisory; errors and a missing JWS check are fatal.
474
+ // Identity and current manager authorization are checked independently below.
475
+ if (result.errors.length === 0 && result.checks.includes('JWS')) {
476
+ const jwtHeader = jwtDecode<{ kid?: string }>(guardianApprovalToken, {
477
+ header: true,
478
+ });
479
+ const jwtPayload = jwtDecode<{
480
+ iss?: string;
481
+ vp?: { holder?: string; proof?: { challenge?: string } };
482
+ nonce?: string;
483
+ }>(guardianApprovalToken);
484
+ const challenge = jwtPayload.vp?.proof?.challenge ?? jwtPayload.nonce;
485
+ const claims: GuardianApprovalToken | null =
486
+ typeof challenge === 'string' ? JSON.parse(challenge) : null;
487
+ const now = Date.now() / 1000;
488
+ const signerDid =
489
+ typeof jwtHeader.kid === 'string' ? jwtHeader.kid.split('#')[0] : undefined;
490
+
491
+ // Verification proves the key's signature; bind every identity claim to that key.
492
+ // Older clients omit iat. Allow clock skew without extending the signed lifetime
493
+ // or accepting a token whose actual expiry has passed.
494
+ if (
495
+ claims &&
496
+ signerDid &&
497
+ jwtPayload.iss === signerDid &&
498
+ jwtPayload.vp?.holder === signerDid &&
499
+ claims.iss === signerDid &&
500
+ typeof claims.exp === 'number' &&
501
+ Number.isFinite(claims.exp) &&
502
+ claims.exp > now &&
503
+ claims.exp <=
504
+ now +
505
+ GUARDIAN_APPROVAL_MAX_TTL_SECONDS +
506
+ GUARDIAN_APPROVAL_CLOCK_SKEW_SECONDS &&
507
+ (claims.iat === undefined ||
508
+ (typeof claims.iat === 'number' &&
509
+ Number.isFinite(claims.iat) &&
510
+ claims.iat <= now + GUARDIAN_APPROVAL_CLOCK_SKEW_SECONDS &&
511
+ claims.exp > claims.iat &&
512
+ claims.exp - claims.iat <= GUARDIAN_APPROVAL_MAX_TTL_SECONDS)) &&
513
+ claims.scope === 'guardian-approval' &&
514
+ claims.sub === getDidWeb(ctx.domain, profile.profileId)
515
+ ) {
516
+ const managers = await getProfilesThatManageAProfile(profile.profileId);
517
+ const guardian = managers.find(
518
+ manager =>
519
+ signerDid === manager.did ||
520
+ signerDid === getDidWeb(ctx.domain, manager.profileId)
521
+ );
522
+ if (guardian) {
523
+ guardianIdentity = { profileId: guardian.profileId, did: signerDid };
524
+ } else {
525
+ console.warn('guardian_approval: unauthorized_manager');
526
+ }
527
+ } else {
528
+ console.warn('guardian_approval: invalid_claims');
529
+ }
530
+ } else {
531
+ console.warn('guardian_approval: verification_failed');
532
+ }
533
+ } catch {
534
+ // Malformed or unverifiable presentations never authorize a guardian-only mutation.
535
+ console.warn('guardian_approval: malformed_or_unverifiable');
461
536
  }
462
-
463
- return next({
464
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: true },
465
- });
466
- } catch {
467
- return next({
468
- ctx: { ...ctx, isChildAccount: true, hasGuardianApproval: false },
469
- });
470
537
  }
538
+
539
+ return next({
540
+ ctx: {
541
+ ...ctx,
542
+ isChildAccount,
543
+ hasGuardianApproval: guardianIdentity !== undefined,
544
+ guardianIdentity,
545
+ },
546
+ });
471
547
  });
@@ -241,7 +241,7 @@ export const skillsRouter = t.router({
241
241
 
242
242
  if (rateCount > SEMANTIC_SEARCH_RATE_LIMIT_PER_MIN) {
243
243
  throw new TRPCError({
244
- code: 'TOO_MANY_REQUESTS' as 'BAD_REQUEST',
244
+ code: 'TOO_MANY_REQUESTS',
245
245
  message: `Rate limit exceeded: max ${SEMANTIC_SEARCH_RATE_LIMIT_PER_MIN} semantic searches per minute`,
246
246
  });
247
247
  }
@@ -1,3 +1,4 @@
1
+ import { finalizeInboxRefresh } from '@helpers/inbox-refresh.helpers';
1
2
  import { TRPCError } from '@trpc/server';
2
3
  import { z } from 'zod';
3
4
  import base64url from 'base64url';
@@ -5,6 +6,7 @@ import base64url from 'base64url';
5
6
  import {
6
7
  VC,
7
8
  UnsignedVC,
9
+ JWE,
8
10
  VP,
9
11
  VPValidator,
10
12
  VCValidator,
@@ -48,6 +50,7 @@ import { resolveRecipientLocale } from '@helpers/getRecipientLocale.helpers';
48
50
  import { logCredentialClaimed, logCredentialFailed } from '@helpers/activity.helpers';
49
51
  import { handleConnectionPromptsForCredentialClaim } from '@helpers/connectionPrompt.helpers';
50
52
  import { decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
53
+ import { getBitstringStatusListEntries } from '@learncard/helpers';
51
54
  import {
52
55
  EXHAUSTED,
53
56
  exhaustExchangeChallengeForToken,
@@ -347,6 +350,7 @@ async function handlePresentationForClaim(
347
350
  const verificationResult = await learnCard.invoke.verifyPresentation(verifiablePresentation, {
348
351
  challenge,
349
352
  domain,
353
+ proofPurpose: 'authentication',
350
354
  });
351
355
 
352
356
  if (verificationResult.errors.length > 0 || !verificationResult.checks.includes('proof')) {
@@ -405,13 +409,15 @@ async function handlePresentationForClaim(
405
409
  // Inject OBv3 skill alignments based on boost's framework/skills
406
410
  await injectObv3AlignmentsIntoCredentialForBoost(boostCredential, boost, domain);
407
411
 
408
- const vc = (await issueCredentialWithSigningAuthority(
409
- { type: 'profile', profile: saOwnerProfile },
410
- boostCredential,
411
- signingAuthorityForUser,
412
- domain,
413
- false
414
- )) as VC;
412
+ const vc = (
413
+ await issueCredentialWithSigningAuthority(
414
+ { type: 'profile', profile: saOwnerProfile },
415
+ boostCredential,
416
+ signingAuthorityForUser,
417
+ domain,
418
+ false
419
+ )
420
+ ).credential as VC;
415
421
 
416
422
  // Mark the challenge as used
417
423
  await useClaimLinkForBoost(exchangeInfo.boostUri, exchangeInfo.challenge);
@@ -543,6 +549,7 @@ async function handleInboxClaimPresentation(
543
549
  const verificationResult = await learnCard.invoke.verifyPresentation(verifiablePresentation, {
544
550
  challenge,
545
551
  domain: ctx.domain,
552
+ proofPurpose: 'authentication',
546
553
  });
547
554
 
548
555
  if (verificationResult.errors.length > 0 || !verificationResult.checks.includes('proof')) {
@@ -594,19 +601,34 @@ async function handleInboxClaimPresentation(
594
601
  }
595
602
 
596
603
  const claimedCredentials: VC[] = [];
604
+ let deliveryEncryptionFailed = false;
597
605
 
598
606
  // Process each pending credential in parallel
599
607
  const credentialProcessingPromises = pendingCredentials.map(async inboxCredential => {
600
608
  try {
601
609
  let finalCredential: VC;
602
- const credentialPayload = await decryptInboxCredential(inboxCredential.credential);
603
-
604
- if (inboxCredential.isSigned) {
610
+ const credentialPayload = inboxCredential.refreshId
611
+ ? undefined
612
+ : await decryptInboxCredential(inboxCredential.credential);
613
+
614
+ if (inboxCredential.refreshId) {
615
+ // A contact token authorizes delivery to the VP's DID. Never attach an unrelated
616
+ // contact profile's decryption key to that holder's refresh aggregate.
617
+ const boundProfile = await getProfileByDid(holderDid);
618
+ finalCredential = (
619
+ await finalizeInboxRefresh({
620
+ inboxId: inboxCredential.id,
621
+ holderDid,
622
+ holderProfile: boundProfile,
623
+ domain: ctx.domain,
624
+ })
625
+ ).credential;
626
+ } else if (inboxCredential.isSigned) {
605
627
  // Credential is already signed
606
- finalCredential = JSON.parse(credentialPayload) as VC;
628
+ finalCredential = JSON.parse(credentialPayload!) as VC;
607
629
  } else {
608
630
  // Need to sign the credential using signing authority
609
- const unsignedCredential = JSON.parse(credentialPayload) as UnsignedVC;
631
+ const unsignedCredential = JSON.parse(credentialPayload!) as UnsignedVC;
610
632
  const inboxCredentialSigningAuthorityEndpoint =
611
633
  (inboxCredential.signingAuthority?.endpoint as string) ?? undefined;
612
634
  const inboxCredentialSigningAuthorityName =
@@ -658,25 +680,40 @@ async function handleInboxClaimPresentation(
658
680
  unsignedCredential.issuer = signingAuthorityForUser.relationship.did;
659
681
 
660
682
  // Sign the credential
661
- finalCredential = (await issueCredentialWithSigningAuthority(
662
- { type: 'profile', profile: issuerProfile },
663
- unsignedCredential,
664
- signingAuthorityForUser,
665
- ctx.domain,
666
- false // don't encrypt
667
- )) as VC;
683
+ finalCredential = (
684
+ await issueCredentialWithSigningAuthority(
685
+ { type: 'profile', profile: issuerProfile },
686
+ unsignedCredential,
687
+ signingAuthorityForUser,
688
+ ctx.domain,
689
+ false // don't encrypt
690
+ )
691
+ ).credential as VC;
668
692
  }
669
693
 
670
- // Avoid the seeded encryption wrapper, which adds the service as a recipient.
671
- const deliveryLearnCard = await getEmptyLearnCard();
672
- const encryptedDelivery = await deliveryLearnCard.invoke.createDagJwe(finalCredential, [
673
- holderDid,
674
- ]);
675
- const finalized = await finalizeAndWipeInboxCredential(inboxCredential.id, {
676
- recipientDid: holderDid,
677
- credential: encryptedDelivery,
678
- });
679
- if (!finalized) throw new Error('Inbox credential is no longer pending');
694
+ let encryptedDelivery: JWE | undefined;
695
+ if (!inboxCredential.refreshId) {
696
+ // Avoid the seeded encryption wrapper, which adds the service as a recipient.
697
+ const deliveryLearnCard = await getEmptyLearnCard();
698
+
699
+ try {
700
+ encryptedDelivery = await deliveryLearnCard.invoke.createDagJwe(
701
+ finalCredential,
702
+ [holderDid]
703
+ );
704
+ if (!encryptedDelivery.recipients?.length) {
705
+ throw new Error('No supported delivery encryption recipients');
706
+ }
707
+ } catch (error) {
708
+ deliveryEncryptionFailed = true;
709
+ throw error;
710
+ }
711
+ const finalized = await finalizeAndWipeInboxCredential(inboxCredential.id, {
712
+ recipientDid: holderDid,
713
+ credential: encryptedDelivery,
714
+ });
715
+ if (!finalized) throw new Error('Inbox credential is no longer pending');
716
+ }
680
717
 
681
718
  // Record the claim only after finalization succeeds so failed compare-and-swap
682
719
  // attempts cannot leave a misleading audit relationship behind.
@@ -690,14 +727,18 @@ async function handleInboxClaimPresentation(
690
727
 
691
728
  // Store credential and create boost relationship if this was a boost issuance
692
729
  const boostUri = inboxCredential.boostUri;
693
- if (holderProfile && boostUri) {
730
+ if (!inboxCredential.refreshId && holderProfile && boostUri) {
694
731
  try {
695
732
  const boost = await getBoostByUri(boostUri);
696
733
  const issuerProfile = await getProfileByDid(inboxCredential.issuerDid);
697
734
 
698
735
  if (boost && issuerProfile) {
699
736
  // Store the credential in the database
700
- const credentialInstance = await storeCredential(encryptedDelivery);
737
+ const credentialInstance = await storeCredential({
738
+ kind: 'issued-credential',
739
+ credential: encryptedDelivery!,
740
+ statusEntries: getBitstringStatusListEntries(finalCredential),
741
+ });
701
742
 
702
743
  // Create the boost instance relationship
703
744
  await createBoostInstanceOfRelationship(credentialInstance, boost);
@@ -868,6 +909,14 @@ async function handleInboxClaimPresentation(
868
909
 
869
910
  const settledCredentials = await Promise.all(credentialProcessingPromises);
870
911
  const inboxDeliveries = settledCredentials.filter(c => c !== null);
912
+ if (inboxDeliveries.length === 0 && deliveryEncryptionFailed) {
913
+ // Preserve the pending credentials and challenge so a compatible holder can retry.
914
+ throw new TRPCError({
915
+ code: 'BAD_REQUEST',
916
+ message:
917
+ 'Unable to encrypt inbox delivery. The holder DID must resolve to a supported X25519 key-agreement method.',
918
+ });
919
+ }
871
920
  claimedCredentials.push(...inboxDeliveries.map(delivery => delivery.credential));
872
921
 
873
922
  // Create response VP with all claimed credentials
@@ -0,0 +1,54 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type { Notification } from '../delivery.service';
3
+
4
+ const mocks = vi.hoisted(() => ({ send: vi.fn(), legacySend: vi.fn(), render: vi.fn() }));
5
+ vi.mock('@environment', () => ({ environment: {} }));
6
+ vi.mock('postmark', () => ({
7
+ ServerClient: class {
8
+ sendEmail = mocks.send;
9
+ sendEmailWithTemplate = mocks.legacySend;
10
+ },
11
+ }));
12
+ vi.mock('@learncard/email-templates', () => ({
13
+ resolveBranding: () => ({}),
14
+ renderEmail: mocks.render,
15
+ }));
16
+ import { PostmarkAdapter } from './postmark.adapter';
17
+
18
+ const notification: Notification = {
19
+ contactMethod: { type: 'email', value: 'holder@example.com' },
20
+ templateId: 'credential-updated',
21
+ templateModel: { issuer: { name: 'School' }, credential: { name: 'Biology' } },
22
+ };
23
+
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ mocks.render.mockResolvedValue({ html: '<p>Updated</p>', text: 'Updated', subject: 'Updated' });
27
+ mocks.send.mockResolvedValue({});
28
+ });
29
+
30
+ describe('update email provider attempts', () => {
31
+ it('sends the locally rendered update once', async () => {
32
+ await new PostmarkAdapter('test-key').send(notification);
33
+ expect(mocks.send).toHaveBeenCalledTimes(1);
34
+ expect(mocks.legacySend).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it('does not try a legacy template after an ambiguous provider failure', async () => {
38
+ mocks.send.mockRejectedValueOnce(new Error('Acknowledgement lost'));
39
+ await expect(new PostmarkAdapter('test-key').send(notification)).rejects.toThrow(
40
+ 'Acknowledgement lost'
41
+ );
42
+ expect(mocks.send).toHaveBeenCalledTimes(1);
43
+ expect(mocks.legacySend).not.toHaveBeenCalled();
44
+ });
45
+
46
+ it('fails without sending when the local update template cannot render', async () => {
47
+ mocks.render.mockRejectedValueOnce(new Error('Render failed'));
48
+ await expect(new PostmarkAdapter('test-key').send(notification)).rejects.toThrow(
49
+ 'Render failed'
50
+ );
51
+ expect(mocks.send).not.toHaveBeenCalled();
52
+ expect(mocks.legacySend).not.toHaveBeenCalled();
53
+ });
54
+ });
@@ -41,6 +41,7 @@ const LOCAL_TEMPLATE_MAP: Record<string, TemplateId> = {
41
41
  'guardian-credential-approval': 'guardian-credential-approval',
42
42
  'guardian-email-otp': 'guardian-email-otp',
43
43
  'guardian-rejected-credential': 'guardian-rejected-credential',
44
+ 'credential-updated': 'credential-updated',
44
45
  };
45
46
 
46
47
  export class PostmarkAdapter implements DeliveryService {
@@ -85,6 +86,8 @@ export class PostmarkAdapter implements DeliveryService {
85
86
  `[PostmarkAdapter] Local render failed for "${notification.templateId}":`,
86
87
  renderError
87
88
  );
89
+ // This template is owned locally; there is no legacy provider alias.
90
+ if (notification.templateId === 'credential-updated') throw renderError;
88
91
  }
89
92
 
90
93
  if (rendered) {
@@ -104,6 +107,9 @@ export class PostmarkAdapter implements DeliveryService {
104
107
  `[PostmarkAdapter] sendEmail API failed for "${notification.templateId}":`,
105
108
  sendError
106
109
  );
110
+ // A lost provider acknowledgement may already have sent the email.
111
+ // Do not bypass the refresh delivery claim with a second attempt.
112
+ if (notification.templateId === 'credential-updated') throw sendError;
107
113
  }
108
114
  }
109
115
  }
@@ -217,6 +223,12 @@ export class PostmarkAdapter implements DeliveryService {
217
223
  recipient: model.recipient,
218
224
  };
219
225
 
226
+ case 'credential-updated':
227
+ return {
228
+ issuer: model.issuer,
229
+ credential: model.credential,
230
+ };
231
+
220
232
  default:
221
233
  return model as TemplateDataMap[TemplateId];
222
234
  }
@@ -7,7 +7,8 @@ export const getRegistryService = (): RegistryService => {
7
7
  if (
8
8
  environment.NODE_ENV === 'test' ||
9
9
  environment.TRUSTED_ISSUERS_WHITELIST ||
10
- environment.IS_CI
10
+ environment.IS_CI ||
11
+ environment.IS_E2E_TEST
11
12
  ) {
12
13
  return new WhitelistAdapter();
13
14
  }
@@ -33,6 +33,7 @@ export const CredentialActivityValidator = z.object({
33
33
  eventType: CredentialActivityEventTypeValidator,
34
34
  timestamp: z.string(),
35
35
  actorProfileId: z.string().optional(),
36
+ onBehalfOf: z.string().optional(),
36
37
  recipientType: CredentialActivityRecipientTypeValidator,
37
38
  recipientIdentifier: z.string(),
38
39
  boostUri: z.string().optional(),
@@ -85,6 +86,7 @@ export const CredentialActivityStatsValidator = z.object({
85
86
  export type CredentialActivityStats = z.infer<typeof CredentialActivityStatsValidator>;
86
87
 
87
88
  export type LogCredentialActivityParams = {
89
+ onBehalfOf?: string;
88
90
  actorProfileId?: string;
89
91
  eventType: CredentialActivityEventType;
90
92
  recipientType: CredentialActivityRecipientType;