sos-sdk-node-ts 0.1.11 → 0.1.13
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/CHANGELOG.md +15 -1
- package/README.md +7 -0
- package/dist/host-organization-directory-client.d.ts +78 -0
- package/dist/host-organization-directory-client.js +248 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/server-passkey-profile.d.ts +158 -0
- package/dist/server-passkey-profile.js +206 -0
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.1.13 - 2026-10-21
|
|
4
|
+
|
|
5
|
+
- Add the trusted-BFF host organization directory client. It submits and polls
|
|
6
|
+
the canonical host `org.schema/Organization/_search` route, reads a
|
|
7
|
+
DIDComm-wrapped paginated Bundle, preserves opaque cursors and surfaces
|
|
8
|
+
Bundle `OperationOutcome` failures without accepting browser-selected host
|
|
9
|
+
authority. The same client performs status, suspend, reactivate and purge
|
|
10
|
+
operations and reads their authoritative Bundle results.
|
|
11
|
+
|
|
12
|
+
## 0.1.12 - 2026-10-21
|
|
13
|
+
|
|
14
|
+
- Add the product-neutral server passkey state machine used by SOSChain, UHC
|
|
15
|
+
and Vet BFFs. It requires platform user verification, binds credentials to
|
|
16
|
+
one verified login and profile, and releases a sealed profile-unlock secret
|
|
17
|
+
only through a short-lived, one-time server grant.
|
|
4
18
|
|
|
5
19
|
## 0.1.11 - 2026-10-20
|
|
6
20
|
|
package/README.md
CHANGED
|
@@ -69,6 +69,13 @@ BFF. It matches exact aliases inside `Organization.sameAs` CSV values and
|
|
|
69
69
|
rejects removed animal-card DID layouts before UI mapping. See
|
|
70
70
|
[the authorized subject directory contract](docs/AUTHORIZED_SUBJECT_DIRECTORY.md).
|
|
71
71
|
|
|
72
|
+
`sos-sdk-node-ts/host-organization-directory-client` is the separate trusted-
|
|
73
|
+
BFF client for a host operator's organization directory. It sends the
|
|
74
|
+
canonical `org.schema/Organization/_search` request, polls the matching
|
|
75
|
+
response route and returns the privacy-minimized, paginated Bundle projection.
|
|
76
|
+
The host jurisdiction and network come from trusted server configuration;
|
|
77
|
+
they are never browser-selected authority.
|
|
78
|
+
|
|
72
79
|
## Secure-message CSPRNG adapter
|
|
73
80
|
|
|
74
81
|
`sos-sdk-node-ts/secure-message-crypto` exports `NodeSecureRandomSource` and
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type HostOrganizationDirectoryRoute = Readonly<{
|
|
2
|
+
jurisdiction: string;
|
|
3
|
+
network: 'test' | 'local-network' | 'test-network' | 'network';
|
|
4
|
+
}>;
|
|
5
|
+
export type HostOrganizationDirectoryFilters = Readonly<{
|
|
6
|
+
jurisdiction?: string;
|
|
7
|
+
sector?: string;
|
|
8
|
+
status?: 'active' | 'suspended' | 'revoked';
|
|
9
|
+
identifierType?: string;
|
|
10
|
+
identifierValue?: string;
|
|
11
|
+
legalName?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type HostOrganizationDirectoryEntry = Readonly<{
|
|
15
|
+
id: string;
|
|
16
|
+
legalName: string;
|
|
17
|
+
name: string;
|
|
18
|
+
identifierType: string;
|
|
19
|
+
identifierValue: string;
|
|
20
|
+
jurisdiction: string;
|
|
21
|
+
sector: string;
|
|
22
|
+
status: 'active' | 'suspended' | 'revoked';
|
|
23
|
+
sameAs?: string;
|
|
24
|
+
claims: Readonly<Record<string, unknown>>;
|
|
25
|
+
}>;
|
|
26
|
+
export type HostOrganizationDirectoryPage = Readonly<{
|
|
27
|
+
total: number;
|
|
28
|
+
pageSize: number;
|
|
29
|
+
returned: number;
|
|
30
|
+
hasNext: boolean;
|
|
31
|
+
nextCursor?: string;
|
|
32
|
+
organizations: readonly HostOrganizationDirectoryEntry[];
|
|
33
|
+
}>;
|
|
34
|
+
export type HostOrganizationDirectorySearch = Readonly<{
|
|
35
|
+
filters?: HostOrganizationDirectoryFilters;
|
|
36
|
+
pageSize?: number;
|
|
37
|
+
cursor?: string;
|
|
38
|
+
requestThid?: string;
|
|
39
|
+
pollOptions?: Readonly<{
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
intervalMs?: number;
|
|
42
|
+
}>;
|
|
43
|
+
}>;
|
|
44
|
+
export type HostOrganizationLifecycleAction = '_status' | '_disable' | '_enable' | '_purge';
|
|
45
|
+
export type HostOrganizationLifecycleTarget = Readonly<{
|
|
46
|
+
identifierType: string;
|
|
47
|
+
identifierValue: string;
|
|
48
|
+
sameAs?: string;
|
|
49
|
+
}>;
|
|
50
|
+
export type HostOrganizationLifecycleResult = Readonly<{
|
|
51
|
+
status: 'active' | 'suspended' | 'revoked';
|
|
52
|
+
disposition?: string;
|
|
53
|
+
organizationId?: string;
|
|
54
|
+
}>;
|
|
55
|
+
export type SosGatewayHostOrganizationDirectoryClientOptions = Readonly<{
|
|
56
|
+
baseUrl: string;
|
|
57
|
+
bearerToken?: string;
|
|
58
|
+
appId?: string;
|
|
59
|
+
appVersion?: string;
|
|
60
|
+
defaultHeaders?: Readonly<Record<string, string>>;
|
|
61
|
+
requestTimeoutMs?: number;
|
|
62
|
+
fetchImpl?: typeof fetch;
|
|
63
|
+
}>;
|
|
64
|
+
/** Trusted-BFF transport for the host-only Organization registry directory. */
|
|
65
|
+
export declare class SosGatewayHostOrganizationDirectoryClient {
|
|
66
|
+
private readonly baseUrl;
|
|
67
|
+
private readonly headers;
|
|
68
|
+
private readonly requestTimeoutMs;
|
|
69
|
+
private readonly fetchImpl;
|
|
70
|
+
constructor(options: SosGatewayHostOrganizationDirectoryClientOptions);
|
|
71
|
+
search(route: HostOrganizationDirectoryRoute, input?: HostOrganizationDirectorySearch): Promise<HostOrganizationDirectoryPage>;
|
|
72
|
+
lifecycle(route: HostOrganizationDirectoryRoute, action: HostOrganizationLifecycleAction, target: HostOrganizationLifecycleTarget, options?: Readonly<{
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
intervalMs?: number;
|
|
75
|
+
}>): Promise<HostOrganizationLifecycleResult>;
|
|
76
|
+
private submitAndPoll;
|
|
77
|
+
private post;
|
|
78
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { buildGatewayApplicationHeaders } from 'sos-data-utils-ts/gateway-application-identity';
|
|
3
|
+
const CLAIM_LEGAL_NAME = 'org.schema.Organization.legalName';
|
|
4
|
+
const CLAIM_NAME = 'org.schema.Organization.name';
|
|
5
|
+
const CLAIM_IDENTIFIER_TYPE = 'org.schema.Organization.identifier.additionalType';
|
|
6
|
+
const CLAIM_IDENTIFIER_VALUE = 'org.schema.Organization.identifier.value';
|
|
7
|
+
const CLAIM_JURISDICTION = 'org.schema.Organization.address.addressCountry';
|
|
8
|
+
const CLAIM_SAME_AS = 'org.schema.Organization.sameAs';
|
|
9
|
+
const CLAIM_SECTOR = 'org.schema.Service.category';
|
|
10
|
+
const CLAIM_STATUS = 'org.schema.Action.tenantAuthorization.status';
|
|
11
|
+
/** Trusted-BFF transport for the host-only Organization registry directory. */
|
|
12
|
+
export class SosGatewayHostOrganizationDirectoryClient {
|
|
13
|
+
baseUrl;
|
|
14
|
+
headers;
|
|
15
|
+
requestTimeoutMs;
|
|
16
|
+
fetchImpl;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
19
|
+
this.requestTimeoutMs = positiveInteger(options.requestTimeoutMs ?? 15_000, 'request_timeout_invalid');
|
|
20
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
21
|
+
const bearerToken = optional(options.bearerToken);
|
|
22
|
+
const appId = optional(options.appId);
|
|
23
|
+
this.headers = Object.freeze({
|
|
24
|
+
'Content-Type': 'application/didcomm-plain+json',
|
|
25
|
+
Accept: 'application/didcomm-plain+json',
|
|
26
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
27
|
+
...(appId ? buildGatewayApplicationHeaders({ appId, appVersion: options.appVersion }) : {}),
|
|
28
|
+
...(options.defaultHeaders ?? {}),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async search(route, input = {}) {
|
|
32
|
+
const context = normalizeRoute(route);
|
|
33
|
+
const thid = optional(input.requestThid) || `host-organization-directory-${randomUUID()}`;
|
|
34
|
+
const claims = filterClaims(input.filters);
|
|
35
|
+
const body = {
|
|
36
|
+
resourceType: 'Bundle',
|
|
37
|
+
type: 'searchset',
|
|
38
|
+
data: [{
|
|
39
|
+
type: 'Organization-search-v1.0',
|
|
40
|
+
request: { method: 'GET', url: 'Organization/_search' },
|
|
41
|
+
resource: { resourceType: 'Organization', meta: { claims } },
|
|
42
|
+
}],
|
|
43
|
+
meta: { pagination: {
|
|
44
|
+
...(input.pageSize === undefined ? {} : { pageSize: positiveInteger(input.pageSize, 'page_size_invalid') }),
|
|
45
|
+
...(optional(input.cursor) ? { cursor: optional(input.cursor) } : {}),
|
|
46
|
+
} },
|
|
47
|
+
};
|
|
48
|
+
const prefix = `/host/cds-${encodeURIComponent(context.jurisdiction)}/v1/${encodeURIComponent(context.network)}/registry/org.schema/Organization`;
|
|
49
|
+
const response = await this.submitAndPoll(`${prefix}/_search`, `${prefix}/_search-response`, { thid, body }, input.pollOptions);
|
|
50
|
+
return readPage(response.body);
|
|
51
|
+
}
|
|
52
|
+
async lifecycle(route, action, target, options) {
|
|
53
|
+
const context = normalizeRoute(route);
|
|
54
|
+
const identifierType = required(target.identifierType, 'host_lifecycle_identifier_type_required');
|
|
55
|
+
const identifierValue = required(target.identifierValue, 'host_lifecycle_identifier_value_required');
|
|
56
|
+
const thid = `host-organization-lifecycle-${randomUUID()}`;
|
|
57
|
+
const claims = {
|
|
58
|
+
[CLAIM_IDENTIFIER_TYPE]: identifierType,
|
|
59
|
+
[CLAIM_IDENTIFIER_VALUE]: identifierValue,
|
|
60
|
+
...(optional(target.sameAs) ? { [CLAIM_SAME_AS]: optional(target.sameAs) } : {}),
|
|
61
|
+
};
|
|
62
|
+
const body = {
|
|
63
|
+
resourceType: 'Bundle',
|
|
64
|
+
type: 'batch',
|
|
65
|
+
data: [{
|
|
66
|
+
type: `Organization-${action.slice(1)}-request-v1.0`,
|
|
67
|
+
request: { method: 'POST', url: `Organization/${action}` },
|
|
68
|
+
resource: { resourceType: 'Organization', meta: { claims } },
|
|
69
|
+
}],
|
|
70
|
+
};
|
|
71
|
+
const prefix = `/host/cds-${encodeURIComponent(context.jurisdiction)}/v1/${encodeURIComponent(context.network)}/registry/org.schema/Organization`;
|
|
72
|
+
const response = await this.submitAndPoll(`${prefix}/${action}`, `${prefix}/${action}-response`, { thid, body }, options);
|
|
73
|
+
return readLifecycleResult(response.body);
|
|
74
|
+
}
|
|
75
|
+
async submitAndPoll(submitPath, pollPath, payload, options) {
|
|
76
|
+
const submit = await this.post(submitPath, payload);
|
|
77
|
+
requireSuccessfulResponse(submit, 'gateway_submit_failed');
|
|
78
|
+
const timeoutMs = positiveInteger(options?.timeoutMs ?? 30_000, 'poll_timeout_invalid');
|
|
79
|
+
const intervalMs = positiveInteger(options?.intervalMs ?? 250, 'poll_interval_invalid');
|
|
80
|
+
const started = Date.now();
|
|
81
|
+
for (;;) {
|
|
82
|
+
const poll = await this.post(pollPath, { thid: payload.thid });
|
|
83
|
+
if (poll.status !== 202) {
|
|
84
|
+
requireSuccessfulResponse(poll, 'gateway_poll_failed');
|
|
85
|
+
return poll;
|
|
86
|
+
}
|
|
87
|
+
if (Date.now() - started >= timeoutMs)
|
|
88
|
+
throw new Error('gateway_poll_timeout');
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async post(path, payload) {
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
95
|
+
try {
|
|
96
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
97
|
+
method: 'POST', headers: this.headers, body: JSON.stringify(payload), signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
const raw = await response.text();
|
|
100
|
+
let body = {};
|
|
101
|
+
if (raw) {
|
|
102
|
+
try {
|
|
103
|
+
body = JSON.parse(raw);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
body = raw;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { status: response.status, body };
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function normalizeRoute(value) {
|
|
117
|
+
const keys = Object.keys(value);
|
|
118
|
+
if (keys.some(key => key !== 'jurisdiction' && key !== 'network'))
|
|
119
|
+
throw new Error('route_context_invalid');
|
|
120
|
+
const jurisdiction = segment(value.jurisdiction);
|
|
121
|
+
const network = segment(value.network);
|
|
122
|
+
if (!['test', 'local-network', 'test-network', 'network'].includes(network))
|
|
123
|
+
throw new Error('route_context_invalid');
|
|
124
|
+
return Object.freeze({ jurisdiction, network: network });
|
|
125
|
+
}
|
|
126
|
+
function filterClaims(filters = {}) {
|
|
127
|
+
return Object.freeze(Object.fromEntries([
|
|
128
|
+
[CLAIM_JURISDICTION, filters.jurisdiction],
|
|
129
|
+
[CLAIM_SECTOR, filters.sector],
|
|
130
|
+
[CLAIM_STATUS, filters.status],
|
|
131
|
+
[CLAIM_IDENTIFIER_TYPE, filters.identifierType],
|
|
132
|
+
[CLAIM_IDENTIFIER_VALUE, filters.identifierValue],
|
|
133
|
+
[CLAIM_LEGAL_NAME, filters.legalName],
|
|
134
|
+
[CLAIM_NAME, filters.name],
|
|
135
|
+
].flatMap(([key, value]) => optional(value) ? [[key, optional(value)]] : [])));
|
|
136
|
+
}
|
|
137
|
+
function readPage(value) {
|
|
138
|
+
const root = record(value);
|
|
139
|
+
const bundle = record(root.body || root);
|
|
140
|
+
const data = Array.isArray(bundle.data) ? bundle.data : [];
|
|
141
|
+
const outcome = data.flatMap((entry) => {
|
|
142
|
+
const response = record(record(entry).response);
|
|
143
|
+
const result = record(response.outcome);
|
|
144
|
+
return result.resourceType === 'OperationOutcome' && Array.isArray(result.issue) ? result.issue : [];
|
|
145
|
+
}).map(record).find(issue => ['fatal', 'error'].includes(String(issue.severity || '').toLowerCase()));
|
|
146
|
+
if (outcome)
|
|
147
|
+
throw new Error(`host_directory_failed:${String(outcome.diagnostics || outcome.code || 'operation_outcome')}`);
|
|
148
|
+
if (bundle.resourceType !== 'Bundle' || bundle.type !== 'searchset')
|
|
149
|
+
throw new Error('host_directory_bundle_invalid');
|
|
150
|
+
const organizations = data.flatMap((entry) => {
|
|
151
|
+
const row = record(entry);
|
|
152
|
+
const response = record(row.response);
|
|
153
|
+
const resource = record(row.resource);
|
|
154
|
+
const claims = Object.freeze({ ...record(record(resource.meta).claims) });
|
|
155
|
+
if (resource.resourceType !== 'Organization' || !String(response.status || '').startsWith('2'))
|
|
156
|
+
return [];
|
|
157
|
+
const status = String(claims[CLAIM_STATUS] || '');
|
|
158
|
+
if (!['active', 'suspended', 'revoked'].includes(status))
|
|
159
|
+
throw new Error('host_directory_status_invalid');
|
|
160
|
+
return [Object.freeze({
|
|
161
|
+
id: String(resource.id || row.id || ''),
|
|
162
|
+
legalName: String(claims[CLAIM_LEGAL_NAME] || ''),
|
|
163
|
+
name: String(claims[CLAIM_NAME] || claims[CLAIM_LEGAL_NAME] || ''),
|
|
164
|
+
identifierType: String(claims[CLAIM_IDENTIFIER_TYPE] || ''),
|
|
165
|
+
identifierValue: String(claims[CLAIM_IDENTIFIER_VALUE] || ''),
|
|
166
|
+
jurisdiction: String(claims[CLAIM_JURISDICTION] || ''),
|
|
167
|
+
sector: String(claims[CLAIM_SECTOR] || ''),
|
|
168
|
+
status: status,
|
|
169
|
+
...(optional(claims[CLAIM_SAME_AS]) ? { sameAs: optional(claims[CLAIM_SAME_AS]) } : {}),
|
|
170
|
+
claims,
|
|
171
|
+
})];
|
|
172
|
+
});
|
|
173
|
+
const pagination = record(record(bundle.meta).pagination);
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
total: nonNegativeInteger(bundle.total, 'host_directory_total_invalid'),
|
|
176
|
+
pageSize: positiveInteger(pagination.pageSize, 'host_directory_page_size_invalid'),
|
|
177
|
+
returned: nonNegativeInteger(pagination.returned, 'host_directory_returned_invalid'),
|
|
178
|
+
hasNext: pagination.hasNext === true,
|
|
179
|
+
...(optional(pagination.nextCursor) ? { nextCursor: optional(pagination.nextCursor) } : {}),
|
|
180
|
+
organizations: Object.freeze(organizations),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function readLifecycleResult(value) {
|
|
184
|
+
const root = record(value);
|
|
185
|
+
const bundle = record(root.body || root);
|
|
186
|
+
const data = Array.isArray(bundle.data) ? bundle.data : [];
|
|
187
|
+
const issue = data.flatMap((entry) => {
|
|
188
|
+
const outcome = record(record(record(entry).response).outcome);
|
|
189
|
+
return outcome.resourceType === 'OperationOutcome' && Array.isArray(outcome.issue) ? outcome.issue : [];
|
|
190
|
+
}).map(record).find(candidate => ['fatal', 'error'].includes(String(candidate.severity || '').toLowerCase()));
|
|
191
|
+
if (issue)
|
|
192
|
+
throw new Error(`host_lifecycle_failed:${String(issue.diagnostics || issue.code || 'operation_outcome')}`);
|
|
193
|
+
const entry = data.map(record).find(candidate => String(record(candidate.response).status || '').startsWith('2'));
|
|
194
|
+
if (!entry)
|
|
195
|
+
throw new Error('host_lifecycle_response_invalid');
|
|
196
|
+
const resource = record(entry.resource);
|
|
197
|
+
const claims = record(record(resource.meta).claims);
|
|
198
|
+
const status = String(claims[CLAIM_STATUS] || '');
|
|
199
|
+
if (!['active', 'suspended', 'revoked'].includes(status))
|
|
200
|
+
throw new Error('host_lifecycle_status_invalid');
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
status: status,
|
|
203
|
+
...(optional(claims['org.schema.Action.tenantAuthorization.lifecycleDisposition']) ? { disposition: optional(claims['org.schema.Action.tenantAuthorization.lifecycleDisposition']) } : {}),
|
|
204
|
+
...(optional(resource.id) ? { organizationId: optional(resource.id) } : {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function requireSuccessfulResponse(response, code) {
|
|
208
|
+
if (response.status >= 200 && response.status < 300)
|
|
209
|
+
return;
|
|
210
|
+
throw new Error(`${code}:${response.status}`);
|
|
211
|
+
}
|
|
212
|
+
function normalizeBaseUrl(value) {
|
|
213
|
+
const url = new URL(value);
|
|
214
|
+
if (url.protocol !== 'https:' && url.hostname !== 'localhost' && url.hostname !== '127.0.0.1')
|
|
215
|
+
throw new Error('gateway_base_url_insecure');
|
|
216
|
+
return url.href.replace(/\/$/u, '');
|
|
217
|
+
}
|
|
218
|
+
function segment(value) {
|
|
219
|
+
const result = String(value ?? '').trim();
|
|
220
|
+
if (!result || result.includes('/') || result === '.' || result === '..')
|
|
221
|
+
throw new Error('route_context_invalid');
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
function record(value) {
|
|
225
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
226
|
+
}
|
|
227
|
+
function optional(value) {
|
|
228
|
+
const result = String(value ?? '').trim();
|
|
229
|
+
return result || undefined;
|
|
230
|
+
}
|
|
231
|
+
function required(value, code) {
|
|
232
|
+
const result = optional(value);
|
|
233
|
+
if (!result)
|
|
234
|
+
throw new Error(code);
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function positiveInteger(value, code) {
|
|
238
|
+
const result = Number(value);
|
|
239
|
+
if (!Number.isInteger(result) || result < 1)
|
|
240
|
+
throw new Error(code);
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
function nonNegativeInteger(value, code) {
|
|
244
|
+
const result = Number(value);
|
|
245
|
+
if (!Number.isInteger(result) || result < 0)
|
|
246
|
+
throw new Error(code);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export * from './bundle-sync-service.js';
|
|
|
7
7
|
export * from './encounter-transfer-audience-service.js';
|
|
8
8
|
export * from './bff-email-assertion.js';
|
|
9
9
|
export * from './subject-card-directory-client.js';
|
|
10
|
+
export * from './server-passkey-profile.js';
|
|
11
|
+
export * from './host-organization-directory-client.js';
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,5 @@ export * from './bundle-sync-service.js';
|
|
|
7
7
|
export * from './encounter-transfer-audience-service.js';
|
|
8
8
|
export * from './bff-email-assertion.js';
|
|
9
9
|
export * from './subject-card-directory-client.js';
|
|
10
|
+
export * from './server-passkey-profile.js';
|
|
11
|
+
export * from './host-organization-directory-client.js';
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export type ServerPasskeyCredential = Readonly<{
|
|
2
|
+
credentialId: string;
|
|
3
|
+
publicKey: string;
|
|
4
|
+
counter: number;
|
|
5
|
+
transports?: readonly string[];
|
|
6
|
+
ownerId: string;
|
|
7
|
+
loginIdentifier: string;
|
|
8
|
+
profileId: string;
|
|
9
|
+
sealedUnlockSecret: string;
|
|
10
|
+
deviceType: string;
|
|
11
|
+
backedUp: boolean;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type ServerPasskeyCeremony = Readonly<{
|
|
15
|
+
kind: 'registration' | 'authentication';
|
|
16
|
+
challenge: string;
|
|
17
|
+
ownerId: string;
|
|
18
|
+
loginIdentifier: string;
|
|
19
|
+
profileId: string;
|
|
20
|
+
expiresAt: string;
|
|
21
|
+
}>;
|
|
22
|
+
export type ServerPasskeyAccessGrant = Readonly<{
|
|
23
|
+
ownerId: string;
|
|
24
|
+
loginIdentifier: string;
|
|
25
|
+
profileId: string;
|
|
26
|
+
credentialKey: string;
|
|
27
|
+
expiresAt: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
}>;
|
|
30
|
+
export interface ServerPasskeyStore {
|
|
31
|
+
listCredentials(loginIdentifier: string, profileId?: string): Promise<ServerPasskeyCredential[]>;
|
|
32
|
+
getCredential(credentialKey: string): Promise<ServerPasskeyCredential | undefined>;
|
|
33
|
+
getCeremony(ceremonyId: string): Promise<ServerPasskeyCeremony | undefined>;
|
|
34
|
+
putCeremony(ceremonyId: string, ceremony: ServerPasskeyCeremony): Promise<void>;
|
|
35
|
+
completeRegistration(ceremonyId: string, credentialKey: string, credential: ServerPasskeyCredential): Promise<void>;
|
|
36
|
+
completeAuthentication(ceremonyId: string, credentialKey: string, counter: number, accessKey: string, access: ServerPasskeyAccessGrant): Promise<void>;
|
|
37
|
+
consumeAccess(accessKey: string): Promise<ServerPasskeyAccessGrant | undefined>;
|
|
38
|
+
}
|
|
39
|
+
export interface ServerPasskeyCeremonyAdapter {
|
|
40
|
+
credentialId(response: unknown): string;
|
|
41
|
+
registrationOptions(input: Readonly<{
|
|
42
|
+
ownerId: string;
|
|
43
|
+
loginIdentifier: string;
|
|
44
|
+
profileId: string;
|
|
45
|
+
existing: readonly ServerPasskeyCredential[];
|
|
46
|
+
}>): Promise<Readonly<{
|
|
47
|
+
challenge: string;
|
|
48
|
+
options: any;
|
|
49
|
+
}>>;
|
|
50
|
+
verifyRegistration(input: Readonly<{
|
|
51
|
+
response: unknown;
|
|
52
|
+
expectedChallenge: string;
|
|
53
|
+
}>): Promise<Readonly<{
|
|
54
|
+
credentialId: string;
|
|
55
|
+
publicKey: string;
|
|
56
|
+
counter: number;
|
|
57
|
+
transports?: readonly string[];
|
|
58
|
+
deviceType: string;
|
|
59
|
+
backedUp: boolean;
|
|
60
|
+
}>>;
|
|
61
|
+
authenticationOptions(input: Readonly<{
|
|
62
|
+
loginIdentifier: string;
|
|
63
|
+
profileId: string;
|
|
64
|
+
credentials: readonly ServerPasskeyCredential[];
|
|
65
|
+
}>): Promise<Readonly<{
|
|
66
|
+
challenge: string;
|
|
67
|
+
options: any;
|
|
68
|
+
}>>;
|
|
69
|
+
verifyAuthentication(input: Readonly<{
|
|
70
|
+
response: unknown;
|
|
71
|
+
expectedChallenge: string;
|
|
72
|
+
credential: ServerPasskeyCredential;
|
|
73
|
+
}>): Promise<Readonly<{
|
|
74
|
+
newCounter: number;
|
|
75
|
+
}>>;
|
|
76
|
+
}
|
|
77
|
+
export type ServerPasskeySealer = Readonly<{
|
|
78
|
+
seal(value: string, aad: string): Promise<string>;
|
|
79
|
+
unseal(value: string, aad: string): Promise<string>;
|
|
80
|
+
}>;
|
|
81
|
+
export type ServerPasskeyProfileServiceOptions = Readonly<{
|
|
82
|
+
store: ServerPasskeyStore;
|
|
83
|
+
sealer: ServerPasskeySealer;
|
|
84
|
+
profiles: Readonly<{
|
|
85
|
+
assertBinding(input: Readonly<{
|
|
86
|
+
ownerId: string;
|
|
87
|
+
loginIdentifier: string;
|
|
88
|
+
profileId: string;
|
|
89
|
+
}>): Promise<void>;
|
|
90
|
+
verifyAndGetUnlockSecret(input: Readonly<{
|
|
91
|
+
ownerId: string;
|
|
92
|
+
loginIdentifier: string;
|
|
93
|
+
profileId: string;
|
|
94
|
+
pin: string;
|
|
95
|
+
}>): Promise<string>;
|
|
96
|
+
}>;
|
|
97
|
+
ceremonies: ServerPasskeyCeremonyAdapter;
|
|
98
|
+
issueLoginToken(loginIdentifier: string): Promise<string>;
|
|
99
|
+
now?: () => Date;
|
|
100
|
+
randomId?: () => string;
|
|
101
|
+
randomToken?: () => string;
|
|
102
|
+
ceremonyTtlMs?: number;
|
|
103
|
+
accessTtlMs?: number;
|
|
104
|
+
}>;
|
|
105
|
+
/** Product-neutral passkey state machine for SOS, health and animal portal BFFs. */
|
|
106
|
+
export declare class ServerPasskeyProfileService {
|
|
107
|
+
private readonly options;
|
|
108
|
+
constructor(options: ServerPasskeyProfileServiceOptions);
|
|
109
|
+
hasCredential(loginIdentifier: string, profileId: string): Promise<boolean>;
|
|
110
|
+
beginRegistration(input: Readonly<{
|
|
111
|
+
ownerId: string;
|
|
112
|
+
loginIdentifier: string;
|
|
113
|
+
profileId: string;
|
|
114
|
+
}>): Promise<{
|
|
115
|
+
ceremonyId: string;
|
|
116
|
+
options: any;
|
|
117
|
+
}>;
|
|
118
|
+
completeRegistration(input: Readonly<{
|
|
119
|
+
ceremonyId: string;
|
|
120
|
+
ownerId: string;
|
|
121
|
+
loginIdentifier: string;
|
|
122
|
+
pin: string;
|
|
123
|
+
response: unknown;
|
|
124
|
+
}>): Promise<Readonly<{
|
|
125
|
+
credentialId: string;
|
|
126
|
+
}>>;
|
|
127
|
+
beginAuthentication(input: Readonly<{
|
|
128
|
+
loginIdentifier: string;
|
|
129
|
+
profileId?: string;
|
|
130
|
+
}>): Promise<{
|
|
131
|
+
ceremonyId: string;
|
|
132
|
+
options: any;
|
|
133
|
+
}>;
|
|
134
|
+
completeAuthentication(input: Readonly<{
|
|
135
|
+
ceremonyId: string;
|
|
136
|
+
response: unknown;
|
|
137
|
+
}>): Promise<Readonly<{
|
|
138
|
+
loginToken: string;
|
|
139
|
+
profileId: string;
|
|
140
|
+
profileAccessToken: string;
|
|
141
|
+
}>>;
|
|
142
|
+
consumeProfileAccess(input: Readonly<{
|
|
143
|
+
accessToken: string;
|
|
144
|
+
ownerId: string;
|
|
145
|
+
profileId: string;
|
|
146
|
+
loginIdentifier?: string;
|
|
147
|
+
}>): Promise<Readonly<{
|
|
148
|
+
unlockSecret: string;
|
|
149
|
+
}>>;
|
|
150
|
+
private now;
|
|
151
|
+
private expiresAt;
|
|
152
|
+
}
|
|
153
|
+
/** Canonical SimpleWebAuthn adapter shared by Node portal BFFs. */
|
|
154
|
+
export declare function createSimpleWebAuthnCeremonyAdapter(input: Readonly<{
|
|
155
|
+
rpName: string;
|
|
156
|
+
rpId: string;
|
|
157
|
+
origins: readonly string[];
|
|
158
|
+
}>): ServerPasskeyCeremonyAdapter;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
|
|
3
|
+
const CEREMONY_TTL_MS = 5 * 60 * 1000;
|
|
4
|
+
const ACCESS_TTL_MS = 15 * 60 * 1000;
|
|
5
|
+
/** Product-neutral passkey state machine for SOS, health and animal portal BFFs. */
|
|
6
|
+
export class ServerPasskeyProfileService {
|
|
7
|
+
options;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
async hasCredential(loginIdentifier, profileId) {
|
|
12
|
+
return (await this.options.store.listCredentials(normalizeLoginIdentifier(loginIdentifier), profileId)).length > 0;
|
|
13
|
+
}
|
|
14
|
+
async beginRegistration(input) {
|
|
15
|
+
const loginIdentifier = normalizeLoginIdentifier(input.loginIdentifier);
|
|
16
|
+
await this.options.profiles.assertBinding({ ...input, loginIdentifier });
|
|
17
|
+
const existing = await this.options.store.listCredentials(loginIdentifier, input.profileId);
|
|
18
|
+
const generated = await this.options.ceremonies.registrationOptions({ ...input, loginIdentifier, existing });
|
|
19
|
+
const ceremonyId = this.options.randomId?.() ?? randomUUID();
|
|
20
|
+
await this.options.store.putCeremony(ceremonyId, {
|
|
21
|
+
kind: 'registration', challenge: generated.challenge, ownerId: input.ownerId,
|
|
22
|
+
loginIdentifier, profileId: input.profileId,
|
|
23
|
+
expiresAt: this.expiresAt(this.options.ceremonyTtlMs ?? CEREMONY_TTL_MS),
|
|
24
|
+
});
|
|
25
|
+
return { ceremonyId, options: generated.options };
|
|
26
|
+
}
|
|
27
|
+
async completeRegistration(input) {
|
|
28
|
+
const ceremony = await this.options.store.getCeremony(input.ceremonyId);
|
|
29
|
+
const loginIdentifier = normalizeLoginIdentifier(input.loginIdentifier);
|
|
30
|
+
requireCeremony(ceremony, 'registration', this.now(), { ownerId: input.ownerId, loginIdentifier });
|
|
31
|
+
await this.options.profiles.assertBinding({ ownerId: input.ownerId, loginIdentifier, profileId: ceremony.profileId });
|
|
32
|
+
const verified = await this.options.ceremonies.verifyRegistration({
|
|
33
|
+
response: input.response, expectedChallenge: ceremony.challenge,
|
|
34
|
+
});
|
|
35
|
+
const unlockSecret = await this.options.profiles.verifyAndGetUnlockSecret({
|
|
36
|
+
ownerId: input.ownerId, loginIdentifier, profileId: ceremony.profileId, pin: input.pin,
|
|
37
|
+
});
|
|
38
|
+
if (!unlockSecret)
|
|
39
|
+
throw new Error('profile_unlock_secret_missing');
|
|
40
|
+
const credentialKey = digest(verified.credentialId);
|
|
41
|
+
await this.options.store.completeRegistration(input.ceremonyId, credentialKey, {
|
|
42
|
+
credentialId: verified.credentialId,
|
|
43
|
+
publicKey: verified.publicKey,
|
|
44
|
+
counter: verified.counter,
|
|
45
|
+
...(verified.transports?.length ? { transports: verified.transports } : {}),
|
|
46
|
+
ownerId: input.ownerId,
|
|
47
|
+
loginIdentifier,
|
|
48
|
+
profileId: ceremony.profileId,
|
|
49
|
+
sealedUnlockSecret: await this.options.sealer.seal(unlockSecret, `${verified.credentialId}:profile-unlock-secret`),
|
|
50
|
+
deviceType: verified.deviceType,
|
|
51
|
+
backedUp: verified.backedUp,
|
|
52
|
+
createdAt: this.now().toISOString(),
|
|
53
|
+
});
|
|
54
|
+
return { credentialId: verified.credentialId };
|
|
55
|
+
}
|
|
56
|
+
async beginAuthentication(input) {
|
|
57
|
+
const loginIdentifier = normalizeLoginIdentifier(input.loginIdentifier);
|
|
58
|
+
const credentials = await this.options.store.listCredentials(loginIdentifier, input.profileId);
|
|
59
|
+
if (!credentials.length)
|
|
60
|
+
throw new Error('passkey_not_registered');
|
|
61
|
+
const profileIds = new Set(credentials.map(credential => credential.profileId));
|
|
62
|
+
if (profileIds.size !== 1)
|
|
63
|
+
throw new Error('passkey_profile_selection_required');
|
|
64
|
+
const first = credentials[0];
|
|
65
|
+
const generated = await this.options.ceremonies.authenticationOptions({
|
|
66
|
+
loginIdentifier, profileId: first.profileId, credentials,
|
|
67
|
+
});
|
|
68
|
+
const ceremonyId = this.options.randomId?.() ?? randomUUID();
|
|
69
|
+
await this.options.store.putCeremony(ceremonyId, {
|
|
70
|
+
kind: 'authentication', challenge: generated.challenge, ownerId: first.ownerId,
|
|
71
|
+
loginIdentifier, profileId: first.profileId,
|
|
72
|
+
expiresAt: this.expiresAt(this.options.ceremonyTtlMs ?? CEREMONY_TTL_MS),
|
|
73
|
+
});
|
|
74
|
+
return { ceremonyId, options: generated.options };
|
|
75
|
+
}
|
|
76
|
+
async completeAuthentication(input) {
|
|
77
|
+
const ceremony = await this.options.store.getCeremony(input.ceremonyId);
|
|
78
|
+
requireCeremony(ceremony, 'authentication', this.now());
|
|
79
|
+
const credentialId = this.options.ceremonies.credentialId(input.response);
|
|
80
|
+
const credentialKey = digest(credentialId);
|
|
81
|
+
const credential = await this.options.store.getCredential(credentialKey);
|
|
82
|
+
if (!credential || credential.ownerId !== ceremony.ownerId || credential.loginIdentifier !== ceremony.loginIdentifier
|
|
83
|
+
|| credential.profileId !== ceremony.profileId)
|
|
84
|
+
throw new Error('passkey_ceremony_invalid');
|
|
85
|
+
await this.options.profiles.assertBinding({
|
|
86
|
+
ownerId: credential.ownerId, loginIdentifier: credential.loginIdentifier, profileId: credential.profileId,
|
|
87
|
+
});
|
|
88
|
+
const verified = await this.options.ceremonies.verifyAuthentication({
|
|
89
|
+
response: input.response, expectedChallenge: ceremony.challenge, credential,
|
|
90
|
+
});
|
|
91
|
+
const profileAccessToken = this.options.randomToken?.() ?? randomBytes(32).toString('base64url');
|
|
92
|
+
await this.options.store.completeAuthentication(input.ceremonyId, credentialKey, verified.newCounter, digest(profileAccessToken), {
|
|
93
|
+
ownerId: credential.ownerId, loginIdentifier: credential.loginIdentifier,
|
|
94
|
+
profileId: credential.profileId, credentialKey,
|
|
95
|
+
expiresAt: this.expiresAt(this.options.accessTtlMs ?? ACCESS_TTL_MS),
|
|
96
|
+
createdAt: this.now().toISOString(),
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
loginToken: await this.options.issueLoginToken(credential.loginIdentifier),
|
|
100
|
+
profileId: credential.profileId,
|
|
101
|
+
profileAccessToken,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async consumeProfileAccess(input) {
|
|
105
|
+
if (!input.accessToken)
|
|
106
|
+
throw new Error('passkey_profile_access_required');
|
|
107
|
+
const access = await this.options.store.consumeAccess(digest(input.accessToken));
|
|
108
|
+
const loginIdentifier = input.loginIdentifier ? normalizeLoginIdentifier(input.loginIdentifier) : undefined;
|
|
109
|
+
if (!access || access.ownerId !== input.ownerId || access.profileId !== input.profileId
|
|
110
|
+
|| (loginIdentifier && access.loginIdentifier !== loginIdentifier) || new Date(access.expiresAt) <= this.now()) {
|
|
111
|
+
throw new Error('passkey_profile_access_invalid');
|
|
112
|
+
}
|
|
113
|
+
const credential = await this.options.store.getCredential(access.credentialKey);
|
|
114
|
+
if (!credential || credential.ownerId !== input.ownerId || credential.profileId !== input.profileId
|
|
115
|
+
|| (loginIdentifier && credential.loginIdentifier !== loginIdentifier))
|
|
116
|
+
throw new Error('passkey_profile_access_invalid');
|
|
117
|
+
return {
|
|
118
|
+
unlockSecret: await this.options.sealer.unseal(credential.sealedUnlockSecret, `${credential.credentialId}:profile-unlock-secret`),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
now() { return this.options.now?.() ?? new Date(); }
|
|
122
|
+
expiresAt(milliseconds) { return new Date(this.now().getTime() + milliseconds).toISOString(); }
|
|
123
|
+
}
|
|
124
|
+
function normalizeLoginIdentifier(value) {
|
|
125
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
126
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) && !/^\+[1-9]\d{7,14}$/.test(normalized)) {
|
|
127
|
+
throw new Error('invalid_login_identifier');
|
|
128
|
+
}
|
|
129
|
+
return normalized;
|
|
130
|
+
}
|
|
131
|
+
function digest(value) { return createHash('sha256').update(value).digest('hex'); }
|
|
132
|
+
function requireCeremony(ceremony, kind, now, identity) {
|
|
133
|
+
if (!ceremony || ceremony.kind !== kind || new Date(ceremony.expiresAt) <= now
|
|
134
|
+
|| (identity && (ceremony.ownerId !== identity.ownerId || ceremony.loginIdentifier !== identity.loginIdentifier))) {
|
|
135
|
+
throw new Error('passkey_ceremony_invalid');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Canonical SimpleWebAuthn adapter shared by Node portal BFFs. */
|
|
139
|
+
export function createSimpleWebAuthnCeremonyAdapter(input) {
|
|
140
|
+
const rpName = String(input.rpName || '').trim();
|
|
141
|
+
const rpId = String(input.rpId || '').trim();
|
|
142
|
+
const origins = input.origins.map(value => String(value || '').trim()).filter(Boolean);
|
|
143
|
+
if (!rpName || !rpId || !origins.length || origins.some(origin => !origin.startsWith('https://'))) {
|
|
144
|
+
throw new Error('passkey_relying_party_not_configured');
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
credentialId(response) { return String(response?.id || ''); },
|
|
148
|
+
async registrationOptions({ ownerId, loginIdentifier, existing }) {
|
|
149
|
+
const options = await generateRegistrationOptions({
|
|
150
|
+
rpName, rpID: rpId, userName: loginIdentifier, userDisplayName: loginIdentifier,
|
|
151
|
+
userID: new TextEncoder().encode(ownerId), attestationType: 'none',
|
|
152
|
+
excludeCredentials: existing.map(credential => ({
|
|
153
|
+
id: credential.credentialId,
|
|
154
|
+
transports: credential.transports,
|
|
155
|
+
})),
|
|
156
|
+
authenticatorSelection: { residentKey: 'required', userVerification: 'required' },
|
|
157
|
+
preferredAuthenticatorType: 'localDevice',
|
|
158
|
+
});
|
|
159
|
+
return { challenge: options.challenge, options };
|
|
160
|
+
},
|
|
161
|
+
async verifyRegistration({ response, expectedChallenge }) {
|
|
162
|
+
const verification = await verifyRegistrationResponse({
|
|
163
|
+
response: response,
|
|
164
|
+
expectedChallenge, expectedOrigin: [...origins], expectedRPID: rpId, requireUserVerification: true,
|
|
165
|
+
});
|
|
166
|
+
if (!verification.verified || !verification.registrationInfo)
|
|
167
|
+
throw new Error('passkey_registration_failed');
|
|
168
|
+
const credential = verification.registrationInfo.credential;
|
|
169
|
+
return {
|
|
170
|
+
credentialId: credential.id,
|
|
171
|
+
publicKey: Buffer.from(credential.publicKey).toString('base64url'),
|
|
172
|
+
counter: credential.counter,
|
|
173
|
+
transports: response.response.transports,
|
|
174
|
+
deviceType: verification.registrationInfo.credentialDeviceType,
|
|
175
|
+
backedUp: verification.registrationInfo.credentialBackedUp,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
async authenticationOptions({ credentials }) {
|
|
179
|
+
const options = await generateAuthenticationOptions({
|
|
180
|
+
rpID: rpId,
|
|
181
|
+
allowCredentials: credentials.map(credential => ({
|
|
182
|
+
id: credential.credentialId,
|
|
183
|
+
transports: credential.transports,
|
|
184
|
+
})),
|
|
185
|
+
userVerification: 'required',
|
|
186
|
+
});
|
|
187
|
+
return { challenge: options.challenge, options };
|
|
188
|
+
},
|
|
189
|
+
async verifyAuthentication({ response, expectedChallenge, credential }) {
|
|
190
|
+
const verification = await verifyAuthenticationResponse({
|
|
191
|
+
response: response,
|
|
192
|
+
expectedChallenge, expectedOrigin: [...origins], expectedRPID: rpId,
|
|
193
|
+
credential: {
|
|
194
|
+
id: credential.credentialId,
|
|
195
|
+
publicKey: Uint8Array.from(Buffer.from(credential.publicKey, 'base64url')),
|
|
196
|
+
counter: credential.counter,
|
|
197
|
+
transports: credential.transports,
|
|
198
|
+
},
|
|
199
|
+
requireUserVerification: true,
|
|
200
|
+
});
|
|
201
|
+
if (!verification.verified)
|
|
202
|
+
throw new Error('passkey_authentication_failed');
|
|
203
|
+
return { newCounter: verification.authenticationInfo.newCounter };
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sos-sdk-node-ts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Product-neutral Node adapters shared by SOSChain, Vet and UHC runtimes",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
"./subject-card-directory-client": {
|
|
48
48
|
"types": "./dist/subject-card-directory-client.d.ts",
|
|
49
49
|
"default": "./dist/subject-card-directory-client.js"
|
|
50
|
+
},
|
|
51
|
+
"./host-organization-directory-client": {
|
|
52
|
+
"types": "./dist/host-organization-directory-client.d.ts",
|
|
53
|
+
"default": "./dist/host-organization-directory-client.js"
|
|
50
54
|
}
|
|
51
55
|
},
|
|
52
56
|
"files": [
|
|
@@ -65,6 +69,7 @@
|
|
|
65
69
|
"prepack": "npm run check"
|
|
66
70
|
},
|
|
67
71
|
"dependencies": {
|
|
72
|
+
"@simplewebauthn/server": "13.3.2",
|
|
68
73
|
"sos-data-utils-ts": "0.6.13",
|
|
69
74
|
"sos-sdk-core-ts": "0.5.2"
|
|
70
75
|
},
|