@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.
- package/.env.example +4 -0
- package/AGENTS.md +177 -159
- package/CHANGELOG.md +148 -0
- package/README.md +26 -8
- package/compose.inbox.yml +7 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/auth-grant/read.d.ts +1 -0
- package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +5 -3
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.test.d.ts +2 -0
- package/dist/accesslayer/credential/create.test.d.ts.map +1 -0
- package/dist/accesslayer/credential/relationships/read.d.ts +1 -0
- package/dist/accesslayer/credential/relationships/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/didkit_wasm_bg.wasm +0 -0
- package/dist/helpers/activity.helpers.d.ts +4 -0
- package/dist/helpers/activity.helpers.d.ts.map +1 -1
- package/dist/helpers/boost-storage.helpers.test.d.ts +2 -0
- package/dist/helpers/boost-storage.helpers.test.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts +3 -7
- package/dist/helpers/boost.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 +4 -2
- 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/issuedCredentialStatus.helpers.d.ts +5 -0
- package/dist/helpers/issuedCredentialStatus.helpers.d.ts.map +1 -0
- 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/helpers/signingAuthority.helpers.d.ts +3 -2
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/signingAuthority.helpers.test.d.ts +2 -0
- package/dist/helpers/signingAuthority.helpers.test.d.ts.map +1 -0
- package/dist/helpers/status-list.helpers.d.ts.map +1 -1
- package/dist/helpers/status-list.helpers.test.d.ts +2 -0
- package/dist/helpers/status-list.helpers.test.d.ts.map +1 -0
- 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/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Credential.d.ts +1 -0
- package/dist/models/Credential.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/app-store.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 +506 -12
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +48 -2
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-refreshes.d.ts +4 -4
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +8 -2
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/did-metadata.d.ts +8 -8
- package/dist/routes/federation.d.ts +2 -2
- package/dist/routes/inbox.d.ts +973 -31
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +47 -127
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +6 -6
- package/dist/routes/storage.d.ts +63 -63
- package/dist/routes/test.d.ts +6 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts +14 -14
- 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/consentflowcontract.d.ts +22 -8
- package/dist/types/consentflowcontract.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/credential.d.ts +7 -0
- package/dist/types/credential.d.ts.map +1 -1
- package/dist/types/did-metadata.d.ts +2 -2
- 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/app-store-listing/relationships/read.ts +4 -3
- package/src/accesslayer/auth-grant/read.ts +2 -1
- package/src/accesslayer/consentflowcontract/relationships/create.ts +15 -5
- package/src/accesslayer/consentflowcontract/relationships/read.ts +161 -63
- package/src/accesslayer/consentflowcontract/relationships/update.ts +43 -10
- package/src/accesslayer/credential/create.test.ts +141 -0
- package/src/accesslayer/credential/create.ts +27 -3
- 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/boost-storage.helpers.test.ts +242 -0
- package/src/helpers/boost.helpers.ts +58 -437
- 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 +385 -107
- package/src/helpers/credential.helpers.ts +6 -2
- package/src/helpers/finalize-inbox.helpers.ts +39 -22
- 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 +301 -50
- package/src/helpers/issuedCredentialStatus.helpers.ts +11 -0
- 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/helpers/signingAuthority.helpers.test.ts +198 -0
- package/src/helpers/signingAuthority.helpers.ts +41 -18
- package/src/helpers/status-list.helpers.test.ts +77 -0
- package/src/helpers/status-list.helpers.ts +31 -8
- package/src/inbox-queue-local.ts +28 -0
- package/src/models/AuthGrant.ts +1 -0
- package/src/models/ConsentFlowTerms.ts +4 -0
- package/src/models/ConsentFlowTransaction.ts +4 -0
- package/src/models/Credential.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 +69 -52
- package/src/routes/auth-grants.ts +13 -11
- package/src/routes/boosts.ts +943 -120
- package/src/routes/contracts.ts +55 -7
- 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 +164 -88
- package/src/routes/skills.ts +1 -1
- package/src/routes/workflows.ts +80 -31
- 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/consentflowcontract.ts +21 -3
- package/src/types/credential-refresh.ts +21 -2
- package/src/types/credential.ts +13 -1
- package/src/types/inbox-batch.ts +46 -0
- package/test/app-store-listing.spec.ts +66 -30
- package/test/atomic-counter.e2e.ts +12 -0
- package/test/auth-grant.spec.ts +21 -21
- package/test/bitstring-status-list.spec.ts +12 -9
- package/test/consentflow.spec.ts +453 -26
- package/test/credential-refresh-email.spec.ts +663 -0
- package/test/credential-refresh-publication.spec.ts +13 -5
- package/test/guardian-gated-routes.spec.ts +337 -180
- 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/inbox-security.e2e.ts +48 -3
- package/test/openapi.spec.ts +41 -1
- package/test/refresh-send.spec.ts +1799 -0
- package/test/registry.spec.ts +56 -11
- package/test/revoke-suspend-per-instance.spec.ts +9 -1
- package/vitest.config.ts +13 -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,
|
|
@@ -175,6 +191,29 @@ import {
|
|
|
175
191
|
} from '@accesslayer/role/relationships/create';
|
|
176
192
|
import { updateDefaultPermissionsForBoost } from '@accesslayer/role/relationships/update';
|
|
177
193
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
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';
|
|
178
217
|
import { removeConnectionsForBoost } from '@helpers/connection.helpers';
|
|
179
218
|
import { issueToInbox } from '@helpers/inbox.helpers';
|
|
180
219
|
import { findInboxServiceEndpoint } from '@helpers/federation.helpers';
|
|
@@ -241,7 +280,336 @@ const resolveBoostCredentialInstance = async ({
|
|
|
241
280
|
return getCredentialInstanceForBoostAndProfile(boostId, recipientProfileId);
|
|
242
281
|
};
|
|
243
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
|
+
|
|
244
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
|
+
|
|
245
613
|
getBoostAlignments: profileRoute
|
|
246
614
|
.meta({
|
|
247
615
|
openapi: {
|
|
@@ -691,6 +1059,7 @@ export const boostsRouter = t.router({
|
|
|
691
1059
|
// Log credential activity FIRST to get activityId for chaining
|
|
692
1060
|
const activityId = await logCredentialSent({
|
|
693
1061
|
actorProfileId: profile.profileId,
|
|
1062
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
694
1063
|
recipientType: 'profile',
|
|
695
1064
|
recipientIdentifier: targetProfile.profileId,
|
|
696
1065
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -735,6 +1104,147 @@ export const boostsRouter = t.router({
|
|
|
735
1104
|
const { contractUri } = input;
|
|
736
1105
|
const { domain } = ctx;
|
|
737
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
|
+
|
|
738
1248
|
// Check if recipient is email/phone (routes to Universal Inbox)
|
|
739
1249
|
const inboxRecipient = isInboxRecipient(input.recipient);
|
|
740
1250
|
|
|
@@ -742,9 +1252,23 @@ export const boostsRouter = t.router({
|
|
|
742
1252
|
let boost = null as BoostInstance | null;
|
|
743
1253
|
let boostUri = '';
|
|
744
1254
|
let boostCreated = false;
|
|
745
|
-
let boostCreatedFromSignedCredential = 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,48 +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
|
-
|
|
801
|
-
|
|
1352
|
+
boostUri = getBoostUri(boost.id, domain);
|
|
1353
|
+
boostCreated = true;
|
|
1354
|
+
}
|
|
802
1355
|
}
|
|
803
1356
|
|
|
804
1357
|
if (!boost) {
|
|
@@ -828,6 +1381,298 @@ export const boostsRouter = t.router({
|
|
|
828
1381
|
});
|
|
829
1382
|
}
|
|
830
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
|
+
|
|
831
1676
|
// Route to Universal Inbox for email/phone recipients
|
|
832
1677
|
if (inboxRecipient) {
|
|
833
1678
|
// Try to resolve recipient profile for auto-populating template variables + recipient DID
|
|
@@ -884,11 +1729,12 @@ export const boostsRouter = t.router({
|
|
|
884
1729
|
});
|
|
885
1730
|
}
|
|
886
1731
|
|
|
887
|
-
|
|
888
|
-
credential
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1732
|
+
if (!inboxRefreshRequested)
|
|
1733
|
+
credential = await appendBitstringStatusListEntries(
|
|
1734
|
+
credential,
|
|
1735
|
+
profile.profileId,
|
|
1736
|
+
domain
|
|
1737
|
+
);
|
|
892
1738
|
}
|
|
893
1739
|
|
|
894
1740
|
// Build inbox configuration from SendOptions
|
|
@@ -896,6 +1742,7 @@ export const boostsRouter = t.router({
|
|
|
896
1742
|
const activityId = await traceDb('logCredentialSent:inbox', () =>
|
|
897
1743
|
logCredentialSent({
|
|
898
1744
|
actorProfileId: profile.profileId,
|
|
1745
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
899
1746
|
recipientType: inboxRecipient.type,
|
|
900
1747
|
recipientIdentifier: inboxRecipient.value,
|
|
901
1748
|
boostUri,
|
|
@@ -915,6 +1762,9 @@ export const boostsRouter = t.router({
|
|
|
915
1762
|
credential,
|
|
916
1763
|
{
|
|
917
1764
|
...inboxConfig,
|
|
1765
|
+
refresh: inboxRefreshRequested,
|
|
1766
|
+
idempotencyKey: inboxKey,
|
|
1767
|
+
refreshRequestDigest: inboxDigest,
|
|
918
1768
|
activityId,
|
|
919
1769
|
integrationId: input.integrationId,
|
|
920
1770
|
},
|
|
@@ -922,12 +1772,15 @@ export const boostsRouter = t.router({
|
|
|
922
1772
|
)
|
|
923
1773
|
);
|
|
924
1774
|
|
|
925
|
-
|
|
1775
|
+
const result = {
|
|
926
1776
|
type: 'boost' as const,
|
|
927
1777
|
credentialUri: '',
|
|
928
1778
|
uri: boostUri,
|
|
929
|
-
activityId,
|
|
1779
|
+
activityId: inboxResult.inboxCredential.activityId ?? activityId,
|
|
930
1780
|
inbox: {
|
|
1781
|
+
...(inboxResult.refresh
|
|
1782
|
+
? { refresh: inboxResult.refresh }
|
|
1783
|
+
: {}),
|
|
931
1784
|
issuanceId: inboxResult.inboxCredential.id,
|
|
932
1785
|
status: inboxResult.status,
|
|
933
1786
|
claimUrl: inboxResult.claimUrl,
|
|
@@ -936,12 +1789,16 @@ export const boostsRouter = t.router({
|
|
|
936
1789
|
: {}),
|
|
937
1790
|
},
|
|
938
1791
|
};
|
|
1792
|
+
if (inboxIntent)
|
|
1793
|
+
await markRefreshSendIntentDelivered(inboxIntent, result);
|
|
1794
|
+
return result;
|
|
939
1795
|
} catch (error) {
|
|
940
1796
|
// Log FAILED activity when issueToInbox fails
|
|
941
1797
|
await traceDb('logCredentialFailed:inbox', () =>
|
|
942
1798
|
logCredentialFailed({
|
|
943
1799
|
activityId,
|
|
944
1800
|
actorProfileId: profile.profileId,
|
|
1801
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
945
1802
|
recipientType: inboxRecipient.type,
|
|
946
1803
|
recipientIdentifier: inboxRecipient.value,
|
|
947
1804
|
boostUri,
|
|
@@ -1011,23 +1868,26 @@ export const boostsRouter = t.router({
|
|
|
1011
1868
|
signedVc = await traceInternal(
|
|
1012
1869
|
'issueCredentialWithSigningAuthority:remoteInbox',
|
|
1013
1870
|
async () =>
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1871
|
+
(
|
|
1872
|
+
await issueCredentialWithSigningAuthority(
|
|
1873
|
+
{ type: 'profile', profile },
|
|
1874
|
+
await appendBitstringStatusListEntries(
|
|
1875
|
+
unsignedVc,
|
|
1876
|
+
profile.profileId,
|
|
1877
|
+
domain
|
|
1878
|
+
),
|
|
1879
|
+
signingAuthority,
|
|
1880
|
+
domain,
|
|
1881
|
+
false
|
|
1882
|
+
)
|
|
1883
|
+
).credential
|
|
1025
1884
|
);
|
|
1026
1885
|
}
|
|
1027
1886
|
|
|
1028
1887
|
const activityId = await traceDb('logCredentialSent:remoteInbox', () =>
|
|
1029
1888
|
logCredentialSent({
|
|
1030
1889
|
actorProfileId: profile.profileId,
|
|
1890
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1031
1891
|
recipientType: 'profile',
|
|
1032
1892
|
recipientIdentifier: input.recipient,
|
|
1033
1893
|
boostUri,
|
|
@@ -1070,6 +1930,7 @@ export const boostsRouter = t.router({
|
|
|
1070
1930
|
logCredentialFailed({
|
|
1071
1931
|
activityId,
|
|
1072
1932
|
actorProfileId: profile.profileId,
|
|
1933
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1073
1934
|
recipientType: 'profile',
|
|
1074
1935
|
recipientIdentifier: input.recipient,
|
|
1075
1936
|
boostUri,
|
|
@@ -1129,53 +1990,13 @@ export const boostsRouter = t.router({
|
|
|
1129
1990
|
});
|
|
1130
1991
|
}
|
|
1131
1992
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (contractUri) {
|
|
1141
|
-
decodedContractUri = decodeURIComponent(contractUri);
|
|
1142
|
-
contractDetails = await traceDb('getContractDetailsByUri', () =>
|
|
1143
|
-
getContractDetailsByUri(decodedContractUri!)
|
|
1144
|
-
);
|
|
1145
|
-
|
|
1146
|
-
if (!contractDetails) {
|
|
1147
|
-
throw new TRPCError({
|
|
1148
|
-
code: 'NOT_FOUND',
|
|
1149
|
-
message: 'Could not find contract',
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
const terms = await traceDb('getContractTermsForProfile', () =>
|
|
1154
|
-
getContractTermsForProfile(targetProfile, contractDetails!.contract)
|
|
1155
|
-
);
|
|
1156
|
-
|
|
1157
|
-
const writers = await traceDb('getWritersForContract', () =>
|
|
1158
|
-
getWritersForContract(contractDetails!.contract)
|
|
1159
|
-
);
|
|
1160
|
-
const isWriter = writers.some(
|
|
1161
|
-
writer => writer.profileId === profile.profileId
|
|
1162
|
-
);
|
|
1163
|
-
const isDenied =
|
|
1164
|
-
terms?.terms.deniedWriters?.includes(profile.profileId) ?? false;
|
|
1165
|
-
const categoryAllowed = boost.category
|
|
1166
|
-
? terms?.terms.write?.credentials?.categories?.[boost.category] === true
|
|
1167
|
-
: true;
|
|
1168
|
-
|
|
1169
|
-
if (terms && isWriter && !isDenied && categoryAllowed) {
|
|
1170
|
-
contractTerms = terms;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
if (boostCreated && contractDetails) {
|
|
1175
|
-
await traceDb('setRelatedBoostForContract', () =>
|
|
1176
|
-
setRelatedBoostForContract(contractDetails!.contract, boost!)
|
|
1177
|
-
);
|
|
1178
|
-
}
|
|
1993
|
+
const contractTerms = await resolveContractForSend({
|
|
1994
|
+
profile,
|
|
1995
|
+
targetProfile: targetProfile!,
|
|
1996
|
+
boost: boost!,
|
|
1997
|
+
boostCreated,
|
|
1998
|
+
contractUri,
|
|
1999
|
+
});
|
|
1179
2000
|
|
|
1180
2001
|
let signedVc: VC | JWE;
|
|
1181
2002
|
|
|
@@ -1217,17 +2038,19 @@ export const boostsRouter = t.router({
|
|
|
1217
2038
|
signedVc = await traceInternal(
|
|
1218
2039
|
'issueCredentialWithSigningAuthority',
|
|
1219
2040
|
async () =>
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
2041
|
+
(
|
|
2042
|
+
await issueCredentialWithSigningAuthority(
|
|
2043
|
+
{ type: 'profile', profile },
|
|
2044
|
+
await appendBitstringStatusListEntries(
|
|
2045
|
+
unsignedVc,
|
|
2046
|
+
profile.profileId,
|
|
2047
|
+
domain
|
|
2048
|
+
),
|
|
2049
|
+
signingAuthority,
|
|
2050
|
+
domain,
|
|
2051
|
+
false
|
|
2052
|
+
)
|
|
2053
|
+
).credential
|
|
1231
2054
|
);
|
|
1232
2055
|
}
|
|
1233
2056
|
|
|
@@ -1237,6 +2060,7 @@ export const boostsRouter = t.router({
|
|
|
1237
2060
|
const activityId = await traceDb('logCredentialSent', () =>
|
|
1238
2061
|
logCredentialSent({
|
|
1239
2062
|
actorProfileId: profile.profileId,
|
|
2063
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1240
2064
|
recipientType: 'profile',
|
|
1241
2065
|
recipientIdentifier: targetProfile.profileId,
|
|
1242
2066
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -1255,7 +2079,6 @@ export const boostsRouter = t.router({
|
|
|
1255
2079
|
credential: signedVc,
|
|
1256
2080
|
domain,
|
|
1257
2081
|
skipNotification,
|
|
1258
|
-
skipCertification: boostCreatedFromSignedCredential,
|
|
1259
2082
|
contractTerms: contractTerms ?? undefined,
|
|
1260
2083
|
activityId,
|
|
1261
2084
|
integrationId: input.integrationId,
|
|
@@ -1268,6 +2091,7 @@ export const boostsRouter = t.router({
|
|
|
1268
2091
|
logCredentialFailed({
|
|
1269
2092
|
activityId,
|
|
1270
2093
|
actorProfileId: profile.profileId,
|
|
2094
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1271
2095
|
recipientType: 'profile',
|
|
1272
2096
|
recipientIdentifier: targetProfile.profileId,
|
|
1273
2097
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -1453,8 +2277,7 @@ export const boostsRouter = t.router({
|
|
|
1453
2277
|
|
|
1454
2278
|
const decodedUri = decodeURIComponent(uri);
|
|
1455
2279
|
const { domain: uriDomain } = getUriParts(decodedUri, true);
|
|
1456
|
-
//
|
|
1457
|
-
// injected alignment targetUrls equal the verifier's re-computed ones.
|
|
2280
|
+
// Use the bare domain so alignment targetUrls match issuance-time injection.
|
|
1458
2281
|
const alignmentsDomain = getDomainFromUri(decodedUri);
|
|
1459
2282
|
const [boost, boostInstance] = await Promise.all([
|
|
1460
2283
|
getBoostByUriWithDefaultClaimPermissions(decodedUri),
|
|
@@ -3760,7 +4583,7 @@ export const boostsRouter = t.router({
|
|
|
3760
4583
|
});
|
|
3761
4584
|
}
|
|
3762
4585
|
|
|
3763
|
-
let credential:
|
|
4586
|
+
let credential: IssuedCredential;
|
|
3764
4587
|
try {
|
|
3765
4588
|
credential = await issueCredentialWithSigningAuthority(
|
|
3766
4589
|
{ type: 'profile', profile },
|