gdc-sdk-node-ts 2.5.0 → 2.5.2

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.
@@ -7,6 +7,9 @@ const CLAIM_ITEM_CONDITION = 'org.schema.IndividualProduct.itemCondition';
7
7
  const CLAIM_PERSON_EMAIL = 'org.schema.Person.email';
8
8
  const CLAIM_PERSON_TELEPHONE = 'org.schema.Person.telephone';
9
9
  const CLAIM_ORGANIZATION_SAME_AS = 'org.schema.Organization.sameAs';
10
+ const CLAIM_ORGANIZATION_OWNER_EMAIL = 'org.schema.Organization.owner.email';
11
+ const CLAIM_ORGANIZATION_OWNER_TELEPHONE = 'org.schema.Organization.owner.telephone';
12
+ const CLAIM_ORGANIZATION_MEMBER_ROLE = 'org.schema.Organization.member.role';
10
13
  const CLAIM_RELATED_PERSON_ROLE = 'RelatedPerson.role';
11
14
  const CLAIM_OCCUPATION_IDENTIFIER = 'org.schema.Person.hasOccupation.identifier.value';
12
15
  /**
@@ -23,6 +26,24 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
23
26
  if (!email && !telephone) {
24
27
  throw new Error('A verified email or telephone is required to list authorized subjects.');
25
28
  }
29
+ const ownerResult = await deps.submitAndPoll(deps.individualOrganizationSearchPath(routeContext), deps.individualOrganizationSearchPollPath(routeContext), {
30
+ thid: input.requestThid || createThreadId('authorized-subject-owner'),
31
+ body: {
32
+ resourceType: 'Bundle',
33
+ type: 'batch',
34
+ data: [{
35
+ type: 'Family-search-v1.0',
36
+ request: { method: 'POST' },
37
+ meta: { claims: {
38
+ '@context': SCHEMA_CONTEXT,
39
+ ...(email ? { [CLAIM_ORGANIZATION_OWNER_EMAIL]: email } : {}),
40
+ ...(telephone ? { [CLAIM_ORGANIZATION_OWNER_TELEPHONE]: telephone } : {}),
41
+ } },
42
+ }],
43
+ },
44
+ }, input.pollOptions);
45
+ const ownedSubjects = readOwnedSubjects(ownerResult, { email, telephone });
46
+ const subjectsByDid = new Map(ownedSubjects.map((subject) => [subject.subjectDid, subject]));
26
47
  const licenseClaims = {
27
48
  '@context': SCHEMA_CONTEXT,
28
49
  [CLAIM_AUDIENCE_TYPE]: INDIVIDUAL_AUDIENCE_TYPE,
@@ -42,7 +63,7 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
42
63
  },
43
64
  }, input.pollOptions);
44
65
  const licenses = readAcceptedLicenses(licenseResult);
45
- const resolved = await Promise.all(licenses.map(async (license) => {
66
+ const resolved = await Promise.all(licenses.filter((license) => !subjectsByDid.has(license.subjectDid)).map(async (license) => {
46
67
  const organizationClaims = {
47
68
  '@context': SCHEMA_CONTEXT,
48
69
  [CLAIM_ORGANIZATION_SAME_AS]: license.subjectDid,
@@ -74,7 +95,38 @@ export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, inp
74
95
  subjectClaims,
75
96
  };
76
97
  }));
77
- return resolved.filter((value) => Boolean(value));
98
+ for (const subject of resolved.filter((value) => Boolean(value))) {
99
+ subjectsByDid.set(subject.subjectDid, subject);
100
+ }
101
+ return [...subjectsByDid.values()];
102
+ }
103
+ function readOwnedSubjects(result, verified) {
104
+ const resources = readData(result).flatMap((entry) => {
105
+ if (entry?.resource?.resourceType === 'Bundle' && Array.isArray(entry.resource.entry)) {
106
+ return entry.resource.entry.map((item) => item?.resource).filter(Boolean);
107
+ }
108
+ return [];
109
+ });
110
+ return resources.flatMap((resource) => {
111
+ const claims = resource?.meta?.claims;
112
+ if (!claims || typeof claims !== 'object')
113
+ return [];
114
+ const ownerEmail = String(claims[CLAIM_ORGANIZATION_OWNER_EMAIL] || '').trim().toLowerCase();
115
+ const ownerTelephone = String(claims[CLAIM_ORGANIZATION_OWNER_TELEPHONE] || '').trim();
116
+ if (!((verified.email && ownerEmail === verified.email) || (verified.telephone && ownerTelephone === verified.telephone))) {
117
+ return [];
118
+ }
119
+ const subjectDid = String(claims[CLAIM_ORGANIZATION_SAME_AS] || '').trim();
120
+ if (!subjectDid)
121
+ return [];
122
+ const role = String(claims[CLAIM_ORGANIZATION_MEMBER_ROLE] || '').trim();
123
+ return [{
124
+ subjectDid,
125
+ ...(role ? { role } : {}),
126
+ grantClaims: {},
127
+ subjectClaims: claims,
128
+ }];
129
+ });
78
130
  }
79
131
  function readAcceptedLicenses(result) {
80
132
  const rows = readData(result)[0]?.resource?.data;
@@ -180,6 +180,16 @@ export declare class NodeManagedWallet implements IWallet {
180
180
  * Packs one payload into a transport envelope signed and encrypted by the runtime.
181
181
  */
