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.
@@ -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, { email, telephone });
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 = readFirstClaims(organizationResult);
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 = String(claims[CLAIM_ORGANIZATION_MEMBER_ROLE] || '').trim();
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') ? 'CAREGIVER' : occupation;
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 readFirstClaims(result) {
171
+ function readFirstOrganization(result) {
157
172
  const entry = readData(result)[0];
158
173
  const claims = entry?.resource?.meta?.claims || entry?.meta?.claims;
159
- return claims && typeof claims === 'object' ? claims : {};
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
- alternateName: string;
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
- if (!alternateName)
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
- return [{ resourceId: String(resource.id), alternateName, claims: resourceClaims }];
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
- offerId: string;
81
- offerPreview: OfferPreview;
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. */
@@ -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
- if (onboardingDraft && !hasSignedPdfEvidence(onboardingDraft)) {
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: registrationSummary?.status,
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: string;
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
- subjectDid: string;
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
- sealedAccessToken: string;
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
- const profile = await this.requireOwnedProfile(ownerId, session.profileId);
613
- const seed = await this.options.sealer.unseal(session.sealedUnlockedWalletSeed, `${sessionId}:unlocked-wallet-seed`);
614
- const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
615
- const wallet = await this.createWallet(walletKeyDerivationId, seed);
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
- ...(profile.attester ? { attester: profile.attester } : {}),
624
- secureTransportAdapter: {
625
- pack: (message) => wallet.packForRecipientWithContext(bindTransportActor(message, profile.actorDid, profile.clientId), profile.providerDid, { context }),
626
- unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
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
- || (profile.actorMode !== 'self' && profile.actorMode !== 'controller')) {
821
+ || (session.actorMode !== 'self' && session.actorMode !== 'controller')) {
645
822
  throw new Error('Profile is not an individual controller.');
646
823
  }
647
- const profileAttester = profile.attester;
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
- if (profile.clientInstanceId && !derivedKids.has(profile.clientInstanceId)) {
950
- throw new Error('Authorized wallet seed does not match the registered client instance.');
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.16",
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.15",
41
+ "gdc-common-utils-ts": "2.9.16",
42
42
  "gdc-sdk-core-ts": "2.9.10"
43
43
  },
44
44
  "devDependencies": {