sos-sdk-node-ts 0.1.14 → 0.1.15
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
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.15 - 2026-10-22
|
|
6
|
+
|
|
7
|
+
- Add product-neutral verification and resolution for an existing professional
|
|
8
|
+
DID, employee-authorization URN or signed professional SHC. Invitation and
|
|
9
|
+
device registration remain separate host-managed operations.
|
|
10
|
+
|
|
5
11
|
## 0.1.14 - 2026-10-21
|
|
6
12
|
|
|
7
13
|
- Add a product-neutral server-profile owner assertion for personal passkey
|
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export * from './subject-card-directory-client.js';
|
|
|
10
10
|
export * from './server-passkey-profile.js';
|
|
11
11
|
export * from './host-organization-directory-client.js';
|
|
12
12
|
export * from './secure-organization-registration.js';
|
|
13
|
+
export * from './portable-professional-evidence.js';
|
package/dist/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export * from './subject-card-directory-client.js';
|
|
|
10
10
|
export * from './server-passkey-profile.js';
|
|
11
11
|
export * from './host-organization-directory-client.js';
|
|
12
12
|
export * from './secure-organization-registration.js';
|
|
13
|
+
export * from './portable-professional-evidence.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type PortableProfessionalProvider = Readonly<{
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
tenantId: string;
|
|
4
|
+
jurisdiction: string;
|
|
5
|
+
sector: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type PortableProfessionalEnrollment = Readonly<{
|
|
8
|
+
sector: string;
|
|
9
|
+
employeeAuthorizationUrn: string;
|
|
10
|
+
professionalDid: string;
|
|
11
|
+
providerDid: string;
|
|
12
|
+
gatewayBaseUrl: string;
|
|
13
|
+
tenantId: string;
|
|
14
|
+
jurisdiction: string;
|
|
15
|
+
role: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Derives routing candidates only from evidence shape plus a server-owned host
|
|
19
|
+
* and sector allowlist. The returned candidate grants no authority: the signed
|
|
20
|
+
* credential or authoritative DID documents are still verified by the
|
|
21
|
+
* resolution functions below.
|
|
22
|
+
*/
|
|
23
|
+
export declare function derivePortableProfessionalProviderCandidates(input: Readonly<{
|
|
24
|
+
professionalCredential?: unknown;
|
|
25
|
+
professionalIdentifier?: unknown;
|
|
26
|
+
sectorHint?: unknown;
|
|
27
|
+
trustedGatewayBaseUrls: readonly string[];
|
|
28
|
+
allowedSectors: readonly string[];
|
|
29
|
+
}>): PortableProfessionalProvider[];
|
|
30
|
+
/**
|
|
31
|
+
* Verifies one signed professional SHC and returns only its existing
|
|
32
|
+
* organization-issued authorization reference. It creates neither an
|
|
33
|
+
* invitation nor a device registration.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveProfessionalShcEnrollment(input: Readonly<{
|
|
36
|
+
credential: unknown;
|
|
37
|
+
providers: readonly PortableProfessionalProvider[];
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
nowSeconds?: number;
|
|
40
|
+
}>): Promise<Readonly<{
|
|
41
|
+
professionalIdentifier: string;
|
|
42
|
+
sectorHint: string;
|
|
43
|
+
}>>;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves one organization-issued DID or authorization URN through
|
|
46
|
+
* server-owned provider configuration. The result still requires the host
|
|
47
|
+
* profile manager to perform licence, contact and device-registration checks.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolvePortableProfessionalEnrollment(input: Readonly<{
|
|
50
|
+
professionalIdentifier: unknown;
|
|
51
|
+
sectorHint: unknown;
|
|
52
|
+
verifiedEmail: unknown;
|
|
53
|
+
providers: readonly PortableProfessionalProvider[];
|
|
54
|
+
fetchImpl?: typeof fetch;
|
|
55
|
+
}>): Promise<PortableProfessionalEnrollment>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import { inflateRawSync } from 'node:zlib';
|
|
3
|
+
import { buildProfessionalDidWeb } from 'gdc-common-utils-ts/utils/did';
|
|
4
|
+
const EMPLOYEE_AUTHORIZATION_URN_PATTERN = /^urn:cds-([a-z]{2}(?:-[a-z0-9]{2,3})?):v[1-9][0-9]*:organization:([a-z][a-z0-9-]*):([^:]+):member:(z[1-9A-HJ-NP-Za-km-z]+):([0-9]+)$/i;
|
|
5
|
+
function record(value) {
|
|
6
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
function text(value) {
|
|
9
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
10
|
+
}
|
|
11
|
+
function identifiers(resource) {
|
|
12
|
+
return (Array.isArray(resource.identifier) ? resource.identifier : [])
|
|
13
|
+
.map(item => text(record(item).value))
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
function providerIssuer(provider) {
|
|
17
|
+
return `${new URL(provider.baseUrl).origin}/${encodeURIComponent(provider.tenantId)}/cds-${encodeURIComponent(provider.jurisdiction)}/v1/${encodeURIComponent(provider.sector)}`;
|
|
18
|
+
}
|
|
19
|
+
function decodeShcQr(value) {
|
|
20
|
+
const normalized = text(value);
|
|
21
|
+
if (!/^shc:\/\d+$/.test(normalized))
|
|
22
|
+
throw new TypeError('professional_credential_shc_required');
|
|
23
|
+
const digits = normalized.slice('shc:/'.length);
|
|
24
|
+
if (!digits || digits.length % 2 !== 0)
|
|
25
|
+
throw new TypeError('professional_credential_shc_invalid');
|
|
26
|
+
let compact = '';
|
|
27
|
+
for (let index = 0; index < digits.length; index += 2) {
|
|
28
|
+
const pair = Number(digits.slice(index, index + 2));
|
|
29
|
+
if (pair > 77)
|
|
30
|
+
throw new TypeError('professional_credential_shc_invalid');
|
|
31
|
+
compact += String.fromCharCode(pair + 45);
|
|
32
|
+
}
|
|
33
|
+
return compact;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Derives routing candidates only from evidence shape plus a server-owned host
|
|
37
|
+
* and sector allowlist. The returned candidate grants no authority: the signed
|
|
38
|
+
* credential or authoritative DID documents are still verified by the
|
|
39
|
+
* resolution functions below.
|
|
40
|
+
*/
|
|
41
|
+
export function derivePortableProfessionalProviderCandidates(input) {
|
|
42
|
+
const hasCredential = Boolean(text(input.professionalCredential));
|
|
43
|
+
const professionalIdentifier = text(input.professionalIdentifier);
|
|
44
|
+
if (hasCredential === Boolean(professionalIdentifier))
|
|
45
|
+
throw new TypeError('professional_evidence_path_invalid');
|
|
46
|
+
let tenantId = '';
|
|
47
|
+
let jurisdiction = '';
|
|
48
|
+
let sector = text(input.sectorHint).toLowerCase();
|
|
49
|
+
let issuerOrigin = '';
|
|
50
|
+
if (hasCredential) {
|
|
51
|
+
const parts = decodeShcQr(input.professionalCredential).split('.');
|
|
52
|
+
if (parts.length !== 3)
|
|
53
|
+
throw new TypeError('professional_credential_shc_invalid');
|
|
54
|
+
let payload;
|
|
55
|
+
try {
|
|
56
|
+
payload = record(JSON.parse(inflateRawSync(Buffer.from(parts[1], 'base64url')).toString('utf8')));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new TypeError('professional_credential_shc_invalid');
|
|
60
|
+
}
|
|
61
|
+
const issuer = new URL(text(payload.iss));
|
|
62
|
+
const match = issuer.pathname.match(/^\/([^/]+)\/cds-([^/]+)\/v[1-9][0-9]*\/([a-z][a-z0-9-]*)\/?$/i);
|
|
63
|
+
if (!match)
|
|
64
|
+
throw new TypeError('professional_credential_issuer_invalid');
|
|
65
|
+
tenantId = decodeURIComponent(match[1]);
|
|
66
|
+
jurisdiction = decodeURIComponent(match[2]);
|
|
67
|
+
sector = decodeURIComponent(match[3]).toLowerCase();
|
|
68
|
+
issuerOrigin = issuer.origin;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const authorization = EMPLOYEE_AUTHORIZATION_URN_PATTERN.exec(professionalIdentifier);
|
|
72
|
+
const did = professionalIdentifier.match(/:([^:]+):cds-([^:]+):v[1-9][0-9]*:([a-z][a-z0-9-]*):employee:/i);
|
|
73
|
+
if (authorization) {
|
|
74
|
+
jurisdiction = authorization[1];
|
|
75
|
+
tenantId = authorization[3];
|
|
76
|
+
}
|
|
77
|
+
else if (did) {
|
|
78
|
+
tenantId = did[1];
|
|
79
|
+
jurisdiction = did[2];
|
|
80
|
+
if (sector && sector !== did[3].toLowerCase())
|
|
81
|
+
throw new TypeError('professional_sector_mismatch');
|
|
82
|
+
sector = did[3].toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw new TypeError('professional_credential_invalid');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const allowedSectors = new Set(input.allowedSectors.map(value => text(value).toLowerCase()).filter(Boolean));
|
|
89
|
+
if (!sector || !allowedSectors.has(sector))
|
|
90
|
+
throw new TypeError('professional_sector_not_allowed');
|
|
91
|
+
const origins = [...new Set(input.trustedGatewayBaseUrls.map(value => new URL(value).origin))]
|
|
92
|
+
.filter(origin => !issuerOrigin || origin === issuerOrigin);
|
|
93
|
+
if (origins.length === 0)
|
|
94
|
+
throw new TypeError('professional_provider_not_allowed');
|
|
95
|
+
return origins.map(baseUrl => Object.freeze({ baseUrl, tenantId, jurisdiction: jurisdiction.toUpperCase(), sector }));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Verifies one signed professional SHC and returns only its existing
|
|
99
|
+
* organization-issued authorization reference. It creates neither an
|
|
100
|
+
* invitation nor a device registration.
|
|
101
|
+
*/
|
|
102
|
+
export async function resolveProfessionalShcEnrollment(input) {
|
|
103
|
+
const compact = decodeShcQr(input.credential);
|
|
104
|
+
const parts = compact.split('.');
|
|
105
|
+
if (parts.length !== 3)
|
|
106
|
+
throw new TypeError('professional_credential_shc_invalid');
|
|
107
|
+
let header;
|
|
108
|
+
let payload;
|
|
109
|
+
try {
|
|
110
|
+
header = record(JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8')));
|
|
111
|
+
payload = record(JSON.parse(inflateRawSync(Buffer.from(parts[1], 'base64url')).toString('utf8')));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new TypeError('professional_credential_shc_invalid');
|
|
115
|
+
}
|
|
116
|
+
if (header.alg !== 'ES256' || header.zip !== 'DEF' || !text(header.kid)) {
|
|
117
|
+
throw new TypeError('professional_credential_header_invalid');
|
|
118
|
+
}
|
|
119
|
+
const issuer = text(payload.iss);
|
|
120
|
+
const notBefore = Number(payload.nbf);
|
|
121
|
+
if (!issuer.startsWith('https://') || !Number.isInteger(notBefore)
|
|
122
|
+
|| notBefore > (input.nowSeconds ?? Math.floor(Date.now() / 1000))) {
|
|
123
|
+
throw new TypeError('professional_credential_time_or_issuer_invalid');
|
|
124
|
+
}
|
|
125
|
+
const provider = input.providers.find(candidate => providerIssuer(candidate) === issuer);
|
|
126
|
+
if (!provider)
|
|
127
|
+
throw new TypeError('professional_credential_provider_not_allowed');
|
|
128
|
+
const jwksResponse = await (input.fetchImpl ?? fetch)(`${issuer}/.well-known/jwks.json`, {
|
|
129
|
+
headers: { accept: 'application/jwk-set+json, application/json' },
|
|
130
|
+
});
|
|
131
|
+
if (!jwksResponse.ok)
|
|
132
|
+
throw new TypeError('professional_credential_jwks_unavailable');
|
|
133
|
+
const jwks = record(await jwksResponse.json());
|
|
134
|
+
const key = (Array.isArray(jwks.keys) ? jwks.keys : []).map(record)
|
|
135
|
+
.find(candidate => candidate.kid === header.kid && candidate.kty === 'EC' && candidate.crv === 'P-256');
|
|
136
|
+
if (!key)
|
|
137
|
+
throw new TypeError('professional_credential_key_not_found');
|
|
138
|
+
let valid = false;
|
|
139
|
+
try {
|
|
140
|
+
valid = verify('sha256', Buffer.from(`${parts[0]}.${parts[1]}`), {
|
|
141
|
+
key: createPublicKey({ key: key, format: 'jwk' }),
|
|
142
|
+
dsaEncoding: 'ieee-p1363',
|
|
143
|
+
}, Buffer.from(parts[2], 'base64url'));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
valid = false;
|
|
147
|
+
}
|
|
148
|
+
if (!valid)
|
|
149
|
+
throw new TypeError('professional_credential_signature_invalid');
|
|
150
|
+
const credentialSubject = record(record(payload.vc).credentialSubject);
|
|
151
|
+
const bundle = record(credentialSubject.fhirBundle);
|
|
152
|
+
if (bundle.resourceType !== 'Bundle')
|
|
153
|
+
throw new TypeError('professional_credential_bundle_invalid');
|
|
154
|
+
const resources = (Array.isArray(bundle.entry) ? bundle.entry : [])
|
|
155
|
+
.map(entry => record(record(entry).resource));
|
|
156
|
+
const organization = resources.find(resource => resource.resourceType === 'Organization');
|
|
157
|
+
const practitioner = resources.find(resource => resource.resourceType === 'Practitioner');
|
|
158
|
+
const practitionerRole = resources.find(resource => resource.resourceType === 'PractitionerRole');
|
|
159
|
+
if (!organization || !practitioner || !practitionerRole || practitionerRole.active === false) {
|
|
160
|
+
throw new TypeError('professional_credential_resources_required');
|
|
161
|
+
}
|
|
162
|
+
const authorizationUrn = identifiers(practitionerRole)
|
|
163
|
+
.find(value => EMPLOYEE_AUTHORIZATION_URN_PATTERN.test(value));
|
|
164
|
+
const authorization = authorizationUrn
|
|
165
|
+
? EMPLOYEE_AUTHORIZATION_URN_PATTERN.exec(authorizationUrn)
|
|
166
|
+
: undefined;
|
|
167
|
+
if (!authorization || !authorizationUrn)
|
|
168
|
+
throw new TypeError('professional_credential_authorization_missing');
|
|
169
|
+
const [, jurisdiction, , organizationOfficialId] = authorization;
|
|
170
|
+
if (jurisdiction.toLowerCase() !== provider.jurisdiction.toLowerCase()
|
|
171
|
+
|| organizationOfficialId !== provider.tenantId
|
|
172
|
+
|| !identifiers(organization).includes(provider.tenantId)
|
|
173
|
+
|| identifiers(practitioner).length === 0
|
|
174
|
+
|| text(record(practitionerRole.organization).reference) !== 'resource:0'
|
|
175
|
+
|| text(record(practitionerRole.practitioner).reference) !== 'resource:1') {
|
|
176
|
+
throw new TypeError('professional_credential_binding_invalid');
|
|
177
|
+
}
|
|
178
|
+
return Object.freeze({ professionalIdentifier: authorizationUrn, sectorHint: provider.sector });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Resolves one organization-issued DID or authorization URN through
|
|
182
|
+
* server-owned provider configuration. The result still requires the host
|
|
183
|
+
* profile manager to perform licence, contact and device-registration checks.
|
|
184
|
+
*/
|
|
185
|
+
export async function resolvePortableProfessionalEnrollment(input) {
|
|
186
|
+
const professionalIdentifier = text(input.professionalIdentifier);
|
|
187
|
+
const sectorHint = text(input.sectorHint).toLowerCase();
|
|
188
|
+
const verifiedEmail = text(input.verifiedEmail).toLowerCase();
|
|
189
|
+
if (!/^[a-z][a-z0-9-]*$/.test(sectorHint))
|
|
190
|
+
throw new TypeError('professional_sector_hint_required');
|
|
191
|
+
if (!verifiedEmail)
|
|
192
|
+
throw new TypeError('verified_email_required');
|
|
193
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
194
|
+
let employeeAuthorizationUrn = professionalIdentifier;
|
|
195
|
+
let authorization = EMPLOYEE_AUTHORIZATION_URN_PATTERN.exec(employeeAuthorizationUrn);
|
|
196
|
+
if (!authorization && professionalIdentifier.startsWith('did:web:')) {
|
|
197
|
+
const didMatches = input.providers.flatMap(provider => {
|
|
198
|
+
const context = `:${provider.tenantId}:cds-${provider.jurisdiction}:v1:${provider.sector}:employee:`;
|
|
199
|
+
if (provider.sector.toLowerCase() !== sectorHint || !professionalIdentifier.includes(context))
|
|
200
|
+
return [];
|
|
201
|
+
const employeePath = professionalIdentifier.slice(professionalIdentifier.indexOf(context) + context.length);
|
|
202
|
+
const [memberId, ...roleParts] = employeePath.split(':');
|
|
203
|
+
const role = roleParts.join(':');
|
|
204
|
+
return memberId && role ? [{ provider, memberId, role }] : [];
|
|
205
|
+
});
|
|
206
|
+
if (didMatches.length === 0)
|
|
207
|
+
throw new TypeError('professional_provider_not_found');
|
|
208
|
+
if (didMatches.length > 1)
|
|
209
|
+
throw new TypeError('multiple_professional_providers_found');
|
|
210
|
+
const { provider, memberId, role } = didMatches[0];
|
|
211
|
+
const didUrl = `${providerIssuer(provider)}/employee/${encodeURIComponent(memberId)}/${encodeURIComponent(role)}/did.json`;
|
|
212
|
+
const didResponse = await fetchImpl(didUrl, { headers: { accept: 'application/did+json, application/json' } });
|
|
213
|
+
if (!didResponse.ok)
|
|
214
|
+
throw new TypeError('professional_did_unavailable');
|
|
215
|
+
const didDocument = record(await didResponse.json());
|
|
216
|
+
if (text(didDocument.id) !== professionalIdentifier)
|
|
217
|
+
throw new TypeError('professional_did_invalid');
|
|
218
|
+
employeeAuthorizationUrn = (Array.isArray(didDocument.alsoKnownAs) ? didDocument.alsoKnownAs : [])
|
|
219
|
+
.map(String).find(value => EMPLOYEE_AUTHORIZATION_URN_PATTERN.test(value)) || '';
|
|
220
|
+
authorization = EMPLOYEE_AUTHORIZATION_URN_PATTERN.exec(employeeAuthorizationUrn);
|
|
221
|
+
}
|
|
222
|
+
if (!authorization)
|
|
223
|
+
throw new TypeError('professional_credential_invalid');
|
|
224
|
+
const [, authorizationJurisdiction, , organizationOfficialId, , authorizationRoleCode] = authorization;
|
|
225
|
+
const matches = input.providers.filter(provider => provider.tenantId === organizationOfficialId
|
|
226
|
+
&& provider.jurisdiction.toLowerCase() === authorizationJurisdiction.toLowerCase()
|
|
227
|
+
&& provider.sector.toLowerCase() === sectorHint);
|
|
228
|
+
if (matches.length === 0)
|
|
229
|
+
throw new TypeError('professional_provider_not_found');
|
|
230
|
+
if (matches.length > 1)
|
|
231
|
+
throw new TypeError('multiple_professional_providers_found');
|
|
232
|
+
const provider = matches[0];
|
|
233
|
+
const gatewayBaseUrl = new URL(provider.baseUrl).origin;
|
|
234
|
+
const response = await fetchImpl(`${providerIssuer(provider)}/.well-known/did.json`, {
|
|
235
|
+
headers: { accept: 'application/did+json, application/json' },
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok)
|
|
238
|
+
throw new TypeError('professional_tenant_did_unavailable');
|
|
239
|
+
const document = record(await response.json());
|
|
240
|
+
const providerDid = text(document.id);
|
|
241
|
+
const expectedContext = `:${provider.tenantId}:cds-${provider.jurisdiction}:v1:${provider.sector}`;
|
|
242
|
+
const services = Array.isArray(document.service) ? document.service : [];
|
|
243
|
+
const hasJwks = services.some(service => text(record(service).type) === 'JsonWebKeyService2020'
|
|
244
|
+
&& /^https:\/\//i.test(text(record(service).serviceEndpoint)));
|
|
245
|
+
if (!providerDid.startsWith('did:web:') || !providerDid.endsWith(expectedContext) || !hasJwks) {
|
|
246
|
+
throw new TypeError('professional_tenant_did_invalid');
|
|
247
|
+
}
|
|
248
|
+
const role = `ISCO-08|${authorizationRoleCode}`;
|
|
249
|
+
const professionalDid = buildProfessionalDidWeb({ organizationDidWeb: providerDid, email: verifiedEmail, role });
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
sector: provider.sector,
|
|
252
|
+
employeeAuthorizationUrn,
|
|
253
|
+
professionalDid,
|
|
254
|
+
providerDid,
|
|
255
|
+
gatewayBaseUrl,
|
|
256
|
+
tenantId: provider.tenantId,
|
|
257
|
+
jurisdiction: provider.jurisdiction.toUpperCase(),
|
|
258
|
+
role,
|
|
259
|
+
});
|
|
260
|
+
}
|
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.15",
|
|
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",
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"./secure-organization-registration": {
|
|
56
56
|
"types": "./dist/secure-organization-registration.d.ts",
|
|
57
57
|
"default": "./dist/secure-organization-registration.js"
|
|
58
|
+
},
|
|
59
|
+
"./portable-professional-evidence": {
|
|
60
|
+
"types": "./dist/portable-professional-evidence.d.ts",
|
|
61
|
+
"default": "./dist/portable-professional-evidence.js"
|
|
58
62
|
}
|
|
59
63
|
},
|
|
60
64
|
"files": [
|