182
182
  packForRecipientWithContext(content: any, recipientDidOrJwk: string | JWK, options: WalletPackOptions): Promise<string>;
183
+ /**
184
+ * Packs the first activation/DCR requests before GW can resolve this
185
+ * installation's communication keys from a registered DID document.
186
+ *
187
+ * The embedded public keys are bootstrap response and signature material;
188
+ * callers must still authenticate the HTTP request with the trusted OIDC
189
+ * token and one server-authorized activation code. Post-DCR operations must
190
+ * use `packForRecipientWithContext` so registered key custody is resolved.
191
+ */
192
+ packProfileBootstrapForRecipientWithContext(content: any, recipientDidOrJwk: string | JWK, options: WalletPackOptions): Promise<string>;
183
193
  /**
184
194
  * Legacy unpack shape retained for app compatibility.
185
195
  */
@@ -467,6 +467,46 @@ export class NodeManagedWallet {
467
467
  key: encryptionKey,
468
468
  });
469
469
  }
470
+ /**
471
+ * Packs the first activation/DCR requests before GW can resolve this
472
+ * installation's communication keys from a registered DID document.
473
+ *
474
+ * The embedded public keys are bootstrap response and signature material;
475
+ * callers must still authenticate the HTTP request with the trusted OIDC
476
+ * token and one server-authorized activation code. Post-DCR operations must
477
+ * use `packForRecipientWithContext` so registered key custody is resolved.
478
+ */
479
+ async packProfileBootstrapForRecipientWithContext(content, recipientDidOrJwk, options) {
480
+ const recipientJwk = typeof recipientDidOrJwk === 'string'
481
+ ? await this.resolveRecipientPublicJwk(recipientDidOrJwk)
482
+ : recipientDidOrJwk;
483
+ const signingKey = this.requireManagedKey(options.context, {
484
+ ownerScope: 'runtime',
485
+ purpose: 'comm-signing',
486
+ }, 'sig');
487
+ const encryptionKey = this.requireManagedKey(options.context, {
488
+ ownerScope: 'runtime',
489
+ purpose: 'comm-encryption',
490
+ }, 'enc');
491
+ const compactJws = await this.signCompactJws(options.context, {
492
+ header: {
493
+ typ: 'JWS',
494
+ jwk: signingKey.descriptor.publicJwk,
495
+ },
496
+ claims: content,
497
+ key: { keyId: signingKey.descriptor.kid },
498
+ });
499
+ const senderEncryptionKey = {
500
+ ...encryptionKey.descriptor.publicJwk,
501
+ dBytes: encryptionKey.privateMaterial,
502
+ };
503
+ return this.cryptography.encryptJweToCompact(compactJws, {
504
+ enc: 'A256GCM',
505
+ skid: encryptionKey.descriptor.kid,
506
+ jwk: encryptionKey.descriptor.publicJwk,
507
+ cty: 'JWS',
508
+ }, senderEncryptionKey, recipientJwk);
509
+ }
470
510
  /**
471
511
  * Legacy unpack shape retained for app compatibility.
472
512
  */
