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.
- package/CHANGELOG.md +10 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.d.ts +35 -1
- package/build/client/auth.js +75 -0
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +26 -0
- package/build/compiler/toil-docs.generated.js +5 -4
- package/docs/README.md +2 -1
- package/docs/concepts/ai-guide.md +132 -0
- package/docs/frontend/data-fetching.md +17 -0
- package/docs/frontend/rendering.md +5 -5
- package/docs/llms.txt +1 -0
- package/docs/services/cookies.md +2 -2
- package/package.json +1 -1
- package/server/auth/AuthController.ts +29 -1
- package/src/client/auth.ts +125 -0
- package/src/client/index.ts +9 -1
- package/src/compiler/docs.ts +26 -0
- package/src/compiler/toil-docs.generated.ts +5 -4
- package/test/pqauth-e2e.test.ts +5 -5
- package/test/pqauth-email.test.ts +83 -44
package/test/pqauth-e2e.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
175
|
-
const session = await Auth.login('ada', 'correct horse battery
|
|
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-
|
|
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-
|
|
228
|
-
await Auth.login('heidi', 'pw-heidi-
|
|
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-
|
|
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-
|
|
272
|
+
await expect(Auth.resetPassword(token1, 'new-pw-unusedA1')).rejects.toThrow();
|
|
268
273
|
// ...only the latest does.
|
|
269
|
-
await Auth.resetPassword(token2, 'new-pw-
|
|
270
|
-
const session = await Auth.login('ivan', 'new-pw-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
325
|
+
await Auth.resetPassword(token, 'newpw-erin-strongA1');
|
|
321
326
|
|
|
322
|
-
await expect(Auth.login('erin', 'oldpw-erin-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
376
|
-
await Auth.login('dave', 'dave-pw-
|
|
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-
|
|
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-
|
|
417
|
-
await Auth.login('eve', 'eve-pw-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
531
|
-
await Auth.login('gwen', 'gwen-pw-
|
|
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-
|
|
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-
|
|
560
|
-
await Auth.login('ivy', 'ivy-pw-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
});
|