gdc-sdk-node-ts 2.3.12 → 2.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -572,6 +572,29 @@ Reusable payload examples:
572
572
  - [gdc-common-utils-ts/src/examples/lifecycle.ts](https://github.com/Global-DataCare/gdc-common-utils-ts/blob/main/src/examples/lifecycle.ts)
573
573
  - [gdc-common-utils-ts/src/examples/api-flow-examples.ts](https://github.com/Global-DataCare/gdc-common-utils-ts/blob/main/src/examples/api-flow-examples.ts)
574
574
 
575
+ ## Local terminology route for Next.js
576
+
577
+ `LocalTerminologyBffService` keeps complete JSON catalogs in the application
578
+ BFF and returns only matching options:
579
+
580
+ ```ts
581
+ const terminology = new LocalTerminologyBffService(catalogs);
582
+
583
+ export async function GET(request: Request) {
584
+ const url = new URL(request.url);
585
+ return Response.json(terminology.search({
586
+ text: url.searchParams.get('text') || '',
587
+ language: url.searchParams.get('language') || 'en',
588
+ jurisdiction: url.searchParams.get('jurisdiction') || undefined,
589
+ systems: url.searchParams.getAll('system'),
590
+ limit: 20,
591
+ }));
592
+ }
593
+ ```
594
+
595
+ The application validates which systems are allowed for the specific coded
596
+ form field. This MVP service performs no external terminology request.
597
+
575
598
  ## API Index
576
599
 
577
600
  ## Full Public Surface
package/dist/index.d.ts CHANGED
@@ -41,3 +41,4 @@ export * from './orchestration/individual-member-sdk.js';
41
41
  export * from './orchestration/personal-sdk.js';
42
42
  export * from './orchestration/professional-sdk.js';
43
43
  export * from './legacy-compat.js';
44
+ export * from './local-terminology-bff.js';
package/dist/index.js CHANGED
@@ -42,3 +42,4 @@ export * from './orchestration/individual-member-sdk.js';
42
42
  export * from './orchestration/personal-sdk.js';
43
43
  export * from './orchestration/professional-sdk.js';
44
44
  export * from './legacy-compat.js';
45
+ export * from './local-terminology-bff.js';
@@ -0,0 +1,17 @@
1
+ import { type TerminologyCatalogDocument, type TerminologySearchInput, type TerminologySearchResult } from 'gdc-sdk-core-ts';
2
+ /** Small primary document returned by an application-owned terminology route. */
3
+ export type TerminologySearchPrimaryDocument = Readonly<{
4
+ data: readonly TerminologySearchResult[];
5
+ }>;
6
+ /**
7
+ * Framework-neutral local terminology service for Node/BFF applications.
8
+ *
9
+ * A Next.js route validates its query, calls `search(...)`, and returns the
10
+ * resulting primary document. Complete catalogs remain server-side.
11
+ */
12
+ export declare class LocalTerminologyBffService {
13
+ private readonly provider;
14
+ constructor(catalogs: readonly TerminologyCatalogDocument[]);
15
+ /** Searches the configured local fallback catalogs. */
16
+ search(input: TerminologySearchInput): TerminologySearchPrimaryDocument;
17
+ }
@@ -0,0 +1,16 @@
1
+ import { LocalTerminologyProvider, } from 'gdc-sdk-core-ts';
2
+ /**
3
+ * Framework-neutral local terminology service for Node/BFF applications.
4
+ *
5
+ * A Next.js route validates its query, calls `search(...)`, and returns the
6
+ * resulting primary document. Complete catalogs remain server-side.
7
+ */
8
+ export class LocalTerminologyBffService {
9
+ constructor(catalogs) {
10
+ this.provider = new LocalTerminologyProvider(catalogs);
11
+ }
12
+ /** Searches the configured local fallback catalogs. */
13
+ search(input) {
14
+ return { data: this.provider.search(input) };
15
+ }
16
+ }
@@ -486,7 +486,9 @@ export class NodeManagedWallet {
486
486
  alg: algorithm,
487
487
  purpose: `${ownerId}:${purpose}`,
488
488
  seed: request.mode === 'deterministic' && request.seedMaterial !== undefined
489
- ? this.deriveSignerSeed(request.seedMaterial, ownerId, purpose)
489
+ ? (algorithm.startsWith('ML-DSA')
490
+ ? this.deriveSeedBytes(request.seedMaterial, ownerId, purpose, 32)
491
+ : this.deriveSignerSeed(request.seedMaterial, ownerId, purpose))
490
492
  : undefined,
491
493
  cryptography: this.cryptography,
492
494
  });
@@ -28,4 +28,6 @@ type RecoverOrganizationControllerWithIssueDeps = {
28
28
  } & Record<string, unknown>, pollOptions?: PollOptions) => Promise<SubmitAndPollResult>;
