@technomoron/mail-magic 1.0.40 → 1.0.41
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/CHANGES +23 -2
- package/README.md +5 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -1
- package/dist/esm/api/assets.d.ts +9 -0
- package/dist/esm/api/auth.d.ts +2 -0
- package/dist/esm/api/auth.js +17 -8
- package/dist/esm/api/forms.d.ts +9 -0
- package/dist/esm/api/forms.js +7 -4
- package/dist/esm/api/mailer.d.ts +11 -0
- package/dist/esm/api/mailer.js +4 -4
- package/dist/esm/bin/mail-magic.d.ts +2 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/models/db.d.ts +5 -0
- package/dist/esm/models/domain.d.ts +24 -0
- package/dist/esm/models/form.d.ts +50 -0
- package/dist/esm/models/form.js +16 -13
- package/dist/esm/models/init.d.ts +12 -0
- package/dist/esm/models/recipient.d.ts +24 -0
- package/dist/esm/models/txmail.d.ts +42 -0
- package/dist/esm/models/user.d.ts +33 -0
- package/dist/esm/server.d.ts +8 -0
- package/dist/esm/store/envloader.d.ts +188 -0
- package/dist/esm/store/envloader.js +9 -4
- package/dist/esm/store/store.d.ts +37 -0
- package/dist/esm/store/store.js +1 -1
- package/dist/esm/swagger.d.ts +10 -0
- package/dist/esm/types.d.ts +32 -0
- package/dist/esm/util/captcha.d.ts +7 -0
- package/dist/esm/util/captcha.js +4 -1
- package/dist/esm/util/email.d.ts +3 -0
- package/dist/esm/util/form-replyto.d.ts +6 -0
- package/dist/esm/util/form-submission.d.ts +24 -0
- package/dist/esm/util/forms.d.ts +140 -0
- package/dist/esm/util/forms.js +17 -29
- package/dist/esm/util/paths.d.ts +15 -0
- package/dist/esm/util/paths.js +17 -0
- package/dist/esm/util/ratelimit.d.ts +16 -0
- package/dist/esm/util/ratelimit.js +6 -1
- package/dist/esm/util/route.d.ts +1 -0
- package/dist/esm/util/shared-template-flatten.d.ts +17 -0
- package/dist/esm/util/uploads.d.ts +10 -0
- package/dist/esm/util/utils.d.ts +27 -0
- package/dist/esm/util.d.ts +7 -0
- package/docs/swagger/openapi.json +16 -12
- package/examples/.env-dist +21 -0
- package/examples/README.md +74 -0
- package/examples/data/example.test/form-template/base.njk +4 -0
- package/examples/data/example.test/form-template/en/base.njk +1 -0
- package/examples/data/example.test/form-template/en/change-password.njk +5 -0
- package/examples/data/example.test/form-template/en/confirm-account.njk +5 -0
- package/examples/data/example.test/form-template/en/contact.njk +5 -0
- package/examples/data/example.test/form-template/en/partials/fields.njk +5 -0
- package/examples/data/example.test/form-template/en/welcome-signup.njk +5 -0
- package/examples/data/example.test/form-template/nb/base.njk +1 -0
- package/examples/data/example.test/form-template/nb/change-password.njk +5 -0
- package/examples/data/example.test/form-template/nb/confirm-account.njk +5 -0
- package/examples/data/example.test/form-template/nb/contact.njk +5 -0
- package/examples/data/example.test/form-template/nb/partials/fields.njk +5 -0
- package/examples/data/example.test/form-template/nb/welcome-signup.njk +5 -0
- package/examples/data/example.test/form-template/partials/header.njk +1 -0
- package/examples/data/example.test/tx-template/base.njk +16 -0
- package/examples/data/example.test/tx-template/en/base.njk +1 -0
- package/examples/data/example.test/tx-template/en/change-password.njk +7 -0
- package/examples/data/example.test/tx-template/en/confirm.njk +6 -0
- package/examples/data/example.test/tx-template/en/invoice.njk +8 -0
- package/examples/data/example.test/tx-template/en/partials/header.njk +1 -0
- package/examples/data/example.test/tx-template/en/partials/line-items.njk +14 -0
- package/examples/data/example.test/tx-template/en/receipt.njk +7 -0
- package/examples/data/example.test/tx-template/en/welcome.njk +5 -0
- package/examples/data/example.test/tx-template/nb/base.njk +1 -0
- package/examples/data/example.test/tx-template/nb/change-password.njk +6 -0
- package/examples/data/example.test/tx-template/nb/confirm.njk +6 -0
- package/examples/data/example.test/tx-template/nb/invoice.njk +7 -0
- package/examples/data/example.test/tx-template/nb/partials/header.njk +1 -0
- package/examples/data/example.test/tx-template/nb/receipt.njk +6 -0
- package/examples/data/example.test/tx-template/nb/welcome.njk +5 -0
- package/examples/data/example.test/tx-template/partials/header.njk +7 -0
- package/examples/data/init-data.json +213 -0
- package/examples/scripts/mm-api.ts +206 -0
- package/examples/scripts/public-form.ts +100 -0
- package/examples/scripts/send-messages.ts +114 -0
- package/package.json +6 -4
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export declare const envOptions: {
|
|
2
|
+
NODE_ENV: {
|
|
3
|
+
description: string;
|
|
4
|
+
options: string[];
|
|
5
|
+
default: string;
|
|
6
|
+
};
|
|
7
|
+
API_PORT: {
|
|
8
|
+
description: string;
|
|
9
|
+
default: string;
|
|
10
|
+
type: "number";
|
|
11
|
+
};
|
|
12
|
+
API_HOST: {
|
|
13
|
+
description: string;
|
|
14
|
+
default: string;
|
|
15
|
+
};
|
|
16
|
+
DB_AUTO_RELOAD: {
|
|
17
|
+
description: string;
|
|
18
|
+
type: "boolean";
|
|
19
|
+
default: false;
|
|
20
|
+
};
|
|
21
|
+
DB_FORCE_SYNC: {
|
|
22
|
+
description: string;
|
|
23
|
+
type: "boolean";
|
|
24
|
+
default: false;
|
|
25
|
+
};
|
|
26
|
+
DB_SYNC_ALTER: {
|
|
27
|
+
description: string;
|
|
28
|
+
type: "boolean";
|
|
29
|
+
default: false;
|
|
30
|
+
};
|
|
31
|
+
API_URL: {
|
|
32
|
+
description: string;
|
|
33
|
+
default: string;
|
|
34
|
+
};
|
|
35
|
+
API_BASE_PATH: {
|
|
36
|
+
description: string;
|
|
37
|
+
default: string;
|
|
38
|
+
};
|
|
39
|
+
ASSET_PUBLIC_BASE: {
|
|
40
|
+
description: string;
|
|
41
|
+
default: string;
|
|
42
|
+
};
|
|
43
|
+
SWAGGER_ENABLED: {
|
|
44
|
+
description: string;
|
|
45
|
+
type: "boolean";
|
|
46
|
+
default: false;
|
|
47
|
+
};
|
|
48
|
+
SWAGGER_PATH: {
|
|
49
|
+
description: string;
|
|
50
|
+
default: string;
|
|
51
|
+
};
|
|
52
|
+
ADMIN_ENABLED: {
|
|
53
|
+
description: string;
|
|
54
|
+
default: false;
|
|
55
|
+
type: "boolean";
|
|
56
|
+
};
|
|
57
|
+
ADMIN_APP_PATH: {
|
|
58
|
+
description: string;
|
|
59
|
+
default: string;
|
|
60
|
+
};
|
|
61
|
+
ASSET_ROUTE: {
|
|
62
|
+
description: string;
|
|
63
|
+
default: string;
|
|
64
|
+
};
|
|
65
|
+
CONFIG_PATH: {
|
|
66
|
+
description: string;
|
|
67
|
+
default: string;
|
|
68
|
+
};
|
|
69
|
+
GEN_ENV_TEMPLATE: {
|
|
70
|
+
description: string;
|
|
71
|
+
default: false;
|
|
72
|
+
type: "boolean";
|
|
73
|
+
};
|
|
74
|
+
DB_USER: {
|
|
75
|
+
description: string;
|
|
76
|
+
};
|
|
77
|
+
DB_PASS: {
|
|
78
|
+
description: string;
|
|
79
|
+
};
|
|
80
|
+
DB_NAME: {
|
|
81
|
+
description: string;
|
|
82
|
+
default: string;
|
|
83
|
+
};
|
|
84
|
+
DB_HOST: {
|
|
85
|
+
description: string;
|
|
86
|
+
default: string;
|
|
87
|
+
};
|
|
88
|
+
DB_TYPE: {
|
|
89
|
+
description: string;
|
|
90
|
+
options: string[];
|
|
91
|
+
default: string;
|
|
92
|
+
};
|
|
93
|
+
DB_LOG: {
|
|
94
|
+
description: string;
|
|
95
|
+
default: string;
|
|
96
|
+
type: "boolean";
|
|
97
|
+
};
|
|
98
|
+
DEBUG: {
|
|
99
|
+
description: string;
|
|
100
|
+
default: false;
|
|
101
|
+
type: "boolean";
|
|
102
|
+
};
|
|
103
|
+
AUTOESCAPE_HTML: {
|
|
104
|
+
description: string;
|
|
105
|
+
default: true;
|
|
106
|
+
type: "boolean";
|
|
107
|
+
};
|
|
108
|
+
SMTP_HOST: {
|
|
109
|
+
description: string;
|
|
110
|
+
default: string;
|
|
111
|
+
};
|
|
112
|
+
SMTP_PORT: {
|
|
113
|
+
description: string;
|
|
114
|
+
default: number;
|
|
115
|
+
type: "number";
|
|
116
|
+
};
|
|
117
|
+
SMTP_SECURE: {
|
|
118
|
+
description: string;
|
|
119
|
+
default: false;
|
|
120
|
+
type: "boolean";
|
|
121
|
+
};
|
|
122
|
+
SMTP_TLS_REJECT: {
|
|
123
|
+
description: string;
|
|
124
|
+
default: true;
|
|
125
|
+
type: "boolean";
|
|
126
|
+
};
|
|
127
|
+
SMTP_REQUIRE_TLS: {
|
|
128
|
+
description: string;
|
|
129
|
+
default: true;
|
|
130
|
+
type: "boolean";
|
|
131
|
+
};
|
|
132
|
+
SMTP_USER: {
|
|
133
|
+
description: string;
|
|
134
|
+
default: string;
|
|
135
|
+
};
|
|
136
|
+
SMTP_PASSWORD: {
|
|
137
|
+
description: string;
|
|
138
|
+
default: string;
|
|
139
|
+
};
|
|
140
|
+
UPLOAD_PATH: {
|
|
141
|
+
description: string;
|
|
142
|
+
default: string;
|
|
143
|
+
};
|
|
144
|
+
UPLOAD_MAX: {
|
|
145
|
+
description: string;
|
|
146
|
+
default: number;
|
|
147
|
+
type: "number";
|
|
148
|
+
};
|
|
149
|
+
FORM_RATE_LIMIT_WINDOW_SEC: {
|
|
150
|
+
description: string;
|
|
151
|
+
default: number;
|
|
152
|
+
type: "number";
|
|
153
|
+
};
|
|
154
|
+
FORM_RATE_LIMIT_MAX: {
|
|
155
|
+
description: string;
|
|
156
|
+
default: number;
|
|
157
|
+
type: "number";
|
|
158
|
+
};
|
|
159
|
+
FORM_MAX_ATTACHMENTS: {
|
|
160
|
+
description: string;
|
|
161
|
+
default: number;
|
|
162
|
+
type: "number";
|
|
163
|
+
};
|
|
164
|
+
FORM_KEEP_UPLOADS: {
|
|
165
|
+
description: string;
|
|
166
|
+
default: true;
|
|
167
|
+
type: "boolean";
|
|
168
|
+
};
|
|
169
|
+
FORM_CAPTCHA_PROVIDER: {
|
|
170
|
+
description: string;
|
|
171
|
+
options: string[];
|
|
172
|
+
default: string;
|
|
173
|
+
};
|
|
174
|
+
FORM_CAPTCHA_SECRET: {
|
|
175
|
+
description: string;
|
|
176
|
+
default: string;
|
|
177
|
+
};
|
|
178
|
+
FORM_CAPTCHA_REQUIRED: {
|
|
179
|
+
description: string;
|
|
180
|
+
default: false;
|
|
181
|
+
type: "boolean";
|
|
182
|
+
};
|
|
183
|
+
API_TOKEN_PEPPER: {
|
|
184
|
+
description: string;
|
|
185
|
+
required: true;
|
|
186
|
+
transform: (raw: string) => string;
|
|
187
|
+
};
|
|
188
|
+
};
|
|
@@ -27,7 +27,7 @@ export const envOptions = defineEnvOptions({
|
|
|
27
27
|
DB_SYNC_ALTER: {
|
|
28
28
|
description: 'Alter existing tables on startup to match models (requires write access to the DB)',
|
|
29
29
|
type: 'boolean',
|
|
30
|
-
default:
|
|
30
|
+
default: false
|
|
31
31
|
},
|
|
32
32
|
API_URL: {
|
|
33
33
|
description: 'Sets the public URL for the API (i.e. https://ml.example.com:3790)',
|
|
@@ -88,7 +88,7 @@ export const envOptions = defineEnvOptions({
|
|
|
88
88
|
},
|
|
89
89
|
DB_TYPE: {
|
|
90
90
|
description: 'Database type for the API database',
|
|
91
|
-
options: ['sqlite'],
|
|
91
|
+
options: ['sqlite', 'mysql', 'postgres'],
|
|
92
92
|
default: 'sqlite'
|
|
93
93
|
},
|
|
94
94
|
DB_LOG: {
|
|
@@ -121,8 +121,13 @@ export const envOptions = defineEnvOptions({
|
|
|
121
121
|
type: 'boolean'
|
|
122
122
|
},
|
|
123
123
|
SMTP_TLS_REJECT: {
|
|
124
|
-
description: '
|
|
125
|
-
default:
|
|
124
|
+
description: 'Validate the SMTP server TLS certificate. Set false to allow self-signed certificates.',
|
|
125
|
+
default: true,
|
|
126
|
+
type: 'boolean'
|
|
127
|
+
},
|
|
128
|
+
SMTP_REQUIRE_TLS: {
|
|
129
|
+
description: 'Require STARTTLS upgrade on SMTP connection. Set false for servers that do not support STARTTLS (e.g. MailHog).',
|
|
130
|
+
default: true,
|
|
126
131
|
type: 'boolean'
|
|
127
132
|
},
|
|
128
133
|
SMTP_USER: {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { envConfig } from '@technomoron/env-loader';
|
|
2
|
+
import { Transporter } from 'nodemailer';
|
|
3
|
+
import { Sequelize } from 'sequelize';
|
|
4
|
+
import { envOptions } from './envloader.js';
|
|
5
|
+
import type SMTPTransport from 'nodemailer/lib/smtp-transport';
|
|
6
|
+
type UploadedFile = {
|
|
7
|
+
path: string;
|
|
8
|
+
filename?: string;
|
|
9
|
+
destination?: string;
|
|
10
|
+
};
|
|
11
|
+
export type MailStoreVars = envConfig<typeof envOptions>;
|
|
12
|
+
type AutoReloadHandle = {
|
|
13
|
+
close: () => void;
|
|
14
|
+
};
|
|
15
|
+
type AutoReloadContext = {
|
|
16
|
+
vars: Pick<MailStoreVars, 'DB_AUTO_RELOAD'>;
|
|
17
|
+
config_filename: (name: string) => string;
|
|
18
|
+
print_debug: (msg: string) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function enableInitDataAutoReload(ctx: AutoReloadContext, reload: () => void): AutoReloadHandle | null;
|
|
21
|
+
export declare class mailStore {
|
|
22
|
+
private env;
|
|
23
|
+
vars: MailStoreVars;
|
|
24
|
+
transport?: Transporter<SMTPTransport.SentMessageInfo>;
|
|
25
|
+
api_db: Sequelize | null;
|
|
26
|
+
configpath: string;
|
|
27
|
+
uploadTemplate?: string;
|
|
28
|
+
uploadStagingPath?: string;
|
|
29
|
+
autoReloadHandle: AutoReloadHandle | null;
|
|
30
|
+
print_debug(msg: string): void;
|
|
31
|
+
config_filename(name: string): string;
|
|
32
|
+
resolveUploadPath(domainName?: string): string;
|
|
33
|
+
getUploadStagingPath(): string;
|
|
34
|
+
relocateUploads(domainName: string | null, files: UploadedFile[]): Promise<void>;
|
|
35
|
+
init(overrides?: Partial<MailStoreVars>): Promise<this>;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
package/dist/esm/store/store.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { mailApiServer } from './server.js';
|
|
2
|
+
type SwaggerInstallOptions = {
|
|
3
|
+
apiBasePath: string;
|
|
4
|
+
assetRoute: string;
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
swaggerEnabled?: boolean;
|
|
7
|
+
swaggerPath?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function installMailMagicSwagger(server: mailApiServer, opts: SwaggerInstallOptions): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ApiRequest } from '@technomoron/api-server-base';
|
|
2
|
+
import { api_domain } from './models/domain.js';
|
|
3
|
+
import { api_user } from './models/user.js';
|
|
4
|
+
export interface mailApiKey {
|
|
5
|
+
uid: number;
|
|
6
|
+
}
|
|
7
|
+
export interface mailApiRequest extends ApiRequest {
|
|
8
|
+
user?: api_user;
|
|
9
|
+
domain?: api_domain;
|
|
10
|
+
locale?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface formType {
|
|
13
|
+
rcpt: string;
|
|
14
|
+
sender: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
template: string;
|
|
17
|
+
}
|
|
18
|
+
export interface StoredFile {
|
|
19
|
+
filename: string;
|
|
20
|
+
path: string;
|
|
21
|
+
cid?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RequestMeta {
|
|
24
|
+
client_ip: string;
|
|
25
|
+
received_at: string;
|
|
26
|
+
ip_chain: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface UploadedFile {
|
|
29
|
+
originalname: string;
|
|
30
|
+
path: string;
|
|
31
|
+
fieldname: string;
|
|
32
|
+
}
|
package/dist/esm/util/captcha.js
CHANGED
|
@@ -4,7 +4,10 @@ export async function verifyCaptcha(params) {
|
|
|
4
4
|
hcaptcha: 'https://hcaptcha.com/siteverify',
|
|
5
5
|
recaptcha: 'https://www.google.com/recaptcha/api/siteverify'
|
|
6
6
|
};
|
|
7
|
-
const endpoint = endpoints[params.provider]
|
|
7
|
+
const endpoint = endpoints[params.provider];
|
|
8
|
+
if (!endpoint) {
|
|
9
|
+
throw new Error(`Unknown CAPTCHA provider: "${params.provider}"`);
|
|
10
|
+
}
|
|
8
11
|
const body = new URLSearchParams();
|
|
9
12
|
body.set('secret', params.secret);
|
|
10
13
|
body.set('response', params.token);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const form_submission_schema: z.ZodObject<{
|
|
3
|
+
_mm_form_key: z.ZodString;
|
|
4
|
+
_mm_locale: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
5
|
+
_mm_recipients: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
6
|
+
email: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
7
|
+
name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
8
|
+
first_name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
9
|
+
last_name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
10
|
+
'cf-turnstile-response': z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
11
|
+
'h-captcha-response': z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
12
|
+
'g-recaptcha-response': z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
13
|
+
captcha: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
14
|
+
}, z.core.$loose>;
|
|
15
|
+
export type ParsedFormSubmission = {
|
|
16
|
+
mm: {
|
|
17
|
+
form_key: string;
|
|
18
|
+
locale: string;
|
|
19
|
+
captcha_token: string;
|
|
20
|
+
recipients_raw: unknown;
|
|
21
|
+
};
|
|
22
|
+
fields: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
export declare function parseFormSubmissionInput(raw: unknown): ParsedFormSubmission;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ApiRequest } from '@technomoron/api-server-base';
|
|
2
|
+
import { api_form } from '../models/form.js';
|
|
3
|
+
import { api_recipient } from '../models/recipient.js';
|
|
4
|
+
import { ParsedFormSubmission } from './form-submission.js';
|
|
5
|
+
import type { api_domain } from '../models/domain.js';
|
|
6
|
+
import type { api_user } from '../models/user.js';
|
|
7
|
+
import type { RequestMeta, UploadedFile } from '../types.js';
|
|
8
|
+
export declare function parsePublicSubmissionOrThrow(apireq: ApiRequest): ParsedFormSubmission;
|
|
9
|
+
export declare function enforceAttachmentPolicy(env: {
|
|
10
|
+
FORM_MAX_ATTACHMENTS: number;
|
|
11
|
+
}, rawFiles: UploadedFile[]): void;
|
|
12
|
+
export declare function filterSubmissionFields(rawFields: Record<string, unknown>, allowedFields: unknown): Record<string, unknown>;
|
|
13
|
+
export declare function enforceCaptchaPolicy(params: {
|
|
14
|
+
vars: {
|
|
15
|
+
FORM_CAPTCHA_REQUIRED: boolean;
|
|
16
|
+
FORM_CAPTCHA_SECRET: string;
|
|
17
|
+
FORM_CAPTCHA_PROVIDER: string;
|
|
18
|
+
};
|
|
19
|
+
form: {
|
|
20
|
+
captcha_required: boolean;
|
|
21
|
+
};
|
|
22
|
+
captchaToken: string;
|
|
23
|
+
clientIp: string;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
export declare function buildReplyToValue(form: {
|
|
26
|
+
replyto_email: string;
|
|
27
|
+
replyto_from_fields: boolean;
|
|
28
|
+
}, fields: Record<string, unknown>): (string | {
|
|
29
|
+
name: string;
|
|
30
|
+
address: string;
|
|
31
|
+
}) | undefined;
|
|
32
|
+
export declare function parseIdnameList(value: unknown, field: string): string[];
|
|
33
|
+
export type FormRecipientPayload = {
|
|
34
|
+
idnameRaw: string;
|
|
35
|
+
emailRaw: string;
|
|
36
|
+
nameRaw: string;
|
|
37
|
+
formKeyRaw: string;
|
|
38
|
+
formid: string;
|
|
39
|
+
localeRaw: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function parseRecipientPayload(body: Record<string, unknown>): FormRecipientPayload;
|
|
42
|
+
export declare function normalizeRecipientIdname(raw: string): string;
|
|
43
|
+
export declare function normalizeRecipientEmail(raw: string): {
|
|
44
|
+
email: string;
|
|
45
|
+
mailbox: {
|
|
46
|
+
address: string;
|
|
47
|
+
name?: string | null;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export declare function normalizeRecipientName(raw: string, mailboxName?: string | null): string;
|
|
51
|
+
export declare function resolveFormKeyForRecipient(params: {
|
|
52
|
+
formKeyRaw: string;
|
|
53
|
+
formid: string;
|
|
54
|
+
localeRaw: string;
|
|
55
|
+
user: api_user;
|
|
56
|
+
domain: api_domain;
|
|
57
|
+
}): Promise<string>;
|
|
58
|
+
export declare function parseAllowedFields(raw: unknown): string[];
|
|
59
|
+
export type FormTemplateInput = {
|
|
60
|
+
template: string;
|
|
61
|
+
sender: string;
|
|
62
|
+
recipient: string;
|
|
63
|
+
idname: string;
|
|
64
|
+
subject: string;
|
|
65
|
+
locale: string;
|
|
66
|
+
secret: string;
|
|
67
|
+
replyto_email: string;
|
|
68
|
+
replyto_from_fields: boolean;
|
|
69
|
+
allowed_fields: string[];
|
|
70
|
+
captcha_required: boolean;
|
|
71
|
+
};
|
|
72
|
+
export declare function parseFormTemplatePayload(body: Record<string, unknown>): FormTemplateInput;
|
|
73
|
+
export declare function validateFormTemplatePayload(payload: FormTemplateInput): void;
|
|
74
|
+
export declare function buildFormTemplatePaths(params: {
|
|
75
|
+
user: api_user;
|
|
76
|
+
domain: api_domain;
|
|
77
|
+
idname: string;
|
|
78
|
+
locale: string;
|
|
79
|
+
}): {
|
|
80
|
+
localeSlug: string;
|
|
81
|
+
slug: string;
|
|
82
|
+
filename: string;
|
|
83
|
+
};
|
|
84
|
+
export declare function resolveFormKeyForTemplate(params: {
|
|
85
|
+
user_id: number;
|
|
86
|
+
domain_id: number;
|
|
87
|
+
locale: string;
|
|
88
|
+
idname: string;
|
|
89
|
+
}): Promise<string>;
|
|
90
|
+
export declare function buildFormTemplateRecord(params: {
|
|
91
|
+
form_key: string;
|
|
92
|
+
user_id: number;
|
|
93
|
+
domain_id: number;
|
|
94
|
+
locale: string;
|
|
95
|
+
slug: string;
|
|
96
|
+
filename: string;
|
|
97
|
+
payload: FormTemplateInput;
|
|
98
|
+
}): {
|
|
99
|
+
form_key: string;
|
|
100
|
+
user_id: number;
|
|
101
|
+
domain_id: number;
|
|
102
|
+
locale: string;
|
|
103
|
+
idname: string;
|
|
104
|
+
sender: string;
|
|
105
|
+
recipient: string;
|
|
106
|
+
subject: string;
|
|
107
|
+
template: string;
|
|
108
|
+
slug: string;
|
|
109
|
+
filename: string;
|
|
110
|
+
secret: string;
|
|
111
|
+
replyto_email: string;
|
|
112
|
+
replyto_from_fields: boolean;
|
|
113
|
+
allowed_fields: string[];
|
|
114
|
+
captcha_required: boolean;
|
|
115
|
+
files: never[];
|
|
116
|
+
};
|
|
117
|
+
export declare function resolveRecipients(form: api_form, recipientsRaw: unknown): Promise<api_recipient[]>;
|
|
118
|
+
export declare function buildRecipientTo(form: api_form, recipients: api_recipient[]): string | (string | {
|
|
119
|
+
name: string;
|
|
120
|
+
address: string;
|
|
121
|
+
})[];
|
|
122
|
+
export declare function getPrimaryRecipientInfo(form: api_form, recipients: api_recipient[]): {
|
|
123
|
+
rcptEmail: string;
|
|
124
|
+
rcptName: string;
|
|
125
|
+
rcptIdname: string;
|
|
126
|
+
rcptIdnames: string[];
|
|
127
|
+
};
|
|
128
|
+
export declare function buildSubmissionContext(params: {
|
|
129
|
+
form_key: string;
|
|
130
|
+
localeRaw: string;
|
|
131
|
+
recipients: string[];
|
|
132
|
+
rcptEmail: string;
|
|
133
|
+
rcptName: string;
|
|
134
|
+
rcptIdname: string;
|
|
135
|
+
rcptIdnames: string[];
|
|
136
|
+
attachmentMap: Record<string, string>;
|
|
137
|
+
fields: Record<string, unknown>;
|
|
138
|
+
files: UploadedFile[];
|
|
139
|
+
meta: RequestMeta;
|
|
140
|
+
}): Record<string, unknown>;
|
package/dist/esm/util/forms.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
1
|
import { ApiError } from '@technomoron/api-server-base';
|
|
3
2
|
import { api_form } from '../models/form.js';
|
|
4
3
|
import { api_recipient } from '../models/recipient.js';
|
|
@@ -6,6 +5,7 @@ import { verifyCaptcha } from './captcha.js';
|
|
|
6
5
|
import { parseMailbox } from './email.js';
|
|
7
6
|
import { extractReplyToFromSubmission } from './form-replyto.js';
|
|
8
7
|
import { parseFormSubmissionInput } from './form-submission.js';
|
|
8
|
+
import { buildFormSlugAndFilename } from './paths.js';
|
|
9
9
|
import { normalizeBoolean, normalizeSlug } from './utils.js';
|
|
10
10
|
export function parsePublicSubmissionOrThrow(apireq) {
|
|
11
11
|
try {
|
|
@@ -297,36 +297,24 @@ export function validateFormTemplatePayload(payload) {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
export function buildFormTemplatePaths(params) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
filenameParts.push(formSlug);
|
|
309
|
-
let filename = path.join(...filenameParts);
|
|
310
|
-
if (!filename.endsWith('.njk')) {
|
|
311
|
-
filename += '.njk';
|
|
312
|
-
}
|
|
313
|
-
return { localeSlug, slug, filename };
|
|
300
|
+
return buildFormSlugAndFilename({
|
|
301
|
+
domainName: params.domain.name,
|
|
302
|
+
domainLocale: params.domain.locale,
|
|
303
|
+
userLocale: params.user.locale,
|
|
304
|
+
idname: params.idname,
|
|
305
|
+
locale: params.locale
|
|
306
|
+
});
|
|
314
307
|
}
|
|
315
308
|
export async function resolveFormKeyForTemplate(params) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return existing?.form_key || '';
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
return '';
|
|
329
|
-
}
|
|
309
|
+
const existing = await api_form.findOne({
|
|
310
|
+
where: {
|
|
311
|
+
user_id: params.user_id,
|
|
312
|
+
domain_id: params.domain_id,
|
|
313
|
+
locale: params.locale,
|
|
314
|
+
idname: params.idname
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return existing?.form_key || '';
|
|
330
318
|
}
|
|
331
319
|
export function buildFormTemplateRecord(params) {
|
|
332
320
|
return {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const SEGMENT_PATTERN: RegExp;
|
|
2
|
+
export declare function normalizeSubdir(value: string): string;
|
|
3
|
+
export declare function assertSafeRelativePath(filename: string, label: string): string;
|
|
4
|
+
export declare function buildFormSlugAndFilename(params: {
|
|
5
|
+
domainName: string;
|
|
6
|
+
domainLocale: string;
|
|
7
|
+
userLocale: string;
|
|
8
|
+
idname: string;
|
|
9
|
+
locale: string;
|
|
10
|
+
}): {
|
|
11
|
+
localeSlug: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function buildAssetUrl(baseUrl: string, route: string, domainName: string, assetPath: string): string;
|
package/dist/esm/util/paths.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { ApiError } from '@technomoron/api-server-base';
|
|
3
|
+
import { normalizeSlug } from './utils.js';
|
|
3
4
|
export const SEGMENT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
4
5
|
export function normalizeSubdir(value) {
|
|
5
6
|
if (!value) {
|
|
@@ -27,6 +28,22 @@ export function assertSafeRelativePath(filename, label) {
|
|
|
27
28
|
}
|
|
28
29
|
return normalized;
|
|
29
30
|
}
|
|
31
|
+
export function buildFormSlugAndFilename(params) {
|
|
32
|
+
const domainSlug = normalizeSlug(params.domainName);
|
|
33
|
+
const formSlug = normalizeSlug(params.idname);
|
|
34
|
+
const localeSlug = normalizeSlug(params.locale || params.domainLocale || params.userLocale || '');
|
|
35
|
+
const slug = `${domainSlug}${localeSlug ? '-' + localeSlug : ''}-${formSlug}`;
|
|
36
|
+
const filenameParts = [domainSlug, 'form-template'];
|
|
37
|
+
if (localeSlug) {
|
|
38
|
+
filenameParts.push(localeSlug);
|
|
39
|
+
}
|
|
40
|
+
filenameParts.push(formSlug);
|
|
41
|
+
let filename = path.join(...filenameParts);
|
|
42
|
+
if (!filename.endsWith('.njk')) {
|
|
43
|
+
filename += '.njk';
|
|
44
|
+
}
|
|
45
|
+
return { localeSlug, slug, filename };
|
|
46
|
+
}
|
|
30
47
|
export function buildAssetUrl(baseUrl, route, domainName, assetPath) {
|
|
31
48
|
const trimmedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
32
49
|
const normalizedRoute = route ? (route.startsWith('/') ? route : `/${route}`) : '';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ApiRequest } from '@technomoron/api-server-base';
|
|
2
|
+
export type RateLimitDecision = {
|
|
3
|
+
allowed: boolean;
|
|
4
|
+
retryAfterSec: number;
|
|
5
|
+
};
|
|
6
|
+
export declare class FixedWindowRateLimiter {
|
|
7
|
+
private readonly maxKeys;
|
|
8
|
+
private readonly buckets;
|
|
9
|
+
constructor(maxKeys?: number);
|
|
10
|
+
check(key: string, max: number, windowMs: number): RateLimitDecision;
|
|
11
|
+
private prune;
|
|
12
|
+
}
|
|
13
|
+
export declare function enforceFormRateLimit(limiter: FixedWindowRateLimiter, env: {
|
|
14
|
+
FORM_RATE_LIMIT_WINDOW_SEC: number;
|
|
15
|
+
FORM_RATE_LIMIT_MAX: number;
|
|
16
|
+
}, apireq: ApiRequest): void;
|
|
@@ -39,8 +39,13 @@ export class FixedWindowRateLimiter {
|
|
|
39
39
|
}
|
|
40
40
|
export function enforceFormRateLimit(limiter, env, apireq) {
|
|
41
41
|
const clientIp = apireq.getClientIp() ?? '';
|
|
42
|
+
if (!clientIp) {
|
|
43
|
+
// Cannot rate-limit without a resolvable client IP; skip to avoid collapsing
|
|
44
|
+
// all IP-unknown requests into a single shared bucket.
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
42
47
|
const windowMs = Math.max(0, env.FORM_RATE_LIMIT_WINDOW_SEC) * 1000;
|
|
43
|
-
const decision = limiter.check(`form-message:${clientIp
|
|
48
|
+
const decision = limiter.check(`form-message:${clientIp}`, env.FORM_RATE_LIMIT_MAX, windowMs);
|
|
44
49
|
if (!decision.allowed) {
|
|
45
50
|
apireq.res.set('Retry-After', String(decision.retryAfterSec));
|
|
46
51
|
throw new ApiError({ code: 429, message: 'Too many form submissions; try again later' });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeRoute(value: string, fallback?: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type FlattenedAsset = {
|
|
2
|
+
filename: string;
|
|
3
|
+
path: string;
|
|
4
|
+
cid?: string;
|
|
5
|
+
};
|
|
6
|
+
export type FlattenWithAssetsOptions = {
|
|
7
|
+
domainRoot: string;
|
|
8
|
+
templateKey: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
assetFormatter: (urlPath: string) => string;
|
|
11
|
+
normalizeInlineCid?: (urlPath: string) => string;
|
|
12
|
+
};
|
|
13
|
+
export type FlattenWithAssetsResult = {
|
|
14
|
+
html: string;
|
|
15
|
+
assets: FlattenedAsset[];
|
|
16
|
+
};
|
|
17
|
+
export declare function flattenTemplateWithAssets(options: FlattenWithAssetsOptions): FlattenWithAssetsResult;
|