@technomoron/mail-magic 1.0.35 → 1.0.37
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 +34 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/package.json +3 -0
- package/dist/{api → esm/api}/assets.js +2 -1
- package/dist/{api → esm/api}/auth.js +1 -1
- package/dist/{api → esm/api}/forms.js +3 -2
- package/dist/{api → esm/api}/mailer.js +3 -22
- package/dist/{index.js → esm/index.js} +3 -15
- package/dist/{models → esm/models}/db.js +10 -2
- package/dist/{models → esm/models}/form.js +20 -2
- package/dist/{models → esm/models}/txmail.js +10 -1
- package/dist/esm/server.js +22 -0
- package/dist/{store → esm/store}/envloader.js +1 -1
- package/dist/{store → esm/store}/store.js +0 -21
- package/dist/{swagger.js → esm/swagger.js} +1 -14
- package/dist/esm/util/route.js +14 -0
- package/package.json +14 -5
- package/dist/server.js +0 -34
- /package/dist/{bin → esm/bin}/mail-magic.js +0 -0
- /package/dist/{models → esm/models}/domain.js +0 -0
- /package/dist/{models → esm/models}/init.js +0 -0
- /package/dist/{models → esm/models}/recipient.js +0 -0
- /package/dist/{models → esm/models}/user.js +0 -0
- /package/dist/{types.js → esm/types.js} +0 -0
- /package/dist/{util → esm/util}/captcha.js +0 -0
- /package/dist/{util → esm/util}/email.js +0 -0
- /package/dist/{util → esm/util}/form-replyto.js +0 -0
- /package/dist/{util → esm/util}/form-submission.js +0 -0
- /package/dist/{util → esm/util}/forms.js +0 -0
- /package/dist/{util → esm/util}/paths.js +0 -0
- /package/dist/{util → esm/util}/ratelimit.js +0 -0
- /package/dist/{util → esm/util}/uploads.js +0 -0
- /package/dist/{util → esm/util}/utils.js +0 -0
- /package/dist/{util.js → esm/util.js} +0 -0
package/CHANGES
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
Version 1.0.37 (2026-02-17)
|
|
2
|
+
|
|
3
|
+
- Remove stale commented-out debug code from server mailer/store paths.
|
|
4
|
+
- Remove unused legacy API-key store scaffolding from `mailStore` (`api_key`,
|
|
5
|
+
`ImailStore`, `keys`, `load_api_keys`, `get_api_key`).
|
|
6
|
+
- Consolidate mailer address validation by reusing shared `util/email` helpers.
|
|
7
|
+
|
|
8
|
+
Version 1.0.36 (2026-02-17)
|
|
9
|
+
|
|
10
|
+
- Remove legacy plaintext-token fallback from request authentication; API key auth
|
|
11
|
+
now requires `token_hmac`.
|
|
12
|
+
- Update token migration regression coverage to verify auth rejection before
|
|
13
|
+
migration and success after migration.
|
|
14
|
+
- Extract shared `normalizeRoute` helper and reuse it across bootstrap and swagger
|
|
15
|
+
modules.
|
|
16
|
+
- Standardize `getBodyValue` usage to `util/utils` imports in API modules for
|
|
17
|
+
clearer utility ownership.
|
|
18
|
+
- Remove unreachable null check after `createTransport()`.
|
|
19
|
+
- Add utility test coverage for route normalization.
|
|
20
|
+
|
|
1
21
|
Version 1.0.35 (2026-02-17)
|
|
2
22
|
|
|
3
23
|
- Enforce deterministic locale resolution for transactional template sends:
|
|
@@ -8,6 +28,20 @@ Version 1.0.35 (2026-02-17)
|
|
|
8
28
|
locales are missing.
|
|
9
29
|
- Add regression tests covering deterministic locale fallback and missing-locale
|
|
10
30
|
behavior for both transactional sends and template asset upload resolution.
|
|
31
|
+
- Harden JSON-backed getters for template/form asset columns so malformed DB JSON
|
|
32
|
+
safely falls back to empty arrays instead of throwing runtime errors.
|
|
33
|
+
- Standardize form send SMTP failure handling to return ApiError responses
|
|
34
|
+
(consistent `message` contract instead of ad-hoc `error` payloads).
|
|
35
|
+
- Guard SQLite-only PRAGMA usage by database dialect during DB sync.
|
|
36
|
+
- Fix startup error text to use the current project name (`mail-magic`).
|
|
37
|
+
- Fix `DB_TYPE` environment description text ("API database" wording).
|
|
38
|
+
- Add regression tests for malformed JSON getter handling, standardized form-send
|
|
39
|
+
failure responses, sqlite PRAGMA guard helper behavior, startup error message,
|
|
40
|
+
and env option description text.
|
|
41
|
+
- Disallow legacy plaintext-token fallback during API authentication; requests now
|
|
42
|
+
authenticate strictly via `token_hmac`.
|
|
43
|
+
- Update token migration regression coverage to verify plaintext auth rejection
|
|
44
|
+
prior to migration and successful auth after migration.
|
|
11
45
|
|
|
12
46
|
Version 1.0.34 (2026-02-10)
|
|
13
47
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const load = () => import('../esm/index.js');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
STARTUP_ERROR_MESSAGE: 'Failed to start mail-magic:',
|
|
7
|
+
createMailMagicServer: async (...args) => (await load()).createMailMagicServer(...args),
|
|
8
|
+
startMailMagicServer: async (...args) => (await load()).startMailMagicServer(...args)
|
|
9
|
+
};
|
|
@@ -5,7 +5,8 @@ import { api_form } from '../models/form.js';
|
|
|
5
5
|
import { api_txmail } from '../models/txmail.js';
|
|
6
6
|
import { SEGMENT_PATTERN, normalizeSubdir } from '../util/paths.js';
|
|
7
7
|
import { moveUploadedFiles } from '../util/uploads.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getBodyValue } from '../util/utils.js';
|
|
9
|
+
import { decodeComponent, sendFileAsync } from '../util.js';
|
|
9
10
|
import { assert_domain_and_user } from './auth.js';
|
|
10
11
|
const DOMAIN_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
11
12
|
export class AssetAPI extends ApiModule {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ApiError } from '@technomoron/api-server-base';
|
|
2
2
|
import { api_domain } from '../models/domain.js';
|
|
3
3
|
import { api_user } from '../models/user.js';
|
|
4
|
-
import { getBodyValue } from '../util.js';
|
|
4
|
+
import { getBodyValue } from '../util/utils.js';
|
|
5
5
|
export async function assert_domain_and_user(apireq) {
|
|
6
6
|
const body = apireq.req.body ?? {};
|
|
7
7
|
const domain = getBodyValue(body, 'domain');
|
|
@@ -8,7 +8,8 @@ import { api_recipient } from '../models/recipient.js';
|
|
|
8
8
|
import { buildFormTemplateRecord, buildFormTemplatePaths, buildRecipientTo, buildReplyToValue, buildSubmissionContext, enforceAttachmentPolicy, enforceCaptchaPolicy, filterSubmissionFields, getPrimaryRecipientInfo, normalizeRecipientEmail, normalizeRecipientIdname, normalizeRecipientName, parseIdnameList, parseFormTemplatePayload, parseRecipientPayload, parsePublicSubmissionOrThrow, resolveFormKeyForTemplate, resolveFormKeyForRecipient, resolveRecipients, validateFormTemplatePayload } from '../util/forms.js';
|
|
9
9
|
import { FixedWindowRateLimiter, enforceFormRateLimit } from '../util/ratelimit.js';
|
|
10
10
|
import { buildAttachments, cleanupUploadedFiles } from '../util/uploads.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getBodyValue } from '../util/utils.js';
|
|
12
|
+
import { buildRequestMeta } from '../util.js';
|
|
12
13
|
import { assert_domain_and_user } from './auth.js';
|
|
13
14
|
export class FormAPI extends ApiModule {
|
|
14
15
|
rateLimiter = new FixedWindowRateLimiter();
|
|
@@ -187,7 +188,7 @@ export class FormAPI extends ApiModule {
|
|
|
187
188
|
catch (error) {
|
|
188
189
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
189
190
|
this.server.storage.print_debug('Error sending email: ' + errorMessage);
|
|
190
|
-
|
|
191
|
+
throw new ApiError({ code: 500, message: `Error sending email: ${errorMessage}` });
|
|
191
192
|
}
|
|
192
193
|
return [200, {}];
|
|
193
194
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { ApiModule, ApiError } from '@technomoron/api-server-base';
|
|
2
|
-
import emailAddresses from 'email-addresses';
|
|
3
2
|
import { convert } from 'html-to-text';
|
|
4
3
|
import nunjucks from 'nunjucks';
|
|
5
4
|
import { api_txmail } from '../models/txmail.js';
|
|
5
|
+
import { validateEmail } from '../util/email.js';
|
|
6
6
|
import { buildRequestMeta } from '../util.js';
|
|
7
7
|
import { assert_domain_and_user } from './auth.js';
|
|
8
8
|
export class MailerAPI extends ApiModule {
|
|
9
|
-
//
|
|
10
|
-
// Validate and return the parsed email address
|
|
11
|
-
//
|
|
12
|
-
validateEmail(email) {
|
|
13
|
-
const parsed = emailAddresses.parseOneAddress(email);
|
|
14
|
-
if (parsed) {
|
|
15
|
-
return parsed.address;
|
|
16
|
-
}
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
9
|
//
|
|
20
10
|
// Validate a set of email addresses. Return arrays of invalid
|
|
21
11
|
// and valid email addresses.
|
|
@@ -27,7 +17,7 @@ export class MailerAPI extends ApiModule {
|
|
|
27
17
|
.map((email) => email.trim())
|
|
28
18
|
.filter((email) => email !== '');
|
|
29
19
|
emails.forEach((email) => {
|
|
30
|
-
const addr =
|
|
20
|
+
const addr = validateEmail(email);
|
|
31
21
|
if (addr) {
|
|
32
22
|
valid.push(addr);
|
|
33
23
|
}
|
|
@@ -56,13 +46,6 @@ export class MailerAPI extends ApiModule {
|
|
|
56
46
|
sender,
|
|
57
47
|
template
|
|
58
48
|
};
|
|
59
|
-
/*
|
|
60
|
-
console.log(JSON.stringify({
|
|
61
|
-
user: apireq.user,
|
|
62
|
-
domain: apireq.domain,
|
|
63
|
-
domain_id: apireq.domain.domain_id,
|
|
64
|
-
data
|
|
65
|
-
}, undefined, 2)); */
|
|
66
49
|
try {
|
|
67
50
|
const [templateRecord, created] = await api_txmail.upsert(data, {
|
|
68
51
|
returning: true
|
|
@@ -94,7 +77,6 @@ export class MailerAPI extends ApiModule {
|
|
|
94
77
|
}
|
|
95
78
|
}
|
|
96
79
|
const thevars = parsedVars;
|
|
97
|
-
// const dbdomain = await api_domain.findOne({ where: { domain } });
|
|
98
80
|
const { valid, invalid } = this.validateEmails(rcpt);
|
|
99
81
|
if (invalid.length > 0) {
|
|
100
82
|
throw new ApiError({ code: 400, message: 'Invalid email address(es): ' + invalid.join(',') });
|
|
@@ -145,7 +127,7 @@ export class MailerAPI extends ApiModule {
|
|
|
145
127
|
const replyToValue = (replyTo || reply_to);
|
|
146
128
|
let normalizedReplyTo;
|
|
147
129
|
if (replyToValue) {
|
|
148
|
-
normalizedReplyTo =
|
|
130
|
+
normalizedReplyTo = validateEmail(replyToValue);
|
|
149
131
|
if (!normalizedReplyTo) {
|
|
150
132
|
throw new ApiError({ code: 400, message: 'Invalid reply-to email address' });
|
|
151
133
|
}
|
|
@@ -191,7 +173,6 @@ export class MailerAPI extends ApiModule {
|
|
|
191
173
|
return [200, { Status: 'OK', Message: 'Emails sent successfully' }];
|
|
192
174
|
}
|
|
193
175
|
catch (error) {
|
|
194
|
-
// console.log(JSON.stringify(e, null, 2));
|
|
195
176
|
throw new ApiError({
|
|
196
177
|
code: 500,
|
|
197
178
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -5,20 +5,8 @@ import { MailerAPI } from './api/mailer.js';
|
|
|
5
5
|
import { mailApiServer } from './server.js';
|
|
6
6
|
import { mailStore } from './store/store.js';
|
|
7
7
|
import { installMailMagicSwagger } from './swagger.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return fallback;
|
|
11
|
-
}
|
|
12
|
-
const trimmed = value.trim();
|
|
13
|
-
if (!trimmed) {
|
|
14
|
-
return fallback;
|
|
15
|
-
}
|
|
16
|
-
const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
17
|
-
if (withLeading === '/') {
|
|
18
|
-
return withLeading;
|
|
19
|
-
}
|
|
20
|
-
return withLeading.replace(/\/+$/, '');
|
|
21
|
-
}
|
|
8
|
+
import { normalizeRoute } from './util/route.js';
|
|
9
|
+
export const STARTUP_ERROR_MESSAGE = 'Failed to start mail-magic:';
|
|
22
10
|
function mergeStaticDirs(base, override) {
|
|
23
11
|
const merged = { ...base, ...(override ?? {}) };
|
|
24
12
|
if (Object.keys(merged).length === 0) {
|
|
@@ -107,7 +95,7 @@ async function bootMailMagic() {
|
|
|
107
95
|
console.log(`mail-magic server listening on ${vars.API_HOST}:${vars.API_PORT}`);
|
|
108
96
|
}
|
|
109
97
|
catch (err) {
|
|
110
|
-
console.error(
|
|
98
|
+
console.error(STARTUP_ERROR_MESSAGE, err);
|
|
111
99
|
process.exit(1);
|
|
112
100
|
}
|
|
113
101
|
}
|
|
@@ -5,6 +5,9 @@ import { importData } from './init.js';
|
|
|
5
5
|
import { init_api_recipient, api_recipient } from './recipient.js';
|
|
6
6
|
import { init_api_txmail, api_txmail } from './txmail.js';
|
|
7
7
|
import { init_api_user, api_user, migrateLegacyApiTokens } from './user.js';
|
|
8
|
+
export function usesSqlitePragmas(db) {
|
|
9
|
+
return db.getDialect() === 'sqlite';
|
|
10
|
+
}
|
|
8
11
|
export async function init_api_db(db, store) {
|
|
9
12
|
await init_api_user(db);
|
|
10
13
|
await init_api_domain(db);
|
|
@@ -71,11 +74,16 @@ export async function init_api_db(db, store) {
|
|
|
71
74
|
foreignKey: 'domain_id',
|
|
72
75
|
as: 'domain'
|
|
73
76
|
});
|
|
74
|
-
|
|
77
|
+
const useSqlitePragmas = usesSqlitePragmas(db);
|
|
78
|
+
if (useSqlitePragmas) {
|
|
79
|
+
await db.query('PRAGMA foreign_keys = OFF');
|
|
80
|
+
}
|
|
75
81
|
const alter = Boolean(store.vars.DB_SYNC_ALTER);
|
|
76
82
|
store.print_debug(`DB sync: alter=${alter} force=${store.vars.DB_FORCE_SYNC}`);
|
|
77
83
|
await db.sync({ alter, force: store.vars.DB_FORCE_SYNC });
|
|
78
|
-
|
|
84
|
+
if (useSqlitePragmas) {
|
|
85
|
+
await db.query('PRAGMA foreign_keys = ON');
|
|
86
|
+
}
|
|
79
87
|
await importData(store);
|
|
80
88
|
try {
|
|
81
89
|
const { migrated, cleared } = await migrateLegacyApiTokens(store.vars.API_TOKEN_PEPPER);
|
|
@@ -170,7 +170,16 @@ export async function init_api_form(api_db) {
|
|
|
170
170
|
get() {
|
|
171
171
|
// This column is stored as JSON text but exposed as `string[]` via getter/setter.
|
|
172
172
|
const raw = this.getDataValue('allowed_fields');
|
|
173
|
-
|
|
173
|
+
if (!raw) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
174
183
|
},
|
|
175
184
|
set(value) {
|
|
176
185
|
this.setDataValue('allowed_fields', JSON.stringify(value ?? []));
|
|
@@ -188,7 +197,16 @@ export async function init_api_form(api_db) {
|
|
|
188
197
|
get() {
|
|
189
198
|
// This column is stored as JSON text but exposed as `StoredFile[]` via getter/setter.
|
|
190
199
|
const raw = this.getDataValue('files');
|
|
191
|
-
|
|
200
|
+
if (!raw) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const parsed = JSON.parse(raw);
|
|
205
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
192
210
|
},
|
|
193
211
|
set(value) {
|
|
194
212
|
this.setDataValue('files', JSON.stringify(value ?? []));
|
|
@@ -131,7 +131,16 @@ export async function init_api_txmail(api_db) {
|
|
|
131
131
|
defaultValue: '[]',
|
|
132
132
|
get() {
|
|
133
133
|
const raw = this.getDataValue('files');
|
|
134
|
-
|
|
134
|
+
if (!raw) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(raw);
|
|
139
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
135
144
|
},
|
|
136
145
|
set(value) {
|
|
137
146
|
this.setDataValue('files', JSON.stringify(value ?? []));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ApiServer } from '@technomoron/api-server-base';
|
|
2
|
+
import { apiTokenToHmac, api_user } from './models/user.js';
|
|
3
|
+
export class mailApiServer extends ApiServer {
|
|
4
|
+
store;
|
|
5
|
+
storage;
|
|
6
|
+
constructor(config, store) {
|
|
7
|
+
super(config);
|
|
8
|
+
this.store = store;
|
|
9
|
+
this.storage = store;
|
|
10
|
+
}
|
|
11
|
+
async getApiKey(token) {
|
|
12
|
+
this.storage.print_debug('Looking up api key');
|
|
13
|
+
const pepper = this.storage.vars.API_TOKEN_PEPPER;
|
|
14
|
+
const token_hmac = apiTokenToHmac(token, pepper);
|
|
15
|
+
const user = await api_user.findOne({ where: { token_hmac } });
|
|
16
|
+
if (user) {
|
|
17
|
+
return { uid: user.user_id };
|
|
18
|
+
}
|
|
19
|
+
this.storage.print_debug('Unable to find user for api key');
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -22,13 +22,9 @@ function create_mail_transport(vars) {
|
|
|
22
22
|
if (user && pass) {
|
|
23
23
|
args.auth = { user, pass };
|
|
24
24
|
}
|
|
25
|
-
// console.log(JSON.stringify(args, undefined, 2));
|
|
26
25
|
const mailer = createTransport({
|
|
27
26
|
...args
|
|
28
27
|
});
|
|
29
|
-
if (!mailer) {
|
|
30
|
-
throw new Error('Unable to create mailer');
|
|
31
|
-
}
|
|
32
28
|
return mailer;
|
|
33
29
|
}
|
|
34
30
|
export class mailStore {
|
|
@@ -36,9 +32,7 @@ export class mailStore {
|
|
|
36
32
|
vars;
|
|
37
33
|
transport;
|
|
38
34
|
api_db = null;
|
|
39
|
-
keys = {};
|
|
40
35
|
configpath = '';
|
|
41
|
-
deflocale;
|
|
42
36
|
uploadTemplate;
|
|
43
37
|
uploadStagingPath;
|
|
44
38
|
print_debug(msg) {
|
|
@@ -102,17 +96,6 @@ export class mailStore {
|
|
|
102
96
|
}
|
|
103
97
|
}));
|
|
104
98
|
}
|
|
105
|
-
async load_api_keys(cfgpath) {
|
|
106
|
-
const keyfile = path.resolve(cfgpath, 'api-keys.json');
|
|
107
|
-
if (fs.existsSync(keyfile)) {
|
|
108
|
-
const raw = fs.readFileSync(keyfile, 'utf-8');
|
|
109
|
-
const jsonData = JSON.parse(raw);
|
|
110
|
-
this.print_debug(`API Key Database loaded from ${keyfile}`);
|
|
111
|
-
return jsonData;
|
|
112
|
-
}
|
|
113
|
-
this.print_debug(`No api-keys.json file found: tried ${keyfile}`);
|
|
114
|
-
return {};
|
|
115
|
-
}
|
|
116
99
|
async init(overrides = {}) {
|
|
117
100
|
// Load env config only via EnvLoader + envOptions (avoid ad-hoc `process.env` parsing here).
|
|
118
101
|
// If DEBUG is enabled, re-load with EnvLoader debug output enabled.
|
|
@@ -168,7 +151,6 @@ export class mailStore {
|
|
|
168
151
|
this.print_debug(`Unable to create upload staging path: ${err}`);
|
|
169
152
|
}
|
|
170
153
|
}
|
|
171
|
-
// this.keys = await this.load_api_keys(this.configpath);
|
|
172
154
|
this.transport = await create_mail_transport(this.vars);
|
|
173
155
|
this.api_db = await connect_api_db(this);
|
|
174
156
|
if (this.vars.DB_AUTO_RELOAD) {
|
|
@@ -185,7 +167,4 @@ export class mailStore {
|
|
|
185
167
|
}
|
|
186
168
|
return this;
|
|
187
169
|
}
|
|
188
|
-
get_api_key(key) {
|
|
189
|
-
return this.keys[key] || null;
|
|
190
|
-
}
|
|
191
170
|
}
|
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
if (!value) {
|
|
6
|
-
return fallback;
|
|
7
|
-
}
|
|
8
|
-
const trimmed = value.trim();
|
|
9
|
-
if (!trimmed) {
|
|
10
|
-
return fallback;
|
|
11
|
-
}
|
|
12
|
-
const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
13
|
-
if (withLeading === '/') {
|
|
14
|
-
return withLeading;
|
|
15
|
-
}
|
|
16
|
-
return withLeading.replace(/\/+$/, '');
|
|
17
|
-
}
|
|
4
|
+
import { normalizeRoute } from './util/route.js';
|
|
18
5
|
function replacePrefix(input, from, to) {
|
|
19
6
|
if (input === from) {
|
|
20
7
|
return to;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function normalizeRoute(value, fallback = '') {
|
|
2
|
+
if (!value) {
|
|
3
|
+
return fallback;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
10
|
+
if (withLeading === '/') {
|
|
11
|
+
return withLeading;
|
|
12
|
+
}
|
|
13
|
+
return withLeading.replace(/\/+$/, '');
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/mail-magic",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"main": "dist/index.js",
|
|
3
|
+
"version": "1.0.37",
|
|
4
|
+
"main": "dist/cjs/index.js",
|
|
5
|
+
"module": "dist/esm/index.js",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
7
|
-
"mail-magic": "dist/bin/mail-magic.js"
|
|
8
|
+
"mail-magic": "dist/esm/bin/mail-magic.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/esm/index.js",
|
|
13
|
+
"require": "./dist/cjs/index.js"
|
|
14
|
+
}
|
|
8
15
|
},
|
|
9
16
|
"files": [
|
|
10
17
|
"dist",
|
|
@@ -18,10 +25,12 @@
|
|
|
18
25
|
"directory": "packages/mail-magic"
|
|
19
26
|
},
|
|
20
27
|
"scripts": {
|
|
21
|
-
"start": "node dist/index.js",
|
|
28
|
+
"start": "node dist/esm/index.js",
|
|
22
29
|
"dev": "NODE_ENV=development nodemon --watch 'src/**/*.ts' --watch 'config/**/*.*' --watch '.env' --exec 'tsx' src/index.ts",
|
|
23
30
|
"run": "NODE_ENV=production npm run start",
|
|
24
|
-
"build": "tsc",
|
|
31
|
+
"build:esm": "tsc --project tsconfig/tsconfig.esm.json",
|
|
32
|
+
"build:cjs": "node scripts/add-shebang.cjs --cjs-only",
|
|
33
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
25
34
|
"postbuild": "node scripts/add-shebang.cjs",
|
|
26
35
|
"prepack": "npm run build",
|
|
27
36
|
"test": "vitest run",
|
package/dist/server.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { ApiServer } from '@technomoron/api-server-base';
|
|
2
|
-
import { apiTokenToHmac, api_user } from './models/user.js';
|
|
3
|
-
export class mailApiServer extends ApiServer {
|
|
4
|
-
store;
|
|
5
|
-
storage;
|
|
6
|
-
constructor(config, store) {
|
|
7
|
-
super(config);
|
|
8
|
-
this.store = store;
|
|
9
|
-
this.storage = store;
|
|
10
|
-
}
|
|
11
|
-
async getApiKey(token) {
|
|
12
|
-
this.storage.print_debug('Looking up api key');
|
|
13
|
-
const pepper = this.storage.vars.API_TOKEN_PEPPER;
|
|
14
|
-
const token_hmac = apiTokenToHmac(token, pepper);
|
|
15
|
-
const user = await api_user.findOne({ where: { token_hmac } });
|
|
16
|
-
if (user) {
|
|
17
|
-
return { uid: user.user_id };
|
|
18
|
-
}
|
|
19
|
-
// Backwards-compatible fallback for legacy databases that still store plaintext tokens.
|
|
20
|
-
const legacy = await api_user.findOne({ where: { token } });
|
|
21
|
-
if (!legacy) {
|
|
22
|
-
this.storage.print_debug('Unable to find user for api key');
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
await legacy.update({ token_hmac, token: '' });
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
// Don't leak token data; just surface the update failure for debugging.
|
|
30
|
-
this.storage.print_debug(`Unable to migrate legacy api token: ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
-
}
|
|
32
|
-
return { uid: legacy.user_id };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|