gdc-sdk-node-ts 2.4.41 → 2.4.44

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
@@ -650,13 +650,13 @@ modules below.
650
650
  - types: `SmartTokenRequestInput`, `SmartTokenExchangeResult`
651
651
  - function: `requestSmartTokenWithDeps(...)`
652
652
  - [`src/resource-operations.ts`](src/resource-operations.ts)
653
- - types: `OrganizationEmployeeCreationInput`, `IpsOrFhirImportInput`, `RelatedPersonUpsertInput`, `CommunicationIngestionInput`, `ClinicalDateRange`, `ClinicalBundleSearchInput`, `ConsentActorTargetInput`, `GrantProfessionalAccessInput`, `GrantProfessionalAccessResult`, `DigitalTwinSecondaryUseConsentInput`, `DigitalTwinSubjectLinkPurgeInput`
654
- - functions: `createOrganizationEmployeeWithDeps(...)`, `importIpsOrFhirAndUpdateIndexWithDeps(...)`, `upsertRelatedPersonAndPollWithDeps(...)`, `ingestCommunicationAndUpdateIndexWithDeps(...)`, `searchClinicalBundleWithDeps(...)`, `searchLatestIpsWithDeps(...)`, `grantProfessionalAccessWithDeps(...)`, `setDigitalTwinSecondaryUseConsentWithDeps(...)`, `purgeDigitalTwinSubjectLinkWithDeps(...)`
653
+ - types: `OrganizationEmployeeCreationInput`, `IpsOrFhirImportInput`, `RelatedPersonUpsertInput`, `CommunicationIngestionInput`, `ClinicalDateRange`, `ClinicalBundleSearchInput`, `ConsentActorTargetInput`, `GrantProfessionalAccessInput`, `GrantProfessionalAccessResult`, `DigitalTwinSecondaryUseConsentInput`, `DigitalTwinSubjectLinkPurgeInput`, `SubjectConsentSearchInput`
654
+ - functions: `createOrganizationEmployeeWithDeps(...)`, `importIpsOrFhirAndUpdateIndexWithDeps(...)`, `upsertRelatedPersonAndPollWithDeps(...)`, `ingestCommunicationAndUpdateIndexWithDeps(...)`, `searchClinicalBundleWithDeps(...)`, `searchSubjectConsentsWithDeps(...)`, `searchLatestIpsWithDeps(...)`, `grantProfessionalAccessWithDeps(...)`, `setDigitalTwinSecondaryUseConsentWithDeps(...)`, `purgeDigitalTwinSubjectLinkWithDeps(...)`
655
655
  - [`src/digital-twin.ts`](src/digital-twin.ts)
656
656
  - types: `DigitalTwinSearchInput`, `DigitalTwinSelectionInput`, `DigitalTwinResearchTag`, `DigitalTwinMaterializationInput`
657
657
  - functions: `searchDigitalTwinsWithDeps(...)`, `saveDigitalTwinSelectionWithDeps(...)`, `materializeDigitalTwinWithDeps(...)`
658
658
  - [`src/orchestration/digital-twin-sdk.ts`](src/orchestration/digital-twin-sdk.ts)
659
- - class: `DigitalTwinSdk` (`requestSmartToken`, `search`, `saveSelection`, `materialize`)
659
+ - class: `DigitalTwinSdk` (`requestSmartToken`, `search`, `saveSelection`, `searchSelections`, `materialize`); both searches expose `ResearchSubject` through FHIR Parameters, with its canonical internal Composition available as `composition`
660
660
  - [`src/session.ts`](src/session.ts)
661
661
  - types: `NodeCapability`, `NodeActorSessionContext`, `ActorSessionContext`
662
662
  - classes: `ActorSession`, `NodeActorSession`
@@ -7,7 +7,7 @@ export type DigitalTwinFhirFormat = 'org.hl7.fhir.r4' | 'org.hl7.fhir.api';
7
7
  export declare function isDigitalTwinSubjectId(value: unknown): value is string;
8
8
  /** Rejects operational DIDs and malformed or caller-invented subject shapes. */
9
9
  export declare function assertDigitalTwinSubjectId(value: unknown): asserts value is string;
10
- /** Search parameters added by the digital-twin Composition profile. */
10
+ /** Search parameters evaluated against a ResearchSubject's canonical Composition index. */
11
11
  export declare const DigitalTwinSearchParameter: Readonly<{
12
12
  Section: "section";
13
13
  DateFrom: "date-from";
@@ -20,7 +20,8 @@ export type DigitalTwinSearchInput = {
20
20
  accessToken?: string;
21
21
  thid?: string;
22
22
  format?: DigitalTwinFhirFormat;
23
- resourceType?: string;
23
+ /** Public twin search resource; exposed for low-level request inspection only. */
24
+ resourceType?: 'ResearchSubject';
24
25
  /** One or more IPS section tokens. Basic search uses OR across sections. */
25
26
  sections?: readonly string[];
26
27
  /** Inclusive clinical-event lower bound as ISO date or dateTime. */
@@ -33,11 +34,19 @@ export type DigitalTwinSearchInput = {
33
34
  filters?: Readonly<Record<string, string | readonly string[] | undefined>>;
34
35
  pollOptions?: PollOptions;
35
36
  };
36
- /** One pseudonymous Composition returned by a digital-twin search. */
37
+ /**
38
+ * One public ResearchSubject digital twin returned by search.
39
+ *
40
+ * `composition` is the canonical Composition GW uses as the aggregate's
41
+ * internal index document. It joins the ResearchSubject to the projected
42
+ * clinical resources later returned by `ResearchSubject/$summary`.
43
+ */
37
44
  export type DigitalTwinSearchMatch = Record<string, unknown> & {
45
+ resourceType?: 'ResearchSubject';
38
46
  id?: string;
39
47
  [CompositionClaim.Subject]: string;
40
48
  [CompositionClaim.Section]?: string;
49
+ composition?: Record<string, unknown>;
41
50
  meta?: {
42
51
  tag?: DigitalTwinResearchTag[];
43
52
  };
@@ -120,11 +129,15 @@ export type DigitalTwinRuntimeDeps = {
120
129
  body: Record<string, unknown>;
121
130
  }, pollOptions?: PollOptions) => Promise<SubmitAndPollResult>;
122
131
  };
123
- /** Opens the GW async response and exposes the matched Compositions directly. */
132
+ /** Opens the GW async response and exposes the matched ResearchSubjects directly. */
124
133
  export declare function readDigitalTwinSearchResult(operation: SubmitAndPollResult): DigitalTwinSearchResult;
125
134
  /** Persists one tagged researcher working copy through `digitaltwin/Composition/_batch`. */
126
135
  export declare function saveDigitalTwinSelectionWithDeps(ctx: RouteContext, input: DigitalTwinSelectionInput, deps: DigitalTwinRuntimeDeps): Promise<SubmitAndPollResult>;
127
- /** Searches the tenant digital-twin index through its public asynchronous route. */
136
+ /**
137
+ * Searches ResearchSubject digital twins through the public asynchronous route.
138
+ * GW filters their canonical Composition index documents internally; callers
139
+ * never switch to a separate public Composition search surface.
140
+ */
128
141
  export declare function searchDigitalTwinsWithDeps(ctx: RouteContext, input: DigitalTwinSearchInput, deps: DigitalTwinRuntimeDeps): Promise<SubmitAndPollResult>;
129
142
  /** Materializes one selected pseudonymous twin as a research summary Bundle. */
130
143
  export declare function materializeDigitalTwinWithDeps(ctx: RouteContext, input: DigitalTwinMaterializationInput, deps: DigitalTwinRuntimeDeps): Promise<SubmitAndPollResult>;
@@ -13,7 +13,7 @@ export function assertDigitalTwinSubjectId(value) {
13
13
  throw new Error('Digital twin subject must be a valid urn:uuid identifier.');
14
14
  }
15
15
  }
