@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 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
- Client library and CLI for the mail-magic server.
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 ships `mm-cli`.
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
- interface templateData {
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 formTemplateData {
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 formRecipientData {
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 sendTemplateData {
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 sendFormData {
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 uploadAssetsData {
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 templateClient {
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: templateData): Promise<unknown>;
89
- sendTemplate(std: sendTemplateData): Promise<unknown>;
90
- storeTxTemplate(td: templateData): Promise<unknown>;
91
- sendTxMessage(std: sendTemplateData): Promise<unknown>;
92
- storeFormTemplate(data: formTemplateData): Promise<unknown>;
93
- storeFormRecipient(data: formRecipientData): Promise<unknown>;
94
- sendFormMessage(data: sendFormData): Promise<unknown>;
95
- uploadAssets(data: uploadAssetsData): Promise<unknown>;
96
- getSwaggerSpec(): Promise<unknown>;
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 templateClient;
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 templateClient {
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 = [], invalid = [];
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
- emails.forEach((email) => {
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(new nunjucks_1.default.FileSystemLoader(['./templates']));
79
- env.renderString(template, {});
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
- if (!td.template) {
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 = templateClient;
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 templateClient {
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 = [], invalid = [];
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
- emails.forEach((email) => {
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(new nunjucks.FileSystemLoader(['./templates']));
74
- env.renderString(template, {});
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
- if (!td.template) {
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 templateClient;
324
+ export default TemplateClient;