gdc-sdk-node-ts 2.9.12 → 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) {
@@ -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
  *
@@ -97,6 +106,12 @@ export type IndividualOrganizationBootstrapIdentity = {
97
106
  providerDidWeb: string;
98
107
  /** Canonical child DID built beneath the exact provider DID returned by GW. */
99
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;
100
115
  };
101
116
  type RegisterIndividualOrganizationDeps = {
102
117
  input: IndividualOrganizationRegistrationInput;
@@ -128,4 +143,7 @@ export declare function startIndividualOrganizationWithDeps(deps: RegisterIndivi
128
143
  * `:organization:taxid:` DID path. `buildIndividualDidWeb(...)` only adds the
129
144
  * individual suffix and never rewrites that provider lineage.
130
145
  */
131
- 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,8 +1,10 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { ClaimsOfferSchemaorg, ClaimsOrganizationSchemaorg, ClaimsPersonSchemaorg, ClaimsServiceSchemaorg, } from 'gdc-common-utils-ts/constants';
4
- import { buildIndividualDidWeb, buildSecureIdValueIndividual, extractPrimaryClaims, normalizeUuid, 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';
5
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';
6
8
  import { GwCoreLifecycleRequestType } from './constants/lifecycle.js';
7
9
  import { resolvePollOptionsFromSeconds } from './poll-options.js';
8
10
  /**
@@ -23,23 +25,39 @@ export async function registerIndividualOrganizationWithDeps(deps) {
23
25
  * in the payload for compatibility, but the owner claims are the live GW
24
26
  * routing/indexing contract for this flow.
25
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
+ }
26
37
  const alternateName = String(deps.input.alternateName || '').trim();
27
- if (!alternateName) {
38
+ if (!onboardingDraft && !alternateName) {
28
39
  throw new Error('registerIndividualOrganization requires alternateName.');
29
40
  }
30
- const controllerEmail = String(deps.input.controllerEmail || '').trim();
31
- const controllerTelephone = String(deps.input.controllerTelephone || '').trim();
32
- 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) {
33
50
  throw new Error('registerIndividualOrganization requires controllerEmail, or controllerTelephone only for compatibility/extension flows.');
34
51
  }
35
52
  const controllerRole = String(deps.input.controllerRole || 'RESPRSN').trim();
36
53
  const controllerIdentifier = canonicalControllerUuid(deps.input.controllerIdentifier
37
54
  || deps.input.additionalClaims?.[ClaimsOrganizationSchemaorg.ownerIdentifierValue]
55
+ || onboardingDraft?.claims?.[ClaimsOrganizationSchemaorg.ownerIdentifierValue]
38
56
  || randomUUID());
39
57
  const claims = {
40
58
  '@context': 'org.schema',
41
59
  ...(deps.input.additionalClaims || {}),
42
- [ClaimsOrganizationSchemaorg.alternateName]: alternateName,
60
+ ...(alternateName ? { [ClaimsOrganizationSchemaorg.alternateName]: alternateName } : {}),
43
61
  [ClaimsOrganizationSchemaorg.ownerIdentifierValue]: controllerIdentifier,
44
62
  [ClaimsServiceSchemaorg.category]: deps.routeCtx.sector,
45
63
  [ClaimsPersonSchemaorg.hasOccupationalRoleValue]: controllerRole,
@@ -56,18 +74,24 @@ export async function registerIndividualOrganizationWithDeps(deps) {
56
74
  }
57
75
  : {}),
58
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
+ };
59
88
  const registrationPayload = {
60
89
  jti: `jti-${createRuntimeUuid()}`,
61
90
  iss: deps.routeCtx.tenantId,
62
91
  aud: deps.routeCtx.tenantId,
63
92
  type: 'application/didcomm-plain+json',
64
93
  thid: `family-org-${createRuntimeUuid()}`,
65
- body: {
66
- data: [{
67
- type: GwCoreLifecycleRequestType.IndividualOrganizationRegistration,
68
- resource: { meta: { claims } },
69
- }],
70
- },
94
+ body: registrationBody,
71
95
  };
72
96
  const pollOptions = resolvePollOptionsFromSeconds(deps.input.timeoutSeconds, deps.input.intervalSeconds, {
73
97
  timeoutMs: deps.defaultTimeoutMs,
@@ -95,7 +119,10 @@ export async function registerIndividualOrganizationWithDeps(deps) {
95
119
  offerPreview: deps.getOfferPreviewFromResponse(registration),
96
120
  registrationStatus: registrationSummary?.status,
97
121
  orderConfirmationRequired: registrationSummary?.status !== 'already_exists',
98
- identity: readIndividualOrganizationBootstrapIdentity(registration.poll.body),
122
+ identity: readIndividualOrganizationBootstrapIdentity(registration.poll.body, {
123
+ controllerEmail,
124
+ controllerTelephone,
125
+ }),
99
126
  };
100
127
  }
101
128
  function canonicalControllerUuid(value) {
@@ -111,6 +138,11 @@ function canonicalControllerUuid(value) {
111
138
  hexadecimal.slice(20),
112
139
  ].join('-');
113
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
+ }
114
146
  /** @deprecated Use `registerIndividualOrganizationWithDeps`. */
115
147
  export async function startIndividualOrganizationWithDeps(deps) {
116
148
  return registerIndividualOrganizationWithDeps(deps);
@@ -123,7 +155,7 @@ export async function startIndividualOrganizationWithDeps(deps) {
123
155
  * `:organization:taxid:` DID path. `buildIndividualDidWeb(...)` only adds the
124
156
  * individual suffix and never rewrites that provider lineage.
125
157
  */
126
- export function readIndividualOrganizationBootstrapIdentity(responseBody) {
158
+ export function readIndividualOrganizationBootstrapIdentity(responseBody, controller) {
127
159
  const root = asRecord(responseBody);
128
160
  const body = asRecord(root?.body) || root;
129
161
  const entries = Array.isArray(body?.data) ? body.data : [];
@@ -144,16 +176,33 @@ export function readIndividualOrganizationBootstrapIdentity(responseBody) {
144
176
  catch {
145
177
  return undefined;
146
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;
147
199
  return {
148
200
  resourceId,
149
201
  secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
150
202
  secureIdValueIndividual,
151
203
  providerDidWeb,
152
- subjectDid: buildIndividualDidWeb({
153
- providerDidWeb,
154
- secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
155
- secureIdValueIndividual,
156
- }),
204
+ subjectDid,
205
+ ...(controllerActorDid ? { controllerActorDid } : {}),
157
206
  };
158
207
  }
159
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.
@@ -315,6 +315,29 @@ export type ServerSelfIndividualControllerEnrollmentInput = Readonly<{
315
315
  redirectUris: string[];
316
316
  clientName: string;
317
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;
340
+ }>;
318
341
  /** Server-owned role evidence used to sign a fresh professional VP. */
319
342
  export type ServerProfessionalProofInput = Readonly<{
320
343
  role: string;
@@ -390,6 +413,12 @@ export type ServerProfileSessionManagerOptions = Readonly<{
390
413
  export declare class ServerProfileSessionManager {
391
414
  private readonly options;
392
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>;
393
422
  /**
394
423
  * Resolves the canonical IPS author/attester projection from one owned
395
424
  * profile without exposing its protected wallet material to the portal.
@@ -72,6 +72,42 @@ export class ServerProfileSessionManager {
72
72
  constructor(options) {
73
73
  this.options = options;
74
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
+ }
75
111
  /**
76
112
  * Resolves the canonical IPS author/attester projection from one owned
77
113
  * profile without exposing its protected wallet material to the portal.
@@ -90,13 +126,17 @@ export class ServerProfileSessionManager {
90
126
  if (!identity) {
91
127
  throw new Error('Self individual-controller enrollment requires the registered individual identity.');
92
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
+ }
93
133
  return this.enroll({
94
134
  ownerId: input.ownerId,
95
135
  profileId: input.profileId,
96
136
  actorKind: ActorKinds.IndividualController,
97
137
  actorMode: 'self',
98
- actorDid: identity.subjectDid,
99
- profileDid: identity.subjectDid,
138
+ actorDid: controllerActorDid,
139
+ profileDid: controllerActorDid,
100
140
  providerDid: identity.providerDidWeb,
101
141
  routeContext: input.routeContext,
102
142
  allowedSubjectDids: [identity.subjectDid],
@@ -183,8 +223,12 @@ export class ServerProfileSessionManager {
183
223
  const activation = await client.activateProfileDeviceWithActivationRequest(activationRequest);
184
224
  const dcrBody = terminalBody(activation.dcr.poll.body);
185
225
  const clientId = findText(dcrBody, ['client_id', 'clientId']);
186
- if (!clientId)
187
- 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
+ }
188
232
  const deviceDid = findText(dcrBody, ['device_did', 'deviceDid', 'did']) || clientId;
189
233
  const now = this.now();
190
234
  const managedVpToken = input.vpToken || (input.professionalProof
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.9.12",
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",