29
29
  };
30
30
  export declare function recoverOrganizationControllerWithIssueWithDeps(deps: RecoverOrganizationControllerWithIssueDeps): Promise<OrganizationControllerRecoveryResult>;
31
+ /** Reads the opaque activation code from a successful Organization/_issue poll result. */
32
+ export declare function readOrganizationIssueActivationCode(result: SubmitAndPollResult): string;
31
33
  export {};
@@ -8,7 +8,7 @@ export async function recoverOrganizationControllerWithIssueWithDeps(deps) {
8
8
  * - this flow must not depend on a new commercial Offer or Order step
9
9
  */
10
10
  const issue = await deps.submitLegalOrganizationIssue(deps.hostCtx, deps.input.issueInput, deps.input.issuePollOptions);
11
- const activationCode = readActivationCodeFromIssueResult(issue);
11
+ const activationCode = readOrganizationIssueActivationCode(issue);
12
12
  if (!activationCode) {
13
13
  throw new Error('recoverOrganizationControllerWithIssue: missing org.schema.IndividualProduct.serialNumber in Organization/_issue response.');
14
14
  }
@@ -32,7 +32,8 @@ export async function recoverOrganizationControllerWithIssueWithDeps(deps) {
32
32
  activation,
33
33
  };
34
34
  }
