@selvajs/server 1.1.0 → 1.2.0
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/dist/notifications/index.d.ts +3 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +3 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/smtp-config.d.ts +37 -0
- package/dist/notifications/smtp-config.d.ts.map +1 -0
- package/dist/notifications/smtp-config.js +41 -0
- package/dist/notifications/smtp-config.js.map +1 -0
- package/dist/notifications/smtp-provider.d.ts +18 -0
- package/dist/notifications/smtp-provider.d.ts.map +1 -0
- package/dist/notifications/smtp-provider.js +71 -0
- package/dist/notifications/smtp-provider.js.map +1 -0
- package/dist/ops/deploymentConfig.d.ts +73 -0
- package/dist/ops/deploymentConfig.d.ts.map +1 -0
- package/dist/ops/deploymentConfig.js +154 -0
- package/dist/ops/deploymentConfig.js.map +1 -0
- package/dist/ops/index.d.ts +1 -0
- package/dist/ops/index.d.ts.map +1 -1
- package/dist/ops/index.js +3 -2
- package/dist/ops/index.js.map +1 -1
- package/package.json +13 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAiC,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface SmtpConfig {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
secure: boolean;
|
|
5
|
+
auth?: {
|
|
6
|
+
user: string;
|
|
7
|
+
pass: string;
|
|
8
|
+
};
|
|
9
|
+
from: string;
|
|
10
|
+
}
|
|
11
|
+
/** The env vars this provider reads. The app passes `process.env` or its framework equivalent. */
|
|
12
|
+
export interface SmtpEnv {
|
|
13
|
+
SMTP_HOST?: string;
|
|
14
|
+
SMTP_PORT?: string;
|
|
15
|
+
SMTP_USER?: string;
|
|
16
|
+
SMTP_PASS?: string;
|
|
17
|
+
SMTP_FROM?: string;
|
|
18
|
+
SMTP_SECURE?: string;
|
|
19
|
+
[key: string]: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read SMTP settings from an env bag. Taken as a parameter rather than read
|
|
23
|
+
* from `process.env` directly: under `vite dev` the framework does not mirror
|
|
24
|
+
* `.env` into `process.env`, so the app must hand over its own resolved env
|
|
25
|
+
* (`$env/dynamic/private` in SvelteKit) or overrides silently fall back to
|
|
26
|
+
* defaults.
|
|
27
|
+
*
|
|
28
|
+
* Returns null when mail is not configured — the caller treats that as "no
|
|
29
|
+
* mail", not as an error.
|
|
30
|
+
*
|
|
31
|
+
* Throws only when `SMTP_HOST` is set but `SMTP_FROM` is missing: that
|
|
32
|
+
* combination is a half-finished config, and failing loudly beats silently
|
|
33
|
+
* never sending. Every message needs an envelope sender, and guessing one
|
|
34
|
+
* produces mail that fails SPF at the recipient.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readSmtpConfig(env: SmtpEnv): SmtpConfig | null;
|
|
37
|
+
//# sourceMappingURL=smtp-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp-config.d.ts","sourceRoot":"","sources":["../../src/notifications/smtp-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC;CACb;AAED,kGAAkG;AAClG,MAAM,WAAW,OAAO;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IAKrB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAClC;AAOD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAsB9D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function parsePort(raw) {
|
|
2
|
+
const n = Number(raw ?? 587);
|
|
3
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? n : 587;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Read SMTP settings from an env bag. Taken as a parameter rather than read
|
|
7
|
+
* from `process.env` directly: under `vite dev` the framework does not mirror
|
|
8
|
+
* `.env` into `process.env`, so the app must hand over its own resolved env
|
|
9
|
+
* (`$env/dynamic/private` in SvelteKit) or overrides silently fall back to
|
|
10
|
+
* defaults.
|
|
11
|
+
*
|
|
12
|
+
* Returns null when mail is not configured — the caller treats that as "no
|
|
13
|
+
* mail", not as an error.
|
|
14
|
+
*
|
|
15
|
+
* Throws only when `SMTP_HOST` is set but `SMTP_FROM` is missing: that
|
|
16
|
+
* combination is a half-finished config, and failing loudly beats silently
|
|
17
|
+
* never sending. Every message needs an envelope sender, and guessing one
|
|
18
|
+
* produces mail that fails SPF at the recipient.
|
|
19
|
+
*/
|
|
20
|
+
export function readSmtpConfig(env) {
|
|
21
|
+
const host = env.SMTP_HOST?.trim();
|
|
22
|
+
if (!host)
|
|
23
|
+
return null;
|
|
24
|
+
const from = env.SMTP_FROM?.trim();
|
|
25
|
+
if (!from) {
|
|
26
|
+
throw new Error('SMTP_HOST is set but SMTP_FROM is missing — mail needs a sender address.');
|
|
27
|
+
}
|
|
28
|
+
const port = parsePort(env.SMTP_PORT);
|
|
29
|
+
const user = env.SMTP_USER?.trim();
|
|
30
|
+
const pass = env.SMTP_PASS;
|
|
31
|
+
return {
|
|
32
|
+
host,
|
|
33
|
+
port,
|
|
34
|
+
// Port 465 is implicit TLS; 587 and 25 start plaintext and upgrade via
|
|
35
|
+
// STARTTLS, which nodemailer does on its own when `secure` is false.
|
|
36
|
+
secure: env.SMTP_SECURE ? env.SMTP_SECURE === 'true' : port === 465,
|
|
37
|
+
auth: user && pass ? { user, pass } : undefined,
|
|
38
|
+
from
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=smtp-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp-config.js","sourceRoot":"","sources":["../../src/notifications/smtp-config.ts"],"names":[],"mappings":"AAuBA,SAAS,SAAS,CAAC,GAAuB;IACzC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY;IAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC;IAE3B,OAAO;QACN,IAAI;QACJ,IAAI;QACJ,uEAAuE;QACvE,qEAAqE;QACrE,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG;QACnE,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;QAC/C,IAAI;KACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ILogger, type INotificationProvider, type OutboundMessage, type SendResult } from '@selvajs/platform';
|
|
2
|
+
import { type SmtpEnv } from './smtp-config.js';
|
|
3
|
+
/**
|
|
4
|
+
* SMTP transport. Any server works — a transactional provider, an internal
|
|
5
|
+
* relay, or a mailbox with an app password.
|
|
6
|
+
*
|
|
7
|
+
* Settings are read per send rather than cached, so a restart is the only
|
|
8
|
+
* thing needed to reconfigure and a rotated password takes effect without
|
|
9
|
+
* anyone remembering there is a cache.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SmtpNotificationProvider implements INotificationProvider {
|
|
12
|
+
private readonly env;
|
|
13
|
+
readonly name = "SMTP";
|
|
14
|
+
constructor(env: SmtpEnv);
|
|
15
|
+
send(message: OutboundMessage, log?: ILogger): Promise<SendResult>;
|
|
16
|
+
isConfigured(): boolean;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=smtp-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp-provider.d.ts","sourceRoot":"","sources":["../../src/notifications/smtp-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAkB,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,wBAAyB,YAAW,qBAAqB;IAGzD,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,QAAQ,CAAC,IAAI,UAAU;gBAEM,GAAG,EAAE,OAAO;IAEnC,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,GAAE,OAA0B,GAAG,OAAO,CAAC,UAAU,CAAC;IA6C1F,YAAY,IAAI,OAAO;CAOvB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NoopLogger } from '@selvajs/platform';
|
|
2
|
+
import { readSmtpConfig } from './smtp-config.js';
|
|
3
|
+
/**
|
|
4
|
+
* SMTP transport. Any server works — a transactional provider, an internal
|
|
5
|
+
* relay, or a mailbox with an app password.
|
|
6
|
+
*
|
|
7
|
+
* Settings are read per send rather than cached, so a restart is the only
|
|
8
|
+
* thing needed to reconfigure and a rotated password takes effect without
|
|
9
|
+
* anyone remembering there is a cache.
|
|
10
|
+
*/
|
|
11
|
+
export class SmtpNotificationProvider {
|
|
12
|
+
env;
|
|
13
|
+
name = 'SMTP';
|
|
14
|
+
constructor(env) {
|
|
15
|
+
this.env = env;
|
|
16
|
+
}
|
|
17
|
+
async send(message, log = new NoopLogger()) {
|
|
18
|
+
let config;
|
|
19
|
+
try {
|
|
20
|
+
config = readSmtpConfig(this.env);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
log.error('SMTP is misconfigured', {
|
|
24
|
+
component: 'SmtpNotificationProvider',
|
|
25
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26
|
+
});
|
|
27
|
+
return { status: 'failed', reason: 'misconfigured' };
|
|
28
|
+
}
|
|
29
|
+
if (!config)
|
|
30
|
+
return { status: 'not-configured' };
|
|
31
|
+
try {
|
|
32
|
+
// Lazy: an instance with no SMTP_HOST never pays to load nodemailer.
|
|
33
|
+
const { createTransport } = await import('nodemailer');
|
|
34
|
+
const transport = createTransport({
|
|
35
|
+
host: config.host,
|
|
36
|
+
port: config.port,
|
|
37
|
+
secure: config.secure,
|
|
38
|
+
auth: config.auth
|
|
39
|
+
});
|
|
40
|
+
await transport.sendMail({
|
|
41
|
+
from: config.from,
|
|
42
|
+
to: message.to,
|
|
43
|
+
subject: message.subject,
|
|
44
|
+
text: message.text,
|
|
45
|
+
html: message.html
|
|
46
|
+
});
|
|
47
|
+
return { status: 'sent' };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
51
|
+
// Recipient is personal data — logged at warn only, as the minimum
|
|
52
|
+
// needed to diagnose a bounce. Never the subject or body.
|
|
53
|
+
log.warn('Failed to send mail', {
|
|
54
|
+
component: 'SmtpNotificationProvider',
|
|
55
|
+
to: message.to,
|
|
56
|
+
kind: message.kind,
|
|
57
|
+
reason
|
|
58
|
+
});
|
|
59
|
+
return { status: 'failed', reason };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
isConfigured() {
|
|
63
|
+
try {
|
|
64
|
+
return readSmtpConfig(this.env) !== null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=smtp-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp-provider.js","sourceRoot":"","sources":["../../src/notifications/smtp-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,UAAU,EAKV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAgB,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,OAAO,wBAAwB;IAGP;IAFpB,IAAI,GAAG,MAAM,CAAC;IAEvB,YAA6B,GAAY;QAAZ,QAAG,GAAH,GAAG,CAAS;IAAG,CAAC;IAE7C,KAAK,CAAC,IAAI,CAAC,OAAwB,EAAE,MAAe,IAAI,UAAU,EAAE;QACnE,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACJ,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,GAAG,CAAC,KAAK,CAAC,uBAAuB,EAAE;gBAClC,SAAS,EAAE,0BAA0B;gBACrC,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACvD,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;QACtD,CAAC;QACD,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAEjD,IAAI,CAAC;YACJ,qEAAqE;YACrE,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YACvD,MAAM,SAAS,GAAG,eAAe,CAAC;gBACjC,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,MAAM,CAAC,IAAI;aACjB,CAAC,CAAC;YAEH,MAAM,SAAS,CAAC,QAAQ,CAAC;gBACxB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,IAAI,EAAE,OAAO,CAAC,IAAI;aAClB,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,mEAAmE;YACnE,0DAA0D;YAC1D,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE;gBAC/B,SAAS,EAAE,0BAA0B;gBACrC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,MAAM;aACN,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACrC,CAAC;IACF,CAAC;IAED,YAAY;QACX,IAAI,CAAC;YACJ,OAAO,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC;CACD"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse-proxy and body-limit configuration rules, evaluated against a plain
|
|
3
|
+
* env bag. Pure predicates — no filesystem, no network, no process.
|
|
4
|
+
*
|
|
5
|
+
* These settings are read by adapter-node, not by Selva, so getting them wrong
|
|
6
|
+
* is invisible: nothing throws, nothing logs, and the deployment serves traffic
|
|
7
|
+
* while a whole class of requests fails. `selva doctor` reports them on the
|
|
8
|
+
* host; the admin health panel runs the same rules so an operator who never
|
|
9
|
+
* opens a shell still sees them.
|
|
10
|
+
*
|
|
11
|
+
* `packages/cli/src/checks/config.js` carries a duplicate of every rule here.
|
|
12
|
+
* Duplicated because the CLI is dependency-free by design — it scaffolds the
|
|
13
|
+
* deployment that installs this runtime, so it cannot import from it. A shared
|
|
14
|
+
* fixture table (`ops/__tests__/deployment-config-fixtures.js`) is asserted by
|
|
15
|
+
* both test suites, so a rule changed on one side fails CI on the other.
|
|
16
|
+
*/
|
|
17
|
+
/** Neutral severity. Callers map it onto their own presentation. */
|
|
18
|
+
export type ConfigVerdict = 'ok' | 'warn' | 'fail';
|
|
19
|
+
export interface ConfigFinding {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
verdict: ConfigVerdict;
|
|
23
|
+
summary: string;
|
|
24
|
+
/** Concrete next step. Absent when the verdict is `ok`. */
|
|
25
|
+
remediation?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Just the vars these rules read. Values are raw strings, as they come from `.env`. */
|
|
28
|
+
export interface DeploymentEnv {
|
|
29
|
+
ORIGIN?: string;
|
|
30
|
+
ADDRESS_HEADER?: string;
|
|
31
|
+
XFF_DEPTH?: string;
|
|
32
|
+
BODY_SIZE_LIMIT?: string;
|
|
33
|
+
COMPUTE_REQUEST_MAX_BYTES?: string;
|
|
34
|
+
[key: string]: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse a `BODY_SIZE_LIMIT` the way adapter-node does, so this agrees with the
|
|
38
|
+
* thing that actually rejects the request.
|
|
39
|
+
*
|
|
40
|
+
* The quirk is load-bearing: adapter-node reads only the LAST character as the
|
|
41
|
+
* suffix, so `60mb` parses as the digits `60m` and happens to mean 60 MB, while
|
|
42
|
+
* `60xy` silently becomes NaN. Returns bytes, or null when the value is one
|
|
43
|
+
* adapter-node can't use.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseBodySizeLimit(raw: string | undefined | null): number | null;
|
|
46
|
+
/**
|
|
47
|
+
* BODY_SIZE_LIMIT has to clear the compute request cap, or adapter-node's global
|
|
48
|
+
* backstop rejects the upload first — with a non-JSON body the app never sees
|
|
49
|
+
* and cannot log. Unset is the dangerous case, not a neutral one: adapter-node
|
|
50
|
+
* falls back to 512 KB, so every upload 413s on a deployment that looks fine.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkBodySizeLimit(env: DeploymentEnv): ConfigFinding;
|
|
53
|
+
/**
|
|
54
|
+
* `ADDRESS_HEADER` / `XFF_DEPTH` are read by adapter-node, not by Selva, and
|
|
55
|
+
* unset they are invisible: nothing fails, nothing logs, and the deployment
|
|
56
|
+
* looks healthy.
|
|
57
|
+
*
|
|
58
|
+
* What actually happens is that `getClientAddress()` returns the socket peer,
|
|
59
|
+
* which behind a reverse proxy is the proxy — `127.0.0.1` for every request
|
|
60
|
+
* from every user. Login rate limiting keys on that, so the whole instance
|
|
61
|
+
* shares one bucket: five failed logins from anywhere return 429 to everyone,
|
|
62
|
+
* and only a *successful* login clears a bucket, which nobody can then reach.
|
|
63
|
+
*
|
|
64
|
+
* `ORIGIN` is the proxy tell. It is only needed when the app is served under a
|
|
65
|
+
* public URL it can't derive from its own socket, so its presence means a proxy
|
|
66
|
+
* is terminating requests. Without it we say nothing: on a directly-reachable
|
|
67
|
+
* app these settings are a footgun, since `X-Forwarded-For` is client-supplied
|
|
68
|
+
* and anyone could then pick their own rate-limit bucket.
|
|
69
|
+
*/
|
|
70
|
+
export declare function checkClientAddress(env: DeploymentEnv): ConfigFinding;
|
|
71
|
+
/** Every deployment-config rule, in report order. */
|
|
72
|
+
export declare function checkDeploymentConfig(env: DeploymentEnv): ConfigFinding[];
|
|
73
|
+
//# sourceMappingURL=deploymentConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploymentConfig.d.ts","sourceRoot":"","sources":["../../src/ops/deploymentConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,oEAAoE;AACpE,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAKnC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAUhF;AAOD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,CA0CpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,CAwDpE;AAED,qDAAqD;AACrD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,EAAE,CAEzE"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse-proxy and body-limit configuration rules, evaluated against a plain
|
|
3
|
+
* env bag. Pure predicates — no filesystem, no network, no process.
|
|
4
|
+
*
|
|
5
|
+
* These settings are read by adapter-node, not by Selva, so getting them wrong
|
|
6
|
+
* is invisible: nothing throws, nothing logs, and the deployment serves traffic
|
|
7
|
+
* while a whole class of requests fails. `selva doctor` reports them on the
|
|
8
|
+
* host; the admin health panel runs the same rules so an operator who never
|
|
9
|
+
* opens a shell still sees them.
|
|
10
|
+
*
|
|
11
|
+
* `packages/cli/src/checks/config.js` carries a duplicate of every rule here.
|
|
12
|
+
* Duplicated because the CLI is dependency-free by design — it scaffolds the
|
|
13
|
+
* deployment that installs this runtime, so it cannot import from it. A shared
|
|
14
|
+
* fixture table (`ops/__tests__/deployment-config-fixtures.js`) is asserted by
|
|
15
|
+
* both test suites, so a rule changed on one side fails CI on the other.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Parse a `BODY_SIZE_LIMIT` the way adapter-node does, so this agrees with the
|
|
19
|
+
* thing that actually rejects the request.
|
|
20
|
+
*
|
|
21
|
+
* The quirk is load-bearing: adapter-node reads only the LAST character as the
|
|
22
|
+
* suffix, so `60mb` parses as the digits `60m` and happens to mean 60 MB, while
|
|
23
|
+
* `60xy` silently becomes NaN. Returns bytes, or null when the value is one
|
|
24
|
+
* adapter-node can't use.
|
|
25
|
+
*/
|
|
26
|
+
export function parseBodySizeLimit(raw) {
|
|
27
|
+
if (raw == null || raw === '')
|
|
28
|
+
return null;
|
|
29
|
+
const value = String(raw).trim();
|
|
30
|
+
if (value === '')
|
|
31
|
+
return null;
|
|
32
|
+
const units = { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024 };
|
|
33
|
+
const multiplier = units[value.at(-1).toUpperCase()] ?? 1;
|
|
34
|
+
const bytes = Number(multiplier !== 1 ? value.slice(0, -1) : value) * multiplier;
|
|
35
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
36
|
+
return null;
|
|
37
|
+
return bytes;
|
|
38
|
+
}
|
|
39
|
+
/** The app's own default for COMPUTE_REQUEST_MAX_BYTES (compute/limits.ts). */
|
|
40
|
+
const COMPUTE_REQUEST_DEFAULT_BYTES = 256 * 1024 * 1024;
|
|
41
|
+
const asMb = (bytes) => `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
42
|
+
/**
|
|
43
|
+
* BODY_SIZE_LIMIT has to clear the compute request cap, or adapter-node's global
|
|
44
|
+
* backstop rejects the upload first — with a non-JSON body the app never sees
|
|
45
|
+
* and cannot log. Unset is the dangerous case, not a neutral one: adapter-node
|
|
46
|
+
* falls back to 512 KB, so every upload 413s on a deployment that looks fine.
|
|
47
|
+
*/
|
|
48
|
+
export function checkBodySizeLimit(env) {
|
|
49
|
+
const base = { id: 'body-size-limit', label: 'Upload size limit' };
|
|
50
|
+
const raw = env.BODY_SIZE_LIMIT;
|
|
51
|
+
const requestCap = Number(env.COMPUTE_REQUEST_MAX_BYTES) || COMPUTE_REQUEST_DEFAULT_BYTES;
|
|
52
|
+
if (raw == null || raw === '') {
|
|
53
|
+
return {
|
|
54
|
+
...base,
|
|
55
|
+
verdict: 'fail',
|
|
56
|
+
summary: 'BODY_SIZE_LIMIT is unset — adapter-node falls back to 512 KB, so uploads over ' +
|
|
57
|
+
'that fail with an opaque 413 no app log records.',
|
|
58
|
+
remediation: `Set BODY_SIZE_LIMIT to at least ${asMb(requestCap)} (the compute request cap) and restart.`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const bytes = parseBodySizeLimit(raw);
|
|
62
|
+
if (bytes === null) {
|
|
63
|
+
return {
|
|
64
|
+
...base,
|
|
65
|
+
verdict: 'fail',
|
|
66
|
+
summary: `BODY_SIZE_LIMIT="${raw}" — adapter-node cannot parse this and throws on boot.`,
|
|
67
|
+
remediation: 'Use a byte count or a single K/M/G suffix, e.g. "256M".'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (bytes < requestCap) {
|
|
71
|
+
return {
|
|
72
|
+
...base,
|
|
73
|
+
verdict: 'warn',
|
|
74
|
+
summary: `BODY_SIZE_LIMIT=${raw} (${asMb(bytes)}) is below COMPUTE_REQUEST_MAX_BYTES ` +
|
|
75
|
+
`(${asMb(requestCap)}) — the global cap rejects first, making the compute cap dead config.`,
|
|
76
|
+
remediation: `Raise BODY_SIZE_LIMIT to at least ${asMb(requestCap)} and restart.`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...base,
|
|
81
|
+
verdict: 'ok',
|
|
82
|
+
summary: `BODY_SIZE_LIMIT=${raw} clears the compute request cap.`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `ADDRESS_HEADER` / `XFF_DEPTH` are read by adapter-node, not by Selva, and
|
|
87
|
+
* unset they are invisible: nothing fails, nothing logs, and the deployment
|
|
88
|
+
* looks healthy.
|
|
89
|
+
*
|
|
90
|
+
* What actually happens is that `getClientAddress()` returns the socket peer,
|
|
91
|
+
* which behind a reverse proxy is the proxy — `127.0.0.1` for every request
|
|
92
|
+
* from every user. Login rate limiting keys on that, so the whole instance
|
|
93
|
+
* shares one bucket: five failed logins from anywhere return 429 to everyone,
|
|
94
|
+
* and only a *successful* login clears a bucket, which nobody can then reach.
|
|
95
|
+
*
|
|
96
|
+
* `ORIGIN` is the proxy tell. It is only needed when the app is served under a
|
|
97
|
+
* public URL it can't derive from its own socket, so its presence means a proxy
|
|
98
|
+
* is terminating requests. Without it we say nothing: on a directly-reachable
|
|
99
|
+
* app these settings are a footgun, since `X-Forwarded-For` is client-supplied
|
|
100
|
+
* and anyone could then pick their own rate-limit bucket.
|
|
101
|
+
*/
|
|
102
|
+
export function checkClientAddress(env) {
|
|
103
|
+
const base = { id: 'client-address', label: 'Client IP behind proxy' };
|
|
104
|
+
const header = env.ADDRESS_HEADER;
|
|
105
|
+
const depth = env.XFF_DEPTH;
|
|
106
|
+
if (!env.ORIGIN) {
|
|
107
|
+
return {
|
|
108
|
+
...base,
|
|
109
|
+
verdict: 'ok',
|
|
110
|
+
summary: 'ADDRESS_HEADER is not needed — ORIGIN is unset, so no reverse proxy is in play.'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!header) {
|
|
114
|
+
return {
|
|
115
|
+
...base,
|
|
116
|
+
verdict: 'fail',
|
|
117
|
+
summary: 'ADDRESS_HEADER is unset while ORIGIN is set — every request looks like it came from ' +
|
|
118
|
+
'the proxy, so all users share ONE login rate-limit bucket. Five failed logins from ' +
|
|
119
|
+
'anyone locks out the whole instance for 15 minutes.',
|
|
120
|
+
remediation: 'Set ADDRESS_HEADER=X-Forwarded-For and XFF_DEPTH=<number of proxies>, then restart. ' +
|
|
121
|
+
'To count your proxies, see docs/self-hosting/deployment/reverse-proxy.md.'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (header.toLowerCase() === 'x-forwarded-for' && !depth) {
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
verdict: 'warn',
|
|
128
|
+
summary: `ADDRESS_HEADER=${header} but XFF_DEPTH is unset. X-Forwarded-For is a ` +
|
|
129
|
+
'client-appendable list, so a caller can prepend fake entries — adapter-node needs ' +
|
|
130
|
+
'the proxy count to know which entry is real.',
|
|
131
|
+
remediation: 'Set XFF_DEPTH=1 for a single reverse proxy, 2 if a CDN or load balancer sits in ' +
|
|
132
|
+
'front of it. Unsure? docs/self-hosting/deployment/reverse-proxy.md shows how to ' +
|
|
133
|
+
'read the header the app actually receives.'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (depth !== undefined && !/^[1-9][0-9]*$/.test(depth)) {
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
verdict: 'fail',
|
|
140
|
+
summary: `XFF_DEPTH="${depth}" is not a positive integer.`,
|
|
141
|
+
remediation: 'It counts proxies from the outside in — use 1 for a single reverse proxy.'
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
...base,
|
|
146
|
+
verdict: 'ok',
|
|
147
|
+
summary: `ADDRESS_HEADER=${header}${depth ? ` (XFF_DEPTH=${depth})` : ''}.`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Every deployment-config rule, in report order. */
|
|
151
|
+
export function checkDeploymentConfig(env) {
|
|
152
|
+
return [checkClientAddress(env), checkBodySizeLimit(env)];
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=deploymentConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploymentConfig.js","sourceRoot":"","sources":["../../src/ops/deploymentConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA4BH;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAA8B;IAChE,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAE9B,MAAM,KAAK,GAA2B,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;IACzF,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC;IACjF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,KAAK,CAAC;AACd,CAAC;AAED,+EAA+E;AAC/E,MAAM,6BAA6B,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAExD,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAkB;IACpD,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IACnE,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC;IAChC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,IAAI,6BAA6B,CAAC;IAE1F,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAC/B,OAAO;YACN,GAAG,IAAI;YACP,OAAO,EAAE,MAAM;YACf,OAAO,EACN,gFAAgF;gBAChF,kDAAkD;YACnD,WAAW,EAAE,mCAAmC,IAAI,CAAC,UAAU,CAAC,yCAAyC;SACzG,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO;YACN,GAAG,IAAI;YACP,OAAO,EAAE,MAAM;YACf,OAAO,EAAE,oBAAoB,GAAG,wDAAwD;YACxF,WAAW,EAAE,yDAAyD;SACtE,CAAC;IACH,CAAC;IAED,IAAI,KAAK,GAAG,UAAU,EAAE,CAAC;QACxB,OAAO;YACN,GAAG,IAAI;YACP,OAAO,EAAE,MAAM;YACf,OAAO,EACN,mBAAmB,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,uCAAuC;gBAC7E,IAAI,IAAI,CAAC,UAAU,CAAC,uEAAuE;YAC5F,WAAW,EAAE,qCAAqC,IAAI,CAAC,UAAU,CAAC,eAAe;SACjF,CAAC;IACH,CAAC;IAED,OAAO;QACN,GAAG,IAAI;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,mBAAmB,GAAG,kCAAkC;KACjE,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAkB;IACpD,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC;IACvE,MAAM,MAAM,GAAG,GAAG,CAAC,cAAc,CAAC;IAClC,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC;IAE5B,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QACjB,OAAO;YACN,GAAG,IAAI;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,iFAAiF;SAC1F,CAAC;IACH,CAAC;IAED,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO;YACN,GAAG,IAAI;YACP,OAAO,EAAE,MAAM;YACf,OAAO,EACN,sFAAsF;gBACtF,qFAAqF;gBACrF,qDAAqD;YACtD,WAAW,EACV,sFAAsF;gBACtF,2EAA2E;SAC5E,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,iBAAiB,IAAI,CAAC,KAAK,EAAE,CAAC;QAC1D,OAAO;YACN,GAAG,IAAI;YACP,OAAO,EAAE,MAAM;YACf,OAAO,EACN,kBAAkB,MAAM,gDAAgD;gBACxE,oFAAoF;gBACpF,8CAA8C;YAC/C,WAAW,EACV,kFAAkF;gBAClF,kFAAkF;gBAClF,4CAA4C;SAC7C,CAAC;IACH,CAAC;IAED,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,OAAO;YACN,GAAG,IAAI;YACP,OAAO,EAAE,MAAM;YACf,OAAO,EAAE,cAAc,KAAK,8BAA8B;YAC1D,WAAW,EAAE,2EAA2E;SACxF,CAAC;IACH,CAAC;IAED,OAAO;QACN,GAAG,IAAI;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,kBAAkB,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,eAAe,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG;KAC3E,CAAC;AACH,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,qBAAqB,CAAC,GAAkB;IACvD,OAAO,CAAC,kBAAkB,CAAC,GAAG,CAAC,EAAE,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3D,CAAC"}
|
package/dist/ops/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { parseSemver, isNewer, compareCore, satisfiesRange, type ReleaseChannel } from './semver.js';
|
|
2
|
+
export { checkClientAddress, checkBodySizeLimit, checkDeploymentConfig, parseBodySizeLimit, type ConfigFinding, type ConfigVerdict, type DeploymentEnv } from './deploymentConfig.js';
|
|
2
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/ops/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,WAAW,EACX,OAAO,EACP,WAAW,EACX,cAAc,EACd,KAAK,cAAc,EACnB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,WAAW,EACX,OAAO,EACP,WAAW,EACX,cAAc,EACd,KAAK,cAAc,EACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC"}
|
package/dist/ops/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Deployment/ops helpers — channel-aware semver comparison
|
|
2
|
-
//
|
|
1
|
+
// Deployment/ops helpers — channel-aware semver comparison, engines.node range
|
|
2
|
+
// checks, and the reverse-proxy/body-limit configuration rules.
|
|
3
3
|
export { parseSemver, isNewer, compareCore, satisfiesRange } from './semver.js';
|
|
4
|
+
export { checkClientAddress, checkBodySizeLimit, checkDeploymentConfig, parseBodySizeLimit } from './deploymentConfig.js';
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
package/dist/ops/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,gEAAgE;AAEhE,OAAO,EACN,WAAW,EACX,OAAO,EACP,WAAW,EACX,cAAc,EAEd,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAIlB,MAAM,uBAAuB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Transport-agnostic server building blocks for apps built on the Selva engine (limits, rate limiting, SSRF guard, remote-definition fetch, definition service + render loader); the solve core lives in @selvajs/solve/server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VektorNode",
|
|
@@ -70,6 +70,11 @@
|
|
|
70
70
|
"selva-source": "./src/ops/index.ts",
|
|
71
71
|
"types": "./dist/ops/index.d.ts",
|
|
72
72
|
"import": "./dist/ops/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./notifications": {
|
|
75
|
+
"selva-source": "./src/notifications/index.ts",
|
|
76
|
+
"types": "./dist/notifications/index.d.ts",
|
|
77
|
+
"import": "./dist/notifications/index.js"
|
|
73
78
|
}
|
|
74
79
|
},
|
|
75
80
|
"files": [
|
|
@@ -79,24 +84,30 @@
|
|
|
79
84
|
"!**/*.spec.*"
|
|
80
85
|
],
|
|
81
86
|
"dependencies": {
|
|
82
|
-
"@selvajs/platform": "^0.19.1",
|
|
83
87
|
"@selvajs/compute": "^4.0.2",
|
|
88
|
+
"@selvajs/platform": "^0.20.0",
|
|
84
89
|
"@selvajs/schemas": "^5.0.1"
|
|
85
90
|
},
|
|
86
91
|
"peerDependencies": {
|
|
87
92
|
"@sentry/node": ">=8",
|
|
93
|
+
"nodemailer": ">=7",
|
|
88
94
|
"pino": ">=9"
|
|
89
95
|
},
|
|
90
96
|
"peerDependenciesMeta": {
|
|
91
97
|
"@sentry/node": {
|
|
92
98
|
"optional": true
|
|
93
99
|
},
|
|
100
|
+
"nodemailer": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
94
103
|
"pino": {
|
|
95
104
|
"optional": true
|
|
96
105
|
}
|
|
97
106
|
},
|
|
98
107
|
"devDependencies": {
|
|
99
108
|
"@types/node": "^26.2.0",
|
|
109
|
+
"@types/nodemailer": "^8.0.1",
|
|
110
|
+
"nodemailer": "^9.0.5",
|
|
100
111
|
"typescript": "~6.0.3",
|
|
101
112
|
"vitest": "^4.1.10",
|
|
102
113
|
"@selvajs/config": "0.0.4"
|