16
- /** Search parameters added by the digital-twin Composition profile. */
16
+ /** Search parameters evaluated against a ResearchSubject's canonical Composition index. */
17
17
  export const DigitalTwinSearchParameter = Object.freeze({
18
18
  Section: 'section',
19
19
  DateFrom: 'date-from',
@@ -59,7 +59,7 @@ function normalizeResearchTags(tags) {
59
59
  }
60
60
  return normalized;
61
61
  }
62
- /** Opens the GW async response and exposes the matched Compositions directly. */
62
+ /** Opens the GW async response and exposes the matched ResearchSubjects directly. */
63
63
  export function readDigitalTwinSearchResult(operation) {
64
64
  const body = operation?.poll?.body;
65
65
  const responseEntries = Array.isArray(body?.data)
@@ -129,10 +129,14 @@ export async function saveDigitalTwinSelectionWithDeps(ctx, input, deps) {
129
129
  : { resourceType: 'Bundle', type: 'batch', entry: [entry] },
130
130
  }, input.pollOptions);
131
131
  }
132
- /** Searches the tenant digital-twin index through its public asynchronous route. */
132
+ /**
133
+ * Searches ResearchSubject digital twins through the public asynchronous route.
134
+ * GW filters their canonical Composition index documents internally; callers
135
+ * never switch to a separate public Composition search surface.
136
+ */
133
137
  export async function searchDigitalTwinsWithDeps(ctx, input, deps) {
134
138
  const format = input.format || 'org.hl7.fhir.r4';
135
- const resourceType = String(input.resourceType || 'Composition').trim();
139
+ const resourceType = String(input.resourceType || 'ResearchSubject').trim();
136
140
  if (!resourceType)
137
141
  throw new Error('Digital twin resourceType is required.');
138
142
  const usesBasicSearch = Boolean(input.sections || input.dateFrom || input.dateTo || input.text);
@@ -1,5 +1,4 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
- import { ResourceTypesFhirR4 } from 'gdc-common-utils-ts/constants/fhir-resource-types';
3
2
  import { ClaimConsent } from 'gdc-common-utils-ts/models/consent-rule';
4
3
  import { isConsentRuleActive } from 'gdc-common-utils-ts/utils/consent';
5
4
  import { requireClientMethod } from './orchestration/client-port.js';
@@ -20,10 +19,7 @@ export class GatewayActiveConsentProvider {
20
19
  if (!normalizedSubject.startsWith('did:')) {
21
20
  throw new Error('Active Consent lookup requires a subject DID.');
22
21
  }
23
- const result = await requireClientMethod(this.client, 'searchClinicalBundle')(this.routeContext, {
24
- subject: normalizedSubject,
25
- includedTypes: [ResourceTypesFhirR4.Consent],
26
- });
22
+ const result = await requireClientMethod(this.client, 'searchSubjectConsents')(this.routeContext, { subject: normalizedSubject });
27
23
  return extractConsentRules(result.poll.body)
28
24
  .filter((rule) => isConsentRuleActive(rule, { subject: normalizedSubject }));
29
25
  }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './identity-bootstrap.js';
4
4
  export * from './async-polling.js';
5
5
  export * from './backend-profile-runtime.js';
6
6
  export * from './server-profile-session.js';
7
+ export * from './profile-recovery-envelope.js';
7
8
  export * from './server-profile-protection.js';
8
9
  export * from './backend-profile-workspace.js';
9
10
  export * from './individual-controller-backend-runtime.js';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from './identity-bootstrap.js';
5
5
  export * from './async-polling.js';
6
6
  export * from './backend-profile-runtime.js';
7
7
  export * from './server-profile-session.js';
8
+ export * from './profile-recovery-envelope.js';
8
9
  export * from './server-profile-protection.js';
9
10
  export * from './backend-profile-workspace.js';
10
11
  export * from './individual-controller-backend-runtime.js';
@@ -12,7 +12,7 @@ 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';
14
14
  import { type IndividualOrganizationBootstrapInput, type IndividualOrganizationStartResult } from './individual-start.js';
15
- import { type CommunicationIngestionInput, type BlockchainArtifactRegistrationInput, type VitalSignBatchCommunicationFromSearchResponseInput, type CommunicationParticipantRuntimeSearchInput, type ClinicalBundleSearchInput, type ClinicalSectionUpdateInput, type ClinicalSummaryReadResult, type ClinicalSummaryRequestInput, type ClinicalSummaryUpdateInput, type GrantProfessionalAccessInput, type GrantProfessionalAccessResult, type DigitalTwinSecondaryUseConsentInput, type DigitalTwinSecondaryUseConsentResult, type DigitalTwinSubjectLinkPurgeInput, type DigitalTwinSubjectLinkPurgeResult, type IndividualMemberLifecycleInput, type IndividualMemberLicenseAddInput, type IndividualMemberLicenseInvitationInput, type IndividualMemberLicenseTransitionInput, type LicenseListRuntimeSearchInput, type LicenseOfferRuntimeSearchInput, type LicenseOrderRuntimeSearchInput, type OrganizationEmployeeCreationInput, type OrganizationEmployeeLicenseAddInput, type OrganizationEmployeeLicenseOfferInput, type OrganizationEmployeeLicenseInvitationInput, type OrganizationEmployeeLifecycleInput, type OrganizationEmployeeSearchInput, type ProfessionalAccessRequestInput, type ProfessionalAccessRequestResult, type RevokeProfessionalAccessInput, type RevokeProfessionalAccessResult, type RelatedPersonUpsertInput } from './resource-operations.js';
15
+ import { type CommunicationIngestionInput, type BlockchainArtifactRegistrationInput, type VitalSignBatchCommunicationFromSearchResponseInput, type CommunicationParticipantRuntimeSearchInput, type ClinicalBundleSearchInput, type ClinicalSectionUpdateInput, type ClinicalSummaryReadResult, type ClinicalSummaryRequestInput, type ClinicalSummaryUpdateInput, type GrantProfessionalAccessInput, type GrantProfessionalAccessResult, type DigitalTwinSecondaryUseConsentInput, type DigitalTwinSecondaryUseConsentResult, type DigitalTwinSubjectLinkPurgeInput, type DigitalTwinSubjectLinkPurgeResult, type SubjectConsentSearchInput, type IndividualMemberLifecycleInput, type IndividualMemberLicenseAddInput, type IndividualMemberLicenseInvitationInput, type IndividualMemberLicenseTransitionInput, type LicenseListRuntimeSearchInput, type LicenseOfferRuntimeSearchInput, type LicenseOrderRuntimeSearchInput, type OrganizationEmployeeCreationInput, type OrganizationEmployeeLicenseAddInput, type OrganizationEmployeeLicenseOfferInput, type OrganizationEmployeeLicenseInvitationInput, type OrganizationEmployeeLifecycleInput, type OrganizationEmployeeSearchInput, type ProfessionalAccessRequestInput, type ProfessionalAccessRequestResult, type RevokeProfessionalAccessInput, type RevokeProfessionalAccessResult, type RelatedPersonUpsertInput } from './resource-operations.js';
16
16
  import type { LegalOrganizationOrderInput } from './host-onboarding.js';
17
17
  import type { SmartTokenExchangeResult } from './smart-token.js';
18
18
  import { type OrganizationEmployeeProvisioningInput, type OrganizationEmployeeProvisioningResult } from './organization-employee-lifecycle.js';
@@ -447,6 +447,8 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
447
447
  setDigitalTwinSecondaryUseConsent(ctx: RouteContext, input: DigitalTwinSecondaryUseConsentInput): Promise<DigitalTwinSecondaryUseConsentResult>;
448
448
  /** Offboards the subject from this index provider without deleting the anonymous twin. */
449
449
  purgeDigitalTwinSubjectLink(ctx: RouteContext, input: DigitalTwinSubjectLinkPurgeInput): Promise<DigitalTwinSubjectLinkPurgeResult>;
450
+ /** Reads the subject-owned Consent records through Communication -> Subject/_search. */
451
+ searchSubjectConsents(ctx: RouteContext, input: SubjectConsentSearchInput): Promise<SubmitAndPollResult>;
450
452
  /**
451
453
  * Closes an existing professional consent by setting its period end and
452
454
  * resubmitting the updated consent resource.
@@ -10,7 +10,7 @@ import { activateEmployeeDeviceWithActivationCodeWithDeps, activateEmployeeDevic
10
10
  import { extractOfferIdFromResponseBody, extractOfferPreviewFromResponseBody, } from './order-offer-summary.js';
11
11
  import { confirmOrganizationLicenseOrderWithDeps } from './organization-license-order.js';
12
12
  import { startIndividualOrganizationWithDeps } from './individual-start.js';
13
- import { createOrganizationEmployeeWithDeps, issueOrganizationEmployeeLicenseWithDeps, disableIndividualMemberWithDeps, disableIndividualOrganizationWithDeps, disableOrganizationEmployeeWithDeps, listIndividualLicenseOffersWithDeps, listIndividualLicenseOrdersWithDeps, listIndividualLicensesWithDeps, addFreeIndividualMemberLicensesWithDeps, issueIndividualMemberLicenseWithDeps, transitionIndividualMemberLicenseWithDeps, listOrganizationLicenseOffersWithDeps, listOrganizationLicenseOrdersWithDeps, listOrganizationLicensesWithDeps, grantProfessionalAccessWithDeps, setDigitalTwinSecondaryUseConsentWithDeps, purgeDigitalTwinSubjectLinkWithDeps, buildClinicalSectionUpdateIngestion, buildClinicalSummaryUpdateIngestion, ingestCommunicationAndUpdateIndexWithDeps, requestProfessionalAccessWithDeps, buildVitalSignBatchCommunicationFromSearchResponse, registerBlockchainArtifactAndUpdateIndexWithDeps, requestClinicalSummaryWithDeps, revokeProfessionalAccessWithDeps, searchCommunicationParticipantsWithDeps, purgeIndividualMemberWithDeps, purgeIndividualOrganizationWithDeps, purgeOrganizationEmployeeWithDeps, prepareEmployeeLifecycleMessageForTransport, searchIndividualLicensesWithDeps, searchIndividualLicenseOffersWithDeps, searchIndividualLicenseOrdersWithDeps, searchOrganizationLicensesWithDeps, searchOrganizationLicenseOffersWithDeps, searchOrganizationLicenseOrdersWithDeps, searchOrganizationEmployeesWithDeps, searchClinicalBundleWithDeps, searchLatestIpsWithDeps, upsertRelatedPersonAndPollWithDeps, normalizeCommunicationPathFormatSegment, addFreeOrganizationEmployeeLicensesWithDeps, requestOrganizationEmployeeLicenseOfferWithDeps, } from './resource-operations.js';
13
+ import { createOrganizationEmployeeWithDeps, issueOrganizationEmployeeLicenseWithDeps, disableIndividualMemberWithDeps, disableIndividualOrganizationWithDeps, disableOrganizationEmployeeWithDeps, listIndividualLicenseOffersWithDeps, listIndividualLicenseOrdersWithDeps, listIndividualLicensesWithDeps, addFreeIndividualMemberLicensesWithDeps, issueIndividualMemberLicenseWithDeps, transitionIndividualMemberLicenseWithDeps, listOrganizationLicenseOffersWithDeps, listOrganizationLicenseOrdersWithDeps, listOrganizationLicensesWithDeps, grantProfessionalAccessWithDeps, setDigitalTwinSecondaryUseConsentWithDeps, purgeDigitalTwinSubjectLinkWithDeps, searchSubjectConsentsWithDeps, buildClinicalSectionUpdateIngestion, buildClinicalSummaryUpdateIngestion, ingestCommunicationAndUpdateIndexWithDeps, requestProfessionalAccessWithDeps, buildVitalSignBatchCommunicationFromSearchResponse, registerBlockchainArtifactAndUpdateIndexWithDeps, requestClinicalSummaryWithDeps, revokeProfessionalAccessWithDeps, searchCommunicationParticipantsWithDeps, purgeIndividualMemberWithDeps, purgeIndividualOrganizationWithDeps, purgeOrganizationEmployeeWithDeps, prepareEmployeeLifecycleMessageForTransport, searchIndividualLicensesWithDeps, searchIndividualLicenseOffersWithDeps, searchIndividualLicenseOrdersWithDeps, searchOrganizationLicensesWithDeps, searchOrganizationLicenseOffersWithDeps, searchOrganizationLicenseOrdersWithDeps, searchOrganizationEmployeesWithDeps, searchClinicalBundleWithDeps, searchLatestIpsWithDeps, upsertRelatedPersonAndPollWithDeps, normalizeCommunicationPathFormatSegment, addFreeOrganizationEmployeeLicensesWithDeps, requestOrganizationEmployeeLicenseOfferWithDeps, } from './resource-operations.js';
14
14
  import { listOrganizationEmployeeLifecycleWithDeps, provisionOrganizationEmployeeWithDeps, } from './organization-employee-lifecycle.js';
15
15
  import { buildRuntimeHeaders, fetchWithTimeout, parseResponseBody, postRenderedWithRuntimeConfig, } from './runtime-transport.js';
16
16
  import { activateOrganizationInGatewayFromIcaProofWithDeps, submitLegalOrganizationCredentialReissuanceWithDeps, submitLegalOrganizationVerificationTransactionWithDeps, submitOrganizationDidBindingWithDeps, } from './runtime-host-submission.js';
@@ -860,6 +860,14 @@ export class HttpRuntimeClient {
860
860
  submitAndPoll: this.submitAndPoll.bind(this),
861
861
  });
862
862
  }
863
+ /** Reads the subject-owned Consent records through Communication -> Subject/_search. */
864
+ async searchSubjectConsents(ctx, input) {
865
+ return searchSubjectConsentsWithDeps(ctx, input, {
866
+ individualCommunicationBatchPath: this.paths.individualCommunicationBatchPath.bind(this.paths),
867
+ individualCommunicationPollPath: this.paths.individualCommunicationPollPath.bind(this.paths),
868
+ submitAndPoll: this.submitAndPoll.bind(this),
869
+ });
870
+ }
863
871
  /**
864
872
  * Closes an existing professional consent by setting its period end and
865
873
  * resubmitting the updated consent resource.
@@ -19,7 +19,7 @@ import type { OrganizationLicenseOrderConfirmInput } from '../organization-licen
19
19
  import type { SmartTokenExchangeResult, SmartTokenRequestInput } from '../smart-token.js';
20
20
  import type { DigitalTwinMaterializationInput, DigitalTwinSearchInput, DigitalTwinSelectionInput } from '../digital-twin.js';
21
21
  import type { AuthorizedIndividualSubject, AuthorizedIndividualSubjectDirectoryInput } from '../authorized-subject-directory.js';
22
- import type { CommunicationIngestionInput, BlockchainArtifactRegistrationInput, CommunicationParticipantRuntimeSearchInput, ClinicalBundleSearchInput, ClinicalSectionUpdateInput, ClinicalSummaryReadResult, ClinicalSummaryRequestInput, ClinicalSummaryUpdateInput, VitalSignBatchCommunicationFromSearchResponseInput, DigitalTwinGenerationInput, DigitalTwinSecondaryUseConsentInput, DigitalTwinSecondaryUseConsentResult, DigitalTwinSubjectLinkPurgeInput, DigitalTwinSubjectLinkPurgeResult, GrantProfessionalAccessInput, GrantProfessionalAccessResult, IndividualMemberLifecycleInput, IndividualMemberLicenseAddInput, IndividualMemberLicenseInvitationInput, IndividualMemberLicenseTransitionInput, IpsOrFhirImportInput, OrganizationEmployeeCreationInput, OrganizationEmployeeLicenseAddInput, OrganizationEmployeeLicenseOfferInput, OrganizationEmployeeLicenseInvitationInput, OrganizationEmployeeLifecycleInput, OrganizationEmployeeSearchInput, ProfessionalAccessRequestInput, ProfessionalAccessRequestResult, RevokeProfessionalAccessInput, RevokeProfessionalAccessResult, RelatedPersonUpsertInput } from '../resource-operations.js';
22
+ import type { CommunicationIngestionInput, BlockchainArtifactRegistrationInput, CommunicationParticipantRuntimeSearchInput, ClinicalBundleSearchInput, ClinicalSectionUpdateInput, ClinicalSummaryReadResult, ClinicalSummaryRequestInput, ClinicalSummaryUpdateInput, VitalSignBatchCommunicationFromSearchResponseInput, DigitalTwinGenerationInput, DigitalTwinSecondaryUseConsentInput, DigitalTwinSecondaryUseConsentResult, DigitalTwinSubjectLinkPurgeInput, DigitalTwinSubjectLinkPurgeResult, GrantProfessionalAccessInput, GrantProfessionalAccessResult, IndividualMemberLifecycleInput, IndividualMemberLicenseAddInput, IndividualMemberLicenseInvitationInput, IndividualMemberLicenseTransitionInput, IpsOrFhirImportInput, OrganizationEmployeeCreationInput, OrganizationEmployeeLicenseAddInput, OrganizationEmployeeLicenseOfferInput, OrganizationEmployeeLicenseInvitationInput, OrganizationEmployeeLifecycleInput, OrganizationEmployeeSearchInput, ProfessionalAccessRequestInput, ProfessionalAccessRequestResult, RevokeProfessionalAccessInput, RevokeProfessionalAccessResult, RelatedPersonUpsertInput, SubjectConsentSearchInput } from '../resource-operations.js';
23
23
  /**
24
24
  * Shared node-runtime activation input.
25
25
  *
@@ -140,6 +140,7 @@ export type RuntimeClient = {
140
140
  generateDigitalTwinFromSubjectData?: (ctx: RouteContext, input: DigitalTwinGenerationInput) => Promise<SubmitAndPollResult>;
141
141
  setDigitalTwinSecondaryUseConsent?: (ctx: RouteContext, input: DigitalTwinSecondaryUseConsentInput) => Promise<DigitalTwinSecondaryUseConsentResult>;
142
142
  purgeDigitalTwinSubjectLink?: (ctx: RouteContext, input: DigitalTwinSubjectLinkPurgeInput) => Promise<DigitalTwinSubjectLinkPurgeResult>;
143
+ searchSubjectConsents?: (ctx: RouteContext, input: SubjectConsentSearchInput) => Promise<SubmitAndPollResult>;
143
144
  searchClinicalBundle?: (ctx: RouteContext, input: ClinicalBundleSearchInput) => Promise<SubmitAndPollResult>;
144
145
  searchDigitalTwins?: (ctx: RouteContext, input: DigitalTwinSearchInput) => Promise<SubmitAndPollResult>;
145
146
  saveDigitalTwinSelection?: (ctx: RouteContext, input: DigitalTwinSelectionInput) => Promise<SubmitAndPollResult>;
@@ -11,7 +11,9 @@ export declare class DigitalTwinSdk {
11
11
  /** Requests the SMART token used by subsequent digital-twin operations. */
12
12
  requestSmartToken(input: SmartTokenRequestInput): Promise<SmartTokenExchangeResult>;
13
13
  /**
14
- * Searches pseudonymous records and exposes matched Compositions directly.
14
+ * Searches the public `ResearchSubject/_search` aggregate with FHIR
15
+ * Parameters. Each match includes the canonical Composition GW uses
16
+ * internally to index that ResearchSubject and connect its resources.
15
17
  *
16
18
  * MVP basic search supplies `sections`, `dateFrom`, optional `dateTo`, and
17
19
  * `text`. GW resolves an omitted `dateTo` to its current time, applies OR
@@ -24,7 +26,11 @@ export declare class DigitalTwinSdk {
24
26
  * The canonical clinical twin is never modified.
25
27
  */
26
28
  saveSelection(ctx: RouteContext, input: DigitalTwinSelectionInput): Promise<SubmitAndPollResult>;
27
- /** Reopens only this employee's saved selections for one exact custom tag. */
29
+ /**
30
+ * Reopens this employee's saved ResearchSubjects through the same public
31
+ * route. The tag is combined with an author filter bound to the actor DID;
32
+ * GW verifies that author against the authenticated SMART `sub`.
33
+ */
28
34
  searchSelections(ctx: RouteContext, input: Omit<DigitalTwinSearchInput, 'filters'> & {
29
35
  section: string;
30
36
  tag: Readonly<{
@@ -16,11 +16,18 @@ export class DigitalTwinSdk {
16
16
  throw new Error('DigitalTwinSdk actorDid must match the actor session.');
17
17
  }
18
18
  const actorDid = this.researcherDid || requestedActorDid;
19
+ const requestedScopes = input.scopes?.length
20
+ ? input.scopes
21
+ : [ServiceCapability.DigitalTwinReader];
19
22
  const result = await requireClientMethod(this.client, 'requestSmartToken')({
20
23
  ...input,
21
24
  actorDid,
22
25
  purpose: input.purpose || HealthcareConsentPurposes.Research,
23
- scopes: input.scopes?.length ? input.scopes : [ServiceCapability.DigitalTwinReader],
26
+ // GW root scopes are explicit queries. Complete the common bare SDK
27
+ // capability so callers do not have to know the SMART wire syntax.
28
+ scopes: requestedScopes.map((scope) => scope === ServiceCapability.DigitalTwinReader
29
+ ? `${scope}?subject=*`
30
+ : scope),
24
31
  });
25
32
  if (result.accessToken)
26
33
  this.smartAccessToken = result.accessToken;
@@ -29,7 +36,9 @@ export class DigitalTwinSdk {
29
36
  return result;
30
37
  }
31
38
  /**
32
- * Searches pseudonymous records and exposes matched Compositions directly.
39
+ * Searches the public `ResearchSubject/_search` aggregate with FHIR
40
+ * Parameters. Each match includes the canonical Composition GW uses
41
+ * internally to index that ResearchSubject and connect its resources.
33
42
  *
34
43
  * MVP basic search supplies `sections`, `dateFrom`, optional `dateTo`, and
35
44
  * `text`. GW resolves an omitted `dateTo` to its current time, applies OR
@@ -59,6 +68,7 @@ export class DigitalTwinSdk {
59
68
  }
60
69
  const operation = await requireClientMethod(this.client, 'searchDigitalTwins')(ctx, {
61
70
  ...input,
71
+ resourceType: 'ResearchSubject',
62
72
  filters,
63
73
  accessToken: input.accessToken || this.smartAccessToken,
64
74
  });
@@ -80,7 +90,11 @@ export class DigitalTwinSdk {
80
90
  accessToken: input.accessToken || this.smartAccessToken,
81
91
  });
82
92
  }
83
- /** Reopens only this employee's saved selections for one exact custom tag. */
93
+ /**
94
+ * Reopens this employee's saved ResearchSubjects through the same public
95
+ * route. The tag is combined with an author filter bound to the actor DID;
96
+ * GW verifies that author against the authenticated SMART `sub`.
97
+ */
84
98
  searchSelections(ctx, input) {
85
99
  const section = String(input.section || '').trim();
86
100
  const system = String(input.tag?.system || '').trim();
@@ -138,7 +138,11 @@ export declare class IndividualControllerSdk {
138
138
  * data already shared for research.
139
139
  */
140
140
  purgeDigitalTwinSubjectLink(ctx: RouteContext, input: DigitalTwinSubjectLinkPurgeInput): Promise<DigitalTwinSubjectLinkPurgeResult>;
141
- /** Returns the current decision for one portal, software or research study. */
141
+ /**
142
+ * Returns the current decision for one portal, software or research study.
143
+ * Consent is read through Communication -> individual `Subject/_search`;
144
+ * this does not query a clinical Bundle or the twin ResearchSubject index.
145
+ */
142
146
  getDigitalTwinSecondaryUseConsentStatus(ctx: RouteContext, input: Readonly<{
143
147
  subjectDid: string;
144
148
  indexProviderOrganizationDid: string;
@@ -208,7 +208,11 @@ export class IndividualControllerSdk {
208
208
  assertFacadeCapability(this.capabilities, ActorCapabilities.IndividualGenerateDigitalTwin, ActorKinds.IndividualController, 'purgeDigitalTwinSubjectLink');
209
209
  return requireClientMethod(this.client, 'purgeDigitalTwinSubjectLink')(ctx, input);
210
210
  }
211
- /** Returns the current decision for one portal, software or research study. */
211
+ /**
212
+ * Returns the current decision for one portal, software or research study.
213
+ * Consent is read through Communication -> individual `Subject/_search`;
214
+ * this does not query a clinical Bundle or the twin ResearchSubject index.
215
+ */
212
216
  async getDigitalTwinSecondaryUseConsentStatus(ctx, input) {
213
217
  assertFacadeCapability(this.capabilities, ActorCapabilities.IndividualGenerateDigitalTwin, ActorKinds.IndividualController, 'getDigitalTwinSecondaryUseConsentStatus');
214
218
  const consents = await new GatewayActiveConsentProvider(this.client, ctx)
@@ -0,0 +1,16 @@
1
+ import type { VerifiableCredentialV2 } from 'gdc-common-utils-ts/models/verifiable-credential';
2
+ import type { IWallet, WalletExecutionContext, WalletKeySelection } from 'gdc-sdk-core-ts';
3
+ /** Input required to add one human-authorizer proof to the out-of-band VC. */
4
+ export type SignOrganizationRegistrationAuthorizationInput = Readonly<{
5
+ credential: VerifiableCredentialV2;
6
+ wallet: IWallet;
7
+ context: WalletExecutionContext;
8
+ key: WalletKeySelection;
9
+ verificationMethod: string;
10
+ createdAt?: string;
11
+ }>;
12
+ /**
13
+ * Adds one detached JWS proof made by the already-unlocked professional
14
+ * profile. The browser never receives the private key or clear wallet seed.
15
+ */
16
+ export declare function signOrganizationRegistrationAuthorizationCredential(input: SignOrganizationRegistrationAuthorizationInput): Promise<VerifiableCredentialV2>;
@@ -0,0 +1,34 @@
1
+ import { canonicalizeOrganizationRegistrationAuthorizationCredential, } from 'gdc-common-utils-ts/utils/organization-registration-authorization';
2
+ /**
3
+ * Adds one detached JWS proof made by the already-unlocked professional
4
+ * profile. The browser never receives the private key or clear wallet seed.
5
+ */
6
+ export async function signOrganizationRegistrationAuthorizationCredential(input) {
7
+ if (!input.wallet.signDetachedJws) {
8
+ throw new Error('Wallet does not support detached organization authorization proofs.');
9
+ }
10
+ const verificationMethod = String(input.verificationMethod || '').trim();
11
+ if (!verificationMethod)
12
+ throw new Error('Organization authorization proof requires verificationMethod.');
13
+ const payload = canonicalizeOrganizationRegistrationAuthorizationCredential(input.credential);
14
+ const jws = await input.wallet.signDetachedJws(input.context, {
15
+ payload,
16
+ header: { typ: 'application/vc+ld+json' },
17
+ key: input.key,
18
+ });
19
+ const existingProofs = Array.isArray(input.credential.proof)
20
+ ? input.credential.proof
21
+ : input.credential.proof
22
+ ? [input.credential.proof]
23
+ : [];
24
+ return {
25
+ ...input.credential,
26
+ proof: [...existingProofs, {
27
+ type: 'JsonWebSignature2020',
28
+ created: input.createdAt || new Date().toISOString(),
29
+ proofPurpose: 'contractAgreement',
30
+ verificationMethod,
31
+ jws,
32
+ }],
33
+ };
34
+ }
@@ -0,0 +1,37 @@
1
+ export type PortableProfileRecoveryEnvelope = Readonly<{
2
+ version: 'gdc-portable-profile-recovery-v1';
3
+ profileId: string;
4
+ walletKeyDerivationId: string;
5
+ kdf: Readonly<{
6
+ name: 'scrypt';
7
+ saltBase64Url: string;
8
+ cost: number;
9
+ blockSize: number;
10
+ parallelization: number;
11
+ keyLength: 32;
12
+ }>;
13
+ cipher: 'A256GCM';
14
+ ivBase64Url: string;
15
+ ciphertextBase64Url: string;
16
+ tagBase64Url: string;
17
+ }>;
18
+ export type PortableProfileRecoveryProtection = Readonly<{
19
+ cost?: number;
20
+ blockSize?: number;
21
+ parallelization?: number;
22
+ }>;
23
+ export declare function createPortableProfileRecoveryEnvelope(input: Readonly<{
24
+ profileId: string;
25
+ walletSeed: string;
26
+ walletKeyDerivationId: string;
27
+ recoverySecret: string;
28
+ protection?: PortableProfileRecoveryProtection;
29
+ }>): PortableProfileRecoveryEnvelope;
30
+ export declare function openPortableProfileRecoveryEnvelope(input: Readonly<{
31
+ envelope: PortableProfileRecoveryEnvelope;
32
+ recoverySecret: string;
33
+ }>): Readonly<{
34
+ profileId: string;
35
+ walletSeed: string;
36
+ walletKeyDerivationId: string;
37
+ }>;
@@ -0,0 +1,113 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
3
+ export function createPortableProfileRecoveryEnvelope(input) {
4
+ const profileId = requiredText(input.profileId, 'profileId');
5
+ const walletKeyDerivationId = requiredText(input.walletKeyDerivationId, 'walletKeyDerivationId');
6
+ requireSeed(input.walletSeed);
7
+ requireRecoverySecret(input.recoverySecret);
8
+ const kdf = {
9
+ name: 'scrypt',
10
+ saltBase64Url: randomBytes(16).toString('base64url'),
11
+ cost: input.protection?.cost ?? 65536,
12
+ blockSize: input.protection?.blockSize ?? 8,
13
+ parallelization: input.protection?.parallelization ?? 1,
14
+ keyLength: 32,
15
+ };
16
+ const key = deriveKey(input.recoverySecret, kdf);
17
+ const iv = randomBytes(12);
18
+ const aad = envelopeAad(profileId, walletKeyDerivationId);
19
+ try {
20
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
21
+ cipher.setAAD(aad);
22
+ const ciphertext = Buffer.concat([
23
+ cipher.update(JSON.stringify({ walletSeed: input.walletSeed }), 'utf8'),
24
+ cipher.final(),
25
+ ]);
26
+ return {
27
+ version: 'gdc-portable-profile-recovery-v1',
28
+ profileId,
29
+ walletKeyDerivationId,
30
+ kdf,
31
+ cipher: 'A256GCM',
32
+ ivBase64Url: iv.toString('base64url'),
33
+ ciphertextBase64Url: ciphertext.toString('base64url'),
34
+ tagBase64Url: cipher.getAuthTag().toString('base64url'),
35
+ };
36
+ }
37
+ finally {
38
+ key.fill(0);
39
+ }
40
+ }
41
+ export function openPortableProfileRecoveryEnvelope(input) {
42
+ const envelope = input.envelope;
43
+ try {
44
+ validateEnvelope(envelope);
45
+ requireRecoverySecret(input.recoverySecret);
46
+ const key = deriveKey(input.recoverySecret, envelope.kdf);
47
+ try {
48
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.ivBase64Url, 'base64url'));
49
+ decipher.setAAD(envelopeAad(envelope.profileId, envelope.walletKeyDerivationId));
50
+ decipher.setAuthTag(Buffer.from(envelope.tagBase64Url, 'base64url'));
51
+ const cleartext = Buffer.concat([
52
+ decipher.update(Buffer.from(envelope.ciphertextBase64Url, 'base64url')),
53
+ decipher.final(),
54
+ ]).toString('utf8');
55
+ const payload = JSON.parse(cleartext);
56
+ const walletSeed = String(payload.walletSeed || '');
57
+ requireSeed(walletSeed);
58
+ return {
59
+ profileId: envelope.profileId,
60
+ walletSeed,
61
+ walletKeyDerivationId: envelope.walletKeyDerivationId,
62
+ };
63
+ }
64
+ finally {
65
+ key.fill(0);
66
+ }
67
+ }
68
+ catch {
69
+ throw new Error('Portable profile recovery envelope could not be opened.');
70
+ }
71
+ }
72
+ function deriveKey(secret, kdf) {
73
+ return scryptSync(secret, Buffer.from(kdf.saltBase64Url, 'base64url'), kdf.keyLength, {
74
+ N: kdf.cost,
75
+ r: kdf.blockSize,
76
+ p: kdf.parallelization,
77
+ maxmem: Math.max(128 * 1024 * 1024, 256 * kdf.cost * kdf.blockSize),
78
+ });
79
+ }
80
+ function envelopeAad(profileId, walletKeyDerivationId) {
81
+ return Buffer.from(JSON.stringify({
82
+ version: 'gdc-portable-profile-recovery-v1',
83
+ profileId,
84
+ walletKeyDerivationId,
85
+ }));
86
+ }
87
+ function requireRecoverySecret(value) {
88
+ const secret = String(value || '');
89
+ if (secret.length < 16 || /^\d+$/.test(secret)) {
90
+ throw new Error('Portable recovery requires a high-entropy recovery secret, not a short PIN.');
91
+ }
92
+ }
93
+ function requireSeed(value) {
94
+ if (!/^[A-Za-z0-9_-]{43}$/.test(value) || Buffer.from(value, 'base64url').byteLength !== 32) {
95
+ throw new Error('Portable recovery requires a 32-byte base64url wallet seed.');
96
+ }
97
+ }
98
+ function requiredText(value, name) {
99
+ const normalized = String(value || '').trim();
100
+ if (!normalized)
101
+ throw new Error(`Portable recovery requires ${name}.`);
102
+ return normalized;
103
+ }
104
+ function validateEnvelope(value) {
105
+ if (value?.version !== 'gdc-portable-profile-recovery-v1'
106
+ || value.cipher !== 'A256GCM'
107
+ || value.kdf?.name !== 'scrypt'
108
+ || value.kdf.keyLength !== 32
109
+ || !value.profileId
110
+ || !value.walletKeyDerivationId) {
111
+ throw new Error('Unsupported portable profile recovery envelope.');
112
+ }
113
+ }
@@ -532,6 +532,14 @@ export type DigitalTwinSubjectLinkPurgeInput = {
532
532
  };
533
533
  /** Async GW result for a subject/twin binding purge. */
534
534
  export type DigitalTwinSubjectLinkPurgeResult = SubmitAndPollResult;
535
+ /** Input for reading the subject-owned Consent records through its individual aggregate. */
536
+ export type SubjectConsentSearchInput = Readonly<{
537
+ subject: string;
538
+ pollOptions?: {
539
+ timeoutMs?: number;
540
+ intervalMs?: number;
541
+ };
542
+ }>;
535
543
  export declare function createOrganizationEmployeeWithDeps(routeCtx: RouteContext, input: OrganizationEmployeeCreationInput, options: {
536
544
  timeoutMs?: number;
537
545
  intervalMs?: number;
@@ -953,6 +961,22 @@ export declare function purgeDigitalTwinSubjectLinkWithDeps(routeCtx: RouteConte
953
961
  intervalMs?: number;
954
962
  }) => Promise<SubmitAndPollResult>;
955
963
  }): Promise<DigitalTwinSubjectLinkPurgeResult>;
964
+ /**
965
+ * Reads Consent through the individual `Subject/_search` aggregate carried by
966
+ * an auditable Communication. `Subject` is the individual aggregate; it is
967
+ * intentionally separate from the public digital-twin `ResearchSubject`
968
+ * aggregate, whose canonical index document is a Composition.
969
+ */
970
+ export declare function searchSubjectConsentsWithDeps(routeCtx: RouteContext, input: SubjectConsentSearchInput, deps: {
971
+ individualCommunicationBatchPath: (ctx: RouteContext, pathFormatSegment: string) => string;
972
+ individualCommunicationPollPath: (ctx: RouteContext, pathFormatSegment: string) => string;
973
+ submitAndPoll: (submitPath: string, pollPath: string, payload: {
974
+ thid?: string;
975
+ } & Record<string, unknown>, pollOptions?: {
976
+ timeoutMs?: number;
977
+ intervalMs?: number;
978
+ }) => Promise<SubmitAndPollResult>;
979
+ }): Promise<SubmitAndPollResult>;
956
980
  export declare function revokeProfessionalAccessWithDeps(routeCtx: RouteContext, input: RevokeProfessionalAccessInput, deps: {
957
981
  individualConsentR4BatchPath: (ctx: RouteContext) => string;
958
982
  individualConsentR4PollPath: (ctx: RouteContext) => string;
@@ -865,6 +865,50 @@ export async function purgeDigitalTwinSubjectLinkWithDeps(routeCtx, input, deps)
865
865
  },
866
866
  }, input.pollOptions);
867
867
  }
868
+ /**
869
+ * Reads Consent through the individual `Subject/_search` aggregate carried by
870
+ * an auditable Communication. `Subject` is the individual aggregate; it is
871
+ * intentionally separate from the public digital-twin `ResearchSubject`
872
+ * aggregate, whose canonical index document is a Composition.
873
+ */
874
+ export async function searchSubjectConsentsWithDeps(routeCtx, input, deps) {
875
+ const subject = String(input.subject || '').trim();
876
+ if (!subject.startsWith('did:')) {
877
+ throw new Error('Consent search requires a subject DID.');
878
+ }
879
+ const format = 'org.hl7.fhir.r4';
880
+ const parameters = {
881
+ resourceType: 'Parameters',
882
+ parameter: [{ name: 'subject', valueString: subject }],
883
+ };
884
+ const thid = `subject-consent-search-${createRuntimeUuid()}`;
885
+ const communication = {
886
+ resourceType: 'Communication',
887
+ status: 'completed',
888
+ subject: { reference: subject },
889
+ payload: [{
890
+ contentReference: {
891
+ reference: 'individual/org.hl7.fhir.api/Subject/_search',
892
+ },
893
+ contentAttachment: {
894
+ contentType: 'application/fhir+json',
895
+ title: 'subject-consent-search-parameters.json',
896
+ data: Buffer.from(JSON.stringify(parameters), 'utf8').toString('base64'),
897
+ },
898
+ }],
899
+ };
900
+ return deps.submitAndPoll(deps.individualCommunicationBatchPath(routeCtx, format), deps.individualCommunicationPollPath(routeCtx, format), {
901
+ thid,
902
+ body: {
903
+ resourceType: 'Bundle',
904
+ type: 'batch',
905
+ entry: [{
906
+ request: { method: 'POST', url: 'Communication' },
907
+ resource: communication,
908
+ }],
909
+ },
910
+ }, input.pollOptions);
911
+ }
868
912
  export async function revokeProfessionalAccessWithDeps(routeCtx, input, deps) {
869
913
  const revokedClaims = ConsentClaims
870
914
  .fromClaims(input.consentClaims)
@@ -3,6 +3,10 @@ import type { ActorKind } from 'gdc-common-utils-ts/models/actor-session';
3
3
  import type { LegalOrganizationVerificationTransactionInput } from 'gdc-common-utils-ts/utils/legal-organization-verification-transaction';
4
4
  import { type AppInfo, type ConfidentialStorageProfile, type PollOptions, type SubmitAndPollResult } from 'gdc-sdk-core-ts';
5
5
  import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
6
+ import { ProfessionalSdk } from './orchestration/professional-sdk.js';
7
+ import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
8
+ import type { SmartTokenExchangeResult } from './smart-token.js';
9
+ import type { DigitalTwinSearchInput, DigitalTwinSearchResult } from './digital-twin.js';
6
10
  import type { RouteContext } from './individual-onboarding.js';
7
11
  import type { HostRouteContext } from './host-onboarding.js';
8
12
  import type { SecureDidcommTransportAdapter } from 'gdc-sdk-core-ts';
@@ -70,6 +74,15 @@ export type ServerEmployeeProfileOtpRotationInput = Readonly<{
70
74
  clientName: string;
71
75
  }>;
72
76
  export type ServerEmployeeProfileOtpRotationResult = ServerProfileRecord;
77
+ export type ServerProfilePinReplacementInput = Readonly<{
78
+ ownerId: string;
79
+ profileId: string;
80
+ /** Server-authorized seed obtained from a passkey, active wallet session or recovery envelope. */
81
+ authorizedWalletSeed: string;
82
+ /** Required when the durable profile still carries a separately protected legacy VP. */
83
+ authorizedVpToken?: string;
84
+ newPin: string;
85
+ }>;
73
86
  export type ServerProfileEnrollmentInput = Readonly<{
74
87
  /** Stable confidential id of the authenticated portal account that owns and lists this profile. */
75
88
  ownerId: string;
@@ -228,6 +241,47 @@ export type OpenedServerOrganizationController = Readonly<{
228
241
  profile: ServerProfileRecord;
229
242
  sdk: OrganizationControllerSdk;
230
243
  }>;
244
+ /** Server-owned role evidence used to sign a fresh professional VP. */
245
+ export type ServerProfessionalProofInput = Readonly<{
246
+ role: string;
247
+ email?: string;
248
+ sameAs?: string | readonly string[];
249
+ telephone?: string;
250
+ credentialMaterial?: string;
251
+ }>;
252
+ /** Reopens one enrolled employee/professional profile behind the BFF boundary. */
253
+ export type ServerProfessionalOpenInput = Readonly<{
254
+ ownerId: string;
255
+ profileId: string;
256
+ /** Fresh signed OIDC id_token proving the authenticated account/email. */
257
+ idToken: string;
258
+ /** Role evidence signed by the managed DCR wallet, independently of idToken. */
259
+ professionalProof: ServerProfessionalProofInput;
260
+ /** Manager-owned PIN path; mutually exclusive with authorizedWalletSeed. */
261
+ pin?: string;
262
+ /** Server-only seed already authorized by a product passkey/session policy. */
263
+ authorizedWalletSeed?: string;
264
+ }>;
265
+ /** Business authorization requested from SMART; OpenID/JWT fields stay SDK-owned. */
266
+ export type ServerProfessionalSmartTokenInput = Readonly<{
267
+ subjectDid?: string;
268
+ purpose?: string;
269
+ scopes: string[];
270
+ acrValues?: string;
271
+ requestBodyClaims?: Record<string, unknown>;
272
+ tokenCacheKey?: string;
273
+ timeoutSeconds?: number;
274
+ intervalSeconds?: number;
275
+ }>;
276
+ /** Role-scoped professional facades with managed SMART proof methods. */
277
+ export type OpenedServerProfessional = Readonly<{
278
+ profile: ServerProfileRecord;
279
+ sdk: ProfessionalSdk;
280
+ digitalTwin: DigitalTwinSdk;
281
+ requestSmartToken(input: ServerProfessionalSmartTokenInput): Promise<SmartTokenExchangeResult>;
282
+ requestDigitalTwinSmartToken(input: ServerProfessionalSmartTokenInput): Promise<SmartTokenExchangeResult>;
283
+ searchDigitalTwins(input: Omit<DigitalTwinSearchInput, 'accessToken'>): Promise<DigitalTwinSearchResult>;
284
+ }>;
231
285
  export type ServerProfileSessionManagerOptions = Readonly<{
232
286
  store: ServerProfileStore;
233
287
  sealer: ServerProfileSealer;
@@ -290,6 +344,12 @@ export declare class ServerProfileSessionManager {
290
344
  /** @deprecated Use `submitLegalOrganizationCredentialReissuanceWithBootstrapWallet`. */
291
345
  submitLegalOrganizationIssueWithBootstrapWallet(input: ServerProfileOrganizationIssueInput): Promise<SubmitAndPollResult>;
292
346
  listProfiles(ownerId: string): Promise<ServerProfileRecord[]>;
347
+ /**
348
+ * Rewraps the exact registered wallet under a new PIN after a separate
349
+ * server-authorized recovery factor has released its secrets. No GW call or
350
+ * DCR occurs because proof-of-possession and every public key remain intact.
351
+ */
352
+ replaceProfilePinFromAuthorizedSecrets(input: ServerProfilePinReplacementInput): Promise<ServerProfileRecord>;
293
353
  unlock(input: ServerProfileUnlockInput): Promise<ResolvedServerProfileSession>;
294
354
  /**
295
355
  * Renews the short SMART bearer while retaining the already-open server
@@ -307,6 +367,16 @@ export declare class ServerProfileSessionManager {
307
367
  * registered for that profile before any gateway request can be sent.
308
368
  */
309
369
  openOrganizationController(input: ServerOrganizationControllerOpenInput): Promise<OpenedServerOrganizationController>;
370
+ /**
371
+ * Opens one registered organization employee/professional without exposing
372
+ * wallet reconstruction, private_key_jwt, VP or transport plumbing.
373
+ *
374
+ * The product authorizes the PIN/passkey/session and supplies business role
375
+ * evidence. The SDK revalidates the registered key set, signs a fresh role
376
+ * VP independently from the OIDC account proof, derives the SMART endpoint
377
+ * from the durable profile route and owns the client assertion.
378
+ */
379
+ openProfessional(input: ServerProfessionalOpenInput): Promise<OpenedServerProfessional>;
310
380
  lock(ownerId: string, sessionId: string): Promise<void>;
311
381
  private createClient;
312
382
  private createWallet;
@@ -7,6 +7,8 @@ import { TransportProfiles, } from 'gdc-sdk-core-ts';
7
7
  import { NodeManagedWallet } from './node-managed-wallet.js';
8
8
  import { NodeHttpClient } from './node-runtime-client.js';
9
9
  import { OrganizationControllerSdk } from './orchestration/organization-controller-sdk.js';
10
+ import { ProfessionalSdk } from './orchestration/professional-sdk.js';
11
+ import { DigitalTwinSdk } from './orchestration/digital-twin-sdk.js';
10
12
  import { buildIdentityOpenIdSmartTokenPath } from './runtime-paths.js';
11
13
  import { createProfileDeviceActivationRequest } from './device-activation.js';
12
14
  import { ProfilePinRejectedError, openServerProfileSecret, protectServerProfileSecret, } from './server-profile-protection.js';
@@ -254,6 +256,38 @@ export class ServerProfileSessionManager {
254
256
  listProfiles(ownerId) {
255
257
  return this.options.store.listProfiles(ownerId);
256
258
  }
259
+ /**
260
+ * Rewraps the exact registered wallet under a new PIN after a separate
261
+ * server-authorized recovery factor has released its secrets. No GW call or
262
+ * DCR occurs because proof-of-possession and every public key remain intact.
263
+ */
264
+ async replaceProfilePinFromAuthorizedSecrets(input) {
265
+ const profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
266
+ const seed = String(input.authorizedWalletSeed || '').trim();
267
+ requireBase64UrlSeed32(seed);
268
+ if (!this.options.store.deleteSessionsForProfile) {
269
+ throw new Error('Profile PIN replacement requires profile-session invalidation support.');
270
+ }
271
+ if (profile.protectedVpToken && !String(input.authorizedVpToken || '').trim()) {
272
+ throw new Error('Profile PIN replacement requires the authorized VP token.');
273
+ }
274
+ const walletKeyDerivationId = normalizedWalletKeyDerivationId(profile.walletKeyDerivationId, profile.profileId);
275
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
276
+ await requireRegisteredProfileKeys(wallet, walletContext(walletKeyDerivationId), profile);
277
+ const updated = {
278
+ ...profile,
279
+ protectedWalletSeed: await protectServerProfileSecret(seed, input.newPin, `${profile.profileId}:wallet-seed`, this.options.sealer, this.options.profileProtection),
280
+ ...(profile.protectedVpToken ? {
281
+ protectedVpToken: await protectServerProfileSecret(String(input.authorizedVpToken), input.newPin, `${profile.profileId}:vp-token`, this.options.sealer, this.options.profileProtection),
282
+ } : {}),
283
+ failedUnlocks: 0,
284
+ lockedUntil: undefined,
285
+ updatedAt: this.now().toISOString(),
286
+ };
287
+ await this.options.store.putProfile(updated);
288
+ await this.options.store.deleteSessionsForProfile(profile.profileId);
289
+ return updated;
290
+ }
257
291
  async unlock(input) {
258
292
  let profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
259
293
  this.requireSubject(profile, input.subjectDid);
@@ -453,6 +487,115 @@ export class ServerProfileSessionManager {
453
487
  });
454
488
  return { profile, sdk: new OrganizationControllerSdk(client) };
455
489
  }
490
+ /**
491
+ * Opens one registered organization employee/professional without exposing
492
+ * wallet reconstruction, private_key_jwt, VP or transport plumbing.
493
+ *
494
+ * The product authorizes the PIN/passkey/session and supplies business role
495
+ * evidence. The SDK revalidates the registered key set, signs a fresh role
496
+ * VP independently from the OIDC account proof, derives the SMART endpoint
497
+ * from the durable profile route and owns the client assertion.
498
+ */
499
+ async openProfessional(input) {
500
+ const profile = await this.requireOwnedProfile(input.ownerId, input.profileId);
501
+ const isProfessional = profile.actorKind === ActorKinds.OrganizationEmployee
502
+ || profile.actorKind === ActorKinds.Professional;
503
+ if (!isProfessional || profile.actorMode !== 'member') {
504
+ throw new Error('Profile is not an organization employee or professional.');
505
+ }
506
+ const idToken = String(input.idToken || '').trim();
507
+ const role = String(input.professionalProof?.role || '').trim();
508
+ if (!idToken)
509
+ throw new Error('Opening a professional requires a signed OIDC idToken.');
510
+ if (!role)
511
+ throw new Error('Opening a professional requires role proof.');
512
+ const hasPin = Boolean(String(input.pin || '').trim());
513
+ const hasAuthorizedSeed = Boolean(String(input.authorizedWalletSeed || '').trim());
514
+ if (hasPin === hasAuthorizedSeed) {
515
+ throw new Error('Opening a professional requires exactly one of pin or authorizedWalletSeed.');
516
+ }
517
+ const seed = hasAuthorizedSeed
518
+ ? String(input.authorizedWalletSeed)
519
+ : await openServerProfileSecret(profile.protectedWalletSeed, String(input.pin), `${profile.profileId}:wallet-seed`, this.options.sealer);
520
+ requireBase64UrlSeed32(seed);
521
+ const walletKeyDerivationId = normalizedWalletKeyDerivationId(profile.walletKeyDerivationId, profile.profileId);
522
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
523
+ const context = walletContext(walletKeyDerivationId);
524
+ await requireRegisteredProfileKeys(wallet, context, profile);
525
+ const secureTransportAdapter = {
526
+ pack: (message) => wallet.packForRecipientWithContext(bindOrganizationControllerTransport(message, profile), profile.providerDid, { context }),
527
+ unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
528
+ };
529
+ const operationalClient = new NodeHttpClient({
530
+ baseUrl: this.options.gatewayBaseUrl,
531
+ ctx: profile.routeContext,
532
+ bearerToken: idToken,
533
+ fetchImpl: this.options.fetchImpl,
534
+ appInfo: this.options.appInfo,
535
+ transportProfile: TransportProfiles.DidcommEncryptedForm,
536
+ secureTransportAdapter,
537
+ });
538
+ // SMART/OpenID uses its own JSON/form protocol and must not be wrapped in
539
+ // the encrypted resource transport selected for later business actions.
540
+ const sdk = new ProfessionalSdk(this.createClient(profile.routeContext, idToken));
541
+ const digitalTwin = new DigitalTwinSdk(operationalClient, profile.actorDid);
542
+ let digitalTwinAccessToken;
543
+ const request = async (smart) => {
544
+ if (!smart.scopes?.length)
545
+ throw new Error('Professional SMART request requires scopes.');
546
+ const now = this.now();
547
+ const audience = [
548
+ this.options.gatewayBaseUrl.replace(/\/+$/, ''),
549
+ buildIdentityOpenIdSmartTokenPath(profile.routeContext),
550
+ ].join('');
551
+ const clientAssertion = await buildWalletClientAssertion(wallet, profile, audience, now);
552
+ const vpToken = await buildManagedProfessionalVp(wallet, context, {
553
+ clientId: profile.clientId,
554
+ actorDid: profile.actorDid,
555
+ profileDid: profile.profileDid,
556
+ ...input.professionalProof,
557
+ role,
558
+ });
559
+ return sdk.requestSmartToken({
560
+ ...profile.routeContext,
561
+ actorDid: profile.actorDid,
562
+ subjectDid: String(smart.subjectDid || '').trim() || undefined,
563
+ clientId: profile.clientId,
564
+ issuer: profile.clientId,
565
+ audience,
566
+ idToken,
567
+ vpToken,
568
+ vpTokenFallback: 'omit',
569
+ clientAssertion,
570
+ clientAssertionType: 'private_key_jwt',
571
+ smartTokenKind: 'openid-smart',
572
+ acrValues: smart.acrValues || profileSmartAcrValues(profile.actorKind),
573
+ purpose: smart.purpose,
574
+ scopes: unique(smart.scopes),
575
+ requestBodyClaims: smart.requestBodyClaims,
576
+ tokenCacheKey: smart.tokenCacheKey,
577
+ timeoutSeconds: smart.timeoutSeconds,
578
+ intervalSeconds: smart.intervalSeconds,
579
+ });
580
+ };
581
+ return {
582
+ profile,
583
+ sdk,
584
+ digitalTwin,
585
+ requestSmartToken: request,
586
+ requestDigitalTwinSmartToken: async (smart) => {
587
+ const result = await request(smart);
588
+ if (result.accessToken)
589
+ digitalTwinAccessToken = result.accessToken;
590
+ return result;
591
+ },
592
+ searchDigitalTwins: (search) => {
593
+ if (!digitalTwinAccessToken)
594
+ throw new Error('Digital twin SMART token has not been granted.');
595
+ return digitalTwin.search(profile.routeContext, { ...search, accessToken: digitalTwinAccessToken });
596
+ },
597
+ };
598
+ }
456
599
  async lock(ownerId, sessionId) {
457
600
  const session = await this.options.store.getSession(sessionId);
458
601
  if (session?.ownerId === ownerId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.4.41",
3
+ "version": "2.4.44",
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.",