gdc-sdk-node-ts 0.12.0 → 2.0.4

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.
Files changed (34) hide show
  1. package/README.md +118 -12
  2. package/dist/backend-profile-runtime.d.ts +262 -0
  3. package/dist/backend-profile-runtime.js +498 -0
  4. package/dist/consent-claim-helpers.d.ts +2 -1
  5. package/dist/family-organization-registration.d.ts +40 -0
  6. package/dist/family-organization-registration.js +54 -0
  7. package/dist/family-organization-search.d.ts +30 -0
  8. package/dist/family-organization-search.js +59 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.js +7 -0
  11. package/dist/individual-controller-backend-runtime.d.ts +59 -0
  12. package/dist/individual-controller-backend-runtime.js +67 -0
  13. package/dist/legal-organization-onboarding-facade.d.ts +70 -0
  14. package/dist/legal-organization-onboarding-facade.js +169 -0
  15. package/dist/node-runtime-client.d.ts +98 -2
  16. package/dist/node-runtime-client.js +178 -6
  17. package/dist/orchestration/client-port.d.ts +31 -1
  18. package/dist/orchestration/individual-controller-sdk.d.ts +23 -1
  19. package/dist/orchestration/individual-controller-sdk.js +28 -0
  20. package/dist/orchestration/organization-controller-sdk.d.ts +22 -1
  21. package/dist/orchestration/organization-controller-sdk.js +25 -0
  22. package/dist/orchestration/personal-sdk.d.ts +3 -1
  23. package/dist/orchestration/personal-sdk.js +4 -0
  24. package/dist/orchestration/professional-sdk.d.ts +16 -1
  25. package/dist/orchestration/professional-sdk.js +21 -0
  26. package/dist/organization-controller-backend-runtime.d.ts +65 -0
  27. package/dist/organization-controller-backend-runtime.js +83 -0
  28. package/dist/professional-backend-runtime.d.ts +36 -0
  29. package/dist/professional-backend-runtime.js +41 -0
  30. package/dist/profile-workspace.d.ts +82 -0
  31. package/dist/profile-workspace.js +127 -0
  32. package/dist/resource-operations.d.ts +79 -5
  33. package/dist/resource-operations.js +50 -5
  34. package/package.json +8 -3
