@learncard/network-brain-service 3.18.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.
- package/.env.example +4 -0
- package/CHANGELOG.md +80 -0
- package/compose.inbox.yml +7 -0
- package/dist/accesslayer/auth-grant/read.d.ts +1 -0
- package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
- package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
- package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
- package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
- package/dist/cache/atomic-counter.test.d.ts +2 -0
- package/dist/cache/atomic-counter.test.d.ts.map +1 -0
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/config/environment.d.ts +46 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/helpers/activity.helpers.d.ts +4 -0
- package/dist/helpers/activity.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
- package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential.helpers.d.ts +2 -1
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
- package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
- package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
- package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox.helpers.d.ts +9 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
- package/dist/helpers/shim.d.ts +1 -1
- package/dist/helpers/shim.d.ts.map +1 -1
- package/dist/inbox-queue-local.d.ts +2 -0
- package/dist/inbox-queue-local.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AuthGrant.d.ts.map +1 -1
- package/dist/models/CredentialActivity.d.ts +1 -0
- package/dist/models/CredentialActivity.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -2
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/InboxCredential.d.ts +1 -0
- package/dist/models/InboxCredential.d.ts.map +1 -1
- package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/routes/act-as.test.d.ts +2 -0
- package/dist/routes/act-as.test.d.ts.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +10 -0
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +494 -0
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +953 -11
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +42 -126
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
- package/dist/services/registry/registry.factory.d.ts.map +1 -1
- package/dist/types/activity.d.ts +4 -0
- package/dist/types/activity.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +10 -1
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/inbox-batch.d.ts +38 -0
- package/dist/types/inbox-batch.d.ts.map +1 -0
- package/inbox-e2e-setup.ts +53 -1
- package/inbox-elasticmq.conf +10 -0
- package/lambda.ts +61 -16
- package/package.json +18 -16
- package/scripts/setup-inbox-queues.ts +60 -0
- package/serverless-local.yml +2 -0
- package/serverless.yml +75 -0
- package/src/accesslayer/auth-grant/read.ts +2 -1
- package/src/accesslayer/credential-activity/create.ts +1 -0
- package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
- package/src/accesslayer/credential-refresh/index.ts +1 -0
- package/src/accesslayer/credential-refresh/read.ts +8 -2
- package/src/accesslayer/credential-refresh/update.ts +4 -0
- package/src/accesslayer/inbox-batch/store.ts +469 -0
- package/src/accesslayer/inbox-credential/create.ts +51 -19
- package/src/accesslayer/inbox-credential/refresh.ts +221 -0
- package/src/accesslayer/inbox-credential/update.ts +1 -1
- package/src/cache/atomic-counter.test-helpers.ts +27 -0
- package/src/cache/atomic-counter.test.ts +6 -0
- package/src/cache/index.ts +25 -13
- package/src/config/environment.ts +26 -0
- package/src/credential-refresh.ts +5 -1
- package/src/docker-entry.ts +8 -0
- package/src/helpers/activity.helpers.ts +4 -0
- package/src/helpers/app-counter.helpers.ts +1 -1
- package/src/helpers/contact-method.helpers.test.ts +43 -0
- package/src/helpers/contact-method.helpers.ts +3 -4
- package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
- package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
- package/src/helpers/credential-refresh-email.helpers.ts +201 -0
- package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
- package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
- package/src/helpers/credential-refresh.helpers.ts +375 -99
- package/src/helpers/credential.helpers.ts +4 -1
- package/src/helpers/finalize-inbox.helpers.ts +29 -14
- package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
- package/src/helpers/inbox-batch-http.helpers.ts +150 -0
- package/src/helpers/inbox-batch-status.helpers.ts +11 -0
- package/src/helpers/inbox-batch.helpers.test.ts +668 -0
- package/src/helpers/inbox-batch.helpers.ts +374 -0
- package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
- package/src/helpers/inbox-queue.helpers.test.ts +253 -0
- package/src/helpers/inbox-queue.helpers.ts +372 -0
- package/src/helpers/inbox-refresh.helpers.ts +432 -0
- package/src/helpers/inbox.helpers.ts +268 -32
- package/src/helpers/notifications.helpers.ts +4 -2
- package/src/helpers/rateLimit.helpers.ts +2 -5
- package/src/helpers/refresh-send-intent.helpers.ts +273 -0
- package/src/helpers/shim.ts +32 -31
- package/src/inbox-queue-local.ts +28 -0
- package/src/models/AuthGrant.ts +1 -0
- package/src/models/CredentialActivity.ts +3 -5
- package/src/models/CredentialRefresh.ts +4 -2
- package/src/models/InboxCredential.ts +2 -0
- package/src/models/credential-refresh-constraints.test.ts +8 -3
- package/src/models/credential-refresh-constraints.ts +5 -0
- package/src/openapi.ts +8 -0
- package/src/routes/act-as.test.ts +239 -0
- package/src/routes/app-store.ts +2 -2
- package/src/routes/auth-grants.ts +13 -11
- package/src/routes/boosts.ts +914 -92
- package/src/routes/credential-refreshes.test.ts +8 -2
- package/src/routes/credential-refreshes.ts +4 -1
- package/src/routes/credentials.ts +2 -1
- package/src/routes/inbox.ts +95 -49
- package/src/routes/index.ts +82 -7
- package/src/routes/skills.ts +1 -1
- package/src/routes/workflows.ts +43 -24
- package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
- package/src/services/delivery/adapters/postmark.adapter.ts +12 -0
- package/src/services/registry/registry.factory.ts +2 -1
- package/src/types/activity.ts +2 -0
- package/src/types/credential-refresh.ts +21 -2
- package/src/types/inbox-batch.ts +46 -0
- package/test/atomic-counter.e2e.ts +12 -0
- package/test/auth-grant.spec.ts +21 -21
- package/test/credential-refresh-email.spec.ts +663 -0
- package/test/credential-refresh-publication.spec.ts +2 -0
- package/test/helpers/inbox-e2e-environment.ts +6 -0
- package/test/inbox-batch.spec.ts +1338 -0
- package/test/inbox-refresh.spec.ts +534 -0
- package/test/openapi.spec.ts +41 -1
- package/test/refresh-send.spec.ts +1799 -0
- package/test/registry.spec.ts +56 -11
- package/vitest.config.ts +9 -0
- package/vitest.inbox-e2e.config.ts +5 -1
- package/vitest.integration.config.ts +2 -0
package/src/routes/boosts.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertInboxRefreshEnabled,
|
|
3
|
+
inboxRefreshRequestDigest,
|
|
4
|
+
getInboxRefreshReplay,
|
|
5
|
+
getInboxRefreshReceipt,
|
|
6
|
+
resumeInboxRefreshDelivery,
|
|
7
|
+
} from '@helpers/inbox-refresh.helpers';
|
|
1
8
|
import { TRPCError } from '@trpc/server';
|
|
2
9
|
import { z } from 'zod';
|
|
3
10
|
import { v4 as uuid } from 'uuid';
|
|
@@ -27,11 +34,19 @@ import {
|
|
|
27
34
|
PaginatedBoostRecipientsWithChildrenValidator,
|
|
28
35
|
SkillQueryValidator,
|
|
29
36
|
SendBoostInputValidator,
|
|
37
|
+
SendBoostTemplateValidator,
|
|
30
38
|
SendBoostResponseValidator,
|
|
39
|
+
PrepareRefreshableSendInputValidator,
|
|
40
|
+
PrepareRefreshableSendResultValidator,
|
|
41
|
+
PrepareRefreshableSendResult,
|
|
31
42
|
AllocateCredentialStatusInputValidator,
|
|
32
43
|
AllocatedBitstringStatusListEntryValidator,
|
|
33
44
|
} from '@learncard/types';
|
|
34
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
isVC2Format,
|
|
47
|
+
injectManagedRefreshService,
|
|
48
|
+
getCredentialIssuerId,
|
|
49
|
+
} from '@learncard/helpers';
|
|
35
50
|
import {
|
|
36
51
|
renderBoostTemplate,
|
|
37
52
|
parseRenderedTemplate,
|
|
@@ -47,6 +62,7 @@ import { t, profileRoute } from '@routes';
|
|
|
47
62
|
|
|
48
63
|
import {
|
|
49
64
|
getBoostByUri,
|
|
65
|
+
getBoostById,
|
|
50
66
|
getBoostsForProfile,
|
|
51
67
|
countBoostsForProfile,
|
|
52
68
|
getBoostsByUri,
|
|
@@ -176,6 +192,28 @@ import {
|
|
|
176
192
|
import { updateDefaultPermissionsForBoost } from '@accesslayer/role/relationships/update';
|
|
177
193
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
178
194
|
import type { IssuedCredential } from '../types/credential';
|
|
195
|
+
import {
|
|
196
|
+
allocateCredentialRefresh,
|
|
197
|
+
extractManagedRefreshHandoff,
|
|
198
|
+
getBoundRefreshBoostId,
|
|
199
|
+
getCredentialRefreshServiceUrl,
|
|
200
|
+
peekCredentialRefreshInitialBinding,
|
|
201
|
+
sendRefreshableCredential,
|
|
202
|
+
} from '@helpers/credential-refresh.helpers';
|
|
203
|
+
import {
|
|
204
|
+
claimRefreshSendIntent,
|
|
205
|
+
computeRefreshSendRequestDigest,
|
|
206
|
+
getRefreshSendIntent,
|
|
207
|
+
markRefreshSendIntentDelivered,
|
|
208
|
+
reconcileBoundRefreshSendIntent,
|
|
209
|
+
recordRefreshSendIntent,
|
|
210
|
+
recordRefreshSendIntentPending,
|
|
211
|
+
type RefreshSendIntent,
|
|
212
|
+
} from '@helpers/refresh-send-intent.helpers';
|
|
213
|
+
import { getCredentialRefreshRuntimeEnvironment } from '@environment';
|
|
214
|
+
import { ensureCredentialRefreshConstraints } from '../models/credential-refresh-constraints';
|
|
215
|
+
import { userHasRequiredScopes } from '@helpers/auth-grant.helpers';
|
|
216
|
+
import { AUTH_GRANT_NO_ACCESS_SCOPE } from 'src/constants/auth-grant';
|
|
179
217
|
import { removeConnectionsForBoost } from '@helpers/connection.helpers';
|
|
180
218
|
import { issueToInbox } from '@helpers/inbox.helpers';
|
|
181
219
|
import { findInboxServiceEndpoint } from '@helpers/federation.helpers';
|
|
@@ -242,7 +280,336 @@ const resolveBoostCredentialInstance = async ({
|
|
|
242
280
|
return getCredentialInstanceForBoostAndProfile(boostId, recipientProfileId);
|
|
243
281
|
};
|
|
244
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Resolves the approved consent-flow contract terms for a unified send, and links a
|
|
285
|
+
* newly created boost to the contract. Shared by the normal and managed-refresh send
|
|
286
|
+
* paths so contract linkage semantics stay identical.
|
|
287
|
+
*/
|
|
288
|
+
const resolveContractForSend = async (params: {
|
|
289
|
+
profile: ProfileType;
|
|
290
|
+
targetProfile: ProfileType;
|
|
291
|
+
boost: BoostInstance;
|
|
292
|
+
boostCreated: boolean;
|
|
293
|
+
contractUri?: string;
|
|
294
|
+
}) => {
|
|
295
|
+
const { profile, targetProfile, boost, boostCreated, contractUri } = params;
|
|
296
|
+
|
|
297
|
+
let contractTerms = null as Awaited<ReturnType<typeof getContractTermsForProfile>> | null;
|
|
298
|
+
let contractDetails: Awaited<ReturnType<typeof getContractDetailsByUri>> | null = null;
|
|
299
|
+
|
|
300
|
+
if (contractUri) {
|
|
301
|
+
const decodedContractUri = decodeURIComponent(contractUri);
|
|
302
|
+
contractDetails = await traceDb('getContractDetailsByUri', () =>
|
|
303
|
+
getContractDetailsByUri(decodedContractUri)
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (!contractDetails) {
|
|
307
|
+
throw new TRPCError({
|
|
308
|
+
code: 'NOT_FOUND',
|
|
309
|
+
message: 'Could not find contract',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const terms = await traceDb('getContractTermsForProfile', () =>
|
|
314
|
+
getContractTermsForProfile(targetProfile, contractDetails!.contract)
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const writers = await traceDb('getWritersForContract', () =>
|
|
318
|
+
getWritersForContract(contractDetails!.contract)
|
|
319
|
+
);
|
|
320
|
+
const isWriter = writers.some(writer => writer.profileId === profile.profileId);
|
|
321
|
+
const isDenied = terms?.terms.deniedWriters?.includes(profile.profileId) ?? false;
|
|
322
|
+
const categoryAllowed = boost.category
|
|
323
|
+
? terms?.terms.write?.credentials?.categories?.[boost.category] === true
|
|
324
|
+
: true;
|
|
325
|
+
|
|
326
|
+
if (terms && isWriter && !isDenied && categoryAllowed) {
|
|
327
|
+
contractTerms = terms;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (boostCreated && contractDetails) {
|
|
332
|
+
await traceDb('setRelatedBoostForContract', () =>
|
|
333
|
+
setRelatedBoostForContract(contractDetails!.contract, boost)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return contractTerms;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/** Validate managed send support and recipient before creating any send state. */
|
|
341
|
+
const validateRefreshSendRecipient = async ({
|
|
342
|
+
profile,
|
|
343
|
+
scope,
|
|
344
|
+
recipient,
|
|
345
|
+
domain,
|
|
346
|
+
}: {
|
|
347
|
+
profile: ProfileType;
|
|
348
|
+
scope?: string;
|
|
349
|
+
recipient: string;
|
|
350
|
+
domain: string;
|
|
351
|
+
}): Promise<ProfileType> => {
|
|
352
|
+
if (!getCredentialRefreshRuntimeEnvironment().CREDENTIAL_REFRESH_ENABLED) {
|
|
353
|
+
throw new TRPCError({
|
|
354
|
+
code: 'NOT_FOUND',
|
|
355
|
+
message: 'Credential refresh is not available',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await ensureCredentialRefreshConstraints();
|
|
360
|
+
|
|
361
|
+
// The route itself requires boosts:write; managed refresh additionally
|
|
362
|
+
// requires the same credentials:write scope as the dedicated
|
|
363
|
+
// /credential-refresh routes.
|
|
364
|
+
if (!userHasRequiredScopes(scope ?? AUTH_GRANT_NO_ACCESS_SCOPE, 'credentials:write')) {
|
|
365
|
+
throw new TRPCError({
|
|
366
|
+
code: 'UNAUTHORIZED',
|
|
367
|
+
message: 'This operation requires credentials:write scope',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (isInboxRecipient(recipient)) {
|
|
372
|
+
throw new TRPCError({
|
|
373
|
+
code: 'BAD_REQUEST',
|
|
374
|
+
message:
|
|
375
|
+
'Managed credential refresh requires a profile or DID recipient; email and phone recipients cannot request refresh.',
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Recipients must resolve to local profiles before anything is
|
|
380
|
+
// created; remote or unresolvable DIDs are rejected here instead of
|
|
381
|
+
// entering the federation/inbox flows.
|
|
382
|
+
const refreshRecipientProfileId = await traceInternal('getProfileIdFromString:refresh', () =>
|
|
383
|
+
getProfileIdFromString(recipient, domain)
|
|
384
|
+
);
|
|
385
|
+
const refreshTargetProfile = refreshRecipientProfileId
|
|
386
|
+
? await traceDb('getProfileByProfileId:refresh', () =>
|
|
387
|
+
getProfileByProfileId(refreshRecipientProfileId)
|
|
388
|
+
)
|
|
389
|
+
: null;
|
|
390
|
+
|
|
391
|
+
if (!refreshTargetProfile) {
|
|
392
|
+
throw new TRPCError({
|
|
393
|
+
code: 'NOT_FOUND',
|
|
394
|
+
message:
|
|
395
|
+
'Managed credential refresh requires a recipient resolvable to a local profile. Remote or unresolvable DIDs are not supported.',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const refreshTarget = refreshTargetProfile;
|
|
400
|
+
|
|
401
|
+
if (
|
|
402
|
+
await traceDb('isRelationshipBlocked:refresh', () =>
|
|
403
|
+
isRelationshipBlocked(profile, refreshTarget)
|
|
404
|
+
)
|
|
405
|
+
) {
|
|
406
|
+
throw new TRPCError({
|
|
407
|
+
code: 'NOT_FOUND',
|
|
408
|
+
message: 'Profile not found. Are you sure this person exists?',
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return refreshTargetProfile;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/** Preserve the inline send template's metadata, skills and claim permissions. */
|
|
416
|
+
const createInlineBoostForSend = async (
|
|
417
|
+
template: z.infer<typeof SendBoostTemplateValidator>,
|
|
418
|
+
profile: ProfileType,
|
|
419
|
+
domain: string
|
|
420
|
+
): Promise<BoostInstance> => {
|
|
421
|
+
const { credential, claimPermissions, skills, ...metadata } = template;
|
|
422
|
+
const boost = await traceDb('createBoost', () =>
|
|
423
|
+
createBoost(credential, profile, metadata, domain)
|
|
424
|
+
);
|
|
425
|
+
if (Array.isArray(skills) && skills.length > 0) {
|
|
426
|
+
await traceDb('addAlignedSkillsToBoost', () => addAlignedSkillsToBoost(boost, skills));
|
|
427
|
+
}
|
|
428
|
+
if (claimPermissions) {
|
|
429
|
+
await traceDb('addClaimPermissionsForBoost', () =>
|
|
430
|
+
addClaimPermissionsForBoost(boost, { ...EMPTY_PERMISSIONS, ...claimPermissions })
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return boost;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const managedRefreshServiceFor = (refreshId: string, domain: string) => ({
|
|
437
|
+
id: getCredentialRefreshServiceUrl(refreshId, domain),
|
|
438
|
+
type: 'LearnCardCredentialRefresh2026' as const,
|
|
439
|
+
authorization: { type: 'LearnCardDIDAuth' as const },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The single server preparation step for a managed refresh send (SDK and signing
|
|
444
|
+
* authority paths): after recipient validation, create/reuse the boost and allocate
|
|
445
|
+
* the refresh. With an idempotencyKey, progress is recorded on a RefreshSendIntent so a
|
|
446
|
+
* retried call reuses the same boost/allocation, or returns the completed result.
|
|
447
|
+
*/
|
|
448
|
+
const prepareManagedRefreshSend = async (params: {
|
|
449
|
+
profile: ProfileType;
|
|
450
|
+
/** Returned by validateRefreshSendRecipient in this request, before any mutation. */
|
|
451
|
+
targetProfile: ProfileType;
|
|
452
|
+
domain: string;
|
|
453
|
+
templateUri?: string;
|
|
454
|
+
template?: z.infer<typeof SendBoostTemplateValidator>;
|
|
455
|
+
contractUri?: string;
|
|
456
|
+
credentialId?: string;
|
|
457
|
+
templateData?: Record<string, unknown>;
|
|
458
|
+
integrationId?: string;
|
|
459
|
+
idempotencyKey?: string;
|
|
460
|
+
}): Promise<PrepareRefreshableSendResult & { intent?: RefreshSendIntent }> => {
|
|
461
|
+
const { profile, targetProfile, domain, templateUri, template, contractUri } = params;
|
|
462
|
+
// Local recipients authenticate refresh requests as their network profile DID,
|
|
463
|
+
// including when the caller addressed them by their controller did:key.
|
|
464
|
+
const holderDid = getDidWeb(domain, targetProfile.profileId);
|
|
465
|
+
|
|
466
|
+
let intent: RefreshSendIntent | undefined;
|
|
467
|
+
|
|
468
|
+
if (params.idempotencyKey) {
|
|
469
|
+
const claim = await claimRefreshSendIntent({
|
|
470
|
+
issuerProfileId: profile.profileId,
|
|
471
|
+
idempotencyKey: params.idempotencyKey,
|
|
472
|
+
requestDigest: computeRefreshSendRequestDigest({
|
|
473
|
+
recipientProfileId: targetProfile.profileId,
|
|
474
|
+
holderDid,
|
|
475
|
+
templateUri,
|
|
476
|
+
template,
|
|
477
|
+
contractUri,
|
|
478
|
+
templateData: params.templateData,
|
|
479
|
+
integrationId: params.integrationId,
|
|
480
|
+
credentialId: params.credentialId,
|
|
481
|
+
}),
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
intent = claim.intent;
|
|
485
|
+
|
|
486
|
+
if (claim.kind !== 'owned') {
|
|
487
|
+
const prepared = {
|
|
488
|
+
boostUri: intent.boostUri!,
|
|
489
|
+
credentialId: intent.credentialId!,
|
|
490
|
+
refreshId: intent.refreshId!,
|
|
491
|
+
refreshService: managedRefreshServiceFor(intent.refreshId!, domain),
|
|
492
|
+
holderDid: intent.holderDid!,
|
|
493
|
+
intent,
|
|
494
|
+
};
|
|
495
|
+
const completed =
|
|
496
|
+
claim.kind === 'delivered'
|
|
497
|
+
? intent.result
|
|
498
|
+
: await reconcileBoundRefreshSendIntent(intent, domain);
|
|
499
|
+
|
|
500
|
+
return completed ? { ...prepared, completed } : prepared;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (template?.status === 'DRAFT') {
|
|
505
|
+
throw new TRPCError({
|
|
506
|
+
code: 'FORBIDDEN',
|
|
507
|
+
message: 'Draft Boosts can not be sent. Only Published Boosts can be sent.',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let boostUri = intent?.boostUri ?? templateUri;
|
|
512
|
+
let boost: BoostInstance | null;
|
|
513
|
+
|
|
514
|
+
if (boostUri) {
|
|
515
|
+
boost = await traceDb('getBoostByUri:prepareRefresh', () => getBoostByUri(boostUri!));
|
|
516
|
+
|
|
517
|
+
if (!boost) throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find boost' });
|
|
518
|
+
} else {
|
|
519
|
+
boost = await createInlineBoostForSend(template!, profile, domain);
|
|
520
|
+
boostUri = getBoostUri(boost.id, domain);
|
|
521
|
+
|
|
522
|
+
if (intent) intent = await recordRefreshSendIntent(intent, { boostUri });
|
|
523
|
+
|
|
524
|
+
await resolveContractForSend({
|
|
525
|
+
profile,
|
|
526
|
+
targetProfile,
|
|
527
|
+
boost,
|
|
528
|
+
boostCreated: true,
|
|
529
|
+
contractUri,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (
|
|
534
|
+
!(await traceDb('canProfileIssueBoost:prepareRefresh', () =>
|
|
535
|
+
canProfileIssueBoost(profile, boost!)
|
|
536
|
+
))
|
|
537
|
+
) {
|
|
538
|
+
throw new TRPCError({
|
|
539
|
+
code: 'UNAUTHORIZED',
|
|
540
|
+
message: 'Profile does not have permissions to issue boost',
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (isDraftBoost(boost)) {
|
|
545
|
+
throw new TRPCError({
|
|
546
|
+
code: 'FORBIDDEN',
|
|
547
|
+
message: 'Draft Boosts can not be sent. Only Published Boosts can be sent.',
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const credentialId = intent?.credentialId ?? params.credentialId ?? `urn:uuid:${uuid()}`;
|
|
552
|
+
let refreshId = intent?.refreshId;
|
|
553
|
+
|
|
554
|
+
if (!refreshId) {
|
|
555
|
+
const allocation = await traceInternal('allocateCredentialRefresh:prepare', () =>
|
|
556
|
+
allocateCredentialRefresh({
|
|
557
|
+
issuerProfile: profile,
|
|
558
|
+
holderProfile: targetProfile,
|
|
559
|
+
holderDid,
|
|
560
|
+
credentialId,
|
|
561
|
+
domain,
|
|
562
|
+
})
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
refreshId = allocation.refreshId;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (intent) {
|
|
569
|
+
intent = await recordRefreshSendIntent(intent, {
|
|
570
|
+
boostUri,
|
|
571
|
+
credentialId,
|
|
572
|
+
refreshId,
|
|
573
|
+
holderDid,
|
|
574
|
+
state: 'prepared',
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
boostUri,
|
|
580
|
+
credentialId,
|
|
581
|
+
refreshId,
|
|
582
|
+
refreshService: managedRefreshServiceFor(refreshId, domain),
|
|
583
|
+
holderDid,
|
|
584
|
+
...(intent ? { intent } : {}),
|
|
585
|
+
};
|
|
586
|
+
};
|
|
587
|
+
|
|
245
588
|
export const boostsRouter = t.router({
|
|
589
|
+
// SDK preparation only: guards, boost anchor and refresh allocation. Never signs or
|
|
590
|
+
// delivers. With an idempotencyKey a retried call reuses the same boost/allocation,
|
|
591
|
+
// or returns `completed` when that key already delivered.
|
|
592
|
+
prepareRefreshableSend: profileRoute
|
|
593
|
+
.meta({ requiredScope: 'boosts:write' })
|
|
594
|
+
.input(PrepareRefreshableSendInputValidator)
|
|
595
|
+
.output(PrepareRefreshableSendResultValidator)
|
|
596
|
+
.mutation(async ({ ctx, input }) => {
|
|
597
|
+
const targetProfile = await validateRefreshSendRecipient({
|
|
598
|
+
profile: ctx.user.profile,
|
|
599
|
+
scope: ctx.user.scope,
|
|
600
|
+
recipient: input.recipient,
|
|
601
|
+
domain: ctx.domain,
|
|
602
|
+
});
|
|
603
|
+
const { intent: _intent, ...prepared } = await prepareManagedRefreshSend({
|
|
604
|
+
...input,
|
|
605
|
+
profile: ctx.user.profile,
|
|
606
|
+
targetProfile,
|
|
607
|
+
domain: ctx.domain,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
return prepared;
|
|
611
|
+
}),
|
|
612
|
+
|
|
246
613
|
getBoostAlignments: profileRoute
|
|
247
614
|
.meta({
|
|
248
615
|
openapi: {
|
|
@@ -692,6 +1059,7 @@ export const boostsRouter = t.router({
|
|
|
692
1059
|
// Log credential activity FIRST to get activityId for chaining
|
|
693
1060
|
const activityId = await logCredentialSent({
|
|
694
1061
|
actorProfileId: profile.profileId,
|
|
1062
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
695
1063
|
recipientType: 'profile',
|
|
696
1064
|
recipientIdentifier: targetProfile.profileId,
|
|
697
1065
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -736,6 +1104,147 @@ export const boostsRouter = t.router({
|
|
|
736
1104
|
const { contractUri } = input;
|
|
737
1105
|
const { domain } = ctx;
|
|
738
1106
|
|
|
1107
|
+
// LC-2198: managed refresh requests are validated in full BEFORE any
|
|
1108
|
+
// mutation — an unsupported refresh send must not create a boost,
|
|
1109
|
+
// allocation, inbox entry, activity, or delivery.
|
|
1110
|
+
const inboxRefreshRequested =
|
|
1111
|
+
input.refresh === true && !!isInboxRecipient(input.recipient);
|
|
1112
|
+
const refreshRequested = input.refresh === true && !inboxRefreshRequested;
|
|
1113
|
+
let inboxIntent: RefreshSendIntent | undefined;
|
|
1114
|
+
const inboxDigest = inboxRefreshRequested
|
|
1115
|
+
? inboxRefreshRequestDigest(input)
|
|
1116
|
+
: undefined;
|
|
1117
|
+
const inboxKey = input.idempotencyKey
|
|
1118
|
+
? `send:${input.idempotencyKey}`
|
|
1119
|
+
: undefined;
|
|
1120
|
+
if (inboxRefreshRequested) {
|
|
1121
|
+
await assertInboxRefreshEnabled(ctx.user.scope);
|
|
1122
|
+
if (input.signedCredential || input.contractUri)
|
|
1123
|
+
throw new TRPCError({
|
|
1124
|
+
code: 'BAD_REQUEST',
|
|
1125
|
+
message:
|
|
1126
|
+
'Inbox refresh requires unsigned content and does not support a consent contract before holder binding.',
|
|
1127
|
+
});
|
|
1128
|
+
if (input.idempotencyKey) {
|
|
1129
|
+
const claim = await claimRefreshSendIntent({
|
|
1130
|
+
issuerProfileId: profile.profileId,
|
|
1131
|
+
idempotencyKey: input.idempotencyKey,
|
|
1132
|
+
requestDigest: inboxDigest!,
|
|
1133
|
+
});
|
|
1134
|
+
if (claim.kind === 'delivered' && claim.intent.result)
|
|
1135
|
+
return claim.intent.result;
|
|
1136
|
+
inboxIntent = claim.intent;
|
|
1137
|
+
const replay = await getInboxRefreshReplay(
|
|
1138
|
+
profile.profileId,
|
|
1139
|
+
inboxKey,
|
|
1140
|
+
inboxDigest!
|
|
1141
|
+
);
|
|
1142
|
+
if (
|
|
1143
|
+
replay &&
|
|
1144
|
+
(replay.claimUrl || replay.inbox.currentStatus !== 'PENDING')
|
|
1145
|
+
) {
|
|
1146
|
+
await resumeInboxRefreshDelivery(replay.inbox.refreshId!, domain);
|
|
1147
|
+
const result = {
|
|
1148
|
+
type: 'boost' as const,
|
|
1149
|
+
uri: inboxIntent.boostUri!,
|
|
1150
|
+
credentialUri: '',
|
|
1151
|
+
activityId: replay.inbox.activityId ?? '',
|
|
1152
|
+
inbox: {
|
|
1153
|
+
issuanceId: replay.inbox.id,
|
|
1154
|
+
status: replay.inbox.currentStatus,
|
|
1155
|
+
claimUrl: replay.claimUrl,
|
|
1156
|
+
guardianStatus: replay.inbox.guardianStatus,
|
|
1157
|
+
refresh: await getInboxRefreshReceipt(
|
|
1158
|
+
replay.inbox.refreshId!,
|
|
1159
|
+
domain
|
|
1160
|
+
),
|
|
1161
|
+
},
|
|
1162
|
+
};
|
|
1163
|
+
await markRefreshSendIntentDelivered(inboxIntent, result);
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
let refreshTargetProfile: ProfileType | null = null;
|
|
1169
|
+
|
|
1170
|
+
if (input.idempotencyKey && !input.refresh) {
|
|
1171
|
+
throw new TRPCError({
|
|
1172
|
+
code: 'BAD_REQUEST',
|
|
1173
|
+
message: 'idempotencyKey is only supported with refresh: true.',
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (refreshRequested) {
|
|
1178
|
+
refreshTargetProfile = await validateRefreshSendRecipient({
|
|
1179
|
+
profile,
|
|
1180
|
+
scope: ctx.user.scope,
|
|
1181
|
+
recipient: input.recipient,
|
|
1182
|
+
domain,
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
if (input.signedCredential) {
|
|
1186
|
+
// Reject signed credentials without a local managed allocation
|
|
1187
|
+
// before any mutation (e.g. boost auto-creation). No unsigned
|
|
1188
|
+
// fallback and no second allocation (plan decision 6).
|
|
1189
|
+
const handoff = extractManagedRefreshHandoff(
|
|
1190
|
+
input.signedCredential,
|
|
1191
|
+
domain
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
if (!handoff) {
|
|
1195
|
+
throw new TRPCError({
|
|
1196
|
+
code: 'BAD_REQUEST',
|
|
1197
|
+
message:
|
|
1198
|
+
'A signed credential sent with refresh must already contain its allocated managed refresh service.',
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (input.idempotencyKey) {
|
|
1203
|
+
const intent = await getRefreshSendIntent(
|
|
1204
|
+
profile.profileId,
|
|
1205
|
+
input.idempotencyKey
|
|
1206
|
+
);
|
|
1207
|
+
if (!intent) {
|
|
1208
|
+
throw new TRPCError({
|
|
1209
|
+
code: 'BAD_REQUEST',
|
|
1210
|
+
message:
|
|
1211
|
+
'idempotencyKey with a pre-signed credential requires a prior boost.prepareRefreshableSend call (tRPC only). For a directly signed REST send, omit idempotencyKey and retry the same signed credential and templateUri.',
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
if (
|
|
1215
|
+
intent.refreshId !== handoff.refreshId ||
|
|
1216
|
+
(input.templateUri && intent.boostUri !== input.templateUri)
|
|
1217
|
+
) {
|
|
1218
|
+
throw new TRPCError({
|
|
1219
|
+
code: 'CONFLICT',
|
|
1220
|
+
message:
|
|
1221
|
+
'This idempotencyKey does not match the prepared refresh send for this credential.',
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Keyed managed send without a pre-signed credential: preparation (boost +
|
|
1229
|
+
// allocation) goes through the idempotency intent instead of the generic
|
|
1230
|
+
// boost creation below.
|
|
1231
|
+
const keyedPrepared =
|
|
1232
|
+
refreshRequested && input.idempotencyKey && !input.signedCredential
|
|
1233
|
+
? await prepareManagedRefreshSend({
|
|
1234
|
+
profile,
|
|
1235
|
+
targetProfile: refreshTargetProfile!,
|
|
1236
|
+
domain,
|
|
1237
|
+
templateUri: input.templateUri,
|
|
1238
|
+
template: input.templateUri ? undefined : input.template,
|
|
1239
|
+
contractUri,
|
|
1240
|
+
templateData: input.templateData,
|
|
1241
|
+
integrationId: input.integrationId,
|
|
1242
|
+
idempotencyKey: input.idempotencyKey,
|
|
1243
|
+
})
|
|
1244
|
+
: undefined;
|
|
1245
|
+
|
|
1246
|
+
if (keyedPrepared?.completed) return keyedPrepared.completed;
|
|
1247
|
+
|
|
739
1248
|
// Check if recipient is email/phone (routes to Universal Inbox)
|
|
740
1249
|
const inboxRecipient = isInboxRecipient(input.recipient);
|
|
741
1250
|
|
|
@@ -744,7 +1253,22 @@ export const boostsRouter = t.router({
|
|
|
744
1253
|
let boostUri = '';
|
|
745
1254
|
let boostCreated = false;
|
|
746
1255
|
|
|
747
|
-
if (
|
|
1256
|
+
if (inboxIntent?.boostUri) {
|
|
1257
|
+
boost = await getBoostByUri(inboxIntent.boostUri);
|
|
1258
|
+
boostUri = inboxIntent.boostUri;
|
|
1259
|
+
} else if (keyedPrepared) {
|
|
1260
|
+
const resolved = await traceDb('getBoostByUri', () =>
|
|
1261
|
+
getBoostByUri(keyedPrepared.boostUri)
|
|
1262
|
+
);
|
|
1263
|
+
if (!resolved) {
|
|
1264
|
+
throw new TRPCError({
|
|
1265
|
+
code: 'NOT_FOUND',
|
|
1266
|
+
message: 'Could not find boost',
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
boost = resolved;
|
|
1270
|
+
boostUri = keyedPrepared.boostUri;
|
|
1271
|
+
} else if (input.templateUri) {
|
|
748
1272
|
const resolved = await traceDb('getBoostByUri', () =>
|
|
749
1273
|
getBoostByUri(input.templateUri!)
|
|
750
1274
|
);
|
|
@@ -757,47 +1281,77 @@ export const boostsRouter = t.router({
|
|
|
757
1281
|
boost = resolved;
|
|
758
1282
|
boostUri = input.templateUri;
|
|
759
1283
|
} else if (input.template) {
|
|
760
|
-
|
|
761
|
-
input.template;
|
|
762
|
-
|
|
763
|
-
boost = await traceDb('createBoost', () =>
|
|
764
|
-
createBoost(credential, profile, metadata, domain)
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
if (Array.isArray(skills) && skills.length > 0) {
|
|
768
|
-
await traceDb('addAlignedSkillsToBoost', () =>
|
|
769
|
-
addAlignedSkillsToBoost(boost!, skills)
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (claimPermissions) {
|
|
774
|
-
await traceDb('addClaimPermissionsForBoost', () =>
|
|
775
|
-
addClaimPermissionsForBoost(boost!, {
|
|
776
|
-
...EMPTY_PERMISSIONS,
|
|
777
|
-
...claimPermissions,
|
|
778
|
-
})
|
|
779
|
-
);
|
|
780
|
-
}
|
|
1284
|
+
boost = await createInlineBoostForSend(input.template, profile, domain);
|
|
781
1285
|
|
|
782
1286
|
boostUri = getBoostUri(boost.id, domain);
|
|
783
1287
|
boostCreated = true;
|
|
784
1288
|
} else if (input.signedCredential) {
|
|
785
|
-
//
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
)
|
|
797
|
-
|
|
1289
|
+
// A replayed managed handoff reuses the boost its refresh is already
|
|
1290
|
+
// bound to (the pre-mutation guard validated the handoff above).
|
|
1291
|
+
const boundBoostId = refreshRequested
|
|
1292
|
+
? await traceDb('getBoundRefreshBoostId', () =>
|
|
1293
|
+
getBoundRefreshBoostId(
|
|
1294
|
+
extractManagedRefreshHandoff(input.signedCredential!, domain)!
|
|
1295
|
+
.refreshId
|
|
1296
|
+
)
|
|
1297
|
+
)
|
|
1298
|
+
: undefined;
|
|
1299
|
+
const boundBoost = boundBoostId
|
|
1300
|
+
? await traceDb('getBoostById:boundRefresh', () =>
|
|
1301
|
+
getBoostById(boundBoostId)
|
|
1302
|
+
)
|
|
1303
|
+
: null;
|
|
1304
|
+
// A keyed handoff whose preparation already created/resolved a
|
|
1305
|
+
// boost (but never delivered) reuses that prepared boost.
|
|
1306
|
+
const preparedBoostUri =
|
|
1307
|
+
!boundBoost && refreshRequested && input.idempotencyKey
|
|
1308
|
+
? (
|
|
1309
|
+
await traceDb('getRefreshSendIntent:signedCredential', () =>
|
|
1310
|
+
getRefreshSendIntent(
|
|
1311
|
+
profile.profileId,
|
|
1312
|
+
input.idempotencyKey!
|
|
1313
|
+
)
|
|
1314
|
+
)
|
|
1315
|
+
)?.boostUri
|
|
1316
|
+
: undefined;
|
|
1317
|
+
const preparedBoost = preparedBoostUri
|
|
1318
|
+
? await traceDb('getBoostByUri:preparedRefresh', () =>
|
|
1319
|
+
getBoostByUri(preparedBoostUri)
|
|
1320
|
+
)
|
|
1321
|
+
: null;
|
|
1322
|
+
|
|
1323
|
+
if (boundBoost) {
|
|
1324
|
+
boost = boundBoost;
|
|
1325
|
+
boostUri = getBoostUri(boundBoost.id, domain);
|
|
1326
|
+
} else if (preparedBoost && preparedBoostUri) {
|
|
1327
|
+
boost = preparedBoost;
|
|
1328
|
+
boostUri = preparedBoostUri;
|
|
1329
|
+
} else {
|
|
1330
|
+
// Auto-create boost from the signed credential. A managed refresh
|
|
1331
|
+
// service belongs to one holder's credential, never to a reusable
|
|
1332
|
+
// template, so it is not stored on the boost.
|
|
1333
|
+
const { refreshService: _refreshService, ...templateCredential } =
|
|
1334
|
+
input.signedCredential as Record<string, unknown>;
|
|
1335
|
+
const name =
|
|
1336
|
+
typeof templateCredential.name === 'string'
|
|
1337
|
+
? templateCredential.name
|
|
1338
|
+
: undefined;
|
|
1339
|
+
|
|
1340
|
+
boost = await traceDb('createBoost:fromSignedCredential', () =>
|
|
1341
|
+
createBoost(
|
|
1342
|
+
(refreshRequested
|
|
1343
|
+
? templateCredential
|
|
1344
|
+
: input.signedCredential!) as typeof input.signedCredential &
|
|
1345
|
+
object,
|
|
1346
|
+
profile,
|
|
1347
|
+
{ ...(name ? { name } : {}) },
|
|
1348
|
+
domain
|
|
1349
|
+
)
|
|
1350
|
+
);
|
|
798
1351
|
|
|
799
|
-
|
|
800
|
-
|
|
1352
|
+
boostUri = getBoostUri(boost.id, domain);
|
|
1353
|
+
boostCreated = true;
|
|
1354
|
+
}
|
|
801
1355
|
}
|
|
802
1356
|
|
|
803
1357
|
if (!boost) {
|
|
@@ -827,6 +1381,298 @@ export const boostsRouter = t.router({
|
|
|
827
1381
|
});
|
|
828
1382
|
}
|
|
829
1383
|
|
|
1384
|
+
if (inboxIntent?.state === 'preparing')
|
|
1385
|
+
inboxIntent = await recordRefreshSendIntent(inboxIntent, {
|
|
1386
|
+
boostUri,
|
|
1387
|
+
state: 'prepared',
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// LC-2198: managed refresh delivery. The recipient was fully validated
|
|
1391
|
+
// in the pre-mutation guard above (feature, scope, supported recipient,
|
|
1392
|
+
// blocklist); the boost passed the same issue-permission and draft
|
|
1393
|
+
// checks as a normal send.
|
|
1394
|
+
if (refreshRequested) {
|
|
1395
|
+
return trace('route', 'sendRefreshable', async () => {
|
|
1396
|
+
const targetProfile = refreshTargetProfile!;
|
|
1397
|
+
let intent: RefreshSendIntent | undefined = keyedPrepared?.intent;
|
|
1398
|
+
|
|
1399
|
+
const contractTerms = await resolveContractForSend({
|
|
1400
|
+
profile,
|
|
1401
|
+
targetProfile,
|
|
1402
|
+
boost: boost!,
|
|
1403
|
+
boostCreated,
|
|
1404
|
+
contractUri,
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
// Matches the unified send's self-send notification behavior.
|
|
1408
|
+
const skipNotification = profile.profileId === targetProfile.profileId;
|
|
1409
|
+
|
|
1410
|
+
let signedVc: VC;
|
|
1411
|
+
let refreshId: string;
|
|
1412
|
+
|
|
1413
|
+
if (input.signedCredential) {
|
|
1414
|
+
// Preallocated handoff (plan decision 6): the caller already
|
|
1415
|
+
// allocated and embedded the managed service before signing.
|
|
1416
|
+
// Ownership, holder, ID, boost anchor, proof, and descriptor
|
|
1417
|
+
// are enforced by the managed send helper.
|
|
1418
|
+
const handoff = extractManagedRefreshHandoff(
|
|
1419
|
+
input.signedCredential,
|
|
1420
|
+
domain
|
|
1421
|
+
)!;
|
|
1422
|
+
signedVc = input.signedCredential;
|
|
1423
|
+
refreshId = handoff.refreshId;
|
|
1424
|
+
|
|
1425
|
+
if (input.idempotencyKey) {
|
|
1426
|
+
const existing = await getRefreshSendIntent(
|
|
1427
|
+
profile.profileId,
|
|
1428
|
+
input.idempotencyKey
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
if (
|
|
1432
|
+
!existing ||
|
|
1433
|
+
existing.refreshId !== refreshId ||
|
|
1434
|
+
existing.boostUri !== boostUri
|
|
1435
|
+
) {
|
|
1436
|
+
throw new TRPCError({
|
|
1437
|
+
code: 'CONFLICT',
|
|
1438
|
+
message:
|
|
1439
|
+
'This idempotencyKey does not match the prepared refresh send for this credential.',
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (existing.state === 'delivered' && existing.result) {
|
|
1444
|
+
return existing.result;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
intent = existing;
|
|
1448
|
+
const reconciled = await reconcileBoundRefreshSendIntent(
|
|
1449
|
+
existing,
|
|
1450
|
+
domain
|
|
1451
|
+
);
|
|
1452
|
+
if (reconciled) return reconciled;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Resume detection: reuse the original delivery activity
|
|
1456
|
+
// instead of duplicating issuance or activity when the exact
|
|
1457
|
+
// same credential is redelivered (e.g. a network retry after
|
|
1458
|
+
// binding already succeeded).
|
|
1459
|
+
const binding = await traceInternal(
|
|
1460
|
+
'peekCredentialRefreshInitialBinding',
|
|
1461
|
+
() =>
|
|
1462
|
+
peekCredentialRefreshInitialBinding({
|
|
1463
|
+
refreshId,
|
|
1464
|
+
credential: signedVc,
|
|
1465
|
+
boostId: boost!.id,
|
|
1466
|
+
})
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
if (binding.bound) {
|
|
1470
|
+
const { uri, receipt } = await sendRefreshableCredential({
|
|
1471
|
+
issuerProfile: profile,
|
|
1472
|
+
refreshId,
|
|
1473
|
+
credential: signedVc,
|
|
1474
|
+
boostUri,
|
|
1475
|
+
skipNotification,
|
|
1476
|
+
domain,
|
|
1477
|
+
activityId: binding.activityId,
|
|
1478
|
+
integrationId: input.integrationId,
|
|
1479
|
+
contractTerms: contractTerms ?? undefined,
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
const response = {
|
|
1483
|
+
type: 'boost' as const,
|
|
1484
|
+
credentialUri: uri,
|
|
1485
|
+
uri: boostUri,
|
|
1486
|
+
activityId: binding.activityId ?? '',
|
|
1487
|
+
refresh: receipt,
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
if (intent)
|
|
1491
|
+
await markRefreshSendIntentDelivered(intent, response);
|
|
1492
|
+
|
|
1493
|
+
return response;
|
|
1494
|
+
}
|
|
1495
|
+
} else {
|
|
1496
|
+
if (keyedPrepared && intent) {
|
|
1497
|
+
const reconciled = await reconcileBoundRefreshSendIntent(
|
|
1498
|
+
intent,
|
|
1499
|
+
domain
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
if (reconciled) return reconciled;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
const signingAuthority = await traceDb(
|
|
1506
|
+
'getPrimarySigningAuthorityForUser:refresh',
|
|
1507
|
+
() => getPrimarySigningAuthorityForUser(profile)
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
if (!signingAuthority) {
|
|
1511
|
+
throw new TRPCError({
|
|
1512
|
+
code: 'PRECONDITION_FAILED',
|
|
1513
|
+
message:
|
|
1514
|
+
'You must register a signing authority before using send without a pre-signed credential. Please register one via registerSigningAuthority or sign the credential client-side.',
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
let unsignedVc: UnsignedVC;
|
|
1519
|
+
|
|
1520
|
+
try {
|
|
1521
|
+
unsignedVc = await traceInternal(
|
|
1522
|
+
'prepareCredentialFromBoost:refresh',
|
|
1523
|
+
() =>
|
|
1524
|
+
prepareCredentialFromBoost(boost!, boostUri, domain, {
|
|
1525
|
+
templateData: input.templateData as Record<
|
|
1526
|
+
string,
|
|
1527
|
+
unknown
|
|
1528
|
+
>,
|
|
1529
|
+
issuerDid: signingAuthority.relationship.did,
|
|
1530
|
+
recipientDid:
|
|
1531
|
+
keyedPrepared?.holderDid ??
|
|
1532
|
+
getDidWeb(domain, targetProfile.profileId),
|
|
1533
|
+
recipientName: targetProfile.displayName,
|
|
1534
|
+
})
|
|
1535
|
+
);
|
|
1536
|
+
} catch (e) {
|
|
1537
|
+
console.error('Failed to prepare boost credential', e);
|
|
1538
|
+
throw new TRPCError({
|
|
1539
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1540
|
+
message: 'Failed to prepare boost credential',
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// A stable credential ID must exist before allocation: the
|
|
1545
|
+
// refresh aggregate is permanently bound to it.
|
|
1546
|
+
unsignedVc.id =
|
|
1547
|
+
keyedPrepared?.credentialId ??
|
|
1548
|
+
unsignedVc.id ??
|
|
1549
|
+
`urn:uuid:${uuid()}`;
|
|
1550
|
+
|
|
1551
|
+
// A keyed send already allocated its refresh during preparation;
|
|
1552
|
+
// otherwise allocate once, then inject the managed service +
|
|
1553
|
+
// inline JSON-LD context so both become part of the signed payload.
|
|
1554
|
+
const allocation = keyedPrepared
|
|
1555
|
+
? {
|
|
1556
|
+
refreshId: keyedPrepared.refreshId,
|
|
1557
|
+
refreshService: keyedPrepared.refreshService,
|
|
1558
|
+
}
|
|
1559
|
+
: await traceInternal('allocateCredentialRefresh:send', () =>
|
|
1560
|
+
allocateCredentialRefresh({
|
|
1561
|
+
issuerProfile: profile,
|
|
1562
|
+
holderProfile: targetProfile,
|
|
1563
|
+
holderDid: getDidWeb(domain, targetProfile.profileId),
|
|
1564
|
+
credentialId: unsignedVc.id!,
|
|
1565
|
+
domain,
|
|
1566
|
+
})
|
|
1567
|
+
);
|
|
1568
|
+
|
|
1569
|
+
refreshId = allocation.refreshId;
|
|
1570
|
+
|
|
1571
|
+
const prepared = injectManagedRefreshService(
|
|
1572
|
+
unsignedVc,
|
|
1573
|
+
allocation.refreshService
|
|
1574
|
+
);
|
|
1575
|
+
|
|
1576
|
+
signedVc = await traceInternal(
|
|
1577
|
+
'issueCredentialWithSigningAuthority:refresh',
|
|
1578
|
+
async () =>
|
|
1579
|
+
(
|
|
1580
|
+
await issueCredentialWithSigningAuthority(
|
|
1581
|
+
{ type: 'profile', profile },
|
|
1582
|
+
prepared,
|
|
1583
|
+
signingAuthority,
|
|
1584
|
+
domain,
|
|
1585
|
+
false
|
|
1586
|
+
)
|
|
1587
|
+
).credential as VC
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// Plaintext templateData is intentionally omitted from refresh
|
|
1592
|
+
// activity metadata: refresh records never persist claim content.
|
|
1593
|
+
const activityId = await traceDb('logCredentialSent:refresh', () =>
|
|
1594
|
+
logCredentialSent({
|
|
1595
|
+
actorProfileId: profile.profileId,
|
|
1596
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1597
|
+
recipientType: 'profile',
|
|
1598
|
+
recipientIdentifier: targetProfile.profileId,
|
|
1599
|
+
recipientProfileId: targetProfile.profileId,
|
|
1600
|
+
boostUri,
|
|
1601
|
+
source: 'send',
|
|
1602
|
+
integrationId: input.integrationId,
|
|
1603
|
+
})
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
if (intent) {
|
|
1607
|
+
await recordRefreshSendIntentPending(
|
|
1608
|
+
intent,
|
|
1609
|
+
{
|
|
1610
|
+
refreshId,
|
|
1611
|
+
refreshService: managedRefreshServiceFor(refreshId, domain),
|
|
1612
|
+
credentialId: signedVc.id as string,
|
|
1613
|
+
issuerDid:
|
|
1614
|
+
getCredentialIssuerId(signedVc) ??
|
|
1615
|
+
getDidWeb(domain, profile.profileId),
|
|
1616
|
+
holderDid:
|
|
1617
|
+
intent.holderDid ??
|
|
1618
|
+
getDidWeb(domain, targetProfile.profileId),
|
|
1619
|
+
...(signedVc.credentialStatus
|
|
1620
|
+
? { credentialStatus: signedVc.credentialStatus }
|
|
1621
|
+
: {}),
|
|
1622
|
+
},
|
|
1623
|
+
activityId
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
try {
|
|
1628
|
+
const { uri, receipt } = await sendRefreshableCredential({
|
|
1629
|
+
issuerProfile: profile,
|
|
1630
|
+
refreshId,
|
|
1631
|
+
credential: signedVc,
|
|
1632
|
+
boostUri,
|
|
1633
|
+
skipNotification,
|
|
1634
|
+
domain,
|
|
1635
|
+
activityId,
|
|
1636
|
+
integrationId: input.integrationId,
|
|
1637
|
+
contractTerms: contractTerms ?? undefined,
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
const response = {
|
|
1641
|
+
type: 'boost' as const,
|
|
1642
|
+
credentialUri: uri,
|
|
1643
|
+
uri: boostUri,
|
|
1644
|
+
activityId,
|
|
1645
|
+
refresh: receipt,
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
if (intent) await markRefreshSendIntentDelivered(intent, response);
|
|
1649
|
+
|
|
1650
|
+
return response;
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
await traceDb('logCredentialFailed:refresh', () =>
|
|
1653
|
+
logCredentialFailed({
|
|
1654
|
+
activityId,
|
|
1655
|
+
actorProfileId: profile.profileId,
|
|
1656
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1657
|
+
recipientType: 'profile',
|
|
1658
|
+
recipientIdentifier: targetProfile.profileId,
|
|
1659
|
+
recipientProfileId: targetProfile.profileId,
|
|
1660
|
+
boostUri,
|
|
1661
|
+
integrationId: input.integrationId,
|
|
1662
|
+
source: 'send',
|
|
1663
|
+
metadata: {
|
|
1664
|
+
error:
|
|
1665
|
+
error instanceof Error
|
|
1666
|
+
? error.message
|
|
1667
|
+
: 'Unknown error',
|
|
1668
|
+
},
|
|
1669
|
+
})
|
|
1670
|
+
);
|
|
1671
|
+
throw error;
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
|
|
830
1676
|
// Route to Universal Inbox for email/phone recipients
|
|
831
1677
|
if (inboxRecipient) {
|
|
832
1678
|
// Try to resolve recipient profile for auto-populating template variables + recipient DID
|
|
@@ -883,11 +1729,12 @@ export const boostsRouter = t.router({
|
|
|
883
1729
|
});
|
|
884
1730
|
}
|
|
885
1731
|
|
|
886
|
-
|
|
887
|
-
credential
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1732
|
+
if (!inboxRefreshRequested)
|
|
1733
|
+
credential = await appendBitstringStatusListEntries(
|
|
1734
|
+
credential,
|
|
1735
|
+
profile.profileId,
|
|
1736
|
+
domain
|
|
1737
|
+
);
|
|
891
1738
|
}
|
|
892
1739
|
|
|
893
1740
|
// Build inbox configuration from SendOptions
|
|
@@ -895,6 +1742,7 @@ export const boostsRouter = t.router({
|
|
|
895
1742
|
const activityId = await traceDb('logCredentialSent:inbox', () =>
|
|
896
1743
|
logCredentialSent({
|
|
897
1744
|
actorProfileId: profile.profileId,
|
|
1745
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
898
1746
|
recipientType: inboxRecipient.type,
|
|
899
1747
|
recipientIdentifier: inboxRecipient.value,
|
|
900
1748
|
boostUri,
|
|
@@ -914,6 +1762,9 @@ export const boostsRouter = t.router({
|
|
|
914
1762
|
credential,
|
|
915
1763
|
{
|
|
916
1764
|
...inboxConfig,
|
|
1765
|
+
refresh: inboxRefreshRequested,
|
|
1766
|
+
idempotencyKey: inboxKey,
|
|
1767
|
+
refreshRequestDigest: inboxDigest,
|
|
917
1768
|
activityId,
|
|
918
1769
|
integrationId: input.integrationId,
|
|
919
1770
|
},
|
|
@@ -921,12 +1772,15 @@ export const boostsRouter = t.router({
|
|
|
921
1772
|
)
|
|
922
1773
|
);
|
|
923
1774
|
|
|
924
|
-
|
|
1775
|
+
const result = {
|
|
925
1776
|
type: 'boost' as const,
|
|
926
1777
|
credentialUri: '',
|
|
927
1778
|
uri: boostUri,
|
|
928
|
-
activityId,
|
|
1779
|
+
activityId: inboxResult.inboxCredential.activityId ?? activityId,
|
|
929
1780
|
inbox: {
|
|
1781
|
+
...(inboxResult.refresh
|
|
1782
|
+
? { refresh: inboxResult.refresh }
|
|
1783
|
+
: {}),
|
|
930
1784
|
issuanceId: inboxResult.inboxCredential.id,
|
|
931
1785
|
status: inboxResult.status,
|
|
932
1786
|
claimUrl: inboxResult.claimUrl,
|
|
@@ -935,12 +1789,16 @@ export const boostsRouter = t.router({
|
|
|
935
1789
|
: {}),
|
|
936
1790
|
},
|
|
937
1791
|
};
|
|
1792
|
+
if (inboxIntent)
|
|
1793
|
+
await markRefreshSendIntentDelivered(inboxIntent, result);
|
|
1794
|
+
return result;
|
|
938
1795
|
} catch (error) {
|
|
939
1796
|
// Log FAILED activity when issueToInbox fails
|
|
940
1797
|
await traceDb('logCredentialFailed:inbox', () =>
|
|
941
1798
|
logCredentialFailed({
|
|
942
1799
|
activityId,
|
|
943
1800
|
actorProfileId: profile.profileId,
|
|
1801
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
944
1802
|
recipientType: inboxRecipient.type,
|
|
945
1803
|
recipientIdentifier: inboxRecipient.value,
|
|
946
1804
|
boostUri,
|
|
@@ -1029,6 +1887,7 @@ export const boostsRouter = t.router({
|
|
|
1029
1887
|
const activityId = await traceDb('logCredentialSent:remoteInbox', () =>
|
|
1030
1888
|
logCredentialSent({
|
|
1031
1889
|
actorProfileId: profile.profileId,
|
|
1890
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1032
1891
|
recipientType: 'profile',
|
|
1033
1892
|
recipientIdentifier: input.recipient,
|
|
1034
1893
|
boostUri,
|
|
@@ -1071,6 +1930,7 @@ export const boostsRouter = t.router({
|
|
|
1071
1930
|
logCredentialFailed({
|
|
1072
1931
|
activityId,
|
|
1073
1932
|
actorProfileId: profile.profileId,
|
|
1933
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1074
1934
|
recipientType: 'profile',
|
|
1075
1935
|
recipientIdentifier: input.recipient,
|
|
1076
1936
|
boostUri,
|
|
@@ -1130,53 +1990,13 @@ export const boostsRouter = t.router({
|
|
|
1130
1990
|
});
|
|
1131
1991
|
}
|
|
1132
1992
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if (contractUri) {
|
|
1142
|
-
decodedContractUri = decodeURIComponent(contractUri);
|
|
1143
|
-
contractDetails = await traceDb('getContractDetailsByUri', () =>
|
|
1144
|
-
getContractDetailsByUri(decodedContractUri!)
|
|
1145
|
-
);
|
|
1146
|
-
|
|
1147
|
-
if (!contractDetails) {
|
|
1148
|
-
throw new TRPCError({
|
|
1149
|
-
code: 'NOT_FOUND',
|
|
1150
|
-
message: 'Could not find contract',
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const terms = await traceDb('getContractTermsForProfile', () =>
|
|
1155
|
-
getContractTermsForProfile(targetProfile, contractDetails!.contract)
|
|
1156
|
-
);
|
|
1157
|
-
|
|
1158
|
-
const writers = await traceDb('getWritersForContract', () =>
|
|
1159
|
-
getWritersForContract(contractDetails!.contract)
|
|
1160
|
-
);
|
|
1161
|
-
const isWriter = writers.some(
|
|
1162
|
-
writer => writer.profileId === profile.profileId
|
|
1163
|
-
);
|
|
1164
|
-
const isDenied =
|
|
1165
|
-
terms?.terms.deniedWriters?.includes(profile.profileId) ?? false;
|
|
1166
|
-
const categoryAllowed = boost.category
|
|
1167
|
-
? terms?.terms.write?.credentials?.categories?.[boost.category] === true
|
|
1168
|
-
: true;
|
|
1169
|
-
|
|
1170
|
-
if (terms && isWriter && !isDenied && categoryAllowed) {
|
|
1171
|
-
contractTerms = terms;
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
if (boostCreated && contractDetails) {
|
|
1176
|
-
await traceDb('setRelatedBoostForContract', () =>
|
|
1177
|
-
setRelatedBoostForContract(contractDetails!.contract, boost!)
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1993
|
+
const contractTerms = await resolveContractForSend({
|
|
1994
|
+
profile,
|
|
1995
|
+
targetProfile: targetProfile!,
|
|
1996
|
+
boost: boost!,
|
|
1997
|
+
boostCreated,
|
|
1998
|
+
contractUri,
|
|
1999
|
+
});
|
|
1180
2000
|
|
|
1181
2001
|
let signedVc: VC | JWE;
|
|
1182
2002
|
|
|
@@ -1240,6 +2060,7 @@ export const boostsRouter = t.router({
|
|
|
1240
2060
|
const activityId = await traceDb('logCredentialSent', () =>
|
|
1241
2061
|
logCredentialSent({
|
|
1242
2062
|
actorProfileId: profile.profileId,
|
|
2063
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1243
2064
|
recipientType: 'profile',
|
|
1244
2065
|
recipientIdentifier: targetProfile.profileId,
|
|
1245
2066
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -1270,6 +2091,7 @@ export const boostsRouter = t.router({
|
|
|
1270
2091
|
logCredentialFailed({
|
|
1271
2092
|
activityId,
|
|
1272
2093
|
actorProfileId: profile.profileId,
|
|
2094
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1273
2095
|
recipientType: 'profile',
|
|
1274
2096
|
recipientIdentifier: targetProfile.profileId,
|
|
1275
2097
|
recipientProfileId: targetProfile.profileId,
|