@percher/shared 1.1.0 → 1.3.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/data-filter.d.ts +27 -0
- package/dist/data-filter.d.ts.map +1 -0
- package/dist/data-filter.js +288 -0
- package/dist/data-filter.js.map +1 -0
- package/dist/data-records.d.ts +359 -0
- package/dist/data-records.d.ts.map +1 -0
- package/dist/data-records.js +124 -0
- package/dist/data-records.js.map +1 -0
- package/dist/deployment-security.d.ts +32 -0
- package/dist/deployment-security.d.ts.map +1 -0
- package/dist/deployment-security.js +146 -0
- package/dist/deployment-security.js.map +1 -0
- package/dist/disposable-email-domains.d.ts +11 -0
- package/dist/disposable-email-domains.d.ts.map +1 -0
- package/dist/disposable-email-domains.js +112 -0
- package/dist/disposable-email-domains.js.map +1 -0
- package/dist/email-budget.d.ts +60 -0
- package/dist/email-budget.d.ts.map +1 -0
- package/dist/email-budget.js +135 -0
- package/dist/email-budget.js.map +1 -0
- package/dist/email-change.d.ts +11 -0
- package/dist/email-change.d.ts.map +1 -0
- package/dist/email-change.js +23 -0
- package/dist/email-change.js.map +1 -0
- package/dist/email-template.d.ts +19 -0
- package/dist/email-template.d.ts.map +1 -0
- package/dist/email-template.js +53 -0
- package/dist/email-template.js.map +1 -0
- package/dist/image-rejection.d.ts +45 -0
- package/dist/image-rejection.d.ts.map +1 -0
- package/dist/image-rejection.js +27 -0
- package/dist/image-rejection.js.map +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/internal-sig.d.ts +2 -2
- package/dist/internal-sig.js +2 -2
- package/dist/ip-prefix.d.ts +43 -0
- package/dist/ip-prefix.d.ts.map +1 -0
- package/dist/ip-prefix.js +225 -0
- package/dist/ip-prefix.js.map +1 -0
- package/dist/legal-changes.d.ts +4 -2
- package/dist/legal-changes.d.ts.map +1 -1
- package/dist/legal-changes.js +124 -0
- package/dist/legal-changes.js.map +1 -1
- package/dist/legal.d.ts +14 -14
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +13 -13
- package/dist/legal.js.map +1 -1
- package/dist/limits.d.ts +11 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -0
- package/dist/limits.js.map +1 -1
- package/dist/oauth-scopes.d.ts +8 -0
- package/dist/oauth-scopes.d.ts.map +1 -1
- package/dist/oauth-scopes.js +11 -13
- package/dist/oauth-scopes.js.map +1 -1
- package/dist/platform.d.ts +3 -3
- package/dist/platform.js +3 -3
- package/dist/signup-capacity.d.ts +12 -0
- package/dist/signup-capacity.d.ts.map +1 -0
- package/dist/signup-capacity.js +29 -0
- package/dist/signup-capacity.js.map +1 -0
- package/dist/signup-day.d.ts +7 -0
- package/dist/signup-day.d.ts.map +1 -0
- package/dist/signup-day.js +42 -0
- package/dist/signup-day.js.map +1 -0
- package/dist/switches.d.ts.map +1 -1
- package/dist/switches.js +2 -1
- package/dist/switches.js.map +1 -1
- package/dist/verified-signup.d.ts +24 -0
- package/dist/verified-signup.d.ts.map +1 -0
- package/dist/verified-signup.js +58 -0
- package/dist/verified-signup.js.map +1 -0
- package/dist/waitlist-token.d.ts +49 -0
- package/dist/waitlist-token.d.ts.map +1 -0
- package/dist/waitlist-token.js +71 -0
- package/dist/waitlist-token.js.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const FAILURE_TEXT = {
|
|
2
|
+
SCANNER_UNAVAILABLE: "The security scanner is unavailable.",
|
|
3
|
+
SCANNER_ENGINE_DEGRADED: "A security scanner did not complete successfully.",
|
|
4
|
+
SCANNER_DATABASE_ERROR: "The vulnerability databases could not be verified as current.",
|
|
5
|
+
KEV_CATALOG_STALE: "The known-exploited vulnerability catalogue is not current.",
|
|
6
|
+
IMMUTABLE_SOURCE_UNAVAILABLE: "The exact image could not be identified.",
|
|
7
|
+
IMMUTABLE_SOURCE_CHANGED: "The image changed during security verification.",
|
|
8
|
+
SCAN_EVIDENCE_UNAVAILABLE: "The security scan result could not be saved or verified.",
|
|
9
|
+
SCANNER_EXECUTION_ERROR: "The security scan could not complete.",
|
|
10
|
+
};
|
|
11
|
+
export function deploymentSecurityActionText(action, context) {
|
|
12
|
+
switch (action) {
|
|
13
|
+
case "retry_scan":
|
|
14
|
+
if (context === "automatic_rollback")
|
|
15
|
+
return "Retry rollback only when security verification is available, or publish a verified replacement.";
|
|
16
|
+
return "Retry publishing when security verification is available. The new image was not published.";
|
|
17
|
+
case "update_dependency":
|
|
18
|
+
return "Update or replace the affected dependency, then publish again.";
|
|
19
|
+
case "wait_for_platform_fix":
|
|
20
|
+
return "Percher must fix the affected platform component before this image can be published.";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Fixed authored text only; never interpolate scanner-supplied values here. */
|
|
24
|
+
export function deploymentSecuritySummary(block) {
|
|
25
|
+
const explanation = block.reason === "scan_unavailable"
|
|
26
|
+
? block.scanFailureCode
|
|
27
|
+
? FAILURE_TEXT[block.scanFailureCode]
|
|
28
|
+
: "The image could not be verified by the security scan."
|
|
29
|
+
: "The image contains a critical or known-exploited vulnerability.";
|
|
30
|
+
return block.context === "automatic_rollback"
|
|
31
|
+
? `Automatic rollback stopped: ${explanation} The current version was kept running and may still be unhealthy.`
|
|
32
|
+
: `Publication stopped: ${explanation}`;
|
|
33
|
+
}
|
|
34
|
+
/** Fixed recovery instructions; scanner diagnostics never enter this channel. */
|
|
35
|
+
export function deploymentSecurityRecoveryPrompt(block) {
|
|
36
|
+
if (block.context === "automatic_rollback") {
|
|
37
|
+
return block.reason === "vulnerability"
|
|
38
|
+
? "Automatic rollback was blocked because the previous image has a critical or known-exploited vulnerability. The current version was kept running and may still be unhealthy. Review the findings and publish a fixed replacement, or wait for Percher's platform fix."
|
|
39
|
+
: "Automatic rollback stopped because the previous image could not be verified. The current version was kept running and may still be unhealthy. Retry rollback only when verification is available, or publish a verified replacement.";
|
|
40
|
+
}
|
|
41
|
+
return block.reason === "vulnerability"
|
|
42
|
+
? "Publication was blocked by security policy. Review the vulnerability details, update affected app dependencies or wait for Percher's platform fix before publishing again."
|
|
43
|
+
: "Publication stopped because security verification was unavailable. Retry publishing when verification is available; the scan has not established whether the image is safe.";
|
|
44
|
+
}
|
|
45
|
+
function text(value, max) {
|
|
46
|
+
return (typeof value === "string" &&
|
|
47
|
+
value.length > 0 &&
|
|
48
|
+
value.length <= max &&
|
|
49
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: diagnostic fields cannot forge terminal lines.
|
|
50
|
+
!/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(value));
|
|
51
|
+
}
|
|
52
|
+
/** Validate persisted/wire data without turning it into a recovery instruction. */
|
|
53
|
+
export function parseDeploymentSecurityBlock(value) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
55
|
+
return null;
|
|
56
|
+
const v = value;
|
|
57
|
+
if (v.version !== 1)
|
|
58
|
+
return null;
|
|
59
|
+
if (v.context !== undefined && v.context !== "automatic_rollback")
|
|
60
|
+
return null;
|
|
61
|
+
if (v.reason !== "vulnerability" && v.reason !== "scan_unavailable")
|
|
62
|
+
return null;
|
|
63
|
+
if (v.imageId !== null &&
|
|
64
|
+
(typeof v.imageId !== "string" || !/^sha256:[a-f0-9]{64}$/.test(v.imageId)))
|
|
65
|
+
return null;
|
|
66
|
+
if (!Array.isArray(v.actions) ||
|
|
67
|
+
v.actions.length < 1 ||
|
|
68
|
+
v.actions.length > 3 ||
|
|
69
|
+
!v.actions.every((a) => ["retry_scan", "update_dependency", "wait_for_platform_fix"].includes(a)) ||
|
|
70
|
+
new Set(v.actions).size !== v.actions.length)
|
|
71
|
+
return null;
|
|
72
|
+
if (!Array.isArray(v.findings) ||
|
|
73
|
+
v.findings.length > 100 ||
|
|
74
|
+
!Number.isSafeInteger(v.totalFindings) ||
|
|
75
|
+
v.totalFindings < v.findings.length ||
|
|
76
|
+
typeof v.truncated !== "boolean" ||
|
|
77
|
+
v.truncated !== v.totalFindings > v.findings.length)
|
|
78
|
+
return null;
|
|
79
|
+
if (v.scanFailureCode !== undefined &&
|
|
80
|
+
(v.reason !== "scan_unavailable" ||
|
|
81
|
+
typeof v.scanFailureCode !== "string" ||
|
|
82
|
+
!Object.hasOwn(FAILURE_TEXT, v.scanFailureCode)))
|
|
83
|
+
return null;
|
|
84
|
+
if (v.reason === "scan_unavailable" &&
|
|
85
|
+
(v.totalFindings !== 0 || v.actions.length !== 1 || v.actions[0] !== "retry_scan"))
|
|
86
|
+
return null;
|
|
87
|
+
if (v.reason === "vulnerability" && (v.totalFindings === 0 || v.actions.includes("retry_scan")))
|
|
88
|
+
return null;
|
|
89
|
+
if (v.reason === "vulnerability" && v.imageId === null)
|
|
90
|
+
return null;
|
|
91
|
+
const findings = [];
|
|
92
|
+
for (const item of v.findings) {
|
|
93
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
94
|
+
return null;
|
|
95
|
+
const f = item;
|
|
96
|
+
if (!text(f.cve, 128) ||
|
|
97
|
+
!text(f.component, 512) ||
|
|
98
|
+
!["Critical", "High", "Medium", "Low"].includes(f.severity) ||
|
|
99
|
+
(f.version !== null && !text(f.version, 512)) ||
|
|
100
|
+
(f.fixedIn !== null && !text(f.fixedIn, 2048)) ||
|
|
101
|
+
typeof f.exploitedKev !== "boolean" ||
|
|
102
|
+
!["user", "percher"].includes(f.responsibility))
|
|
103
|
+
return null;
|
|
104
|
+
findings.push({
|
|
105
|
+
cve: f.cve,
|
|
106
|
+
component: f.component,
|
|
107
|
+
version: f.version,
|
|
108
|
+
fixedIn: f.fixedIn,
|
|
109
|
+
severity: f.severity,
|
|
110
|
+
exploitedKev: f.exploitedKev,
|
|
111
|
+
responsibility: f.responsibility,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (v.reason === "vulnerability") {
|
|
115
|
+
if (findings.length === 0 || findings.some((f) => f.severity !== "Critical" && !f.exploitedKev))
|
|
116
|
+
return null;
|
|
117
|
+
const required = new Set(findings.map((f) => f.responsibility === "percher" ? "wait_for_platform_fix" : "update_dependency"));
|
|
118
|
+
if ([...required].some((action) => !v.actions.includes(action)))
|
|
119
|
+
return null;
|
|
120
|
+
if (!v.truncated &&
|
|
121
|
+
v.actions.some((action) => !required.has(action)))
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
version: 1,
|
|
126
|
+
...(v.context === "automatic_rollback" ? { context: "automatic_rollback" } : {}),
|
|
127
|
+
reason: v.reason,
|
|
128
|
+
imageId: v.imageId,
|
|
129
|
+
actions: v.actions,
|
|
130
|
+
findings,
|
|
131
|
+
totalFindings: v.totalFindings,
|
|
132
|
+
truncated: v.truncated,
|
|
133
|
+
...(v.scanFailureCode ? { scanFailureCode: v.scanFailureCode } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function decodeDeploymentSecurityBlock(json) {
|
|
137
|
+
if (!json || json.length > 400_000)
|
|
138
|
+
return null;
|
|
139
|
+
try {
|
|
140
|
+
return parseDeploymentSecurityBlock(JSON.parse(json));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=deployment-security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-security.js","sourceRoot":"","sources":["../src/deployment-security.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the domain portion of `email` is a known disposable /
|
|
3
|
+
* throwaway address. Trims + lowercases + reads the part after the last
|
|
4
|
+
* `@`. Returns false on inputs that don't look like an email address
|
|
5
|
+
* (no `@`, empty domain) so the caller can still hand the value to
|
|
6
|
+
* BetterAuth's own validation for a proper error message.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isDisposableEmailDomain(email: string): boolean;
|
|
9
|
+
/** Number of entries in the in-memory blocklist — exposed for tests. */
|
|
10
|
+
export declare function disposableDomainCount(): number;
|
|
11
|
+
//# sourceMappingURL=disposable-email-domains.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disposable-email-domains.d.ts","sourceRoot":"","sources":["../src/disposable-email-domains.ts"],"names":[],"mappings":"AAsFA;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAY9D;AAED,wEAAwE;AACxE,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Disposable / throwaway email domain blocklist.
|
|
2
|
+
//
|
|
3
|
+
// Curated subset of the 4000+ entries from
|
|
4
|
+
// https://github.com/disposable-email-domains/disposable-email-domains
|
|
5
|
+
// (CC0 1.0 / public domain — see that repo's LICENSE). We keep ~50 of the
|
|
6
|
+
// most-abused families that show up in signup spam against small SaaS:
|
|
7
|
+
// mailinator, guerrillamail, 10minutemail, tempmail-style and the common
|
|
8
|
+
// rotated subdomains. The upstream list rolls in long-tail additions
|
|
9
|
+
// constantly; we deliberately stay small here so the runtime check is a
|
|
10
|
+
// hot Set lookup rather than a 4000-string scan, and so we can review
|
|
11
|
+
// every entry. Add more as we see real signup-abuse traffic.
|
|
12
|
+
//
|
|
13
|
+
// Lower-cased, no whitespace. Compared against the part after the last
|
|
14
|
+
// "@" of an email address (also lower-cased + trimmed).
|
|
15
|
+
const DISPOSABLE_DOMAINS = new Set([
|
|
16
|
+
// Mailinator family
|
|
17
|
+
"mailinator.com",
|
|
18
|
+
"mailinator.net",
|
|
19
|
+
"mailinator2.com",
|
|
20
|
+
"binkmail.com",
|
|
21
|
+
"bobmail.info",
|
|
22
|
+
"chammy.info",
|
|
23
|
+
"devnullmail.com",
|
|
24
|
+
"letthemeatspam.com",
|
|
25
|
+
"mailnesia.com",
|
|
26
|
+
"mailtothis.com",
|
|
27
|
+
"notmailinator.com",
|
|
28
|
+
"reallymymail.com",
|
|
29
|
+
"safetymail.info",
|
|
30
|
+
"thisisnotmyrealemail.com",
|
|
31
|
+
"tradermail.info",
|
|
32
|
+
"veryrealemail.com",
|
|
33
|
+
"zippymail.info",
|
|
34
|
+
// Guerrilla Mail
|
|
35
|
+
"guerrillamail.com",
|
|
36
|
+
"guerrillamail.net",
|
|
37
|
+
"guerrillamail.org",
|
|
38
|
+
"guerrillamail.biz",
|
|
39
|
+
"guerrillamail.de",
|
|
40
|
+
"guerrillamailblock.com",
|
|
41
|
+
"sharklasers.com",
|
|
42
|
+
"grr.la",
|
|
43
|
+
"spam4.me",
|
|
44
|
+
// 10MinuteMail / temp-mail style
|
|
45
|
+
"10minutemail.com",
|
|
46
|
+
"10minutemail.net",
|
|
47
|
+
"20minutemail.com",
|
|
48
|
+
"30minutemail.com",
|
|
49
|
+
"tempmail.com",
|
|
50
|
+
"temp-mail.org",
|
|
51
|
+
"temp-mail.io",
|
|
52
|
+
"tempmailaddress.com",
|
|
53
|
+
"tempmailo.com",
|
|
54
|
+
"tempinbox.com",
|
|
55
|
+
"throwawaymail.com",
|
|
56
|
+
"yopmail.com",
|
|
57
|
+
"yopmail.fr",
|
|
58
|
+
"yopmail.net",
|
|
59
|
+
"maildrop.cc",
|
|
60
|
+
"trashmail.com",
|
|
61
|
+
"trashmail.de",
|
|
62
|
+
"trashmail.net",
|
|
63
|
+
"trash-mail.com",
|
|
64
|
+
"dispostable.com",
|
|
65
|
+
"fakeinbox.com",
|
|
66
|
+
"getnada.com",
|
|
67
|
+
"mintemail.com",
|
|
68
|
+
"mohmal.com",
|
|
69
|
+
"moakt.cc",
|
|
70
|
+
// Misc high-volume disposables seen in signup abuse
|
|
71
|
+
"formtests.info",
|
|
72
|
+
"mailcatch.com",
|
|
73
|
+
"mailnull.com",
|
|
74
|
+
"spambog.com",
|
|
75
|
+
"spambox.us",
|
|
76
|
+
"spamgourmet.com",
|
|
77
|
+
"emailondeck.com",
|
|
78
|
+
"fakemailgenerator.com",
|
|
79
|
+
"jetable.org",
|
|
80
|
+
"mytrashmail.com",
|
|
81
|
+
"owlpic.com",
|
|
82
|
+
"rcpt.at",
|
|
83
|
+
"trbvm.com",
|
|
84
|
+
]);
|
|
85
|
+
/**
|
|
86
|
+
* Returns true if the domain portion of `email` is a known disposable /
|
|
87
|
+
* throwaway address. Trims + lowercases + reads the part after the last
|
|
88
|
+
* `@`. Returns false on inputs that don't look like an email address
|
|
89
|
+
* (no `@`, empty domain) so the caller can still hand the value to
|
|
90
|
+
* BetterAuth's own validation for a proper error message.
|
|
91
|
+
*/
|
|
92
|
+
export function isDisposableEmailDomain(email) {
|
|
93
|
+
if (!email)
|
|
94
|
+
return false;
|
|
95
|
+
const at = email.lastIndexOf("@");
|
|
96
|
+
if (at <= 0 || at === email.length - 1)
|
|
97
|
+
return false;
|
|
98
|
+
const domain = email
|
|
99
|
+
.slice(at + 1)
|
|
100
|
+
.trim()
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
.replace(/\.+$/, "");
|
|
103
|
+
if (!domain)
|
|
104
|
+
return false;
|
|
105
|
+
const labels = domain.split(".");
|
|
106
|
+
return labels.some((_, index) => DISPOSABLE_DOMAINS.has(labels.slice(index).join(".")));
|
|
107
|
+
}
|
|
108
|
+
/** Number of entries in the in-memory blocklist — exposed for tests. */
|
|
109
|
+
export function disposableDomainCount() {
|
|
110
|
+
return DISPOSABLE_DOMAINS.size;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=disposable-email-domains.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disposable-email-domains.js","sourceRoot":"","sources":["../src/disposable-email-domains.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,2CAA2C;AAC3C,uEAAuE;AACvE,0EAA0E;AAC1E,uEAAuE;AACvE,yEAAyE;AACzE,qEAAqE;AACrE,wEAAwE;AACxE,sEAAsE;AACtE,6DAA6D;AAC7D,EAAE;AACF,uEAAuE;AACvE,wDAAwD;AAExD,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC;IACtD,oBAAoB;IACpB,gBAAgB;IAChB,gBAAgB;IAChB,iBAAiB;IACjB,cAAc;IACd,cAAc;IACd,aAAa;IACb,iBAAiB;IACjB,oBAAoB;IACpB,eAAe;IACf,gBAAgB;IAChB,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;IACjB,0BAA0B;IAC1B,iBAAiB;IACjB,mBAAmB;IACnB,gBAAgB;IAChB,iBAAiB;IACjB,mBAAmB;IACnB,mBAAmB;IACnB,mBAAmB;IACnB,mBAAmB;IACnB,kBAAkB;IAClB,wBAAwB;IACxB,iBAAiB;IACjB,QAAQ;IACR,UAAU;IACV,iCAAiC;IACjC,kBAAkB;IAClB,kBAAkB;IAClB,kBAAkB;IAClB,kBAAkB;IAClB,cAAc;IACd,eAAe;IACf,cAAc;IACd,qBAAqB;IACrB,eAAe;IACf,eAAe;IACf,mBAAmB;IACnB,aAAa;IACb,YAAY;IACZ,aAAa;IACb,aAAa;IACb,eAAe;IACf,cAAc;IACd,eAAe;IACf,gBAAgB;IAChB,iBAAiB;IACjB,eAAe;IACf,aAAa;IACb,eAAe;IACf,YAAY;IACZ,UAAU;IACV,oDAAoD;IACpD,gBAAgB;IAChB,eAAe;IACf,cAAc;IACd,aAAa;IACb,YAAY;IACZ,iBAAiB;IACjB,iBAAiB;IACjB,uBAAuB;IACvB,aAAa;IACb,iBAAiB;IACjB,YAAY;IACZ,SAAS;IACT,WAAW;CACZ,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,MAAM,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACrD,MAAM,MAAM,GAAG,KAAK;SACjB,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;SACb,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvB,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,qBAAqB;IACnC,OAAO,kBAAkB,CAAC,IAAI,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Resend free tier: 100/day, 3,000/month. UTC calendar windows. */
|
|
2
|
+
export declare const EMAIL_BUDGET_LIMITS: {
|
|
3
|
+
readonly daily: 95;
|
|
4
|
+
readonly monthly: 2900;
|
|
5
|
+
readonly notificationDaily: 45;
|
|
6
|
+
readonly crashPerAppDaily: 3;
|
|
7
|
+
};
|
|
8
|
+
export type EmailCategory = "auth" | "operator" | "legal" | "notification";
|
|
9
|
+
interface EmailBudgetCounts {
|
|
10
|
+
total: number;
|
|
11
|
+
auth: number;
|
|
12
|
+
operator: number;
|
|
13
|
+
legal: number;
|
|
14
|
+
notification: number;
|
|
15
|
+
}
|
|
16
|
+
export interface EmailBudgetConnection {
|
|
17
|
+
run(query: string, params?: (string | number)[]): void;
|
|
18
|
+
/** Drizzle's raw Bun SQLite queries return positional rows. */
|
|
19
|
+
get<T extends unknown[]>(query: string, params?: (string | number)[]): T | undefined;
|
|
20
|
+
}
|
|
21
|
+
interface EmailBudgetDatabase extends EmailBudgetConnection {
|
|
22
|
+
immediate<T>(work: (connection: EmailBudgetConnection) => T): T;
|
|
23
|
+
}
|
|
24
|
+
interface EmailBudgetDecision {
|
|
25
|
+
allowed: boolean;
|
|
26
|
+
category: EmailCategory;
|
|
27
|
+
dayKey: string;
|
|
28
|
+
reason?: "notification_daily" | "daily" | "monthly" | "crash_daily";
|
|
29
|
+
overCeiling: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Dashboard-only startup must coexist with the API's migration. */
|
|
32
|
+
export declare const EMAIL_BUDGET_SCHEMA = "\nCREATE TABLE IF NOT EXISTS email_budget_counters (\n period TEXT PRIMARY KEY NOT NULL,\n total INTEGER NOT NULL DEFAULT 0,\n auth INTEGER NOT NULL DEFAULT 0,\n operator INTEGER NOT NULL DEFAULT 0,\n legal INTEGER NOT NULL DEFAULT 0,\n notification INTEGER NOT NULL DEFAULT 0,\n warning INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS email_budget_crashes (\n day TEXT NOT NULL,\n app_id TEXT NOT NULL,\n count INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (day, app_id)\n);";
|
|
33
|
+
/** Attempts, not successful deliveries: uncertain sends never release capacity. */
|
|
34
|
+
export declare class EmailBudget {
|
|
35
|
+
private readonly db;
|
|
36
|
+
private readonly now;
|
|
37
|
+
onWarning?: (usage: ReturnType<EmailBudget["usage"]>) => Promise<unknown>;
|
|
38
|
+
constructor(db: EmailBudgetDatabase, now?: () => Date);
|
|
39
|
+
reserve(category: EmailCategory, options?: {
|
|
40
|
+
count?: number;
|
|
41
|
+
crashAppId?: string;
|
|
42
|
+
}): EmailBudgetDecision;
|
|
43
|
+
usage(at?: Date): {
|
|
44
|
+
dayKey: string;
|
|
45
|
+
monthKey: string;
|
|
46
|
+
day: EmailBudgetCounts;
|
|
47
|
+
month: EmailBudgetCounts;
|
|
48
|
+
limits: {
|
|
49
|
+
readonly daily: 95;
|
|
50
|
+
readonly monthly: 2900;
|
|
51
|
+
readonly notificationDaily: 45;
|
|
52
|
+
readonly crashPerAppDaily: 3;
|
|
53
|
+
};
|
|
54
|
+
uncountedOperatorSenders: string[];
|
|
55
|
+
};
|
|
56
|
+
/** The dashboard leaves a durable pending warning; API monitoring drains it. */
|
|
57
|
+
dispatchWarning(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=email-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-budget.d.ts","sourceRoot":"","sources":["../src/email-budget.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,eAAO,MAAM,mBAAmB;aAC9B,KAAK,EAAE,EAAE;aACT,OAAO,EAAE,IAAK;aACd,iBAAiB,EAAE,EAAE;aACrB,gBAAgB,EAAE,CAAC;CACX,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,cAAc,CAAC;AAC3E,UAAU,iBAAiB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AACD,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC;IACvD,+DAA+D;IAC/D,GAAG,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;CACtF;AACD,UAAU,mBAAoB,SAAQ,qBAAqB;IACzD,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,UAAU,EAAE,qBAAqB,KAAK,CAAC,GAAG,CAAC,CAAC;CACjE;AACD,UAAU,mBAAmB;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;IACpE,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,oEAAoE;AACpE,eAAO,MAAM,mBAAmB,kfAe7B,CAAC;AAoBJ,mFAAmF;AACnF,qBAAa,WAAW;IAIpB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJtB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1E,YACmB,EAAE,EAAE,mBAAmB,EACvB,GAAG,GAAE,MAAM,IAAuB,EACjD;IAEJ,OAAO,CACL,QAAQ,EAAE,aAAa,EACvB,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,GACpD,mBAAmB,CAgErB;IAED,KAAK,CAAC,EAAE,OAAa;;;;;;4BAhJd,EAAE;8BACA,IAAK;wCACK,EAAE;uCACH,CAAC;;;MAwJlB;IAED,gFAAgF;IAC1E,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAarC;CACF"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Resend free tier: 100/day, 3,000/month. UTC calendar windows. */
|
|
2
|
+
export const EMAIL_BUDGET_LIMITS = {
|
|
3
|
+
daily: 95,
|
|
4
|
+
monthly: 2_900,
|
|
5
|
+
notificationDaily: 45,
|
|
6
|
+
crashPerAppDaily: 3,
|
|
7
|
+
};
|
|
8
|
+
/** Dashboard-only startup must coexist with the API's migration. */
|
|
9
|
+
export const EMAIL_BUDGET_SCHEMA = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS email_budget_counters (
|
|
11
|
+
period TEXT PRIMARY KEY NOT NULL,
|
|
12
|
+
total INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
auth INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
operator INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
legal INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
notification INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
warning INTEGER NOT NULL DEFAULT 0
|
|
18
|
+
);
|
|
19
|
+
CREATE TABLE IF NOT EXISTS email_budget_crashes (
|
|
20
|
+
day TEXT NOT NULL,
|
|
21
|
+
app_id TEXT NOT NULL,
|
|
22
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
PRIMARY KEY (day, app_id)
|
|
24
|
+
);`;
|
|
25
|
+
const emptyCounts = () => ({
|
|
26
|
+
total: 0,
|
|
27
|
+
auth: 0,
|
|
28
|
+
operator: 0,
|
|
29
|
+
legal: 0,
|
|
30
|
+
notification: 0,
|
|
31
|
+
});
|
|
32
|
+
function readCounts(db, period) {
|
|
33
|
+
const row = db.get("SELECT total, auth, operator, legal, notification FROM email_budget_counters WHERE period = ?", [period]);
|
|
34
|
+
if (!row)
|
|
35
|
+
return emptyCounts();
|
|
36
|
+
const [total, auth, operator, legal, notification] = row;
|
|
37
|
+
return { total, auth, operator, legal, notification };
|
|
38
|
+
}
|
|
39
|
+
/** Attempts, not successful deliveries: uncertain sends never release capacity. */
|
|
40
|
+
export class EmailBudget {
|
|
41
|
+
db;
|
|
42
|
+
now;
|
|
43
|
+
onWarning;
|
|
44
|
+
constructor(db, now = () => new Date()) {
|
|
45
|
+
this.db = db;
|
|
46
|
+
this.now = now;
|
|
47
|
+
}
|
|
48
|
+
reserve(category, options = {}) {
|
|
49
|
+
if (!["auth", "operator", "legal", "notification"].includes(category))
|
|
50
|
+
throw new Error("Invalid email category");
|
|
51
|
+
const count = options.count ?? 1;
|
|
52
|
+
if (!Number.isSafeInteger(count) || count < 1)
|
|
53
|
+
throw new Error("Invalid email recipient count");
|
|
54
|
+
if (options.crashAppId !== undefined && (category !== "notification" || !options.crashAppId))
|
|
55
|
+
throw new Error("Invalid crash email category or app");
|
|
56
|
+
const dayKey = this.now().toISOString().slice(0, 10);
|
|
57
|
+
const periods = [`day:${dayKey}`, `month:${dayKey.slice(0, 7)}`];
|
|
58
|
+
const decision = this.db.immediate((db) => {
|
|
59
|
+
for (const period of periods)
|
|
60
|
+
db.run("INSERT OR IGNORE INTO email_budget_counters (period) VALUES (?)", [period]);
|
|
61
|
+
const day = readCounts(db, periods[0]);
|
|
62
|
+
const month = readCounts(db, periods[1]);
|
|
63
|
+
const reason = day.total + count > EMAIL_BUDGET_LIMITS.daily
|
|
64
|
+
? "daily"
|
|
65
|
+
: month.total + count > EMAIL_BUDGET_LIMITS.monthly
|
|
66
|
+
? "monthly"
|
|
67
|
+
: category === "notification" &&
|
|
68
|
+
day.notification + count > EMAIL_BUDGET_LIMITS.notificationDaily
|
|
69
|
+
? "notification_daily"
|
|
70
|
+
: undefined;
|
|
71
|
+
if (category === "notification" && reason)
|
|
72
|
+
return { allowed: false, category, dayKey, reason, overCeiling: false };
|
|
73
|
+
if (options.crashAppId !== undefined) {
|
|
74
|
+
const crash = db.get("SELECT count FROM email_budget_crashes WHERE day = ? AND app_id = ?", [dayKey, options.crashAppId]);
|
|
75
|
+
if ((crash?.[0] ?? 0) >= EMAIL_BUDGET_LIMITS.crashPerAppDaily)
|
|
76
|
+
return {
|
|
77
|
+
allowed: false,
|
|
78
|
+
category,
|
|
79
|
+
dayKey,
|
|
80
|
+
reason: "crash_daily",
|
|
81
|
+
overCeiling: false,
|
|
82
|
+
};
|
|
83
|
+
db.run("INSERT INTO email_budget_crashes (day, app_id, count) VALUES (?, ?, 1) ON CONFLICT(day, app_id) DO UPDATE SET count = count + 1", [dayKey, options.crashAppId]);
|
|
84
|
+
}
|
|
85
|
+
for (const period of periods)
|
|
86
|
+
db.run(`UPDATE email_budget_counters SET total = total + ?, ${category} = ${category} + ? WHERE period = ?`, [count, count, period]);
|
|
87
|
+
if (reason)
|
|
88
|
+
db.run("UPDATE email_budget_counters SET warning = 1 WHERE period = ? AND warning = 0", [
|
|
89
|
+
periods[0],
|
|
90
|
+
]);
|
|
91
|
+
return { allowed: true, category, dayKey, reason, overCeiling: reason !== undefined };
|
|
92
|
+
});
|
|
93
|
+
if (!decision.allowed || decision.overCeiling) {
|
|
94
|
+
const line = JSON.stringify({
|
|
95
|
+
event: decision.allowed ? "email_budget_priority_overflow" : "email_budget_skipped",
|
|
96
|
+
...decision,
|
|
97
|
+
crashAppId: options.crashAppId,
|
|
98
|
+
});
|
|
99
|
+
if (decision.overCeiling)
|
|
100
|
+
console.error(line);
|
|
101
|
+
else
|
|
102
|
+
console.warn(line);
|
|
103
|
+
}
|
|
104
|
+
return decision;
|
|
105
|
+
}
|
|
106
|
+
usage(at = this.now()) {
|
|
107
|
+
const dayKey = at.toISOString().slice(0, 10);
|
|
108
|
+
const monthKey = dayKey.slice(0, 7);
|
|
109
|
+
return {
|
|
110
|
+
dayKey,
|
|
111
|
+
monthKey,
|
|
112
|
+
day: readCounts(this.db, `day:${dayKey}`),
|
|
113
|
+
month: readCounts(this.db, `month:${monthKey}`),
|
|
114
|
+
limits: EMAIL_BUDGET_LIMITS,
|
|
115
|
+
uncountedOperatorSenders: ["infra/scripts/lib/backup-alert.py"],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** The dashboard leaves a durable pending warning; API monitoring drains it. */
|
|
119
|
+
async dispatchWarning() {
|
|
120
|
+
if (!this.onWarning)
|
|
121
|
+
return;
|
|
122
|
+
const claimed = this.db.get("UPDATE email_budget_counters SET warning = 2 WHERE period = (SELECT period FROM email_budget_counters WHERE period LIKE 'day:%' AND warning = 1 ORDER BY period LIMIT 1) AND warning = 1 RETURNING period");
|
|
123
|
+
if (!claimed)
|
|
124
|
+
return;
|
|
125
|
+
const [period] = claimed;
|
|
126
|
+
// Claim before dispatch: an operator warning itself goes through this budget.
|
|
127
|
+
try {
|
|
128
|
+
await this.onWarning(this.usage(new Date(`${period.slice(4)}T00:00:00Z`)));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
console.error(JSON.stringify({ event: "email_budget_warning_failed", period }));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=email-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-budget.js","sourceRoot":"","sources":["../src/email-budget.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** How long a change-email link stays usable after it is sent. */
|
|
2
|
+
export declare const EMAIL_CHANGE_LINK_LIFETIME_SECONDS: number;
|
|
3
|
+
/**
|
|
4
|
+
* Pending account email changes. The dashboard writes a row per attempt and
|
|
5
|
+
* the API claims a row in the same transaction that moves the address, so a
|
|
6
|
+
* link is single-use and only the newest one works. Rows cascade away with the
|
|
7
|
+
* auth identity, which is how account erasure removes the addresses they hold.
|
|
8
|
+
* Dashboard-owned like auth_user; the API creates it only in tests.
|
|
9
|
+
*/
|
|
10
|
+
export declare const EMAIL_CHANGE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS account_email_changes (\n id TEXT PRIMARY KEY NOT NULL,\n user_id TEXT NOT NULL,\n from_email TEXT NOT NULL,\n to_email TEXT,\n token_hash TEXT UNIQUE,\n created_at INTEGER NOT NULL,\n completed_at INTEGER,\n FOREIGN KEY (user_id) REFERENCES auth_user(id) ON UPDATE no action ON DELETE cascade\n);\nCREATE INDEX IF NOT EXISTS idx_account_email_changes_user_created\n ON account_email_changes (user_id, created_at);";
|
|
11
|
+
//# sourceMappingURL=email-change.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-change.d.ts","sourceRoot":"","sources":["../src/email-change.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,eAAO,MAAM,kCAAkC,QAAe,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,ycAYkB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** How long a change-email link stays usable after it is sent. */
|
|
2
|
+
export const EMAIL_CHANGE_LINK_LIFETIME_SECONDS = 24 * 60 * 60;
|
|
3
|
+
/**
|
|
4
|
+
* Pending account email changes. The dashboard writes a row per attempt and
|
|
5
|
+
* the API claims a row in the same transaction that moves the address, so a
|
|
6
|
+
* link is single-use and only the newest one works. Rows cascade away with the
|
|
7
|
+
* auth identity, which is how account erasure removes the addresses they hold.
|
|
8
|
+
* Dashboard-owned like auth_user; the API creates it only in tests.
|
|
9
|
+
*/
|
|
10
|
+
export const EMAIL_CHANGE_SCHEMA = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS account_email_changes (
|
|
12
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
13
|
+
user_id TEXT NOT NULL,
|
|
14
|
+
from_email TEXT NOT NULL,
|
|
15
|
+
to_email TEXT,
|
|
16
|
+
token_hash TEXT UNIQUE,
|
|
17
|
+
created_at INTEGER NOT NULL,
|
|
18
|
+
completed_at INTEGER,
|
|
19
|
+
FOREIGN KEY (user_id) REFERENCES auth_user(id) ON UPDATE no action ON DELETE cascade
|
|
20
|
+
);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_account_email_changes_user_created
|
|
22
|
+
ON account_email_changes (user_id, created_at);`;
|
|
23
|
+
//# sourceMappingURL=email-change.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-change.js","sourceRoot":"","sources":["../src/email-change.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;kDAYe,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Fixed first-party asset; the live wordmark identifies us with images blocked.
|
|
2
|
+
* Only its URL ships in npm. The mark artwork stays in the private package. */
|
|
3
|
+
export declare const EMAIL_BRAND_HEADER: string;
|
|
4
|
+
type EmailFooter = "none" | {
|
|
5
|
+
kind: "notifications";
|
|
6
|
+
dashboardUrl: string;
|
|
7
|
+
};
|
|
8
|
+
/** Use the dashboard origin, not an app/deploy path or its query string. */
|
|
9
|
+
export declare function emailNotificationSettingsUrl(dashboardUrl: string): string;
|
|
10
|
+
/** Callers escape values in bodyHtml and explicitly choose the footer policy.
|
|
11
|
+
* Notification settings apply only to mail with an effective preference gate;
|
|
12
|
+
* security/legal notices and signed waitlist removal links keep their own copy. */
|
|
13
|
+
export declare function renderEmail(input: {
|
|
14
|
+
bodyHtml: string;
|
|
15
|
+
footer: EmailFooter;
|
|
16
|
+
lang?: "en" | "sv";
|
|
17
|
+
}): string;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=email-template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-template.d.ts","sourceRoot":"","sources":["../src/email-template.ts"],"names":[],"mappings":"AAGA;+EAC+E;AAC/E,eAAO,MAAM,kBAAkB,QAGxB,CAAC;AAER,KAAK,WAAW,GAAG,MAAM,GAAG;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5E,4EAA4E;AAC5E,wBAAgB,4BAA4B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAUzE;AAED;;mFAEmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACpB,GAAG,MAAM,CA4BT"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BRAND } from "./brand.js";
|
|
2
|
+
import { escapeHtml } from "./escape-html.js";
|
|
3
|
+
/** Fixed first-party asset; the live wordmark identifies us with images blocked.
|
|
4
|
+
* Only its URL ships in npm. The mark artwork stays in the private package. */
|
|
5
|
+
export const EMAIL_BRAND_HEADER = `<p style="margin:0 0 20px;line-height:28px;">
|
|
6
|
+
<img src="https://percher.app/email-mark.png" width="28" height="28" alt="" style="display:inline-block;vertical-align:middle;border:0;margin-right:7px;" />
|
|
7
|
+
<span style="font-size:19px;font-weight:700;letter-spacing:-0.01em;color:${BRAND.ink};vertical-align:middle;">Percher</span>
|
|
8
|
+
</p>`;
|
|
9
|
+
/** Use the dashboard origin, not an app/deploy path or its query string. */
|
|
10
|
+
export function emailNotificationSettingsUrl(dashboardUrl) {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(dashboardUrl);
|
|
13
|
+
if (url.protocol === "https:" || url.protocol === "http:") {
|
|
14
|
+
return `${url.origin}/login?next=%2Faccount%23notifications`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// A malformed dashboard link must not turn the footer into an unsafe URL.
|
|
19
|
+
}
|
|
20
|
+
return "https://percher.app/login?next=%2Faccount%23notifications";
|
|
21
|
+
}
|
|
22
|
+
/** Callers escape values in bodyHtml and explicitly choose the footer policy.
|
|
23
|
+
* Notification settings apply only to mail with an effective preference gate;
|
|
24
|
+
* security/legal notices and signed waitlist removal links keep their own copy. */
|
|
25
|
+
export function renderEmail(input) {
|
|
26
|
+
const footer = input.footer === "none"
|
|
27
|
+
? ""
|
|
28
|
+
: `<p style="margin:24px 0 0;padding-top:16px;border-top:1px solid #e5e5e5;font-size:12px;line-height:1.5;color:#666;"><a href="${escapeHtml(emailNotificationSettingsUrl(input.footer.dashboardUrl))}" style="color:#666;">Manage notifications</a></p>`;
|
|
29
|
+
return `<!doctype html>
|
|
30
|
+
<html lang="${input.lang === "sv" ? "sv" : "en"}">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="utf-8" />
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
34
|
+
<style>
|
|
35
|
+
.percher-email-content h1,.percher-email-content h2{margin:0 0 16px;font-size:22px;line-height:1.3;}
|
|
36
|
+
.percher-email-content pre{white-space:pre-wrap;word-break:break-word;}
|
|
37
|
+
@media only screen and (max-width:480px){.percher-email-content{padding:20px!important;}.percher-email-outer{padding:12px 8px!important;}}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body style="margin:0;padding:0;background:#f5f5f5;">
|
|
41
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;background:#f5f5f5;"><tr><td class="percher-email-outer" align="center" style="padding:24px 12px;">
|
|
42
|
+
<!--[if mso]><table role="presentation" width="600" cellpadding="0" cellspacing="0"><tr><td><![endif]-->
|
|
43
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px;margin:0 auto;background:#fff;border:1px solid #e5e5e5;border-radius:8px;table-layout:fixed;"><tr><td class="percher-email-content" style="padding:28px;font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:1.6;color:${BRAND.ink};overflow-wrap:anywhere;word-break:break-word;">
|
|
44
|
+
${EMAIL_BRAND_HEADER}
|
|
45
|
+
${input.bodyHtml.trim()}
|
|
46
|
+
${footer}
|
|
47
|
+
</td></tr></table>
|
|
48
|
+
<!--[if mso]></td></tr></table><![endif]-->
|
|
49
|
+
</td></tr></table>
|
|
50
|
+
</body>
|
|
51
|
+
</html>`;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=email-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-template.js","sourceRoot":"","sources":["../src/email-template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C;+EAC+E;AAC/E,MAAM,CAAC,MAAM,kBAAkB,GAAG;;+EAE6C,KAAK,CAAC,GAAG;OACjF,CAAC;AAIR,4EAA4E;AAC5E,MAAM,UAAU,4BAA4B,CAAC,YAAoB;IAC/D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,CAAC;QAClC,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAC1D,OAAO,GAAG,GAAG,CAAC,MAAM,wCAAwC,CAAC;QAC/D,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;IAC5E,CAAC;IACD,OAAO,2DAA2D,CAAC;AACrE,CAAC;AAED;;mFAEmF;AACnF,MAAM,UAAU,WAAW,CAAC,KAI3B;IACC,MAAM,MAAM,GACV,KAAK,CAAC,MAAM,KAAK,MAAM;QACrB,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,gIAAgI,UAAU,CAAC,4BAA4B,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,oDAAoD,CAAC;IAC9P,OAAO;cACK,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;;;;;;;;;;;;;yUAa0R,KAAK,CAAC,GAAG;QAC1U,kBAAkB;QAClB,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE;QACrB,MAAM;;;;;QAKN,CAAC;AACT,CAAC"}
|