gdc-sdk-node-ts 2.9.11 → 2.9.14

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/README.md CHANGED
@@ -533,7 +533,7 @@ const individualControllerDid = buildIndividualMemberDidWebFromPrivateIdentifier
533
533
  roleValue: 'RESPRSN',
534
534
  });
535
535
 
536
- // did:web:host.example.org:health-care:organization:taxid:VATES-B00112233:individual:UUID:zG9H82pae9SCXvec3D4YKqhX8bj8F1mRgzxMEdwXXonT7BWsvsUiP2u52sWQTeESpoMee:member:zG9DrMLpQW8eoCc9Ay9AFxuMGiswgJePpbUMz9svJCZ8tKjUd4xoExgCPA5jmHc6hPATJ:RESPRSN
536
+ // did:web:host.example.org:health-care:organization:taxid:VATES-B00112233:individual:multibase:zG9H82pae9SCXvec3D4YKqhX8bj8F1mRgzxMEdwXXonT7BWsvsUiP2u52sWQTeESpoMee:member:zG9DrMLpQW8eoCc9Ay9AFxuMGiswgJePpbUMz9svJCZ8tKjUd4xoExgCPA5jmHc6hPATJ:RESPRSN
537
537
  ```
538
538
 
539
539
  ### Use shared invitation contract from Node
@@ -8,6 +8,20 @@ export type FamilyOrganizationSearchInput = Readonly<{
8
8
  timeoutSeconds?: number;
9
9
  intervalSeconds?: number;
10
10
  }>;
11
+ export type OwnedFamilyOrganizationDirectoryInput = Readonly<{
12
+ verifiedContact: Readonly<{
13
+ email?: string;
14
+ telephone?: string;
15
+ }>;
16
+ requestThid?: string;
17
+ timeoutSeconds?: number;
18
+ intervalSeconds?: number;
19
+ }>;
20
+ export type OwnedFamilyOrganizationDirectoryEntry = Readonly<{
21
+ resourceId: string;
22
+ alternateName: string;
23
+ claims: Readonly<Record<string, unknown>>;
24
+ }>;
11
25
  type SearchFamilyOrganizationWithDeps = {
12
26
  routeCtx: RouteContext;
13
27
  input: FamilyOrganizationSearchInput;
@@ -27,4 +41,12 @@ type SearchFamilyOrganizationWithDeps = {
27
41
  * `null`.
28
42
  */
29
43
  export declare function searchFamilyOrganizationWithDeps(deps: SearchFamilyOrganizationWithDeps): Promise<FamilyOrganizationSummary | null>;
44
+ /**
45
+ * Lists the complete owner-scoped individual Organization directory.
46
+ *
47
+ * CORE requires an exact verified owner contact and deliberately performs no
48
+ * global or prefix scan. Callers may filter the returned alternate names only
49
+ * after this authenticated directory boundary.
50
+ */
51
+ export declare function listOwnedFamilyOrganizationsWithDeps(routeCtx: RouteContext, input: OwnedFamilyOrganizationDirectoryInput, deps: Pick<SearchFamilyOrganizationWithDeps, 'individualFamilyOrganizationSearchPath' | 'individualFamilyOrganizationSearchPollPath' | 'submitAndPoll' | 'defaultTimeoutMs' | 'defaultIntervalMs'>): Promise<OwnedFamilyOrganizationDirectoryEntry[]>;
30
52
  export {};
@@ -49,6 +49,62 @@ export async function searchFamilyOrganizationWithDeps(deps) {
49
49
  }
50
50
  return readFamilyOrganizationSummaryFromResponseBody(result.poll.body);
51
51
  }
52
+ /**
53
+ * Lists the complete owner-scoped individual Organization directory.
54
+ *
55
+ * CORE requires an exact verified owner contact and deliberately performs no
56
+ * global or prefix scan. Callers may filter the returned alternate names only
57
+ * after this authenticated directory boundary.
58
+ */
59
+ export async function listOwnedFamilyOrganizationsWithDeps(routeCtx, input, deps) {
60
+ const email = String(input.verifiedContact?.email || '').trim().toLowerCase();
61
+ const telephone = String(input.verifiedContact?.telephone || '').trim();
62
+ if (!email && !telephone) {
63
+ throw new Error('A verified owner email or telephone is required.');
64
+ }
65
+ const claims = {
66
+ '@context': 'org.schema',
67
+ ...(email ? { [ClaimsOrganizationSchemaorg.ownerEmail]: email } : {}),
68
+ ...(telephone ? { [ClaimsOrganizationSchemaorg.ownerTelephone]: telephone } : {}),
69
+ };
70
+ const pollOptions = resolvePollOptionsFromSeconds(input.timeoutSeconds, input.intervalSeconds, {
71
+ timeoutMs: deps.defaultTimeoutMs,
72
+ intervalMs: deps.defaultIntervalMs,
73
+ });
74
+ const result = await deps.submitAndPoll(deps.individualFamilyOrganizationSearchPath(routeCtx), deps.individualFamilyOrganizationSearchPollPath(routeCtx), {
75
+ jti: `jti-${createRuntimeUuid()}`,
76
+ thid: input.requestThid || `owned-family-directory-${createRuntimeUuid()}`,
77
+ iss: routeCtx.tenantId,
78
+ aud: routeCtx.tenantId,
79
+ type: 'application/api+json',
80
+ body: {
81
+ data: [{
82
+ type: 'Family-search-v1.0',
83
+ resource: { meta: { claims } },
84
+ }],
85
+ },
86
+ }, pollOptions);
87
+ if (result.poll.status !== 200)
88
+ return [];
89
+ const root = result.poll.body?.body || result.poll.body;
90
+ const responseEntries = Array.isArray(root?.data) ? root.data : [];
91
+ const resources = responseEntries.flatMap((entry) => Array.isArray(entry?.resource?.entry)
92
+ ? entry.resource.entry.map((item) => item?.resource).filter(Boolean)
93
+ : []);
94
+ return resources.flatMap((resource) => {
95
+ const resourceClaims = resource?.meta?.claims;
96
+ if (!resource?.id || !resourceClaims || typeof resourceClaims !== 'object')
97
+ return [];
98
+ const ownerEmail = String(resourceClaims[ClaimsOrganizationSchemaorg.ownerEmail] || '').trim().toLowerCase();
99
+ const ownerTelephone = String(resourceClaims[ClaimsOrganizationSchemaorg.ownerTelephone] || '').trim();
100
+ if (!((email && ownerEmail === email) || (telephone && ownerTelephone === telephone)))
101
+ return [];
102
+ const alternateName = String(resourceClaims[ClaimsOrganizationSchemaorg.alternateName] || '').trim();
103
+ if (!alternateName)
104
+ return [];
105
+ return [{ resourceId: String(resource.id), alternateName, claims: resourceClaims }];
106
+ });
107
+ }
52
108
  function createRuntimeUuid() {
53
109
  const fromCrypto = globalThis.crypto?.randomUUID?.();
54
110
  if (fromCrypto) {
@@ -29,8 +29,9 @@ export type IndividualOrganizationOrderResult = SubmitAndPollResult & Readonly<{
29
29
  activationCode: string;
30
30
  /**
31
31
  * Governed `RelatedPerson.identifier` automatically materialized by GW for
32
- * the principal Organization owner/controller. Current GW values use the
33
- * `urn:uuid:<UUID>` form.
32
+ * the principal Organization owner/controller. GW reuses the bare UUID from
33
+ * `Organization.owner.identifier.value`; document helpers expose it as an
34
+ * `urn:uuid:<UUID>` FHIR reference only when a reference is required.
34
35
  */
35
36
  controllerRelatedPersonIdentifier: string;
36
37
  /**
@@ -1,10 +1,19 @@
1
1
  import { SecureIdTypesIndividual } from 'gdc-common-utils-ts';
2
2
  export { buildIndividualMemberDidWebFromPrivateIdentifiers } from 'gdc-common-utils-ts';
3
3
  import type { FamilyRegistrationStatus } from 'gdc-common-utils-ts/utils/family-organization-summary';
4
+ import type { IndividualOnboardingDraftResult } from 'gdc-common-utils-ts/models/individual-onboarding';
4
5
  import type { PollOptions, SubmitAndPollResult } from './orchestration/client-port.js';
5
6
  import type { RouteContext } from './individual-onboarding.js';
6
7
  import type { OfferPreview } from './order-offer-summary.js';
7
8
  export type IndividualOrganizationRegistrationInput = {
9
+ /**
10
+ * Preferred high-level input produced by `createIndividualOnboardingEditor()`.
11
+ *
12
+ * The SDK converts this draft into the GW Bundle and signed-PDF attachment.
13
+ * Registration remains separate from Order confirmation, enrollment and
14
+ * opening a profile.
15
+ */
16
+ onboardingDraft?: IndividualOnboardingDraftResult;
8
17
  /**
9
18
  * Preferred route identifier for the selected personal indexing service provider.
10
19
  *
@@ -25,7 +34,7 @@ export type IndividualOrganizationRegistrationInput = {
25
34
  * This is not the technical subject identifier. It is the nearby name the
26
35
  * controller uses to refer to the person in the UI, for example `Charly`.
27
36
  */
28
- alternateName: string;
37
+ alternateName?: string;
29
38
  /**
30
39
  * CORE-canonical controller contact channel for individual bootstrap.
31
40
  *
@@ -53,6 +62,12 @@ export type IndividualOrganizationRegistrationInput = {
53
62
  * a product gateway extension.
54
63
  */
55
64
  controllerTelephone?: string;
65
+ /**
66
+ * Stable UUID of the principal controller/RESPRSN assignment. In a self
67
+ * registration this may be the same UUID already assigned to the person.
68
+ * Omit only for a brand-new assignment so the SDK creates it once.
69
+ */
70
+ controllerIdentifier?: string;
56
71
  controllerRole?: string;
57
72
  additionalClaims?: Record<string, unknown>;
58
73
  timeoutSeconds?: number;
@@ -91,6 +106,12 @@ export type IndividualOrganizationBootstrapIdentity = {
91
106
  providerDidWeb: string;
92
107
  /** Canonical child DID built beneath the exact provider DID returned by GW. */
93
108
  subjectDid: string;
109
+ /**
110
+ * Canonical principal-controller member DID accepted by individual DCR.
111
+ * Optional only for compatibility with older registration receipts that did
112
+ * not expose the controller contact needed to derive it.
113
+ */
114
+ controllerActorDid?: string;
94
115
  };
95
116
  type RegisterIndividualOrganizationDeps = {
96
117
  input: IndividualOrganizationRegistrationInput;
@@ -122,4 +143,7 @@ export declare function startIndividualOrganizationWithDeps(deps: RegisterIndivi
122
143
  * `:organization:taxid:` DID path. `buildIndividualDidWeb(...)` only adds the
123
144
  * individual suffix and never rewrites that provider lineage.
124
145
  */
125
- export declare function readIndividualOrganizationBootstrapIdentity(responseBody: unknown): IndividualOrganizationBootstrapIdentity | undefined;
146
+ export declare function readIndividualOrganizationBootstrapIdentity(responseBody: unknown, controller?: Readonly<{
147
+ controllerEmail?: string;
148
+ controllerTelephone?: string;
149
+ }>): IndividualOrganizationBootstrapIdentity | undefined;
@@ -1,7 +1,10 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { ClaimsOfferSchemaorg, ClaimsOrganizationSchemaorg, ClaimsPersonSchemaorg, ClaimsServiceSchemaorg, } from 'gdc-common-utils-ts/constants';
3
- import { buildIndividualDidWeb, buildSecureIdValueIndividual, extractPrimaryClaims, readFamilyOrganizationSummaryFromResponseBody, SecureIdTypesIndividual, } from 'gdc-common-utils-ts';
4
+ import { HealthcareActorRoleCodes, HL7_CODING_SYSTEM_V3_ROLE_CODE, buildIndividualMemberDidWebFromPrivateIdentifiers, buildIndividualDidWeb, buildSecureIdValueIndividual, extractPrimaryClaims, normalizeUuid, readFamilyOrganizationSummaryFromResponseBody, SecureIdTypesIndividual, } from 'gdc-common-utils-ts';
4
5
  export { buildIndividualMemberDidWebFromPrivateIdentifiers } from 'gdc-common-utils-ts';
6
+ import { DocumentReferenceClaim } from 'gdc-common-utils-ts/models/interoperable-claims/document-reference-claims';
7
+ import { buildIndividualOrganizationRegistrationGatewayRequestFromDraft } from 'gdc-sdk-core-ts';
5
8
  import { GwCoreLifecycleRequestType } from './constants/lifecycle.js';
6
9
  import { resolvePollOptionsFromSeconds } from './poll-options.js';
7
10
  /**
@@ -22,20 +25,40 @@ export async function registerIndividualOrganizationWithDeps(deps) {
22
25
  * in the payload for compatibility, but the owner claims are the live GW
23
26
  * routing/indexing contract for this flow.
24
27
  */
28
+ const onboardingDraft = deps.input.onboardingDraft;
29
+ if (onboardingDraft && !hasSignedPdfEvidence(onboardingDraft)) {
30
+ const subjectAlternateName = String(onboardingDraft.formFields.subjectAlternateName
31
+ || onboardingDraft.claims?.[ClaimsOrganizationSchemaorg.alternateName]
32
+ || '').trim();
33
+ if (!subjectAlternateName) {
34
+ throw new Error('registerIndividualOrganization subjectAlternateName is required when signed PDF evidence is absent.');
35
+ }
36
+ }
25
37
  const alternateName = String(deps.input.alternateName || '').trim();
26
- if (!alternateName) {
38
+ if (!onboardingDraft && !alternateName) {
27
39
  throw new Error('registerIndividualOrganization requires alternateName.');
28
40
  }
29
- const controllerEmail = String(deps.input.controllerEmail || '').trim();
30
- const controllerTelephone = String(deps.input.controllerTelephone || '').trim();
31
- if (!controllerEmail && !controllerTelephone) {
41
+ const controllerEmail = String(deps.input.controllerEmail
42
+ || onboardingDraft?.formFields.controllerEmail
43
+ || onboardingDraft?.claims?.[ClaimsOrganizationSchemaorg.ownerEmail]
44
+ || '').trim();
45
+ const controllerTelephone = String(deps.input.controllerTelephone
46
+ || onboardingDraft?.formFields.controllerPhone
47
+ || onboardingDraft?.claims?.[ClaimsOrganizationSchemaorg.ownerTelephone]
48
+ || '').trim();
49
+ if (!onboardingDraft && !controllerEmail && !controllerTelephone) {
32
50
  throw new Error('registerIndividualOrganization requires controllerEmail, or controllerTelephone only for compatibility/extension flows.');
33
51
  }
34
52
  const controllerRole = String(deps.input.controllerRole || 'RESPRSN').trim();
53
+ const controllerIdentifier = canonicalControllerUuid(deps.input.controllerIdentifier
54
+ || deps.input.additionalClaims?.[ClaimsOrganizationSchemaorg.ownerIdentifierValue]
55
+ || onboardingDraft?.claims?.[ClaimsOrganizationSchemaorg.ownerIdentifierValue]
56
+ || randomUUID());
35
57
  const claims = {
36
58
  '@context': 'org.schema',
37
59
  ...(deps.input.additionalClaims || {}),
38
- [ClaimsOrganizationSchemaorg.alternateName]: alternateName,
60
+ ...(alternateName ? { [ClaimsOrganizationSchemaorg.alternateName]: alternateName } : {}),
61
+ [ClaimsOrganizationSchemaorg.ownerIdentifierValue]: controllerIdentifier,
39
62
  [ClaimsServiceSchemaorg.category]: deps.routeCtx.sector,
40
63
  [ClaimsPersonSchemaorg.hasOccupationalRoleValue]: controllerRole,
41
64
  ...(controllerEmail
@@ -51,18 +74,24 @@ export async function registerIndividualOrganizationWithDeps(deps) {
51
74
  }
52
75
  : {}),
53
76
  };
77
+ const registrationBody = onboardingDraft
78
+ ? buildIndividualOrganizationRegistrationGatewayRequestFromDraft({
79
+ draft: onboardingDraft,
80
+ routingClaims: claims,
81
+ })
82
+ : {
83
+ data: [{
84
+ type: GwCoreLifecycleRequestType.IndividualOrganizationRegistration,
85
+ resource: { meta: { claims } },
86
+ }],
87
+ };
54
88
  const registrationPayload = {
55
89
  jti: `jti-${createRuntimeUuid()}`,
56
90
  iss: deps.routeCtx.tenantId,
57
91
  aud: deps.routeCtx.tenantId,
58
92
  type: 'application/didcomm-plain+json',
59
93
  thid: `family-org-${createRuntimeUuid()}`,
60
- body: {
61
- data: [{
62
- type: GwCoreLifecycleRequestType.IndividualOrganizationRegistration,
63
- resource: { meta: { claims } },
64
- }],
65
- },
94
+ body: registrationBody,
66
95
  };
67
96
  const pollOptions = resolvePollOptionsFromSeconds(deps.input.timeoutSeconds, deps.input.intervalSeconds, {
68
97
  timeoutMs: deps.defaultTimeoutMs,
@@ -90,9 +119,30 @@ export async function registerIndividualOrganizationWithDeps(deps) {
90
119
  offerPreview: deps.getOfferPreviewFromResponse(registration),
91
120
  registrationStatus: registrationSummary?.status,
92
121
  orderConfirmationRequired: registrationSummary?.status !== 'already_exists',
93
- identity: readIndividualOrganizationBootstrapIdentity(registration.poll.body),
122
+ identity: readIndividualOrganizationBootstrapIdentity(registration.poll.body, {
123
+ controllerEmail,
124
+ controllerTelephone,
125
+ }),
94
126
  };
95
127
  }
128
+ function canonicalControllerUuid(value) {
129
+ const hexadecimal = normalizeUuid(String(value || '').trim());
130
+ if (!hexadecimal) {
131
+ throw new TypeError('controllerIdentifier must be a UUID or urn:uuid identifier.');
132
+ }
133
+ return [
134
+ hexadecimal.slice(0, 8),
135
+ hexadecimal.slice(8, 12),
136
+ hexadecimal.slice(12, 16),
137
+ hexadecimal.slice(16, 20),
138
+ hexadecimal.slice(20),
139
+ ].join('-');
140
+ }
141
+ function hasSignedPdfEvidence(draft) {
142
+ const claims = draft.documentReference?.resource?.meta?.claims;
143
+ return Boolean(String(claims?.[DocumentReferenceClaim.ContentType] || '').trim()
144
+ && String(claims?.[DocumentReferenceClaim.ContentData] || '').trim());
145
+ }
96
146
  /** @deprecated Use `registerIndividualOrganizationWithDeps`. */
97
147
  export async function startIndividualOrganizationWithDeps(deps) {
98
148
  return registerIndividualOrganizationWithDeps(deps);
@@ -105,7 +155,7 @@ export async function startIndividualOrganizationWithDeps(deps) {
105
155
  * `:organization:taxid:` DID path. `buildIndividualDidWeb(...)` only adds the
106
156
  * individual suffix and never rewrites that provider lineage.
107
157
  */
108
- export function readIndividualOrganizationBootstrapIdentity(responseBody) {
158
+ export function readIndividualOrganizationBootstrapIdentity(responseBody, controller) {
109
159
  const root = asRecord(responseBody);
110
160
  const body = asRecord(root?.body) || root;
111
161
  const entries = Array.isArray(body?.data) ? body.data : [];
@@ -126,16 +176,33 @@ export function readIndividualOrganizationBootstrapIdentity(responseBody) {
126
176
  catch {
127
177
  return undefined;
128
178
  }
179
+ const subjectDid = buildIndividualDidWeb({
180
+ providerDidWeb,
181
+ secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
182
+ secureIdValueIndividual,
183
+ });
184
+ const controllerEmail = String(controller?.controllerEmail || claims[ClaimsOrganizationSchemaorg.ownerEmail] || '').trim();
185
+ const controllerTelephone = String(controller?.controllerTelephone || claims[ClaimsOrganizationSchemaorg.ownerTelephone] || '').trim();
186
+ const controllerActorDid = controllerEmail || controllerTelephone
187
+ ? buildIndividualMemberDidWebFromPrivateIdentifiers({
188
+ providerDidWeb,
189
+ secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
190
+ privateIdValueIndividual: resourceId,
191
+ secureIdTypeMember: controllerEmail
192
+ ? SecureIdTypesIndividual.Email
193
+ : SecureIdTypesIndividual.Phone,
194
+ privateIdValueMember: controllerEmail || controllerTelephone,
195
+ roleType: HL7_CODING_SYSTEM_V3_ROLE_CODE,
196
+ roleValue: HealthcareActorRoleCodes.Controller,
197
+ })
198
+ : undefined;
129
199
  return {
130
200
  resourceId,
131
201
  secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
132
202
  secureIdValueIndividual,
133
203
  providerDidWeb,
134
- subjectDid: buildIndividualDidWeb({
135
- providerDidWeb,
136
- secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
137
- secureIdValueIndividual,
138
- }),
204
+ subjectDid,
205
+ ...(controllerActorDid ? { controllerActorDid } : {}),
139
206
  };
140
207
  }
141
208
  function asRecord(value) {
@@ -7,7 +7,7 @@ import type { HostedTenantDescendantKind } from './host-onboarding.js';
7
7
  import type { NodeLegalOrganizationVerificationTransactionInput, NodeOrganizationDidBindingInput, NodeOrganizationActivationInput } from './orchestration/client-port.js';
8
8
  import { type IndividualOrganizationConfirmOrderInput, type IndividualOrganizationOrderResult, type RouteContext } from './individual-onboarding.js';
9
9
  import { type EnsureFamilyOrganizationRegistrationInput } from './family-organization-registration.js';
10
- import { type FamilyOrganizationSearchInput } from './family-organization-search.js';
10
+ import { type FamilyOrganizationSearchInput, type OwnedFamilyOrganizationDirectoryInput } from './family-organization-search.js';
11
11
  import { type SmartTokenRequestInput } from './smart-token.js';
12
12
  import { type EmployeeDeviceActivationRequestInput, type EmployeeDeviceActivationResult, type EmployeeDeviceOtpRecoveryInput, type EmployeeDeviceOtpRecoveryResult, type EmployeeDeviceRevocationInput } from './device-activation.js';
13
13
  import { type OrganizationLicenseOrderConfirmInput } from './organization-license-order.js';
@@ -386,6 +386,12 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
386
386
  missingFields?: string[];
387
387
  updatedAt?: string;
388
388
  }> | null>;
389
+ /** Lists all individual Organizations owned by one verified account contact. */
390
+ listOwnedFamilyOrganizations(ctx: RouteContext, input: OwnedFamilyOrganizationDirectoryInput): Promise<Readonly<{
391
+ resourceId: string;
392
+ alternateName: string;
393
+ claims: Readonly<Record<string, unknown>>;
394
+ }>[]>;
389
395
  /**
390
396
  * Searches one existing family/individual registration and starts the
391
397
  * bootstrap flow only when the registration does not already exist.
@@ -4,7 +4,7 @@ import { confirmLegalOrganizationOrderWithDeps, HostLifecycleRequestType, Hosted
4
4
  import { GwCoreLifecycleRequestType } from './constants/lifecycle.js';
5
5
  import { confirmIndividualOrganizationOrderWithDeps, } from './individual-onboarding.js';
6
6
  import { ensureFamilyOrganizationRegistrationWithDeps, } from './family-organization-registration.js';
7
- import { searchFamilyOrganizationWithDeps } from './family-organization-search.js';
7
+ import { listOwnedFamilyOrganizationsWithDeps, searchFamilyOrganizationWithDeps, } from './family-organization-search.js';
8
8
  import { requestSmartTokenWithDeps } from './smart-token.js';
9
9
  import { activateEmployeeDeviceWithActivationCodeWithDeps, activateEmployeeDeviceWithActivationRequestWithDeps, recoverEmployeeDeviceWithOtpWithDeps, revokeEmployeeDeviceWithDeps, } from './device-activation.js';
10
10
  import { extractOfferIdFromResponseBody, extractOfferPreviewFromResponseBody, } from './order-offer-summary.js';
@@ -680,6 +680,16 @@ export class HttpRuntimeClient {
680
680
  submitAndPoll: this.submitAndPoll.bind(this),
681
681
  });
682
682
  }
683
+ /** Lists all individual Organizations owned by one verified account contact. */
684
+ async listOwnedFamilyOrganizations(ctx, input) {
685
+ return listOwnedFamilyOrganizationsWithDeps(ctx, input, {
686
+ defaultTimeoutMs: 20000,
687
+ defaultIntervalMs: 1000,
688
+ individualFamilyOrganizationSearchPath: this.paths.individualFamilyOrganizationSearchPath.bind(this.paths),
689
+ individualFamilyOrganizationSearchPollPath: this.paths.individualFamilyOrganizationSearchPollPath.bind(this.paths),
690
+ submitAndPoll: this.submitAndPoll.bind(this),
691
+ });
692
+ }
683
693
  /**
684
694
  * Searches one existing family/individual registration and starts the
685
695
  * bootstrap flow only when the registration does not already exist.
@@ -9,6 +9,15 @@ import type { NodeCapability } from '../session.js';
9
9
  import type { IndividualOrganizationLifecycleInput } from 'gdc-sdk-core-ts';
10
10
  import type { BlockchainArtifactRegistrationInput, ClinicalBundleSearchInput, ClinicalSectionUpdateInput, SubjectSectionUpdateInput, ClinicalSummaryReadResult, ClinicalSummaryRequestInput, ClinicalSummaryUpdateInput, CommunicationIngestionInput, CommunicationParticipantRuntimeSearchInput, DigitalTwinGenerationInput, DigitalTwinSecondaryUseConsentInput, DigitalTwinSecondaryUseConsentResult, DigitalTwinSubjectLinkPurgeInput, DigitalTwinSubjectLinkPurgeResult, GrantProfessionalAccessInput, GrantProfessionalAccessResult, IndividualMemberLifecycleInput, IndividualMemberLicenseAddInput, IndividualMemberLicenseInvitationInput, IndividualMemberLicenseTransitionInput, IpsOrFhirImportInput, LicenseListRuntimeSearchInput, LicenseOfferRuntimeSearchInput, LicenseOrderRuntimeSearchInput, RevokeProfessionalAccessInput, RevokeProfessionalAccessResult, ProfessionalAccessRequestDecisionInput, ProfessionalAccessRequestSearchInput, RelatedPersonUpsertInput } from '../resource-operations.js';
11
11
  import type { SmartTokenExchangeResult, SmartTokenRequestInput } from '../smart-token.js';
12
+ /** Profile-owned defaults applied to subject-section mutations after unlock. */
13
+ export type IndividualControllerProfileDefaults = Readonly<{
14
+ attester: SubjectSectionUpdateInput['attester'];
15
+ }>;
16
+ /**
17
+ * High-level section input. An opened controller profile supplies its own
18
+ * protected RESPRSN attester; standalone facades must still provide one.
19
+ */
20
+ export type IndividualControllerSubjectSectionUpdateInput = Omit<SubjectSectionUpdateInput, 'attester'> & Partial<Pick<SubjectSectionUpdateInput, 'attester'>>;
12
21
  /**
13
22
  * Individual-controller oriented facade over a `NodeRuntimeClient`.
14
23
  *
@@ -18,10 +27,11 @@ import type { SmartTokenExchangeResult, SmartTokenRequestInput } from '../smart-
18
27
  export declare class IndividualControllerSdk {
19
28
  private readonly client;
20
29
  private readonly capabilities?;
30
+ private readonly profileDefaults?;
21
31
  /**
22
32
  * @param client Runtime client implementation used to submit and poll GW flows.
23
33
  */
24
- constructor(client: NodeRuntimeClient, capabilities?: readonly NodeCapability[] | undefined);
34
+ constructor(client: NodeRuntimeClient, capabilities?: readonly NodeCapability[] | undefined, profileDefaults?: IndividualControllerProfileDefaults | undefined);
25
35
  /**
26
36
  * Registers the personal organization/subject index and returns its Offer.
27
37
  * Wallet creation, activation exchange, DCR, and session opening are later
@@ -118,7 +128,7 @@ export declare class IndividualControllerSdk {
118
128
  * appointments or contracts, while preserving the indexed Composition
119
129
  * author/attester compatibility contract.
120
130
  */
121
- updateSubjectSection(ctx: RouteContext, input: SubjectSectionUpdateInput): Promise<SubmitAndPollResult>;
131
+ updateSubjectSection(ctx: RouteContext, input: IndividualControllerSubjectSectionUpdateInput): Promise<SubmitAndPollResult>;
122
132
  /**
123
133
  * Updates the multi-section summary through a Composition-first document.
124
134
  * On a direct call, use the authenticated profile `actorDid` as `sender` and
@@ -18,9 +18,10 @@ export class IndividualControllerSdk {
18
18
  /**
19
19
  * @param client Runtime client implementation used to submit and poll GW flows.
20
20
  */
21
- constructor(client, capabilities) {
21
+ constructor(client, capabilities, profileDefaults) {
22
22
  this.client = client;
23
23
  this.capabilities = capabilities;
24
+ this.profileDefaults = profileDefaults;
24
25
  }
25
26
  /**
26
27
  * Registers the personal organization/subject index and returns its Offer.
@@ -173,7 +174,11 @@ export class IndividualControllerSdk {
173
174
  */
174
175
  updateSubjectSection(ctx, input) {
175
176
  assertFacadeCapability(this.capabilities, ActorCapabilities.IndividualIngestCommunication, ActorKinds.IndividualController, 'updateSubjectSection');
176
- return requireClientMethod(this.client, 'updateSubjectSection')(ctx, input);
177
+ const attester = input.attester || this.profileDefaults?.attester;
178
+ if (!attester) {
179
+ throw new Error('updateSubjectSection requires an explicit attester unless the facade comes from an opened controller profile.');
180
+ }
181
+ return requireClientMethod(this.client, 'updateSubjectSection')(ctx, { ...input, attester });
177
182
  }
178
183
  /**
179
184
  * Updates the multi-section summary through a Composition-first document.
@@ -11,7 +11,8 @@ import { ProfessionalSdk } from './orchestration/professional-sdk.js';
11
11
  import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
12
12
  import type { SmartTokenExchangeResult } from './smart-token.js';
13
13
  import type { DigitalTwinSearchInput, DigitalTwinSearchResult } from './digital-twin.js';
14
- import type { RouteContext } from './individual-onboarding.js';
14
+ import type { IndividualOrganizationOrderResult, RouteContext } from './individual-onboarding.js';
15
+ import type { IndividualOrganizationRegistrationResult } from './individual-start.js';
15
16
  import type { HostRouteContext } from './host-onboarding.js';
16
17
  import type { SecureDidcommTransportAdapter } from 'gdc-sdk-core-ts';
17
18
  import { type PinProtectedProfileSecret, type ProfileProtectionOptions, type ServerProfileSealer } from './server-profile-protection.js';
@@ -145,6 +146,13 @@ export type ServerProfileEnrollmentInput = Readonly<{
145
146
  idToken: string;
146
147
  /** One-time code returned by the completed organization flow or employee `License/_issue`. */
147
148
  activationCode: string;
149
+ /**
150
+ * Principal controller identifier projected by the SDK from
151
+ * `Organization.owner.identifier.value` and the matching automatic
152
+ * `RelatedPerson.identifier`. Individual-controller callers pass this
153
+ * identifier; the SDK constructs protected document-attester metadata.
154
+ */
155
+ controllerRelatedPersonIdentifier?: string;
148
156
  /**
149
157
  * Optional explicit installation identity. Normally omit it so the SDK
150
158
  * derives the identity from the wallet key it creates for this profile.
@@ -292,6 +300,43 @@ export type OpenedServerIndividualController = Readonly<{
292
300
  session: ResolvedServerProfileSession;
293
301
  profile: ServerProfileRecord;
294
302
  sdk: IndividualControllerSdk;
303
+ /** Returns the protected RelatedPerson URI for FHIR document attestation. */
304
+ getAttesterUriForDocs(): string;
305
+ }>;
306
+ /** One-time self-controller enrollment. It never opens a working session. */
307
+ export type ServerSelfIndividualControllerEnrollmentInput = Readonly<{
308
+ ownerId: string;
309
+ profileId: string;
310
+ registration: Pick<IndividualOrganizationRegistrationResult, 'identity'>;
311
+ order: Pick<IndividualOrganizationOrderResult, 'activationCode' | 'controllerRelatedPersonIdentifier'>;
312
+ routeContext: RouteContext;
313
+ pin: string;
314
+ idToken: string;
315
+ redirectUris: string[];
316
+ clientName: string;
317
+ }>;
318
+ /**
319
+ * One-time enrollment when the controller and represented subject are
320
+ * different identities. Demographics and alternateName belong to registration
321
+ * evidence and are deliberately not accepted by this operation.
322
+ */
323
+ export type ServerIndividualControllerEnrollmentInput = Readonly<{
324
+ ownerId: string;
325
+ profileId: string;
326
+ /** Stable controller/member DID that owns the wallet and DCR client. */
327
+ controllerActorDid: string;
328
+ /**
329
+ * Authorized subject alias used by the product, when different from the
330
+ * hosted subject DID returned by registration (for example, an animal card).
331
+ */
332
+ subjectDid?: string;
333
+ registration: Pick<IndividualOrganizationRegistrationResult, 'identity'>;
334
+ order: Pick<IndividualOrganizationOrderResult, 'activationCode' | 'controllerRelatedPersonIdentifier'>;
335
+ routeContext: RouteContext;
336
+ pin: string;
337
+ idToken: string;
338
+ redirectUris: string[];
339
+ clientName: string;
295
340
  }>;
296
341
  /** Server-owned role evidence used to sign a fresh professional VP. */
297
342
  export type ServerProfessionalProofInput = Readonly<{
@@ -368,6 +413,12 @@ export type ServerProfileSessionManagerOptions = Readonly<{
368
413
  export declare class ServerProfileSessionManager {
369
414
  private readonly options;
370
415
  constructor(options: ServerProfileSessionManagerOptions);
416
+ /**
417
+ * Enrolls a controller for a different represented subject. The SDK consumes
418
+ * the automatic GW RESPRSN assignment and does not ask the caller to build an
419
+ * attester. This operation does not accept or update subject demographics.
420
+ */
421
+ enrollIndividualController(input: ServerIndividualControllerEnrollmentInput): Promise<ServerProfileRecord>;
371
422
  /**
372
423
  * Resolves the canonical IPS author/attester projection from one owned
373
424
  * profile without exposing its protected wallet material to the portal.
@@ -383,6 +434,13 @@ export declare class ServerProfileSessionManager {
383
434
  */
384
435
  sourceAuthor?: ClinicalSourceAuthorSelection;
385
436
  }>): Promise<ClinicalCreatorIpsExport>;
437
+ /**
438
+ * Enrolls the controller of a self-managed individual from SDK-owned
439
+ * registration and Order projections. The caller never parses claims or
440
+ * constructs FHIR attester metadata, and this operation does not open the
441
+ * resulting profile.
442
+ */
443
+ enrollSelfIndividualController(input: ServerSelfIndividualControllerEnrollmentInput): Promise<ServerProfileRecord>;
386
444
  enroll(input: ServerProfileEnrollmentInput): Promise<ServerProfileRecord>;
387
445
  /**
388
446
  * Rotates an employee wallet after a fresh OTP-authenticated GW recovery.
@@ -24,20 +24,23 @@ import { ProfilePinRejectedError, openServerProfileSecret, protectServerProfileS
24
24
  * RelatedPerson/PractitionerRole assignment returned by GW.
25
25
  */
26
26
  export function buildProfileAttester(input) {
27
- const hexadecimal = normalizeUuid(String(input.assignmentIdentifier || '').trim());
28
- if (!hexadecimal) {
29
- throw new TypeError('Profile attester assignmentIdentifier must be a UUID returned by RelatedPerson or PractitionerRole data.');
30
- }
31
- const uuid = [
32
- hexadecimal.slice(0, 8),
33
- hexadecimal.slice(8, 12),
34
- hexadecimal.slice(12, 16),
35
- hexadecimal.slice(16, 20),
36
- hexadecimal.slice(20),
37
- ].join('-');
27
+ const assignmentIdentifier = String(input.assignmentIdentifier || '').trim();
28
+ const hexadecimal = normalizeUuid(assignmentIdentifier);
29
+ const reference = hexadecimal
30
+ ? `${UrnPrefixes.Uuid}${[
31
+ hexadecimal.slice(0, 8),
32
+ hexadecimal.slice(8, 12),
33
+ hexadecimal.slice(12, 16),
34
+ hexadecimal.slice(16, 20),
35
+ hexadecimal.slice(20),
36
+ ].join('-')}`
37
+ : assignmentIdentifier;
38
+ if (!/^(?:urn:|https?:\/\/)/i.test(reference)) {
39
+ throw new TypeError('Profile attester assignmentIdentifier must be a governed RelatedPerson or PractitionerRole URI.');
40
+ }
38
41
  return Object.freeze({
39
42
  mode: input.mode,
40
- party: Object.freeze({ reference: `${UrnPrefixes.Uuid}${uuid}` }),
43
+ party: Object.freeze({ reference }),
41
44
  });
42
45
  }
43
46
  /**
@@ -69,6 +72,42 @@ export class ServerProfileSessionManager {
69
72
  constructor(options) {
70
73
  this.options = options;
71
74
  }
75
+ /**
76
+ * Enrolls a controller for a different represented subject. The SDK consumes
77
+ * the automatic GW RESPRSN assignment and does not ask the caller to build an
78
+ * attester. This operation does not accept or update subject demographics.
79
+ */
80
+ async enrollIndividualController(input) {
81
+ const identity = input.registration.identity;
82
+ if (!identity) {
83
+ throw new Error('Individual-controller enrollment requires the registered individual identity.');
84
+ }
85
+ const controllerActorDid = String(input.controllerActorDid || '').trim();
86
+ if (!controllerActorDid) {
87
+ throw new Error('Individual-controller enrollment requires controllerActorDid.');
88
+ }
89
+ const subjectDid = String(input.subjectDid || identity.subjectDid).trim();
90
+ if (!subjectDid) {
91
+ throw new Error('Individual-controller enrollment requires subjectDid.');
92
+ }
93
+ return this.enroll({
94
+ ownerId: input.ownerId,
95
+ profileId: input.profileId,
96
+ actorKind: ActorKinds.IndividualController,
97
+ actorMode: 'controller',
98
+ actorDid: controllerActorDid,
99
+ profileDid: controllerActorDid,
100
+ providerDid: identity.providerDidWeb,
101
+ routeContext: input.routeContext,
102
+ allowedSubjectDids: [subjectDid],
103
+ pin: input.pin,
104
+ idToken: input.idToken,
105
+ activationCode: input.order.activationCode,
106
+ controllerRelatedPersonIdentifier: input.order.controllerRelatedPersonIdentifier,
107
+ redirectUris: input.redirectUris,
108
+ clientName: input.clientName,
109
+ });
110
+ }
72
111
  /**
73
112
  * Resolves the canonical IPS author/attester projection from one owned
74
113
  * profile without exposing its protected wallet material to the portal.
@@ -76,11 +115,50 @@ export class ServerProfileSessionManager {
76
115
  async exportClinicalCreatorIps(input) {
77
116
  return exportServerProfileClinicalCreatorIps(await this.requireOwnedProfile(input.ownerId, input.profileId), { sourceAuthor: input.sourceAuthor });
78
117
  }
118
+ /**
119
+ * Enrolls the controller of a self-managed individual from SDK-owned
120
+ * registration and Order projections. The caller never parses claims or
121
+ * constructs FHIR attester metadata, and this operation does not open the
122
+ * resulting profile.
123
+ */
124
+ async enrollSelfIndividualController(input) {
125
+ const identity = input.registration.identity;
126
+ if (!identity) {
127
+ throw new Error('Self individual-controller enrollment requires the registered individual identity.');
128
+ }
129
+ const controllerActorDid = String(identity.controllerActorDid || '').trim();
130
+ if (!controllerActorDid) {
131
+ throw new Error('Self individual-controller enrollment requires the registered controller actor DID.');
132
+ }
133
+ return this.enroll({
134
+ ownerId: input.ownerId,
135
+ profileId: input.profileId,
136
+ actorKind: ActorKinds.IndividualController,
137
+ actorMode: 'self',
138
+ actorDid: controllerActorDid,
139
+ profileDid: controllerActorDid,
140
+ providerDid: identity.providerDidWeb,
141
+ routeContext: input.routeContext,
142
+ allowedSubjectDids: [identity.subjectDid],
143
+ pin: input.pin,
144
+ idToken: input.idToken,
145
+ activationCode: input.order.activationCode,
146
+ controllerRelatedPersonIdentifier: input.order.controllerRelatedPersonIdentifier,
147
+ redirectUris: input.redirectUris,
148
+ clientName: input.clientName,
149
+ });
150
+ }
79
151
  async enroll(input) {
80
152
  const normalizedClinicalCreatorBinding = input.clinicalCreatorBinding
81
153
  ? normalizeClinicalCreatorBinding(input.clinicalCreatorBinding)
82
154
  : undefined;
83
- const attester = input.attester || (normalizedClinicalCreatorBinding
155
+ const controllerAttester = input.controllerRelatedPersonIdentifier
156
+ ? buildProfileAttester({
157
+ assignmentIdentifier: input.controllerRelatedPersonIdentifier,
158
+ mode: CompositionAttesterModes.Personal,
159
+ })
160
+ : undefined;
161
+ const attester = controllerAttester || input.attester || (normalizedClinicalCreatorBinding
84
162
  ? buildProfileAttester({
85
163
  assignmentIdentifier: normalizedClinicalCreatorBinding.authorIdentifier,
86
164
  mode: normalizedClinicalCreatorBinding.kind === FhirIpsCreatorKinds.Professional
@@ -145,8 +223,12 @@ export class ServerProfileSessionManager {
145
223
  const activation = await client.activateProfileDeviceWithActivationRequest(activationRequest);
146
224
  const dcrBody = terminalBody(activation.dcr.poll.body);
147
225
  const clientId = findText(dcrBody, ['client_id', 'clientId']);
148
- if (!clientId)
149
- throw new Error('GW DCR did not return client_id.');
226
+ if (!clientId) {
227
+ const diagnostics = findText(dcrBody, ['diagnostics', 'details', 'message']);
228
+ throw new Error(diagnostics
229
+ ? `GW DCR failed: ${diagnostics}`
230
+ : 'GW DCR did not return client_id.');
231
+ }
150
232
  const deviceDid = findText(dcrBody, ['device_did', 'deviceDid', 'did']) || clientId;
151
233
  const now = this.now();
152
234
  const managedVpToken = input.vpToken || (input.professionalProof
@@ -562,6 +644,10 @@ export class ServerProfileSessionManager {
562
644
  || (profile.actorMode !== 'self' && profile.actorMode !== 'controller')) {
563
645
  throw new Error('Profile is not an individual controller.');
564
646
  }
647
+ const profileAttester = profile.attester;
648
+ if (!profileAttester) {
649
+ throw new Error('The opened individual-controller profile has no RelatedPerson attester.');
650
+ }
565
651
  const client = new NodeHttpClient({
566
652
  baseUrl: this.options.gatewayBaseUrl,
567
653
  ctx: profile.routeContext,
@@ -571,7 +657,20 @@ export class ServerProfileSessionManager {
571
657
  transportProfile: TransportProfiles.DidcommEncryptedForm,
572
658
  secureTransportAdapter: session.secureTransportAdapter,
573
659
  });
574
- return { session, profile, sdk: new IndividualControllerSdk(client) };
660
+ return {
661
+ session,
662
+ profile,
663
+ sdk: new IndividualControllerSdk(client, undefined, {
664
+ attester: profileAttester,
665
+ }),
666
+ getAttesterUriForDocs: () => {
667
+ const reference = String(profileAttester.party.reference || '').trim();
668
+ if (!reference) {
669
+ throw new Error('The opened individual-controller profile has no RelatedPerson attester URI.');
670
+ }
671
+ return reference;
672
+ },
673
+ };
575
674
  }
576
675
  /**
577
676
  * Opens the high-level organization-controller API without exposing wallet,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.9.11",
3
+ "version": "2.9.14",
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.",
@@ -15,7 +15,7 @@
15
15
  "prepare": "npm run build",
16
16
  "type-check": "tsc -p tsconfig.json --noEmit && tsc -p docs/snippets/tsconfig.json",
17
17
  "check:product-neutrality": "node scripts/check-product-neutrality.mjs",
18
- "prepublishOnly": "npm run check:product-neutrality && npm run type-check && npm test",
18
+ "prepublishOnly": "npm run check:product-neutrality && npm run type-check && npm test && npm run test:e2e:live-full-cycle",
19
19
  "local:close": "PORTS=3000 bash ./scripts/local-close.sh",
20
20
  "docker:close": "PORTS=8000 bash ./scripts/local-close.sh",
21
21
  "test": "npm run build && node --test tests/*.test.mjs",
@@ -38,8 +38,8 @@
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.10",
42
- "gdc-sdk-core-ts": "2.9.8"
41
+ "gdc-common-utils-ts": "2.9.14",
42
+ "gdc-sdk-core-ts": "2.9.9"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^20.14.10",