gdc-sdk-node-ts 2.9.16 → 2.9.18
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/dist/authorized-subject-directory.d.ts +4 -0
- package/dist/authorized-subject-directory.js +25 -6
- package/dist/family-organization-search.d.ts +6 -2
- package/dist/family-organization-search.js +27 -3
- package/dist/individual-start.d.ts +19 -2
- package/dist/individual-start.js +32 -4
- package/dist/node-runtime-client.d.ts +4 -1
- package/dist/server-profile-session.d.ts +93 -2
- package/dist/server-profile-session.js +267 -20
- package/package.json +2 -2
|
@@ -20,10 +20,14 @@ export type AuthorizedIndividualSubjectDirectoryInput = Readonly<{
|
|
|
20
20
|
/** One exact subject projection paired with its accepted grant metadata. */
|
|
21
21
|
export type AuthorizedIndividualSubject = Readonly<{
|
|
22
22
|
subjectDid: string;
|
|
23
|
+
/** Opaque provider resource id required for exact lifecycle operations. */
|
|
24
|
+
organizationId?: string;
|
|
23
25
|
role?: string;
|
|
24
26
|
/** Accepted provider grant claims; descriptive metadata, never action authority. */
|
|
25
27
|
grantClaims: Readonly<Record<string, unknown>>;
|
|
26
28
|
authorizationEvidenceId?: string;
|
|
29
|
+
/** Governed RelatedPerson identifier used as this subject relationship's attester. */
|
|
30
|
+
relatedPersonId?: string;
|
|
27
31
|
issuerDid?: string;
|
|
28
32
|
subjectClaims: Readonly<Record<string, unknown>>;
|
|
29
33
|
}>;
|
|
@@ -9,6 +9,7 @@ const CLAIM_PERSON_TELEPHONE = 'org.schema.Person.telephone';
|
|
|
9
9
|
const CLAIM_ORGANIZATION_SAME_AS = 'org.schema.Organization.sameAs';
|
|
10
10
|
const CLAIM_ORGANIZATION_OWNER_EMAIL = 'org.schema.Organization.owner.email';
|
|
11
11
|
const CLAIM_ORGANIZATION_OWNER_TELEPHONE = 'org.schema.Organization.owner.telephone';
|
|
12
|
+
const CLAIM_ORGANIZATION_OWNER_IDENTIFIER = 'org.schema.Organization.owner.identifier.value';
|
|
12
13
|
const CLAIM_ORGANIZATION_MEMBER_ROLE = 'org.schema.Organization.member.role';
|
|
13
14
|
const CLAIM_RELATED_PERSON_ROLE = 'RelatedPerson.role';
|
|
14
15
|
const CLAIM_OCCUPATION_IDENTIFIER = 'org.schema.Person.hasOccupation.identifier.value';
|
|
@@ -42,7 +43,11 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
|
|
|
42
43
|
}],
|
|
43
44
|
},
|
|
44
45
|
}, input.pollOptions);
|
|
45
|
-
const ownedSubjects = readOwnedSubjects(ownerResult, {
|
|
46
|
+
const ownedSubjects = readOwnedSubjects(ownerResult, {
|
|
47
|
+
email,
|
|
48
|
+
telephone,
|
|
49
|
+
ownerRole: routeContext.sector === 'animal-care' ? 'RESPRSN' : undefined,
|
|
50
|
+
});
|
|
46
51
|
const subjectsByDid = new Map(ownedSubjects.map((subject) => [subject.subjectDid, subject]));
|
|
47
52
|
const licenseClaims = {
|
|
48
53
|
'@context': SCHEMA_CONTEXT,
|
|
@@ -80,17 +85,19 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
|
|
|
80
85
|
}],
|
|
81
86
|
},
|
|
82
87
|
}, input.pollOptions);
|
|
83
|
-
const subjectClaims =
|
|
88
|
+
const { claims: subjectClaims, organizationId } = readFirstOrganization(organizationResult);
|
|
84
89
|
if (String(subjectClaims[CLAIM_ORGANIZATION_SAME_AS] || '').trim() !== license.subjectDid) {
|
|
85
90
|
return undefined;
|
|
86
91
|
}
|
|
87
92
|
return {
|
|
88
93
|
subjectDid: license.subjectDid,
|
|
94
|
+
...(organizationId ? { organizationId } : {}),
|
|
89
95
|
...(license.role ? { role: license.role } : {}),
|
|
90
96
|
grantClaims: license.grantClaims,
|
|
91
97
|
...(license.authorizationEvidenceId
|
|
92
98
|
? { authorizationEvidenceId: license.authorizationEvidenceId }
|
|
93
99
|
: {}),
|
|
100
|
+
...(license.relatedPersonId ? { relatedPersonId: license.relatedPersonId } : {}),
|
|
94
101
|
...(license.issuerDid ? { issuerDid: license.issuerDid } : {}),
|
|
95
102
|
subjectClaims,
|
|
96
103
|
};
|
|
@@ -119,10 +126,14 @@ function readOwnedSubjects(result, verified) {
|
|
|
119
126
|
const subjectDid = String(claims[CLAIM_ORGANIZATION_SAME_AS] || '').trim();
|
|
120
127
|
if (!subjectDid)
|
|
121
128
|
return [];
|
|
122
|
-
const role =
|
|
129
|
+
const role = verified.ownerRole
|
|
130
|
+
|| String(claims[CLAIM_ORGANIZATION_MEMBER_ROLE] || '').trim();
|
|
131
|
+
const relatedPersonId = String(claims[CLAIM_ORGANIZATION_OWNER_IDENTIFIER] || '').trim();
|
|
123
132
|
return [{
|
|
124
133
|
subjectDid,
|
|
134
|
+
...(String(resource?.id || '').trim() ? { organizationId: String(resource.id).trim() } : {}),
|
|
125
135
|
...(role ? { role } : {}),
|
|
136
|
+
...(relatedPersonId ? { relatedPersonId } : {}),
|
|
126
137
|
grantClaims: {},
|
|
127
138
|
subjectClaims: claims,
|
|
128
139
|
}];
|
|
@@ -143,20 +154,28 @@ function readAcceptedLicenses(result) {
|
|
|
143
154
|
.map((value) => value.trim().toUpperCase())
|
|
144
155
|
.filter(Boolean);
|
|
145
156
|
const occupation = String(claims[CLAIM_OCCUPATION_IDENTIFIER] || '').trim();
|
|
146
|
-
const role = relatedRoles.includes('CAREGIVER')
|
|
157
|
+
const role = relatedRoles.includes('CAREGIVER')
|
|
158
|
+
? 'CAREGIVER'
|
|
159
|
+
: relatedRoles[0] || occupation;
|
|
160
|
+
const relatedPersonId = String(meta.relatedPersonId || '').trim();
|
|
147
161
|
return [{
|
|
148
162
|
subjectDid,
|
|
149
163
|
...(role ? { role } : {}),
|
|
150
164
|
grantClaims: claims,
|
|
151
165
|
...(meta.identifier ? { authorizationEvidenceId: String(meta.identifier) } : {}),
|
|
166
|
+
...(relatedPersonId ? { relatedPersonId } : {}),
|
|
152
167
|
...(meta.issuerDid ? { issuerDid: String(meta.issuerDid) } : {}),
|
|
153
168
|
}];
|
|
154
169
|
});
|
|
155
170
|
}
|
|
156
|
-
function
|
|
171
|
+
function readFirstOrganization(result) {
|
|
157
172
|
const entry = readData(result)[0];
|
|
158
173
|
const claims = entry?.resource?.meta?.claims || entry?.meta?.claims;
|
|
159
|
-
|
|
174
|
+
const organizationId = String(entry?.resource?.id || entry?.id || '').trim();
|
|
175
|
+
return {
|
|
176
|
+
claims: claims && typeof claims === 'object' ? claims : {},
|
|
177
|
+
...(organizationId ? { organizationId } : {}),
|
|
178
|
+
};
|
|
160
179
|
}
|
|
161
180
|
function readData(result) {
|
|
162
181
|
const pollBody = (result?.poll?.body || {});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FamilyOrganizationSummary } from 'gdc-common-utils-ts/utils/family-organization-summary';
|
|
1
|
+
import { type FamilyOrganizationSummary, type FamilyRegistrationStatus } from 'gdc-common-utils-ts/utils/family-organization-summary';
|
|
2
2
|
import type { PollOptions, SubmitAndPollResult } from './orchestration/client-port.js';
|
|
3
3
|
import type { RouteContext } from './individual-onboarding.js';
|
|
4
4
|
export type FamilyOrganizationSearchInput = Readonly<{
|
|
@@ -19,7 +19,11 @@ export type OwnedFamilyOrganizationDirectoryInput = Readonly<{
|
|
|
19
19
|
}>;
|
|
20
20
|
export type OwnedFamilyOrganizationDirectoryEntry = Readonly<{
|
|
21
21
|
resourceId: string;
|
|
22
|
-
|
|
22
|
+
/** Missing only for an owner-private registration draft. */
|
|
23
|
+
alternateName?: string;
|
|
24
|
+
status?: FamilyRegistrationStatus;
|
|
25
|
+
birthDate?: string;
|
|
26
|
+
missingFields?: string[];
|
|
23
27
|
claims: Readonly<Record<string, unknown>>;
|
|
24
28
|
}>;
|
|
25
29
|
type SearchFamilyOrganizationWithDeps = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
2
|
import { readFamilyOrganizationSummaryFromResponseBody, } from 'gdc-common-utils-ts/utils/family-organization-summary';
|
|
3
|
-
import { ClaimsOrganizationSchemaorg, ClaimsServiceSchemaorg } from 'gdc-common-utils-ts/constants';
|
|
3
|
+
import { ClaimsOrganizationSchemaorg, ClaimsPersonSchemaorg, ClaimsServiceSchemaorg } from 'gdc-common-utils-ts/constants';
|
|
4
4
|
import { resolvePollOptionsFromSeconds } from './poll-options.js';
|
|
5
5
|
/**
|
|
6
6
|
* Searches one existing family/individual organization registration by the
|
|
@@ -100,11 +100,35 @@ export async function listOwnedFamilyOrganizationsWithDeps(routeCtx, input, deps
|
|
|
100
100
|
if (!((email && ownerEmail === email) || (telephone && ownerTelephone === telephone)))
|
|
101
101
|
return [];
|
|
102
102
|
const alternateName = String(resourceClaims[ClaimsOrganizationSchemaorg.alternateName] || '').trim();
|
|
103
|
-
|
|
103
|
+
const rawStatus = String(resourceClaims['org.schema.FamilyRegistration.status'] || '').trim();
|
|
104
|
+
const status = isFamilyRegistrationStatus(rawStatus) ? rawStatus : undefined;
|
|
105
|
+
if (!alternateName && status !== 'draft_saved' && status !== 'resume_required')
|
|
104
106
|
return [];
|
|
105
|
-
|
|
107
|
+
const birthDate = String(resourceClaims[ClaimsPersonSchemaorg.birthDate]
|
|
108
|
+
|| resourceClaims['org.schema.Organization.foundingDate']
|
|
109
|
+
|| '').trim();
|
|
110
|
+
const missingFields = Array.isArray(resourceClaims['org.schema.FamilyRegistration.missingFields'])
|
|
111
|
+
? resourceClaims['org.schema.FamilyRegistration.missingFields']
|
|
112
|
+
.map((value) => String(value || '').trim())
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
: [];
|
|
115
|
+
return [{
|
|
116
|
+
resourceId: String(resource.id),
|
|
117
|
+
...(alternateName ? { alternateName } : {}),
|
|
118
|
+
...(status ? { status } : {}),
|
|
119
|
+
...(birthDate ? { birthDate } : {}),
|
|
120
|
+
...(missingFields.length ? { missingFields } : {}),
|
|
121
|
+
claims: resourceClaims,
|
|
122
|
+
}];
|
|
106
123
|
});
|
|
107
124
|
}
|
|
125
|
+
function isFamilyRegistrationStatus(value) {
|
|
126
|
+
return value === 'new_created'
|
|
127
|
+
|| value === 'draft_saved'
|
|
128
|
+
|| value === 'resume_required'
|
|
129
|
+
|| value === 'already_exists'
|
|
130
|
+
|| value === 'not_found';
|
|
131
|
+
}
|
|
108
132
|
function createRuntimeUuid() {
|
|
109
133
|
const fromCrypto = globalThis.crypto?.randomUUID?.();
|
|
110
134
|
if (fromCrypto) {
|
|
@@ -6,6 +6,14 @@ import type { PollOptions, SubmitAndPollResult } from './orchestration/client-po
|
|
|
6
6
|
import type { RouteContext } from './individual-onboarding.js';
|
|
7
7
|
import type { OfferPreview } from './order-offer-summary.js';
|
|
8
8
|
export type IndividualOrganizationRegistrationInput = {
|
|
9
|
+
/**
|
|
10
|
+
* Explicitly saves an owner-private, cardless registration draft.
|
|
11
|
+
*
|
|
12
|
+
* This is intended for accessible channels that captured a birth year/date
|
|
13
|
+
* but could not reliably capture the subject's name. It never authorizes an
|
|
14
|
+
* Order or public card by itself.
|
|
15
|
+
*/
|
|
16
|
+
registrationIntent?: 'activate' | 'save-private-draft';
|
|
9
17
|
/**
|
|
10
18
|
* Preferred high-level input produced by `createIndividualOnboardingEditor()`.
|
|
11
19
|
*
|
|
@@ -77,8 +85,17 @@ export type IndividualOrganizationRegistrationInput = {
|
|
|
77
85
|
export type IndividualOrganizationBootstrapInput = IndividualOrganizationRegistrationInput;
|
|
78
86
|
export type IndividualOrganizationRegistrationResult = {
|
|
79
87
|
registration: SubmitAndPollResult;
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Owner-private Organization UUID returned by GW for a saved draft.
|
|
90
|
+
*
|
|
91
|
+
* It is not a public card identifier or DID and is present only when
|
|
92
|
+
* `registrationIntent` was `save-private-draft`.
|
|
93
|
+
*/
|
|
94
|
+
draftId?: string;
|
|
95
|
+
/** Absent only for an explicitly requested owner-private draft. */
|
|
96
|
+
offerId?: string;
|
|
97
|
+
/** Absent only for an explicitly requested owner-private draft. */
|
|
98
|
+
offerPreview?: OfferPreview;
|
|
82
99
|
/** Lifecycle state returned by GW for this registration receipt. */
|
|
83
100
|
registrationStatus?: FamilyRegistrationStatus;
|
|
84
101
|
/** False when the same family registration is already active. */
|
package/dist/individual-start.js
CHANGED
|
@@ -26,7 +26,8 @@ export async function registerIndividualOrganizationWithDeps(deps) {
|
|
|
26
26
|
* routing/indexing contract for this flow.
|
|
27
27
|
*/
|
|
28
28
|
const onboardingDraft = deps.input.onboardingDraft;
|
|
29
|
-
|
|
29
|
+
const savesPrivateDraft = deps.input.registrationIntent === 'save-private-draft';
|
|
30
|
+
if (onboardingDraft && !hasSignedPdfEvidence(onboardingDraft) && !savesPrivateDraft) {
|
|
30
31
|
const subjectAlternateName = String(onboardingDraft.formFields.subjectAlternateName
|
|
31
32
|
|| onboardingDraft.claims?.[ClaimsOrganizationSchemaorg.alternateName]
|
|
32
33
|
|| '').trim();
|
|
@@ -35,9 +36,16 @@ export async function registerIndividualOrganizationWithDeps(deps) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
const alternateName = String(deps.input.alternateName || '').trim();
|
|
38
|
-
if (!onboardingDraft && !alternateName) {
|
|
39
|
+
if (!onboardingDraft && !alternateName && !savesPrivateDraft) {
|
|
39
40
|
throw new Error('registerIndividualOrganization requires alternateName.');
|
|
40
41
|
}
|
|
42
|
+
const draftClaims = {
|
|
43
|
+
...(onboardingDraft?.claims || {}),
|
|
44
|
+
...(deps.input.additionalClaims || {}),
|
|
45
|
+
};
|
|
46
|
+
if (!alternateName && savesPrivateDraft && !hasValidHumanBirthDate(draftClaims)) {
|
|
47
|
+
throw new Error('registerIndividualOrganization private draft requires a valid birth year or date.');
|
|
48
|
+
}
|
|
41
49
|
const controllerEmail = String(deps.input.controllerEmail
|
|
42
50
|
|| onboardingDraft?.formFields.controllerEmail
|
|
43
51
|
|| onboardingDraft?.claims?.[ClaimsOrganizationSchemaorg.ownerEmail]
|
|
@@ -99,6 +107,23 @@ export async function registerIndividualOrganizationWithDeps(deps) {
|
|
|
99
107
|
});
|
|
100
108
|
const registration = await deps.submitAndPoll(deps.individualFamilyOrganizationBatchPath(deps.routeCtx), deps.individualFamilyOrganizationPollPath(deps.routeCtx), registrationPayload, pollOptions);
|
|
101
109
|
deps.assertFirstDidcommEntrySuccess?.(registration, 'registerIndividualOrganization.registration');
|
|
110
|
+
const registrationSummary = readFamilyOrganizationSummaryFromResponseBody(registration.poll.body);
|
|
111
|
+
const registrationStatus = registrationSummary?.status;
|
|
112
|
+
if (savesPrivateDraft) {
|
|
113
|
+
if (registrationStatus !== 'draft_saved') {
|
|
114
|
+
throw new Error('registerIndividualOrganization failed: GW did not preserve the requested private draft.');
|
|
115
|
+
}
|
|
116
|
+
const draftId = String(registrationSummary?.organizationId || '').trim();
|
|
117
|
+
if (!draftId) {
|
|
118
|
+
throw new Error('registerIndividualOrganization failed: GW did not return the private draft id.');
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
registration,
|
|
122
|
+
registrationStatus,
|
|
123
|
+
draftId,
|
|
124
|
+
orderConfirmationRequired: false,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
102
127
|
/**
|
|
103
128
|
* Commercial contract for this SDK path:
|
|
104
129
|
* - this helper targets the family/individual commercial bootstrap flow
|
|
@@ -112,12 +137,11 @@ export async function registerIndividualOrganizationWithDeps(deps) {
|
|
|
112
137
|
if (!offerId) {
|
|
113
138
|
throw new Error('registerIndividualOrganization failed: missing offerId in registration response.');
|
|
114
139
|
}
|
|
115
|
-
const registrationSummary = readFamilyOrganizationSummaryFromResponseBody(registration.poll.body);
|
|
116
140
|
return {
|
|
117
141
|
registration,
|
|
118
142
|
offerId,
|
|
119
143
|
offerPreview: deps.getOfferPreviewFromResponse(registration),
|
|
120
|
-
registrationStatus
|
|
144
|
+
registrationStatus,
|
|
121
145
|
orderConfirmationRequired: registrationSummary?.status !== 'already_exists',
|
|
122
146
|
identity: readIndividualOrganizationBootstrapIdentity(registration.poll.body, {
|
|
123
147
|
controllerEmail,
|
|
@@ -125,6 +149,10 @@ export async function registerIndividualOrganizationWithDeps(deps) {
|
|
|
125
149
|
}),
|
|
126
150
|
};
|
|
127
151
|
}
|
|
152
|
+
function hasValidHumanBirthDate(claims) {
|
|
153
|
+
const birthDate = String(claims?.[ClaimsPersonSchemaorg.birthDate] || '').trim();
|
|
154
|
+
return /^(?:19|20)\d{2}(?:-(?:0[1-9]|1[0-2])(?:-(?:0[1-9]|[12]\d|3[01]))?)?$/.test(birthDate);
|
|
155
|
+
}
|
|
128
156
|
function canonicalControllerUuid(value) {
|
|
129
157
|
const hexadecimal = normalizeUuid(String(value || '').trim());
|
|
130
158
|
if (!hexadecimal) {
|
|
@@ -389,7 +389,10 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
|
|
|
389
389
|
/** Lists all individual Organizations owned by one verified account contact. */
|
|
390
390
|
listOwnedFamilyOrganizations(ctx: RouteContext, input: OwnedFamilyOrganizationDirectoryInput): Promise<Readonly<{
|
|
391
391
|
resourceId: string;
|
|
392
|
-
alternateName
|
|
392
|
+
alternateName?: string;
|
|
393
|
+
status?: import("gdc-common-utils-ts").FamilyRegistrationStatus;
|
|
394
|
+
birthDate?: string;
|
|
395
|
+
missingFields?: string[];
|
|
393
396
|
claims: Readonly<Record<string, unknown>>;
|
|
394
397
|
}>[]>;
|
|
395
398
|
/**
|
|
@@ -7,10 +7,12 @@ import { type ClinicalCreatorBindingInput } from 'gdc-common-utils-ts/utils/fhir
|
|
|
7
7
|
import { type AppInfo, type ConfidentialStorageProfile, type PollOptions, type SubmitAndPollResult, type ClinicalCreatorBinding, type ClinicalCreatorIpsExport, type ClinicalSourceAuthorSelection } from 'gdc-sdk-core-ts';
|
|
8
8
|
import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
|
|
9
9
|
import { IndividualControllerSdk } from './orchestration/individual-controller-sdk.js';
|
|
10
|
+
import { IndividualMemberSdk } from './orchestration/individual-member-sdk.js';
|
|
10
11
|
import { ProfessionalSdk } from './orchestration/professional-sdk.js';
|
|
11
12
|
import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
|
|
12
13
|
import type { SmartTokenExchangeResult } from './smart-token.js';
|
|
13
14
|
import type { DigitalTwinSearchInput, DigitalTwinSearchResult } from './digital-twin.js';
|
|
15
|
+
import type { AuthorizedIndividualSubject, AuthorizedSubjectVerifiedContact } from './authorized-subject-directory.js';
|
|
14
16
|
import type { IndividualOrganizationOrderResult, RouteContext } from './individual-onboarding.js';
|
|
15
17
|
import type { IndividualOrganizationRegistrationResult } from './individual-start.js';
|
|
16
18
|
import type { HostRouteContext } from './host-onboarding.js';
|
|
@@ -76,11 +78,21 @@ export type ServerProfileSessionRecord = Readonly<{
|
|
|
76
78
|
sessionId: string;
|
|
77
79
|
ownerId: string;
|
|
78
80
|
profileId: string;
|
|
79
|
-
|
|
81
|
+
/** Present only after the unlocked personal actor selects one authorized subject. */
|
|
82
|
+
subjectDid?: string;
|
|
80
83
|
scopes: string[];
|
|
84
|
+
/** Exact directory results accepted for this unlocked actor session. */
|
|
85
|
+
authorizedSubjectDids?: string[];
|
|
86
|
+
/** Exact relationship projection returned by the encrypted directory. */
|
|
87
|
+
authorizedSubjects?: ServerAuthorizedSubjectGrant[];
|
|
88
|
+
/** Relationship used for the currently selected subject. */
|
|
89
|
+
actorMode?: ServerActorMode;
|
|
90
|
+
/** Subject-specific RelatedPerson assignment; never copied between cards. */
|
|
91
|
+
attester?: ServerProfileAttester;
|
|
81
92
|
sealedUnlockedWalletSeed: string;
|
|
82
93
|
sealedUnlockedVpToken?: string;
|
|
83
|
-
|
|
94
|
+
/** Present only after subject selection completes SMART authorization. */
|
|
95
|
+
sealedAccessToken?: string;
|
|
84
96
|
expiresAt: string;
|
|
85
97
|
}>;
|
|
86
98
|
/** Persistence port; implementations must isolate environment and tenant data. */
|
|
@@ -246,6 +258,50 @@ export type ServerProfileUnlockInput = Readonly<{
|
|
|
246
258
|
pin: string;
|
|
247
259
|
idToken: string;
|
|
248
260
|
}>;
|
|
261
|
+
/** Unlocks one personal actor wallet before choosing any individual card. */
|
|
262
|
+
export type ServerActorProfileUnlockInput = Readonly<{
|
|
263
|
+
ownerId: string;
|
|
264
|
+
profileId: string;
|
|
265
|
+
pin: string;
|
|
266
|
+
}>;
|
|
267
|
+
/** Refreshes the actor's exact GW directory through the unlocked DCR wallet. */
|
|
268
|
+
export type ServerAuthorizedSubjectDirectoryInput = Readonly<{
|
|
269
|
+
ownerId: string;
|
|
270
|
+
sessionId: string;
|
|
271
|
+
idToken: string;
|
|
272
|
+
verifiedContact: AuthorizedSubjectVerifiedContact;
|
|
273
|
+
}>;
|
|
274
|
+
/** Selects one server-confirmed subject and exchanges only its SMART scopes. */
|
|
275
|
+
export type ServerAuthorizedSubjectSelectionInput = Readonly<{
|
|
276
|
+
ownerId: string;
|
|
277
|
+
sessionId: string;
|
|
278
|
+
subjectDid: string;
|
|
279
|
+
scopes: string[];
|
|
280
|
+
idToken: string;
|
|
281
|
+
}>;
|
|
282
|
+
/** Server-owned relationship projection for one directory subject. */
|
|
283
|
+
export type ServerAuthorizedSubjectGrant = Readonly<{
|
|
284
|
+
subjectDid: string;
|
|
285
|
+
actorMode: ServerActorMode;
|
|
286
|
+
attester?: ServerProfileAttester;
|
|
287
|
+
}>;
|
|
288
|
+
/** Wallet material available after PIN unlock but before subject selection. */
|
|
289
|
+
export type ResolvedServerActorProfileSession = Readonly<{
|
|
290
|
+
sessionId: string;
|
|
291
|
+
profile: ServerProfileRecord;
|
|
292
|
+
authorizedSubjectDids: string[];
|
|
293
|
+
secureTransportAdapter: SecureDidcommTransportAdapter;
|
|
294
|
+
confidentialStorageAdapter: Readonly<{
|
|
295
|
+
protect(document: Readonly<{
|
|
296
|
+
id?: string;
|
|
297
|
+
content: unknown;
|
|
298
|
+
}>): Promise<unknown>;
|
|
299
|
+
unprotect(document: Readonly<{
|
|
300
|
+
id?: string;
|
|
301
|
+
jwe: string;
|
|
302
|
+
}>): Promise<unknown>;
|
|
303
|
+
}>;
|
|
304
|
+
}>;
|
|
249
305
|
/** Material available only during an authenticated, unexpired server session. */
|
|
250
306
|
export type ResolvedServerProfileSession = Readonly<{
|
|
251
307
|
sessionId: string;
|
|
@@ -253,6 +309,8 @@ export type ResolvedServerProfileSession = Readonly<{
|
|
|
253
309
|
subjectDid: string;
|
|
254
310
|
scopes: string[];
|
|
255
311
|
accessToken: string;
|
|
312
|
+
/** Relationship authorization selected for this subject, not for the wallet. */
|
|
313
|
+
actorMode: ServerActorMode;
|
|
256
314
|
/** Attester bound to this authenticated and unlocked profile. */
|
|
257
315
|
attester?: ServerProfileAttester;
|
|
258
316
|
secureTransportAdapter: SecureDidcommTransportAdapter;
|
|
@@ -303,6 +361,13 @@ export type OpenedServerIndividualController = Readonly<{
|
|
|
303
361
|
/** Returns the protected RelatedPerson URI for FHIR document attestation. */
|
|
304
362
|
getAttesterUriForDocs(): string;
|
|
305
363
|
}>;
|
|
364
|
+
/** Session-bound individual-member facade using the same personal actor wallet. */
|
|
365
|
+
export type OpenedServerIndividualMember = Readonly<{
|
|
366
|
+
session: ResolvedServerProfileSession;
|
|
367
|
+
profile: ServerProfileRecord;
|
|
368
|
+
sdk: IndividualMemberSdk;
|
|
369
|
+
getAttesterUriForDocs(): string;
|
|
370
|
+
}>;
|
|
306
371
|
/** One-time self-controller enrollment. It never opens a working session. */
|
|
307
372
|
export type ServerSelfIndividualControllerEnrollmentInput = Readonly<{
|
|
308
373
|
ownerId: string;
|
|
@@ -483,6 +548,29 @@ export declare class ServerProfileSessionManager {
|
|
|
483
548
|
*/
|
|
484
549
|
replaceProfilePinFromAuthorizedSecrets(input: ServerProfilePinReplacementInput): Promise<ServerProfileRecord>;
|
|
485
550
|
unlock(input: ServerProfileUnlockInput): Promise<ResolvedServerProfileSession>;
|
|
551
|
+
/**
|
|
552
|
+
* Unlocks one personal actor wallet without selecting a card or requesting a
|
|
553
|
+
* subject SMART token. The returned opaque session can refresh the encrypted
|
|
554
|
+
* authorized-subject directory and then select any exact returned subject.
|
|
555
|
+
*/
|
|
556
|
+
unlockActorProfile(input: ServerActorProfileUnlockInput): Promise<ResolvedServerActorProfileSession>;
|
|
557
|
+
/**
|
|
558
|
+
* Uses the already-unlocked registered wallet for the encrypted directory
|
|
559
|
+
* request, then replaces the session/profile subject list with exact GW
|
|
560
|
+
* results. The verified contact must come from the signed OpenID token.
|
|
561
|
+
*/
|
|
562
|
+
refreshAuthorizedSubjects(input: ServerAuthorizedSubjectDirectoryInput): Promise<Readonly<{
|
|
563
|
+
session: ResolvedServerActorProfileSession;
|
|
564
|
+
subjects: AuthorizedIndividualSubject[];
|
|
565
|
+
}>>;
|
|
566
|
+
/**
|
|
567
|
+
* Exchanges SMART authorization for one subject returned by the encrypted
|
|
568
|
+
* directory. It reuses the same unlocked seed, profile, DCR client and
|
|
569
|
+
* opaque session; this operation deliberately accepts no PIN.
|
|
570
|
+
*/
|
|
571
|
+
selectAuthorizedSubject(input: ServerAuthorizedSubjectSelectionInput): Promise<ResolvedServerProfileSession>;
|
|
572
|
+
/** Resolves an unlocked actor wallet whether or not a card is selected. */
|
|
573
|
+
resolveActorSession(ownerId: string, sessionId: string): Promise<ResolvedServerActorProfileSession>;
|
|
486
574
|
/**
|
|
487
575
|
* Renews the short SMART bearer while retaining the already-open server
|
|
488
576
|
* wallet session. The caller must supply a fresh authenticated account
|
|
@@ -497,6 +585,8 @@ export declare class ServerProfileSessionManager {
|
|
|
497
585
|
* browser.
|
|
498
586
|
*/
|
|
499
587
|
openIndividualController(input: ServerIndividualControllerOpenInput): Promise<OpenedServerIndividualController>;
|
|
588
|
+
/** Opens an accepted caregiver/member relationship after exact subject selection. */
|
|
589
|
+
openIndividualMember(input: ServerIndividualControllerOpenInput): Promise<OpenedServerIndividualMember>;
|
|
500
590
|
/**
|
|
501
591
|
* Opens the high-level organization-controller API without exposing wallet,
|
|
502
592
|
* DIDComm, DCR or HTTP-client plumbing to the integrating BFF.
|
|
@@ -520,6 +610,7 @@ export declare class ServerProfileSessionManager {
|
|
|
520
610
|
private createClient;
|
|
521
611
|
private createRegisteredProfileClient;
|
|
522
612
|
private createWallet;
|
|
613
|
+
private resolveWalletState;
|
|
523
614
|
/**
|
|
524
615
|
* Upgrade legacy profiles deterministically after successful PIN unlock.
|
|
525
616
|
* The protected seed already owns the storage pair, so migration neither
|
|
@@ -14,6 +14,7 @@ import { NodeManagedWallet } from './node-managed-wallet.js';
|
|
|
14
14
|
import { NodeHttpClient } from './node-runtime-client.js';
|
|
15
15
|
import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
|
|
16
16
|
import { IndividualControllerSdk } from './orchestration/individual-controller-sdk.js';
|
|
17
|
+
import { IndividualMemberSdk } from './orchestration/individual-member-sdk.js';
|
|
17
18
|
import { ProfessionalSdk } from './orchestration/professional-sdk.js';
|
|
18
19
|
import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
|
|
19
20
|
import { buildIdentityOpenIdSmartTokenPath } from './runtime-paths.js';
|
|
@@ -545,6 +546,183 @@ export class ServerProfileSessionManager {
|
|
|
545
546
|
});
|
|
546
547
|
return this.resolveSession(input.ownerId, sessionId);
|
|
547
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Unlocks one personal actor wallet without selecting a card or requesting a
|
|
551
|
+
* subject SMART token. The returned opaque session can refresh the encrypted
|
|
552
|
+
* authorized-subject directory and then select any exact returned subject.
|
|
553
|
+
*/
|
|
554
|
+
async unlockActorProfile(input) {
|
|
555
|
+
let profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
|
|
556
|
+
if (profile.actorKind !== ActorKinds.IndividualController) {
|
|
557
|
+
throw new Error('Actor-first unlock is restricted to personal individual profiles.');
|
|
558
|
+
}
|
|
559
|
+
const now = this.now();
|
|
560
|
+
if (profile.lockedUntil && new Date(profile.lockedUntil) > now) {
|
|
561
|
+
throw new Error('Profile is temporarily locked after failed PIN attempts.');
|
|
562
|
+
}
|
|
563
|
+
let seed;
|
|
564
|
+
let vpToken;
|
|
565
|
+
try {
|
|
566
|
+
seed = await openServerProfileSecret(profile.protectedWalletSeed, input.pin, `${profile.profileId}:wallet-seed`, this.options.sealer);
|
|
567
|
+
vpToken = profile.protectedVpToken
|
|
568
|
+
? await openServerProfileSecret(profile.protectedVpToken, input.pin, `${profile.profileId}:vp-token`, this.options.sealer)
|
|
569
|
+
: undefined;
|
|
570
|
+
}
|
|
571
|
+
catch (reason) {
|
|
572
|
+
if (!(reason instanceof ProfilePinRejectedError))
|
|
573
|
+
throw reason;
|
|
574
|
+
const failures = profile.failedUnlocks + 1;
|
|
575
|
+
const max = this.options.maxFailedUnlocks ?? 5;
|
|
576
|
+
const lockedUntil = failures >= max
|
|
577
|
+
? new Date(now.getTime() + (this.options.lockSeconds ?? 300) * 1000).toISOString()
|
|
578
|
+
: undefined;
|
|
579
|
+
await this.options.store.putProfile({
|
|
580
|
+
...profile,
|
|
581
|
+
failedUnlocks: failures,
|
|
582
|
+
lockedUntil,
|
|
583
|
+
updatedAt: now.toISOString(),
|
|
584
|
+
});
|
|
585
|
+
throw new Error('Profile PIN rejected.');
|
|
586
|
+
}
|
|
587
|
+
profile = await this.ensureRequiredStorageProfile(profile, seed);
|
|
588
|
+
const sessionId = randomBytes(32).toString('base64url');
|
|
589
|
+
await this.options.store.putProfile({
|
|
590
|
+
...profile,
|
|
591
|
+
failedUnlocks: 0,
|
|
592
|
+
lockedUntil: undefined,
|
|
593
|
+
updatedAt: now.toISOString(),
|
|
594
|
+
});
|
|
595
|
+
await this.options.store.putSession({
|
|
596
|
+
sessionId,
|
|
597
|
+
ownerId: input.ownerId,
|
|
598
|
+
profileId: profile.profileId,
|
|
599
|
+
scopes: [],
|
|
600
|
+
authorizedSubjectDids: [],
|
|
601
|
+
sealedUnlockedWalletSeed: await this.options.sealer.seal(seed, `${sessionId}:unlocked-wallet-seed`),
|
|
602
|
+
...(vpToken ? {
|
|
603
|
+
sealedUnlockedVpToken: await this.options.sealer.seal(vpToken, `${sessionId}:unlocked-vp-token`),
|
|
604
|
+
} : {}),
|
|
605
|
+
expiresAt: new Date(now.getTime() + (this.options.sessionTtlSeconds ?? 300) * 1000).toISOString(),
|
|
606
|
+
});
|
|
607
|
+
return this.resolveActorSession(input.ownerId, sessionId);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Uses the already-unlocked registered wallet for the encrypted directory
|
|
611
|
+
* request, then replaces the session/profile subject list with exact GW
|
|
612
|
+
* results. The verified contact must come from the signed OpenID token.
|
|
613
|
+
*/
|
|
614
|
+
async refreshAuthorizedSubjects(input) {
|
|
615
|
+
const actorSession = await this.resolveActorSession(input.ownerId, input.sessionId);
|
|
616
|
+
const idToken = String(input.idToken || '').trim();
|
|
617
|
+
if (!idToken)
|
|
618
|
+
throw new Error('Authorized-subject directory requires idToken.');
|
|
619
|
+
const walletState = await this.resolveWalletState(input.ownerId, input.sessionId);
|
|
620
|
+
const client = this.createRegisteredProfileClient(walletState.profile, walletState.wallet, walletState.context, idToken);
|
|
621
|
+
const subjects = await client.listAuthorizedIndividualSubjects(walletState.profile.routeContext, { verifiedContact: input.verifiedContact });
|
|
622
|
+
const authorizedSubjectDids = unique(subjects.map((subject) => subject.subjectDid));
|
|
623
|
+
const authorizedSubjects = subjects.map(toServerAuthorizedSubjectGrant);
|
|
624
|
+
const updatedProfile = {
|
|
625
|
+
...actorSession.profile,
|
|
626
|
+
allowedSubjectDids: authorizedSubjectDids,
|
|
627
|
+
updatedAt: this.now().toISOString(),
|
|
628
|
+
};
|
|
629
|
+
await this.options.store.putProfile(updatedProfile);
|
|
630
|
+
const stored = await this.options.store.getSession(input.sessionId);
|
|
631
|
+
if (!stored || stored.ownerId !== input.ownerId)
|
|
632
|
+
throw new Error('Profile session not found.');
|
|
633
|
+
const { subjectDid: _previousSubjectDid, actorMode: _previousActorMode, attester: _previousAttester, sealedAccessToken: _previousAccessToken, ...actorOnlySession } = stored;
|
|
634
|
+
await this.options.store.putSession({
|
|
635
|
+
...actorOnlySession,
|
|
636
|
+
authorizedSubjectDids,
|
|
637
|
+
authorizedSubjects,
|
|
638
|
+
scopes: [],
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
session: await this.resolveActorSession(input.ownerId, input.sessionId),
|
|
642
|
+
subjects,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Exchanges SMART authorization for one subject returned by the encrypted
|
|
647
|
+
* directory. It reuses the same unlocked seed, profile, DCR client and
|
|
648
|
+
* opaque session; this operation deliberately accepts no PIN.
|
|
649
|
+
*/
|
|
650
|
+
async selectAuthorizedSubject(input) {
|
|
651
|
+
const stored = await this.options.store.getSession(input.sessionId);
|
|
652
|
+
if (!stored || stored.ownerId !== input.ownerId)
|
|
653
|
+
throw new Error('Profile session not found.');
|
|
654
|
+
const subjectDid = String(input.subjectDid || '').trim();
|
|
655
|
+
if (!subjectDid || !(stored.authorizedSubjectDids || []).includes(subjectDid)) {
|
|
656
|
+
throw new Error('Subject is not authorized for this personal actor profile.');
|
|
657
|
+
}
|
|
658
|
+
const selectedGrant = stored.authorizedSubjects?.find((grant) => grant.subjectDid === subjectDid);
|
|
659
|
+
if (!selectedGrant) {
|
|
660
|
+
throw new Error('Subject relationship metadata is missing from this personal actor session.');
|
|
661
|
+
}
|
|
662
|
+
const idToken = String(input.idToken || '').trim();
|
|
663
|
+
if (!idToken)
|
|
664
|
+
throw new Error('Subject selection requires idToken.');
|
|
665
|
+
const scopes = unique(input.scopes);
|
|
666
|
+
if (!scopes.length)
|
|
667
|
+
throw new Error('Subject selection requires scopes.');
|
|
668
|
+
const walletState = await this.resolveWalletState(input.ownerId, input.sessionId);
|
|
669
|
+
const vpToken = stored.sealedUnlockedVpToken
|
|
670
|
+
? await this.options.sealer.unseal(stored.sealedUnlockedVpToken, `${input.sessionId}:unlocked-vp-token`)
|
|
671
|
+
: undefined;
|
|
672
|
+
const smartTokenEndpoint = [
|
|
673
|
+
this.options.gatewayBaseUrl.replace(/\/+$/, ''),
|
|
674
|
+
buildIdentityOpenIdSmartTokenPath(walletState.profile.routeContext),
|
|
675
|
+
].join('');
|
|
676
|
+
const assertion = await buildWalletClientAssertion(walletState.wallet, walletState.profile, smartTokenEndpoint, this.now());
|
|
677
|
+
const token = await this.createRegisteredProfileClient(walletState.profile, walletState.wallet, walletState.context, idToken).requestSmartToken({
|
|
678
|
+
...walletState.profile.routeContext,
|
|
679
|
+
actorDid: walletState.profile.actorDid,
|
|
680
|
+
subjectDid,
|
|
681
|
+
clientId: walletState.profile.clientId,
|
|
682
|
+
issuer: walletState.profile.clientId,
|
|
683
|
+
audience: smartTokenEndpoint,
|
|
684
|
+
idToken,
|
|
685
|
+
vpToken,
|
|
686
|
+
vpTokenFallback: vpToken ? undefined : 'omit',
|
|
687
|
+
clientAssertion: assertion,
|
|
688
|
+
clientAssertionType: 'private_key_jwt',
|
|
689
|
+
smartTokenKind: 'openid-smart',
|
|
690
|
+
acrValues: profileSmartAcrValues(walletState.profile.actorKind),
|
|
691
|
+
scopes,
|
|
692
|
+
tokenCacheKey: `profile:${walletState.profile.profileId}:${subjectDid}:${scopes.join(',')}`,
|
|
693
|
+
});
|
|
694
|
+
if (token.status !== 'fetched' || !token.accessToken) {
|
|
695
|
+
throw new Error('SMART token exchange failed.');
|
|
696
|
+
}
|
|
697
|
+
const { attester: _previousSelectedAttester, ...sessionWithoutAttester } = stored;
|
|
698
|
+
await this.options.store.putSession({
|
|
699
|
+
...sessionWithoutAttester,
|
|
700
|
+
subjectDid,
|
|
701
|
+
actorMode: selectedGrant.actorMode,
|
|
702
|
+
...(selectedGrant.attester ? { attester: selectedGrant.attester } : {}),
|
|
703
|
+
scopes,
|
|
704
|
+
sealedAccessToken: await this.options.sealer.seal(token.accessToken, `${input.sessionId}:access-token`),
|
|
705
|
+
});
|
|
706
|
+
return this.resolveSession(input.ownerId, input.sessionId);
|
|
707
|
+
}
|
|
708
|
+
/** Resolves an unlocked actor wallet whether or not a card is selected. */
|
|
709
|
+
async resolveActorSession(ownerId, sessionId) {
|
|
710
|
+
const stored = await this.options.store.getSession(sessionId);
|
|
711
|
+
if (!stored || stored.ownerId !== ownerId)
|
|
712
|
+
throw new Error('Profile session not found.');
|
|
713
|
+
if (new Date(stored.expiresAt) <= this.now()) {
|
|
714
|
+
await this.options.store.deleteSession(sessionId);
|
|
715
|
+
throw new Error('Profile session expired.');
|
|
716
|
+
}
|
|
717
|
+
const state = await this.resolveWalletState(ownerId, sessionId);
|
|
718
|
+
return {
|
|
719
|
+
sessionId,
|
|
720
|
+
profile: state.profile,
|
|
721
|
+
authorizedSubjectDids: [...(stored.authorizedSubjectDids || [])],
|
|
722
|
+
secureTransportAdapter: state.secureTransportAdapter,
|
|
723
|
+
confidentialStorageAdapter: state.confidentialStorageAdapter,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
548
726
|
/**
|
|
549
727
|
* Renews the short SMART bearer while retaining the already-open server
|
|
550
728
|
* wallet session. The caller must supply a fresh authenticated account
|
|
@@ -562,6 +740,9 @@ export class ServerProfileSessionManager {
|
|
|
562
740
|
if (!normalizedIdToken)
|
|
563
741
|
throw new Error('Profile session refresh requires idToken.');
|
|
564
742
|
const profile = await this.requireOwnedProfile(ownerId, session.profileId);
|
|
743
|
+
if (!session.subjectDid || !session.sealedAccessToken) {
|
|
744
|
+
throw new Error('Profile session has no selected subject.');
|
|
745
|
+
}
|
|
565
746
|
this.requireSubject(profile, session.subjectDid);
|
|
566
747
|
const seed = await this.options.sealer.unseal(session.sealedUnlockedWalletSeed, `${sessionId}:unlocked-wallet-seed`);
|
|
567
748
|
const vpToken = session.sealedUnlockedVpToken
|
|
@@ -609,26 +790,22 @@ export class ServerProfileSessionManager {
|
|
|
609
790
|
await this.options.store.deleteSession(sessionId);
|
|
610
791
|
throw new Error('Profile session expired.');
|
|
611
792
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
const context = walletContext(walletKeyDerivationId);
|
|
793
|
+
if (!session.subjectDid || !session.sealedAccessToken) {
|
|
794
|
+
throw new Error('Profile session has no selected subject.');
|
|
795
|
+
}
|
|
796
|
+
const state = await this.resolveWalletState(ownerId, sessionId);
|
|
617
797
|
return {
|
|
618
798
|
sessionId,
|
|
619
|
-
profile,
|
|
799
|
+
profile: state.profile,
|
|
620
800
|
subjectDid: session.subjectDid,
|
|
621
801
|
scopes: session.scopes,
|
|
622
802
|
accessToken: await this.options.sealer.unseal(session.sealedAccessToken, `${sessionId}:access-token`),
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
confidentialStorageAdapter:
|
|
629
|
-
protect: (document) => wallet.protectManagedConfidentialData(document, context),
|
|
630
|
-
unprotect: (document) => wallet.unprotectManagedConfidentialData(document, context),
|
|
631
|
-
},
|
|
803
|
+
actorMode: session.actorMode ?? state.profile.actorMode,
|
|
804
|
+
...(session.attester
|
|
805
|
+
? { attester: session.attester }
|
|
806
|
+
: (!session.authorizedSubjects && state.profile.attester ? { attester: state.profile.attester } : {})),
|
|
807
|
+
secureTransportAdapter: state.secureTransportAdapter,
|
|
808
|
+
confidentialStorageAdapter: state.confidentialStorageAdapter,
|
|
632
809
|
};
|
|
633
810
|
}
|
|
634
811
|
/**
|
|
@@ -641,10 +818,10 @@ export class ServerProfileSessionManager {
|
|
|
641
818
|
const session = await this.resolveSession(input.ownerId, input.sessionId);
|
|
642
819
|
const profile = session.profile;
|
|
643
820
|
if (profile.actorKind !== ActorKinds.IndividualController
|
|
644
|
-
|| (
|
|
821
|
+
|| (session.actorMode !== 'self' && session.actorMode !== 'controller')) {
|
|
645
822
|
throw new Error('Profile is not an individual controller.');
|
|
646
823
|
}
|
|
647
|
-
const profileAttester =
|
|
824
|
+
const profileAttester = session.attester;
|
|
648
825
|
if (!profileAttester) {
|
|
649
826
|
throw new Error('The opened individual-controller profile has no RelatedPerson attester.');
|
|
650
827
|
}
|
|
@@ -672,6 +849,32 @@ export class ServerProfileSessionManager {
|
|
|
672
849
|
},
|
|
673
850
|
};
|
|
674
851
|
}
|
|
852
|
+
/** Opens an accepted caregiver/member relationship after exact subject selection. */
|
|
853
|
+
async openIndividualMember(input) {
|
|
854
|
+
const session = await this.resolveSession(input.ownerId, input.sessionId);
|
|
855
|
+
if (session.profile.actorKind !== ActorKinds.IndividualController || session.actorMode !== 'member') {
|
|
856
|
+
throw new Error('Selected subject relationship is not an individual member.');
|
|
857
|
+
}
|
|
858
|
+
const attester = session.attester;
|
|
859
|
+
if (!attester) {
|
|
860
|
+
throw new Error('The selected individual-member relationship has no RelatedPerson attester.');
|
|
861
|
+
}
|
|
862
|
+
const client = new NodeHttpClient({
|
|
863
|
+
baseUrl: this.options.gatewayBaseUrl,
|
|
864
|
+
ctx: session.profile.routeContext,
|
|
865
|
+
bearerToken: session.accessToken,
|
|
866
|
+
fetchImpl: this.options.fetchImpl,
|
|
867
|
+
appInfo: this.options.appInfo,
|
|
868
|
+
transportProfile: TransportProfiles.DidcommEncryptedForm,
|
|
869
|
+
secureTransportAdapter: session.secureTransportAdapter,
|
|
870
|
+
});
|
|
871
|
+
return {
|
|
872
|
+
session,
|
|
873
|
+
profile: session.profile,
|
|
874
|
+
sdk: new IndividualMemberSdk(client),
|
|
875
|
+
getAttesterUriForDocs: () => attester.party.reference,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
675
878
|
/**
|
|
676
879
|
* Opens the high-level organization-controller API without exposing wallet,
|
|
677
880
|
* DIDComm, DCR or HTTP-client plumbing to the integrating BFF.
|
|
@@ -877,6 +1080,34 @@ export class ServerProfileSessionManager {
|
|
|
877
1080
|
});
|
|
878
1081
|
return wallet;
|
|
879
1082
|
}
|
|
1083
|
+
async resolveWalletState(ownerId, sessionId) {
|
|
1084
|
+
const session = await this.options.store.getSession(sessionId);
|
|
1085
|
+
if (!session || session.ownerId !== ownerId)
|
|
1086
|
+
throw new Error('Profile session not found.');
|
|
1087
|
+
if (new Date(session.expiresAt) <= this.now()) {
|
|
1088
|
+
await this.options.store.deleteSession(sessionId);
|
|
1089
|
+
throw new Error('Profile session expired.');
|
|
1090
|
+
}
|
|
1091
|
+
const profile = await this.requireOwnedProfile(ownerId, session.profileId);
|
|
1092
|
+
const seed = await this.options.sealer.unseal(session.sealedUnlockedWalletSeed, `${sessionId}:unlocked-wallet-seed`);
|
|
1093
|
+
const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
|
|
1094
|
+
const wallet = await this.createWallet(walletKeyDerivationId, seed);
|
|
1095
|
+
const context = walletContext(walletKeyDerivationId);
|
|
1096
|
+
await requireRegisteredProfileKeys(wallet, context, profile);
|
|
1097
|
+
return {
|
|
1098
|
+
profile,
|
|
1099
|
+
wallet,
|
|
1100
|
+
context,
|
|
1101
|
+
secureTransportAdapter: {
|
|
1102
|
+
pack: (message) => wallet.packForRecipientWithContext(bindTransportActor(message, profile.actorDid, profile.clientId), profile.providerDid, { context }),
|
|
1103
|
+
unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
|
|
1104
|
+
},
|
|
1105
|
+
confidentialStorageAdapter: {
|
|
1106
|
+
protect: (document) => wallet.protectManagedConfidentialData(document, context),
|
|
1107
|
+
unprotect: (document) => wallet.unprotectManagedConfidentialData(document, context),
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
880
1111
|
/**
|
|
881
1112
|
* Upgrade legacy profiles deterministically after successful PIN unlock.
|
|
882
1113
|
* The protected seed already owns the storage pair, so migration neither
|
|
@@ -946,9 +1177,25 @@ async function requireRegisteredProfileKeys(wallet, context, profile) {
|
|
|
946
1177
|
if (!registeredKids.length || registeredKids.some((kid) => !derivedKids.has(kid))) {
|
|
947
1178
|
throw new Error('Authorized wallet seed does not match registered profile keys.');
|
|
948
1179
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1180
|
+
}
|
|
1181
|
+
function toServerAuthorizedSubjectGrant(subject) {
|
|
1182
|
+
const role = String(subject.role || '').trim().toUpperCase().split(/[|/#:]/).at(-1) || '';
|
|
1183
|
+
const actorMode = role === 'ONESELF'
|
|
1184
|
+
? 'self'
|
|
1185
|
+
: role === 'RESPRSN'
|
|
1186
|
+
? 'controller'
|
|
1187
|
+
: 'member';
|
|
1188
|
+
const relatedPersonId = String(subject.relatedPersonId || '').trim();
|
|
1189
|
+
return {
|
|
1190
|
+
subjectDid: subject.subjectDid,
|
|
1191
|
+
actorMode,
|
|
1192
|
+
...(relatedPersonId ? {
|
|
1193
|
+
attester: buildProfileAttester({
|
|
1194
|
+
assignmentIdentifier: relatedPersonId,
|
|
1195
|
+
mode: CompositionAttesterModes.Personal,
|
|
1196
|
+
}),
|
|
1197
|
+
} : {}),
|
|
1198
|
+
};
|
|
952
1199
|
}
|
|
953
1200
|
/** Keeps the OpenID proof class aligned with the durable actor profile. */
|
|
954
1201
|
function profileSmartAcrValues(actorKind) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdc-sdk-node-ts",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.18",
|
|
4
4
|
"description": "Next-generation Node runtime package for the GDC SDK family",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Antifraud Services Inc.",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:e2e:live-gw:clean": "bash ./scripts/run-live-gw-clean.sh"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"gdc-common-utils-ts": "2.9.
|
|
41
|
+
"gdc-common-utils-ts": "2.9.16",
|
|
42
42
|
"gdc-sdk-core-ts": "2.9.10"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|