@@ -86,6 +86,7 @@ export type NodeHttpClientOptions = HttpRuntimeClientOptions;
86
86
  export declare class HttpRuntimeClient implements NodeRuntimeClient {
87
87
  private readonly baseUrl;
88
88
  private readonly bearerToken?;
89
+ private activeSmartBearerToken?;
89
90
  private readonly runtimeVpToken?;
90
91
  private readonly resolvedAppInfo?;
91
92
  private readonly ctx?;
@@ -461,6 +462,10 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
461
462
  * Route details are resolved either from `input` compatibility fields
462
463
  * (`tenantId` / `jurisdiction` / `sector`) or from
463
464
  * the default client route context configured in the constructor.
465
+ * A fetched actor-scoped access token is retained by this runtime for later
466
+ * clinical operations. Token acquisition and renewal continue to use the
467
+ * original account bearer, so applications must not reconstruct the SDK or
468
+ * manually replace its transport after this method returns.
464
469
  */
465
470
  requestSmartToken(input: SmartTokenRequestInput): Promise<SmartTokenExchangeResult>;
466
471
  /**
@@ -33,7 +33,7 @@ export class HttpRuntimeClient {
33
33
  get transportConfig() {
34
34
  return {
35
35
  baseUrl: this.baseUrl,
36
- bearerToken: this.bearerToken,
36
+ bearerToken: this.activeSmartBearerToken || this.bearerToken,
37
37
  defaultHeaders: this.defaultHeaders,
38
38
  requestTimeoutMs: this.requestTimeoutMs,
39
39
  httpTraceFile: this.httpTraceFile,
@@ -886,10 +886,14 @@ export class HttpRuntimeClient {
886
886
  * Route details are resolved either from `input` compatibility fields
887
887
  * (`tenantId` / `jurisdiction` / `sector`) or from
888
888
  * the default client route context configured in the constructor.
889
+ * A fetched actor-scoped access token is retained by this runtime for later
890
+ * clinical operations. Token acquisition and renewal continue to use the
891
+ * original account bearer, so applications must not reconstruct the SDK or
892
+ * manually replace its transport after this method returns.
889
893
  */
890
894
  async requestSmartToken(input) {
891
895
  const routeCtx = this.paths.routeCtxFromInput(input);
892
- return requestSmartTokenWithDeps({
896
+ const result = await requestSmartTokenWithDeps({
893
897
  input,
894
898
  routeCtx,
895
899
  baseUrl: this.baseUrl,
@@ -900,9 +904,16 @@ export class HttpRuntimeClient {
900
904
  identityOpenIdSmartTokenPath: this.paths.identityOpenIdSmartTokenPath.bind(this.paths),
901
905
  identityOpenIdSmartTokenPollPath: this.paths.identityOpenIdSmartTokenPollPath.bind(this.paths),
902
906
  resolveSmartTokenEndpoint: this.smartTokenEndpointResolver,
903
- submitAndPoll: this.submitAndPoll.bind(this),
907
+ // Token acquisition and refresh remain authenticated by the original
908
+ // account bearer. A previously granted SMART token must not replace the
909
+ // OIDC proof used to request the next actor-scoped token.
910
+ submitAndPoll: this.submitAndPollWithBearerToken.bind(this, this.bearerToken),
904
911
  setTokenCache: (tokenCacheKey, token) => this.tokenCache.set(tokenCacheKey, token),
905
912
  });
913
+ if (result.status === 'fetched' && result.accessToken) {
914
+ this.activeSmartBearerToken = result.accessToken;
915
+ }
916
+ return result;
906
917
  }
907
918
  /**
908
919
  * Registers any actor profile device through activation-code exchange plus
@@ -80,6 +80,12 @@ export declare class IndividualControllerSdk {
80
80
  revokeProfessionalAccess(ctx: RouteContext, input: RevokeProfessionalAccessInput): Promise<RevokeProfessionalAccessResult>;
81
81
  /**
82
82
  * Imports a FHIR/IPS payload and waits until it is indexed.
83
+ *
84
+ * The source `Composition.author` is provenance, not delegated authority.
85
+ * A controller that imports an IPS authored by an external URN does not gain
86
+ * permission to rewrite or delete it. A correction requires independently
87
+ * verified source provenance; merely resubmitting the same URN is
88
+ * insufficient and unsigned/demo imports remain locally immutable.
83
89
  */
84
90
  importIpsOrFhirAndUpdateIndex(ctx: RouteContext, input: IpsOrFhirImportInput): Promise<SubmitAndPollResult>;
85
91
  /**
@@ -119,6 +119,12 @@ export class IndividualControllerSdk {
119
119
  }
120
120
  /**
121
121
  * Imports a FHIR/IPS payload and waits until it is indexed.
122
+ *
123
+ * The source `Composition.author` is provenance, not delegated authority.
124
+ * A controller that imports an IPS authored by an external URN does not gain
125
+ * permission to rewrite or delete it. A correction requires independently
126
+ * verified source provenance; merely resubmitting the same URN is
127
+ * insufficient and unsigned/demo imports remain locally immutable.
122
128
  */
123
129
  importIpsOrFhirAndUpdateIndex(ctx, input) {
124
130
  assertFacadeCapability(this.capabilities, ActorCapabilities.IndividualImportIps, ActorKinds.IndividualController, 'importIpsOrFhirAndUpdateIndex');
@@ -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
+ }
@@ -208,6 +208,14 @@ export type IndividualMemberLifecycleInput = {
208
208
  dataType?: string;
209
209
  };
210
210
  export type IpsOrFhirImportInput = {
211
+ /**
212
+ * Server-built IPS/FHIR payload whose original Composition author is
213
+ * preserved as provenance. Importing an external `urn:*` author never makes
214
+ * the authenticated controller that author and does not grant local update
215
+ * or delete authority. Repeating the same author URN is not proof that a
216
+ * correction came from the same source; unsigned/demo imports therefore
217
+ * remain locally immutable under the strict audit contract.
218
+ */
211
219
  compositionPayload: {
212
220
  thid?: string;
213
221
  } & Record<string, unknown>;
@@ -298,6 +306,8 @@ type ClinicalUpdateRuntimeOptions = Readonly<{
298
306
  * `ifMatch` for optimistic concurrency. GW authorizes it against the creator
299
307
  * DID plus the caller's linked verified login identity; contact identifiers
300
308
  * are never written into the clinical resource.
309
+ * An externally authored `urn:*` may be preserved on first import, but a local
310
+ * controller cannot subsequently update or delete that source-authored fact.
301
311
  */
302
312
  export type ClinicalSectionUpdateInput = ClinicalSectionUpdateCommunicationInput & ClinicalUpdateRuntimeOptions;
303
313
  /** Updates one complete multi-section clinical summary document. */
@@ -44,7 +44,18 @@ export class ServerProfileSessionManager {
44
44
  const idToken = String(input.idToken || '').trim();
45
45
  if (!idToken)
46
46
  throw new Error('Profile enrollment requires a signed OIDC idToken for account/email binding.');
47
- const client = this.createClient(input.routeContext, idToken);
47
+ const client = new NodeHttpClient({
48
+ baseUrl: this.options.gatewayBaseUrl,
49
+ ctx: input.routeContext,
50
+ bearerToken: idToken,
51
+ appInfo: this.options.appInfo,
52
+ fetchImpl: this.options.fetchImpl,
53
+ transportProfile: TransportProfiles.DidcommEncryptedForm,
54
+ secureTransportAdapter: {
55
+ pack: (message) => wallet.packProfileBootstrapForRecipientWithContext({ ...message, iss: input.actorDid }, input.providerDid, { context }),
56
+ unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
57
+ },
58
+ });
48
59
  const redirectUris = input.redirectUris || input.dcrRedirectUris || [];
49
60
  const clientName = String(input.clientName || input.dcrClientName || '').trim();
50
61
  const activationRequest = createProfileDeviceActivationRequest({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
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.6.0",
41
+ "gdc-common-utils-ts": "2.6.2",
42
42
  "gdc-sdk-core-ts": "2.4.0"
43
43
  },
44
44
  "devDependencies": {