gdc-sdk-node-ts 2.4.34 → 2.4.36

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.
@@ -10,6 +10,9 @@ export declare function assertDigitalTwinSubjectId(value: unknown): asserts valu
10
10
  /** Search parameters added by the digital-twin Composition profile. */
11
11
  export declare const DigitalTwinSearchParameter: Readonly<{
12
12
  Section: "section";
13
+ DateFrom: "date-from";
14
+ DateTo: "date-to";
15
+ Text: "text";
13
16
  MetaTag: "Composition.meta-tag";
14
17
  }>;
15
18
  export type DigitalTwinSearchInput = {
@@ -18,6 +21,15 @@ export type DigitalTwinSearchInput = {
18
21
  thid?: string;
19
22
  format?: DigitalTwinFhirFormat;
20
23
  resourceType?: string;
24
+ /** One or more IPS section tokens. Basic search uses OR across sections. */
25
+ sections?: readonly string[];
26
+ /** Inclusive clinical-event lower bound as ISO date or dateTime. */
27
+ dateFrom?: string;
28
+ /** Inclusive upper bound. When omitted, GW resolves its current time. */
29
+ dateTo?: string;
30
+ /** Case- and accent-insensitive text matched against GW's private derived index. */
31
+ text?: string;
32
+ /** Advanced/compatibility filters. Do not combine with basic-search fields. */
21
33
  filters?: Readonly<Record<string, string | readonly string[] | undefined>>;
22
34
  pollOptions?: PollOptions;
23
35
  };
@@ -16,6 +16,9 @@ export function assertDigitalTwinSubjectId(value) {
16
16
  /** Search parameters added by the digital-twin Composition profile. */
17
17
  export const DigitalTwinSearchParameter = Object.freeze({
18
18
  Section: 'section',
19
+ DateFrom: 'date-from',
20
+ DateTo: 'date-to',
21
+ Text: 'text',
19
22
  MetaTag: 'Composition.meta-tag',
20
23
  });
21
24
  /**
@@ -132,10 +135,42 @@ export async function searchDigitalTwinsWithDeps(ctx, input, deps) {
132
135
  const resourceType = String(input.resourceType || 'Composition').trim();
133
136
  if (!resourceType)
134
137
  throw new Error('Digital twin resourceType is required.');
135
- const parameters = Object.entries(input.filters || {}).flatMap(([name, value]) => {
138
+ const usesBasicSearch = Boolean(input.sections || input.dateFrom || input.dateTo || input.text);
139
+ if (usesBasicSearch && Object.keys(input.filters || {}).length > 0) {
140
+ throw new Error('Digital twin basic search cannot be combined with advanced filters.');
141
+ }
142
+ const sections = (input.sections || []).map((value) => String(value || '').trim()).filter(Boolean);
143
+ const dateFrom = String(input.dateFrom || '').trim();
144
+ const dateTo = String(input.dateTo || '').trim();
145
+ const searchText = String(input.text || '').trim();
146
+ if (usesBasicSearch) {
147
+ if (sections.length === 0)
148
+ throw new Error('Digital twin basic search requires at least one section.');
149
+ if (!dateFrom)
150
+ throw new Error('Digital twin basic search requires dateFrom.');
151
+ if (!searchText)
152
+ throw new Error('Digital twin basic search requires text.');
153
+ const fromMs = Date.parse(dateFrom);
154
+ const toMs = dateTo ? Date.parse(dateTo) : undefined;
155
+ if (Number.isNaN(fromMs))
156
+ throw new Error('Digital twin basic search dateFrom must be an ISO date or dateTime.');
157
+ if (dateTo && Number.isNaN(toMs))
158
+ throw new Error('Digital twin basic search dateTo must be an ISO date or dateTime.');
159
+ if (toMs !== undefined && toMs < fromMs)
160
+ throw new Error('Digital twin basic search dateTo must be on or after dateFrom.');
161
+ }
162
+ const advancedParameters = Object.entries(input.filters || {}).flatMap(([name, value]) => {
136
163
  const values = Array.isArray(value) ? value : value === undefined ? [] : [value];
137
164
  return values.map((item) => ({ name, valueString: String(item) }));
138
165
  });
166
+ const parameters = usesBasicSearch
167
+ ? [
168
+ ...sections.map((section) => ({ name: DigitalTwinSearchParameter.Section, valueString: section })),
169
+ { name: DigitalTwinSearchParameter.DateFrom, valueDate: dateFrom },
170
+ ...(dateTo ? [{ name: DigitalTwinSearchParameter.DateTo, valueDate: dateTo }] : []),
171
+ { name: DigitalTwinSearchParameter.Text, valueString: searchText },
172
+ ]
173
+ : advancedParameters;
139
174
  const thid = String(input.thid || randomUUID());
140
175
  const body = format === 'org.hl7.fhir.api'
141
176
  ? {
@@ -145,7 +180,7 @@ export async function searchDigitalTwinsWithDeps(ctx, input, deps) {
145
180
  meta: {
146
181
  claims: Object.fromEntries([
147
182
  ['@context', format],
148
- ...parameters.map((parameter) => [parameter.name, parameter.valueString]),
183
+ ...parameters.map((parameter) => [parameter.name, parameter.valueString || parameter.valueDate || '']),
149
184
  ]),
150
185
  },
151
186
  }],
@@ -1,6 +1,6 @@
1
1
  import type { SubmitAndPollResult } from 'gdc-sdk-core-ts';
2
2
  import type { FamilyOrganizationSummary } from 'gdc-common-utils-ts/utils/family-organization-summary';
3
- import type { IndividualOrganizationConfirmOrderInput, RouteContext } from './individual-onboarding.js';
3
+ import type { IndividualOrganizationConfirmOrderInput, IndividualOrganizationOrderResult, RouteContext } from './individual-onboarding.js';
4
4
  import type { IndividualOrganizationBootstrapInput, IndividualOrganizationStartResult } from './individual-start.js';
5
5
  import type { EnsureFamilyOrganizationRegistrationInput, EnsureFamilyOrganizationRegistrationResult } from './family-organization-registration.js';
6
6
  import type { FamilyOrganizationSearchInput } from './family-organization-search.js';
@@ -45,7 +45,7 @@ export declare class IndividualControllerBackendRuntime {
45
45
  /**
46
46
  * Confirms the order returned by the individual bootstrap flow.
47
47
  */
48
- confirmIndividualOrganizationOrder(profile: BackendIndividualControllerProfile, input: IndividualOrganizationConfirmOrderInput): Promise<SubmitAndPollResult>;
48
+ confirmIndividualOrganizationOrder(profile: BackendIndividualControllerProfile, input: IndividualOrganizationConfirmOrderInput): Promise<IndividualOrganizationOrderResult>;
49
49
  /**
50
50
  * Reads the authoritative subject document through the canonical
51
51
  * `Communication -> Subject/$summary -> FHIR Parameters` lifecycle.
@@ -21,6 +21,13 @@ export type IndividualOrganizationConfirmOrderInput = {
21
21
  timeoutSeconds?: number;
22
22
  intervalSeconds?: number;
23
23
  };
24
+ /**
25
+ * Terminal individual Order result with the opaque code required by the
26
+ * subsequent managed-wallet activation and DCR flow.
27
+ */
28
+ export type IndividualOrganizationOrderResult = SubmitAndPollResult & Readonly<{
29
+ activationCode: string;
30
+ }>;
24
31
  type ConfirmIndividualOrganizationOrderDeps = {
25
32
  input: IndividualOrganizationConfirmOrderInput;
26
33
  routeCtx: RouteContext;
@@ -32,5 +39,11 @@ type ConfirmIndividualOrganizationOrderDeps = {
32
39
  thid?: string;
33
40
  } & Record<string, unknown>, options?: PollOptions) => Promise<SubmitAndPollResult>;
34
41
  };
35
- export declare function confirmIndividualOrganizationOrderWithDeps(deps: ConfirmIndividualOrganizationOrderDeps): Promise<SubmitAndPollResult>;
42
+ export declare function confirmIndividualOrganizationOrderWithDeps(deps: ConfirmIndividualOrganizationOrderDeps): Promise<IndividualOrganizationOrderResult>;
43
+ /**
44
+ * Reads the opaque controller activation code from a completed individual
45
+ * Order. Integrators should consume `result.activationCode` instead of calling
46
+ * this reader directly; it remains public for response-adapter compatibility.
47
+ */
48
+ export declare function readIndividualOrganizationActivationCode(responseBody: unknown): string | undefined;
36
49
  export {};
@@ -1,4 +1,5 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { extractPrimaryClaims } from 'gdc-common-utils-ts';
2
3
  import { resolvePollOptionsFromSeconds } from './poll-options.js';
3
4
  export async function confirmIndividualOrganizationOrderWithDeps(deps) {
4
5
  /**
@@ -35,7 +36,21 @@ export async function confirmIndividualOrganizationOrderWithDeps(deps) {
35
36
  timeoutMs: deps.defaultTimeoutMs,
36
37
  intervalMs: deps.defaultIntervalMs,
37
38
  });
38
- return deps.submitAndPoll(deps.individualFamilyOrderBatchPath(deps.routeCtx), deps.individualFamilyOrderPollPath(deps.routeCtx), payload, pollOptions);
39
+ const order = await deps.submitAndPoll(deps.individualFamilyOrderBatchPath(deps.routeCtx), deps.individualFamilyOrderPollPath(deps.routeCtx), payload, pollOptions);
40
+ const activationCode = readIndividualOrganizationActivationCode(order.poll.body);
41
+ if (!activationCode) {
42
+ throw new Error('confirmIndividualOrganizationOrder failed: missing controller activation code in GW Order response.');
43
+ }
44
+ return { ...order, activationCode };
45
+ }
46
+ /**
47
+ * Reads the opaque controller activation code from a completed individual
48
+ * Order. Integrators should consume `result.activationCode` instead of calling
49
+ * this reader directly; it remains public for response-adapter compatibility.
50
+ */
51
+ export function readIndividualOrganizationActivationCode(responseBody) {
52
+ const claims = extractPrimaryClaims(responseBody);
53
+ return String(claims['org.schema.IndividualProduct.serialNumber'] || '').trim() || undefined;
39
54
  }
40
55
  function createRuntimeUuid() {
41
56
  const fromCrypto = globalThis.crypto?.randomUUID?.();
@@ -11,6 +11,16 @@ export type NodeManagedWalletOptions = {
11
11
  resolveRecipientJwk?: (recipientDid: string) => Promise<JWK>;
12
12
  policy?: Partial<NodeManagedWalletPolicy>;
13
13
  };
14
+ export type NodeProfessionalIdentityVpInput = Readonly<{
15
+ clientId: string;
16
+ actorDid: string;
17
+ profileDid?: string;
18
+ role: string;
19
+ email?: string;
20
+ sameAs?: string | readonly string[];
21
+ telephone?: string;
22
+ credentialMaterial?: string;
23
+ }>;
14
24
  export type NodeCommunicationWalletInitialization = {
15
25
  /**
16
26
  * Stable secret used to reconstruct the same communication keys after a
@@ -67,26 +77,29 @@ export declare class NodeManagedWallet implements IWallet {
67
77
  */
68
78
  getPublicJwks(context?: WalletExecutionContext, filter?: WalletKeySelection): Promise<WalletKeyDescriptor[]>;
69
79
  /**
70
- * Initializes the runtime communication wallet and returns exactly the
71
- * public signing/encryption JWKS accepted as `controller.publicKeys` by the
72
- * legacy organization activation builder.
80
+ * Initializes one user/profile device communication wallet and returns only
81
+ * its public signing/encryption JWKS. The wallet is actor-role neutral: the
82
+ * same custody contract can back a controller, employee/professional or
83
+ * individual-controller profile.
73
84
  *
74
- * This does not replace the professional-role signing key supplied as
75
- * `controller.publicSignKey`. The role key identifies the historical legal
76
- * representative; these runtime keys protect DIDComm communications.
85
+ * These communication keys do not replace the actor's person/professional-
86
+ * role signing key. In the historical legal-representative flow, for example,
87
+ * this returned set is accepted as `controller.publicKeys`, while the
88
+ * independent role key is supplied as `controller.publicSignKey`.
77
89
  *
78
90
  * The caller owns durable wallet custody. When deterministic provisioning is
79
91
  * used, protect `seedMaterial` in the portal wallet store (for example with
80
92
  * its KMS/KEK and optionally a user PIN) and persist the same non-secret
81
93
  * `context.runtime.runtimeId`. A fresh `NodeManagedWallet` reconstructs the
82
94
  * same private keys from that seed and context after restart, so private JWKs
83
- * do not need separate persistence. ICA and GW receive only this returned
84
- * public JWKS.
95
+ * do not need separate persistence. Only the returned public JWKS may be
96
+ * submitted when the selected high-level onboarding flow requires it.
85
97
  */
86
98
  initializeCommunicationJsonWebKeySet(context: WalletExecutionContext, options?: NodeCommunicationWalletInitialization): Promise<JwkSet>;
87
99
  /**
88
100
  * Returns only the public DIDComm signing/encryption keys for a previously
89
- * initialized runtime wallet. Private material never leaves the wallet.
101
+ * initialized user/profile device runtime wallet. Private material never
102
+ * leaves the wallet, and the returned keys do not convey an actor role.
90
103
  */
91
104
  getCommunicationJsonWebKeySet(context: WalletExecutionContext): Promise<JwkSet>;
92
105
  /**
@@ -139,6 +152,12 @@ export declare class NodeManagedWallet implements IWallet {
139
152
  * Builds one compact JWS using one managed signing key.
140
153
  */
141
154
  signCompactJws(context: WalletExecutionContext, request: WalletCompactJwsRequest): Promise<string>;
155
+ /**
156
+ * Builds and signs the canonical employee/professional VP with the managed
157
+ * `vp-token-signing` key. Callers provide identity facts only; they do not
158
+ * assemble JOSE headers, choose a JWK or handle private key material.
159
+ */
160
+ signProfessionalIdentityVp(context: WalletExecutionContext, input: NodeProfessionalIdentityVpInput): Promise<string>;
142
161
  /**
143
162
  * Builds one detached compact JWS using one managed signing key.
144
163
  */
@@ -4,6 +4,7 @@ import { CryptographyService } from 'gdc-common-utils-ts/CryptographyService';
4
4
  import { Content } from 'gdc-common-utils-ts/utils/content';
5
5
  import { createJwtSigner } from 'gdc-common-utils-ts/utils/jwt-signer';
6
6
  import { buildJwtCompact, prepareJwtForSignature } from 'gdc-common-utils-ts/utils/jwt';
7
+ import { buildProfessionalIdentityVpPayload } from 'gdc-common-utils-ts/utils/professional-smart';
7
8
  import { NodeCryptoHelper } from './node-crypto-helper.js';
8
9
  const DEFAULT_POLICY = {
9
10
  defaults: {
@@ -134,21 +135,23 @@ export class NodeManagedWallet {
134
135
  return descriptors;
135
136
  }
136
137
  /**
137
- * Initializes the runtime communication wallet and returns exactly the
138
- * public signing/encryption JWKS accepted as `controller.publicKeys` by the
139
- * legacy organization activation builder.
138
+ * Initializes one user/profile device communication wallet and returns only
139
+ * its public signing/encryption JWKS. The wallet is actor-role neutral: the
140
+ * same custody contract can back a controller, employee/professional or
141
+ * individual-controller profile.
140
142
  *
141
- * This does not replace the professional-role signing key supplied as
142
- * `controller.publicSignKey`. The role key identifies the historical legal
143
- * representative; these runtime keys protect DIDComm communications.
143
+ * These communication keys do not replace the actor's person/professional-
144
+ * role signing key. In the historical legal-representative flow, for example,
145
+ * this returned set is accepted as `controller.publicKeys`, while the
146
+ * independent role key is supplied as `controller.publicSignKey`.
144
147
  *
145
148
  * The caller owns durable wallet custody. When deterministic provisioning is
146
149
  * used, protect `seedMaterial` in the portal wallet store (for example with
147
150
  * its KMS/KEK and optionally a user PIN) and persist the same non-secret
148
151
  * `context.runtime.runtimeId`. A fresh `NodeManagedWallet` reconstructs the
149
152
  * same private keys from that seed and context after restart, so private JWKs
150
- * do not need separate persistence. ICA and GW receive only this returned
151
- * public JWKS.
153
+ * do not need separate persistence. Only the returned public JWKS may be
154
+ * submitted when the selected high-level onboarding flow requires it.
152
155
  */
153
156
  async initializeCommunicationJsonWebKeySet(context, options = {}) {
154
157
  if (!context.runtime?.runtimeId) {
@@ -164,7 +167,8 @@ export class NodeManagedWallet {
164
167
  }
165
168
  /**
166
169
  * Returns only the public DIDComm signing/encryption keys for a previously
167
- * initialized runtime wallet. Private material never leaves the wallet.
170
+ * initialized user/profile device runtime wallet. Private material never
171
+ * leaves the wallet, and the returned keys do not convey an actor role.
168
172
  */
169
173
  async getCommunicationJsonWebKeySet(context) {
170
174
  if (!context.runtime?.runtimeId) {
@@ -335,6 +339,37 @@ export class NodeManagedWallet {
335
339
  const signature = await this.sign(prepared.signingInput, context, request.key);
336
340
  return buildJwtCompact(prepared.encodedHeader, prepared.encodedPayload, signature);
337
341
  }
342
+ /**
343
+ * Builds and signs the canonical employee/professional VP with the managed
344
+ * `vp-token-signing` key. Callers provide identity facts only; they do not
345
+ * assemble JOSE headers, choose a JWK or handle private key material.
346
+ */
347
+ async signProfessionalIdentityVp(context, input) {
348
+ const [vpSigningKey] = await this.getPublicJwks(context, {
349
+ ownerScope: 'runtime',
350
+ purpose: 'vp-token-signing',
351
+ });
352
+ if (!vpSigningKey)
353
+ throw new Error('Managed VP signing key is not available.');
354
+ const payload = buildProfessionalIdentityVpPayload({
355
+ clientId: input.clientId,
356
+ actorDid: input.actorDid,
357
+ role: input.role,
358
+ ...(input.email ? { email: input.email } : {}),
359
+ ...(input.sameAs ? { sameAs: input.sameAs } : {}),
360
+ ...(input.telephone ? { telephone: input.telephone } : {}),
361
+ ...(input.credentialMaterial ? { credentialMaterial: input.credentialMaterial } : {}),
362
+ });
363
+ return this.signCompactJws(context, {
364
+ header: { typ: 'JWT', jwk: vpSigningKey.publicJwk },
365
+ claims: {
366
+ iss: vpSigningKey.kid,
367
+ sub: input.profileDid || input.actorDid,
368
+ ...payload,
369
+ },
370
+ key: { ownerScope: 'runtime', purpose: 'vp-token-signing' },
371
+ });
372
+ }
338
373
  /**
339
374
  * Builds one detached compact JWS using one managed signing key.
340
375
  */
@@ -5,7 +5,7 @@ import { type ResolvedAppInfo, type CommunicationClinicalFormatRenderers, type S
5
5
  import { type HostRouteContext, type HostedTenantLifecycleInput } from './host-onboarding.js';
6
6
  import type { HostedTenantDescendantKind } from './host-onboarding.js';
7
7
  import type { NodeLegalOrganizationVerificationTransactionInput, NodeOrganizationDidBindingInput, NodeOrganizationActivationInput } from './orchestration/client-port.js';
8
- import { type IndividualOrganizationConfirmOrderInput, type RouteContext } from './individual-onboarding.js';
8
+ import { type IndividualOrganizationConfirmOrderInput, type IndividualOrganizationOrderResult, type RouteContext } from './individual-onboarding.js';
9
9
  import { type EnsureFamilyOrganizationRegistrationInput } from './family-organization-registration.js';
10
10
  import { type FamilyOrganizationSearchInput } from './family-organization-search.js';
11
11
  import { type SmartTokenRequestInput } from './smart-token.js';
@@ -376,7 +376,7 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
376
376
  /**
377
377
  * Confirms the order returned by `startIndividualOrganization(...)`.
378
378
  */
379
- confirmIndividualOrganizationOrder(input: IndividualOrganizationConfirmOrderInput): Promise<SubmitAndPollResult>;
379
+ confirmIndividualOrganizationOrder(input: IndividualOrganizationConfirmOrderInput): Promise<IndividualOrganizationOrderResult>;
380
380
  /**
381
381
  * Disables a hosted individual/family organization without releasing licenses.
382
382
  *
@@ -9,7 +9,7 @@ import type { EmployeeDeviceActivationResult, EmployeeDeviceActivationRequestInp
9
9
  import type { OrganizationEmployeeProvisioningInput, OrganizationEmployeeProvisioningResult } from '../organization-employee-lifecycle.js';
10
10
  import type { OrganizationEmployeeLifecycleRecord } from 'gdc-common-utils-ts/models/organization-employee-lifecycle';
11
11
  import type { HostRouteContext, HostedTenantLifecycleInput, LegalOrganizationOrderInput } from '../host-onboarding.js';
12
- import type { IndividualOrganizationConfirmOrderInput, RouteContext } from '../individual-onboarding.js';
12
+ import type { IndividualOrganizationConfirmOrderInput, IndividualOrganizationOrderResult, RouteContext } from '../individual-onboarding.js';
13
13
  import type { IndividualOrganizationBootstrapInput, IndividualOrganizationStartResult } from '../individual-start.js';
14
14
  import type { FamilyOrganizationSearchInput } from '../family-organization-search.js';
15
15
  import type { FhirR5SubscriptionTopic } from 'gdc-common-utils-ts/models/fhir-r5-subscription';
@@ -106,7 +106,7 @@ export type RuntimeClient = {
106
106
  startIndividualOrganization?: (input: IndividualOrganizationBootstrapInput) => Promise<IndividualOrganizationStartResult>;
107
107
  searchFamilyOrganization?: (ctx: RouteContext, input: FamilyOrganizationSearchInput) => Promise<FamilyOrganizationSummary | null>;
108
108
  ensureFamilyOrganizationRegistration?: (ctx: RouteContext, input: EnsureFamilyOrganizationRegistrationInput) => Promise<EnsureFamilyOrganizationRegistrationResult>;
109
- confirmIndividualOrganizationOrder?: (input: IndividualOrganizationConfirmOrderInput) => Promise<SubmitAndPollResult>;
109
+ confirmIndividualOrganizationOrder?: (input: IndividualOrganizationConfirmOrderInput) => Promise<IndividualOrganizationOrderResult>;
110
110
  disableIndividual?: (ctx: RouteContext, input: IndividualOrganizationLifecycleInput, pollOptions?: PollOptions) => Promise<SubmitAndPollResult>;
111
111
  purgeIndividual?: (ctx: RouteContext, input: IndividualOrganizationLifecycleInput, pollOptions?: PollOptions) => Promise<SubmitAndPollResult>;
112
112
  disableIndividualMember?: (ctx: RouteContext, input: IndividualMemberLifecycleInput, pollOptions?: PollOptions) => Promise<SubmitAndPollResult>;
@@ -10,7 +10,14 @@ export declare class DigitalTwinSdk {
10
10
  constructor(client: NodeRuntimeClient, actorDid?: string);
11
11
  /** Requests the SMART token used by subsequent digital-twin operations. */
12
12
  requestSmartToken(input: SmartTokenRequestInput): Promise<SmartTokenExchangeResult>;
13
- /** Searches pseudonymous records and exposes matched Compositions directly. */
13
+ /**
14
+ * Searches pseudonymous records and exposes matched Compositions directly.
15
+ *
16
+ * MVP basic search supplies `sections`, `dateFrom`, optional `dateTo`, and
17
+ * `text`. GW resolves an omitted `dateTo` to its current time, applies OR
18
+ * across sections and requires text/date to match the same clinical record.
19
+ * Organization/tenant scope comes from the authenticated SMART token.
20
+ */
14
21
  search(ctx: RouteContext, input: DigitalTwinSearchInput): Promise<DigitalTwinSearchResult>;
15
22
  /**
16
23
  * Saves a tagged researcher-owned working selection for one matching twin.
@@ -28,7 +28,14 @@ export class DigitalTwinSdk {
28
28
  this.researcherDid = actorDid;
29
29
  return result;
30
30
  }
31
- /** Searches pseudonymous records and exposes matched Compositions directly. */
31
+ /**
32
+ * Searches pseudonymous records and exposes matched Compositions directly.
33
+ *
34
+ * MVP basic search supplies `sections`, `dateFrom`, optional `dateTo`, and
35
+ * `text`. GW resolves an omitted `dateTo` to its current time, applies OR
36
+ * across sections and requires text/date to match the same clinical record.
37
+ * Organization/tenant scope comes from the authenticated SMART token.
38
+ */
32
39
  async search(ctx, input) {
33
40
  const filters = { ...(input.filters || {}) };
34
41
  const isPrivateSelectionSearch = Object.keys(filters).some((name) => {
@@ -3,7 +3,7 @@ import { type NodeRuntimeClient, type PollOptions, type SubmitAndPollResult, typ
3
3
  import type { FamilyOrganizationSummary } from 'gdc-common-utils-ts/utils/family-organization-summary';
4
4
  import type { EnsureFamilyOrganizationRegistrationInput, EnsureFamilyOrganizationRegistrationResult } from '../family-organization-registration.js';
5
5
  import type { FamilyOrganizationSearchInput } from '../family-organization-search.js';
6
- import type { IndividualOrganizationConfirmOrderInput, RouteContext } from '../individual-onboarding.js';
6
+ import type { IndividualOrganizationConfirmOrderInput, IndividualOrganizationOrderResult, RouteContext } from '../individual-onboarding.js';
7
7
  import type { IndividualOrganizationBootstrapInput, IndividualOrganizationStartResult } from '../individual-start.js';
8
8
  import type { NodeCapability } from '../session.js';
9
9
  import type { IndividualOrganizationLifecycleInput } from 'gdc-sdk-core-ts';
@@ -39,7 +39,7 @@ export declare class IndividualControllerSdk {
39
39
  /**
40
40
  * Confirms the order returned by `startIndividualOrganization(...)`.
41
41
  */
42
- confirmIndividualOrganizationOrder(input: IndividualOrganizationConfirmOrderInput): Promise<SubmitAndPollResult>;
42
+ confirmIndividualOrganizationOrder(input: IndividualOrganizationConfirmOrderInput): Promise<IndividualOrganizationOrderResult>;
43
43
  /**
44
44
  * Disables the hosted individual/family organization without freeing licenses.
45
45
  */
@@ -218,6 +218,12 @@ export async function enrollInvitedOrganizationEmployeeWithDeps(input, deps) {
218
218
  clientInstanceId: input.clientInstanceId,
219
219
  dcrRedirectUris: input.dcrRedirectUris,
220
220
  dcrClientName: input.dcrClientName,
221
- vpToken: input.idToken,
221
+ // The grant contains the server-verified stable actor alias and role. The
222
+ // profile runtime signs the VP with its managed DCR wallet after GW has
223
+ // returned the real client_id; idToken remains only the OIDC account proof.
224
+ professionalProof: {
225
+ role: input.grant.employeeRoleCode,
226
+ sameAs: input.grant.employeeActorIdentifier,
227
+ },
222
228
  });
223
229
  }
@@ -1,7 +1,7 @@
1
1
  import type { JWK } from 'gdc-common-utils-ts/models/jwk';
2
2
  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
- import { type ConfidentialStorageProfile, type PollOptions, type SubmitAndPollResult } from 'gdc-sdk-core-ts';
4
+ import { type AppInfo, type ConfidentialStorageProfile, type PollOptions, type SubmitAndPollResult } from 'gdc-sdk-core-ts';
5
5
  import type { RouteContext } from './individual-onboarding.js';
6
6
  import type { HostRouteContext } from './host-onboarding.js';
7
7
  import type { SecureDidcommTransportAdapter } from 'gdc-sdk-core-ts';
@@ -31,7 +31,8 @@ export type ServerProfileRecord = Readonly<{
31
31
  /** Server-owned policy; browser input is never authoritative for this value. */
32
32
  confidentialStorageProfile?: ConfidentialStorageProfile;
33
33
  protectedWalletSeed: PinProtectedProfileSecret;
34
- protectedVpToken: PinProtectedProfileSecret;
34
+ /** Present only when this actor kind requires an independent signed role/relationship VP. */
35
+ protectedVpToken?: PinProtectedProfileSecret;
35
36
  failedUnlocks: number;
36
37
  lockedUntil?: string;
37
38
  createdAt: string;
@@ -95,8 +96,25 @@ export type ServerProfileEnrollmentInput = Readonly<{
95
96
  * derives the identity from the wallet key it creates for this profile.
96
97
  */
97
98
  clientInstanceId?: string;
98
- /** Signed actor/controller VP protected with the profile for later proof and session operations. */
99
- vpToken: string;
99
+ /**
100
+ * Signed actor/controller VP protected for later SMART operations. Required
101
+ * for organization/professional/member actors. Individual-controller
102
+ * profiles omit it: their account `id_token`, DCR device binding and
103
+ * provider-side relationship policy are evaluated independently.
104
+ */
105
+ vpToken?: string;
106
+ /**
107
+ * High-level employee/professional proof source. When supplied, the SDK
108
+ * builds and signs the VP with the managed DCR wallet after registration;
109
+ * callers do not compose JWT/JWK material.
110
+ */
111
+ professionalProof?: Readonly<{
112
+ role: string;
113
+ email?: string;
114
+ sameAs?: string | readonly string[];
115
+ telephone?: string;
116
+ credentialMaterial?: string;
117
+ }>;
100
118
  /** Redirect URIs owned by this portal/app installation. */
101
119
  redirectUris?: string[];
102
120
  /** Human-readable portal/app name. */
@@ -132,6 +150,20 @@ export type ServerProfileOrganizationIssueInput = Readonly<{
132
150
  verificationInput: LegalOrganizationVerificationTransactionInput;
133
151
  pollOptions?: PollOptions;
134
152
  }>;
153
+ export type ServerOrganizationControllerVpInput = Readonly<{
154
+ /** Same protected seed that committed the controller public key to ICA. */
155
+ walletSeed: string;
156
+ /** Stable application-owned derivation id reused after every restart. */
157
+ walletKeyDerivationId: string;
158
+ /** Terminal ICA/GW verification body containing the issued organization, representative and controller VCs. */
159
+ verificationResponseBody: unknown;
160
+ /** Exact organization tenant identifier represented by the controller proof. */
161
+ tenantId: string;
162
+ /** Exact audience required by the receiving GW/host policy. */
163
+ audience: string;
164
+ /** Optional stable sameAs selector when the response contains several controller credentials. */
165
+ controllerSameAs?: string;
166
+ }>;
135
167
  /** One explicit unlock request; scopes and subject remain session-bound. */
136
168
  export type ServerProfileUnlockInput = Readonly<{
137
169
  ownerId: string;
@@ -166,6 +198,8 @@ export type ServerProfileSessionManagerOptions = Readonly<{
166
198
  sealer: ServerProfileSealer;
167
199
  gatewayBaseUrl: string;
168
200
  resolveRecipientJwk: (recipientDid: string) => Promise<JWK>;
201
+ /** Stable identity of the portal/application, shared by all of its user wallets. */
202
+ appInfo?: AppInfo;
169
203
  fetchImpl?: typeof fetch;
170
204
  sessionTtlSeconds?: number;
171
205
  maxFailedUnlocks?: number;
@@ -196,6 +230,15 @@ export declare class ServerProfileSessionManager {
196
230
  walletSeed: string;
197
231
  walletKeyDerivationId: string;
198
232
  }>): Promise<ServerProfileEnrollmentPublicKey[]>;
233
+ /**
234
+ * Builds and signs the canonical controller VP directly from ICA-issued
235
+ * credentials. The caller never assembles a VP/JWS or exports a private key.
236
+ *
237
+ * This proof is independent from the signed OIDC id_token: the VP proves
238
+ * organization/controller authority, while id_token proves control of the
239
+ * verified login identifier used by Token/_exchange and DCR.
240
+ */
241
+ buildOrganizationControllerVpFromIcaProof(input: ServerOrganizationControllerVpInput): Promise<string>;
199
242
  /**
200
243
  * Reissues an existing organization controller activation through protected
201
244
  * DIDComm transport before a durable profile exists. The deterministic seed
@@ -1,6 +1,8 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import { ActorKinds } from 'gdc-common-utils-ts/constants/actor-session';
4
+ import { readLegalOrganizationVerificationCredentialPairFromResponseBody, readServiceControllerCredentialFromResponseBody, } from 'gdc-common-utils-ts/utils/legal-organization-verification-result';
5
+ import { addLegalRepresentativeCredential, addOrganizationCredential, addServiceControllerCredential, createVP, } from 'gdc-common-utils-ts/utils/vp-token';
4
6
  import { TransportProfiles, } from 'gdc-sdk-core-ts';
5
7
  import { NodeManagedWallet } from './node-managed-wallet.js';
6
8
  import { NodeHttpClient } from './node-runtime-client.js';
@@ -62,6 +64,14 @@ export class ServerProfileSessionManager {
62
64
  throw new Error('GW DCR did not return client_id.');
63
65
  const deviceDid = findText(dcrBody, ['device_did', 'deviceDid', 'did']) || clientId;
64
66
  const now = this.now();
67
+ const managedVpToken = input.vpToken || (input.professionalProof
68
+ ? await buildManagedProfessionalVp(wallet, context, {
69
+ clientId,
70
+ actorDid: input.actorDid,
71
+ profileDid: input.profileDid,
72
+ ...input.professionalProof,
73
+ })
74
+ : undefined);
65
75
  const record = {
66
76
  profileId: input.profileId,
67
77
  walletKeyDerivationId,
@@ -80,7 +90,9 @@ export class ServerProfileSessionManager {
80
90
  storagePublicJwk: storagePublicJwk,
81
91
  confidentialStorageProfile: this.options.requiredConfidentialStorageProfile ?? 'confidential-basic-v1',
82
92
  protectedWalletSeed: await protectServerProfileSecret(seed, input.pin, `${input.profileId}:wallet-seed`, this.options.sealer, this.options.profileProtection),
83
- protectedVpToken: await protectServerProfileSecret(input.vpToken, input.pin, `${input.profileId}:vp-token`, this.options.sealer, this.options.profileProtection),
93
+ ...(managedVpToken ? {
94
+ protectedVpToken: await protectServerProfileSecret(managedVpToken, input.pin, `${input.profileId}:vp-token`, this.options.sealer, this.options.profileProtection),
95
+ } : {}),
84
96
  failedUnlocks: 0,
85
97
  createdAt: now.toISOString(),
86
98
  updatedAt: now.toISOString(),
@@ -109,6 +121,57 @@ export class ServerProfileSessionManager {
109
121
  publicJwk: entry.publicJwk,
110
122
  }));
111
123
  }
124
+ /**
125
+ * Builds and signs the canonical controller VP directly from ICA-issued
126
+ * credentials. The caller never assembles a VP/JWS or exports a private key.
127
+ *
128
+ * This proof is independent from the signed OIDC id_token: the VP proves
129
+ * organization/controller authority, while id_token proves control of the
130
+ * verified login identifier used by Token/_exchange and DCR.
131
+ */
132
+ async buildOrganizationControllerVpFromIcaProof(input) {
133
+ requireBase64UrlSeed32(input.walletSeed);
134
+ const walletKeyDerivationId = normalizedWalletKeyDerivationId(input.walletKeyDerivationId, '');
135
+ if (!walletKeyDerivationId) {
136
+ throw new Error('buildOrganizationControllerVpFromIcaProof requires walletKeyDerivationId.');
137
+ }
138
+ const tenantId = String(input.tenantId || '').trim();
139
+ const audience = String(input.audience || '').trim();
140
+ if (!tenantId)
141
+ throw new Error('buildOrganizationControllerVpFromIcaProof requires tenantId.');
142
+ if (!audience)
143
+ throw new Error('buildOrganizationControllerVpFromIcaProof requires audience.');
144
+ const { organizationCredential, legalRepresentativeCredential } = readLegalOrganizationVerificationCredentialPairFromResponseBody(input.verificationResponseBody);
145
+ const controllerCredential = readServiceControllerCredentialFromResponseBody(input.verificationResponseBody, input.controllerSameAs);
146
+ if (!controllerCredential) {
147
+ throw new Error('ICA verification response does not contain a ServiceControllerCredential.');
148
+ }
149
+ const wallet = await this.createWallet(walletKeyDerivationId, input.walletSeed);
150
+ const context = walletContext(walletKeyDerivationId);
151
+ const [actorSigningKey] = await wallet.getPublicJwks(context, {
152
+ ownerScope: 'profile',
153
+ purpose: 'actor-signing',
154
+ });
155
+ if (!actorSigningKey)
156
+ throw new Error('Controller actor-signing key is not available.');
157
+ const vp = createVP({
158
+ iss: actorSigningKey.kid,
159
+ sub: tenantId,
160
+ aud: audience,
161
+ vp: { holder: actorSigningKey.kid, verifiableCredential: [] },
162
+ });
163
+ addOrganizationCredential(vp, organizationCredential);
164
+ addLegalRepresentativeCredential(vp, legalRepresentativeCredential);
165
+ addServiceControllerCredential(vp, controllerCredential);
166
+ return wallet.signCompactJws(context, {
167
+ header: {
168
+ typ: 'JWT',
169
+ jwk: actorSigningKey.publicJwk,
170
+ },
171
+ claims: vp,
172
+ key: { ownerScope: 'profile', purpose: 'actor-signing' },
173
+ });
174
+ }
112
175
  /**
113
176
  * Reissues an existing organization controller activation through protected
114
177
  * DIDComm transport before a durable profile exists. The deterministic seed
@@ -126,6 +189,7 @@ export class ServerProfileSessionManager {
126
189
  baseUrl: this.options.gatewayBaseUrl,
127
190
  ctx: input.routeContext,
128
191
  bearerToken: input.bearerToken,
192
+ appInfo: this.options.appInfo,
129
193
  fetchImpl: this.options.fetchImpl,
130
194
  transportProfile: TransportProfiles.DidcommEncryptedForm,
131
195
  secureTransportAdapter: {
@@ -153,7 +217,9 @@ export class ServerProfileSessionManager {
153
217
  let vpToken;
154
218
  try {
155
219
  seed = await openServerProfileSecret(profile.protectedWalletSeed, input.pin, `${profile.profileId}:wallet-seed`, this.options.sealer);
156
- vpToken = await openServerProfileSecret(profile.protectedVpToken, input.pin, `${profile.profileId}:vp-token`, this.options.sealer);
220
+ vpToken = profile.protectedVpToken
221
+ ? await openServerProfileSecret(profile.protectedVpToken, input.pin, `${profile.profileId}:vp-token`, this.options.sealer)
222
+ : undefined;
157
223
  }
158
224
  catch (reason) {
159
225
  if (!(reason instanceof ProfilePinRejectedError))
@@ -183,6 +249,7 @@ export class ServerProfileSessionManager {
183
249
  audience: smartTokenEndpoint,
184
250
  idToken: input.idToken,
185
251
  vpToken,
252
+ vpTokenFallback: vpToken ? undefined : 'omit',
186
253
  clientAssertion: assertion,
187
254
  clientAssertionType: 'private_key_jwt',
188
255
  smartTokenKind: 'openid-smart',
@@ -247,6 +314,7 @@ export class ServerProfileSessionManager {
247
314
  ctx,
248
315
  bearerToken,
249
316
  fetchImpl: this.options.fetchImpl,
317
+ appInfo: this.options.appInfo,
250
318
  });
251
319
  }
252
320
  async createWallet(walletKeyDerivationId, seed) {
@@ -323,6 +391,10 @@ function profileSmartAcrValues(actorKind) {
323
391
  ? 'urn:antifraud:acr:openid4vp:individual'
324
392
  : 'urn:antifraud:acr:openid4vp:employee';
325
393
  }
394
+ /** Creates the role VP after DCR has returned the wallet's actual client_id. */
395
+ async function buildManagedProfessionalVp(wallet, context, input) {
396
+ return wallet.signProfessionalIdentityVp(context, input);
397
+ }
326
398
  async function buildWalletClientAssertion(wallet, profile, audience, now) {
327
399
  const seconds = Math.floor(now.getTime() / 1000);
328
400
  return wallet.signCompactJws(walletContext(profile.walletKeyDerivationId || profile.profileId), {
@@ -361,10 +433,17 @@ function requireEnrollment(input) {
361
433
  providerDid: input.providerDid,
362
434
  activationCode: input.activationCode,
363
435
  idToken: input.idToken,
364
- vpToken: input.vpToken,
365
436
  }))
366
437
  if (!String(value || '').trim())
367
438
  throw new Error(`Profile enrollment requires ${name}.`);
439
+ if (input.actorKind !== ActorKinds.IndividualController
440
+ && !String(input.vpToken || '').trim()
441
+ && !input.professionalProof) {
442
+ throw new Error('Profile enrollment requires vpToken for this actor kind.');
443
+ }
444
+ if (input.professionalProof && !String(input.professionalProof.role || '').trim()) {
445
+ throw new Error('Profile enrollment professionalProof requires role.');
446
+ }
368
447
  if (!input.allowedSubjectDids.length)
369
448
  throw new Error('Profile enrollment requires an allowed subject.');
370
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.4.34",
3
+ "version": "2.4.36",
4
4
  "description": "Next-generation Node runtime package for the GDC SDK family",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Antifraud Services Inc.",
@@ -38,7 +38,7 @@
38
38
  "test:e2e:live-gw:clean": "bash ./scripts/run-live-gw-clean.sh"
39
39
  },
40
40
  "dependencies": {
41
- "gdc-common-utils-ts": "2.5.15",
41
+ "gdc-common-utils-ts": "2.5.20",
42
42
  "gdc-sdk-core-ts": "~2.3.18"
43
43
  },
44
44
  "devDependencies": {
@@ -1,16 +0,0 @@
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>;
@@ -1,34 +0,0 @@
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
- }