toiljs 0.0.104 → 0.0.106
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/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.d.ts +28 -0
- package/build/compiler/auth-emails.js +165 -0
- package/build/compiler/emails.d.ts +2 -0
- package/build/compiler/emails.js +4 -1
- package/build/compiler/index.js +12 -0
- package/build/compiler/toil-docs.generated.js +7 -6
- package/docs/README.md +2 -1
- package/docs/auth/README.md +2 -0
- package/docs/auth/emails.md +91 -0
- package/docs/introduction/README.md +19 -10
- package/docs/introduction/hyperscale.md +1 -1
- package/docs/introduction/vs-other-frameworks.md +3 -3
- package/docs/introduction/why-toil.md +2 -4
- package/docs/llms.txt +1 -0
- package/package.json +1 -1
- package/server/auth/AuthController.ts +16 -40
- package/server/globals/email.ts +15 -2
- package/src/compiler/auth-emails.ts +279 -0
- package/src/compiler/emails.ts +14 -1
- package/src/compiler/index.ts +24 -1
- package/src/compiler/toil-docs.generated.ts +7 -6
- package/test/auth-emails.test.ts +115 -0
- package/test/email-console.test.ts +2 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in auth email OVERRIDE codegen (src/compiler/auth-emails.ts): the
|
|
3
|
+
* pure fold (a rendered emails/auth-*.tsx over the default) and the AssemblyScript
|
|
4
|
+
* `AuthEmail` module it bakes. The Vite-render + file IO in `renderAuthEmails` is
|
|
5
|
+
* covered by the example build; here we test the string transforms in isolation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { __test } from '../src/compiler/auth-emails';
|
|
10
|
+
import type { RenderedEmail } from '../src/compiler/emails';
|
|
11
|
+
|
|
12
|
+
const { moduleSource, effectiveFromOverride, SPECS } = __test;
|
|
13
|
+
|
|
14
|
+
/** A defaults-only parts map (what a project with no overrides generates). */
|
|
15
|
+
function defaultParts(): Record<string, { subject: string; text: string; html: string }> {
|
|
16
|
+
const parts: Record<string, { subject: string; text: string; html: string }> = {};
|
|
17
|
+
for (const [name, spec] of Object.entries(SPECS))
|
|
18
|
+
parts[name] = {
|
|
19
|
+
subject: spec.defaultSubject,
|
|
20
|
+
text: spec.defaultText,
|
|
21
|
+
html: spec.defaultHtml,
|
|
22
|
+
};
|
|
23
|
+
return parts;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function rendered(name: string, over: Partial<RenderedEmail>): RenderedEmail {
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
subject: over.subject ?? name,
|
|
30
|
+
html: over.html ?? '',
|
|
31
|
+
text: over.text ?? '',
|
|
32
|
+
tokens: over.tokens ?? [],
|
|
33
|
+
purpose: over.purpose ?? name.toLowerCase(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('AuthEmail module codegen', () => {
|
|
38
|
+
it('emits confirm/reset/twofa, each through the DETACHED send with its purpose', () => {
|
|
39
|
+
const src = moduleSource(defaultParts());
|
|
40
|
+
expect(src).toContain('export namespace AuthEmail {');
|
|
41
|
+
expect(src).toContain('export function confirm(to: string, link: string): void');
|
|
42
|
+
expect(src).toContain('export function reset(to: string, link: string): void');
|
|
43
|
+
// twofa takes the subject as a runtime arg (login vs setup), not baked.
|
|
44
|
+
expect(src).toContain('export function twofa(to: string, code: string, subject: string): void');
|
|
45
|
+
// Every send is detached (the anti-enumeration property), never the suspending send.
|
|
46
|
+
expect(src.match(/\.sendDetached\(/g)).toHaveLength(3);
|
|
47
|
+
expect(src).not.toContain('.send(to');
|
|
48
|
+
expect(src).toContain('"verify"');
|
|
49
|
+
expect(src).toContain('"reset"');
|
|
50
|
+
expect(src).toContain('"2fa"');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('bakes the default subject for confirm/reset but passes `subject` through for twofa', () => {
|
|
54
|
+
const src = moduleSource(defaultParts());
|
|
55
|
+
expect(src).toContain('new EmailTemplate("Confirm your account"');
|
|
56
|
+
expect(src).toContain('new EmailTemplate("Reset your password"');
|
|
57
|
+
expect(src).toContain('new EmailTemplate(subject,'); // twofa uses the arg
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('interpolates the right token per email (link for confirm/reset, code for twofa)', () => {
|
|
61
|
+
const src = moduleSource(defaultParts());
|
|
62
|
+
expect(src).toContain('v.set("link", link)');
|
|
63
|
+
expect(src).toContain('v.set("code", code)');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('escapes baked HTML into a valid string literal', () => {
|
|
67
|
+
const parts = defaultParts();
|
|
68
|
+
parts.AuthConfirm = {
|
|
69
|
+
subject: 'Hi "there"',
|
|
70
|
+
text: 'a\nb',
|
|
71
|
+
html: '<a href="x">c</a>',
|
|
72
|
+
};
|
|
73
|
+
const src = moduleSource(parts);
|
|
74
|
+
// JSON.stringify escaping keeps the emitted AS parseable (quotes/newlines escaped).
|
|
75
|
+
expect(src).toContain('"Hi \\"there\\""');
|
|
76
|
+
expect(src).toContain('"<a href=\\"x\\">c</a>"');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('effectiveFromOverride', () => {
|
|
81
|
+
it('uses the override subject/text/html when the template sets a subject', () => {
|
|
82
|
+
const eff = effectiveFromOverride(
|
|
83
|
+
'AuthConfirm',
|
|
84
|
+
SPECS.AuthConfirm,
|
|
85
|
+
rendered('AuthConfirm', {
|
|
86
|
+
subject: 'Verify your Acme account',
|
|
87
|
+
html: '<p>{{link}}</p>',
|
|
88
|
+
text: '{{link}}',
|
|
89
|
+
tokens: ['link'],
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
expect(eff.subject).toBe('Verify your Acme account');
|
|
93
|
+
expect(eff.html).toBe('<p>{{link}}</p>');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('falls back to the default subject when the template did not set one', () => {
|
|
97
|
+
// renderModule defaults `subject` to the module name when no `export const subject`.
|
|
98
|
+
const eff = effectiveFromOverride(
|
|
99
|
+
'AuthConfirm',
|
|
100
|
+
SPECS.AuthConfirm,
|
|
101
|
+
rendered('AuthConfirm', { subject: 'AuthConfirm', html: '<p>{{link}}</p>', tokens: ['link'] }),
|
|
102
|
+
);
|
|
103
|
+
expect(eff.subject).toBe('Confirm your account');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('ignores an override subject for twofa (login/setup subject is contextual)', () => {
|
|
107
|
+
const eff = effectiveFromOverride(
|
|
108
|
+
'Auth2fa',
|
|
109
|
+
SPECS.Auth2fa,
|
|
110
|
+
rendered('Auth2fa', { subject: 'ignored', html: '<b>{{code}}</b>', tokens: ['code'] }),
|
|
111
|
+
);
|
|
112
|
+
expect(eff.subject).toBe(''); // subjectFromArg: baked subject stays empty (the arg wins at runtime)
|
|
113
|
+
expect(eff.html).toBe('<b>{{code}}</b>');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -74,13 +74,13 @@ describe('dev email emulator', () => {
|
|
|
74
74
|
'<p><a href="https://x.com/a?b=1">Open</a></p>',
|
|
75
75
|
40,
|
|
76
76
|
);
|
|
77
|
-
const joined = lines.map((l) => l.replace(/\[[0-9;]*m/g, '')).join('\n');
|
|
77
|
+
const joined = lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, '')).join('\n');
|
|
78
78
|
expect(joined).toContain('Hi there & welcome');
|
|
79
79
|
expect(joined).toContain('Open');
|
|
80
80
|
expect(joined).toContain('(https://x.com/a?b=1)');
|
|
81
81
|
// a blank separator line sits between the two paragraphs
|
|
82
82
|
expect(lines).toContain('');
|
|
83
83
|
// no line exceeds the wrap width (visible chars; color is off under vitest)
|
|
84
|
-
for (const l of lines) expect(l.replace(/\[[0-9;]*m/g, '').length).toBeLessThanOrEqual(40);
|
|
84
|
+
for (const l of lines) expect(l.replace(/\x1b\[[0-9;]*m/g, '').length).toBeLessThanOrEqual(40);
|
|
85
85
|
});
|
|
86
86
|
});
|