@technomoron/mail-magic 1.0.32 → 1.0.33

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.
Files changed (40) hide show
  1. package/CHANGES +10 -0
  2. package/README.md +213 -122
  3. package/dist/api/assets.js +9 -56
  4. package/dist/api/auth.js +1 -12
  5. package/dist/api/form-replyto.js +44 -0
  6. package/dist/api/form-submission.js +95 -0
  7. package/dist/api/forms.js +262 -318
  8. package/dist/api/mailer.js +1 -1
  9. package/dist/bin/mail-magic.js +2 -2
  10. package/dist/index.js +30 -18
  11. package/dist/models/db.js +5 -5
  12. package/dist/models/domain.js +16 -8
  13. package/dist/models/form.js +110 -38
  14. package/dist/models/init.js +34 -74
  15. package/dist/models/recipient.js +12 -8
  16. package/dist/models/txmail.js +22 -25
  17. package/dist/models/user.js +14 -10
  18. package/dist/server.js +1 -1
  19. package/dist/store/store.js +53 -22
  20. package/dist/swagger.js +107 -0
  21. package/dist/util/captcha.js +24 -0
  22. package/dist/util/email.js +19 -0
  23. package/dist/util/paths.js +41 -0
  24. package/dist/util/ratelimit.js +48 -0
  25. package/dist/util/uploads.js +48 -0
  26. package/dist/util/utils.js +151 -0
  27. package/dist/util.js +4 -127
  28. package/docs/config-example/example.test/assets/files/banner.png +1 -0
  29. package/docs/config-example/example.test/assets/images/logo.png +1 -0
  30. package/docs/config-example/example.test/form-template/base.njk +6 -0
  31. package/docs/config-example/example.test/form-template/contact.njk +9 -0
  32. package/docs/config-example/example.test/form-template/partials/fields.njk +3 -0
  33. package/docs/config-example/example.test/tx-template/base.njk +10 -0
  34. package/docs/config-example/example.test/tx-template/partials/header.njk +1 -0
  35. package/docs/config-example/example.test/tx-template/welcome.njk +10 -0
  36. package/docs/config-example/init-data.json +57 -0
  37. package/docs/form-security.md +194 -0
  38. package/docs/swagger/openapi.json +1321 -0
  39. package/{TUTORIAL.MD → docs/tutorial.md} +24 -15
  40. package/package.json +3 -3
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ import { getBodyValue } from '../util.js';
3
+ const ALLOWED_MM_KEYS = new Set(['_mm_form_key', '_mm_locale', '_mm_recipients']);
4
+ function asRecord(input) {
5
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
6
+ return {};
7
+ }
8
+ return input;
9
+ }
10
+ function getCaptchaTokenFromBody(body) {
11
+ return getBodyValue(body, 'cf-turnstile-response', 'h-captcha-response', 'g-recaptcha-response', 'captcha');
12
+ }
13
+ const optionalStringish = z.union([z.string(), z.array(z.string())]).optional();
14
+ // Public form submission payload schema.
15
+ // - Validates/normalizes system fields under the `_mm_*` namespace.
16
+ // - Allows arbitrary non-system fields through (exposed to templates as `_fields_`).
17
+ // - Rejects unknown `_mm_*` keys (except `_mm_file*` attachment field names).
18
+ export const form_submission_schema = z
19
+ .object({
20
+ _mm_form_key: z
21
+ .string()
22
+ .min(1)
23
+ .describe('Required. Public form key identifying which form configuration to use.'),
24
+ _mm_locale: z
25
+ .string()
26
+ .optional()
27
+ .default('')
28
+ .describe('Optional locale hint used when rendering and for recipient resolution.'),
29
+ _mm_recipients: z
30
+ .union([z.string(), z.array(z.string())])
31
+ .optional()
32
+ .describe('Optional list of recipient idnames (array) or comma-separated string. Recipients are resolved server-side.'),
33
+ // Common fields used to derive Reply-To (optional; no defaults).
34
+ email: optionalStringish.describe('Optional submitter email used to derive Reply-To.'),
35
+ name: optionalStringish.describe('Optional submitter name used to derive Reply-To.'),
36
+ first_name: optionalStringish.describe('Optional submitter first name used to derive Reply-To.'),
37
+ last_name: optionalStringish.describe('Optional submitter last name used to derive Reply-To.'),
38
+ // Provider-native CAPTCHA token field names (accepted as-is; not part of the `_mm_*` namespace).
39
+ 'cf-turnstile-response': optionalStringish.describe('Cloudflare Turnstile token (accepted as-is).'),
40
+ 'h-captcha-response': optionalStringish.describe('hCaptcha token (accepted as-is).'),
41
+ 'g-recaptcha-response': optionalStringish.describe('Google reCAPTCHA token (accepted as-is).'),
42
+ captcha: optionalStringish.describe('Generic/legacy captcha token field (accepted as-is).')
43
+ })
44
+ .passthrough()
45
+ .superRefine((obj, ctx) => {
46
+ for (const key of Object.keys(obj)) {
47
+ if (!key.startsWith('_mm_')) {
48
+ continue;
49
+ }
50
+ if (key.startsWith('_mm_file')) {
51
+ // Files arrive in req.files, but allow clients to pass harmless metadata fields.
52
+ continue;
53
+ }
54
+ if (!ALLOWED_MM_KEYS.has(key)) {
55
+ ctx.addIssue({
56
+ code: z.ZodIssueCode.custom,
57
+ message: `Unknown system field "${key}"`
58
+ });
59
+ }
60
+ }
61
+ })
62
+ .describe('Public form submission payload. System fields must be `_mm_*`. All other fields are treated as user fields and exposed to templates as `_fields_`.');
63
+ export function parseFormSubmissionInput(raw) {
64
+ const body = asRecord(raw);
65
+ // Enforce that system params are _mm_* only (except provider captcha fields).
66
+ // We intentionally do not accept non-_mm aliases for system params.
67
+ const mm_form_key = getBodyValue(body, '_mm_form_key');
68
+ const mm_locale = getBodyValue(body, '_mm_locale');
69
+ const mm_recipients = body._mm_recipients;
70
+ const mm_captcha_token = getCaptchaTokenFromBody(body);
71
+ const parsed = form_submission_schema.parse({
72
+ ...body,
73
+ _mm_form_key: mm_form_key,
74
+ _mm_locale: mm_locale,
75
+ _mm_recipients: mm_recipients
76
+ });
77
+ const { _mm_form_key, _mm_locale, _mm_recipients, ...rest } = parsed;
78
+ // Expose non-system fields to templates. Keep all non-`_mm_*` keys verbatim.
79
+ const fields = {};
80
+ for (const [key, value] of Object.entries(rest)) {
81
+ if (key.startsWith('_mm_')) {
82
+ continue;
83
+ }
84
+ fields[key] = value;
85
+ }
86
+ return {
87
+ mm: {
88
+ form_key: _mm_form_key,
89
+ locale: _mm_locale,
90
+ captcha_token: mm_captcha_token,
91
+ recipients_raw: _mm_recipients
92
+ },
93
+ fields
94
+ };
95
+ }