toiljs 0.0.99 → 0.0.101

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.
@@ -97,10 +97,10 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
97
97
  it(
98
98
  'registers then logs in (full OPRF + ML-DSA + ML-KEM mutual-auth chain)',
99
99
  async () => {
100
- await Auth.register('ada', 'correct horse battery staple', 'ada@example.com');
100
+ await Auth.register('ada', 'correct horse battery stapleA1', 'ada@example.com');
101
101
  // login resolves ONLY if the server's mutual-auth confirmation tag
102
102
  // verified against the client's own shared secret.
103
- const session = await Auth.login('ada', 'correct horse battery staple');
103
+ const session = await Auth.login('ada', 'correct horse battery stapleA1');
104
104
  expect(session.length).toBeGreaterThan(0);
105
105
 
106
106
  // Regression guard: the freshly minted session cookie (captured by the
@@ -129,7 +129,7 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
129
129
  it(
130
130
  'rejects a wrong password at login',
131
131
  async () => {
132
- await Auth.register('bob', 'hunter2-correct', 'bob@example.com');
132
+ await Auth.register('bob', 'hunter2-correctA1', 'bob@example.com');
133
133
  await expect(Auth.login('bob', 'hunter2-WRONG')).rejects.toThrow(/login failed|request failed/);
134
134
  },
135
135
  60_000,
@@ -146,12 +146,12 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
146
146
  it(
147
147
  'rejects a duplicate registration with a clear (not generic) message',
148
148
  async () => {
149
- await Auth.register('dupe', 'correct horse battery staple', 'dupe@example.com');
149
+ await Auth.register('dupe', 'correct horse battery stapleA1', 'dupe@example.com');
150
150
  // Second registration of the same username must say so explicitly,
151
151
  // not return the generic "request failed". (The username-taken check fires
152
152
  // before the email check, so it stays `already registered` even with the same email.)
153
153
  await expect(
154
- Auth.register('dupe', 'correct horse battery staple', 'dupe@example.com'),
154
+ Auth.register('dupe', 'correct horse battery stapleA1', 'dupe@example.com'),
155
155
  ).rejects.toThrow(/already registered/);
156
156
  },
157
157
  60_000,
@@ -27,9 +27,14 @@ import { __sentEmails, __clearSentEmails } from '../src/devserver/email/index.js
27
27
  import {
28
28
  Auth,
29
29
  AuthErrorCode,
30
+ checkPassword,
30
31
  EmailInUseError,
31
32
  EmailNotConfirmedError,
33
+ InvalidEmailError,
34
+ InvalidUsernameError,
35
+ isValidEmail,
32
36
  TwoFactorRequiredError,
37
+ WeakPasswordError,
33
38
  } from '../src/client/auth.js';
34
39
  import { DataReader, DataWriter } from '../src/io/codec.js';
35
40
 
@@ -171,8 +176,8 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
171
176
  it(
172
177
  'confirmation OFF (default): register auto-confirms, login succeeds, /auth/me returns the user',
173
178
  async () => {
174
- await Auth.register('ada', 'correct horse battery staple', 'ada@example.com');
175
- const session = await Auth.login('ada', 'correct horse battery staple');
179
+ await Auth.register('ada', 'correct horse battery stapleA1', 'ada@example.com');
180
+ const session = await Auth.login('ada', 'correct horse battery stapleA1');
176
181
  expect(session.length).toBeGreaterThan(0);
177
182
 
178
183
  // The controller's `@auth`-gated /auth/me returns bytes(toilUserId) str(username).
@@ -195,7 +200,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
195
200
  async () => {
196
201
  // grace lives on the victim tenant (realm = normalized Host "victim.com").
197
202
  setHost('victim.com');
198
- await Auth.register('grace', 'pw-grace-correct', 'grace@x.com');
203
+ await Auth.register('grace', 'pw-grace-correctA1', 'grace@x.com');
199
204
  __clearSentEmails();
200
205
  // Attacker triggers reset/request with a Host that routes to the victim
201
206
  // (the edge strip_port -> "victim.com", which is grace's realm, so the
@@ -224,8 +229,8 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
224
229
  it(
225
230
  'a session minted for one tenant does NOT verify for another (#2 cross-tenant session)',
226
231
  async () => {
227
- await Auth.register('heidi', 'pw-heidi-correct', 'heidi@x.com');
228
- await Auth.login('heidi', 'pw-heidi-correct'); // minted under the shim Host localhost:3000
232
+ await Auth.register('heidi', 'pw-heidi-correctA1', 'heidi@x.com');
233
+ await Auth.login('heidi', 'pw-heidi-correctA1'); // minted under the shim Host localhost:3000
229
234
  const sess = jar.get('toil_sess'); // dev is plain HTTP -> unprefixed cookie
230
235
  expect(sess).toBeTruthy();
231
236
  const meHeaders = (host: string): [string, string][] => [
@@ -256,7 +261,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
256
261
  it(
257
262
  'token bound: a new reset request invalidates the previous token (#5c growth bound)',
258
263
  async () => {
259
- await Auth.register('ivan', 'pw-ivan-correct', 'ivan@x.com');
264
+ await Auth.register('ivan', 'pw-ivan-correctA1', 'ivan@x.com');
260
265
  await Auth.requestPasswordReset('ivan@x.com');
261
266
  const token1 = tokenFromEmail('reset', 'ivan@x.com');
262
267
  __clearSentEmails();
@@ -264,10 +269,10 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
264
269
  const token2 = tokenFromEmail('reset', 'ivan@x.com');
265
270
  expect(token2).not.toBe(token1);
266
271
  // The superseded token no longer works...
267
- await expect(Auth.resetPassword(token1, 'new-pw-unused')).rejects.toThrow();
272
+ await expect(Auth.resetPassword(token1, 'new-pw-unusedA1')).rejects.toThrow();
268
273
  // ...only the latest does.
269
- await Auth.resetPassword(token2, 'new-pw-ivan');
270
- const session = await Auth.login('ivan', 'new-pw-ivan');
274
+ await Auth.resetPassword(token2, 'new-pw-ivanA1');
275
+ const session = await Auth.login('ivan', 'new-pw-ivanA1');
271
276
  expect(session.length).toBeGreaterThan(0);
272
277
  },
273
278
  60_000,
@@ -277,17 +282,17 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
277
282
  'confirmation ON: login is refused until the emailed token confirms the account',
278
283
  async () => {
279
284
  setConfirmation(true);
280
- await Auth.register('bob', 'hunter2-correct', 'bob@example.com');
285
+ await Auth.register('bob', 'hunter2-correctA1', 'bob@example.com');
281
286
 
282
287
  // A valid credential is not enough: the domain requires a confirmed email.
283
- await expect(Auth.login('bob', 'hunter2-correct')).rejects.toThrow(EmailNotConfirmedError);
288
+ await expect(Auth.login('bob', 'hunter2-correctA1')).rejects.toThrow(EmailNotConfirmedError);
284
289
 
285
290
  // Read the confirm link out of the captured email and confirm.
286
291
  const token = tokenFromEmail('confirm', 'bob@example.com');
287
292
  await Auth.confirmEmail(token);
288
293
 
289
294
  // Now login succeeds.
290
- const session = await Auth.login('bob', 'hunter2-correct');
295
+ const session = await Auth.login('bob', 'hunter2-correctA1');
291
296
  expect(session.length).toBeGreaterThan(0);
292
297
  },
293
298
  60_000,
@@ -296,10 +301,10 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
296
301
  it(
297
302
  'duplicate email is rejected at registration (distinct "email already in use")',
298
303
  async () => {
299
- await Auth.register('carol', 'carol-pw-strong', 'dupe@x.com');
304
+ await Auth.register('carol', 'carol-pw-strongA1', 'dupe@x.com');
300
305
  // Typed error: a distinct EmailInUseError carrying the stable code, so a UI can
301
306
  // branch reliably (never on the message/name string).
302
- const err = await Auth.register('dave', 'dave-pw-strong', 'dupe@x.com').catch(
307
+ const err = await Auth.register('dave', 'dave-pw-strongA1', 'dupe@x.com').catch(
303
308
  (e: unknown) => e,
304
309
  );
305
310
  expect(err).toBeInstanceOf(EmailInUseError);
@@ -311,18 +316,18 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
311
316
  it(
312
317
  'password reset round trip: old password stops working, the new one logs in',
313
318
  async () => {
314
- await Auth.register('erin', 'oldpw-erin-strong', 'erin@x.com');
319
+ await Auth.register('erin', 'oldpw-erin-strongA1', 'erin@x.com');
315
320
  // Sanity: confirmation is off, so she can log in before the reset.
316
- expect((await Auth.login('erin', 'oldpw-erin-strong')).length).toBeGreaterThan(0);
321
+ expect((await Auth.login('erin', 'oldpw-erin-strongA1')).length).toBeGreaterThan(0);
317
322
 
318
323
  await Auth.requestPasswordReset('erin@x.com');
319
324
  const token = tokenFromEmail('reset', 'erin@x.com');
320
- await Auth.resetPassword(token, 'newpw-erin-strong');
325
+ await Auth.resetPassword(token, 'newpw-erin-strongA1');
321
326
 
322
- await expect(Auth.login('erin', 'oldpw-erin-strong')).rejects.toThrow(
327
+ await expect(Auth.login('erin', 'oldpw-erin-strongA1')).rejects.toThrow(
323
328
  /login failed|request failed/,
324
329
  );
325
- expect((await Auth.login('erin', 'newpw-erin-strong')).length).toBeGreaterThan(0);
330
+ expect((await Auth.login('erin', 'newpw-erin-strongA1')).length).toBeGreaterThan(0);
326
331
  },
327
332
  60_000,
328
333
  );
@@ -341,10 +346,10 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
341
346
  it(
342
347
  'a reset token is one-time: replaying /auth/reset/finish with the consumed token fails',
343
348
  async () => {
344
- await Auth.register('frank', 'oldpw-frank-strong', 'frank@x.com');
349
+ await Auth.register('frank', 'oldpw-frank-strongA1', 'frank@x.com');
345
350
  await Auth.requestPasswordReset('frank@x.com');
346
351
  const tokenHex = tokenFromEmail('reset', 'frank@x.com');
347
- await Auth.resetPassword(tokenHex, 'newpw-frank-strong'); // consumes the token
352
+ await Auth.resetPassword(tokenHex, 'newpw-frank-strongA1'); // consumes the token
348
353
 
349
354
  // Replay reset/finish with a valid-length pk/proof so the controller reaches the
350
355
  // token-consume step, which now finds the token already deleted -> generic fail.
@@ -372,8 +377,8 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
372
377
  it(
373
378
  '(a) full email-2FA login round-trip: enable, login requires a code, verify mints the session',
374
379
  async () => {
375
- await Auth.register('dave', 'dave-pw-strong', 'dave@x.com');
376
- await Auth.login('dave', 'dave-pw-strong'); // 2FA off -> session
380
+ await Auth.register('dave', 'dave-pw-strongA1', 'dave@x.com');
381
+ await Auth.login('dave', 'dave-pw-strongA1'); // 2FA off -> session
377
382
 
378
383
  // Enable email 2FA: setup delivers a code, confirm switches the method on.
379
384
  await Auth.setupTwoFactor(Auth.TwoFactorMethod.Email);
@@ -387,7 +392,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
387
392
  // verifies serverConfirm before throwing), but NO session is minted.
388
393
  let err: unknown;
389
394
  try {
390
- await Auth.login('dave', 'dave-pw-strong');
395
+ await Auth.login('dave', 'dave-pw-strongA1');
391
396
  } catch (e) {
392
397
  err = e;
393
398
  }
@@ -413,8 +418,8 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
413
418
  it(
414
419
  '(b) 2FA code security: wrong code rejected + attempt-limited to death, correct code single-use',
415
420
  async () => {
416
- await Auth.register('eve', 'eve-pw-strong', 'eve@x.com');
417
- await Auth.login('eve', 'eve-pw-strong');
421
+ await Auth.register('eve', 'eve-pw-strongA1', 'eve@x.com');
422
+ await Auth.login('eve', 'eve-pw-strongA1');
418
423
  await Auth.setupTwoFactor(Auth.TwoFactorMethod.Email);
419
424
  await Auth.confirmTwoFactorSetup(codeFromEmail('eve@x.com'));
420
425
 
@@ -423,7 +428,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
423
428
  jar.clear();
424
429
  let err: unknown;
425
430
  try {
426
- await Auth.login('eve', 'eve-pw-strong');
431
+ await Auth.login('eve', 'eve-pw-strongA1');
427
432
  } catch (e) {
428
433
  err = e;
429
434
  }
@@ -447,7 +452,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
447
452
  __resetRatelimitForTests();
448
453
  let err2: unknown;
449
454
  try {
450
- await Auth.login('eve', 'eve-pw-strong');
455
+ await Auth.login('eve', 'eve-pw-strongA1');
451
456
  } catch (e) {
452
457
  err2 = e;
453
458
  }
@@ -464,7 +469,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
464
469
  it(
465
470
  '(c) cross-flow: reset tokens and 2FA codes are NOT interchangeable (namespace separation)',
466
471
  async () => {
467
- await Auth.register('frank', 'frank-pw-strong', 'frank@x.com');
472
+ await Auth.register('frank', 'frank-pw-strongA1', 'frank@x.com');
468
473
 
469
474
  // ---- direction 1: a LIVE reset token is useless at /auth/2fa/verify ----
470
475
  await Auth.requestPasswordReset('frank@x.com');
@@ -475,7 +480,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
475
480
  await expect(Auth.verifyTwoFactor(resetToken, resetToken)).rejects.toThrow();
476
481
 
477
482
  // ---- direction 2: a LIVE 2FA login challenge id is useless at /auth/reset/finish ----
478
- await Auth.login('frank', 'frank-pw-strong'); // session (2FA still off here)
483
+ await Auth.login('frank', 'frank-pw-strongA1'); // session (2FA still off here)
479
484
  __clearSentEmails();
480
485
  await Auth.setupTwoFactor(Auth.TwoFactorMethod.Email);
481
486
  await Auth.confirmTwoFactorSetup(codeFromEmail('frank@x.com'));
@@ -484,7 +489,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
484
489
  __resetRatelimitForTests();
485
490
  let err: unknown;
486
491
  try {
487
- await Auth.login('frank', 'frank-pw-strong');
492
+ await Auth.login('frank', 'frank-pw-strongA1');
488
493
  } catch (e) {
489
494
  err = e;
490
495
  }
@@ -527,8 +532,8 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
527
532
  it(
528
533
  '(d) NO session cookie is set on the ST_TWOFA_REQUIRED login response',
529
534
  async () => {
530
- await Auth.register('gwen', 'gwen-pw-strong', 'gwen@x.com');
531
- await Auth.login('gwen', 'gwen-pw-strong');
535
+ await Auth.register('gwen', 'gwen-pw-strongA1', 'gwen@x.com');
536
+ await Auth.login('gwen', 'gwen-pw-strongA1');
532
537
  await Auth.setupTwoFactor(Auth.TwoFactorMethod.Email);
533
538
  await Auth.confirmTwoFactorSetup(codeFromEmail('gwen@x.com'));
534
539
 
@@ -538,7 +543,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
538
543
 
539
544
  let err: unknown;
540
545
  try {
541
- await Auth.login('gwen', 'gwen-pw-strong');
546
+ await Auth.login('gwen', 'gwen-pw-strongA1');
542
547
  } catch (e) {
543
548
  err = e;
544
549
  }
@@ -556,8 +561,8 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
556
561
  it(
557
562
  '(e) login<->setup 2FA challenges are separate: a setup code cannot mint a login session, and setup/verify never silently disables 2FA',
558
563
  async () => {
559
- await Auth.register('ivy', 'ivy-pw-strong', 'ivy@x.com');
560
- await Auth.login('ivy', 'ivy-pw-strong'); // session; 2FA still off
564
+ await Auth.register('ivy', 'ivy-pw-strongA1', 'ivy@x.com');
565
+ await Auth.login('ivy', 'ivy-pw-strongA1'); // session; 2FA still off
561
566
 
562
567
  // Begin a 2FA SETUP: writes a challenge into `twoFaSetup` (keyed by
563
568
  // username), NOT a login challenge into `twoFaLogins`.
@@ -593,7 +598,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
593
598
  '(f) cross-flow: confirm tokens and 2FA codes are NOT interchangeable (namespace separation)',
594
599
  async () => {
595
600
  setConfirmation(true);
596
- await Auth.register('jade', 'jade-pw-strong', 'jade@x.com'); // unconfirmed -> confirm token emailed
601
+ await Auth.register('jade', 'jade-pw-strongA1', 'jade@x.com'); // unconfirmed -> confirm token emailed
597
602
  const confirmToken = tokenFromEmail('confirm', 'jade@x.com'); // live confirmTokens entry
598
603
 
599
604
  // ---- direction 1: a LIVE confirm token is useless at /auth/2fa/verify ----
@@ -607,7 +612,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
607
612
  __resetRatelimitForTests();
608
613
  await Auth.confirmEmail(confirmToken);
609
614
  __resetRatelimitForTests();
610
- expect((await Auth.login('jade', 'jade-pw-strong')).length).toBeGreaterThan(0);
615
+ expect((await Auth.login('jade', 'jade-pw-strongA1')).length).toBeGreaterThan(0);
611
616
 
612
617
  // ---- direction 2: a LIVE 2FA login id/code is useless at /auth/confirm ----
613
618
  // Enable 2FA (jade is confirmed + has a session from the login above).
@@ -622,7 +627,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
622
627
  __resetRatelimitForTests();
623
628
  let err: unknown;
624
629
  try {
625
- await Auth.login('jade', 'jade-pw-strong');
630
+ await Auth.login('jade', 'jade-pw-strongA1');
626
631
  } catch (e) {
627
632
  err = e;
628
633
  }
@@ -665,7 +670,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
665
670
  // ---- confirm token: minted on A, unredeemable on B ----
666
671
  setConfirmation(true);
667
672
  setHost(A);
668
- await Auth.register('mia', 'mia-pw-strong', 'mia@x.com'); // realm A + confirm token emailed
673
+ await Auth.register('mia', 'mia-pw-strongA1', 'mia@x.com'); // realm A + confirm token emailed
669
674
  const confirmTok = tokenFromEmail('confirm', 'mia@x.com');
670
675
 
671
676
  setHost(B);
@@ -687,16 +692,16 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
687
692
 
688
693
  setHost(B);
689
694
  __resetRatelimitForTests();
690
- await expect(Auth.resetPassword(resetTok, 'mia-pw-new')).rejects.toThrow(); // realm B miss
695
+ await expect(Auth.resetPassword(resetTok, 'mia-pw-newA1')).rejects.toThrow(); // realm B miss
691
696
 
692
697
  setHost(A);
693
698
  __resetRatelimitForTests();
694
- await Auth.resetPassword(resetTok, 'mia-pw-new'); // realm A -> resets
699
+ await Auth.resetPassword(resetTok, 'mia-pw-newA1'); // realm A -> resets
695
700
 
696
701
  // ---- 2FA login code: minted on A, unusable on B ----
697
702
  setHost(A);
698
703
  __resetRatelimitForTests();
699
- await Auth.login('mia', 'mia-pw-new'); // session on A (2FA still off)
704
+ await Auth.login('mia', 'mia-pw-newA1'); // session on A (2FA still off)
700
705
  __clearSentEmails();
701
706
  await Auth.setupTwoFactor(Auth.TwoFactorMethod.Email);
702
707
  await Auth.confirmTwoFactorSetup(codeFromEmail('mia@x.com')); // 2FA enabled on A
@@ -706,7 +711,7 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
706
711
  __resetRatelimitForTests();
707
712
  let err: unknown;
708
713
  try {
709
- await Auth.login('mia', 'mia-pw-new'); // -> 2FA challenge (twoFaId + code, realm A)
714
+ await Auth.login('mia', 'mia-pw-newA1'); // -> 2FA challenge (twoFaId + code, realm A)
710
715
  } catch (e) {
711
716
  err = e;
712
717
  }
@@ -726,4 +731,38 @@ describe.skipIf(!haveWasm)('built-in auth: email verification + password reset (
726
731
  },
727
732
  180_000,
728
733
  );
734
+
735
+ // ---- input validation (client-side, before any crypto / network) ----
736
+
737
+ it('checkPassword + isValidEmail: the pure validators behave', () => {
738
+ expect(checkPassword('Str0ng-pw!')).toEqual([]); // meets every rule
739
+ expect(checkPassword('short1A').sort()).toEqual(['minLength', 'special'].sort()); // 7 chars, no special
740
+ expect(checkPassword('alllowercase1!')).toEqual(['uppercase']);
741
+ expect(checkPassword('NoDigits!!')).toEqual(['digit']);
742
+ expect(isValidEmail('bob@example.com')).toBe(true);
743
+ expect(isValidEmail('bobfwehfuewhfwf.com')).toBe(false); // no @
744
+ expect(isValidEmail('a@b')).toBe(false); // no dotted domain
745
+ expect(isValidEmail('a b@x.com')).toBe(false); // space
746
+ });
747
+
748
+ it('register rejects a weak password with WeakPasswordError (before any network)', async () => {
749
+ const err = await Auth.register('newbie', 'weak', 'newbie@x.com').catch((e: unknown) => e);
750
+ expect(err).toBeInstanceOf(WeakPasswordError);
751
+ expect((err as WeakPasswordError).code).toBe(AuthErrorCode.WeakPassword);
752
+ // the unmet rules are exposed so a UI can show a checklist
753
+ expect((err as WeakPasswordError).unmet).toContain('minLength');
754
+ expect((err as WeakPasswordError).unmet).toContain('uppercase');
755
+ // no confirm email was sent (it threw before the request)
756
+ __clearSentEmails();
757
+ expect(__sentEmails.length).toBe(0);
758
+ });
759
+
760
+ it('register rejects an invalid email + a bad username with typed errors', async () => {
761
+ await expect(Auth.register('newbie', 'Str0ng-pw!', 'not-an-email')).rejects.toBeInstanceOf(
762
+ InvalidEmailError,
763
+ );
764
+ await expect(Auth.register('', 'Str0ng-pw!', 'ok@x.com')).rejects.toBeInstanceOf(
765
+ InvalidUsernameError,
766
+ );
767
+ });
729
768
  });