@@ -0,0 +1,498 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { ActorKinds } from 'gdc-common-utils-ts/constants/actor-session';
3
+ import { ProfileAppTypes } from 'gdc-common-utils-ts/constants';
4
+ import { JobStatus } from 'gdc-common-utils-ts/models/confidential-job';
5
+ import { buildActorSessionDescriptorForActorKind, expandActorSessionDescriptorToFacades, prepareLoadedActorProfile, prepareLoadProfile, } from 'gdc-sdk-core-ts';
6
+ import { createActorSessionsFromFacades } from './gdc-session-bridge.js';
7
+ export const BackendSubjectIndexReadModes = Object.freeze({
8
+ LatestIps: 'latest-ips',
9
+ ClinicalBundle: 'clinical-bundle',
10
+ });
11
+ /**
12
+ * Default backend-generic runtime implementation backed by injected adapters.
13
+ *
14
+ * This class is the first concrete v2 slice intended for backend consumers that
15
+ * need one reusable actor-aware profile runtime after authentication.
16
+ */
17
+ export class BackendProfileRuntime {
18
+ constructor(adapters, options = {}) {
19
+ this.adapters = adapters;
20
+ this.options = options;
21
+ }
22
+ async loadProfile(input) {
23
+ const loadedProfile = await this.adapters.loadProfile(input);
24
+ return {
25
+ ...loadedProfile,
26
+ actorSessions: createActorSessionsFromFacades(loadedProfile.facades, this.options.facadeClient),
27
+ };
28
+ }
29
+ async closeProfile(_profileKey) { }
30
+ async registerTrustedDevice(input) {
31
+ return this.adapters.registerTrustedDevice(input);
32
+ }
33
+ async connectToSubjectIndex(input) {
34
+ return this.adapters.connectToSubjectIndex(input);
35
+ }
36
+ async getSubjectIndexComposition(input) {
37
+ return this.adapters.getSubjectIndexComposition(input);
38
+ }
39
+ }
40
+ /**
41
+ * Current concrete backend profile runtime over one injected runtime client.
42
+ *
43
+ * This is the pragmatic v2 bridge for backend consumers that already possess
44
+ * an authenticated `RuntimeClient` and need `loadProfile(...)` to materialize
45
+ * actor facades immediately against the current GW CORE contract.
46
+ */
47
+ export class DirectBackendProfileRuntime {
48
+ constructor(options) {
49
+ this.loadedProfiles = new Map();
50
+ this.options = options;
51
+ }
52
+ async loadProfile(input) {
53
+ const normalized = prepareLoadProfile(input);
54
+ const profileId = String(normalized.profileId
55
+ || normalized.profileDid
56
+ || normalized.subjectDid
57
+ || normalized.providerDid).trim();
58
+ const resolvedAppType = normalized.appType || ProfileAppTypes.Family;
59
+ const descriptor = {
60
+ profileId,
61
+ actorKind: normalized.actorKind,
62
+ actorRole: normalized.actorRole,
63
+ providerDid: normalized.providerDid,
64
+ runtimeClass: normalized.runtimeClass,
65
+ profileDid: normalized.profileDid,
66
+ subjectDid: normalized.subjectDid,
67
+ email: normalized.email,
68
+ phone: normalized.phone,
69
+ deviceDid: normalized.deviceDid,
70
+ appType: resolvedAppType,
71
+ };
72
+ const session = buildActorSessionDescriptorForActorKind({
73
+ actorKind: normalized.actorKind,
74
+ appType: resolvedAppType,
75
+ profileId: descriptor.profileId,
76
+ profileDid: descriptor.profileDid,
77
+ role: descriptor.actorRole,
78
+ });
79
+ const facades = expandActorSessionDescriptorToFacades(session);
80
+ const jobManager = this.options.createJobManager
81
+ ? this.options.createJobManager(descriptor, normalized)
82
+ : createJobManagerInMemory(descriptor);
83
+ const loadedProfile = prepareLoadedActorProfile({
84
+ descriptor,
85
+ session,
86
+ facades,
87
+ jobManager,
88
+ });
89
+ const backendProfile = {
90
+ ...loadedProfile,
91
+ actorSessions: createActorSessionsFromFacades(loadedProfile.facades, this.options.facadeClient),
92
+ };
93
+ this.rememberLoadedProfile(backendProfile);
94
+ return backendProfile;
95
+ }
96
+ async closeProfile(profileKey) {
97
+ const profile = this.resolveLoadedProfile(profileKey);
98
+ profile.jobManager.shutdown();
99
+ this.forgetLoadedProfile(profile);
100
+ }
101
+ async registerTrustedDevice(input) {
102
+ if (this.options.registerTrustedDevice) {
103
+ return this.options.registerTrustedDevice(input);
104
+ }
105
+ return {
106
+ trustedDeviceId: input.deviceDid,
107
+ status: 'already-trusted',
108
+ };
109
+ }
110
+ async connectToSubjectIndex(input) {
111
+ if (this.options.connectToSubjectIndex) {
112
+ return this.options.connectToSubjectIndex(input);
113
+ }
114
+ return {
115
+ subjectId: input.subjectId,
116
+ userId: input.userId,
117
+ userRoleCode: input.userRoleCode,
118
+ status: 'already-connected',
119
+ };
120
+ }
121
+ async getSubjectIndexComposition(input) {
122
+ if (this.options.getSubjectIndexComposition) {
123
+ return this.options.getSubjectIndexComposition(input);
124
+ }
125
+ const routeContext = this.options.defaultRouteContext;
126
+ if (!routeContext) {
127
+ throw new Error('DirectBackendProfileRuntime requires defaultRouteContext to read subject index data.');
128
+ }
129
+ const profile = this.resolveLoadedProfile(input.userId);
130
+ if (profile.descriptor.actorKind !== ActorKinds.IndividualController) {
131
+ throw new Error(`DirectBackendProfileRuntime currently resolves subject index reads through IndividualController only, not '${profile.descriptor.actorKind}'.`);
132
+ }
133
+ const sdk = requireBackendIndividualControllerSdk(profile);
134
+ if (this.options.subjectIndexReadMode === BackendSubjectIndexReadModes.ClinicalBundle) {
135
+ const searchResult = await sdk.searchClinicalBundle(routeContext, {
136
+ subject: input.subjectId,
137
+ });
138
+ return {
139
+ subjectId: input.subjectId,
140
+ userId: input.userId,
141
+ userRoleCode: input.userRoleCode,
142
+ composition: searchResult.poll.body,
143
+ };
144
+ }
145
+ const latestIps = await sdk.getLatestIps(routeContext, {
146
+ subject: input.subjectId,
147
+ });
148
+ return {
149
+ subjectId: input.subjectId,
150
+ userId: input.userId,
151
+ userRoleCode: input.userRoleCode,
152
+ composition: latestIps.poll.body,
153
+ };
154
+ }
155
+ rememberLoadedProfile(profile) {
156
+ const keys = new Set([
157
+ profile.descriptor.profileId,
158
+ String(profile.descriptor.profileDid || '').trim(),
159
+ String(profile.descriptor.subjectDid || '').trim(),
160
+ String(profile.descriptor.email || '').trim(),
161
+ String(profile.descriptor.phone || '').trim(),
162
+ ].filter(Boolean));
163
+ for (const key of keys) {
164
+ this.loadedProfiles.set(key, profile);
165
+ }
166
+ }
167
+ forgetLoadedProfile(profile) {
168
+ const keys = new Set([
169
+ profile.descriptor.profileId,
170
+ String(profile.descriptor.profileDid || '').trim(),
171
+ String(profile.descriptor.subjectDid || '').trim(),
172
+ String(profile.descriptor.email || '').trim(),
173
+ String(profile.descriptor.phone || '').trim(),
174
+ ].filter(Boolean));
175
+ for (const key of keys) {
176
+ this.loadedProfiles.delete(key);
177
+ }
178
+ }
179
+ resolveLoadedProfile(userId) {
180
+ const normalizedUserId = String(userId || '').trim();
181
+ const direct = this.loadedProfiles.get(normalizedUserId);
182
+ if (direct) {
183
+ return direct;
184
+ }
185
+ throw new Error(`DirectBackendProfileRuntime has not loaded one backend profile for '${normalizedUserId}'.`);
186
+ }
187
+ }
188
+ /**
189
+ * Preferred developer-facing factory for the current backend profile runtime.
190
+ *
191
+ * Use this helper in tutorials and app/BFF code when you already have one
192
+ * configured runtime client and want the canonical
193
+ * `loadProfile(...) -> session -> actor facade` entrypoint without exposing the
194
+ * concrete class name in every example.
195
+ */
196
+ export function createBackendProfileRuntime(options) {
197
+ return new DirectBackendProfileRuntime(options);
198
+ }
199
+ /**
200
+ * Minimal in-memory `JobManager` for backend runtimes that do not need durable
201
+ * persistence during one live session.
202
+ */
203
+ export function createJobManagerInMemory(descriptor) {
204
+ let isInitialized = false;
205
+ let sequence = 0;
206
+ const jobs = new Map();
207
+ let listener;
208
+ function notify() {
209
+ listener?.();
210
+ }
211
+ function nextSequence() {
212
+ sequence += 1;
213
+ return sequence;
214
+ }
215
+ function inferFormType(content) {
216
+ const first = Array.isArray(content?.body?.data) ? content.body.data[0] : undefined;
217
+ return String(first?.type || content?.type || '').trim();
218
+ }
219
+ function cloneJob(job) {
220
+ return {
221
+ ...job,
222
+ indexed: job.indexed ? structuredClone(job.indexed) : job.indexed,
223
+ content: job.content ? structuredClone(job.content) : job.content,
224
+ jwe: job.jwe ? structuredClone(job.jwe) : job.jwe,
225
+ };
226
+ }
227
+ function matchesQuery(job, query) {
228
+ const conditions = Array.isArray(query.where) ? query.where : [];
229
+ return conditions.every((condition) => {
230
+ const currentValue = job[condition.attribute];
231
+ if ('equals' in condition) {
232
+ return currentValue === condition.equals;
233
+ }
234
+ if ('in' in condition) {
235
+ return Array.isArray(condition.in) && condition.in.includes(currentValue);
236
+ }
237
+ return true;
238
+ });
239
+ }
240
+ return {
241
+ descriptor: { ...descriptor },
242
+ get isInitialized() {
243
+ return isInitialized;
244
+ },
245
+ async initialize() {
246
+ isInitialized = true;
247
+ },
248
+ shutdown() {
249
+ isInitialized = false;
250
+ jobs.clear();
251
+ },
252
+ setListener(nextListener) {
253
+ listener = nextListener;
254
+ },
255
+ async createJob(content, selector) {
256
+ const now = Date.now();
257
+ const job = {
258
+ id: createRuntimeUuid(),
259
+ thid: String(content?.thid || '').trim() || undefined,
260
+ status: JobStatus.DRAFT,
261
+ sequence: nextSequence(),
262
+ createdAtTimestamp: now,
263
+ content: structuredClone(content),
264
+ ...selector,
265
+ };
266
+ jobs.set(job.id, job);
267
+ notify();
268
+ return cloneJob(job);
269
+ },
270
+ async findDraftJobByFormType(formType) {
271
+ const normalizedFormType = String(formType || '').trim();
272
+ for (const job of jobs.values()) {
273
+ if (job.status !== JobStatus.DRAFT)
274
+ continue;
275
+ if (inferFormType(job.content) === normalizedFormType) {
276
+ return cloneJob(job);
277
+ }
278
+ }
279
+ return null;
280
+ },
281
+ async createOrUpdateDraftJob(content, selector) {
282
+ const formType = inferFormType(content);
283
+ const existing = formType ? await this.findDraftJobByFormType(formType) : null;
284
+ if (!existing) {
285
+ return this.createJob(content, selector);
286
+ }
287
+ const current = jobs.get(existing.id);
288
+ if (!current) {
289
+ return this.createJob(content, selector);
290
+ }
291
+ const updated = {
292
+ ...current,
293
+ ...selector,
294
+ thid: String(content?.thid || '').trim() || current.thid,
295
+ content: structuredClone(content),
296
+ previousSequence: current.sequence,
297
+ sequence: nextSequence(),
298
+ };
299
+ jobs.set(updated.id, updated);
300
+ notify();
301
+ return cloneJob(updated);
302
+ },
303
+ async sync() { },
304
+ async queryJobs(query) {
305
+ const filtered = [...jobs.values()].filter(job => matchesQuery(job, query));
306
+ if (query.orderBy) {
307
+ const { attribute, direction } = query.orderBy;
308
+ filtered.sort((left, right) => {
309
+ const a = left[attribute];
310
+ const b = right[attribute];
311
+ if (a === b)
312
+ return 0;
313
+ const cmp = a > b ? 1 : -1;
314
+ return direction === 'desc' ? -cmp : cmp;
315
+ });
316
+ }
317
+ const offset = Math.max(0, Number(query.offset || 0));
318
+ const limit = query.limit == null ? filtered.length : Math.max(0, Number(query.limit));
319
+ return filtered.slice(offset, offset + limit).map(cloneJob);
320
+ },
321
+ async submitJob(job) {
322
+ const current = jobs.get(job.id);
323
+ if (!current) {
324
+ throw new Error(`JobManager in-memory store cannot submit unknown job '${job.id}'.`);
325
+ }
326
+ jobs.set(job.id, {
327
+ ...current,
328
+ status: JobStatus.SENT,
329
+ previousSequence: current.sequence,
330
+ sequence: nextSequence(),
331
+ });
332
+ notify();
333
+ },
334
+ async sealJobWithToken(job) {
335
+ return job;
336
+ },
337
+ async getJobResponseByThid(thid) {
338
+ for (const job of jobs.values()) {
339
+ if (job.thid === thid && job.responseMessageId) {
340
+ return {
341
+ id: job.responseMessageId,
342
+ thid,
343
+ };
344
+ }
345
+ }
346
+ return null;
347
+ },
348
+ generateId() {
349
+ return createRuntimeUuid();
350
+ },
351
+ };
352
+ }
353
+ /**
354
+ * Requires one backend profile-runtime method from one runtime client.
355
+ */
356
+ export function requireBackendProfileRuntimeMethod(client, method) {
357
+ const candidate = client[method];
358
+ if (typeof candidate !== 'function') {
359
+ throw new Error(`BackendProfileRuntimeClient does not implement '${String(method)}'.`);
360
+ }
361
+ return candidate.bind(client);
362
+ }
363
+ /**
364
+ * Canonical backend helper for loading one actor profile after authentication.
365
+ */
366
+ export async function loadBackendProfile(client, input) {
367
+ return requireBackendProfileRuntimeMethod(client, 'loadProfile')(input);
368
+ }
369
+ /**
370
+ * Canonical backend helper for closing one loaded actor profile and clearing
371
+ * runtime-owned in-memory state.
372
+ */
373
+ export async function closeBackendProfile(client, profileKey) {
374
+ return requireBackendProfileRuntimeMethod(client, 'closeProfile')(profileKey);
375
+ }
376
+ /**
377
+ * Canonical backend helper for registering one trusted device/runtime context.
378
+ */
379
+ export async function registerBackendTrustedDevice(client, input) {
380
+ return requireBackendProfileRuntimeMethod(client, 'registerTrustedDevice')(input);
381
+ }
382
+ /**
383
+ * Canonical backend helper for connecting one actor profile to one subject
384
+ * index after the profile is already loaded.
385
+ */
386
+ export async function connectBackendToSubjectIndex(client, input) {
387
+ return requireBackendProfileRuntimeMethod(client, 'connectToSubjectIndex')(input);
388
+ }
389
+ /**
390
+ * Canonical backend helper for reading one subject index composition after the
391
+ * relationship is already established.
392
+ */
393
+ export async function getBackendSubjectIndexComposition(client, input) {
394
+ return requireBackendProfileRuntimeMethod(client, 'getSubjectIndexComposition')(input);
395
+ }
396
+ /**
397
+ * Returns one materialized backend actor session by actor kind.
398
+ *
399
+ * Backend callers should use this instead of scanning `actorSessions` manually.
400
+ */
401
+ export function requireBackendActorSession(profile, actorKind) {
402
+ const session = profile.actorSessions.find(candidate => candidate.actorKind === actorKind);
403
+ if (!session) {
404
+ throw new Error(`Loaded backend profile does not expose actor kind '${actorKind}'.`);
405
+ }
406
+ return session;
407
+ }
408
+ /**
409
+ * Returns the individual-controller session from one loaded backend profile.
410
+ *
411
+ * This helper is the first concrete bridge from the generic backend runtime to
412
+ * the current individual bootstrap/index use case.
413
+ */
414
+ export function requireBackendIndividualControllerSession(profile) {
415
+ return requireBackendActorSession(profile, ActorKinds.IndividualController);
416
+ }
417
+ /**
418
+ * Materializes the individual-controller facade directly from one loaded
419
+ * backend profile.
420
+ */
421
+ export function requireBackendIndividualControllerSdk(profile) {
422
+ return requireBackendIndividualControllerSession(profile).asIndividualController();
423
+ }
424
+ /**
425
+ * Returns the organization-controller session from one loaded backend profile.
426
+ */
427
+ export function requireBackendOrganizationControllerSession(profile) {
428
+ return requireBackendActorSession(profile, ActorKinds.OrganizationController);
429
+ }
430
+ /**
431
+ * Materializes the organization-controller facade directly from one loaded
432
+ * backend profile.
433
+ */
434
+ export function requireBackendOrganizationControllerSdk(profile) {
435
+ return requireBackendOrganizationControllerSession(profile).asOrganizationController();
436
+ }
437
+ /**
438
+ * Returns the professional session from one loaded backend profile.
439
+ */
440
+ export function requireBackendProfessionalSession(profile) {
441
+ return requireBackendActorSession(profile, ActorKinds.Professional);
442
+ }
443
+ /**
444
+ * Materializes the professional facade directly from one loaded backend
445
+ * profile.
446
+ */
447
+ export function requireBackendProfessionalSdk(profile) {
448
+ return requireBackendProfessionalSession(profile).asProfessional();
449
+ }
450
+ /**
451
+ * Loads one backend profile and resolves the individual-controller session in
452
+ * one step.
453
+ *
454
+ * This is the first pragmatic use-case helper on top of the generic backend
455
+ * profile runtime, because the current stable CORE bootstrap baseline starts
456
+ * from the individual-controller flow.
457
+ */
458
+ export async function loadBackendIndividualControllerProfile(client, input) {
459
+ const profile = await loadBackendProfile(client, input);
460
+ const session = requireBackendIndividualControllerSession(profile);
461
+ return {
462
+ profile,
463
+ session,
464
+ sdk: session.asIndividualController(),
465
+ };
466
+ }
467
+ /**
468
+ * Loads one backend profile and resolves the organization-controller session in
469
+ * one step.
470
+ */
471
+ export async function loadBackendOrganizationControllerProfile(client, input) {
472
+ const profile = await loadBackendProfile(client, input);
473
+ const session = requireBackendOrganizationControllerSession(profile);
474
+ return {
475
+ profile,
476
+ session,
477
+ sdk: session.asOrganizationController(),
478
+ };
479
+ }
480
+ /**
481
+ * Loads one backend profile and resolves the professional session in one step.
482
+ */
483
+ export async function loadBackendProfessionalProfile(client, input) {
484
+ const profile = await loadBackendProfile(client, input);
485
+ const session = requireBackendProfessionalSession(profile);
486
+ return {
487
+ profile,
488
+ session,
489
+ sdk: session.asProfessional(),
490
+ };
491
+ }
492
+ function createRuntimeUuid() {
493
+ const fromCrypto = globalThis.crypto?.randomUUID?.();
494
+ if (fromCrypto) {
495
+ return fromCrypto;
496
+ }
497
+ return `fallback-${Date.now()}-${Math.random().toString(16).slice(2)}`;
498
+ }
@@ -1,4 +1,5 @@
1
1
  /**
2
2
  * Node runtime re-export of canonical consent claim helpers.
3
3
  */
