gdc-sdk-node-ts 2.4.36 → 2.4.39
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 +15 -12
- package/dist/authorized-subject-directory.d.ts +51 -0
- package/dist/authorized-subject-directory.js +116 -0
- package/dist/device-activation.d.ts +22 -0
- package/dist/device-activation.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node-runtime-client.d.ts +10 -1
- package/dist/node-runtime-client.js +27 -1
- package/dist/orchestration/client-port.d.ts +3 -0
- package/dist/runtime-client-paths.d.ts +2 -0
- package/dist/runtime-client-paths.js +3 -1
- package/dist/runtime-paths.d.ts +4 -0
- package/dist/runtime-paths.js +8 -0
- package/dist/server-profile-session.d.ts +25 -0
- package/dist/server-profile-session.js +105 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,40 +72,43 @@ If you are integrating this package for the first time, open these in order:
|
|
|
72
72
|
3. [docs/101-PROFESSIONAL-CONSENT-SMART.md](./docs/101-PROFESSIONAL-CONSENT-SMART.md)
|
|
73
73
|
Canonical professional DID, consent, VP and SMART flow without literal
|
|
74
74
|
sections or caller-built audience URLs.
|
|
75
|
-
4. [docs/101-
|
|
75
|
+
4. [docs/101-AUTHORIZED_SUBJECT_DIRECTORY.md](./docs/101-AUTHORIZED_SUBJECT_DIRECTORY.md)
|
|
76
|
+
Signed OpenID account discovery of already-authorized subjects without
|
|
77
|
+
treating the account token as VP, SMART or wallet proof.
|
|
78
|
+
5. [docs/101-SDK_INTEGRATION.md](./docs/101-SDK_INTEGRATION.md)
|
|
76
79
|
Real backend setup plus the public runtime entrypoints:
|
|
77
80
|
`HostOnboardingSdk`, `OrganizationControllerSdk`,
|
|
78
81
|
`IndividualControllerSdk`, `ProfessionalSdk`, route-context usage, and the
|
|
79
82
|
canonical `ProfileRuntime -> loadProfile(...) -> workspace/session -> actor facade -> submit/poll` shape.
|
|
80
|
-
|
|
83
|
+
6. [tests/101-live-full-cycle-bff-runtime.e2e.test.mjs](./tests/101-live-full-cycle-bff-runtime.e2e.test.mjs)
|
|
81
84
|
Canonical live backend/BFF walkthrough on a fresh local GW lifecycle:
|
|
82
85
|
host/tenant activation, employee provisioning, individual bootstrap,
|
|
83
86
|
consent grant, professional SMART token, clinical read, and final cleanup.
|
|
84
|
-
|
|
87
|
+
7. [docs/101-LIVE_GW_LOCAL.md](./docs/101-LIVE_GW_LOCAL.md)
|
|
85
88
|
Exact TTY/local/Docker commands for running the SDK against a real local GW
|
|
86
89
|
CORE, including tenant bootstrap and employee-seat setup.
|
|
87
|
-
|
|
90
|
+
8. [docs/101-DISCOVERY.md](./docs/101-DISCOVERY.md)
|
|
88
91
|
Node/BFF dataspace discovery, hosting-operator resolution, provider
|
|
89
92
|
resolution, and the correct integration boundary for fallback and cache.
|
|
90
|
-
|
|
93
|
+
9. [gdc-sdk-core-ts/docs/101-SDK_FLOWS.md](https://github.com/Global-DataCare/gdc-sdk-core-ts/blob/main/docs/101-SDK_FLOWS.md)
|
|
91
94
|
Actor split and business-flow map across organization, individual,
|
|
92
95
|
permissions, invitation, import, and SMART flows.
|
|
93
|
-
|
|
96
|
+
10. [gwtemplate-node-ts/docs/PORTAL_API_TO_GW_CORE.md](https://github.com/Global-DataCare/gwtemplate-node-ts/blob/main/docs/PORTAL_API_TO_GW_CORE.md)
|
|
94
97
|
Canonical portal/BFF functional map over GW CORE, including the domain
|
|
95
98
|
split between `employees`, `related persons`, `members`, and `consents`.
|
|
96
|
-
|
|
99
|
+
11. [gdc-common-utils-ts/src/examples/](https://github.com/Global-DataCare/gdc-common-utils-ts/tree/main/src/examples)
|
|
97
100
|
Shared payload values used by the docs and tests.
|
|
98
|
-
|
|
101
|
+
12. [gdc-common-utils-ts/docs/101-LIFECYCLE.md](https://github.com/Global-DataCare/gdc-common-utils-ts/blob/main/docs/101-LIFECYCLE.md)
|
|
99
102
|
Canonical `enable/disable/delete` semantics and copy/paste placeholders.
|
|
100
|
-
|
|
103
|
+
13. [tests/101-backend-profile-runtime.test.mjs](./tests/101-backend-profile-runtime.test.mjs)
|
|
101
104
|
Technical runtime slice for profile/device/session orchestration internals.
|
|
102
105
|
Read this after the public actor SDK guides, not before them.
|
|
103
|
-
|
|
106
|
+
14. [tests/101-individual-controller-backend-runtime.test.mjs](./tests/101-individual-controller-backend-runtime.test.mjs)
|
|
104
107
|
Technical wrapper slice around the generic profile runtime. This is not the
|
|
105
108
|
main onboarding path for new integrators.
|
|
106
|
-
|
|
109
|
+
15. [docs/V2_INDIVIDUAL_REGISTRATION_RECONCILIATION.md](./docs/V2_INDIVIDUAL_REGISTRATION_RECONCILIATION.md)
|
|
107
110
|
Historical reconciliation note for the current CORE registration baseline.
|
|
108
|
-
|
|
111
|
+
16. [docs/NEXT_STEPS.md](./docs/NEXT_STEPS.md)
|
|
109
112
|
Follow-up scope after GW CORE live validation, including the future user job
|
|
110
113
|
manager boundary.
|
|
111
114
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SubmitAndPollResult } from 'gdc-sdk-core-ts';
|
|
2
|
+
import type { RouteContext } from './individual-onboarding.js';
|
|
3
|
+
/** Verified contact extracted by the BFF from a signed OpenID `id_token`. */
|
|
4
|
+
export type AuthorizedSubjectVerifiedContact = Readonly<{
|
|
5
|
+
email?: string;
|
|
6
|
+
telephone?: string;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* High-level input for listing subject grants already accepted by an
|
|
10
|
+
* individual index. The contact locates grants; it is not role or action proof.
|
|
11
|
+
*/
|
|
12
|
+
export type AuthorizedIndividualSubjectDirectoryInput = Readonly<{
|
|
13
|
+
verifiedContact: AuthorizedSubjectVerifiedContact;
|
|
14
|
+
requestThid?: string;
|
|
15
|
+
pollOptions?: Readonly<{
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
intervalMs?: number;
|
|
18
|
+
}>;
|
|
19
|
+
}>;
|
|
20
|
+
/** One exact subject projection paired with its accepted grant metadata. */
|
|
21
|
+
export type AuthorizedIndividualSubject = Readonly<{
|
|
22
|
+
subjectDid: string;
|
|
23
|
+
role?: string;
|
|
24
|
+
/** Accepted provider grant claims; descriptive metadata, never action authority. */
|
|
25
|
+
grantClaims: Readonly<Record<string, unknown>>;
|
|
26
|
+
authorizationEvidenceId?: string;
|
|
27
|
+
issuerDid?: string;
|
|
28
|
+
subjectClaims: Readonly<Record<string, unknown>>;
|
|
29
|
+
}>;
|
|
30
|
+
type DirectoryDeps = Readonly<{
|
|
31
|
+
individualLicenseSearchPath: (ctx: RouteContext) => string;
|
|
32
|
+
individualLicenseSearchPollPath: (ctx: RouteContext) => string;
|
|
33
|
+
individualOrganizationSearchPath: (ctx: RouteContext) => string;
|
|
34
|
+
individualOrganizationSearchPollPath: (ctx: RouteContext) => string;
|
|
35
|
+
submitAndPoll: (submitPath: string, pollPath: string, payload: {
|
|
36
|
+
thid?: string;
|
|
37
|
+
} & Record<string, unknown>, pollOptions?: {
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
intervalMs?: number;
|
|
40
|
+
}) => Promise<SubmitAndPollResult>;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Lists already-authorized individual subjects without exposing GW plumbing.
|
|
44
|
+
*
|
|
45
|
+
* The caller supplies a verified contact taken from a signed OpenID token.
|
|
46
|
+
* That account/contact proof is intentionally insufficient for protected
|
|
47
|
+
* actions: professional role proof remains a separate `vp_token`, and SMART
|
|
48
|
+
* access remains bound to a registered wallet/device session.
|
|
49
|
+
*/
|
|
50
|
+
export declare function listAuthorizedIndividualSubjectsWithDeps(routeContext: RouteContext, input: AuthorizedIndividualSubjectDirectoryInput, deps: DirectoryDeps): Promise<AuthorizedIndividualSubject[]>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
const SCHEMA_CONTEXT = 'org.schema';
|
|
3
|
+
const INDIVIDUAL_AUDIENCE_TYPE = 'Individual';
|
|
4
|
+
const ACCEPTED_ITEM_CONDITION = 'https://schema.org/UsedCondition';
|
|
5
|
+
const CLAIM_AUDIENCE_TYPE = 'org.schema.IndividualProduct.audience.audienceType';
|
|
6
|
+
const CLAIM_ITEM_CONDITION = 'org.schema.IndividualProduct.itemCondition';
|
|
7
|
+
const CLAIM_PERSON_EMAIL = 'org.schema.Person.email';
|
|
8
|
+
const CLAIM_PERSON_TELEPHONE = 'org.schema.Person.telephone';
|
|
9
|
+
const CLAIM_ORGANIZATION_SAME_AS = 'org.schema.Organization.sameAs';
|
|
10
|
+
const CLAIM_RELATED_PERSON_ROLE = 'RelatedPerson.role';
|
|
11
|
+
const CLAIM_OCCUPATION_IDENTIFIER = 'org.schema.Person.hasOccupation.identifier.value';
|
|
12
|
+
/**
|
|
13
|
+
* Lists already-authorized individual subjects without exposing GW plumbing.
|
|
14
|
+
*
|
|
15
|
+
* The caller supplies a verified contact taken from a signed OpenID token.
|
|
16
|
+
* That account/contact proof is intentionally insufficient for protected
|
|
17
|
+
* actions: professional role proof remains a separate `vp_token`, and SMART
|
|
18
|
+
* access remains bound to a registered wallet/device session.
|
|
19
|
+
*/
|
|
20
|
+
export async function listAuthorizedIndividualSubjectsWithDeps(routeContext, input, deps) {
|
|
21
|
+
const email = String(input.verifiedContact?.email || '').trim().toLowerCase();
|
|
22
|
+
const telephone = String(input.verifiedContact?.telephone || '').trim();
|
|
23
|
+
if (!email && !telephone) {
|
|
24
|
+
throw new Error('A verified email or telephone is required to list authorized subjects.');
|
|
25
|
+
}
|
|
26
|
+
const licenseClaims = {
|
|
27
|
+
'@context': SCHEMA_CONTEXT,
|
|
28
|
+
[CLAIM_AUDIENCE_TYPE]: INDIVIDUAL_AUDIENCE_TYPE,
|
|
29
|
+
[CLAIM_ITEM_CONDITION]: ACCEPTED_ITEM_CONDITION,
|
|
30
|
+
...(email ? { [CLAIM_PERSON_EMAIL]: email } : {}),
|
|
31
|
+
...(telephone ? { [CLAIM_PERSON_TELEPHONE]: telephone } : {}),
|
|
32
|
+
};
|
|
33
|
+
const licenseResult = await deps.submitAndPoll(deps.individualLicenseSearchPath(routeContext), deps.individualLicenseSearchPollPath(routeContext), {
|
|
34
|
+
thid: input.requestThid || createThreadId('authorized-subject-license'),
|
|
35
|
+
body: {
|
|
36
|
+
resourceType: 'Bundle',
|
|
37
|
+
type: 'batch',
|
|
38
|
+
data: [{
|
|
39
|
+
request: { method: 'POST' },
|
|
40
|
+
meta: { status: 'active', claims: licenseClaims },
|
|
41
|
+
}],
|
|
42
|
+
},
|
|
43
|
+
}, input.pollOptions);
|
|
44
|
+
const licenses = readAcceptedLicenses(licenseResult);
|
|
45
|
+
const resolved = await Promise.all(licenses.map(async (license) => {
|
|
46
|
+
const organizationClaims = {
|
|
47
|
+
'@context': SCHEMA_CONTEXT,
|
|
48
|
+
[CLAIM_ORGANIZATION_SAME_AS]: license.subjectDid,
|
|
49
|
+
};
|
|
50
|
+
const organizationResult = await deps.submitAndPoll(deps.individualOrganizationSearchPath(routeContext), deps.individualOrganizationSearchPollPath(routeContext), {
|
|
51
|
+
thid: createThreadId('authorized-subject-record'),
|
|
52
|
+
body: {
|
|
53
|
+
resourceType: 'Bundle',
|
|
54
|
+
type: 'batch',
|
|
55
|
+
data: [{
|
|
56
|
+
type: 'Family-search-v1.0',
|
|
57
|
+
request: { method: 'POST' },
|
|
58
|
+
meta: { claims: organizationClaims },
|
|
59
|
+
}],
|
|
60
|
+
},
|
|
61
|
+
}, input.pollOptions);
|
|
62
|
+
const subjectClaims = readFirstClaims(organizationResult);
|
|
63
|
+
if (String(subjectClaims[CLAIM_ORGANIZATION_SAME_AS] || '').trim() !== license.subjectDid) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
subjectDid: license.subjectDid,
|
|
68
|
+
...(license.role ? { role: license.role } : {}),
|
|
69
|
+
grantClaims: license.grantClaims,
|
|
70
|
+
...(license.authorizationEvidenceId
|
|
71
|
+
? { authorizationEvidenceId: license.authorizationEvidenceId }
|
|
72
|
+
: {}),
|
|
73
|
+
...(license.issuerDid ? { issuerDid: license.issuerDid } : {}),
|
|
74
|
+
subjectClaims,
|
|
75
|
+
};
|
|
76
|
+
}));
|
|
77
|
+
return resolved.filter((value) => Boolean(value));
|
|
78
|
+
}
|
|
79
|
+
function readAcceptedLicenses(result) {
|
|
80
|
+
const rows = readData(result)[0]?.resource?.data;
|
|
81
|
+
if (!Array.isArray(rows))
|
|
82
|
+
return [];
|
|
83
|
+
return rows.flatMap((row) => {
|
|
84
|
+
const meta = row?.meta || row?.resource?.meta || {};
|
|
85
|
+
const subjectDid = String(meta.authorizedSubjectDid || '').trim();
|
|
86
|
+
if (!subjectDid)
|
|
87
|
+
return [];
|
|
88
|
+
const claims = (meta.claims || {});
|
|
89
|
+
const relatedRoles = String(claims[CLAIM_RELATED_PERSON_ROLE] || '')
|
|
90
|
+
.split(',')
|
|
91
|
+
.map((value) => value.trim().toUpperCase())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
const occupation = String(claims[CLAIM_OCCUPATION_IDENTIFIER] || '').trim();
|
|
94
|
+
const role = relatedRoles.includes('CAREGIVER') ? 'CAREGIVER' : occupation;
|
|
95
|
+
return [{
|
|
96
|
+
subjectDid,
|
|
97
|
+
...(role ? { role } : {}),
|
|
98
|
+
grantClaims: claims,
|
|
99
|
+
...(meta.identifier ? { authorizationEvidenceId: String(meta.identifier) } : {}),
|
|
100
|
+
...(meta.issuerDid ? { issuerDid: String(meta.issuerDid) } : {}),
|
|
101
|
+
}];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function readFirstClaims(result) {
|
|
105
|
+
const entry = readData(result)[0];
|
|
106
|
+
const claims = entry?.meta?.claims || entry?.resource?.meta?.claims;
|
|
107
|
+
return claims && typeof claims === 'object' ? claims : {};
|
|
108
|
+
}
|
|
109
|
+
function readData(result) {
|
|
110
|
+
const pollBody = (result?.poll?.body || {});
|
|
111
|
+
const root = pollBody?.body || pollBody;
|
|
112
|
+
return Array.isArray(root?.data) ? root.data : [];
|
|
113
|
+
}
|
|
114
|
+
function createThreadId(prefix) {
|
|
115
|
+
return `${prefix}-${globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`}`;
|
|
116
|
+
}
|
|
@@ -68,6 +68,28 @@ export type EmployeeDeviceActivationResult = {
|
|
|
68
68
|
exchange: SubmitAndPollResult;
|
|
69
69
|
dcr: SubmitAndPollResult;
|
|
70
70
|
};
|
|
71
|
+
export type EmployeeDeviceOtpRecoveryInput = Readonly<{
|
|
72
|
+
idToken: string;
|
|
73
|
+
clientInstanceId: string;
|
|
74
|
+
pollOptions?: PollOptions;
|
|
75
|
+
}>;
|
|
76
|
+
export type EmployeeDeviceOtpRecoveryResult = Readonly<{
|
|
77
|
+
activationCode: string;
|
|
78
|
+
licenseId: string;
|
|
79
|
+
employeeRole: string;
|
|
80
|
+
employeeActorIdentifier: string;
|
|
81
|
+
recovery: SubmitAndPollResult;
|
|
82
|
+
}>;
|
|
83
|
+
/** Requests a fresh replacement credential for one OTP-authenticated installation. */
|
|
84
|
+
export declare function recoverEmployeeDeviceWithOtpWithDeps(deps: Readonly<{
|
|
85
|
+
routeCtx: RouteContext;
|
|
86
|
+
input: EmployeeDeviceOtpRecoveryInput;
|
|
87
|
+
identityEmployeeRecoveryPath: (ctx: RouteContext) => string;
|
|
88
|
+
identityEmployeeRecoveryPollPath: (ctx: RouteContext) => string;
|
|
89
|
+
submitAndPollWithBearerToken: (bearerToken: string | undefined, submitPath: string, pollPath: string, payload: {
|
|
90
|
+
thid?: string;
|
|
91
|
+
} & Record<string, unknown>, pollOptions?: PollOptions) => Promise<SubmitAndPollResult>;
|
|
92
|
+
}>): Promise<EmployeeDeviceOtpRecoveryResult>;
|
|
71
93
|
export type EmployeeDeviceRevocationInput = {
|
|
72
94
|
licenseId: string;
|
|
73
95
|
clientId: string;
|
|
@@ -56,6 +56,26 @@ export function createProfileDeviceActivationRequest(input) {
|
|
|
56
56
|
};
|
|
57
57
|
return draft;
|
|
58
58
|
}
|
|
59
|
+
/** Requests a fresh replacement credential for one OTP-authenticated installation. */
|
|
60
|
+
export async function recoverEmployeeDeviceWithOtpWithDeps(deps) {
|
|
61
|
+
const idToken = requiredText(deps.input.idToken, 'fresh OTP identity token');
|
|
62
|
+
const clientInstanceId = requiredText(deps.input.clientInstanceId, 'client instance id');
|
|
63
|
+
const recovery = await deps.submitAndPollWithBearerToken(idToken, deps.identityEmployeeRecoveryPath(deps.routeCtx), deps.identityEmployeeRecoveryPollPath(deps.routeCtx), { thid: `employee-recovery-${createRuntimeUuid()}`, client_instance_id: clientInstanceId }, deps.input.pollOptions);
|
|
64
|
+
const failedStatus = [recovery.submit.status, recovery.poll.status]
|
|
65
|
+
.find(status => status < 200 || status >= 300);
|
|
66
|
+
if (failedStatus !== undefined) {
|
|
67
|
+
throw new Error(`recoverEmployeeDeviceWithOtp: recovery failed (HTTP ${failedStatus}).`);
|
|
68
|
+
}
|
|
69
|
+
const terminal = responseBody(recovery.poll.body);
|
|
70
|
+
const activationCode = findResponseText(terminal, 'activation_code');
|
|
71
|
+
const licenseId = findResponseText(terminal, 'license_id');
|
|
72
|
+
const employeeRole = findResponseText(terminal, 'employee_role');
|
|
73
|
+
const employeeActorIdentifier = findResponseText(terminal, 'employee_same_as');
|
|
74
|
+
if (!activationCode || !licenseId || !employeeRole || !employeeActorIdentifier) {
|
|
75
|
+
throw new Error('recoverEmployeeDeviceWithOtp: incomplete recovery response.');
|
|
76
|
+
}
|
|
77
|
+
return { activationCode, licenseId, employeeRole, employeeActorIdentifier, recovery };
|
|
78
|
+
}
|
|
59
79
|
export async function revokeEmployeeDeviceWithDeps(deps) {
|
|
60
80
|
const licenseId = String(deps.input.licenseId || '').trim();
|
|
61
81
|
const clientId = String(deps.input.clientId || '').trim();
|
|
@@ -147,6 +167,44 @@ function resolveDcrPayload(input) {
|
|
|
147
167
|
return { ...input.dcrPayload };
|
|
148
168
|
throw new Error('Device activation requires a device registration built by createProfileDeviceActivationRequest.');
|
|
149
169
|
}
|
|
170
|
+
function responseBody(value) {
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
try {
|
|
173
|
+
return responseBody(JSON.parse(value));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
180
|
+
return {};
|
|
181
|
+
const record = value;
|
|
182
|
+
return record.body && typeof record.body === 'object' && !Array.isArray(record.body)
|
|
183
|
+
? record.body
|
|
184
|
+
: record;
|
|
185
|
+
}
|
|
186
|
+
function findResponseText(value, key) {
|
|
187
|
+
if (!value || typeof value !== 'object')
|
|
188
|
+
return '';
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
for (const child of value) {
|
|
191
|
+
const found = findResponseText(child, key);
|
|
192
|
+
if (found)
|
|
193
|
+
return found;
|
|
194
|
+
}
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
const record = value;
|
|
198
|
+
const direct = normalizedText(record[key]);
|
|
199
|
+
if (direct)
|
|
200
|
+
return direct;
|
|
201
|
+
for (const child of Object.values(record)) {
|
|
202
|
+
const found = findResponseText(child, key);
|
|
203
|
+
if (found)
|
|
204
|
+
return found;
|
|
205
|
+
}
|
|
206
|
+
return '';
|
|
207
|
+
}
|
|
150
208
|
function normalizedText(value) {
|
|
151
209
|
return typeof value === 'string' ? value.trim() : '';
|
|
152
210
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from './fhir-r5-subscription-runtime.js';
|
|
|
24
24
|
export * from './order-offer-summary.js';
|
|
25
25
|
export * from './organization-license-order.js';
|
|
26
26
|
export * from './resource-operations.js';
|
|
27
|
+
export * from './authorized-subject-directory.js';
|
|
27
28
|
export * from './digital-twin.js';
|
|
28
29
|
export * from './operational-actor-did.js';
|
|
29
30
|
export * from './profile-workspace.js';
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export * from './fhir-r5-subscription-runtime.js';
|
|
|
25
25
|
export * from './order-offer-summary.js';
|
|
26
26
|
export * from './organization-license-order.js';
|
|
27
27
|
export * from './resource-operations.js';
|
|
28
|
+
export * from './authorized-subject-directory.js';
|
|
28
29
|
export * from './digital-twin.js';
|
|
29
30
|
export * from './operational-actor-did.js';
|
|
30
31
|
export * from './profile-workspace.js';
|
|
@@ -9,7 +9,7 @@ import { type IndividualOrganizationConfirmOrderInput, type IndividualOrganizati
|
|
|
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';
|
|
12
|
-
import { type EmployeeDeviceActivationRequestInput, type EmployeeDeviceActivationResult, type EmployeeDeviceRevocationInput } from './device-activation.js';
|
|
12
|
+
import { type EmployeeDeviceActivationRequestInput, type EmployeeDeviceActivationResult, type EmployeeDeviceOtpRecoveryInput, type EmployeeDeviceOtpRecoveryResult, type EmployeeDeviceRevocationInput } from './device-activation.js';
|
|
13
13
|
import { type OrganizationLicenseOrderConfirmInput } from './organization-license-order.js';
|
|
14
14
|
import { type IndividualOrganizationBootstrapInput, type IndividualOrganizationStartResult } from './individual-start.js';
|
|
15
15
|
import { type CommunicationIngestionInput, type BlockchainArtifactRegistrationInput, type VitalSignBatchCommunicationFromSearchResponseInput, type CommunicationParticipantRuntimeSearchInput, type ClinicalBundleSearchInput, type ClinicalSectionUpdateInput, type ClinicalSummaryReadResult, type ClinicalSummaryRequestInput, type ClinicalSummaryUpdateInput, type GrantProfessionalAccessInput, type GrantProfessionalAccessResult, type DigitalTwinSecondaryUseConsentInput, type DigitalTwinSecondaryUseConsentResult, type DigitalTwinSubjectLinkPurgeInput, type DigitalTwinSubjectLinkPurgeResult, type IndividualMemberLifecycleInput, type IndividualMemberLicenseAddInput, type IndividualMemberLicenseInvitationInput, type IndividualMemberLicenseTransitionInput, type LicenseListRuntimeSearchInput, type LicenseOfferRuntimeSearchInput, type LicenseOrderRuntimeSearchInput, type OrganizationEmployeeCreationInput, type OrganizationEmployeeLicenseAddInput, type OrganizationEmployeeLicenseOfferInput, type OrganizationEmployeeLicenseInvitationInput, type OrganizationEmployeeLifecycleInput, type OrganizationEmployeeSearchInput, type ProfessionalAccessRequestInput, type ProfessionalAccessRequestResult, type RevokeProfessionalAccessInput, type RevokeProfessionalAccessResult, type RelatedPersonUpsertInput } from './resource-operations.js';
|
|
@@ -21,6 +21,7 @@ import type { NodeRuntimeClient, PollOptions, PollResult, SubmitAndPollResult, S
|
|
|
21
21
|
import { type FhirR5SubscriptionBatchInput } from './fhir-r5-subscription-runtime.js';
|
|
22
22
|
import type { FhirR5SubscriptionTopic } from 'gdc-common-utils-ts/models/fhir-r5-subscription';
|
|
23
23
|
import { type DigitalTwinMaterializationInput, type DigitalTwinSearchInput, type DigitalTwinSelectionInput } from './digital-twin.js';
|
|
24
|
+
import { type AuthorizedIndividualSubject, type AuthorizedIndividualSubjectDirectoryInput } from './authorized-subject-directory.js';
|
|
24
25
|
export type HttpRuntimeClientOptions = {
|
|
25
26
|
baseUrl: string;
|
|
26
27
|
bearerToken?: string;
|
|
@@ -414,6 +415,12 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
|
|
|
414
415
|
* with optional filters.
|
|
415
416
|
*/
|
|
416
417
|
listIndividualLicenses(ctx: RouteContext, input?: LicenseListRuntimeSearchInput): Promise<SubmitAndPollResult>;
|
|
418
|
+
/**
|
|
419
|
+
* Resolves the complete high-level directory of subjects already authorized
|
|
420
|
+
* for a verified OpenID contact. The SDK owns both GW searches and polling;
|
|
421
|
+
* the result itself grants no professional role or SMART capability.
|
|
422
|
+
*/
|
|
423
|
+
listAuthorizedIndividualSubjects(ctx: RouteContext, input: AuthorizedIndividualSubjectDirectoryInput): Promise<AuthorizedIndividualSubject[]>;
|
|
417
424
|
/** Adds zero-cost seats to one individual organization's member pool. */
|
|
418
425
|
addFreeIndividualMemberLicenses(ctx: RouteContext, input: IndividualMemberLicenseAddInput): Promise<SubmitAndPollResult>;
|
|
419
426
|
/** Reserves one individual seat for an existing FHIR v3-role contact. */
|
|
@@ -465,6 +472,8 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
|
|
|
465
472
|
* The implementation is actor-neutral and shares the profile-device flow.
|
|
466
473
|
*/
|
|
467
474
|
activateEmployeeDeviceWithActivationRequest(input: EmployeeDeviceActivationRequestInput): Promise<EmployeeDeviceActivationResult>;
|
|
475
|
+
/** Obtains a replacement activation credential after fresh OTP verification. */
|
|
476
|
+
recoverEmployeeDeviceWithOtp(input: EmployeeDeviceOtpRecoveryInput & Partial<RouteContext>): Promise<EmployeeDeviceOtpRecoveryResult>;
|
|
468
477
|
/**
|
|
469
478
|
* Creates or updates a `RelatedPerson` for non-employee family/caregiver
|
|
470
479
|
* roles such as a grandfather, guardian, or external caregiver.
|
|
@@ -6,7 +6,7 @@ import { confirmIndividualOrganizationOrderWithDeps, } from './individual-onboar
|
|
|
6
6
|
import { ensureFamilyOrganizationRegistrationWithDeps, } from './family-organization-registration.js';
|
|
7
7
|
import { searchFamilyOrganizationWithDeps } from './family-organization-search.js';
|
|
8
8
|
import { requestSmartTokenWithDeps } from './smart-token.js';
|
|
9
|
-
import { activateEmployeeDeviceWithActivationCodeWithDeps, activateEmployeeDeviceWithActivationRequestWithDeps, revokeEmployeeDeviceWithDeps, } from './device-activation.js';
|
|
9
|
+
import { activateEmployeeDeviceWithActivationCodeWithDeps, activateEmployeeDeviceWithActivationRequestWithDeps, recoverEmployeeDeviceWithOtpWithDeps, revokeEmployeeDeviceWithDeps, } from './device-activation.js';
|
|
10
10
|
import { extractOfferIdFromResponseBody, extractOfferPreviewFromResponseBody, } from './order-offer-summary.js';
|
|
11
11
|
import { confirmOrganizationLicenseOrderWithDeps } from './organization-license-order.js';
|
|
12
12
|
import { startIndividualOrganizationWithDeps } from './individual-start.js';
|
|
@@ -19,6 +19,7 @@ import { RuntimeClientPaths } from './runtime-client-paths.js';
|
|
|
19
19
|
import { runtimeUuid, wrapBundleAsGatewayTransactionMessage } from './runtime-message.js';
|
|
20
20
|
import { submitFhirR5SubscriptionBatchWithDeps, submitFhirR5SubscriptionTopicBatchWithDeps, } from './fhir-r5-subscription-runtime.js';
|
|
21
21
|
import { materializeDigitalTwinWithDeps, saveDigitalTwinSelectionWithDeps, searchDigitalTwinsWithDeps, } from './digital-twin.js';
|
|
22
|
+
import { listAuthorizedIndividualSubjectsWithDeps, } from './authorized-subject-directory.js';
|
|
22
23
|
const bootstrapFacade = createBootstrapFacade();
|
|
23
24
|
/**
|
|
24
25
|
* Runtime-oriented HTTP client for Node backends, BFFs, and workers that need
|
|
@@ -751,6 +752,20 @@ export class HttpRuntimeClient {
|
|
|
751
752
|
submitAndPoll: this.submitAndPoll.bind(this),
|
|
752
753
|
});
|
|
753
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Resolves the complete high-level directory of subjects already authorized
|
|
757
|
+
* for a verified OpenID contact. The SDK owns both GW searches and polling;
|
|
758
|
+
* the result itself grants no professional role or SMART capability.
|
|
759
|
+
*/
|
|
760
|
+
async listAuthorizedIndividualSubjects(ctx, input) {
|
|
761
|
+
return listAuthorizedIndividualSubjectsWithDeps(ctx, input, {
|
|
762
|
+
individualLicenseSearchPath: this.paths.individualLicenseSearchPath.bind(this.paths),
|
|
763
|
+
individualLicenseSearchPollPath: this.paths.individualLicenseSearchPollPath.bind(this.paths),
|
|
764
|
+
individualOrganizationSearchPath: this.paths.individualFamilyOrganizationSearchPath.bind(this.paths),
|
|
765
|
+
individualOrganizationSearchPollPath: this.paths.individualFamilyOrganizationSearchPollPath.bind(this.paths),
|
|
766
|
+
submitAndPoll: this.submitAndPoll.bind(this),
|
|
767
|
+
});
|
|
768
|
+
}
|
|
754
769
|
/** Adds zero-cost seats to one individual organization's member pool. */
|
|
755
770
|
async addFreeIndividualMemberLicenses(ctx, input) {
|
|
756
771
|
return addFreeIndividualMemberLicensesWithDeps(ctx, input, {
|
|
@@ -909,6 +924,17 @@ export class HttpRuntimeClient {
|
|
|
909
924
|
async activateEmployeeDeviceWithActivationRequest(input) {
|
|
910
925
|
return this.activateProfileDeviceWithActivationRequest(input);
|
|
911
926
|
}
|
|
927
|
+
/** Obtains a replacement activation credential after fresh OTP verification. */
|
|
928
|
+
async recoverEmployeeDeviceWithOtp(input) {
|
|
929
|
+
const routeCtx = this.paths.routeCtxFromInput(input);
|
|
930
|
+
return recoverEmployeeDeviceWithOtpWithDeps({
|
|
931
|
+
routeCtx,
|
|
932
|
+
input,
|
|
933
|
+
identityEmployeeRecoveryPath: this.paths.identityEmployeeRecoveryPath.bind(this.paths),
|
|
934
|
+
identityEmployeeRecoveryPollPath: this.paths.identityEmployeeRecoveryPollPath.bind(this.paths),
|
|
935
|
+
submitAndPollWithBearerToken: this.submitAndPollWithBearerToken.bind(this),
|
|
936
|
+
});
|
|
937
|
+
}
|
|
912
938
|
/**
|
|
913
939
|
* Creates or updates a `RelatedPerson` for non-employee family/caregiver
|
|
914
940
|
* roles such as a grandfather, guardian, or external caregiver.
|
|
@@ -18,6 +18,7 @@ import type { EnsureFamilyOrganizationRegistrationInput, EnsureFamilyOrganizatio
|
|
|
18
18
|
import type { OrganizationLicenseOrderConfirmInput } from '../organization-license-order.js';
|
|
19
19
|
import type { SmartTokenExchangeResult, SmartTokenRequestInput } from '../smart-token.js';
|
|
20
20
|
import type { DigitalTwinMaterializationInput, DigitalTwinSearchInput, DigitalTwinSelectionInput } from '../digital-twin.js';
|
|
21
|
+
import type { AuthorizedIndividualSubject, AuthorizedIndividualSubjectDirectoryInput } from '../authorized-subject-directory.js';
|
|
21
22
|
import type { CommunicationIngestionInput, BlockchainArtifactRegistrationInput, CommunicationParticipantRuntimeSearchInput, ClinicalBundleSearchInput, ClinicalSectionUpdateInput, ClinicalSummaryReadResult, ClinicalSummaryRequestInput, ClinicalSummaryUpdateInput, VitalSignBatchCommunicationFromSearchResponseInput, DigitalTwinGenerationInput, DigitalTwinSecondaryUseConsentInput, DigitalTwinSecondaryUseConsentResult, DigitalTwinSubjectLinkPurgeInput, DigitalTwinSubjectLinkPurgeResult, GrantProfessionalAccessInput, GrantProfessionalAccessResult, IndividualMemberLifecycleInput, IndividualMemberLicenseAddInput, IndividualMemberLicenseInvitationInput, IndividualMemberLicenseTransitionInput, IpsOrFhirImportInput, OrganizationEmployeeCreationInput, OrganizationEmployeeLicenseAddInput, OrganizationEmployeeLicenseOfferInput, OrganizationEmployeeLicenseInvitationInput, OrganizationEmployeeLifecycleInput, OrganizationEmployeeSearchInput, ProfessionalAccessRequestInput, ProfessionalAccessRequestResult, RevokeProfessionalAccessInput, RevokeProfessionalAccessResult, RelatedPersonUpsertInput } from '../resource-operations.js';
|
|
22
23
|
/**
|
|
23
24
|
* Shared node-runtime activation input.
|
|
@@ -124,6 +125,8 @@ export type RuntimeClient = {
|
|
|
124
125
|
revokeProfessionalAccess?: (ctx: RouteContext, input: RevokeProfessionalAccessInput) => Promise<RevokeProfessionalAccessResult>;
|
|
125
126
|
searchIndividualLicenses?: (ctx: RouteContext, input: LicenseListRuntimeSearchInput) => Promise<SubmitAndPollResult>;
|
|
126
127
|
listIndividualLicenses?: (ctx: RouteContext, input?: LicenseListRuntimeSearchInput) => Promise<SubmitAndPollResult>;
|
|
128
|
+
/** Lists exact subjects already authorized for one verified OpenID contact. */
|
|
129
|
+
listAuthorizedIndividualSubjects?: (ctx: RouteContext, input: AuthorizedIndividualSubjectDirectoryInput) => Promise<AuthorizedIndividualSubject[]>;
|
|
127
130
|
addFreeIndividualMemberLicenses?: (ctx: RouteContext, input: IndividualMemberLicenseAddInput) => Promise<SubmitAndPollResult>;
|
|
128
131
|
issueIndividualMemberLicense?: (ctx: RouteContext, input: IndividualMemberLicenseInvitationInput) => Promise<SubmitAndPollResult>;
|
|
129
132
|
transitionIndividualMemberLicense?: (ctx: RouteContext, action: '_accept' | '_deactivate' | '_release', input: IndividualMemberLicenseTransitionInput) => Promise<SubmitAndPollResult>;
|
|
@@ -109,6 +109,8 @@ export declare class RuntimeClientPaths {
|
|
|
109
109
|
digitalTwinCommunicationPollPath(ctx: RouteContext, format: string): string;
|
|
110
110
|
identityTokenExchangePath(ctx: RouteContext): string;
|
|
111
111
|
identityTokenExchangePollPath(ctx: RouteContext): string;
|
|
112
|
+
identityEmployeeRecoveryPath(ctx: RouteContext): string;
|
|
113
|
+
identityEmployeeRecoveryPollPath(ctx: RouteContext): string;
|
|
112
114
|
identityDeviceDcrPath(ctx: RouteContext): string;
|
|
113
115
|
identityDeviceDcrPollPath(ctx: RouteContext): string;
|
|
114
116
|
identityDeviceRevokePath(ctx: RouteContext): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
2
|
import { GwCoreLifecycleAction } from './constants/lifecycle.js';
|
|
3
|
-
import { buildHostRegistryPath, buildIdentityDeviceDcrPath, buildIdentityDeviceDcrPollPath, buildIdentityDeviceRevokePath, buildIdentityDeviceRevokePollPath, buildIdentityLicenseIssuePath, buildIdentityLicenseIssuePollPath, buildIdentityOpenIdSmartTokenPath, buildIdentityOpenIdSmartTokenPollPath, buildIdentityTokenExchangePath, buildIdentityTokenExchangePollPath, buildOrganizationDidBindingPath, buildOrganizationDidBindingPollPath, buildV1Path, } from './runtime-paths.js';
|
|
3
|
+
import { buildHostRegistryPath, buildIdentityDeviceDcrPath, buildIdentityDeviceDcrPollPath, buildIdentityDeviceRevokePath, buildIdentityDeviceRevokePollPath, buildIdentityEmployeeRecoveryPath, buildIdentityEmployeeRecoveryPollPath, buildIdentityLicenseIssuePath, buildIdentityLicenseIssuePollPath, buildIdentityOpenIdSmartTokenPath, buildIdentityOpenIdSmartTokenPollPath, buildIdentityTokenExchangePath, buildIdentityTokenExchangePollPath, buildOrganizationDidBindingPath, buildOrganizationDidBindingPollPath, buildV1Path, } from './runtime-paths.js';
|
|
4
4
|
import { requireHostRouteContext, requireRouteContext, routeCtxFromInput, } from './runtime-route-context.js';
|
|
5
5
|
export class RuntimeClientPaths {
|
|
6
6
|
constructor(defaultCtx) {
|
|
@@ -123,6 +123,8 @@ export class RuntimeClientPaths {
|
|
|
123
123
|
digitalTwinCommunicationPollPath(ctx, format) { return this.v1Path(ctx, 'digitaltwin', format, 'Communication', '_batch-response'); }
|
|
124
124
|
identityTokenExchangePath(ctx) { return buildIdentityTokenExchangePath(ctx); }
|
|
125
125
|
identityTokenExchangePollPath(ctx) { return buildIdentityTokenExchangePollPath(ctx); }
|
|
126
|
+
identityEmployeeRecoveryPath(ctx) { return buildIdentityEmployeeRecoveryPath(ctx); }
|
|
127
|
+
identityEmployeeRecoveryPollPath(ctx) { return buildIdentityEmployeeRecoveryPollPath(ctx); }
|
|
126
128
|
identityDeviceDcrPath(ctx) { return buildIdentityDeviceDcrPath(ctx); }
|
|
127
129
|
identityDeviceDcrPollPath(ctx) { return buildIdentityDeviceDcrPollPath(ctx); }
|
|
128
130
|
identityDeviceRevokePath(ctx) { return buildIdentityDeviceRevokePath(ctx); }
|
package/dist/runtime-paths.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export declare function buildOrganizationDidBindingPath(routeCtx: RouteContext):
|
|
|
6
6
|
export declare function buildOrganizationDidBindingPollPath(routeCtx: RouteContext): string;
|
|
7
7
|
export declare function buildIdentityTokenExchangePath(ctx: RouteContext): string;
|
|
8
8
|
export declare function buildIdentityTokenExchangePollPath(ctx: RouteContext): string;
|
|
9
|
+
/** Fresh OTP-assisted replacement credential for one existing installation. */
|
|
10
|
+
export declare function buildIdentityEmployeeRecoveryPath(ctx: RouteContext): string;
|
|
11
|
+
/** Poll endpoint paired with employee wallet recovery. */
|
|
12
|
+
export declare function buildIdentityEmployeeRecoveryPollPath(ctx: RouteContext): string;
|
|
9
13
|
export declare function buildIdentityDeviceDcrPath(ctx: RouteContext): string;
|
|
10
14
|
export declare function buildIdentityDeviceDcrPollPath(ctx: RouteContext): string;
|
|
11
15
|
/** Canonical controller operation that reserves an employee seat activation credential. */
|
package/dist/runtime-paths.js
CHANGED
|
@@ -28,6 +28,14 @@ export function buildIdentityTokenExchangePath(ctx) {
|
|
|
28
28
|
export function buildIdentityTokenExchangePollPath(ctx) {
|
|
29
29
|
return buildIdentityAuthPath(ctx, IdentityAuthActions.ExchangeResponse);
|
|
30
30
|
}
|
|
31
|
+
/** Fresh OTP-assisted replacement credential for one existing installation. */
|
|
32
|
+
export function buildIdentityEmployeeRecoveryPath(ctx) {
|
|
33
|
+
return buildIdentityAuthPath(ctx, '_recover');
|
|
34
|
+
}
|
|
35
|
+
/** Poll endpoint paired with employee wallet recovery. */
|
|
36
|
+
export function buildIdentityEmployeeRecoveryPollPath(ctx) {
|
|
37
|
+
return buildIdentityAuthPath(ctx, '_recover-response');
|
|
38
|
+
}
|
|
31
39
|
export function buildIdentityDeviceDcrPath(ctx) {
|
|
32
40
|
return buildIdentityAuthPath(ctx, IdentityAuthActions.Dcr);
|
|
33
41
|
}
|
|
@@ -45,6 +45,7 @@ export type ServerProfileSessionRecord = Readonly<{
|
|
|
45
45
|
subjectDid: string;
|
|
46
46
|
scopes: string[];
|
|
47
47
|
sealedUnlockedWalletSeed: string;
|
|
48
|
+
sealedUnlockedVpToken?: string;
|
|
48
49
|
sealedAccessToken: string;
|
|
49
50
|
expiresAt: string;
|
|
50
51
|
}>;
|
|
@@ -56,7 +57,18 @@ export type ServerProfileStore = Readonly<{
|
|
|
56
57
|
getSession(sessionId: string): Promise<ServerProfileSessionRecord | undefined>;
|
|
57
58
|
putSession(session: ServerProfileSessionRecord): Promise<void>;
|
|
58
59
|
deleteSession(sessionId: string): Promise<void>;
|
|
60
|
+
/** Required by key rotation so no session can retain the superseded seed. */
|
|
61
|
+
deleteSessionsForProfile?(profileId: string): Promise<void>;
|
|
59
62
|
}>;
|
|
63
|
+
export type ServerEmployeeProfileOtpRotationInput = Readonly<{
|
|
64
|
+
ownerId: string;
|
|
65
|
+
profileId: string;
|
|
66
|
+
idToken: string;
|
|
67
|
+
newPin: string;
|
|
68
|
+
redirectUris: string[];
|
|
69
|
+
clientName: string;
|
|
70
|
+
}>;
|
|
71
|
+
export type ServerEmployeeProfileOtpRotationResult = ServerProfileRecord;
|
|
60
72
|
export type ServerProfileEnrollmentInput = Readonly<{
|
|
61
73
|
/** Stable confidential id of the authenticated portal account that owns and lists this profile. */
|
|
62
74
|
ownerId: string;
|
|
@@ -221,6 +233,13 @@ export declare class ServerProfileSessionManager {
|
|
|
221
233
|
private readonly options;
|
|
222
234
|
constructor(options: ServerProfileSessionManagerOptions);
|
|
223
235
|
enroll(input: ServerProfileEnrollmentInput): Promise<ServerProfileRecord>;
|
|
236
|
+
/**
|
|
237
|
+
* Rotates an employee wallet after a fresh OTP-authenticated GW recovery.
|
|
238
|
+
* The old PIN and seed are never opened: DCR replaces the installation with
|
|
239
|
+
* newly generated keys, all old BFF sessions are removed, and the new wallet
|
|
240
|
+
* is opened once with the new PIN.
|
|
241
|
+
*/
|
|
242
|
+
rotateEmployeeProfileWithOtp(input: ServerEmployeeProfileOtpRotationInput): Promise<ServerEmployeeProfileOtpRotationResult>;
|
|
224
243
|
/**
|
|
225
244
|
* Derives only the public enrollment descriptors for a server-governed
|
|
226
245
|
* recovery seed. This is intended for pre-DCR controller binding requests;
|
|
@@ -249,6 +268,12 @@ export declare class ServerProfileSessionManager {
|
|
|
249
268
|
submitLegalOrganizationIssueWithBootstrapWallet(input: ServerProfileOrganizationIssueInput): Promise<SubmitAndPollResult>;
|
|
250
269
|
listProfiles(ownerId: string): Promise<ServerProfileRecord[]>;
|
|
251
270
|
unlock(input: ServerProfileUnlockInput): Promise<ResolvedServerProfileSession>;
|
|
271
|
+
/**
|
|
272
|
+
* Renews the short SMART bearer while retaining the already-open server
|
|
273
|
+
* wallet session. The caller must supply a fresh authenticated account
|
|
274
|
+
* token; neither the PIN nor wallet keys return to the browser.
|
|
275
|
+
*/
|
|
276
|
+
refreshSession(ownerId: string, sessionId: string, idToken: string): Promise<ResolvedServerProfileSession>;
|
|
252
277
|
resolveSession(ownerId: string, sessionId: string): Promise<ResolvedServerProfileSession>;
|
|
253
278
|
lock(ownerId: string, sessionId: string): Promise<void>;
|
|
254
279
|
private createClient;
|
|
@@ -100,6 +100,53 @@ export class ServerProfileSessionManager {
|
|
|
100
100
|
await this.options.store.putProfile(record);
|
|
101
101
|
return record;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Rotates an employee wallet after a fresh OTP-authenticated GW recovery.
|
|
105
|
+
* The old PIN and seed are never opened: DCR replaces the installation with
|
|
106
|
+
* newly generated keys, all old BFF sessions are removed, and the new wallet
|
|
107
|
+
* is opened once with the new PIN.
|
|
108
|
+
*/
|
|
109
|
+
async rotateEmployeeProfileWithOtp(input) {
|
|
110
|
+
const previous = await this.requireOwnedProfile(input.ownerId, input.profileId);
|
|
111
|
+
if (previous.actorKind !== ActorKinds.OrganizationEmployee || previous.actorMode !== 'member') {
|
|
112
|
+
throw new Error('OTP wallet rotation is restricted to organization employee profiles.');
|
|
113
|
+
}
|
|
114
|
+
const clientInstanceId = String(previous.clientInstanceId || '').trim();
|
|
115
|
+
if (!clientInstanceId)
|
|
116
|
+
throw new Error('Employee profile has no registered installation id.');
|
|
117
|
+
if (!this.options.store.deleteSessionsForProfile) {
|
|
118
|
+
throw new Error('Employee wallet rotation requires profile-session invalidation support.');
|
|
119
|
+
}
|
|
120
|
+
const client = this.createClient(previous.routeContext, input.idToken);
|
|
121
|
+
const replacement = await client.recoverEmployeeDeviceWithOtp({
|
|
122
|
+
...previous.routeContext,
|
|
123
|
+
idToken: input.idToken,
|
|
124
|
+
clientInstanceId,
|
|
125
|
+
});
|
|
126
|
+
const profile = await this.enroll({
|
|
127
|
+
ownerId: previous.ownerId,
|
|
128
|
+
profileId: previous.profileId,
|
|
129
|
+
actorKind: previous.actorKind,
|
|
130
|
+
actorMode: previous.actorMode,
|
|
131
|
+
actorDid: previous.actorDid,
|
|
132
|
+
profileDid: previous.profileDid,
|
|
133
|
+
providerDid: previous.providerDid,
|
|
134
|
+
routeContext: previous.routeContext,
|
|
135
|
+
allowedSubjectDids: previous.allowedSubjectDids,
|
|
136
|
+
pin: input.newPin,
|
|
137
|
+
idToken: input.idToken,
|
|
138
|
+
activationCode: replacement.activationCode,
|
|
139
|
+
clientInstanceId,
|
|
140
|
+
redirectUris: input.redirectUris,
|
|
141
|
+
clientName: input.clientName,
|
|
142
|
+
professionalProof: {
|
|
143
|
+
role: replacement.employeeRole,
|
|
144
|
+
sameAs: replacement.employeeActorIdentifier,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
await this.options.store.deleteSessionsForProfile(profile.profileId);
|
|
148
|
+
return profile;
|
|
149
|
+
}
|
|
103
150
|
/**
|
|
104
151
|
* Derives only the public enrollment descriptors for a server-governed
|
|
105
152
|
* recovery seed. This is intended for pre-DCR controller binding requests;
|
|
@@ -269,11 +316,69 @@ export class ServerProfileSessionManager {
|
|
|
269
316
|
subjectDid: input.subjectDid,
|
|
270
317
|
scopes: unique(input.scopes),
|
|
271
318
|
sealedUnlockedWalletSeed: await this.options.sealer.seal(seed, `${sessionId}:unlocked-wallet-seed`),
|
|
319
|
+
...(vpToken ? {
|
|
320
|
+
sealedUnlockedVpToken: await this.options.sealer.seal(vpToken, `${sessionId}:unlocked-vp-token`),
|
|
321
|
+
} : {}),
|
|
272
322
|
sealedAccessToken: await this.options.sealer.seal(token.accessToken, `${sessionId}:access-token`),
|
|
273
323
|
expiresAt: expiresAt.toISOString(),
|
|
274
324
|
});
|
|
275
325
|
return this.resolveSession(input.ownerId, sessionId);
|
|
276
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Renews the short SMART bearer while retaining the already-open server
|
|
329
|
+
* wallet session. The caller must supply a fresh authenticated account
|
|
330
|
+
* token; neither the PIN nor wallet keys return to the browser.
|
|
331
|
+
*/
|
|
332
|
+
async refreshSession(ownerId, sessionId, idToken) {
|
|
333
|
+
const session = await this.options.store.getSession(sessionId);
|
|
334
|
+
if (!session || session.ownerId !== ownerId)
|
|
335
|
+
throw new Error('Profile session not found.');
|
|
336
|
+
if (new Date(session.expiresAt) <= this.now()) {
|
|
337
|
+
await this.options.store.deleteSession(sessionId);
|
|
338
|
+
throw new Error('Profile session expired.');
|
|
339
|
+
}
|
|
340
|
+
const normalizedIdToken = String(idToken || '').trim();
|
|
341
|
+
if (!normalizedIdToken)
|
|
342
|
+
throw new Error('Profile session refresh requires idToken.');
|
|
343
|
+
const profile = await this.requireOwnedProfile(ownerId, session.profileId);
|
|
344
|
+
this.requireSubject(profile, session.subjectDid);
|
|
345
|
+
const seed = await this.options.sealer.unseal(session.sealedUnlockedWalletSeed, `${sessionId}:unlocked-wallet-seed`);
|
|
346
|
+
const vpToken = session.sealedUnlockedVpToken
|
|
347
|
+
? await this.options.sealer.unseal(session.sealedUnlockedVpToken, `${sessionId}:unlocked-vp-token`)
|
|
348
|
+
: undefined;
|
|
349
|
+
const walletKeyDerivationId = profile.walletKeyDerivationId || profile.profileId;
|
|
350
|
+
const wallet = await this.createWallet(walletKeyDerivationId, seed);
|
|
351
|
+
const now = this.now();
|
|
352
|
+
const smartTokenEndpoint = [
|
|
353
|
+
this.options.gatewayBaseUrl.replace(/\/+$/, ''),
|
|
354
|
+
buildIdentityOpenIdSmartTokenPath(profile.routeContext),
|
|
355
|
+
].join('');
|
|
356
|
+
const assertion = await buildWalletClientAssertion(wallet, profile, smartTokenEndpoint, now);
|
|
357
|
+
const token = await this.createClient(profile.routeContext, normalizedIdToken).requestSmartToken({
|
|
358
|
+
...profile.routeContext,
|
|
359
|
+
actorDid: profile.actorDid,
|
|
360
|
+
subjectDid: session.subjectDid,
|
|
361
|
+
clientId: profile.clientId,
|
|
362
|
+
issuer: profile.clientId,
|
|
363
|
+
audience: smartTokenEndpoint,
|
|
364
|
+
idToken: normalizedIdToken,
|
|
365
|
+
vpToken,
|
|
366
|
+
vpTokenFallback: vpToken ? undefined : 'omit',
|
|
367
|
+
clientAssertion: assertion,
|
|
368
|
+
clientAssertionType: 'private_key_jwt',
|
|
369
|
+
smartTokenKind: 'openid-smart',
|
|
370
|
+
acrValues: profileSmartAcrValues(profile.actorKind),
|
|
371
|
+
scopes: unique(session.scopes),
|
|
372
|
+
tokenCacheKey: `profile-refresh:${sessionId}:${now.getTime()}`,
|
|
373
|
+
});
|
|
374
|
+
if (token.status !== 'fetched' || !token.accessToken)
|
|
375
|
+
throw new Error('SMART token refresh failed.');
|
|
376
|
+
await this.options.store.putSession({
|
|
377
|
+
...session,
|
|
378
|
+
sealedAccessToken: await this.options.sealer.seal(token.accessToken, `${sessionId}:access-token`),
|
|
379
|
+
});
|
|
380
|
+
return this.resolveSession(ownerId, sessionId);
|
|
381
|
+
}
|
|
277
382
|
async resolveSession(ownerId, sessionId) {
|
|
278
383
|
const session = await this.options.store.getSession(sessionId);
|
|
279
384
|
if (!session || session.ownerId !== ownerId)
|