@technomoron/mail-magic-client 1.0.28 → 1.0.32
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 +29 -0
- package/README.md +2 -65
- package/dist/cjs/mail-magic-client.d.ts +24 -18
- package/dist/cjs/mail-magic-client.js +15 -18
- package/dist/esm/mail-magic-client.js +15 -18
- package/dist/mail-magic-client.js +322 -329
- package/package.json +46 -51
- package/dist/cli-env.js +0 -55
- package/dist/cli-helpers.js +0 -405
- package/dist/cli.js +0 -335
- package/dist/preprocess.js +0 -317
package/CHANGES
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
CHANGES
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
Unreleased (2026-02-22)
|
|
5
|
+
|
|
6
|
+
- chore(release): add package-level `release:check` script and wire `release` to shared publish script.
|
|
7
|
+
- chore(scripts): replace `rm -rf` cleanup scripts with `rimraf`.
|
|
8
|
+
- test(logging): quiet package test output by default (silent Vitest with compact dot reporter).
|
|
9
|
+
- (Changes generated/assisted by Codex (profile: chatgpt-5.3-codex/medium).)
|
|
10
|
+
|
|
11
|
+
Version 1.0.32 (2026-02-22)
|
|
12
|
+
|
|
13
|
+
- chore(changes): normalize this package changelog to required CHANGES format.
|
|
14
|
+
- (Changes generated/assisted by Codex (profile: chatgpt-5.3-codex/medium).)
|
|
15
|
+
|
|
16
|
+
Version 1.0.31 (2026-02-19)
|
|
17
|
+
|
|
18
|
+
- Extract CLI implementation to dedicated `@technomoron/mail-magic-cli`
|
|
19
|
+
package; remove CLI build/bin and CLI-only dependencies from this package.
|
|
20
|
+
- Replace `forEach` with `for...of` and use separate `const` declarations in
|
|
21
|
+
`validateEmails` for consistency with server-side style.
|
|
22
|
+
|
|
23
|
+
Version 1.0.30 (2026-02-17)
|
|
24
|
+
|
|
25
|
+
- Refactor template preprocess compilation to use per-invocation configuration
|
|
26
|
+
(remove module-level mutable config state).
|
|
27
|
+
- Add regression coverage to ensure preprocess options (such as
|
|
28
|
+
`inline_includes`) do not leak between compile calls.
|
|
29
|
+
|
|
1
30
|
Version 1.0.29 (2026-02-11)
|
|
2
31
|
|
|
3
32
|
- Expand client coverage for mail-magic-owned endpoints:
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @technomoron/mail-magic-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed client library for the mail-magic server.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -95,73 +95,10 @@ await client.sendFormMessage({
|
|
|
95
95
|
|
|
96
96
|
## CLI
|
|
97
97
|
|
|
98
|
-
The package
|
|
99
|
-
|
|
100
|
-
### .mmcli-env
|
|
101
|
-
|
|
102
|
-
Create `.mmcli-env` in your working directory to set defaults:
|
|
103
|
-
|
|
104
|
-
```ini
|
|
105
|
-
MMCLI_API=http://127.0.0.1:3776
|
|
106
|
-
MMCLI_TOKEN=example-token
|
|
107
|
-
MMCLI_DOMAIN=example.test
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
`MMCLI_TOKEN` is treated as the server token string. As a convenience, `MMCLI_USERNAME` + `MMCLI_PASSWORD` can be used
|
|
111
|
-
to build a combined token string (for legacy setups).
|
|
112
|
-
|
|
113
|
-
### Template Commands
|
|
114
|
-
|
|
115
|
-
Compile a template locally:
|
|
116
|
-
|
|
117
|
-
```bash
|
|
118
|
-
mm-cli compile --input ./templates --output ./templates-dist
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Push a single transactional template (compile + upload):
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
mm-cli push --template tx-template/en/welcome --domain example.test --input ./templates
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Dry-run a single template upload:
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
mm-cli push --template tx-template/en/welcome --domain example.test --input ./templates --dry-run
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Push an entire config-style directory:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
mm-cli push-dir --input ./data --domain example.test
|
|
137
|
-
mm-cli push-dir --input ./data --domain example.test --dry-run
|
|
138
|
-
mm-cli push-dir --input ./data --domain example.test --skip-assets
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Asset Uploads
|
|
142
|
-
|
|
143
|
-
Upload stand-alone domain assets:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
mm-cli assets --file ./logo.png --domain example.test
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Dry-run an asset upload:
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
mm-cli assets --file ./logo.png --domain example.test --dry-run
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Upload assets scoped to a template:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
mm-cli assets --file ./hero.png --domain example.test --template-type tx --template welcome --locale en --path images
|
|
159
|
-
```
|
|
98
|
+
The CLI is now a separate package: `@technomoron/mail-magic-cli`.
|
|
160
99
|
|
|
161
100
|
## Notes
|
|
162
101
|
|
|
163
|
-
- `push-dir` expects a `init-data.json` and domain folders that match the server config layout.
|
|
164
|
-
- Asset uploads use the server endpoint `POST /api/v1/assets`.
|
|
165
102
|
- OpenAPI spec (when enabled): `await client.getSwaggerSpec()`
|
|
166
103
|
- Public asset fetch helpers:
|
|
167
104
|
- `await client.fetchPublicAsset('example.test', 'images/logo.png')` -> `/asset/{domain}/{path}`
|
|
@@ -3,7 +3,13 @@ type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
};
|
|
5
5
|
type RequestBody = JsonValue | object;
|
|
6
|
-
|
|
6
|
+
export type ApiResponse<T = unknown> = {
|
|
7
|
+
Status?: string;
|
|
8
|
+
data?: T;
|
|
9
|
+
message?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
export interface StoreTxTemplateInput {
|
|
7
13
|
template: string;
|
|
8
14
|
domain: string;
|
|
9
15
|
sender?: string;
|
|
@@ -12,7 +18,7 @@ interface templateData {
|
|
|
12
18
|
locale?: string;
|
|
13
19
|
part?: boolean;
|
|
14
20
|
}
|
|
15
|
-
interface
|
|
21
|
+
export interface StoreFormTemplateInput {
|
|
16
22
|
idname: string;
|
|
17
23
|
domain: string;
|
|
18
24
|
template: string;
|
|
@@ -26,7 +32,7 @@ interface formTemplateData {
|
|
|
26
32
|
allowed_fields?: string[] | string;
|
|
27
33
|
captcha_required?: boolean;
|
|
28
34
|
}
|
|
29
|
-
interface
|
|
35
|
+
export interface StoreFormRecipientInput {
|
|
30
36
|
domain: string;
|
|
31
37
|
idname: string;
|
|
32
38
|
email: string;
|
|
@@ -35,7 +41,7 @@ interface formRecipientData {
|
|
|
35
41
|
formid?: string;
|
|
36
42
|
locale?: string;
|
|
37
43
|
}
|
|
38
|
-
interface
|
|
44
|
+
export interface SendTxMessageInput {
|
|
39
45
|
name: string;
|
|
40
46
|
rcpt: string;
|
|
41
47
|
domain: string;
|
|
@@ -45,21 +51,21 @@ interface sendTemplateData {
|
|
|
45
51
|
headers?: Record<string, string>;
|
|
46
52
|
attachments?: AttachmentInput[];
|
|
47
53
|
}
|
|
48
|
-
interface
|
|
54
|
+
export interface SendFormMessageInput {
|
|
49
55
|
_mm_form_key: string;
|
|
50
56
|
_mm_locale?: string;
|
|
51
57
|
_mm_recipients?: string[] | string;
|
|
52
58
|
fields?: Record<string, unknown>;
|
|
53
59
|
attachments?: AttachmentInput[];
|
|
54
60
|
}
|
|
55
|
-
type AttachmentInput = {
|
|
61
|
+
export type AttachmentInput = {
|
|
56
62
|
path: string;
|
|
57
63
|
filename?: string;
|
|
58
64
|
contentType?: string;
|
|
59
65
|
field?: string;
|
|
60
66
|
};
|
|
61
67
|
type UploadAssetInput = string | AttachmentInput;
|
|
62
|
-
interface
|
|
68
|
+
export interface UploadAssetsInput {
|
|
63
69
|
domain: string;
|
|
64
70
|
files: UploadAssetInput[];
|
|
65
71
|
templateType?: 'tx' | 'form';
|
|
@@ -67,7 +73,7 @@ interface uploadAssetsData {
|
|
|
67
73
|
locale?: string;
|
|
68
74
|
path?: string;
|
|
69
75
|
}
|
|
70
|
-
declare class
|
|
76
|
+
declare class TemplateClient {
|
|
71
77
|
private baseURL;
|
|
72
78
|
private apiKey;
|
|
73
79
|
constructor(baseURL: string, apiKey: string);
|
|
@@ -85,15 +91,15 @@ declare class templateClient {
|
|
|
85
91
|
private createAttachmentPayload;
|
|
86
92
|
private appendFields;
|
|
87
93
|
private postFormData;
|
|
88
|
-
storeTemplate(td:
|
|
89
|
-
sendTemplate(std:
|
|
90
|
-
storeTxTemplate(td:
|
|
91
|
-
sendTxMessage(std:
|
|
92
|
-
storeFormTemplate(data:
|
|
93
|
-
storeFormRecipient(data:
|
|
94
|
-
sendFormMessage(data:
|
|
95
|
-
uploadAssets(data:
|
|
96
|
-
getSwaggerSpec(): Promise<
|
|
94
|
+
storeTemplate(td: StoreTxTemplateInput): Promise<ApiResponse>;
|
|
95
|
+
sendTemplate(std: SendTxMessageInput): Promise<ApiResponse>;
|
|
96
|
+
storeTxTemplate(td: StoreTxTemplateInput): Promise<ApiResponse>;
|
|
97
|
+
sendTxMessage(std: SendTxMessageInput): Promise<ApiResponse>;
|
|
98
|
+
storeFormTemplate(data: StoreFormTemplateInput): Promise<ApiResponse>;
|
|
99
|
+
storeFormRecipient(data: StoreFormRecipientInput): Promise<ApiResponse>;
|
|
100
|
+
sendFormMessage(data: SendFormMessageInput): Promise<ApiResponse>;
|
|
101
|
+
uploadAssets(data: UploadAssetsInput): Promise<ApiResponse>;
|
|
102
|
+
getSwaggerSpec(): Promise<ApiResponse>;
|
|
97
103
|
fetchPublicAsset(domain: string, assetPath: string, viaApiBase?: boolean): Promise<ArrayBuffer>;
|
|
98
104
|
}
|
|
99
|
-
export default
|
|
105
|
+
export default TemplateClient;
|
|
@@ -7,7 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const email_addresses_1 = __importDefault(require("email-addresses"));
|
|
9
9
|
const nunjucks_1 = __importDefault(require("nunjucks"));
|
|
10
|
-
class
|
|
10
|
+
class TemplateClient {
|
|
11
11
|
constructor(baseURL, apiKey) {
|
|
12
12
|
this.baseURL = baseURL;
|
|
13
13
|
this.apiKey = apiKey;
|
|
@@ -30,13 +30,11 @@ class templateClient {
|
|
|
30
30
|
headers['Content-Type'] = 'application/json';
|
|
31
31
|
options.body = JSON.stringify(body);
|
|
32
32
|
}
|
|
33
|
-
// console.log(JSON.stringify({ options, url }));
|
|
34
33
|
const response = await fetch(url, options);
|
|
35
34
|
const j = await response.json();
|
|
36
35
|
if (response.ok) {
|
|
37
36
|
return j;
|
|
38
37
|
}
|
|
39
|
-
// console.log(JSON.stringify(j, undefined, 2));
|
|
40
38
|
if (j && j.message) {
|
|
41
39
|
throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
|
|
42
40
|
}
|
|
@@ -57,12 +55,13 @@ class templateClient {
|
|
|
57
55
|
return this.request('DELETE', command, body);
|
|
58
56
|
}
|
|
59
57
|
validateEmails(list) {
|
|
60
|
-
const valid = []
|
|
58
|
+
const valid = [];
|
|
59
|
+
const invalid = [];
|
|
61
60
|
const emails = list
|
|
62
61
|
.split(',')
|
|
63
62
|
.map((email) => email.trim())
|
|
64
63
|
.filter((email) => email !== '');
|
|
65
|
-
|
|
64
|
+
for (const email of emails) {
|
|
66
65
|
const parsed = email_addresses_1.default.parseOneAddress(email);
|
|
67
66
|
if (parsed && parsed.address) {
|
|
68
67
|
valid.push(parsed.address);
|
|
@@ -70,15 +69,21 @@ class templateClient {
|
|
|
70
69
|
else {
|
|
71
70
|
invalid.push(email);
|
|
72
71
|
}
|
|
73
|
-
}
|
|
72
|
+
}
|
|
74
73
|
return { valid, invalid };
|
|
75
74
|
}
|
|
76
75
|
validateTemplate(template) {
|
|
77
76
|
try {
|
|
78
|
-
const env = new nunjucks_1.default.Environment(
|
|
79
|
-
|
|
77
|
+
const env = new nunjucks_1.default.Environment(null, { autoescape: true });
|
|
78
|
+
const compiled = nunjucks_1.default.compile(template, env);
|
|
79
|
+
compiled.render({});
|
|
80
80
|
}
|
|
81
81
|
catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
// Syntax validation should not require local template loaders.
|
|
84
|
+
if (/template not found|no loader|unable to find template/i.test(message)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
82
87
|
if (error instanceof Error) {
|
|
83
88
|
throw new Error(`Template validation failed: ${error.message}`);
|
|
84
89
|
}
|
|
@@ -144,13 +149,7 @@ class templateClient {
|
|
|
144
149
|
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
145
150
|
}
|
|
146
151
|
async storeTemplate(td) {
|
|
147
|
-
|
|
148
|
-
throw new Error('No template data provided');
|
|
149
|
-
}
|
|
150
|
-
this.validateTemplate(td.template);
|
|
151
|
-
if (td.sender) {
|
|
152
|
-
this.validateSender(td.sender);
|
|
153
|
-
}
|
|
152
|
+
// Backward-compatible alias for transactional template storage.
|
|
154
153
|
return this.storeTxTemplate(td);
|
|
155
154
|
}
|
|
156
155
|
async sendTemplate(std) {
|
|
@@ -177,7 +176,6 @@ class templateClient {
|
|
|
177
176
|
if (invalid.length > 0) {
|
|
178
177
|
throw new Error('Invalid email address(es): ' + invalid.join(','));
|
|
179
178
|
}
|
|
180
|
-
// this.validateTemplate(template);
|
|
181
179
|
const body = {
|
|
182
180
|
name: std.name,
|
|
183
181
|
rcpt: std.rcpt,
|
|
@@ -187,7 +185,6 @@ class templateClient {
|
|
|
187
185
|
replyTo: std.replyTo,
|
|
188
186
|
headers: std.headers
|
|
189
187
|
};
|
|
190
|
-
// console.log(JSON.stringify(body, undefined, 2));
|
|
191
188
|
if (std.attachments && std.attachments.length > 0) {
|
|
192
189
|
if (std.headers) {
|
|
193
190
|
throw new Error('Headers are not supported with attachment uploads');
|
|
@@ -329,4 +326,4 @@ class templateClient {
|
|
|
329
326
|
return response.arrayBuffer();
|
|
330
327
|
}
|
|
331
328
|
}
|
|
332
|
-
exports.default =
|
|
329
|
+
exports.default = TemplateClient;
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import emailAddresses from 'email-addresses';
|
|
4
4
|
import nunjucks from 'nunjucks';
|
|
5
|
-
class
|
|
5
|
+
class TemplateClient {
|
|
6
6
|
constructor(baseURL, apiKey) {
|
|
7
7
|
this.baseURL = baseURL;
|
|
8
8
|
this.apiKey = apiKey;
|
|
@@ -25,13 +25,11 @@ class templateClient {
|
|
|
25
25
|
headers['Content-Type'] = 'application/json';
|
|
26
26
|
options.body = JSON.stringify(body);
|
|
27
27
|
}
|
|
28
|
-
// console.log(JSON.stringify({ options, url }));
|
|
29
28
|
const response = await fetch(url, options);
|
|
30
29
|
const j = await response.json();
|
|
31
30
|
if (response.ok) {
|
|
32
31
|
return j;
|
|
33
32
|
}
|
|
34
|
-
// console.log(JSON.stringify(j, undefined, 2));
|
|
35
33
|
if (j && j.message) {
|
|
36
34
|
throw new Error(`FETCH FAILED: ${response.status} ${j.message}`);
|
|
37
35
|
}
|
|
@@ -52,12 +50,13 @@ class templateClient {
|
|
|
52
50
|
return this.request('DELETE', command, body);
|
|
53
51
|
}
|
|
54
52
|
validateEmails(list) {
|
|
55
|
-
const valid = []
|
|
53
|
+
const valid = [];
|
|
54
|
+
const invalid = [];
|
|
56
55
|
const emails = list
|
|
57
56
|
.split(',')
|
|
58
57
|
.map((email) => email.trim())
|
|
59
58
|
.filter((email) => email !== '');
|
|
60
|
-
|
|
59
|
+
for (const email of emails) {
|
|
61
60
|
const parsed = emailAddresses.parseOneAddress(email);
|
|
62
61
|
if (parsed && parsed.address) {
|
|
63
62
|
valid.push(parsed.address);
|
|
@@ -65,15 +64,21 @@ class templateClient {
|
|
|
65
64
|
else {
|
|
66
65
|
invalid.push(email);
|
|
67
66
|
}
|
|
68
|
-
}
|
|
67
|
+
}
|
|
69
68
|
return { valid, invalid };
|
|
70
69
|
}
|
|
71
70
|
validateTemplate(template) {
|
|
72
71
|
try {
|
|
73
|
-
const env = new nunjucks.Environment(
|
|
74
|
-
|
|
72
|
+
const env = new nunjucks.Environment(null, { autoescape: true });
|
|
73
|
+
const compiled = nunjucks.compile(template, env);
|
|
74
|
+
compiled.render({});
|
|
75
75
|
}
|
|
76
76
|
catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
// Syntax validation should not require local template loaders.
|
|
79
|
+
if (/template not found|no loader|unable to find template/i.test(message)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
77
82
|
if (error instanceof Error) {
|
|
78
83
|
throw new Error(`Template validation failed: ${error.message}`);
|
|
79
84
|
}
|
|
@@ -139,13 +144,7 @@ class templateClient {
|
|
|
139
144
|
throw new Error(`FETCH FAILED: ${response.status} ${response.statusText}`);
|
|
140
145
|
}
|
|
141
146
|
async storeTemplate(td) {
|
|
142
|
-
|
|
143
|
-
throw new Error('No template data provided');
|
|
144
|
-
}
|
|
145
|
-
this.validateTemplate(td.template);
|
|
146
|
-
if (td.sender) {
|
|
147
|
-
this.validateSender(td.sender);
|
|
148
|
-
}
|
|
147
|
+
// Backward-compatible alias for transactional template storage.
|
|
149
148
|
return this.storeTxTemplate(td);
|
|
150
149
|
}
|
|
151
150
|
async sendTemplate(std) {
|
|
@@ -172,7 +171,6 @@ class templateClient {
|
|
|
172
171
|
if (invalid.length > 0) {
|
|
173
172
|
throw new Error('Invalid email address(es): ' + invalid.join(','));
|
|
174
173
|
}
|
|
175
|
-
// this.validateTemplate(template);
|
|
176
174
|
const body = {
|
|
177
175
|
name: std.name,
|
|
178
176
|
rcpt: std.rcpt,
|
|
@@ -182,7 +180,6 @@ class templateClient {
|
|
|
182
180
|
replyTo: std.replyTo,
|
|
183
181
|
headers: std.headers
|
|
184
182
|
};
|
|
185
|
-
// console.log(JSON.stringify(body, undefined, 2));
|
|
186
183
|
if (std.attachments && std.attachments.length > 0) {
|
|
187
184
|
if (std.headers) {
|
|
188
185
|
throw new Error('Headers are not supported with attachment uploads');
|
|
@@ -324,4 +321,4 @@ class templateClient {
|
|
|
324
321
|
return response.arrayBuffer();
|
|
325
322
|
}
|
|
326
323
|
}
|
|
327
|
-
export default
|
|
324
|
+
export default TemplateClient;
|