toiljs 0.0.87 → 0.0.89
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 +5 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.d.ts +12 -1
- package/build/client/auth.js +82 -4
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/analytics/index.js +41 -20
- package/build/devserver/config/ratelimit.d.ts +1 -0
- package/build/devserver/config/ratelimit.js +3 -0
- package/build/devserver/email/index.d.ts +10 -0
- package/build/devserver/email/index.js +16 -0
- package/build/devserver/runtime/host.js +33 -49
- package/build/devserver/runtime/module.js +1 -0
- package/examples/basic/client/routes/pq.tsx +12 -2
- package/examples/basic/server/main.ts +0 -1
- package/examples/basic/server/routes/Session.ts +5 -11
- package/package.json +3 -3
- package/server/auth/AuthController.ts +467 -24
- package/server/globals/auth.ts +44 -2
- package/server/globals/email.ts +54 -1
- package/src/client/auth.ts +130 -4
- package/src/devserver/analytics/index.ts +61 -30
- package/src/devserver/config/ratelimit.ts +7 -0
- package/src/devserver/email/index.ts +44 -0
- package/src/devserver/runtime/host.ts +58 -64
- package/src/devserver/runtime/module.ts +1 -0
- package/test/analytics-dev.test.ts +34 -12
- package/test/devserver-email.test.ts +48 -1
- package/test/pqauth-e2e.test.ts +18 -9
- package/test/pqauth-email.test.ts +333 -0
- package/examples/basic/server/routes/Auth.ts +0 -268
|
@@ -21,7 +21,7 @@ import { devEnvGet, devEnvGetSecure } from '../config/env.js';
|
|
|
21
21
|
import { ratelimitCheck } from '../config/ratelimit.js';
|
|
22
22
|
import { buildAnalyticsImports } from '../analytics/index.js';
|
|
23
23
|
import { buildDatabaseImports, type DbDevState, freshDbState } from '../db/index.js';
|
|
24
|
-
import { EmailStatus, getEmailService } from '../email/index.js';
|
|
24
|
+
import { EmailStatus, getEmailService, recordSentEmail } from '../email/index.js';
|
|
25
25
|
import { parseEmailBlob } from '../email/wire.js';
|
|
26
26
|
import { buildCryptoImports, type CryptoState, freshCryptoState } from './crypto.js';
|
|
27
27
|
|
|
@@ -190,6 +190,48 @@ function envLookup(
|
|
|
190
190
|
return bytes.length;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* `env::email_send` / `env::email_send_detached` in dev: the FULL email pipeline
|
|
195
|
+
* (./email) — parse + recipient validation + dedup + per-min/day budget +
|
|
196
|
+
* per-recipient cap run SYNCHRONOUSLY (exact status: BadRecipient/Deduped/Budget/
|
|
197
|
+
* RecipientCapped), then the real provider send is FIRE-AND-FORGET (a sync wasm
|
|
198
|
+
* import can't await it), so the guest gets `Sent` optimistically and the true
|
|
199
|
+
* outcome is logged. Unconfigured email stays a log-only mock returning `Sent`.
|
|
200
|
+
*
|
|
201
|
+
* The dev path already fires-and-forgets, so the SUSPENDING `email_send` and the
|
|
202
|
+
* NON-SUSPENDING `email_send_detached` map to the exact same behaviour here (both
|
|
203
|
+
* return immediately). BOTH feed the `__sentEmails` capture seam, so the email
|
|
204
|
+
* confirm/reset tests find their tokens whichever import the guest used. Mirrors
|
|
205
|
+
* the edge's `email_send_import.rs`.
|
|
206
|
+
*/
|
|
207
|
+
function devEmailSend(ref: MemoryRef, reqPtr: number, reqLen: number): number {
|
|
208
|
+
const raw = readBytes(ref, reqPtr, reqLen);
|
|
209
|
+
const svc = getEmailService();
|
|
210
|
+
if (svc === null) {
|
|
211
|
+
const parsed = parseEmailBlob(raw);
|
|
212
|
+
if (parsed !== null) recordSentEmail(parsed); // dev test seam
|
|
213
|
+
const to = parsed?.to ?? '<unparsed>';
|
|
214
|
+
process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
|
|
215
|
+
return EmailStatus.Sent;
|
|
216
|
+
}
|
|
217
|
+
const { status, parsed } = svc.prepare(raw);
|
|
218
|
+
if (parsed === null) {
|
|
219
|
+
process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
|
|
220
|
+
return status;
|
|
221
|
+
}
|
|
222
|
+
recordSentEmail(parsed); // dev test seam
|
|
223
|
+
void svc
|
|
224
|
+
.deliver(parsed)
|
|
225
|
+
.then((s) => {
|
|
226
|
+
const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
|
|
227
|
+
process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
|
|
228
|
+
})
|
|
229
|
+
.catch((e: unknown) => {
|
|
230
|
+
process.stdout.write(` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`);
|
|
231
|
+
});
|
|
232
|
+
return EmailStatus.Sent; // optimistic; sync wasm can't await the send
|
|
233
|
+
}
|
|
234
|
+
|
|
193
235
|
/**
|
|
194
236
|
* The portion of the `env.*` request surface that is SHARED by the daemon (cold)
|
|
195
237
|
* box: panic hook, `Environment.get`/`getSecure`, `email_send`, `thread_spawn`,
|
|
@@ -226,34 +268,13 @@ export function buildEnvImports(
|
|
|
226
268
|
// `Date.now()` -> wall-clock milliseconds, matching the edge host.
|
|
227
269
|
'Date.now': (): bigint => BigInt(Date.now()),
|
|
228
270
|
|
|
229
|
-
// `env::email_send`: the FULL email pipeline in
|
|
230
|
-
// mail, so this stays in the shared subset (00 B2 /
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
|
|
237
|
-
return EmailStatus.Sent;
|
|
238
|
-
}
|
|
239
|
-
const { status, parsed } = svc.prepare(raw);
|
|
240
|
-
if (parsed === null) {
|
|
241
|
-
process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
|
|
242
|
-
return status;
|
|
243
|
-
}
|
|
244
|
-
void svc
|
|
245
|
-
.deliver(parsed)
|
|
246
|
-
.then((s) => {
|
|
247
|
-
const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
|
|
248
|
-
process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
|
|
249
|
-
})
|
|
250
|
-
.catch((e: unknown) => {
|
|
251
|
-
process.stdout.write(
|
|
252
|
-
` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`,
|
|
253
|
-
);
|
|
254
|
-
});
|
|
255
|
-
return EmailStatus.Sent; // optimistic; sync wasm can't await the send
|
|
256
|
-
},
|
|
271
|
+
// `env::email_send` / `email_send_detached`: the FULL email pipeline in
|
|
272
|
+
// dev. A daemon may send mail, so this stays in the shared subset (00 B2 /
|
|
273
|
+
// doc 08 AN-8). Both map to the same fire-and-forget dev path.
|
|
274
|
+
email_send: (reqPtr: number, reqLen: number): number =>
|
|
275
|
+
devEmailSend(ref, reqPtr, reqLen),
|
|
276
|
+
email_send_detached: (reqPtr: number, reqLen: number): number =>
|
|
277
|
+
devEmailSend(ref, reqPtr, reqLen),
|
|
257
278
|
};
|
|
258
279
|
}
|
|
259
280
|
|
|
@@ -340,41 +361,14 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
|
|
|
340
361
|
return d.allowed ? 1 : -Math.max(1, d.retryAfterSecs);
|
|
341
362
|
},
|
|
342
363
|
|
|
343
|
-
// `env::email_send
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const raw = readBytes(ref, reqPtr, reqLen);
|
|
352
|
-
const svc = getEmailService();
|
|
353
|
-
if (svc === null) {
|
|
354
|
-
const to = parseEmailBlob(raw)?.to ?? '<unparsed>';
|
|
355
|
-
process.stdout.write(
|
|
356
|
-
` ✉ dev email_send -> ${to} (no email config; not sent)\n`,
|
|
357
|
-
);
|
|
358
|
-
return EmailStatus.Sent;
|
|
359
|
-
}
|
|
360
|
-
const { status, parsed } = svc.prepare(raw);
|
|
361
|
-
if (parsed === null) {
|
|
362
|
-
process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
|
|
363
|
-
return status;
|
|
364
|
-
}
|
|
365
|
-
void svc
|
|
366
|
-
.deliver(parsed)
|
|
367
|
-
.then((s) => {
|
|
368
|
-
const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
|
|
369
|
-
process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
|
|
370
|
-
})
|
|
371
|
-
.catch((e: unknown) => {
|
|
372
|
-
process.stdout.write(
|
|
373
|
-
` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`,
|
|
374
|
-
);
|
|
375
|
-
});
|
|
376
|
-
return EmailStatus.Sent; // optimistic; sync wasm can't await the send
|
|
377
|
-
},
|
|
364
|
+
// `env::email_send` (suspending on the edge) / `email_send_detached`
|
|
365
|
+
// (non-suspending on the edge): the FULL dev email pipeline. Both map
|
|
366
|
+
// to the same fire-and-forget dev path (a sync wasm import can't await
|
|
367
|
+
// the provider send), and BOTH feed `__sentEmails`. See devEmailSend.
|
|
368
|
+
email_send: (reqPtr: number, reqLen: number): number =>
|
|
369
|
+
devEmailSend(ref, reqPtr, reqLen),
|
|
370
|
+
email_send_detached: (reqPtr: number, reqLen: number): number =>
|
|
371
|
+
devEmailSend(ref, reqPtr, reqLen),
|
|
378
372
|
|
|
379
373
|
// `Environment.get` / `getSecure`: copy one tenant env value into the
|
|
380
374
|
// guest buffer. Returns the byte length (0 = present-but-empty), -1 if
|
|
@@ -26,25 +26,36 @@ describe('dev analytics stub v2 frame parity', () => {
|
|
|
26
26
|
let p = 0;
|
|
27
27
|
const u16 = () => { const v = buf.readUInt16LE(p); p += 2; return v; };
|
|
28
28
|
const u32 = () => { const v = buf.readUInt32LE(p); p += 4; return v; };
|
|
29
|
-
const i64 = () => { const v = buf.readBigInt64LE(p); p += 8; return v; };
|
|
30
29
|
const u64 = () => { const v = buf.readBigUInt64LE(p); p += 8; return v; };
|
|
30
|
+
const f64 = () => { const v = buf.readDoubleLE(p); p += 8; return v; };
|
|
31
31
|
|
|
32
32
|
expect(u16()).toBe(2); // FRAME_VERSION
|
|
33
33
|
u64(); // now_ms
|
|
34
34
|
const count = u32();
|
|
35
|
-
expect(count).toBe(
|
|
35
|
+
expect(count).toBe(42); // METRIC_COUNTERS (edge N_COUNTERS; no WasmDispatches)
|
|
36
36
|
const life: bigint[] = [];
|
|
37
|
-
for (let i = 0; i < count; i++) life.push(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(life[
|
|
41
|
-
expect(life[
|
|
42
|
-
expect(
|
|
43
|
-
expect(
|
|
44
|
-
expect(
|
|
37
|
+
for (let i = 0; i < count; i++) life.push(u64()); // counters are u64 LE now
|
|
38
|
+
// Indices are the AUTHORITATIVE edge MetricId ids (metric_id.rs): a mislabel here means the dev
|
|
39
|
+
// dashboard shows the wrong metric for every id past StaticHits.
|
|
40
|
+
expect(life[0]).toBe(42n); // Requests (id 0)
|
|
41
|
+
expect(life[1]).toBe(12345n); // BytesOutL1 (id 1)
|
|
42
|
+
expect(life[11]).toBe(1_000_000n); // GasUsed (id 11, was 12 in the stale layout)
|
|
43
|
+
expect(life[40]).toBe(900n); // CacheHits (id 40, was 41)
|
|
44
|
+
expect(life[41]).toBe(100n); // CacheMisses (id 41, was 42)
|
|
45
|
+
expect(u64()).toBe(3n); // connectedStreams
|
|
46
|
+
expect(u64()).toBe(65536n); // committedMemory
|
|
47
|
+
expect(u64()).toBe(5n); // reqMinuteUsed
|
|
45
48
|
expect(u64()).toBe(100n); // reqMinuteCap
|
|
46
|
-
expect(
|
|
49
|
+
expect(u64()).toBe(42n); // reqDayUsed
|
|
47
50
|
expect(u64()).toBe(5000n); // reqDayCap
|
|
51
|
+
// 7 LIVE per-second rates (f64 LE), appended after the windows.
|
|
52
|
+
expect(f64()).toBeCloseTo(0.7); // rps
|
|
53
|
+
expect(f64()).toBeCloseTo(68.2); // bytesInPerSec
|
|
54
|
+
expect(f64()).toBeCloseTo(205.75); // bytesOutPerSec
|
|
55
|
+
expect(f64()).toBeCloseTo(34.1); // streamBytesInPerSec
|
|
56
|
+
expect(f64()).toBeCloseTo(136.5); // streamBytesOutPerSec
|
|
57
|
+
expect(f64()).toBeCloseTo(0.28); // dbOpsPerSec
|
|
58
|
+
expect(f64()).toBeCloseTo(16_666.67); // gasPerSec
|
|
48
59
|
expect(p).toBe(buf.length); // no trailing bytes
|
|
49
60
|
});
|
|
50
61
|
|
|
@@ -69,8 +80,19 @@ describe('dev analytics stub v2 frame parity', () => {
|
|
|
69
80
|
expect(b2.readUInt32LE(4)).toBe(60); // bucketSecs
|
|
70
81
|
expect(b2.readUInt32LE(16)).toBe(60); // count
|
|
71
82
|
|
|
72
|
-
//
|
|
83
|
+
// range 8 (D60) + 9 (D90) -> DAY ring: 60/90 buckets, bucketSecs 86400.
|
|
84
|
+
imports.analytics_series(0, 0, 0, 8);
|
|
85
|
+
const b60 = db.lastResult as unknown as Buffer;
|
|
86
|
+
expect(b60.readUInt32LE(4)).toBe(86_400); // bucketSecs
|
|
87
|
+
expect(b60.readUInt32LE(16)).toBe(60); // count
|
|
88
|
+
imports.analytics_series(0, 0, 0, 9);
|
|
89
|
+
const b90 = db.lastResult as unknown as Buffer;
|
|
90
|
+
expect(b90.readUInt32LE(4)).toBe(86_400); // bucketSecs
|
|
91
|
+
expect(b90.readUInt32LE(16)).toBe(90); // count
|
|
92
|
+
|
|
93
|
+
// A bad metric / range -> ABSENT (negative), no frame. Range 10 is past D90.
|
|
73
94
|
expect(imports.analytics_series(0, 0, 999, 3)).toBeLessThan(0);
|
|
74
95
|
expect(imports.analytics_series(0, 0, 0, 99)).toBeLessThan(0);
|
|
96
|
+
expect(imports.analytics_series(0, 0, 0, 10)).toBeLessThan(0);
|
|
75
97
|
});
|
|
76
98
|
});
|
|
@@ -9,11 +9,17 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
9
9
|
|
|
10
10
|
import { EmailCaps } from '../src/devserver/email/caps.js';
|
|
11
11
|
import { resolveEmailConfig, type ResolvedEmailConfig } from '../src/devserver/email/config.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
NodeEmailService,
|
|
14
|
+
__sentEmails,
|
|
15
|
+
__clearSentEmails,
|
|
16
|
+
resetEmailService,
|
|
17
|
+
} from '../src/devserver/email/index.js';
|
|
13
18
|
import { sendResend } from '../src/devserver/email/providers.js';
|
|
14
19
|
import { EmailStatus } from '../src/devserver/email/status.js';
|
|
15
20
|
import { validFrom, validRecipient } from '../src/devserver/email/validate.js';
|
|
16
21
|
import { parseEmailBlob } from '../src/devserver/email/wire.js';
|
|
22
|
+
import { buildHostImports, freshDispatchState, type MemoryRef } from '../src/devserver/runtime/host.js';
|
|
17
23
|
|
|
18
24
|
/** Encode an `email_send` blob exactly like the guest (`server/globals/email.ts`). */
|
|
19
25
|
function encodeBlob(to: string, subject: string, purpose: string, body: string, html: string): Buffer {
|
|
@@ -239,3 +245,44 @@ describe('NodeEmailService pipeline', () => {
|
|
|
239
245
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
240
246
|
});
|
|
241
247
|
});
|
|
248
|
+
|
|
249
|
+
describe('host imports: email_send_detached (the anti-enumeration fire-and-forget path)', () => {
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
resetEmailService();
|
|
252
|
+
__clearSentEmails();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/** Build a dev host import table over a fresh wasm memory holding `blob` at offset 0. */
|
|
256
|
+
function wireHost(blob: Buffer): {
|
|
257
|
+
env: Record<string, (...a: number[]) => number>;
|
|
258
|
+
len: number;
|
|
259
|
+
} {
|
|
260
|
+
const memory = new WebAssembly.Memory({ initial: 1 });
|
|
261
|
+
Buffer.from(memory.buffer).set(blob, 0);
|
|
262
|
+
const ref: MemoryRef = { memory };
|
|
263
|
+
const imports = buildHostImports(ref, freshDispatchState());
|
|
264
|
+
return { env: imports.env as Record<string, (...a: number[]) => number>, len: blob.length };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
it('is registered and returns a queue-accept status immediately (no provider round-trip)', () => {
|
|
268
|
+
resetEmailService(); // unconfigured: log-only stub, still constant-time
|
|
269
|
+
__clearSentEmails();
|
|
270
|
+
const { env, len } = wireHost(encodeBlob('someone@x.com', 'Subj', 'verify', 'tok', '<b>x</b>'));
|
|
271
|
+
// The detached import exists (mirrors the edge's env.email_send_detached).
|
|
272
|
+
expect(typeof env.email_send_detached).toBe('function');
|
|
273
|
+
// It returns synchronously with a queue-accept status (Sent == accepted/queued).
|
|
274
|
+
expect(env.email_send_detached(0, len)).toBe(EmailStatus.Sent);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('feeds the __sentEmails capture seam so confirm/reset tests still find the token', () => {
|
|
278
|
+
resetEmailService();
|
|
279
|
+
__clearSentEmails();
|
|
280
|
+
const link = 'https://app.example.com/reset#token=deadbeef';
|
|
281
|
+
const { env, len } = wireHost(encodeBlob('user@x.com', 'Reset', 'reset', link, ''));
|
|
282
|
+
env.email_send_detached(0, len);
|
|
283
|
+
const captured = __sentEmails.find((e) => e.to === 'user@x.com');
|
|
284
|
+
expect(captured).toBeTruthy();
|
|
285
|
+
expect(captured!.purpose).toBe('reset');
|
|
286
|
+
expect(captured!.text).toContain('token=deadbeef'); // the emailed token survives to the seam
|
|
287
|
+
});
|
|
288
|
+
});
|
package/test/pqauth-e2e.test.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { ristretto255_oprf } from '@noble/curves/ed25519.js';
|
|
|
21
21
|
|
|
22
22
|
import { WasmServerModule } from '../src/devserver/index.js';
|
|
23
23
|
import { __resetDbForTests } from '../src/devserver/db/index.js';
|
|
24
|
+
import { __resetRatelimitForTests } from '../src/devserver/config/ratelimit.js';
|
|
24
25
|
import { Auth } from '../src/client/auth.js';
|
|
25
26
|
import { DataReader, DataWriter } from '../src/io/codec.js';
|
|
26
27
|
|
|
@@ -85,6 +86,9 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
|
|
|
85
86
|
|
|
86
87
|
beforeEach(() => {
|
|
87
88
|
__resetDbForTests();
|
|
89
|
+
// The built-in controller decorates every route with `@ratelimit`; the dev limiter is a
|
|
90
|
+
// module-level singleton, so reset it per case to keep the many dispatches in this file isolated.
|
|
91
|
+
__resetRatelimitForTests();
|
|
88
92
|
mod = loadModule();
|
|
89
93
|
restoreFetch = installFetchShim(mod);
|
|
90
94
|
});
|
|
@@ -93,7 +97,7 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
|
|
|
93
97
|
it(
|
|
94
98
|
'registers then logs in (full OPRF + ML-DSA + ML-KEM mutual-auth chain)',
|
|
95
99
|
async () => {
|
|
96
|
-
await Auth.register('ada', 'correct horse battery staple');
|
|
100
|
+
await Auth.register('ada', 'correct horse battery staple', 'ada@example.com');
|
|
97
101
|
// login resolves ONLY if the server's mutual-auth confirmation tag
|
|
98
102
|
// verified against the client's own shared secret.
|
|
99
103
|
const session = await Auth.login('ada', 'correct horse battery staple');
|
|
@@ -125,7 +129,7 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
|
|
|
125
129
|
it(
|
|
126
130
|
'rejects a wrong password at login',
|
|
127
131
|
async () => {
|
|
128
|
-
await Auth.register('bob', 'hunter2-correct');
|
|
132
|
+
await Auth.register('bob', 'hunter2-correct', 'bob@example.com');
|
|
129
133
|
await expect(Auth.login('bob', 'hunter2-WRONG')).rejects.toThrow(/login failed|request failed/);
|
|
130
134
|
},
|
|
131
135
|
60_000,
|
|
@@ -142,12 +146,13 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
|
|
|
142
146
|
it(
|
|
143
147
|
'rejects a duplicate registration with a clear (not generic) message',
|
|
144
148
|
async () => {
|
|
145
|
-
await Auth.register('dupe', 'correct horse battery staple');
|
|
149
|
+
await Auth.register('dupe', 'correct horse battery staple', 'dupe@example.com');
|
|
146
150
|
// Second registration of the same username must say so explicitly,
|
|
147
|
-
// not return the generic "request failed".
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
// not return the generic "request failed". (The username-taken check fires
|
|
152
|
+
// before the email check, so it stays `already registered` even with the same email.)
|
|
153
|
+
await expect(
|
|
154
|
+
Auth.register('dupe', 'correct horse battery staple', 'dupe@example.com'),
|
|
155
|
+
).rejects.toThrow(/already registered/);
|
|
151
156
|
},
|
|
152
157
|
60_000,
|
|
153
158
|
);
|
|
@@ -155,7 +160,10 @@ describe.skipIf(!haveWasm)('post-quantum auth end-to-end (client <-> example was
|
|
|
155
160
|
|
|
156
161
|
// Lower-level wire checks that don't need the heavy Argon2id derivation.
|
|
157
162
|
describe.skipIf(!haveWasm)('post-quantum auth wire-level (anti-enumeration, replay)', () => {
|
|
158
|
-
beforeEach(() =>
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
__resetDbForTests();
|
|
165
|
+
__resetRatelimitForTests();
|
|
166
|
+
});
|
|
159
167
|
|
|
160
168
|
const oprf = ristretto255_oprf.oprf;
|
|
161
169
|
const loginStart = (m: WasmServerModule, username: string) => {
|
|
@@ -193,7 +201,8 @@ describe.skipIf(!haveWasm)('post-quantum auth wire-level (anti-enumeration, repl
|
|
|
193
201
|
expect(a.salt.length).toBe(16);
|
|
194
202
|
expect(a.nonce.length).toBe(32);
|
|
195
203
|
expect(a.evaluated.length).toBe(32);
|
|
196
|
-
|
|
204
|
+
// The built-in controller returns `TOIL_AUTH_AUDIENCE` or the literal `'toil'` (dev has no secret set).
|
|
205
|
+
expect(a.aud).toBe('toil');
|
|
197
206
|
// The randomized fields DO differ (fresh challenge each call).
|
|
198
207
|
expect(hex(a.cid)).not.toBe(hex(b.cid));
|
|
199
208
|
});
|