4
- export { addActorIdentifierList, addActorRoleList, addActors, addCategories, addCategoryList, addClaimValues, addPurposeList, addPurposes, addRoles, addSections, getActorIdentifierList, getActorRoleList, getActors, getCategories, getCategoryList, getClaimValues, getConsentDate, getConsentIdentifier, getConsentPeriodEnd, getConsentPeriodStart, getPurposeList, getPurposes, getRoles, getSections, setActorIdentifierList, setActorRoleList, setActors, setCategories, setCategoryList, setClaimValues, setConsentDate, setConsentIdentifier, setConsentPeriodEnd, setConsentPeriodStart, setPurposeList, setPurposes, setRoles, setSections, type ConsentInteroperableClaims, } from 'gdc-sdk-core-ts';
4
+ export { addActorIdentifierList, addActorRoleList, addActors, addCategories, addCategoryList, addClaimValues, addPurposeList, addPurposes, addRoles, addSections, getActorIdentifierList, getActorRoleList, getActors, getCategories, getCategoryList, getClaimValues, getConsentDate, getConsentIdentifier, getConsentPeriodEnd, getConsentPeriodStart, getPurposeList, getPurposes, getRoles, getSections, setActorIdentifierList, setActorRoleList, setActors, setCategories, setCategoryList, setClaimValues, setConsentDate, setConsentIdentifier, setConsentPeriodEnd, setConsentPeriodStart, setPurposeList, setPurposes, setRoles, setSections, } from 'gdc-sdk-core-ts';
5
+ export type { ConsentInteroperableClaims, } from 'gdc-sdk-core-ts';
@@ -0,0 +1,40 @@
1
+ import type { FamilyOrganizationSummary } from 'gdc-common-utils-ts/utils/family-organization-summary';
2
+ import type { RouteContext } from './individual-onboarding.js';
3
+ import { type IndividualOrganizationStartResult } from './individual-start.js';
4
+ import { type FamilyOrganizationSearchInput } from './family-organization-search.js';
5
+ import type { PollOptions, SubmitAndPollResult } from './orchestration/client-port.js';
6
+ export type EnsureFamilyOrganizationRegistrationInput = FamilyOrganizationSearchInput & Readonly<{
7
+ controllerEmail?: string;
8
+ controllerRole?: string;
9
+ serviceProviderDid?: string;
10
+ tenantId?: string;
11
+ jurisdiction?: string;
12
+ sector?: string;
13
+ additionalClaims?: Record<string, unknown>;
14
+ }>;
15
+ export type EnsureFamilyOrganizationRegistrationResult = Readonly<{
16
+ status: 'already_exists' | 'resume_required' | 'new_created';
17
+ summary?: FamilyOrganizationSummary;
18
+ started?: IndividualOrganizationStartResult;
19
+ }>;
20
+ type EnsureFamilyOrganizationRegistrationDeps = {
21
+ routeCtx: RouteContext;
22
+ input: EnsureFamilyOrganizationRegistrationInput;
23
+ defaultTimeoutMs?: number;
24
+ defaultIntervalMs?: number;
25
+ individualFamilyOrganizationSearchPath: (ctx: RouteContext) => string;
26
+ individualFamilyOrganizationSearchPollPath: (ctx: RouteContext) => string;
27
+ individualFamilyOrganizationBatchPath: (ctx: RouteContext) => string;
28
+ individualFamilyOrganizationPollPath: (ctx: RouteContext) => string;
29
+ submitAndPoll: (submitPath: string, pollPath: string, payload: {
30
+ thid?: string;
31
+ } & Record<string, unknown>, options?: PollOptions) => Promise<SubmitAndPollResult>;
32
+ };
33
+ /**
34
+ * High-level controller orchestration for phone-first onboarding channels:
35
+ * - first search whether the family/individual registration already exists
36
+ * - if it exists, return the normalized summary
37
+ * - otherwise start the bootstrap flow using the same business input
38
+ */
39
+ export declare function ensureFamilyOrganizationRegistrationWithDeps(deps: EnsureFamilyOrganizationRegistrationDeps): Promise<EnsureFamilyOrganizationRegistrationResult>;
40
+ export {};
@@ -0,0 +1,54 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { startIndividualOrganizationWithDeps, } from './individual-start.js';
3
+ import { searchFamilyOrganizationWithDeps, } from './family-organization-search.js';
4
+ import { extractOfferIdFromResponseBody, extractOfferPreviewFromResponseBody } from './order-offer-summary.js';
5
+ /**
6
+ * High-level controller orchestration for phone-first onboarding channels:
7
+ * - first search whether the family/individual registration already exists
8
+ * - if it exists, return the normalized summary
9
+ * - otherwise start the bootstrap flow using the same business input
10
+ */
11
+ export async function ensureFamilyOrganizationRegistrationWithDeps(deps) {
12
+ const summary = await searchFamilyOrganizationWithDeps({
13
+ routeCtx: deps.routeCtx,
14
+ input: deps.input,
15
+ defaultTimeoutMs: deps.defaultTimeoutMs,
16
+ defaultIntervalMs: deps.defaultIntervalMs,
17
+ individualFamilyOrganizationSearchPath: deps.individualFamilyOrganizationSearchPath,
18
+ individualFamilyOrganizationSearchPollPath: deps.individualFamilyOrganizationSearchPollPath,
19
+ submitAndPoll: deps.submitAndPoll,
20
+ });
21
+ if (summary?.status === 'already_exists' || summary?.status === 'resume_required') {
22
+ return {
23
+ status: summary.status,
24
+ summary,
25
+ };
26
+ }
27
+ const started = await startIndividualOrganizationWithDeps({
28
+ input: {
29
+ serviceProviderDid: deps.input.serviceProviderDid,
30
+ tenantId: deps.input.tenantId,
31
+ jurisdiction: deps.input.jurisdiction,
32
+ sector: deps.input.sector,
33
+ alternateName: deps.input.usualname,
34
+ controllerEmail: deps.input.controllerEmail,
35
+ controllerTelephone: deps.input.controllerPhone,
36
+ controllerRole: deps.input.controllerRole,
37
+ additionalClaims: deps.input.additionalClaims,
38
+ timeoutSeconds: deps.input.timeoutSeconds,
39
+ intervalSeconds: deps.input.intervalSeconds,
40
+ },
41
+ routeCtx: deps.routeCtx,
42
+ defaultTimeoutMs: deps.defaultTimeoutMs,
43
+ defaultIntervalMs: deps.defaultIntervalMs,
44
+ individualFamilyOrganizationBatchPath: deps.individualFamilyOrganizationBatchPath,
45
+ individualFamilyOrganizationPollPath: deps.individualFamilyOrganizationPollPath,
46
+ submitAndPoll: deps.submitAndPoll,
47
+ getOfferIdFromResponse: (result) => extractOfferIdFromResponseBody(result.poll.body),
48
+ getOfferPreviewFromResponse: (result) => extractOfferPreviewFromResponseBody(result.poll.body),
49
+ });
50
+ return {
51
+ status: 'new_created',
52
+ started,
53
+ };
54
+ }
@@ -0,0 +1,30 @@
1
+ import { type FamilyOrganizationSummary } from 'gdc-common-utils-ts/utils/family-organization-summary';
2
+ import type { PollOptions, SubmitAndPollResult } from './orchestration/client-port.js';
3
+ import type { RouteContext } from './individual-onboarding.js';
4
+ export type FamilyOrganizationSearchInput = Readonly<{
5
+ controllerPhone: string;
6
+ usualname: string;
7
+ birthDate?: string;
8
+ timeoutSeconds?: number;
9
+ intervalSeconds?: number;
10
+ }>;
11
+ type SearchFamilyOrganizationWithDeps = {
12
+ routeCtx: RouteContext;
13
+ input: FamilyOrganizationSearchInput;
14
+ defaultTimeoutMs?: number;
15
+ defaultIntervalMs?: number;
16
+ individualFamilyOrganizationSearchPath: (ctx: RouteContext) => string;
17
+ individualFamilyOrganizationSearchPollPath: (ctx: RouteContext) => string;
18
+ submitAndPoll: (submitPath: string, pollPath: string, payload: {
19
+ thid?: string;
20
+ } & Record<string, unknown>, options?: PollOptions) => Promise<SubmitAndPollResult>;
21
+ };
22
+ /**
23
+ * Searches one existing family/individual organization registration by the
24
+ * current phone-first business key used by UHC/UNID channel flows.
25
+ *
26
+ * Returns one normalized summary when the registration exists, otherwise
27
+ * `null`.
28
+ */
29
+ export declare function searchFamilyOrganizationWithDeps(deps: SearchFamilyOrganizationWithDeps): Promise<FamilyOrganizationSummary | null>;
30
+ export {};
@@ -0,0 +1,59 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { readFamilyOrganizationSummaryFromResponseBody, } from 'gdc-common-utils-ts/utils/family-organization-summary';
3
+ import { ClaimsOrganizationSchemaorg, ClaimsServiceSchemaorg } from 'gdc-common-utils-ts/constants';
4
+ import { resolvePollOptionsFromSeconds } from './poll-options.js';
5
+ /**
6
+ * Searches one existing family/individual organization registration by the
7
+ * current phone-first business key used by UHC/UNID channel flows.
8
+ *
9
+ * Returns one normalized summary when the registration exists, otherwise
10
+ * `null`.
11
+ */
12
+ export async function searchFamilyOrganizationWithDeps(deps) {
13
+ const controllerPhone = String(deps.input.controllerPhone || '').trim();
14
+ const usualname = String(deps.input.usualname || '').trim();
15
+ const birthDate = String(deps.input.birthDate || '').trim();
16
+ if (!controllerPhone) {
17
+ throw new Error('searchFamilyOrganization requires controllerPhone.');
18
+ }
19
+ if (!usualname) {
20
+ throw new Error('searchFamilyOrganization requires usualname.');
21
+ }
22
+ const claims = {
23
+ '@context': 'org.schema',
24
+ [ClaimsOrganizationSchemaorg.ownerTelephone]: controllerPhone,
25
+ [ClaimsOrganizationSchemaorg.alternateName]: usualname,
26
+ [ClaimsServiceSchemaorg.category]: deps.routeCtx.sector,
27
+ ...(birthDate ? { 'org.schema.Organization.foundingDate': birthDate } : {}),
28
+ };
29
+ const payload = {
30
+ jti: `jti-${createRuntimeUuid()}`,
31
+ thid: `family-search-${createRuntimeUuid()}`,
32
+ iss: deps.routeCtx.tenantId,
33
+ aud: deps.routeCtx.tenantId,
34
+ type: 'application/api+json',
35
+ body: {
36
+ data: [{
37
+ type: 'Family-search-v1.0',
38
+ meta: { claims },
39
+ resource: { meta: { claims } },
40
+ }],
41
+ },
42
+ };
43
+ const pollOptions = resolvePollOptionsFromSeconds(deps.input.timeoutSeconds, deps.input.intervalSeconds, {
44
+ timeoutMs: deps.defaultTimeoutMs,
45
+ intervalMs: deps.defaultIntervalMs,
46
+ });
47
+ const result = await deps.submitAndPoll(deps.individualFamilyOrganizationSearchPath(deps.routeCtx), deps.individualFamilyOrganizationSearchPollPath(deps.routeCtx), payload, pollOptions);
48
+ if (result.poll.status !== 200) {
49
+ return null;
50
+ }
51
+ return readFamilyOrganizationSummaryFromResponseBody(result.poll.body);
52
+ }
53
+ function createRuntimeUuid() {
54
+ const fromCrypto = globalThis.crypto?.randomUUID?.();
55
+ if (fromCrypto) {
56
+ return fromCrypto;
57
+ }
58
+ return `fallback-${Date.now()}-${Math.random().toString(16).slice(2)}`;
59
+ }