35
- function readActivationCodeFromIssueResult(result) {
35
+ /** Reads the opaque activation code from a successful Organization/_issue poll result. */
36
+ export function readOrganizationIssueActivationCode(result) {
36
37
  const pollBody = (result?.poll?.body || {});
37
38
  const body = (pollBody.body || pollBody);
38
39
  const data = Array.isArray(body.data) ? body.data : Array.isArray(pollBody.data) ? pollBody.data : [];
@@ -9,6 +9,8 @@ export type ServerActorMode = 'self' | 'controller' | 'member';
9
9
  /** Durable public metadata plus PIN-and-host protected private material. */
10
10
  export type ServerProfileRecord = Readonly<{
11
11
  profileId: string;
12
+ /** Stable key-derivation identity. Defaults to profileId for legacy records. */
13
+ walletKeyDerivationId?: string;
12
14
  ownerId: string;
13
15
  actorKind: ActorKind;
14
16
  actorMode: ServerActorMode;
@@ -64,6 +66,21 @@ export type ServerProfileEnrollmentInput = Readonly<{
64
66
  idToken: string;
65
67
  activationCode: string;
66
68
  vpToken: string;
69
+ /**
70
+ * Optional server-only recovery seed. It must be 32 bytes encoded as
71
+ * base64url and must never be accepted from an untrusted browser payload.
72
+ */
73
+ walletSeed?: string;
74
+ /** Stable identity used to reproduce the same keys independently of profile storage IDs. */
75
+ walletKeyDerivationId?: string;
76
+ }>;
77
+ export type ServerProfileEnrollmentPublicKey = Readonly<{
78
+ ownerScope: string;
79
+ purpose: string;
80
+ use: string;
81
+ alg: string;
82
+ kid: string;
83
+ publicJwk: Record<string, unknown>;
67
84
  }>;
68
85
  /** One explicit unlock request; scopes and subject remain session-bound. */
69
86
  export type ServerProfileUnlockInput = Readonly<{
@@ -120,6 +137,15 @@ export declare class ServerProfileSessionManager {
120
137
  private readonly options;
121
138
  constructor(options: ServerProfileSessionManagerOptions);
122
139
  enroll(input: ServerProfileEnrollmentInput): Promise<ServerProfileRecord>;
140
+ /**
141
+ * Derives only the public enrollment descriptors for a server-governed
142
+ * recovery seed. This is intended for pre-DCR controller binding requests;
143
+ * no private material or seed is returned.
144
+ */
145
+ prepareEnrollmentPublicKeys(input: Readonly<{
146
+ walletSeed: string;
147
+ walletKeyDerivationId: string;
148
+ }>): Promise<ServerProfileEnrollmentPublicKey[]>;
123
149
  listProfiles(ownerId: string): Promise<ServerProfileRecord[]>;
124
150
  unlock(input: ServerProfileUnlockInput): Promise<ResolvedServerProfileSession>;
125
151
  resolveSession(ownerId: string, sessionId: string): Promise<ResolvedServerProfileSession>;
@@ -18,9 +18,12 @@ export class ServerProfileSessionManager {
18
18
  }
19
19
  async enroll(input) {
20
20
  requireEnrollment(input);
21
- const seed = randomBytes(32).toString('base64url');
22
- const wallet = await this.createWallet(input.profileId, seed);
23
- const context = walletContext(input.profileId);
21
+ const seed = input.walletSeed || randomBytes(32).toString('base64url');
22
+ if (input.walletSeed)
23
+ requireBase64UrlSeed32(input.walletSeed);
24
+ const walletKeyDerivationId = normalizedWalletKeyDerivationId(input.walletKeyDerivationId, input.profileId);
25
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
26
+ const context = walletContext(walletKeyDerivationId);
24
27
  const publicKeys = await wallet.getPublicJwks(context, {});
25
28
  const storagePublicJwk = publicKeys.find((entry) => entry.purpose === 'document-at-rest')?.publicJwk;
26
29
  if (!storagePublicJwk)
@@ -45,6 +48,7 @@ export class ServerProfileSessionManager {
45
48
  const now = this.now();
46
49
  const record = {
47
50
  profileId: input.profileId,
51
+ walletKeyDerivationId,
48
52
  ownerId: input.ownerId,
49
53
  actorKind: input.actorKind,
50
54
  actorMode: input.actorMode,
@@ -67,6 +71,27 @@ export class ServerProfileSessionManager {
67
71
  await this.options.store.putProfile(record);
68
72
  return record;
69
73
  }
74
+ /**
75
+ * Derives only the public enrollment descriptors for a server-governed
76
+ * recovery seed. This is intended for pre-DCR controller binding requests;
77
+ * no private material or seed is returned.
78
+ */
79
+ async prepareEnrollmentPublicKeys(input) {
80
+ requireBase64UrlSeed32(input.walletSeed);
81
+ const walletKeyDerivationId = normalizedWalletKeyDerivationId(input.walletKeyDerivationId, '');
82
+ if (!walletKeyDerivationId)
83
+ throw new Error('prepareEnrollmentPublicKeys requires walletKeyDerivationId.');
84
+ const wallet = await this.createWallet(walletKeyDerivationId, input.walletSeed);
85
+ const descriptors = await wallet.getPublicJwks(walletContext(walletKeyDerivationId), {});
86
+ return descriptors.map((entry) => ({
87
+ ownerScope: entry.ownerScope,
88
+ purpose: entry.purpose,
89
+ use: entry.use,
90
+ alg: entry.alg,
91
+ kid: entry.kid,
92
+ publicJwk: entry.publicJwk,
93
+ }));
94
+ }
70
95
  listProfiles(ownerId) {
71
96
  return this.options.store.listProfiles(ownerId);
72
97
  }
@@ -95,7 +120,8 @@ export class ServerProfileSessionManager {
95
120
  throw new Error('Profile PIN rejected.');
96
121
  }
97
122
  profile = await this.ensureRequiredStorageProfile(profile, seed);
98
- const wallet = await this.createWallet(profile.profileId, seed);
123
+ const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
124
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
99
125
  const smartTokenEndpoint = [
100
126
  this.options.gatewayBaseUrl.replace(/\/+$/, ''),
101
127
  buildIdentityOpenIdSmartTokenPath(profile.routeContext),
@@ -143,8 +169,9 @@ export class ServerProfileSessionManager {
143
169
  }
144
170
  const profile = await this.requireOwnedProfile(ownerId, session.profileId);
145
171
  const seed = await this.options.sealer.unseal(session.sealedUnlockedWalletSeed, `${sessionId}:unlocked-wallet-seed`);
146
- const wallet = await this.createWallet(profile.profileId, seed);
147
- const context = walletContext(profile.profileId);
172
+ const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
173
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
174
+ const context = walletContext(walletKeyDerivationId);
148
175
  return {
149
176
  sessionId,
150
177
  profile,
@@ -174,9 +201,9 @@ export class ServerProfileSessionManager {
174
201
  fetchImpl: this.options.fetchImpl,
175
202
  });
176
203
  }
177
- async createWallet(profileId, seed) {
204
+ async createWallet(walletKeyDerivationId, seed) {
178
205
  const wallet = new NodeManagedWallet({ resolveRecipientJwk: this.options.resolveRecipientJwk });
179
- const context = walletContext(profileId);
206
+ const context = walletContext(walletKeyDerivationId);
180
207
  await wallet.provisionManagedKeys(context, {
181
208
  ownerScope: 'profile',
182
209
  purposes: ['actor-signing', 'document-at-rest'],
@@ -201,8 +228,9 @@ export class ServerProfileSessionManager {
201
228
  const required = this.options.requiredConfidentialStorageProfile ?? profile.confidentialStorageProfile ?? 'confidential-basic-v1';
202
229
  if (required !== 'confidential-pqc-v1')
203
230
  return profile;
204
- const wallet = await this.createWallet(profile.profileId, seed);
205
- const storageKeys = await wallet.getPublicJwks(walletContext(profile.profileId), {
231
+ const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
232
+ const wallet = await this.createWallet(walletKeyDerivationId, seed);
233
+ const storageKeys = await wallet.getPublicJwks(walletContext(walletKeyDerivationId), {
206
234
  ownerScope: 'profile', purpose: 'document-at-rest', alg: 'ML-KEM-768',
207
235
  });
208
236
  const storagePublicJwk = storageKeys[0]?.publicJwk;
@@ -234,7 +262,7 @@ export class ServerProfileSessionManager {
234
262
  }
235
263
  async function buildWalletClientAssertion(wallet, profile, audience, now) {
236
264
  const seconds = Math.floor(now.getTime() / 1000);
237
- return wallet.signCompactJws(walletContext(profile.profileId), {
265
+ return wallet.signCompactJws(walletContext(profile.walletKeyDerivationId || profile.profileId), {
238
266
  header: { alg: 'ES384', typ: 'JWT' },
239
267
  claims: {
240
268
  iss: profile.clientId,
@@ -247,12 +275,20 @@ async function buildWalletClientAssertion(wallet, profile, audience, now) {
247
275
  key: { ownerScope: 'runtime', purpose: 'openid-id-token-signing' },
248
276
  });
249
277
  }
250
- function walletContext(profileId) {
278
+ function walletContext(walletKeyDerivationId) {
251
279
  return {
252
- profile: { profileId },
253
- runtime: { runtimeId: `${profileId}:server-runtime`, runtimeType: 'backend-service' },
280
+ profile: { profileId: walletKeyDerivationId },
281
+ runtime: { runtimeId: `${walletKeyDerivationId}:server-runtime`, runtimeType: 'backend-service' },
254
282
  };
255
283
  }
284
+ function normalizedWalletKeyDerivationId(value, profileId) {
285
+ return String(value || profileId).trim();
286
+ }
287
+ function requireBase64UrlSeed32(seed) {
288
+ if (!/^[A-Za-z0-9_-]{43}$/.test(seed) || Buffer.from(seed, 'base64url').byteLength !== 32) {
289
+ throw new Error('Profile enrollment walletSeed must be a 32-byte base64url value.');
290
+ }
291
+ }
256
292
  function requireEnrollment(input) {
257
293
  for (const [name, value] of Object.entries({
258
294
  ownerId: input.ownerId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-sdk-node-ts",
3
- "version": "2.3.12",
3
+ "version": "2.3.14",
4
4
  "description": "Next-generation Node runtime package for the GDC SDK family",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Antifraud Services Inc.",
@@ -37,8 +37,8 @@
37
37
  "test:e2e:live-gw:clean": "bash ./scripts/run-live-gw-clean.sh"
38
38
  },
39
39
  "dependencies": {
40
- "gdc-common-utils-ts": "^2.3.17",
41
- "gdc-sdk-core-ts": "~2.3.12"
40
+ "gdc-common-utils-ts": "^2.3.18",
41
+ "gdc-sdk-core-ts": "~2.3.13"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^20.14.10",