@learncard/network-brain-service 3.19.1 → 4.0.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 +44 -111
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +109 -0
- package/README.md +9 -2
- package/bootstrapLambda.test.ts +72 -0
- package/config/config.dev.json +29 -0
- package/config/config.example.json +29 -0
- package/config/config.json +1 -0
- package/config/config.production.json +29 -0
- package/config/config.scouts.dev.json +24 -0
- package/config/config.scouts.production.json +24 -0
- package/contractEventsLambda.ts +18 -0
- package/contractEventsLambdaApp.ts +19 -0
- package/didWebLambda.ts +13 -18
- package/didWebLambdaApp.ts +22 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
- package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
- package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
- package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
- package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
- 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/relationships/create.d.ts +4 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/accesslayer/share-link/abandon.d.ts.map +1 -1
- package/dist/accesslayer/share-link/attachment.d.ts +37 -0
- package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
- package/dist/accesslayer/share-link/finalize.d.ts.map +1 -1
- package/dist/accesslayer/share-link/helpers.d.ts +3 -0
- package/dist/accesslayer/share-link/helpers.d.ts.map +1 -1
- package/dist/accesslayer/share-link/recovery.d.ts.map +1 -1
- package/dist/accesslayer/share-link/reserve.d.ts.map +1 -1
- package/dist/accesslayer/share-link/revoke.d.ts.map +1 -1
- package/dist/accesslayer/share-link/types.d.ts +8 -3
- package/dist/accesslayer/share-link/types.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +49 -0
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/applyDockerStageConfig.d.ts +2 -0
- package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
- package/dist/config/environment.d.ts +5 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +112 -0
- package/dist/config/stageConfig.d.ts.map +1 -0
- package/dist/config/stageConfig.test.d.ts +2 -0
- package/dist/config/stageConfig.test.d.ts.map +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/docker-entry.d.ts.map +1 -1
- package/dist/helpers/app-manifest.helpers.d.ts +5 -0
- package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
- package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
- package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
- package/dist/helpers/consent-referral.helpers.d.ts +2 -0
- package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
- package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
- package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts +25 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-requests.helpers.d.ts +19 -0
- package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
- package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inline-template.helpers.d.ts +45 -0
- package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts +6 -0
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/profile.helpers.d.ts +1 -0
- package/dist/helpers/profile.helpers.d.ts.map +1 -1
- package/dist/helpers/share-link-attachment-access.d.ts +4 -0
- package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment.d.ts +18 -0
- package/dist/helpers/share-link-attachment.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -1
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/runner.d.ts +1 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/runtime.d.ts +2 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/types.d.ts +4 -1
- package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -1
- package/dist/helpers/share-link-owner-projection.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/production.d.ts +3 -5
- package/dist/helpers/share-link-policy/production.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/resolver.d.ts +1 -1
- package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/types.d.ts +8 -2
- package/dist/helpers/share-link-policy/types.d.ts.map +1 -1
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/smartResume.helpers.d.ts +3 -0
- package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AppManifestVersion.d.ts +20 -0
- package/dist/models/AppManifestVersion.d.ts.map +1 -0
- package/dist/models/AppStoreListing.d.ts +13 -0
- package/dist/models/AppStoreListing.d.ts.map +1 -1
- package/dist/models/ConsentFlowContract.d.ts +13 -2
- package/dist/models/ConsentFlowContract.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/Integration.d.ts +2 -0
- package/dist/models/Integration.d.ts.map +1 -1
- package/dist/models/ShareLink.d.ts +2 -0
- package/dist/models/ShareLink.d.ts.map +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/share-link-constraints.d.ts.map +1 -1
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/routes/app-store.d.ts +619 -0
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +2 -2
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +146 -3
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +34 -0
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +52 -2
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +55 -0
- package/dist/routes/public-share-links.d.ts.map +1 -1
- package/dist/routes/share-links.d.ts +175 -14
- package/dist/routes/share-links.d.ts.map +1 -1
- package/dist/routes/test.d.ts +5 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/types/app-manifest.d.ts +339 -0
- package/dist/types/app-manifest.d.ts.map +1 -0
- package/dist/types/app-store-listing.d.ts +34 -0
- package/dist/types/app-store-listing.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +30 -3
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +1 -1
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +49 -195
- package/lambdaApp.ts +203 -0
- package/package.json +20 -18
- package/scripts/export-openapi.ts +20 -0
- package/scripts/offline-schema-environment.ts +18 -0
- package/scripts/verify-python-contracts.ts +72 -0
- package/serverless-local.yml +3 -6
- package/serverless.function-env.cjs +50 -0
- package/serverless.local.yml +0 -1
- package/serverless.yml +27 -74
- package/shareLinkMaintenanceLambda.ts +13 -44
- package/shareLinkMaintenanceLambdaApp.ts +50 -0
- package/src/accesslayer/app-manifest-version/create.ts +57 -0
- package/src/accesslayer/app-manifest-version/read.ts +129 -0
- package/src/accesslayer/app-manifest-version/update.ts +61 -0
- package/src/accesslayer/app-store-listing/read.ts +6 -0
- package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
- package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
- package/src/accesslayer/app-store-listing/update.ts +15 -0
- package/src/accesslayer/boost/relationships/create.ts +8 -2
- package/src/accesslayer/consentflowcontract/create.ts +81 -8
- package/src/accesslayer/consentflowcontract/read.ts +84 -10
- package/src/accesslayer/consentflowcontract/relationships/create.ts +116 -93
- package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
- package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
- package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/update.ts +240 -189
- package/src/accesslayer/credential/create.test.ts +2 -115
- package/src/accesslayer/credential/create.ts +11 -6
- package/src/accesslayer/credential/relationships/create.ts +17 -10
- package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
- package/src/accesslayer/inbox-credential/update.ts +4 -2
- package/src/accesslayer/share-link/abandon.ts +20 -0
- package/src/accesslayer/share-link/attachment.ts +360 -0
- package/src/accesslayer/share-link/finalize.ts +55 -7
- package/src/accesslayer/share-link/helpers.ts +33 -2
- package/src/accesslayer/share-link/recovery.ts +20 -0
- package/src/accesslayer/share-link/reserve.ts +85 -6
- package/src/accesslayer/share-link/revoke.ts +35 -0
- package/src/accesslayer/share-link/types.ts +9 -4
- package/src/cache/app-store.caches.ts +35 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/environment.test.ts +17 -12
- package/src/config/environment.ts +1 -0
- package/src/config/serverlessFunctionEnv.test.ts +105 -0
- package/src/config/stageConfig.test.ts +25 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +24 -0
- package/src/helpers/app-manifest.helpers.ts +185 -0
- package/src/helpers/claim-hooks.helpers.ts +20 -7
- package/src/helpers/consent-referral.helpers.ts +6 -0
- package/src/helpers/consent-scopes.helpers.ts +58 -0
- package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
- package/src/helpers/contract-events.helpers.ts +540 -0
- package/src/helpers/contract-requests.helpers.ts +211 -0
- package/src/helpers/finalize-inbox.helpers.ts +163 -44
- package/src/helpers/inbox.helpers.ts +51 -66
- package/src/helpers/inline-template.helpers.ts +560 -0
- package/src/helpers/notificationMessages.ts +59 -0
- package/src/helpers/notificationQueue.helpers.ts +23 -5
- package/src/helpers/notifications.helpers.ts +25 -1
- package/src/helpers/profile.helpers.ts +6 -0
- package/src/helpers/share-link-attachment-access.ts +87 -0
- package/src/helpers/share-link-attachment.ts +115 -0
- package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
- package/src/helpers/share-link-coordinator/coordinator.ts +15 -2
- package/src/helpers/share-link-coordinator/recovery-runner.ts +15 -0
- package/src/helpers/share-link-maintenance/runner.ts +28 -0
- package/src/helpers/share-link-maintenance/runtime.ts +6 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +3 -0
- package/src/helpers/share-link-maintenance/types.ts +2 -0
- package/src/helpers/share-link-owner-projection.ts +2 -0
- package/src/helpers/share-link-policy/production.ts +37 -17
- package/src/helpers/share-link-policy/resolver.ts +16 -11
- package/src/helpers/share-link-policy/types.ts +15 -7
- package/src/helpers/signingAuthority.helpers.ts +23 -14
- package/src/helpers/smartResume.helpers.ts +123 -0
- package/src/models/AppManifestVersion.ts +43 -0
- package/src/models/AppStoreListing.ts +32 -0
- package/src/models/ConsentFlowContract.ts +34 -3
- package/src/models/ConsentFlowTerms.ts +10 -0
- package/src/models/ConsentFlowTransaction.ts +3 -0
- package/src/models/Integration.ts +2 -0
- package/src/models/ShareLink.ts +4 -0
- package/src/models/index.ts +23 -0
- package/src/models/share-link-constraints.ts +5 -0
- package/src/openapi.ts +44 -8
- package/src/routes/app-store.ts +1054 -27
- package/src/routes/boosts.ts +12 -14
- package/src/routes/contracts.ts +382 -262
- package/src/routes/inbox.ts +4 -2
- package/src/routes/index.ts +4 -1
- package/src/routes/profile-manager.ts +19 -0
- package/src/routes/profiles.ts +12 -0
- package/src/routes/public-share-links.ts +169 -0
- package/src/routes/share-links.ts +69 -0
- package/src/routes/workflows.ts +3 -2
- package/src/types/app-manifest.ts +28 -0
- package/src/types/app-store-listing.ts +13 -0
- package/src/types/consentflowcontract.ts +25 -4
- package/test/app-manifest.helpers.spec.ts +170 -0
- package/test/app-manifest.routes.spec.ts +299 -0
- package/test/app-store-listing-updates.spec.ts +236 -0
- package/test/app-store-listing.spec.ts +65 -3
- package/test/consent-data-boundaries.spec.ts +352 -0
- package/test/consent-recipients.spec.ts +513 -0
- package/test/consent-scopes.helpers.spec.ts +67 -0
- package/test/consentflow.spec.ts +63 -2
- package/test/contract-requests.spec.ts +1588 -0
- package/test/credential-storage.neo4j.spec.ts +122 -0
- package/test/guardian-gated-routes.spec.ts +36 -0
- package/test/inbox-security.e2e.ts +26 -10
- package/test/inbox.spec.ts +184 -8
- package/test/inline-template.helpers.spec.ts +49 -0
- package/test/managed-recipient-inbox.spec.ts +24 -2
- package/test/openapi.spec.ts +14 -73
- package/test/profiles.spec.ts +30 -0
- package/test/share-link-attachment.neo4j.spec.ts +262 -0
- package/test/share-link-attachment.unit.spec.ts +244 -0
- package/test/share-link-lifecycle.neo4j.spec.ts +145 -2
- package/test/share-link-maintenance.unit.spec.ts +32 -5
- package/test/share-link-owner-route.unit.spec.ts +70 -2
- package/test/share-link-policy.unit.spec.ts +18 -4
- package/test/share-link-production-policy.unit.spec.ts +58 -9
- package/test/share-link-public-route.unit.spec.ts +279 -0
- package/test/share-link-service-policy.neo4j.spec.ts +403 -0
- package/test/signing-authority-encryption.spec.ts +75 -0
- package/test/smart-resume-consent.spec.ts +233 -0
- package/test/smart-resume-upload.spec.ts +65 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +13 -0
- package/vitest.inbox-e2e.config.ts +2 -0
- package/workerLambda.test.ts +100 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { BindParam, QueryBuilder } from 'neogma';
|
|
3
|
+
import { TRPCError } from '@trpc/server';
|
|
4
|
+
import type { SendContractRequest } from '@learncard/types';
|
|
5
|
+
import {
|
|
6
|
+
getContractByUri,
|
|
7
|
+
getContractRequestAccess,
|
|
8
|
+
getStoredContractRequest,
|
|
9
|
+
} from '@accesslayer/consentflowcontract/read';
|
|
10
|
+
import { getProfileByProfileId } from '@accesslayer/profile/read';
|
|
11
|
+
import { getWritersForContract } from '@accesslayer/consentflowcontract/relationships/read';
|
|
12
|
+
import {
|
|
13
|
+
canReadContractData,
|
|
14
|
+
lockContractAudience,
|
|
15
|
+
runAudienceMutation,
|
|
16
|
+
} from '@accesslayer/consentflowcontract/relationships/recipients';
|
|
17
|
+
import { getProfileIdFromString } from './did.helpers';
|
|
18
|
+
import { userHasRequiredScopes } from './auth-grant.helpers';
|
|
19
|
+
import { enforceRateLimits } from './rateLimit.helpers';
|
|
20
|
+
import { requestEventCypher, tryDispatchContractEvent } from './contract-events.helpers';
|
|
21
|
+
import type { ProfileType } from 'types/profile';
|
|
22
|
+
import type { DbContractType } from 'types/consentflowcontract';
|
|
23
|
+
|
|
24
|
+
/** Resolve shared request inputs without granting writers access to consented data. */
|
|
25
|
+
export const resolveContractRequest = async (
|
|
26
|
+
contractUri: string,
|
|
27
|
+
targetIdentifier: string,
|
|
28
|
+
profile: ProfileType,
|
|
29
|
+
domain: string,
|
|
30
|
+
allowRecipients = true
|
|
31
|
+
): Promise<{ contract: DbContractType; target: ProfileType }> => {
|
|
32
|
+
const contract = await getContractByUri(contractUri);
|
|
33
|
+
if (!contract) throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
|
|
34
|
+
if (!(await canManageContractRequests(contract, profile.profileId, allowRecipients)))
|
|
35
|
+
throw new TRPCError({
|
|
36
|
+
code: 'UNAUTHORIZED',
|
|
37
|
+
message: 'You do not have permission to send requests for this contract.',
|
|
38
|
+
});
|
|
39
|
+
const targetId = await getProfileIdFromString(targetIdentifier, domain);
|
|
40
|
+
const target = targetId ? await getProfileByProfileId(targetId) : null;
|
|
41
|
+
if (!target) throw new TRPCError({ code: 'NOT_FOUND', message: 'Target profile not found' });
|
|
42
|
+
return { contract, target };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const canManageContractRequests = async (
|
|
46
|
+
contract: DbContractType,
|
|
47
|
+
profileId: string,
|
|
48
|
+
allowRecipients = false
|
|
49
|
+
): Promise<boolean> => {
|
|
50
|
+
const access = await getContractRequestAccess(contract.id, profileId);
|
|
51
|
+
return access.isManager || (allowRecipients && access.isRecipient);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** One request per contract/target in v1. Exact pending retries do not create another event. */
|
|
55
|
+
export const sendGenericContractRequest = async (
|
|
56
|
+
input: SendContractRequest,
|
|
57
|
+
profile: ProfileType,
|
|
58
|
+
domain: string
|
|
59
|
+
): Promise<boolean> => {
|
|
60
|
+
const { contract, target } = await resolveContractRequest(
|
|
61
|
+
input.contractUri,
|
|
62
|
+
input.targetProfileId,
|
|
63
|
+
profile,
|
|
64
|
+
domain
|
|
65
|
+
);
|
|
66
|
+
if (target.profileId === profile.profileId)
|
|
67
|
+
throw new TRPCError({
|
|
68
|
+
code: 'BAD_REQUEST',
|
|
69
|
+
message: 'You cannot send a request to yourself.',
|
|
70
|
+
});
|
|
71
|
+
await enforceRateLimits([
|
|
72
|
+
{
|
|
73
|
+
key: `contract-request-rate:${contract.id}:${profile.profileId}`,
|
|
74
|
+
limit: 500,
|
|
75
|
+
windowSeconds: 3600,
|
|
76
|
+
description: '500 contract requests per hour',
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
const requestId = randomUUID();
|
|
80
|
+
const query = lockContractAudience(
|
|
81
|
+
new QueryBuilder(
|
|
82
|
+
new BindParam({
|
|
83
|
+
targetProfileId: target.profileId,
|
|
84
|
+
senderProfileId: profile.profileId,
|
|
85
|
+
requestId,
|
|
86
|
+
externalReferenceId: input.externalReferenceId ?? null,
|
|
87
|
+
message: input.message ?? null,
|
|
88
|
+
now: new Date().toISOString(),
|
|
89
|
+
domain,
|
|
90
|
+
eventFrom: profile.profileId,
|
|
91
|
+
eventKind: 'request_sent',
|
|
92
|
+
})
|
|
93
|
+
),
|
|
94
|
+
contract.id
|
|
95
|
+
).raw(`
|
|
96
|
+
MATCH (target:Profile {profileId: $targetProfileId})
|
|
97
|
+
WITH contract, target WHERE $targetProfileId <> $senderProfileId AND EXISTS {
|
|
98
|
+
MATCH (contract)-[:CREATED_BY|CAN_WRITE|SHARES_DATA_WITH]->(:Profile {profileId: $senderProfileId})
|
|
99
|
+
} AND CASE WHEN contract.expiresAt IS NULL OR trim(contract.expiresAt) = '' THEN true ELSE datetime(contract.expiresAt) > datetime($now) END
|
|
100
|
+
AND NOT EXISTS {
|
|
101
|
+
MATCH (target)-[:CREATED_BY]->(terms:ConsentFlowTerms)-[:CONSENTS_TO]->(contract)
|
|
102
|
+
WHERE (terms.status = 'live' OR (terms.status = 'stale' AND terms.oneTime = true)) AND
|
|
103
|
+
CASE WHEN terms.expiresAt IS NULL OR trim(terms.expiresAt) = '' THEN true ELSE datetime(terms.expiresAt) > datetime($now) END
|
|
104
|
+
}
|
|
105
|
+
OPTIONAL MATCH (contract)-[existing:REQUESTED_FOR]->(target)
|
|
106
|
+
WITH contract, target, existing WHERE existing IS NULL OR
|
|
107
|
+
(existing.status = 'pending' AND existing.requestId IS NOT NULL AND
|
|
108
|
+
existing.requestedBy = $senderProfileId AND
|
|
109
|
+
coalesce(existing.externalReferenceId, '') = coalesce($externalReferenceId, '') AND
|
|
110
|
+
coalesce(existing.message, '') = coalesce($message, ''))
|
|
111
|
+
MERGE (contract)-[request:REQUESTED_FOR]->(target)
|
|
112
|
+
ON CREATE SET request += {requestId: $requestId, requestedBy: $senderProfileId,
|
|
113
|
+
externalReferenceId: $externalReferenceId, message: $message,
|
|
114
|
+
requestedAt: $now, status: 'pending', readStatus: 'unseen'}
|
|
115
|
+
${requestEventCypher}
|
|
116
|
+
`);
|
|
117
|
+
const result = await runAudienceMutation(query);
|
|
118
|
+
if (!result.records.length)
|
|
119
|
+
throw new TRPCError({
|
|
120
|
+
code: 'CONFLICT',
|
|
121
|
+
message:
|
|
122
|
+
'Active consent or a conflicting/terminal request already exists, or the contract changed.',
|
|
123
|
+
});
|
|
124
|
+
await tryDispatchContractEvent(result.records[0]!.get('eventId'));
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Target-only denial; cancellation also permits owner/writers and the requesting recipient. */
|
|
129
|
+
export const decideGenericContractRequest = async ({
|
|
130
|
+
contract,
|
|
131
|
+
targetProfileId,
|
|
132
|
+
actorProfileId,
|
|
133
|
+
status,
|
|
134
|
+
domain,
|
|
135
|
+
}: {
|
|
136
|
+
contract: DbContractType;
|
|
137
|
+
targetProfileId: string;
|
|
138
|
+
actorProfileId: string;
|
|
139
|
+
status: 'denied' | 'cancelled';
|
|
140
|
+
domain: string;
|
|
141
|
+
}): Promise<boolean> => {
|
|
142
|
+
const request = await getStoredContractRequest(contract.id, targetProfileId);
|
|
143
|
+
if (!request?.requestId)
|
|
144
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract request not found' });
|
|
145
|
+
const isTarget = actorProfileId === targetProfileId;
|
|
146
|
+
const writers = await getWritersForContract(contract);
|
|
147
|
+
const authorized =
|
|
148
|
+
isTarget ||
|
|
149
|
+
(status === 'cancelled' &&
|
|
150
|
+
(writers.some(profile => profile.profileId === actorProfileId) ||
|
|
151
|
+
(request.requestedBy === actorProfileId &&
|
|
152
|
+
(await canReadContractData(contract.id, actorProfileId)))));
|
|
153
|
+
if (!authorized)
|
|
154
|
+
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'You cannot change this request' });
|
|
155
|
+
const result = await runAudienceMutation(
|
|
156
|
+
lockContractAudience(
|
|
157
|
+
new QueryBuilder(
|
|
158
|
+
new BindParam({
|
|
159
|
+
targetProfileId,
|
|
160
|
+
actorProfileId,
|
|
161
|
+
status,
|
|
162
|
+
domain,
|
|
163
|
+
now: new Date().toISOString(),
|
|
164
|
+
eventKind: status === 'denied' ? 'request_denied' : 'request_cancelled',
|
|
165
|
+
eventFrom: actorProfileId,
|
|
166
|
+
})
|
|
167
|
+
),
|
|
168
|
+
contract.id
|
|
169
|
+
).raw(`
|
|
170
|
+
MATCH (contract)-[request:REQUESTED_FOR]->(target:Profile {profileId: $targetProfileId})
|
|
171
|
+
WHERE request.requestId IS NOT NULL AND request.status IN ['pending', $status]
|
|
172
|
+
AND ($actorProfileId = $targetProfileId OR ($status = 'cancelled' AND
|
|
173
|
+
(EXISTS { MATCH (contract)-[:CREATED_BY|CAN_WRITE]->(:Profile {profileId: $actorProfileId}) } OR
|
|
174
|
+
(request.requestedBy = $actorProfileId AND EXISTS {
|
|
175
|
+
MATCH (contract)-[:SHARES_DATA_WITH]->(:Profile {profileId: $actorProfileId}) }))))
|
|
176
|
+
SET request.status = $status
|
|
177
|
+
${requestEventCypher}
|
|
178
|
+
`)
|
|
179
|
+
);
|
|
180
|
+
if (!result.records.length)
|
|
181
|
+
throw new TRPCError({
|
|
182
|
+
code: 'CONFLICT',
|
|
183
|
+
message: 'Request already decided or permission changed',
|
|
184
|
+
});
|
|
185
|
+
await tryDispatchContractEvent(result.records[0]!.get('eventId'));
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const markGenericContractRequestSeen = async (
|
|
190
|
+
contractId: string,
|
|
191
|
+
targetProfileId: string
|
|
192
|
+
): Promise<boolean> => {
|
|
193
|
+
const result = await runAudienceMutation(
|
|
194
|
+
lockContractAudience(new QueryBuilder(new BindParam({ targetProfileId })), contractId)
|
|
195
|
+
.raw(`MATCH (contract)-[request:REQUESTED_FOR]->(:Profile {profileId: $targetProfileId})
|
|
196
|
+
WHERE request.requestId IS NOT NULL
|
|
197
|
+
SET request.readStatus = 'seen' RETURN request`)
|
|
198
|
+
);
|
|
199
|
+
if (!result.records.length)
|
|
200
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Request not found' });
|
|
201
|
+
return true;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Legacy request mutations retain their scope behavior; attributed requests require write. */
|
|
205
|
+
export const assertGenericRequestWriteScope = (scope?: string): void => {
|
|
206
|
+
if (!userHasRequiredScopes(scope ?? '', 'contracts:write'))
|
|
207
|
+
throw new TRPCError({
|
|
208
|
+
code: 'UNAUTHORIZED',
|
|
209
|
+
message: 'This operation requires contracts:write scope',
|
|
210
|
+
});
|
|
211
|
+
};
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
LCNNotificationTypeEnumValidator,
|
|
4
4
|
LCNInboxStatusEnumValidator,
|
|
5
5
|
VC,
|
|
6
|
+
JWE,
|
|
6
7
|
UnsignedVC,
|
|
8
|
+
VCValidator,
|
|
9
|
+
JWEValidator,
|
|
10
|
+
InboxCredentialType,
|
|
7
11
|
} from '@learncard/types';
|
|
8
12
|
import { ProfileType } from 'types/profile';
|
|
9
13
|
import { getContactMethodsForProfile } from '@accesslayer/contact-method/read';
|
|
@@ -21,6 +25,113 @@ import { getLearnCard, getEmptyLearnCard } from '@helpers/learnCard.helpers';
|
|
|
21
25
|
import { logCredentialClaimed, logCredentialFailed } from '@helpers/activity.helpers';
|
|
22
26
|
import { handleConnectionPromptsForCredentialClaim } from '@helpers/connectionPrompt.helpers';
|
|
23
27
|
import { decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
|
|
28
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
29
|
+
import { getBitstringStatusListEntries } from '@learncard/helpers';
|
|
30
|
+
import type { IssuedCredential } from 'types/credential';
|
|
31
|
+
import { getBoostByUri } from '@accesslayer/boost/read';
|
|
32
|
+
import { storeCredential } from '@accesslayer/credential/create';
|
|
33
|
+
import { createBoostInstanceOfRelationship } from '@accesslayer/boost/relationships/create';
|
|
34
|
+
import {
|
|
35
|
+
createSentCredentialRelationship,
|
|
36
|
+
createReceivedCredentialRelationship,
|
|
37
|
+
setDefaultClaimedRole,
|
|
38
|
+
} from '@accesslayer/credential/relationships/create';
|
|
39
|
+
import { neogma } from '@instance';
|
|
40
|
+
import { processClaimHooks } from '@helpers/claim-hooks.helpers';
|
|
41
|
+
import { clearDidWebCacheForChildProfileManagers } from '@accesslayer/boost/relationships/update';
|
|
42
|
+
|
|
43
|
+
/** Graph-only completion: a failed write retains pending escrow for the next attempt. */
|
|
44
|
+
const finalizeAcceptedDelivery = async (
|
|
45
|
+
inboxCredential: InboxCredentialType,
|
|
46
|
+
issued: IssuedCredential,
|
|
47
|
+
recipient: ProfileType,
|
|
48
|
+
sender: ProfileType
|
|
49
|
+
): Promise<boolean> => {
|
|
50
|
+
const boost = inboxCredential.boostUri
|
|
51
|
+
? await getBoostByUri(inboxCredential.boostUri)
|
|
52
|
+
: undefined;
|
|
53
|
+
const session = neogma.driver.session();
|
|
54
|
+
const transaction = session.beginTransaction({ timeout: 30_000 });
|
|
55
|
+
// Signing and status-index allocation happen before this bounded graph transaction;
|
|
56
|
+
// a retry or lost claim race may leave unused indices, intentionally, rather than
|
|
57
|
+
// hold Neo4j locks across external signing.
|
|
58
|
+
try {
|
|
59
|
+
const finalized = await finalizeAndWipeInboxCredential(
|
|
60
|
+
inboxCredential.id,
|
|
61
|
+
{ recipientDid: recipient.did, credential: JWEValidator.parse(issued.credential) },
|
|
62
|
+
{},
|
|
63
|
+
transaction
|
|
64
|
+
);
|
|
65
|
+
if (!finalized) {
|
|
66
|
+
// Another claim or expiry won the lock. It is not a new issuance failure.
|
|
67
|
+
await transaction.rollback();
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const instance = await storeCredential(issued, transaction);
|
|
71
|
+
if (boost) await createBoostInstanceOfRelationship(instance, boost, transaction);
|
|
72
|
+
await createSentCredentialRelationship(
|
|
73
|
+
{ type: 'profile', profile: sender },
|
|
74
|
+
recipient,
|
|
75
|
+
instance,
|
|
76
|
+
undefined,
|
|
77
|
+
inboxCredential.activityId,
|
|
78
|
+
inboxCredential.integrationId,
|
|
79
|
+
transaction
|
|
80
|
+
);
|
|
81
|
+
await createReceivedCredentialRelationship(
|
|
82
|
+
recipient,
|
|
83
|
+
sender,
|
|
84
|
+
instance,
|
|
85
|
+
undefined,
|
|
86
|
+
transaction
|
|
87
|
+
);
|
|
88
|
+
await processClaimHooks(recipient, instance, transaction);
|
|
89
|
+
await setDefaultClaimedRole(recipient, instance, transaction);
|
|
90
|
+
if (
|
|
91
|
+
!(await createClaimedRelationship(
|
|
92
|
+
recipient.profileId,
|
|
93
|
+
inboxCredential.id,
|
|
94
|
+
'finalize',
|
|
95
|
+
transaction
|
|
96
|
+
))
|
|
97
|
+
) {
|
|
98
|
+
throw new Error('Inbox claim recipient no longer exists');
|
|
99
|
+
}
|
|
100
|
+
await transaction.commit();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
await transaction
|
|
103
|
+
.rollback()
|
|
104
|
+
.catch(rollbackError =>
|
|
105
|
+
console.error(
|
|
106
|
+
'Failed to roll back inbox finalization',
|
|
107
|
+
inboxCredential.id,
|
|
108
|
+
rollbackError
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
throw error;
|
|
112
|
+
} finally {
|
|
113
|
+
// Closing the session cannot turn a committed delivery into a pending failure.
|
|
114
|
+
await session
|
|
115
|
+
.close()
|
|
116
|
+
.catch(error =>
|
|
117
|
+
console.error(
|
|
118
|
+
'Failed to close inbox finalization session',
|
|
119
|
+
inboxCredential.id,
|
|
120
|
+
error
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (boost) {
|
|
125
|
+
await clearDidWebCacheForChildProfileManagers(boost.id).catch(error =>
|
|
126
|
+
console.error(
|
|
127
|
+
'Could not clear did:web cache after inbox claim',
|
|
128
|
+
inboxCredential.id,
|
|
129
|
+
error
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
24
135
|
|
|
25
136
|
export async function finalizeInboxCredentialsForProfile(
|
|
26
137
|
profile: ProfileType,
|
|
@@ -30,8 +141,8 @@ export async function finalizeInboxCredentialsForProfile(
|
|
|
30
141
|
claimed: number;
|
|
31
142
|
errors: number;
|
|
32
143
|
guardianPending: number;
|
|
33
|
-
verifiableCredentials: VC[];
|
|
34
|
-
deliveries: { id: string; credential: VC }[];
|
|
144
|
+
verifiableCredentials: (VC | JWE)[];
|
|
145
|
+
deliveries: { id: string; credential: VC | JWE }[];
|
|
35
146
|
}> {
|
|
36
147
|
const contactMethods = await getContactMethodsForProfile(profile.did);
|
|
37
148
|
const verifiedContacts = contactMethods.filter(cm => cm.isVerified);
|
|
@@ -48,8 +159,8 @@ export async function finalizeInboxCredentialsForProfile(
|
|
|
48
159
|
lcDid = lc.id.did();
|
|
49
160
|
} catch {}
|
|
50
161
|
|
|
51
|
-
const verifiableCredentials: VC[] = [];
|
|
52
|
-
const deliveries: { id: string; credential: VC }[] = [];
|
|
162
|
+
const verifiableCredentials: (VC | JWE)[] = [];
|
|
163
|
+
const deliveries: { id: string; credential: VC | JWE }[] = [];
|
|
53
164
|
|
|
54
165
|
for (const cm of verifiedContacts) {
|
|
55
166
|
const pending = await getAcceptedPendingInboxCredentialsForContactMethodId(cm.id);
|
|
@@ -79,7 +190,8 @@ export async function finalizeInboxCredentialsForProfile(
|
|
|
79
190
|
}
|
|
80
191
|
|
|
81
192
|
try {
|
|
82
|
-
let finalCredential: VC;
|
|
193
|
+
let finalCredential: VC | JWE;
|
|
194
|
+
let issued: IssuedCredential | undefined;
|
|
83
195
|
const credentialPayload = inboxCredential.refreshId
|
|
84
196
|
? undefined
|
|
85
197
|
: await decryptInboxCredential(inboxCredential.credential);
|
|
@@ -102,29 +214,16 @@ export async function finalizeInboxCredentialsForProfile(
|
|
|
102
214
|
if (!endpoint || !name)
|
|
103
215
|
throw new Error('Inbox credential missing signing authority info');
|
|
104
216
|
|
|
105
|
-
|
|
106
|
-
if (!issuerProfile) throw new Error('Issuer profile not found');
|
|
217
|
+
if (!senderProfile) throw new Error('Issuer profile not found');
|
|
107
218
|
|
|
108
219
|
const signingAuthorityForUser = await getSigningAuthorityForUserByName(
|
|
109
|
-
|
|
220
|
+
senderProfile,
|
|
110
221
|
endpoint,
|
|
111
222
|
name
|
|
112
223
|
);
|
|
113
224
|
if (!signingAuthorityForUser) throw new Error('Signing authority not found');
|
|
114
225
|
|
|
115
|
-
|
|
116
|
-
if (Array.isArray(unsignedCredential.credentialSubject)) {
|
|
117
|
-
unsignedCredential.credentialSubject =
|
|
118
|
-
unsignedCredential.credentialSubject.map(sub => ({
|
|
119
|
-
...sub,
|
|
120
|
-
id: (sub as any).did || (sub as any).id || profile.did,
|
|
121
|
-
}));
|
|
122
|
-
} else {
|
|
123
|
-
(unsignedCredential.credentialSubject as any).id =
|
|
124
|
-
(unsignedCredential as any).credentialSubject?.did ||
|
|
125
|
-
(unsignedCredential as any).credentialSubject?.id ||
|
|
126
|
-
profile.did;
|
|
127
|
-
}
|
|
226
|
+
setCredentialSubjectIds(unsignedCredential, profile.did);
|
|
128
227
|
|
|
129
228
|
// Set issuer from signing authority
|
|
130
229
|
unsignedCredential.issuer = signingAuthorityForUser.relationship.did;
|
|
@@ -136,36 +235,56 @@ export async function finalizeInboxCredentialsForProfile(
|
|
|
136
235
|
? getAppDidWeb(domain, listingSlug)
|
|
137
236
|
: undefined;
|
|
138
237
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
238
|
+
issued = await issueCredentialWithSigningAuthority(
|
|
239
|
+
{ type: 'profile', profile: senderProfile },
|
|
240
|
+
unsignedCredential,
|
|
241
|
+
signingAuthorityForUser,
|
|
242
|
+
domain,
|
|
243
|
+
undefined,
|
|
244
|
+
ownerDidOverride,
|
|
245
|
+
undefined,
|
|
246
|
+
// Preserve issuer did:key access alongside the managed/app did:web reader.
|
|
247
|
+
[senderProfile.did]
|
|
248
|
+
);
|
|
249
|
+
finalCredential = issued.credential;
|
|
149
250
|
} else {
|
|
150
251
|
finalCredential = JSON.parse(credentialPayload!) as VC;
|
|
151
252
|
}
|
|
152
253
|
|
|
153
254
|
if (!inboxCredential.refreshId) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
255
|
+
if (!senderProfile) throw new Error('Issuer profile not found');
|
|
256
|
+
if (!issued) {
|
|
257
|
+
const signedCredential = VCValidator.parse(finalCredential);
|
|
258
|
+
const learnCard = await getEmptyLearnCard();
|
|
259
|
+
issued = {
|
|
260
|
+
kind: 'issued-credential',
|
|
261
|
+
credential: await learnCard.invoke.createDagJwe(signedCredential, [
|
|
262
|
+
profile.did,
|
|
263
|
+
inboxCredential.issuerDid,
|
|
264
|
+
]),
|
|
265
|
+
statusEntries: getBitstringStatusListEntries(signedCredential),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
!(await finalizeAcceptedDelivery(
|
|
270
|
+
inboxCredential,
|
|
271
|
+
issued,
|
|
272
|
+
profile,
|
|
273
|
+
senderProfile
|
|
274
|
+
))
|
|
275
|
+
)
|
|
276
|
+
continue;
|
|
277
|
+
finalCredential = issued.credential;
|
|
165
278
|
}
|
|
166
279
|
|
|
167
|
-
//
|
|
168
|
-
|
|
280
|
+
// Managed refresh retains its separate lifecycle; ordinary claims audit in the transaction.
|
|
281
|
+
if (inboxCredential.refreshId) {
|
|
282
|
+
await createClaimedRelationship(
|
|
283
|
+
profile.profileId,
|
|
284
|
+
inboxCredential.id,
|
|
285
|
+
'finalize'
|
|
286
|
+
);
|
|
287
|
+
}
|
|
169
288
|
|
|
170
289
|
if (
|
|
171
290
|
senderProfile &&
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
|
|
29
29
|
import { ProfileType, SigningAuthorityForUserType } from 'types/profile';
|
|
30
30
|
import type { IssuedCredential } from 'types/credential';
|
|
31
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
31
32
|
import { getBitstringStatusListEntries } from '@learncard/helpers';
|
|
32
33
|
import { createInboxCredential } from '@accesslayer/inbox-credential/create';
|
|
33
34
|
import { Context } from '@routes';
|
|
@@ -111,51 +112,48 @@ export const claimIntoInbox = async (
|
|
|
111
112
|
);
|
|
112
113
|
|
|
113
114
|
if (existingProfile) {
|
|
114
|
-
|
|
115
|
-
let finalCredential: VC;
|
|
116
|
-
|
|
115
|
+
let issued: IssuedCredential;
|
|
117
116
|
if (isSigned) {
|
|
118
|
-
|
|
117
|
+
const signedCredential = credential as VC;
|
|
118
|
+
const learnCard = await getEmptyLearnCard();
|
|
119
|
+
issued = {
|
|
120
|
+
kind: 'issued-credential',
|
|
121
|
+
credential: await learnCard.invoke.createDagJwe(signedCredential, [
|
|
122
|
+
existingProfile.did,
|
|
123
|
+
issuerProfile.did,
|
|
124
|
+
]),
|
|
125
|
+
statusEntries: getBitstringStatusListEntries(signedCredential),
|
|
126
|
+
};
|
|
119
127
|
} else {
|
|
120
|
-
|
|
128
|
+
const unsignedCredential = credential as UnsignedVC;
|
|
129
|
+
setCredentialSubjectIds(unsignedCredential, existingProfile.did);
|
|
130
|
+
// Listings sign as the app, but the issuing profile must remain a reader.
|
|
121
131
|
const ownerDidOverride = listingSlug
|
|
122
132
|
? getAppDidWeb(ctx.domain, listingSlug)
|
|
123
133
|
: undefined;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
).credential as VC;
|
|
134
|
+
issued = await issueCredentialWithSigningAuthority(
|
|
135
|
+
{ type: 'profile', profile: issuerProfile },
|
|
136
|
+
unsignedCredential,
|
|
137
|
+
signingAuthorityForUser,
|
|
138
|
+
ctx.domain,
|
|
139
|
+
undefined,
|
|
140
|
+
ownerDidOverride,
|
|
141
|
+
undefined,
|
|
142
|
+
[issuerProfile.did]
|
|
143
|
+
);
|
|
135
144
|
}
|
|
136
|
-
|
|
137
|
-
// Use the explicit-recipient API; the seeded encryption plugin also adds the service DID.
|
|
138
|
-
const learnCard = await getEmptyLearnCard();
|
|
139
|
-
const encryptedDelivery = await learnCard.invoke.createDagJwe(finalCredential, [
|
|
140
|
-
existingProfile.did,
|
|
141
|
-
issuerProfile.did,
|
|
142
|
-
]);
|
|
143
145
|
await sendCredential(
|
|
144
146
|
issuerProfile,
|
|
145
147
|
existingProfile,
|
|
146
|
-
|
|
147
|
-
kind: 'issued-credential',
|
|
148
|
-
credential: encryptedDelivery,
|
|
149
|
-
statusEntries: getBitstringStatusListEntries(finalCredential),
|
|
150
|
-
},
|
|
148
|
+
issued,
|
|
151
149
|
ctx.domain,
|
|
152
150
|
undefined,
|
|
153
151
|
activityId,
|
|
154
152
|
integrationId
|
|
155
153
|
);
|
|
156
|
-
// Record successful delivery without creating temporary service-readable escrow.
|
|
157
154
|
const finalizedInboxCredential = await createInboxCredential({
|
|
158
|
-
credential
|
|
155
|
+
// Delivered audit rows never retain a credential payload.
|
|
156
|
+
credential: '',
|
|
159
157
|
isSigned: true,
|
|
160
158
|
delivered: true,
|
|
161
159
|
isAccepted: true,
|
|
@@ -166,7 +164,6 @@ export const claimIntoInbox = async (
|
|
|
166
164
|
activityId,
|
|
167
165
|
expiresInDays,
|
|
168
166
|
});
|
|
169
|
-
|
|
170
167
|
return {
|
|
171
168
|
status: LCNInboxStatusEnumValidator.enum.ISSUED,
|
|
172
169
|
inboxCredential: finalizedInboxCredential,
|
|
@@ -333,50 +330,39 @@ export const issueToInbox = async (
|
|
|
333
330
|
const recipientIsManaged = recipientManagers.length > 0;
|
|
334
331
|
|
|
335
332
|
if (existingProfile && !guardianEmail && !recipientIsManaged && !configuration.refresh) {
|
|
336
|
-
|
|
337
|
-
let finalCredential: VC;
|
|
338
|
-
|
|
333
|
+
let issued: IssuedCredential;
|
|
339
334
|
if (isSigned) {
|
|
340
|
-
|
|
335
|
+
const signedCredential = credential as VC;
|
|
336
|
+
const learnCard = await getEmptyLearnCard();
|
|
337
|
+
issued = {
|
|
338
|
+
kind: 'issued-credential',
|
|
339
|
+
credential: await learnCard.invoke.createDagJwe(signedCredential, [
|
|
340
|
+
existingProfile.did,
|
|
341
|
+
issuerProfile.did,
|
|
342
|
+
]),
|
|
343
|
+
statusEntries: getBitstringStatusListEntries(signedCredential),
|
|
344
|
+
};
|
|
341
345
|
} else {
|
|
342
|
-
// Sign the credential using signing authority
|
|
343
346
|
const signingAuthorityForUser = await getSigningAuthorityForUserByName(
|
|
344
347
|
issuerProfile,
|
|
345
348
|
signingAuthority!.endpoint,
|
|
346
349
|
signingAuthority!.name
|
|
347
350
|
);
|
|
348
|
-
|
|
349
351
|
if (!signingAuthorityForUser) {
|
|
350
352
|
throw new InboxIssuancePreflightError({
|
|
351
353
|
code: 'NOT_FOUND',
|
|
352
354
|
message: 'Signing authority not found for issuer',
|
|
353
355
|
});
|
|
354
356
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
).credential as VC;
|
|
357
|
+
const unsignedCredential = credential as UnsignedVC;
|
|
358
|
+
setCredentialSubjectIds(unsignedCredential, existingProfile.did);
|
|
359
|
+
issued = await issueCredentialWithSigningAuthority(
|
|
360
|
+
{ type: 'profile', profile: issuerProfile },
|
|
361
|
+
unsignedCredential,
|
|
362
|
+
signingAuthorityForUser,
|
|
363
|
+
ctx.domain
|
|
364
|
+
);
|
|
365
365
|
}
|
|
366
|
-
|
|
367
|
-
// Send credential using appropriate helper (sendBoost handles boost tracking)
|
|
368
|
-
// Pass activityId and integrationId so they're stored on the relationship for CLAIMED chaining
|
|
369
|
-
const learnCard = await getEmptyLearnCard();
|
|
370
|
-
const encryptedDelivery = await learnCard.invoke.createDagJwe(finalCredential, [
|
|
371
|
-
existingProfile.did,
|
|
372
|
-
issuerProfile.did,
|
|
373
|
-
]);
|
|
374
|
-
// Carry only public status coordinates alongside the newly encrypted payload.
|
|
375
|
-
const delivery: IssuedCredential = {
|
|
376
|
-
kind: 'issued-credential',
|
|
377
|
-
credential: encryptedDelivery,
|
|
378
|
-
statusEntries: getBitstringStatusListEntries(finalCredential),
|
|
379
|
-
};
|
|
380
366
|
const boost = boostUri ? await getBoostByUri(boostUri) : undefined;
|
|
381
367
|
await checkpointDelivery();
|
|
382
368
|
if (boostUri && boost) {
|
|
@@ -384,27 +370,26 @@ export const issueToInbox = async (
|
|
|
384
370
|
from: { type: 'profile', profile: issuerProfile },
|
|
385
371
|
to: existingProfile,
|
|
386
372
|
boost,
|
|
387
|
-
credential:
|
|
373
|
+
credential: issued,
|
|
388
374
|
domain: ctx.domain,
|
|
389
375
|
activityId,
|
|
390
376
|
integrationId,
|
|
391
377
|
});
|
|
392
378
|
} else {
|
|
393
|
-
// Fall back to ordinary delivery when no boost exists for the supplied URI.
|
|
394
379
|
await sendCredential(
|
|
395
380
|
issuerProfile,
|
|
396
381
|
existingProfile,
|
|
397
|
-
|
|
382
|
+
issued,
|
|
398
383
|
ctx.domain,
|
|
399
384
|
undefined,
|
|
400
385
|
activityId,
|
|
401
386
|
integrationId
|
|
402
387
|
);
|
|
403
388
|
}
|
|
404
|
-
|
|
405
389
|
// Record successful delivery without creating temporary service-readable escrow.
|
|
406
390
|
const finalizedInboxCredential = await createInboxCredential({
|
|
407
|
-
credential
|
|
391
|
+
// Delivered audit rows never retain a credential payload.
|
|
392
|
+
credential: '',
|
|
408
393
|
isSigned: true,
|
|
409
394
|
delivered: true,
|
|
410
395
|
recipient,
|