@technomoron/mail-magic 1.0.42 → 1.0.44
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 +24 -3
- package/README.md +1 -1
- package/dist/esm/api/assets.js +19 -4
- package/dist/esm/api/auth.js +1 -1
- package/dist/esm/api/forms.js +8 -0
- package/dist/esm/api/mailer.js +37 -4
- package/dist/esm/models/form.js +1 -2
- package/dist/esm/models/init.js +2 -2
- package/dist/esm/models/txmail.js +4 -4
- package/dist/esm/models/user.js +1 -1
- package/dist/esm/store/store.d.ts +1 -1
- package/dist/esm/store/store.js +17 -6
- package/dist/esm/swagger.js +2 -2
- package/dist/esm/util/form-replyto.js +5 -14
- package/dist/esm/util/forms.js +0 -1
- package/dist/esm/util/paths.d.ts +0 -1
- package/dist/esm/util/paths.js +4 -1
- package/dist/esm/util/utils.d.ts +7 -0
- package/dist/esm/util/utils.js +7 -0
- package/docs/tutorial.md +1 -1
- package/package.json +1 -1
package/CHANGES
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
CHANGES
|
|
2
2
|
=======
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
4
|
+
Version 1.0.44 (2026-03-05)
|
|
5
|
+
|
|
6
|
+
- fix(autoreload): catch unhandled promise rejections from async `reload()` in `enableInitDataAutoReload` `onChange` callback.
|
|
7
|
+
- fix(mailer): validate that parsed `vars` is a non-null, non-array object after `JSON.parse`; return 400 for invalid types.
|
|
8
|
+
- docs(utils): add doc comment to `buildRequestMeta` clarifying it is informational and requires a trusted reverse proxy for reliable IP data.
|
|
9
|
+
- (Changes generated/assisted by Claude Code (profile: anthropic-claude-opus-4-6/high).)
|
|
10
|
+
|
|
11
|
+
Version 1.0.43 (2026-03-04)
|
|
12
|
+
|
|
13
|
+
- fix(security): add allowlist for custom email headers on `/v1/tx/message` to prevent header injection via arbitrary keys (e.g. `Bcc`, `From`, `Sender`).
|
|
14
|
+
- fix(security): sanitize Swagger error response to avoid leaking internal filesystem paths.
|
|
15
|
+
- fix(security): reject `..` and `.` path segments explicitly in `normalizeSubdir`.
|
|
16
|
+
- fix(forms): return 500 error instead of silent success when all `form_key` generation attempts are exhausted.
|
|
17
|
+
- fix(mailer): guard `transport` with null check instead of non-null assertion; return 503 when transport is unavailable.
|
|
18
|
+
- fix(mailer): stop leaking internal error details (SMTP errors, file paths) in `/v1/tx/message` error responses.
|
|
19
|
+
- fix(autoreload): debounce `fs.watch` callback (300ms) to prevent reload storms from rapid file change events.
|
|
20
|
+
- refactor(form-replyto): replace duplicated `getFirstStringField` with shared `getBodyValue` from utils.
|
|
21
|
+
- docs(readme): fix `rcpt` example from JSON array to comma-separated string to match actual API contract.
|
|
22
|
+
- feat(locale): implement 3-step locale fallback for template lookup: request locale → domain default locale → empty locale.
|
|
23
|
+
- fix(auth): resolve default locale from domain record instead of hardcoding `'en'` when request omits `locale`.
|
|
24
|
+
- refactor(locale): remove `user.locale` from all locale resolution chains; domain locale is the sole default.
|
|
25
|
+
- docs(tutorial): replace non-existent `now().iso8601()` Nunjucks call with `default('unknown')`.
|
|
26
|
+
- test(autoreload): update store-autoreload tests to use fake timers for debounced reload.
|
|
7
27
|
- perf(forms): batch recipient resolution for `_mm_recipients` into scoped + fallback `IN` queries while preserving precedence and requested order.
|
|
8
28
|
- test(forms): add coverage for multi-recipient resolution order with scoped-over-domain recipient precedence.
|
|
9
29
|
- (Changes generated/assisted by Codex (profile: openai-gpt-5-codex/medium).)
|
|
30
|
+
- (Changes generated/assisted by Claude Code (profile: anthropic-claude-opus-4-6/high).)
|
|
10
31
|
|
|
11
32
|
Version 1.0.42 (2026-02-27)
|
|
12
33
|
|
package/README.md
CHANGED
package/dist/esm/api/assets.js
CHANGED
|
@@ -47,9 +47,17 @@ export class AssetAPI extends ApiModule {
|
|
|
47
47
|
}
|
|
48
48
|
const templateType = templateTypeRaw.toLowerCase();
|
|
49
49
|
const domainId = apireq.domain.domain_id;
|
|
50
|
+
const deflocale = apireq.domain.locale || '';
|
|
50
51
|
if (templateType === 'tx') {
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
let template = await api_txmail.findOne({ where: { name: templateName, domain_id: domainId, locale } });
|
|
53
|
+
if (!template && deflocale && deflocale !== locale) {
|
|
54
|
+
template = await api_txmail.findOne({
|
|
55
|
+
where: { name: templateName, domain_id: domainId, locale: deflocale }
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!template && locale !== '') {
|
|
59
|
+
template = await api_txmail.findOne({ where: { name: templateName, domain_id: domainId, locale: '' } });
|
|
60
|
+
}
|
|
53
61
|
if (!template) {
|
|
54
62
|
throw new ApiError({
|
|
55
63
|
code: 404,
|
|
@@ -65,8 +73,15 @@ export class AssetAPI extends ApiModule {
|
|
|
65
73
|
return path.dirname(candidate);
|
|
66
74
|
}
|
|
67
75
|
if (templateType === 'form') {
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
let form = await api_form.findOne({ where: { idname: templateName, domain_id: domainId, locale } });
|
|
77
|
+
if (!form && deflocale && deflocale !== locale) {
|
|
78
|
+
form = await api_form.findOne({
|
|
79
|
+
where: { idname: templateName, domain_id: domainId, locale: deflocale }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!form && locale !== '') {
|
|
83
|
+
form = await api_form.findOne({ where: { idname: templateName, domain_id: domainId, locale: '' } });
|
|
84
|
+
}
|
|
70
85
|
if (!form) {
|
|
71
86
|
throw new ApiError({
|
|
72
87
|
code: 404,
|
package/dist/esm/api/auth.js
CHANGED
package/dist/esm/api/forms.js
CHANGED
|
@@ -89,6 +89,7 @@ export class FormAPI extends ApiModule {
|
|
|
89
89
|
payload
|
|
90
90
|
});
|
|
91
91
|
let created = false;
|
|
92
|
+
let upserted = false;
|
|
92
93
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
93
94
|
try {
|
|
94
95
|
const [form, wasCreated] = await api_form.upsert(record, {
|
|
@@ -96,6 +97,7 @@ export class FormAPI extends ApiModule {
|
|
|
96
97
|
conflictFields: ['user_id', 'domain_id', 'locale', 'idname']
|
|
97
98
|
});
|
|
98
99
|
created = wasCreated ?? false;
|
|
100
|
+
upserted = true;
|
|
99
101
|
form_key = form.form_key || form_key;
|
|
100
102
|
this.server.storage.print_debug(`Form template upserted: ${form.idname} (created=${wasCreated})`);
|
|
101
103
|
break;
|
|
@@ -115,6 +117,9 @@ export class FormAPI extends ApiModule {
|
|
|
115
117
|
});
|
|
116
118
|
}
|
|
117
119
|
}
|
|
120
|
+
if (!upserted) {
|
|
121
|
+
throw new ApiError({ code: 500, message: 'Unable to generate a unique form_key after multiple attempts' });
|
|
122
|
+
}
|
|
118
123
|
return [200, { Status: 'OK', created, form_key }];
|
|
119
124
|
}
|
|
120
125
|
async postSendForm(apireq) {
|
|
@@ -185,6 +190,9 @@ export class FormAPI extends ApiModule {
|
|
|
185
190
|
...(replyToValue ? { replyTo: replyToValue } : {})
|
|
186
191
|
};
|
|
187
192
|
try {
|
|
193
|
+
if (!this.server.storage.transport) {
|
|
194
|
+
throw new ApiError({ code: 503, message: 'Mail transport is not available' });
|
|
195
|
+
}
|
|
188
196
|
const info = await this.server.storage.transport.sendMail(mailOptions);
|
|
189
197
|
this.server.storage.print_debug('Email sent: ' + info.response);
|
|
190
198
|
}
|
package/dist/esm/api/mailer.js
CHANGED
|
@@ -89,6 +89,9 @@ export class MailerAPI extends ApiModule {
|
|
|
89
89
|
throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
if (!parsedVars || typeof parsedVars !== 'object' || Array.isArray(parsedVars)) {
|
|
93
|
+
throw new ApiError({ code: 400, message: '"vars" must be a JSON object' });
|
|
94
|
+
}
|
|
92
95
|
const thevars = parsedVars;
|
|
93
96
|
const { valid, invalid } = this.validateEmails(rcpt);
|
|
94
97
|
if (invalid.length > 0) {
|
|
@@ -96,10 +99,18 @@ export class MailerAPI extends ApiModule {
|
|
|
96
99
|
}
|
|
97
100
|
let template = null;
|
|
98
101
|
const domain_id = apireq.domain.domain_id;
|
|
102
|
+
const deflocale = apireq.domain.locale || '';
|
|
99
103
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
// 1. Exact locale match
|
|
105
|
+
template = await api_txmail.findOne({ where: { name, domain_id, locale } });
|
|
106
|
+
// 2. Domain/user default locale (if different from request locale)
|
|
107
|
+
if (!template && deflocale && deflocale !== locale) {
|
|
108
|
+
template = await api_txmail.findOne({ where: { name, domain_id, locale: deflocale } });
|
|
109
|
+
}
|
|
110
|
+
// 3. Empty-locale fallback (if not already tried above)
|
|
111
|
+
if (!template && locale !== '') {
|
|
112
|
+
template = await api_txmail.findOne({ where: { name, domain_id, locale: '' } });
|
|
113
|
+
}
|
|
103
114
|
}
|
|
104
115
|
catch (error) {
|
|
105
116
|
throw new ApiError({
|
|
@@ -145,6 +156,19 @@ export class MailerAPI extends ApiModule {
|
|
|
145
156
|
throw new ApiError({ code: 400, message: 'Invalid reply-to email address' });
|
|
146
157
|
}
|
|
147
158
|
}
|
|
159
|
+
const ALLOWED_CUSTOM_HEADERS = new Set([
|
|
160
|
+
'x-mailer',
|
|
161
|
+
'x-priority',
|
|
162
|
+
'x-entity-ref-id',
|
|
163
|
+
'list-unsubscribe',
|
|
164
|
+
'list-unsubscribe-post',
|
|
165
|
+
'list-id',
|
|
166
|
+
'precedence',
|
|
167
|
+
'references',
|
|
168
|
+
'in-reply-to',
|
|
169
|
+
'message-id',
|
|
170
|
+
'importance'
|
|
171
|
+
]);
|
|
148
172
|
let normalizedHeaders;
|
|
149
173
|
if (headers !== undefined) {
|
|
150
174
|
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
|
|
@@ -155,6 +179,9 @@ export class MailerAPI extends ApiModule {
|
|
|
155
179
|
if (typeof value !== 'string') {
|
|
156
180
|
throw new ApiError({ code: 400, message: `headers.${key} must be a string` });
|
|
157
181
|
}
|
|
182
|
+
if (!ALLOWED_CUSTOM_HEADERS.has(key.toLowerCase())) {
|
|
183
|
+
throw new ApiError({ code: 400, message: `Header "${key}" is not allowed` });
|
|
184
|
+
}
|
|
158
185
|
normalizedHeaders[key] = value;
|
|
159
186
|
}
|
|
160
187
|
}
|
|
@@ -181,14 +208,20 @@ export class MailerAPI extends ApiModule {
|
|
|
181
208
|
...(normalizedReplyTo ? { replyTo: normalizedReplyTo } : {}),
|
|
182
209
|
...(normalizedHeaders ? { headers: normalizedHeaders } : {})
|
|
183
210
|
};
|
|
211
|
+
if (!this.server.storage.transport) {
|
|
212
|
+
throw new ApiError({ code: 503, message: 'Mail transport is not available' });
|
|
213
|
+
}
|
|
184
214
|
await this.server.storage.transport.sendMail(sendargs);
|
|
185
215
|
}
|
|
186
216
|
return [200, { Status: 'OK', Message: 'Emails sent successfully' }];
|
|
187
217
|
}
|
|
188
218
|
catch (error) {
|
|
219
|
+
if (error instanceof ApiError) {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
189
222
|
throw new ApiError({
|
|
190
223
|
code: 500,
|
|
191
|
-
message:
|
|
224
|
+
message: 'Failed to render or send email'
|
|
192
225
|
});
|
|
193
226
|
}
|
|
194
227
|
}
|
package/dist/esm/models/form.js
CHANGED
|
@@ -231,12 +231,11 @@ export async function init_api_form(api_db) {
|
|
|
231
231
|
return api_form;
|
|
232
232
|
}
|
|
233
233
|
export async function upsert_form(record) {
|
|
234
|
-
const {
|
|
234
|
+
const { domain } = await user_and_domain(record.domain_id);
|
|
235
235
|
const dname = normalizeSlug(domain.name);
|
|
236
236
|
const { slug, filename: generatedFilename } = buildFormSlugAndFilename({
|
|
237
237
|
domainName: domain.name,
|
|
238
238
|
domainLocale: domain.locale,
|
|
239
|
-
userLocale: user.locale,
|
|
240
239
|
idname: record.idname,
|
|
241
240
|
locale: record.locale
|
|
242
241
|
});
|
package/dist/esm/models/init.js
CHANGED
|
@@ -67,12 +67,12 @@ async function _load_template(store, filename, pathname, user, domain, locale, t
|
|
|
67
67
|
}
|
|
68
68
|
export async function loadFormTemplate(store, form) {
|
|
69
69
|
const { user, domain } = await user_and_domain(form.domain_id);
|
|
70
|
-
const locale = form.locale || domain.locale ||
|
|
70
|
+
const locale = form.locale || domain.locale || null;
|
|
71
71
|
return _load_template(store, form.filename, '', user, domain, locale, 'form-template');
|
|
72
72
|
}
|
|
73
73
|
export async function loadTxTemplate(store, template) {
|
|
74
74
|
const { user, domain } = await user_and_domain(template.domain_id);
|
|
75
|
-
const locale = template.locale || domain.locale ||
|
|
75
|
+
const locale = template.locale || domain.locale || null;
|
|
76
76
|
return _load_template(store, template.filename, '', user, domain, locale, 'tx-template');
|
|
77
77
|
}
|
|
78
78
|
export async function importData(store) {
|
|
@@ -29,10 +29,10 @@ export const api_txmail_schema = z
|
|
|
29
29
|
export class api_txmail extends Model {
|
|
30
30
|
}
|
|
31
31
|
export async function upsert_txmail(record) {
|
|
32
|
-
const {
|
|
32
|
+
const { domain } = await user_and_domain(record.domain_id);
|
|
33
33
|
const dname = normalizeSlug(domain.name);
|
|
34
34
|
const name = normalizeSlug(record.name);
|
|
35
|
-
const locale = normalizeSlug(record.locale || domain.locale ||
|
|
35
|
+
const locale = normalizeSlug(record.locale || domain.locale || '');
|
|
36
36
|
if (!record.slug) {
|
|
37
37
|
record.slug = `${dname}${locale ? '-' + locale : ''}-${name}`;
|
|
38
38
|
}
|
|
@@ -157,10 +157,10 @@ export async function init_api_txmail(api_db) {
|
|
|
157
157
|
]
|
|
158
158
|
});
|
|
159
159
|
api_txmail.addHook('beforeValidate', async (template) => {
|
|
160
|
-
const {
|
|
160
|
+
const { domain } = await user_and_domain(template.domain_id);
|
|
161
161
|
const dname = normalizeSlug(domain.name);
|
|
162
162
|
const name = normalizeSlug(template.name);
|
|
163
|
-
const locale = normalizeSlug(template.locale || domain.locale ||
|
|
163
|
+
const locale = normalizeSlug(template.locale || domain.locale || '');
|
|
164
164
|
template.slug ||= `${dname}${locale ? '-' + locale : ''}-${name}`;
|
|
165
165
|
if (!template.filename) {
|
|
166
166
|
const parts = [dname, 'tx-template'];
|
package/dist/esm/models/user.js
CHANGED
|
@@ -10,7 +10,7 @@ export const api_user_schema = z
|
|
|
10
10
|
name: z.string().min(1).describe('Display name for the user.'),
|
|
11
11
|
email: z.string().email().describe('User email address.'),
|
|
12
12
|
domain: z.number().int().nonnegative().nullable().optional().describe('Default domain ID for the user.'),
|
|
13
|
-
locale: z.string().default('').describe('
|
|
13
|
+
locale: z.string().default('').describe('Reserved. Locale resolution uses the domain locale.')
|
|
14
14
|
})
|
|
15
15
|
.describe('User account record and API credentials.');
|
|
16
16
|
export class api_user extends Model {
|
|
@@ -18,7 +18,7 @@ type AutoReloadContext = {
|
|
|
18
18
|
config_filename: (name: string) => string;
|
|
19
19
|
print_debug: (msg: string) => void;
|
|
20
20
|
};
|
|
21
|
-
export declare function enableInitDataAutoReload(ctx: AutoReloadContext, reload: () => void): AutoReloadHandle | null;
|
|
21
|
+
export declare function enableInitDataAutoReload(ctx: AutoReloadContext, reload: () => void | Promise<void>): AutoReloadHandle | null;
|
|
22
22
|
export declare class mailStore {
|
|
23
23
|
private env;
|
|
24
24
|
vars: MailStoreVars;
|
package/dist/esm/store/store.js
CHANGED
|
@@ -30,14 +30,25 @@ export function enableInitDataAutoReload(ctx, reload) {
|
|
|
30
30
|
}
|
|
31
31
|
const initDataPath = ctx.config_filename('init-data.json');
|
|
32
32
|
ctx.print_debug('Enabling auto reload of init-data.json');
|
|
33
|
+
let debounceTimer = null;
|
|
33
34
|
const onChange = () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
reload();
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
ctx.print_debug(`Failed to reload config: ${err}`);
|
|
35
|
+
if (debounceTimer) {
|
|
36
|
+
clearTimeout(debounceTimer);
|
|
40
37
|
}
|
|
38
|
+
debounceTimer = setTimeout(() => {
|
|
39
|
+
debounceTimer = null;
|
|
40
|
+
ctx.print_debug('Config file changed, reloading...');
|
|
41
|
+
// reload() may be sync or async — try/catch handles a synchronous
|
|
42
|
+
// throw, while Promise.resolve().catch() handles an async rejection.
|
|
43
|
+
try {
|
|
44
|
+
Promise.resolve(reload()).catch((err) => {
|
|
45
|
+
ctx.print_debug(`Failed to reload config: ${err}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
ctx.print_debug(`Failed to reload config: ${err}`);
|
|
50
|
+
}
|
|
51
|
+
}, 300);
|
|
41
52
|
};
|
|
42
53
|
try {
|
|
43
54
|
const watcher = fs.watch(initDataPath, { persistent: false }, onChange);
|
package/dist/esm/swagger.js
CHANGED
|
@@ -54,8 +54,8 @@ function loadPackagedOpenApiSpec() {
|
|
|
54
54
|
cachedSpec = JSON.parse(raw);
|
|
55
55
|
return cachedSpec;
|
|
56
56
|
}
|
|
57
|
-
catch
|
|
58
|
-
cachedSpecError =
|
|
57
|
+
catch {
|
|
58
|
+
cachedSpecError = 'Failed to load OpenAPI spec';
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import emailAddresses from 'email-addresses';
|
|
2
|
-
|
|
3
|
-
const value = body[key];
|
|
4
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
5
|
-
return String(value[0] ?? '');
|
|
6
|
-
}
|
|
7
|
-
if (value !== undefined && value !== null) {
|
|
8
|
-
return String(value);
|
|
9
|
-
}
|
|
10
|
-
return '';
|
|
11
|
-
}
|
|
2
|
+
import { getBodyValue } from './utils.js';
|
|
12
3
|
function sanitizeHeaderValue(value, maxLen) {
|
|
13
4
|
const trimmed = String(value ?? '').trim();
|
|
14
5
|
if (!trimmed) {
|
|
@@ -21,7 +12,7 @@ function sanitizeHeaderValue(value, maxLen) {
|
|
|
21
12
|
return trimmed.slice(0, maxLen);
|
|
22
13
|
}
|
|
23
14
|
export function extractReplyToFromSubmission(body) {
|
|
24
|
-
const emailRaw = sanitizeHeaderValue(
|
|
15
|
+
const emailRaw = sanitizeHeaderValue(getBodyValue(body, 'email'), 320);
|
|
25
16
|
if (!emailRaw) {
|
|
26
17
|
return undefined;
|
|
27
18
|
}
|
|
@@ -34,10 +25,10 @@ export function extractReplyToFromSubmission(body) {
|
|
|
34
25
|
return undefined;
|
|
35
26
|
}
|
|
36
27
|
// Prefer a single "name" field, otherwise compose from first_name/last_name.
|
|
37
|
-
let name = sanitizeHeaderValue(
|
|
28
|
+
let name = sanitizeHeaderValue(getBodyValue(body, 'name'), 200);
|
|
38
29
|
if (!name) {
|
|
39
|
-
const first = sanitizeHeaderValue(
|
|
40
|
-
const last = sanitizeHeaderValue(
|
|
30
|
+
const first = sanitizeHeaderValue(getBodyValue(body, 'first_name'), 100);
|
|
31
|
+
const last = sanitizeHeaderValue(getBodyValue(body, 'last_name'), 100);
|
|
41
32
|
name = sanitizeHeaderValue(`${first}${first && last ? ' ' : ''}${last}`, 200);
|
|
42
33
|
}
|
|
43
34
|
return name ? { name, address } : address;
|
package/dist/esm/util/forms.js
CHANGED
|
@@ -301,7 +301,6 @@ export function buildFormTemplatePaths(params) {
|
|
|
301
301
|
return buildFormSlugAndFilename({
|
|
302
302
|
domainName: params.domain.name,
|
|
303
303
|
domainLocale: params.domain.locale,
|
|
304
|
-
userLocale: params.user.locale,
|
|
305
304
|
idname: params.idname,
|
|
306
305
|
locale: params.locale
|
|
307
306
|
});
|
package/dist/esm/util/paths.d.ts
CHANGED
package/dist/esm/util/paths.js
CHANGED
|
@@ -12,6 +12,9 @@ export function normalizeSubdir(value) {
|
|
|
12
12
|
}
|
|
13
13
|
const segments = cleaned.split('/').filter(Boolean);
|
|
14
14
|
for (const segment of segments) {
|
|
15
|
+
if (segment === '..' || segment === '.') {
|
|
16
|
+
throw new ApiError({ code: 400, message: `Invalid path segment "${segment}"` });
|
|
17
|
+
}
|
|
15
18
|
if (!SEGMENT_PATTERN.test(segment)) {
|
|
16
19
|
throw new ApiError({ code: 400, message: `Invalid path segment "${segment}"` });
|
|
17
20
|
}
|
|
@@ -31,7 +34,7 @@ export function assertSafeRelativePath(filename, label) {
|
|
|
31
34
|
export function buildFormSlugAndFilename(params) {
|
|
32
35
|
const domainSlug = normalizeSlug(params.domainName);
|
|
33
36
|
const formSlug = normalizeSlug(params.idname);
|
|
34
|
-
const localeSlug = normalizeSlug(params.locale || params.domainLocale ||
|
|
37
|
+
const localeSlug = normalizeSlug(params.locale || params.domainLocale || '');
|
|
35
38
|
const slug = `${domainSlug}${localeSlug ? '-' + localeSlug : ''}-${formSlug}`;
|
|
36
39
|
const filenameParts = [domainSlug, 'form-template'];
|
|
37
40
|
if (localeSlug) {
|
package/dist/esm/util/utils.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export declare function user_and_domain(domain_id: number): Promise<{
|
|
|
19
19
|
user: api_user;
|
|
20
20
|
domain: api_domain;
|
|
21
21
|
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Collect informational request metadata (client IP, IP chain, timestamp) for
|
|
24
|
+
* use in template rendering context. The values are **not** used for security
|
|
25
|
+
* decisions such as rate limiting — those rely on `getClientIp()` which is
|
|
26
|
+
* trust-proxy aware. For the IP chain to be meaningful the server must sit
|
|
27
|
+
* behind a trusted reverse proxy that sets the forwarded headers.
|
|
28
|
+
*/
|
|
22
29
|
export declare function buildRequestMeta(rawReq: unknown): RequestMeta;
|
|
23
30
|
export declare function decodeComponent(value: string | string[] | undefined): string;
|
|
24
31
|
export declare function getBodyValue(body: Record<string, unknown>, ...keys: string[]): string;
|
package/dist/esm/util/utils.js
CHANGED
|
@@ -60,6 +60,13 @@ function resolveHeader(headers, key) {
|
|
|
60
60
|
}
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Collect informational request metadata (client IP, IP chain, timestamp) for
|
|
65
|
+
* use in template rendering context. The values are **not** used for security
|
|
66
|
+
* decisions such as rate limiting — those rely on `getClientIp()` which is
|
|
67
|
+
* trust-proxy aware. For the IP chain to be meaningful the server must sit
|
|
68
|
+
* behind a trusted reverse proxy that sets the forwarded headers.
|
|
69
|
+
*/
|
|
63
70
|
export function buildRequestMeta(rawReq) {
|
|
64
71
|
const req = (rawReq ?? {});
|
|
65
72
|
const headers = req.headers ?? {};
|
package/docs/tutorial.md
CHANGED
|
@@ -264,7 +264,7 @@ Create `${CONFIG_ROOT}/init-data.json` so the service can bootstrap the MyOrg us
|
|
|
264
264
|
<tr>
|
|
265
265
|
<td style="padding:16px 24px;background:#f9fafb;color:#4b5563;font-size:12px;">
|
|
266
266
|
<strong>Sender IP:</strong> {{ _meta_.client_ip | default('unknown') }} ·
|
|
267
|
-
<strong>Received at:</strong> {{ _meta_.received_at | default(
|
|
267
|
+
<strong>Received at:</strong> {{ _meta_.received_at | default('unknown') }}
|
|
268
268
|
</td>
|
|
269
269
|
</tr>
|
|
270
270
|
{% endif %}
|