toiljs 0.0.51 → 0.0.53
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 +19 -0
- package/TYPESCRIPT_LAW.md +12601 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +16 -1
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.d.ts +9 -20
- package/build/client/auth.js +112 -95
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/crypto.js +33 -0
- package/build/devserver/host.js +2 -0
- package/build/devserver/kv.d.ts +3 -0
- package/build/devserver/kv.js +53 -0
- package/build/devserver/module.js +2 -1
- package/docs/auth-todo.md +149 -0
- package/docs/auth.md +234 -173
- package/examples/basic/client/routes/pq.tsx +72 -103
- package/examples/basic/server/core/AppHandler.ts +24 -3
- package/examples/basic/server/main.ts +0 -1
- package/examples/basic/server/routes/Auth.ts +304 -99
- package/examples/basic/server/routes/Session.ts +5 -2
- package/package.json +2 -1
- package/server/globals/auth.ts +263 -10
- package/src/cli/diagnostics.ts +22 -0
- package/src/cli/doctor.ts +2 -0
- package/src/client/auth.ts +192 -174
- package/src/client/index.ts +2 -2
- package/src/compiler/generate.ts +1 -1
- package/src/devserver/crypto.ts +54 -0
- package/src/devserver/host.ts +6 -0
- package/src/devserver/kv.ts +93 -0
- package/src/devserver/module.ts +4 -1
- package/test/devserver-pqauth.test.ts +153 -0
- package/test/doctor.test.ts +22 -0
- package/test/pqauth-e2e.test.ts +207 -0
- package/examples/basic/server/routes/PqDemo.ts +0 -127
package/server/globals/auth.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// and the toiljs dev-server mock).
|
|
11
11
|
|
|
12
12
|
import { DataWriter, DataReader } from 'data';
|
|
13
|
+
import { HmacImportParams, HmacParams, ALG_SHA_256, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
Server,
|
|
@@ -37,6 +38,38 @@ declare function __toilMldsaVerify(
|
|
|
37
38
|
ctxLen: i32,
|
|
38
39
|
): i32;
|
|
39
40
|
|
|
41
|
+
// Host import: ML-KEM-768 (FIPS 203) decapsulation. Recovers the 32-byte shared
|
|
42
|
+
// secret from the client's ciphertext using the server static secret key,
|
|
43
|
+
// written to `outPtr`. Returns 0 on success, negative on error. The secret key
|
|
44
|
+
// is the server's own identity key (NOT password-derived), configured at
|
|
45
|
+
// startup and passed per call; the host never stores it.
|
|
46
|
+
// @ts-ignore: decorator
|
|
47
|
+
@external('env', 'crypto.mlkem_decapsulate')
|
|
48
|
+
declare function __toilMlkemDecapsulate(
|
|
49
|
+
ctPtr: usize,
|
|
50
|
+
ctLen: i32,
|
|
51
|
+
skPtr: usize,
|
|
52
|
+
skLen: i32,
|
|
53
|
+
outPtr: usize,
|
|
54
|
+
): i32;
|
|
55
|
+
|
|
56
|
+
// Host import: RFC 9497 OPRF (mode 0x00, ristretto255-SHA512) server evaluation.
|
|
57
|
+
// Derives the per-user key from (seed, info=username) and blind-evaluates the
|
|
58
|
+
// client's blinded element, writing the 32-byte evaluated element to `outPtr`.
|
|
59
|
+
// Returns 0 on success, negative on error. `seed` is the server's secret OPRF
|
|
60
|
+
// master seed, configured at startup and passed per call.
|
|
61
|
+
// @ts-ignore: decorator
|
|
62
|
+
@external('env', 'crypto.voprf_evaluate')
|
|
63
|
+
declare function __toilVoprfEvaluate(
|
|
64
|
+
seedPtr: usize,
|
|
65
|
+
seedLen: i32,
|
|
66
|
+
infoPtr: usize,
|
|
67
|
+
infoLen: i32,
|
|
68
|
+
blindedPtr: usize,
|
|
69
|
+
blindedLen: i32,
|
|
70
|
+
outPtr: usize,
|
|
71
|
+
): i32;
|
|
72
|
+
|
|
40
73
|
// HMAC key for signing session cookies. The SAME secret must be configured on
|
|
41
74
|
// every edge instance (a sealed cookie minted by one is opened by another) and
|
|
42
75
|
// must NEVER reach the client. There is no host-config secret mechanism yet, so
|
|
@@ -49,6 +82,51 @@ let __sessionSecret: Uint8Array = Uint8Array.wrap(
|
|
|
49
82
|
String.UTF8.encode('toil-dev-insecure-session-secret-CHANGE-ME'),
|
|
50
83
|
);
|
|
51
84
|
|
|
85
|
+
// OPRF master seed (RFC 9497 DeriveKeyPair input). Per-user OPRF keys are
|
|
86
|
+
// derived from this + the username, so it is a server secret of the same
|
|
87
|
+
// sensitivity as a password-hash pepper: a leak enables an offline dictionary
|
|
88
|
+
// attack (but precomputation stays impossible until it leaks). Configured at
|
|
89
|
+
// startup via `AuthService.setOprfSeed`; the DEV default below is well-known.
|
|
90
|
+
// 32 bytes (RFC 9497 Ns for ristretto255); `setOprfSeed` MUST also pass 32.
|
|
91
|
+
let __oprfSeed: Uint8Array = Uint8Array.wrap(
|
|
92
|
+
String.UTF8.encode('toil-dev-oprf-seedXXCHANGE-ME-32'),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Server static ML-KEM-768 secret (decapsulation) key. The matching public key
|
|
96
|
+
// is PINNED in the client; only the holder of this key can decapsulate, so a
|
|
97
|
+
// correct shared secret authenticates the server. Configured at startup via
|
|
98
|
+
// `AuthService.setServerKemSecretKey` (2400 bytes). Empty until set; mutual-auth
|
|
99
|
+
// calls fail closed if unset.
|
|
100
|
+
let __serverKemSk: Uint8Array = new Uint8Array(0);
|
|
101
|
+
|
|
102
|
+
// Server static ML-KEM-768 PUBLIC (encapsulation) key, used only to compute the
|
|
103
|
+
// key identity bound into the login transcript (`serverKemKeyId`). The client
|
|
104
|
+
// pins the same key. Configured at startup via `setServerKemPublicKey`.
|
|
105
|
+
let __serverKemPk: Uint8Array = new Uint8Array(0);
|
|
106
|
+
|
|
107
|
+
// HMAC-SHA256(key, msg) via the ambient Web Crypto (same path SecureCookies
|
|
108
|
+
// uses). The session-key derivation and the mutual-auth confirmation tag are
|
|
109
|
+
// both keyed PRFs over the transcript; the client mirrors this with hash-wasm.
|
|
110
|
+
function __hmacSha256(key: Uint8Array, msg: Uint8Array): Uint8Array {
|
|
111
|
+
const k = crypto.subtle.importKey(
|
|
112
|
+
'raw',
|
|
113
|
+
key,
|
|
114
|
+
new HmacImportParams(ALG_SHA_256),
|
|
115
|
+
false,
|
|
116
|
+
USAGE_SIGN | USAGE_VERIFY,
|
|
117
|
+
);
|
|
118
|
+
return crypto.subtle.sign(new HmacParams(), k, msg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `utf8(label) || transcriptHash` -- the HMAC message body for the derivations.
|
|
122
|
+
function __labelled(label: string, transcriptHash: Uint8Array): Uint8Array {
|
|
123
|
+
const lb = Uint8Array.wrap(String.UTF8.encode(label));
|
|
124
|
+
const buf = new Uint8Array(lb.length + transcriptHash.length);
|
|
125
|
+
buf.set(lb, 0);
|
|
126
|
+
buf.set(transcriptHash, lb.length);
|
|
127
|
+
return buf;
|
|
128
|
+
}
|
|
129
|
+
|
|
52
130
|
// Whether the current request arrived over HTTPS. A TLS edge / proxy signals it
|
|
53
131
|
// with `x-forwarded-proto: https`; absent (plain HTTP, including `toiljs dev`)
|
|
54
132
|
// the session uses plain cookies so they actually round-trip in the browser.
|
|
@@ -224,17 +302,23 @@ export namespace AuthService {
|
|
|
224
302
|
|
|
225
303
|
/**
|
|
226
304
|
* Build the canonical login message `M` the client signs and the server
|
|
227
|
-
* verifies
|
|
228
|
-
* with its OWN stored values, never
|
|
229
|
-
*
|
|
305
|
+
* verifies. ONE fixed binary layout, no JSON and no version negotiation. The
|
|
306
|
+
* server MUST call this with its OWN stored values, never fields echoed by
|
|
307
|
+
* the client. Both ends produce byte-identical bytes via `DataWriter`:
|
|
230
308
|
*
|
|
231
|
-
* u8
|
|
232
|
-
* str
|
|
233
|
-
* str
|
|
234
|
-
* bytes cid
|
|
235
|
-
* bytes nonce
|
|
236
|
-
* u64
|
|
237
|
-
* u64
|
|
309
|
+
* u8 tag = 1 (format marker, not a compat switch)
|
|
310
|
+
* str sub (username)
|
|
311
|
+
* str aud (this service's audience; server-config constant)
|
|
312
|
+
* bytes cid (challenge id)
|
|
313
|
+
* bytes nonce (32 random bytes)
|
|
314
|
+
* u64 iat
|
|
315
|
+
* u64 exp
|
|
316
|
+
* bytes ct (ML-KEM ciphertext; binds the key encapsulation)
|
|
317
|
+
* u32 memKiB (Argon2id params, bound so a MITM cannot slip a
|
|
318
|
+
* u32 iterations downgrade past the signature)
|
|
319
|
+
* u32 parallelism
|
|
320
|
+
* bytes serverKemKeyId (SHA-256 of the server KEM public key; binds the
|
|
321
|
+
* server identity the client encapsulated to)
|
|
238
322
|
*/
|
|
239
323
|
export function buildLoginMessage(
|
|
240
324
|
sub: string,
|
|
@@ -243,6 +327,11 @@ export namespace AuthService {
|
|
|
243
327
|
nonce: Uint8Array,
|
|
244
328
|
iat: u64,
|
|
245
329
|
exp: u64,
|
|
330
|
+
ciphertext: Uint8Array,
|
|
331
|
+
memKiB: u32,
|
|
332
|
+
iterations: u32,
|
|
333
|
+
parallelism: u32,
|
|
334
|
+
serverKemKeyId: Uint8Array,
|
|
246
335
|
): Uint8Array {
|
|
247
336
|
const w = new DataWriter();
|
|
248
337
|
w.writeU8(1);
|
|
@@ -252,6 +341,11 @@ export namespace AuthService {
|
|
|
252
341
|
w.writeBytes(nonce);
|
|
253
342
|
w.writeU64(iat);
|
|
254
343
|
w.writeU64(exp);
|
|
344
|
+
w.writeBytes(ciphertext);
|
|
345
|
+
w.writeU32(memKiB);
|
|
346
|
+
w.writeU32(iterations);
|
|
347
|
+
w.writeU32(parallelism);
|
|
348
|
+
w.writeBytes(serverKemKeyId);
|
|
255
349
|
return w.toBytes();
|
|
256
350
|
}
|
|
257
351
|
|
|
@@ -278,4 +372,163 @@ export namespace AuthService {
|
|
|
278
372
|
);
|
|
279
373
|
return result == 1;
|
|
280
374
|
}
|
|
375
|
+
|
|
376
|
+
/** ML-KEM-768 (FIPS 203) sizes. */
|
|
377
|
+
export const KEM_CIPHERTEXT_LEN: i32 = 1088;
|
|
378
|
+
export const KEM_SECRET_KEY_LEN: i32 = 2400;
|
|
379
|
+
export const KEM_PUBLIC_KEY_LEN: i32 = 1184;
|
|
380
|
+
export const SHARED_SECRET_LEN: i32 = 32;
|
|
381
|
+
/** Serialized ristretto255 OPRF element (blinded / evaluated). */
|
|
382
|
+
export const OPRF_ELEMENT_LEN: i32 = 32;
|
|
383
|
+
/** RFC 9497 DeriveKeyPair seed length (ristretto255 `Ns`). */
|
|
384
|
+
export const OPRF_SEED_LEN: i32 = 32;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Configure the OPRF master seed (32 bytes). Per-user OPRF keys are derived
|
|
388
|
+
* from this + the username. Call once at startup from `main.ts`; identical
|
|
389
|
+
* on every instance, kept out of any client bundle.
|
|
390
|
+
*/
|
|
391
|
+
export function setOprfSeed(seed: Uint8Array): void {
|
|
392
|
+
__oprfSeed = seed;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Configure the server static ML-KEM-768 secret (decapsulation) key (2400
|
|
397
|
+
* bytes). The matching public key is pinned in the client. Call once at
|
|
398
|
+
* startup; identical on every instance, never in a client bundle.
|
|
399
|
+
*/
|
|
400
|
+
export function setServerKemSecretKey(secretKey: Uint8Array): void {
|
|
401
|
+
__serverKemSk = secretKey;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Configure the server static ML-KEM-768 PUBLIC key (1184 bytes), used to
|
|
406
|
+
* compute {@link serverKemKeyId}. Must be the key the client pins. (It is the
|
|
407
|
+
* `ek` embedded in the decapsulation key, so a tenant can pass
|
|
408
|
+
* `secretKey.slice(1152, 2336)` rather than store it twice.)
|
|
409
|
+
*/
|
|
410
|
+
export function setServerKemPublicKey(publicKey: Uint8Array): void {
|
|
411
|
+
__serverKemPk = publicKey;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* OPRF server step: blind-evaluate the client's `blinded` element under the
|
|
416
|
+
* per-user key derived from the master seed + `username`. Returns the 32-byte
|
|
417
|
+
* evaluated element, or an empty array on any failure. The client unblinds +
|
|
418
|
+
* finalizes locally and feeds the result into Argon2id (the keyed salt).
|
|
419
|
+
*/
|
|
420
|
+
export function oprfEvaluate(username: string, blinded: Uint8Array): Uint8Array {
|
|
421
|
+
if (blinded.length != OPRF_ELEMENT_LEN) return new Uint8Array(0);
|
|
422
|
+
const info = Uint8Array.wrap(String.UTF8.encode(username));
|
|
423
|
+
const out = new Uint8Array(OPRF_ELEMENT_LEN);
|
|
424
|
+
const rc = __toilVoprfEvaluate(
|
|
425
|
+
__oprfSeed.dataStart,
|
|
426
|
+
__oprfSeed.length,
|
|
427
|
+
info.dataStart,
|
|
428
|
+
info.length,
|
|
429
|
+
blinded.dataStart,
|
|
430
|
+
blinded.length,
|
|
431
|
+
out.dataStart,
|
|
432
|
+
);
|
|
433
|
+
return rc == 0 ? out : new Uint8Array(0);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Decapsulate the client's ML-KEM ciphertext with the server static secret
|
|
438
|
+
* key, returning the 32-byte shared secret (empty on failure / unset key).
|
|
439
|
+
* Only the genuine server can produce this, so it underpins mutual auth.
|
|
440
|
+
*/
|
|
441
|
+
export function mlkemDecapsulate(ciphertext: Uint8Array): Uint8Array {
|
|
442
|
+
if (__serverKemSk.length != KEM_SECRET_KEY_LEN || ciphertext.length != KEM_CIPHERTEXT_LEN) {
|
|
443
|
+
return new Uint8Array(0);
|
|
444
|
+
}
|
|
445
|
+
const out = new Uint8Array(SHARED_SECRET_LEN);
|
|
446
|
+
const rc = __toilMlkemDecapsulate(
|
|
447
|
+
ciphertext.dataStart,
|
|
448
|
+
ciphertext.length,
|
|
449
|
+
__serverKemSk.dataStart,
|
|
450
|
+
__serverKemSk.length,
|
|
451
|
+
out.dataStart,
|
|
452
|
+
);
|
|
453
|
+
return rc == 0 ? out : new Uint8Array(0);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** SHA-256 over `data` (ambient Web Crypto), for transcript/confirm hashing. */
|
|
457
|
+
export function sha256(data: Uint8Array): Uint8Array {
|
|
458
|
+
return crypto.subtle.digest('SHA-256', data);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** `SHA-256(serverKemPublicKey)` -- the key identity bound into the login
|
|
462
|
+
* message, so the signature commits to which server key the client
|
|
463
|
+
* encapsulated to. The client computes the same hash over its pinned key. */
|
|
464
|
+
export function serverKemKeyId(): Uint8Array {
|
|
465
|
+
return sha256(__serverKemPk);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Domain separators for the session-key derivation and confirmation tag. */
|
|
469
|
+
export const SESSION_KEY_LABEL: string = 'toil-session-key-v1';
|
|
470
|
+
export const SERVER_CONFIRM_LABEL: string = 'toil-server-confirm-v1';
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Derive the authenticated session key `K` from the ML-KEM shared secret,
|
|
474
|
+
* bound to the transcript: `K = HMAC-SHA256(sharedSecret, SESSION_KEY_LABEL ||
|
|
475
|
+
* transcriptHash)`. The shared secret is already a uniform 32-byte key, so it
|
|
476
|
+
* keys the HMAC directly (an HKDF-Expand step). Both ends derive the same `K`
|
|
477
|
+
* iff the KEM exchange and transcript match.
|
|
478
|
+
*
|
|
479
|
+
* NOTE: `K` is the handle for future channel binding. Binding the *session
|
|
480
|
+
* cookie* to the transport (so a stolen cookie is useless on another channel)
|
|
481
|
+
* needs the TLS exporter, which the wasm guest cannot see -- that is an
|
|
482
|
+
* edge/transport follow-up, not doable purely here.
|
|
483
|
+
*/
|
|
484
|
+
export function deriveSessionKey(sharedSecret: Uint8Array, transcriptHash: Uint8Array): Uint8Array {
|
|
485
|
+
return __hmacSha256(sharedSecret, __labelled(SESSION_KEY_LABEL, transcriptHash));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The server's mutual-auth confirmation tag: `HMAC-SHA256(K, SERVER_CONFIRM_LABEL
|
|
490
|
+
* || transcriptHash)`, where `K` is {@link deriveSessionKey}. Only a server
|
|
491
|
+
* that decapsulated correctly (i.e. holds the KEM secret key) derives the same
|
|
492
|
+
* `K`, so the client verifying this tag proves the server's identity.
|
|
493
|
+
*/
|
|
494
|
+
export function serverConfirmTag(sessionKey: Uint8Array, transcriptHash: Uint8Array): Uint8Array {
|
|
495
|
+
return __hmacSha256(sessionKey, __labelled(SERVER_CONFIRM_LABEL, transcriptHash));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Registration proof-of-possession context (binds a signature to "register"
|
|
499
|
+
* so it can never validate as a login). Byte-identical to the client. */
|
|
500
|
+
export const REGISTER_CONTEXT: string = 'qauth:register:v1';
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* The registration PoP message: `u8(1) str(username) bytes(publicKey)`,
|
|
504
|
+
* signed by the client under {@link REGISTER_CONTEXT}. Verifying it proves
|
|
505
|
+
* the registrant holds the secret key for the public key it is registering.
|
|
506
|
+
*/
|
|
507
|
+
export function buildRegisterMessage(username: string, publicKey: Uint8Array): Uint8Array {
|
|
508
|
+
const w = new DataWriter();
|
|
509
|
+
w.writeU8(1);
|
|
510
|
+
w.writeString(username);
|
|
511
|
+
w.writeBytes(publicKey);
|
|
512
|
+
return w.toBytes();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Verify a registration proof-of-possession over `message` against the
|
|
516
|
+
* submitted `publicKey`, under {@link REGISTER_CONTEXT}. */
|
|
517
|
+
export function verifyRegister(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): bool {
|
|
518
|
+
if (publicKey.length != PUBLIC_KEY_LEN || signature.length != SIGNATURE_LEN) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
const ctx = Uint8Array.wrap(String.UTF8.encode(REGISTER_CONTEXT));
|
|
522
|
+
const result = __toilMldsaVerify(
|
|
523
|
+
publicKey.dataStart,
|
|
524
|
+
publicKey.length,
|
|
525
|
+
message.dataStart,
|
|
526
|
+
message.length,
|
|
527
|
+
signature.dataStart,
|
|
528
|
+
signature.length,
|
|
529
|
+
ctx.dataStart,
|
|
530
|
+
ctx.length,
|
|
531
|
+
);
|
|
532
|
+
return result == 1;
|
|
533
|
+
}
|
|
281
534
|
}
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -96,6 +96,28 @@ export function checkPackageManager(lockfiles: readonly string[]): Check {
|
|
|
96
96
|
return { id: 'pm', label: 'Package manager', status: 'pass', detail: lockfiles.join(', ') };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Flags `npx toiljs ...` inside package.json scripts. Under `npm run`, `node_modules/.bin` is
|
|
101
|
+
* already on PATH, so the `npx` is redundant; worse, the extra `npx` process puts the console in
|
|
102
|
+
* raw / VT-input mode and does not restore it when the run is Ctrl+C'd, leaving the terminal
|
|
103
|
+
* echoing arrow keys as `^[[A` and mis-reading typed input (Windows cmd especially). The scaffold
|
|
104
|
+
* uses a bare `toiljs <cmd>`; this catches projects whose scripts wrap it in `npx`.
|
|
105
|
+
*/
|
|
106
|
+
export function checkDevScripts(scripts: Record<string, string>): Check {
|
|
107
|
+
const NPX_TOILJS = /(?:^|[\s&|;(])npx\s+toiljs\b/;
|
|
108
|
+
const offenders = Object.keys(scripts).filter((name) => NPX_TOILJS.test(scripts[name] ?? ''));
|
|
109
|
+
if (offenders.length === 0) {
|
|
110
|
+
return { id: 'scripts-npx', label: 'Scripts', status: 'pass' };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: 'scripts-npx',
|
|
114
|
+
label: 'Scripts',
|
|
115
|
+
status: 'warn',
|
|
116
|
+
detail: `${offenders.join(', ')} run via "npx toiljs"`,
|
|
117
|
+
fix: 'Drop npx: use "toiljs <cmd>" (npm run already puts node_modules/.bin on PATH). The npx layer can leave the terminal in raw mode after Ctrl+C, which garbles the shell.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
export function checkToiljsInstalled(version: string | null): Check {
|
|
100
122
|
return version
|
|
101
123
|
? { id: 'toiljs', label: 'toiljs', status: 'pass', detail: version }
|
package/src/cli/doctor.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type Check,
|
|
17
17
|
checkBasePath,
|
|
18
18
|
checkConfigLoads,
|
|
19
|
+
checkDevScripts,
|
|
19
20
|
checkDir,
|
|
20
21
|
checkDuplicatePatterns,
|
|
21
22
|
type CheckGroup,
|
|
@@ -670,6 +671,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
670
671
|
checkRoutesPresent(routes.length),
|
|
671
672
|
checkDuplicatePatterns(mainPatterns),
|
|
672
673
|
checkRelativeAssets(assetIssues),
|
|
674
|
+
checkDevScripts(projectPkg ? stringRecord(projectPkg.scripts) : {}),
|
|
673
675
|
],
|
|
674
676
|
},
|
|
675
677
|
{
|