@kubun/credential 0.13.0
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/LICENSE.md +57 -0
- package/lib/authority.d.ts +29 -0
- package/lib/authority.js +26 -0
- package/lib/builder.d.ts +82 -0
- package/lib/builder.js +76 -0
- package/lib/digest.d.ts +53 -0
- package/lib/digest.js +82 -0
- package/lib/errors.d.ts +169 -0
- package/lib/errors.js +248 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +10 -0
- package/lib/key-cache.d.ts +34 -0
- package/lib/key-cache.js +55 -0
- package/lib/manager.d.ts +280 -0
- package/lib/manager.js +881 -0
- package/lib/op.d.ts +128 -0
- package/lib/op.js +304 -0
- package/lib/pin-throttle.d.ts +44 -0
- package/lib/pin-throttle.js +48 -0
- package/lib/provenance.d.ts +6 -0
- package/lib/provenance.js +8 -0
- package/lib/suite.d.ts +112 -0
- package/lib/suite.js +317 -0
- package/lib/vocabulary.d.ts +16 -0
- package/lib/vocabulary.js +17 -0
- package/package.json +45 -0
package/lib/suite.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { deriveSharedSecretAsync } from '@kokuin/jwe';
|
|
2
|
+
import { gcm } from '@noble/ciphers/aes.js';
|
|
3
|
+
import { argon2idAsync } from '@noble/hashes/argon2.js';
|
|
4
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
5
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
6
|
+
import { concatBytes } from '@noble/hashes/utils.js';
|
|
7
|
+
import { canonicalStringify, fromB64, fromUTF, toB64 } from '@sozai/codec';
|
|
8
|
+
import { CredentialSecretRejected, CredentialTampered, InvalidCredentialFactors, NoAvailableFactor, UnsupportedCredentialSuite } from './errors.js';
|
|
9
|
+
/** X25519 / HKDF-SHA256 / Argon2id / A256GCM. */ export const SUITE_KUBUN_CRED_1 = 1;
|
|
10
|
+
const IV_LENGTH = 12;
|
|
11
|
+
const CONTENT_KEY_LENGTH = 32;
|
|
12
|
+
const FACTOR_SECRET_LENGTH = 32;
|
|
13
|
+
const SALT_LENGTH = 16;
|
|
14
|
+
const WRAP_INFO_PREFIX = 'kubun/credential-wrap/v1';
|
|
15
|
+
const ARGON2_COST_LIMITS = {
|
|
16
|
+
m: {
|
|
17
|
+
min: 8,
|
|
18
|
+
max: 262_144
|
|
19
|
+
},
|
|
20
|
+
t: {
|
|
21
|
+
min: 1,
|
|
22
|
+
max: 10
|
|
23
|
+
},
|
|
24
|
+
p: {
|
|
25
|
+
min: 1,
|
|
26
|
+
max: 16
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Per-wrapping, not per-suite: the suite fixes the algorithm and the wrapping
|
|
31
|
+
* fixes the cost, so cost rises with hardware without minting a suite.
|
|
32
|
+
*
|
|
33
|
+
* OWASP's first Argon2id configuration. Measured at 340ms on an M2 Max; the
|
|
34
|
+
* 46MiB/t=1 alternative costs the same time for 2.4x the memory, which is the
|
|
35
|
+
* scarcer resource on the Expo and browser targets.
|
|
36
|
+
*/ export const DEFAULT_ARGON2_COST = {
|
|
37
|
+
m: 19456,
|
|
38
|
+
t: 2,
|
|
39
|
+
p: 1
|
|
40
|
+
};
|
|
41
|
+
function invalidArgon2Cost(cost) {
|
|
42
|
+
for (const name of [
|
|
43
|
+
'm',
|
|
44
|
+
't',
|
|
45
|
+
'p'
|
|
46
|
+
]){
|
|
47
|
+
const value = cost[name];
|
|
48
|
+
const limits = ARGON2_COST_LIMITS[name];
|
|
49
|
+
if (!Number.isSafeInteger(value) || value < limits.min || value > limits.max) {
|
|
50
|
+
return `${name} must be an integer from ${limits.min} through ${limits.max}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The wrapping's key-encryption key.
|
|
57
|
+
*
|
|
58
|
+
* `salt` is the wrapping id, so two wrappings of the same content key under the
|
|
59
|
+
* same secrets still derive different KEKs. `info` binds the suite, key and
|
|
60
|
+
* version, which is what stops a wrapping at version N opening ciphertext at
|
|
61
|
+
* N+1 — a rotation's whole point.
|
|
62
|
+
*/ function deriveKEK(secrets, wrappingID, suiteID, keyID, keyVersion) {
|
|
63
|
+
const info = fromUTF(`${WRAP_INFO_PREFIX}:${suiteID}:${keyID}:${keyVersion}`);
|
|
64
|
+
return hkdf(sha256, concatBytes(...secrets), fromUTF(wrappingID), info, CONTENT_KEY_LENGTH);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The wrapping's additional authenticated data: the factor descriptors, in
|
|
68
|
+
* order, canonically serialized.
|
|
69
|
+
*
|
|
70
|
+
* Order is load-bearing twice over — the KEK concatenates the secrets in this
|
|
71
|
+
* order, and the AAD covers it — so a reordered list fails the tag rather than
|
|
72
|
+
* deriving a different key and failing indistinguishably from a wrong secret.
|
|
73
|
+
*/ function wrappingAAD(descriptors) {
|
|
74
|
+
return fromUTF(canonicalStringify(descriptors));
|
|
75
|
+
}
|
|
76
|
+
/** Binds an entry's ciphertext to the exact key, version and entry it was written for. */ function entryAAD(suiteID, keyID, keyVersion, entryID) {
|
|
77
|
+
return fromUTF(`${suiteID}:${keyID}:${keyVersion}:${entryID}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The ECDH output is raw, not uniformly random. It is safe here only because
|
|
81
|
+
* `deriveKEK` runs every factor secret through HKDF with its own domain
|
|
82
|
+
* separation — a caller consuming this as a key directly would not be.
|
|
83
|
+
*/ async function didFactorSecretForSender(recipientDID, methods) {
|
|
84
|
+
const { sharedSecret, ephemeralPublicKey } = await deriveSharedSecretAsync(recipientDID, {
|
|
85
|
+
methods: methods ?? []
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
secret: sharedSecret,
|
|
89
|
+
params: {
|
|
90
|
+
recipient: recipientDID,
|
|
91
|
+
epk: toB64(ephemeralPublicKey)
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The KDF runs here rather than in the manager.
|
|
97
|
+
*
|
|
98
|
+
* A caller deriving the secret itself would have to read the descriptor's salt
|
|
99
|
+
* and cost, which makes the suite's `passphraseKDF` a property nothing enforces:
|
|
100
|
+
* a later scrypt suite would silently be fed Argon2 output.
|
|
101
|
+
*/ async function secretFactorForSender(runtime, input, cost) {
|
|
102
|
+
const invalid = invalidArgon2Cost(cost);
|
|
103
|
+
if (invalid != null) {
|
|
104
|
+
throw new InvalidCredentialFactors(`invalid Argon2 cost: ${invalid}`);
|
|
105
|
+
}
|
|
106
|
+
const salt = runtime.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
107
|
+
const secret = await argon2idAsync(input, salt, {
|
|
108
|
+
...cost,
|
|
109
|
+
dkLen: FACTOR_SECRET_LENGTH
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
secret,
|
|
113
|
+
params: {
|
|
114
|
+
salt: toB64(salt),
|
|
115
|
+
...cost
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function secretFactorForRecipient(kind, input, params) {
|
|
120
|
+
if (input == null) {
|
|
121
|
+
throw new NoAvailableFactor(kind);
|
|
122
|
+
}
|
|
123
|
+
const { salt, m, t, p } = params;
|
|
124
|
+
if (typeof salt !== 'string' || typeof m !== 'number' || typeof t !== 'number' || typeof p !== 'number') {
|
|
125
|
+
throw new CredentialTampered(`${kind} factor descriptor carries no usable Argon2 parameters`);
|
|
126
|
+
}
|
|
127
|
+
const invalid = invalidArgon2Cost({
|
|
128
|
+
m,
|
|
129
|
+
t,
|
|
130
|
+
p
|
|
131
|
+
});
|
|
132
|
+
if (invalid != null) {
|
|
133
|
+
throw new CredentialTampered(`${kind} factor descriptor has invalid Argon2 cost: ${invalid}`);
|
|
134
|
+
}
|
|
135
|
+
let decodedSalt;
|
|
136
|
+
try {
|
|
137
|
+
decodedSalt = fromB64(salt);
|
|
138
|
+
} catch (cause) {
|
|
139
|
+
throw new CredentialTampered(`${kind} factor descriptor carries an invalid Argon2 salt`, {
|
|
140
|
+
cause
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (decodedSalt.length !== SALT_LENGTH) {
|
|
144
|
+
throw new CredentialTampered(`${kind} factor descriptor Argon2 salt must be ${SALT_LENGTH} bytes`);
|
|
145
|
+
}
|
|
146
|
+
return await argon2idAsync(input, decodedSalt, {
|
|
147
|
+
m,
|
|
148
|
+
t,
|
|
149
|
+
p,
|
|
150
|
+
dkLen: FACTOR_SECRET_LENGTH
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Factor-list rules that hold regardless of suite.
|
|
155
|
+
*
|
|
156
|
+
* A second `passphrase` or `pin` is refused because unwrap inputs are keyed by
|
|
157
|
+
* kind: it could only ever be handed the same secret as the first, making an AND
|
|
158
|
+
* that reads as two factors and is one. Repeated `did` factors are fine and
|
|
159
|
+
* meaningful — each descriptor carries its own recipient, so "Alice AND Bob" is
|
|
160
|
+
* a list of two.
|
|
161
|
+
*/ export function validateFactorList(kinds) {
|
|
162
|
+
// Zero factors is not a weaker AND, it is no AND: `deriveKEK` runs over no
|
|
163
|
+
// secrets at all, so the wrapping opens for anyone holding the row.
|
|
164
|
+
if (kinds.length === 0) {
|
|
165
|
+
throw new InvalidCredentialFactors('a wrapping needs at least one factor');
|
|
166
|
+
}
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
for (const kind of kinds){
|
|
169
|
+
if (kind === 'did') {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (seen.has(kind)) {
|
|
173
|
+
throw new InvalidCredentialFactors(`duplicate "${kind}" factor`);
|
|
174
|
+
}
|
|
175
|
+
seen.add(kind);
|
|
176
|
+
}
|
|
177
|
+
// A pin is confirmation-bearing, not entropy-bearing: four digits are
|
|
178
|
+
// grindable by anyone holding the ciphertext. It is only worth anything
|
|
179
|
+
// chained to a key that device possession supplies, and a passphrase does not
|
|
180
|
+
// substitute — the threat model is the borrowed unlocked device, not the
|
|
181
|
+
// attacker with the database.
|
|
182
|
+
if (seen.has('pin') && !kinds.includes('did')) {
|
|
183
|
+
throw new InvalidCredentialFactors('a "pin" factor requires a "did" factor in the same wrapping');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function didFactorSecretForRecipient(identity, params) {
|
|
187
|
+
if (identity == null) {
|
|
188
|
+
throw new NoAvailableFactor('did');
|
|
189
|
+
}
|
|
190
|
+
const epk = params.epk;
|
|
191
|
+
if (typeof epk !== 'string') {
|
|
192
|
+
throw new CredentialTampered('did factor descriptor carries no epk');
|
|
193
|
+
}
|
|
194
|
+
// No `kid`: the sender always resolves the first published keyAgreement key, and
|
|
195
|
+
// on a multi-KEM identity another one silently recovers different bytes.
|
|
196
|
+
return await identity.agreeKey(fromB64(epk));
|
|
197
|
+
}
|
|
198
|
+
const suite1 = {
|
|
199
|
+
id: SUITE_KUBUN_CRED_1,
|
|
200
|
+
name: 'KUBUN_CRED_1',
|
|
201
|
+
generateContentKey (runtime) {
|
|
202
|
+
return runtime.getRandomValues(new Uint8Array(CONTENT_KEY_LENGTH));
|
|
203
|
+
},
|
|
204
|
+
async wrap (params) {
|
|
205
|
+
validateFactorList(params.factors.map((factor)=>factor.kind));
|
|
206
|
+
const secrets = [];
|
|
207
|
+
const descriptors = [];
|
|
208
|
+
for (const factor of params.factors){
|
|
209
|
+
switch(factor.kind){
|
|
210
|
+
case 'did':
|
|
211
|
+
{
|
|
212
|
+
const { secret, params: factorParams } = await didFactorSecretForSender(factor.recipientDID, params.methods);
|
|
213
|
+
secrets.push(secret);
|
|
214
|
+
descriptors.push({
|
|
215
|
+
kind: 'did',
|
|
216
|
+
params: factorParams
|
|
217
|
+
});
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'passphrase':
|
|
221
|
+
case 'pin':
|
|
222
|
+
{
|
|
223
|
+
const { secret, params: factorParams } = await secretFactorForSender(params.runtime, factor.input, factor.cost ?? DEFAULT_ARGON2_COST);
|
|
224
|
+
secrets.push(secret);
|
|
225
|
+
descriptors.push({
|
|
226
|
+
kind: factor.kind,
|
|
227
|
+
params: factorParams
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const kek = deriveKEK(secrets, params.wrappingID, SUITE_KUBUN_CRED_1, params.keyID, params.keyVersion);
|
|
234
|
+
const iv = params.runtime.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
235
|
+
return {
|
|
236
|
+
descriptors,
|
|
237
|
+
iv,
|
|
238
|
+
wrappedKey: gcm(kek, iv, wrappingAAD(descriptors)).encrypt(params.contentKey)
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
async unwrap (params) {
|
|
242
|
+
// Validated on open too, not only on create: wrappings arrive off the wire
|
|
243
|
+
// (`credential:key-grant`), and a pin-only wrapping is openable by anyone who
|
|
244
|
+
// guesses four digits, a factorless one by anyone at all. A wrapping this
|
|
245
|
+
// build refuses to make is not one it agrees to open.
|
|
246
|
+
validateFactorList(params.descriptors.map((descriptor)=>descriptor.kind));
|
|
247
|
+
const secrets = [];
|
|
248
|
+
const userSuppliedKinds = [];
|
|
249
|
+
for (const descriptor of params.descriptors){
|
|
250
|
+
switch(descriptor.kind){
|
|
251
|
+
case 'did':
|
|
252
|
+
secrets.push(await didFactorSecretForRecipient(params.identity, descriptor.params));
|
|
253
|
+
break;
|
|
254
|
+
case 'passphrase':
|
|
255
|
+
case 'pin':
|
|
256
|
+
secrets.push(await secretFactorForRecipient(descriptor.kind, params.inputs?.[descriptor.kind], descriptor.params));
|
|
257
|
+
userSuppliedKinds.push(descriptor.kind);
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
throw new NoAvailableFactor(descriptor.kind);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const kek = deriveKEK(secrets, params.wrappingID, SUITE_KUBUN_CRED_1, params.keyID, params.keyVersion);
|
|
264
|
+
try {
|
|
265
|
+
return gcm(kek, params.iv, wrappingAAD(params.descriptors)).decrypt(params.wrappedKey);
|
|
266
|
+
} catch (cause) {
|
|
267
|
+
// One tag failure is everything that is observable here, so which error
|
|
268
|
+
// this is amounts to a policy about who could have been wrong. With a
|
|
269
|
+
// user-supplied factor in the list the secret is the likely cause and the
|
|
270
|
+
// caller needs a retry path; with none, nothing the user typed could have
|
|
271
|
+
// been wrong and the remaining causes are all tampering.
|
|
272
|
+
if (userSuppliedKinds.length > 0) {
|
|
273
|
+
throw new CredentialSecretRejected(userSuppliedKinds, {
|
|
274
|
+
cause,
|
|
275
|
+
keyID: params.keyID
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
throw new CredentialTampered('credential wrapping failed authentication: its descriptors or ciphertext were altered, or it belongs to another key', {
|
|
279
|
+
cause
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
encryptEntry (params) {
|
|
284
|
+
const iv = params.runtime.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
285
|
+
const aad = entryAAD(SUITE_KUBUN_CRED_1, params.keyID, params.keyVersion, params.entryID);
|
|
286
|
+
return {
|
|
287
|
+
iv,
|
|
288
|
+
ciphertext: gcm(params.contentKey, iv, aad).encrypt(params.plaintext)
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
decryptEntry (params) {
|
|
292
|
+
const aad = entryAAD(SUITE_KUBUN_CRED_1, params.keyID, params.keyVersion, params.entryID);
|
|
293
|
+
try {
|
|
294
|
+
return gcm(params.contentKey, params.iv, aad).decrypt(params.ciphertext);
|
|
295
|
+
} catch (cause) {
|
|
296
|
+
throw new CredentialTampered('credential entry failed authentication: it was altered, or is being read under the wrong key version', {
|
|
297
|
+
cause
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const SUITES = new Map([
|
|
303
|
+
[
|
|
304
|
+
SUITE_KUBUN_CRED_1,
|
|
305
|
+
suite1
|
|
306
|
+
]
|
|
307
|
+
]);
|
|
308
|
+
/** The suite a key names, or `UnsupportedCredentialSuite` — never a silent skip. */ export function getSuite(id) {
|
|
309
|
+
const suite = SUITES.get(id);
|
|
310
|
+
if (suite == null) {
|
|
311
|
+
throw new UnsupportedCredentialSuite(id);
|
|
312
|
+
}
|
|
313
|
+
return suite;
|
|
314
|
+
}
|
|
315
|
+
export function isSupportedSuite(id) {
|
|
316
|
+
return SUITES.has(id);
|
|
317
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability action a device presents to administer (grant/rotate/revoke/
|
|
3
|
+
* factor-change) a controller-owned credential on its owner's behalf. It rides
|
|
4
|
+
* the same `checkCapability` matcher documents use — an action is matched by
|
|
5
|
+
* segment, so a `credential/*` grant covers it, exactly as `document/*` covers
|
|
6
|
+
* `document/write`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CREDENTIAL_ADMINISTER_ACTION = "credential/administer";
|
|
9
|
+
/**
|
|
10
|
+
* The user-wide capability resource for a controller's credentials, mirroring
|
|
11
|
+
* `urn:kubun:user:<owner>` for documents: one delegation covers all of a
|
|
12
|
+
* controller's credentials rather than naming each key. The DID is normalized so
|
|
13
|
+
* the resource is an equality key — the same reason `owner_did` is normalized and
|
|
14
|
+
* a wrapping's recipient is not.
|
|
15
|
+
*/
|
|
16
|
+
export declare function credentialUserResource(controllerDID: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
/**
|
|
3
|
+
* The capability action a device presents to administer (grant/rotate/revoke/
|
|
4
|
+
* factor-change) a controller-owned credential on its owner's behalf. It rides
|
|
5
|
+
* the same `checkCapability` matcher documents use — an action is matched by
|
|
6
|
+
* segment, so a `credential/*` grant covers it, exactly as `document/*` covers
|
|
7
|
+
* `document/write`.
|
|
8
|
+
*/ export const CREDENTIAL_ADMINISTER_ACTION = 'credential/administer';
|
|
9
|
+
/**
|
|
10
|
+
* The user-wide capability resource for a controller's credentials, mirroring
|
|
11
|
+
* `urn:kubun:user:<owner>` for documents: one delegation covers all of a
|
|
12
|
+
* controller's credentials rather than naming each key. The DID is normalized so
|
|
13
|
+
* the resource is an equality key — the same reason `owner_did` is normalized and
|
|
14
|
+
* a wrapping's recipient is not.
|
|
15
|
+
*/ export function credentialUserResource(controllerDID) {
|
|
16
|
+
return `urn:kubun:credential:user:${normalizeDID(controllerDID)}`;
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kubun/credential",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"keywords": [],
|
|
5
|
+
"license": "see LICENSE.md",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"types": "lib/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"lib/*",
|
|
15
|
+
"LICENSE.md"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@kokuin/jwe": "^0.1.0",
|
|
19
|
+
"@kokuin/token": "^0.5.0",
|
|
20
|
+
"@noble/ciphers": "^2.3.0",
|
|
21
|
+
"@noble/hashes": "^2.3.0",
|
|
22
|
+
"@sozai/codec": "^0.4.0",
|
|
23
|
+
"@sozai/runtime": "^0.1.0",
|
|
24
|
+
"@kubun/hlc": "^0.13.0",
|
|
25
|
+
"@kubun/store-credential": "^0.13.0",
|
|
26
|
+
"@kubun/id": "^0.13.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@kubun/db-better-sqlite": "^0.13.0",
|
|
30
|
+
"@kubun/db": "^0.13.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
37
|
+
"build:clean": "del lib",
|
|
38
|
+
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
39
|
+
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
40
|
+
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
41
|
+
"test": "pnpm run test:types && pnpm run test:unit",
|
|
42
|
+
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
43
|
+
"test:unit": "vitest run"
|
|
44
|
+
}
|
|
45
|
+
}
|