@getstrata/core 2.2.21 → 2.2.23
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/CHANGELOG.md +8 -0
- package/dist/core/mail/mailer.d.ts +4 -0
- package/dist/entries/audit/exportAuditLogs.js +9 -6
- package/dist/entries/auth/scimAuthMiddleware.js +9 -6
- package/dist/entries/facades.js +279 -113
- package/dist/entries/jobs/exportAuditLogsJob.js +9 -6
- package/dist/entries/mail/mailer.js +278 -112
- package/dist/entries/queue/queueMetrics.js +9 -6
- package/dist/entries/tenant/databaseTenantContext.js +9 -6
- package/dist/entries/tenant/enableTenantRls.js +17 -17
- package/dist/entries/tenant/tenantDatabaseScope.js +9 -6
- package/dist/index.js +287 -118
- package/package.json +1 -1
|
@@ -221,6 +221,7 @@ var init_tenancyConfig = __esm(() => {
|
|
|
221
221
|
|
|
222
222
|
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
223
223
|
import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
|
|
224
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
224
225
|
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
225
226
|
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
226
227
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
@@ -243,12 +244,12 @@ async function runWithTenantDatabase(tenant, callback) {
|
|
|
243
244
|
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
244
245
|
}
|
|
245
246
|
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
246
|
-
return await begin.bind(pool)(async (transaction) => {
|
|
247
|
+
return await begin.bind(pool)(AsyncLocalStorage2.bind(async (transaction) => {
|
|
247
248
|
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
248
249
|
return await runWithDatabaseConnection(transaction, async () => {
|
|
249
250
|
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
250
251
|
});
|
|
251
|
-
});
|
|
252
|
+
}));
|
|
252
253
|
}));
|
|
253
254
|
}
|
|
254
255
|
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
@@ -717,6 +718,7 @@ var init_repositoryConnection = __esm(() => {
|
|
|
717
718
|
});
|
|
718
719
|
|
|
719
720
|
// ../../src/core/database/transaction.ts
|
|
721
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
720
722
|
async function runInSavepoint(connection, operation) {
|
|
721
723
|
const inherited = transactionFrame.getStore();
|
|
722
724
|
let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
|
|
@@ -776,9 +778,9 @@ async function runInTransaction(operation, options = {}) {
|
|
|
776
778
|
signal?.throwIfAborted();
|
|
777
779
|
if (!supportsTransactions(connection))
|
|
778
780
|
throw new Error("Reserved database connection does not support transactions.");
|
|
779
|
-
return await connection.begin(async (transaction) => {
|
|
781
|
+
return await connection.begin(AsyncLocalStorage3.bind(async (transaction) => {
|
|
780
782
|
return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
|
|
781
|
-
});
|
|
783
|
+
}));
|
|
782
784
|
} finally {
|
|
783
785
|
await reserved?.release();
|
|
784
786
|
}
|
|
@@ -800,6 +802,7 @@ var init_transaction = __esm(() => {
|
|
|
800
802
|
|
|
801
803
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
802
804
|
import { getDefaultDatabasePool as getDefaultDatabasePool3 } from "@getstrata/core/database/defaultConnection";
|
|
805
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
803
806
|
async function applyBypassToTransaction(transaction, bypass) {
|
|
804
807
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
805
808
|
bypass ? "true" : "false"
|
|
@@ -851,10 +854,10 @@ async function runWithScopedTenantTransaction(apply, callback) {
|
|
|
851
854
|
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
852
855
|
}
|
|
853
856
|
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
854
|
-
return await begin.bind(pool)(async (transaction) => {
|
|
857
|
+
return await begin.bind(pool)(AsyncLocalStorage4.bind(async (transaction) => {
|
|
855
858
|
await apply(transaction);
|
|
856
859
|
return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
|
|
857
|
-
});
|
|
860
|
+
}));
|
|
858
861
|
}));
|
|
859
862
|
}
|
|
860
863
|
async function runWithMigrationBypass(callback) {
|
|
@@ -95,8 +95,8 @@ function resolveSmtpConfig() {
|
|
|
95
95
|
if (!from) {
|
|
96
96
|
throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
|
|
97
97
|
}
|
|
98
|
-
const port = Number
|
|
99
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
98
|
+
const port = Number(process.env.MAIL_PORT ?? "587");
|
|
99
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
100
100
|
throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
|
|
101
101
|
}
|
|
102
102
|
return {
|
|
@@ -104,6 +104,7 @@ function resolveSmtpConfig() {
|
|
|
104
104
|
port,
|
|
105
105
|
from,
|
|
106
106
|
secure: (process.env.MAIL_SECURE ?? "false") === "true",
|
|
107
|
+
...process.env.MAIL_TIMEOUT_MS !== undefined ? { timeoutMs: smtpTimeout(Number(process.env.MAIL_TIMEOUT_MS)) } : {},
|
|
107
108
|
...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
|
|
108
109
|
...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
|
|
109
110
|
};
|
|
@@ -113,6 +114,12 @@ function assertSafeSmtpField(value, field) {
|
|
|
113
114
|
if (/[\r\n]/u.test(value)) {
|
|
114
115
|
throw new Error(`Mail ${field} must not contain CR or LF.`);
|
|
115
116
|
}
|
|
117
|
+
for (const character of value) {
|
|
118
|
+
const code = character.charCodeAt(0);
|
|
119
|
+
if (code < 32 && code !== 9 || code === 127) {
|
|
120
|
+
throw new Error(`Mail ${field} must not contain control characters.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
116
123
|
return value;
|
|
117
124
|
}
|
|
118
125
|
function smtpEnvelopeAddress(value, field) {
|
|
@@ -133,148 +140,307 @@ function redactMailBody(body) {
|
|
|
133
140
|
function encodeBase64(value) {
|
|
134
141
|
return Buffer.from(value, "utf8").toString("base64");
|
|
135
142
|
}
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
while (remainder.includes(`\r
|
|
140
|
-
`)) {
|
|
141
|
-
const index = remainder.indexOf(`\r
|
|
142
|
-
`);
|
|
143
|
-
const line = remainder.slice(0, index);
|
|
144
|
-
remainder = remainder.slice(index + 2);
|
|
145
|
-
if (line.length >= 4 && line[3] === "-") {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
responses.push(line);
|
|
143
|
+
function smtpTimeout(value = 1e4) {
|
|
144
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) {
|
|
145
|
+
throw new RangeError("SMTP timeoutMs must be a positive integer within the timer range.");
|
|
149
146
|
}
|
|
150
|
-
return
|
|
147
|
+
return value;
|
|
151
148
|
}
|
|
152
|
-
async function
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
throw new Error(`Unexpected SMTP response: ${response}`);
|
|
149
|
+
async function defaultSmtpTransport(config, message) {
|
|
150
|
+
const timeoutMs = smtpTimeout(config.timeoutMs);
|
|
151
|
+
if (!config.host.trim() || !Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
|
|
152
|
+
throw new RangeError("SMTP requires a host and a port between 1 and 65535.");
|
|
157
153
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
154
|
+
if (Boolean(config.username) !== Boolean(config.password)) {
|
|
155
|
+
throw new Error("SMTP authentication requires both username and password.");
|
|
156
|
+
}
|
|
157
|
+
if (config.username)
|
|
158
|
+
assertSafeSmtpField(config.username, "username");
|
|
159
|
+
if (config.password)
|
|
160
|
+
assertSafeSmtpField(config.password, "password");
|
|
161
|
+
const fromHeader = assertSafeSmtpAddress(config.from, "from");
|
|
162
|
+
const toHeader = assertSafeSmtpAddress(message.to, "to");
|
|
163
|
+
assertSafeSmtpField(message.subject, "subject");
|
|
164
|
+
const fromEnvelope = smtpEnvelopeAddress(config.from, "from");
|
|
165
|
+
const toEnvelope = smtpEnvelopeAddress(message.to, "to");
|
|
166
|
+
if (/[^\x21-\x7e]/u.test(fromEnvelope + toEnvelope)) {
|
|
167
|
+
throw new Error("Default SMTP transport requires ASCII envelopes; SMTPUTF8 is not negotiated.");
|
|
168
|
+
}
|
|
169
|
+
for (const address of [fromEnvelope, toEnvelope]) {
|
|
170
|
+
const at = address.lastIndexOf("@");
|
|
171
|
+
if (Buffer.byteLength(address) > 254 || Buffer.byteLength(address.slice(0, at)) > 64) {
|
|
172
|
+
throw new RangeError("SMTP envelope address exceeds the path/local-part length limit.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
message.signal?.throwIfAborted();
|
|
176
|
+
let socket;
|
|
177
|
+
let finished = false;
|
|
178
|
+
let failed = false;
|
|
179
|
+
let failure;
|
|
180
|
+
let rejectFailure;
|
|
181
|
+
const failurePromise = new Promise((_, reject) => {
|
|
182
|
+
rejectFailure = reject;
|
|
183
|
+
});
|
|
184
|
+
failurePromise.catch(() => {});
|
|
185
|
+
const responses = [];
|
|
162
186
|
const waiters = [];
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
187
|
+
let drain;
|
|
188
|
+
let drainVersion = 0;
|
|
189
|
+
let buffer = "";
|
|
190
|
+
let multilineCode;
|
|
191
|
+
let multilineLines = 0;
|
|
192
|
+
const decoder = new TextDecoder;
|
|
193
|
+
const fail = (reason) => {
|
|
194
|
+
if (finished || failed)
|
|
168
195
|
return;
|
|
196
|
+
failed = true;
|
|
197
|
+
failure = reason;
|
|
198
|
+
rejectFailure(reason);
|
|
199
|
+
waiters.length = 0;
|
|
200
|
+
drain?.();
|
|
201
|
+
drain = undefined;
|
|
202
|
+
socket?.terminate();
|
|
203
|
+
};
|
|
204
|
+
const onAbort = () => fail(message.signal?.reason ?? new DOMException("Mail send aborted.", "AbortError"));
|
|
205
|
+
const timer = setTimeout(() => fail(new DOMException("SMTP send deadline exceeded.", "TimeoutError")), timeoutMs);
|
|
206
|
+
message.signal?.addEventListener("abort", onAbort, { once: true });
|
|
207
|
+
if (message.signal?.aborted)
|
|
208
|
+
onAbort();
|
|
209
|
+
const readResponse = async (expected) => {
|
|
210
|
+
const queued = responses.shift();
|
|
211
|
+
const response = queued ?? await Promise.race([
|
|
212
|
+
failed ? Promise.reject(failure) : new Promise((resolve) => waiters.push(resolve)),
|
|
213
|
+
failurePromise
|
|
214
|
+
]);
|
|
215
|
+
const code = response.slice(0, 3);
|
|
216
|
+
if (!expected.includes(code))
|
|
217
|
+
throw new Error(`Unexpected SMTP response code: ${code}`);
|
|
218
|
+
};
|
|
219
|
+
const write = async (text) => {
|
|
220
|
+
const bytes = Buffer.from(text, "utf8");
|
|
221
|
+
let offset = 0;
|
|
222
|
+
while (offset < bytes.length) {
|
|
223
|
+
if (failed)
|
|
224
|
+
throw failure;
|
|
225
|
+
const version = drainVersion;
|
|
226
|
+
if (!socket)
|
|
227
|
+
throw new Error("SMTP socket is not connected.");
|
|
228
|
+
const written = socket.write(bytes.subarray(offset));
|
|
229
|
+
if (written < 0)
|
|
230
|
+
throw new Error("SMTP socket write failed.");
|
|
231
|
+
offset += written;
|
|
232
|
+
if (offset < bytes.length && version === drainVersion) {
|
|
233
|
+
await Promise.race([
|
|
234
|
+
new Promise((resolve) => {
|
|
235
|
+
drain = resolve;
|
|
236
|
+
}),
|
|
237
|
+
failurePromise
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
169
240
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
buffer = parsed.remainder;
|
|
181
|
-
while (parsed.responses.length > 0 && waiters.length > 0) {
|
|
182
|
-
const response = parsed.responses.shift();
|
|
183
|
-
waiters.shift()?.resolve(response);
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
const connecting = Bun.connect({
|
|
244
|
+
hostname: config.host,
|
|
245
|
+
port: config.port,
|
|
246
|
+
...config.secure ? {
|
|
247
|
+
tls: {
|
|
248
|
+
...config.tls,
|
|
249
|
+
serverName: config.tls?.serverName ?? config.host,
|
|
250
|
+
rejectUnauthorized: true
|
|
184
251
|
}
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
252
|
+
} : {},
|
|
253
|
+
socket: {
|
|
254
|
+
open(value) {
|
|
255
|
+
socket = value;
|
|
256
|
+
if (failed || finished)
|
|
257
|
+
value.terminate();
|
|
258
|
+
},
|
|
259
|
+
data(_socket, chunk) {
|
|
260
|
+
if (failed || finished)
|
|
261
|
+
return;
|
|
262
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
263
|
+
if (Buffer.byteLength(buffer, "utf8") > 16384) {
|
|
264
|
+
fail(new Error("SMTP response exceeded the buffer limit."));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
while (buffer.includes(`\r
|
|
268
|
+
`)) {
|
|
269
|
+
const index = buffer.indexOf(`\r
|
|
270
|
+
`);
|
|
271
|
+
const line = buffer.slice(0, index);
|
|
272
|
+
buffer = buffer.slice(index + 2);
|
|
273
|
+
const match = /^([2-5]\d{2})([- ]|$)/.exec(line);
|
|
274
|
+
if (!match || multilineCode !== undefined && match[1] !== multilineCode) {
|
|
275
|
+
fail(new Error("Malformed SMTP reply."));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (++multilineLines > 128) {
|
|
279
|
+
fail(new Error("SMTP reply exceeded the line limit."));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (match[2] === "-") {
|
|
283
|
+
multilineCode = match[1];
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
multilineCode = undefined;
|
|
287
|
+
multilineLines = 0;
|
|
288
|
+
const waiter = waiters.shift();
|
|
289
|
+
if (waiter)
|
|
290
|
+
waiter(line);
|
|
291
|
+
else {
|
|
292
|
+
responses.push(line);
|
|
293
|
+
if (responses.length > 32) {
|
|
294
|
+
fail(new Error("Too many buffered SMTP replies."));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
drain() {
|
|
301
|
+
drainVersion++;
|
|
302
|
+
const waiting = drain;
|
|
303
|
+
drain = undefined;
|
|
304
|
+
waiting?.();
|
|
305
|
+
},
|
|
306
|
+
end() {
|
|
307
|
+
fail(new Error("SMTP peer closed before the send completed."));
|
|
308
|
+
},
|
|
309
|
+
close() {
|
|
310
|
+
fail(new Error("SMTP connection closed before the send completed."));
|
|
311
|
+
},
|
|
312
|
+
error() {
|
|
313
|
+
fail(new Error("SMTP socket failed."));
|
|
314
|
+
},
|
|
315
|
+
connectError() {
|
|
316
|
+
fail(new Error("SMTP connection failed."));
|
|
317
|
+
},
|
|
318
|
+
handshake(_socket, success) {
|
|
319
|
+
if (!success)
|
|
320
|
+
fail(new Error("SMTP TLS verification failed."));
|
|
190
321
|
}
|
|
191
322
|
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
assertSafeSmtpField(config.password, "password");
|
|
202
|
-
}
|
|
203
|
-
const { socket, readResponse } = await openSmtpConnection(config);
|
|
204
|
-
try {
|
|
205
|
-
await waitForSmtpResponse(readResponse, ["220"]);
|
|
206
|
-
await socket.write(`EHLO ${smtpEhloHost()}\r
|
|
323
|
+
});
|
|
324
|
+
connecting.then((value) => {
|
|
325
|
+
socket = value;
|
|
326
|
+
if (failed || finished)
|
|
327
|
+
value.terminate();
|
|
328
|
+
}, () => {});
|
|
329
|
+
socket = await Promise.race([connecting, failurePromise]);
|
|
330
|
+
await readResponse(["220"]);
|
|
331
|
+
await write(`EHLO ${smtpEhloHost()}\r
|
|
207
332
|
`);
|
|
208
|
-
await
|
|
333
|
+
await readResponse(["250"]);
|
|
209
334
|
if (config.username && config.password) {
|
|
210
|
-
await
|
|
335
|
+
await write(`AUTH LOGIN\r
|
|
211
336
|
`);
|
|
212
|
-
await
|
|
213
|
-
await
|
|
337
|
+
await readResponse(["334"]);
|
|
338
|
+
await write(`${encodeBase64(config.username)}\r
|
|
214
339
|
`);
|
|
215
|
-
await
|
|
216
|
-
await
|
|
340
|
+
await readResponse(["334"]);
|
|
341
|
+
await write(`${encodeBase64(config.password)}\r
|
|
217
342
|
`);
|
|
218
|
-
await
|
|
343
|
+
await readResponse(["235"]);
|
|
219
344
|
}
|
|
220
|
-
|
|
221
|
-
const toHeader = assertSafeSmtpAddress(message.to, "to");
|
|
222
|
-
const from = smtpEnvelopeAddress(config.from, "from");
|
|
223
|
-
const to = smtpEnvelopeAddress(message.to, "to");
|
|
224
|
-
await socket.write(`MAIL FROM:<${from}>\r
|
|
345
|
+
await write(`MAIL FROM:<${fromEnvelope}>\r
|
|
225
346
|
`);
|
|
226
|
-
await
|
|
227
|
-
await
|
|
347
|
+
await readResponse(["250"]);
|
|
348
|
+
await write(`RCPT TO:<${toEnvelope}>\r
|
|
228
349
|
`);
|
|
229
|
-
await
|
|
230
|
-
await
|
|
350
|
+
await readResponse(["250", "251"]);
|
|
351
|
+
await write(`DATA\r
|
|
231
352
|
`);
|
|
232
|
-
await
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
await socket.write(payload);
|
|
239
|
-
await waitForSmtpResponse(readResponse, ["250"]);
|
|
240
|
-
await socket.write(`QUIT\r
|
|
353
|
+
await readResponse(["354"]);
|
|
354
|
+
await write(buildSmtpPayload(fromHeader, { ...message, to: toHeader }));
|
|
355
|
+
await readResponse(["250"]);
|
|
356
|
+
finished = true;
|
|
357
|
+
try {
|
|
358
|
+
socket.write(`QUIT\r
|
|
241
359
|
`);
|
|
242
|
-
|
|
360
|
+
} catch {}
|
|
243
361
|
} finally {
|
|
244
|
-
|
|
362
|
+
finished = true;
|
|
363
|
+
clearTimeout(timer);
|
|
364
|
+
message.signal?.removeEventListener("abort", onAbort);
|
|
365
|
+
waiters.length = 0;
|
|
366
|
+
responses.length = 0;
|
|
367
|
+
drain = undefined;
|
|
368
|
+
socket?.end();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function encodeSmtpHeader(value) {
|
|
372
|
+
if (/^[\x20-\x7e]{0,60}$/u.test(value))
|
|
373
|
+
return value;
|
|
374
|
+
const words = [];
|
|
375
|
+
let part = "";
|
|
376
|
+
for (const character of value) {
|
|
377
|
+
if (Buffer.byteLength(part + character, "utf8") > 45) {
|
|
378
|
+
words.push(`=?UTF-8?B?${encodeBase64(part)}?=`);
|
|
379
|
+
part = "";
|
|
380
|
+
}
|
|
381
|
+
part += character;
|
|
245
382
|
}
|
|
383
|
+
if (part)
|
|
384
|
+
words.push(`=?UTF-8?B?${encodeBase64(part)}?=`);
|
|
385
|
+
return words.join(`\r
|
|
386
|
+
`);
|
|
387
|
+
}
|
|
388
|
+
function smtpAddressHeader(value) {
|
|
389
|
+
if (!value.includes("<"))
|
|
390
|
+
return value;
|
|
391
|
+
const name = value.slice(0, value.lastIndexOf("<")).trim();
|
|
392
|
+
const address = smtpEnvelopeAddress(value, "address");
|
|
393
|
+
if (!name)
|
|
394
|
+
return address;
|
|
395
|
+
const encoded = encodeSmtpHeader(name);
|
|
396
|
+
const display = encoded.startsWith("=?UTF-8?B?") ? encoded : `"${encoded.replace(/[\\"]/gu, "\\$&")}"`;
|
|
397
|
+
return `${display} <${address}>`;
|
|
398
|
+
}
|
|
399
|
+
function smtpMimePart(type, value) {
|
|
400
|
+
const normalized = value.replace(/\r\n|\r|\n/gu, `\r
|
|
401
|
+
`);
|
|
402
|
+
const base64 = /[^\t\r\n\x20-\x7e]/u.test(normalized) || normalized.split(`\r
|
|
403
|
+
`).some((line) => line.length > 998);
|
|
404
|
+
return [
|
|
405
|
+
`Content-Type: ${type}; charset=utf-8`,
|
|
406
|
+
`Content-Transfer-Encoding: ${base64 ? "base64" : "7bit"}`,
|
|
407
|
+
"",
|
|
408
|
+
base64 ? (encodeBase64(normalized).match(/.{1,76}/gu) ?? []).join(`\r
|
|
409
|
+
`) : normalized
|
|
410
|
+
];
|
|
246
411
|
}
|
|
247
412
|
function buildSmtpPayload(from, message) {
|
|
248
|
-
const safeFrom = assertSafeSmtpAddress(from, "from");
|
|
249
|
-
const safeTo = assertSafeSmtpAddress(message.to, "to");
|
|
250
|
-
const safeSubject = assertSafeSmtpField(message.subject, "subject");
|
|
251
413
|
const headers = [
|
|
252
|
-
`From: ${
|
|
253
|
-
`To: ${
|
|
254
|
-
`Subject: ${
|
|
414
|
+
`From: ${smtpAddressHeader(assertSafeSmtpAddress(from, "from"))}`,
|
|
415
|
+
`To: ${smtpAddressHeader(assertSafeSmtpAddress(message.to, "to"))}`,
|
|
416
|
+
`Subject: ${encodeSmtpHeader(assertSafeSmtpField(message.subject, "subject"))}`,
|
|
255
417
|
"MIME-Version: 1.0"
|
|
256
418
|
];
|
|
419
|
+
let lines;
|
|
257
420
|
if (message.html) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
421
|
+
let boundary;
|
|
422
|
+
do {
|
|
423
|
+
boundary = `strata-${crypto.randomUUID()}`;
|
|
424
|
+
} while (message.body.includes(boundary) || message.html.includes(boundary));
|
|
425
|
+
lines = [
|
|
426
|
+
...headers,
|
|
427
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
|
263
428
|
"",
|
|
264
|
-
message.body,
|
|
265
429
|
`--${boundary}`,
|
|
266
|
-
"
|
|
267
|
-
|
|
268
|
-
message.html,
|
|
430
|
+
...smtpMimePart("text/plain", message.body),
|
|
431
|
+
`--${boundary}`,
|
|
432
|
+
...smtpMimePart("text/html", message.html),
|
|
269
433
|
`--${boundary}--`,
|
|
270
434
|
""
|
|
271
435
|
];
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return
|
|
277
|
-
`)
|
|
436
|
+
} else
|
|
437
|
+
lines = [...headers, ...smtpMimePart("text/plain", message.body)];
|
|
438
|
+
const content = lines.join(`\r
|
|
439
|
+
`).replace(/(^|\r\n)\./gu, "$1..");
|
|
440
|
+
return `${content}${content.endsWith(`\r
|
|
441
|
+
`) ? "" : `\r
|
|
442
|
+
`}.\r
|
|
443
|
+
`;
|
|
278
444
|
}
|
|
279
445
|
|
|
280
446
|
class LogMailDriver {
|
|
@@ -531,6 +531,7 @@ var init_tenancyConfig = __esm(() => {
|
|
|
531
531
|
|
|
532
532
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
533
533
|
import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
|
|
534
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
534
535
|
async function applyBypassToTransaction(transaction, bypass) {
|
|
535
536
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
536
537
|
bypass ? "true" : "false"
|
|
@@ -582,10 +583,10 @@ async function runWithScopedTenantTransaction(apply, callback) {
|
|
|
582
583
|
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
583
584
|
}
|
|
584
585
|
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
585
|
-
return await begin.bind(pool)(async (transaction) => {
|
|
586
|
+
return await begin.bind(pool)(AsyncLocalStorage2.bind(async (transaction) => {
|
|
586
587
|
await apply(transaction);
|
|
587
588
|
return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
|
|
588
|
-
});
|
|
589
|
+
}));
|
|
589
590
|
}));
|
|
590
591
|
}
|
|
591
592
|
async function runWithMigrationBypass(callback) {
|
|
@@ -605,6 +606,7 @@ var init_databaseTenantContext = __esm(() => {
|
|
|
605
606
|
|
|
606
607
|
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
607
608
|
import { getDefaultDatabasePool as getDefaultDatabasePool3 } from "@getstrata/core/database/defaultConnection";
|
|
609
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
608
610
|
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
609
611
|
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
610
612
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
@@ -627,12 +629,12 @@ async function runWithTenantDatabase(tenant, callback) {
|
|
|
627
629
|
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
628
630
|
}
|
|
629
631
|
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
630
|
-
return await begin.bind(pool)(async (transaction) => {
|
|
632
|
+
return await begin.bind(pool)(AsyncLocalStorage3.bind(async (transaction) => {
|
|
631
633
|
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
632
634
|
return await runWithDatabaseConnection(transaction, async () => {
|
|
633
635
|
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
634
636
|
});
|
|
635
|
-
});
|
|
637
|
+
}));
|
|
636
638
|
}));
|
|
637
639
|
}
|
|
638
640
|
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
@@ -791,6 +793,7 @@ function createDatabaseConnection(source) {
|
|
|
791
793
|
var init_connection = () => {};
|
|
792
794
|
|
|
793
795
|
// ../../src/core/database/transaction.ts
|
|
796
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
794
797
|
async function runInSavepoint(connection, operation) {
|
|
795
798
|
const inherited = transactionFrame.getStore();
|
|
796
799
|
let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
|
|
@@ -850,9 +853,9 @@ async function runInTransaction(operation, options = {}) {
|
|
|
850
853
|
signal?.throwIfAborted();
|
|
851
854
|
if (!supportsTransactions(connection))
|
|
852
855
|
throw new Error("Reserved database connection does not support transactions.");
|
|
853
|
-
return await connection.begin(async (transaction) => {
|
|
856
|
+
return await connection.begin(AsyncLocalStorage4.bind(async (transaction) => {
|
|
854
857
|
return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
|
|
855
|
-
});
|
|
858
|
+
}));
|
|
856
859
|
} finally {
|
|
857
860
|
await reserved?.release();
|
|
858
861
|
}
|
|
@@ -221,6 +221,7 @@ var init_tenancyConfig = __esm(() => {
|
|
|
221
221
|
|
|
222
222
|
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
223
223
|
import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
|
|
224
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
224
225
|
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
225
226
|
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
226
227
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
@@ -243,12 +244,12 @@ async function runWithTenantDatabase(tenant, callback) {
|
|
|
243
244
|
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
244
245
|
}
|
|
245
246
|
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
246
|
-
return await begin.bind(pool)(async (transaction) => {
|
|
247
|
+
return await begin.bind(pool)(AsyncLocalStorage2.bind(async (transaction) => {
|
|
247
248
|
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
248
249
|
return await runWithDatabaseConnection(transaction, async () => {
|
|
249
250
|
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
250
251
|
});
|
|
251
|
-
});
|
|
252
|
+
}));
|
|
252
253
|
}));
|
|
253
254
|
}
|
|
254
255
|
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
@@ -717,6 +718,7 @@ var init_repositoryConnection = __esm(() => {
|
|
|
717
718
|
});
|
|
718
719
|
|
|
719
720
|
// ../../src/core/database/transaction.ts
|
|
721
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
720
722
|
async function runInSavepoint(connection, operation) {
|
|
721
723
|
const inherited = transactionFrame.getStore();
|
|
722
724
|
let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
|
|
@@ -776,9 +778,9 @@ async function runInTransaction(operation, options = {}) {
|
|
|
776
778
|
signal?.throwIfAborted();
|
|
777
779
|
if (!supportsTransactions(connection))
|
|
778
780
|
throw new Error("Reserved database connection does not support transactions.");
|
|
779
|
-
return await connection.begin(async (transaction) => {
|
|
781
|
+
return await connection.begin(AsyncLocalStorage3.bind(async (transaction) => {
|
|
780
782
|
return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
|
|
781
|
-
});
|
|
783
|
+
}));
|
|
782
784
|
} finally {
|
|
783
785
|
await reserved?.release();
|
|
784
786
|
}
|
|
@@ -800,6 +802,7 @@ var init_transaction = __esm(() => {
|
|
|
800
802
|
|
|
801
803
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
802
804
|
import { getDefaultDatabasePool as getDefaultDatabasePool3 } from "@getstrata/core/database/defaultConnection";
|
|
805
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
803
806
|
async function applyBypassToTransaction(transaction, bypass) {
|
|
804
807
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
805
808
|
bypass ? "true" : "false"
|
|
@@ -851,10 +854,10 @@ async function runWithScopedTenantTransaction(apply, callback) {
|
|
|
851
854
|
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
852
855
|
}
|
|
853
856
|
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
854
|
-
return await begin.bind(pool)(async (transaction) => {
|
|
857
|
+
return await begin.bind(pool)(AsyncLocalStorage4.bind(async (transaction) => {
|
|
855
858
|
await apply(transaction);
|
|
856
859
|
return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
|
|
857
|
-
});
|
|
860
|
+
}));
|
|
858
861
|
}));
|
|
859
862
|
}
|
|
860
863
|
async function runWithMigrationBypass(callback) {
|