@ultimat3/mail 1.0.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/LICENSE +21 -0
- package/README.md +120 -0
- package/package.json +39 -0
- package/src/base64.ts +9 -0
- package/src/blocks.d.ts +47 -0
- package/src/blocks.d.ts.map +1 -0
- package/src/blocks.js +23 -0
- package/src/blocks.js.map +1 -0
- package/src/blocks.ts +56 -0
- package/src/catalog.d.ts +6 -0
- package/src/catalog.d.ts.map +1 -0
- package/src/catalog.js +78 -0
- package/src/catalog.js.map +1 -0
- package/src/catalog.ts +88 -0
- package/src/driver-env.ts +105 -0
- package/src/driver-resend.ts +184 -0
- package/src/driver-smtp.ts +209 -0
- package/src/driver.d.ts +79 -0
- package/src/driver.d.ts.map +1 -0
- package/src/driver.js +115 -0
- package/src/driver.js.map +1 -0
- package/src/driver.ts +174 -0
- package/src/errors.d.ts +23 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +82 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +177 -0
- package/src/html.d.ts +10 -0
- package/src/html.d.ts.map +1 -0
- package/src/html.js +34 -0
- package/src/html.js.map +1 -0
- package/src/html.ts +37 -0
- package/src/idempotency.ts +61 -0
- package/src/index.d.ts +17 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +13 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +110 -0
- package/src/job.d.ts +17 -0
- package/src/job.d.ts.map +1 -0
- package/src/job.js +79 -0
- package/src/job.js.map +1 -0
- package/src/job.ts +36 -0
- package/src/layout.d.ts +41 -0
- package/src/layout.d.ts.map +1 -0
- package/src/layout.js +160 -0
- package/src/layout.js.map +1 -0
- package/src/layout.ts +221 -0
- package/src/mail.d.ts +67 -0
- package/src/mail.d.ts.map +1 -0
- package/src/mail.js +119 -0
- package/src/mail.js.map +1 -0
- package/src/mail.ts +195 -0
- package/src/mime.ts +233 -0
- package/src/render.d.ts +26 -0
- package/src/render.d.ts.map +1 -0
- package/src/render.js +149 -0
- package/src/render.js.map +1 -0
- package/src/render.ts +190 -0
- package/src/smtp-client.ts +246 -0
- package/src/smtp-protocol.ts +184 -0
- package/src/smtp-socket.ts +266 -0
- package/src/templates/index.d.ts +9 -0
- package/src/templates/index.d.ts.map +1 -0
- package/src/templates/index.js +23 -0
- package/src/templates/index.js.map +1 -0
- package/src/templates/index.ts +39 -0
- package/src/templates/invite.d.ts +15 -0
- package/src/templates/invite.d.ts.map +1 -0
- package/src/templates/invite.js +25 -0
- package/src/templates/invite.js.map +1 -0
- package/src/templates/invite.ts +29 -0
- package/src/templates/mfa-enrolled.d.ts +14 -0
- package/src/templates/mfa-enrolled.d.ts.map +1 -0
- package/src/templates/mfa-enrolled.js +26 -0
- package/src/templates/mfa-enrolled.js.map +1 -0
- package/src/templates/mfa-enrolled.ts +37 -0
- package/src/templates/reset-password.d.ts +13 -0
- package/src/templates/reset-password.d.ts.map +1 -0
- package/src/templates/reset-password.js +23 -0
- package/src/templates/reset-password.js.map +1 -0
- package/src/templates/reset-password.ts +27 -0
- package/src/templates/security-alert.d.ts +16 -0
- package/src/templates/security-alert.d.ts.map +1 -0
- package/src/templates/security-alert.js +29 -0
- package/src/templates/security-alert.js.map +1 -0
- package/src/templates/security-alert.ts +36 -0
- package/src/templates/verify-email.d.ts +13 -0
- package/src/templates/verify-email.d.ts.map +1 -0
- package/src/templates/verify-email.js +23 -0
- package/src/templates/verify-email.js.map +1 -0
- package/src/templates/verify-email.ts +27 -0
- package/src/templates/welcome.d.ts +13 -0
- package/src/templates/welcome.d.ts.map +1 -0
- package/src/templates/welcome.js +20 -0
- package/src/templates/welcome.js.map +1 -0
- package/src/templates/welcome.ts +24 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Single responsibility: the public API of @ultimat3/mail. Explicit named exports only —
|
|
2
|
+
// other packages call `defineMail`, `send` and the driver seam, and nothing else.
|
|
3
|
+
export { blocks } from './blocks';
|
|
4
|
+
export { MAIL_CATALOG, MAIL_CATALOG_SOURCE, registerMailCatalog } from './catalog';
|
|
5
|
+
export { createLogDriver, createMemoryDriver, createResendDriver, createSmtpDriver, mailDriver, messageHeaders, resetMailDriver, setMailDriver, tryMailDriver, } from './driver';
|
|
6
|
+
export { driverUnavailable, layoutUnknown, localeMissing, MAIL_ERROR_CODES, MAIL_ERROR_TITLES, MailError, mailDuplicate, templateUnknown, textMissing, transportNotImplemented, } from './errors';
|
|
7
|
+
export { escapeHtml, safeUrl } from './html';
|
|
8
|
+
export { mailIdempotencyKey, mailMessageSchema, sendMailJob } from './job';
|
|
9
|
+
export { BASE_LAYOUT, baseLayout, DARK_RULES, darkModeCss, layoutFor, MAIL_FONT_STACK, MAIL_TOKENS, MAIL_WIDTH_PX, registeredLayouts, registerLayout, token, } from './layout';
|
|
10
|
+
export { defineMail, mailFor, registeredMailIds, registeredMails, renderMessage, resetMails, send, sendById, } from './mail';
|
|
11
|
+
export { FOOTER_KEYS, renderMail, textOf, UNSUBSCRIBE_KEY } from './render';
|
|
12
|
+
export { FRAMEWORK_MAILS, inviteInput, inviteMail, MFA_METHODS, mfaEnrolledInput, mfaEnrolledMail, resetPasswordInput, resetPasswordMail, securityAlertInput, securityAlertMail, verifyEmailInput, verifyEmailMail, welcomeInput, welcomeMail, } from './templates';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,kFAAkF;AAGlF,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAUnF,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,eAAe,EACf,WAAW,EACX,uBAAuB,GACxB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAS3E,OAAO,EACL,WAAW,EACX,UAAU,EACV,UAAU,EACV,WAAW,EACX,SAAS,EACT,eAAe,EACf,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,KAAK,GACN,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,UAAU,EACV,OAAO,EACP,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,UAAU,EACV,IAAI,EACJ,QAAQ,GACT,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE5E,OAAO,EACL,eAAe,EAEf,WAAW,EACX,UAAU,EACV,WAAW,EAEX,gBAAgB,EAChB,eAAe,EAEf,kBAAkB,EAClB,iBAAiB,EAEjB,kBAAkB,EAClB,iBAAiB,EAEjB,gBAAgB,EAChB,eAAe,EAEf,YAAY,EACZ,WAAW,GACZ,MAAM,aAAa,CAAC"}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Single responsibility: the public API of @ultimat3/mail. Explicit named exports only —
|
|
2
|
+
// other packages call `defineMail`, `send` and the driver seam, and nothing else.
|
|
3
|
+
|
|
4
|
+
/** Re-exported so a `defineMail` file needs one import, not two. Same object as schema's. */
|
|
5
|
+
export type { Infer } from '@ultimat3/schema';
|
|
6
|
+
export { t } from '@ultimat3/schema';
|
|
7
|
+
export type { CalloutTone, MailBlock, MailTemplate, TemplateArgs } from './blocks';
|
|
8
|
+
export { blocks } from './blocks';
|
|
9
|
+
|
|
10
|
+
export { MAIL_CATALOG, MAIL_CATALOG_SOURCE, registerMailCatalog } from './catalog';
|
|
11
|
+
export type {
|
|
12
|
+
MailDriver,
|
|
13
|
+
MailMessage,
|
|
14
|
+
MemoryMailDriver,
|
|
15
|
+
SendResult,
|
|
16
|
+
SentMail,
|
|
17
|
+
} from './driver';
|
|
18
|
+
export {
|
|
19
|
+
createLogDriver,
|
|
20
|
+
createMemoryDriver,
|
|
21
|
+
envelopeRecipients,
|
|
22
|
+
isMemoryDriver,
|
|
23
|
+
mailDriver,
|
|
24
|
+
messageHeaders,
|
|
25
|
+
resetMailDriver,
|
|
26
|
+
setMailDriver,
|
|
27
|
+
tryMailDriver,
|
|
28
|
+
} from './driver';
|
|
29
|
+
export type { MailEnvironment, MailSelection } from './driver-env';
|
|
30
|
+
export { MAIL_ENV_KEYS, selectMailDriver } from './driver-env';
|
|
31
|
+
export type { MailFetch, ResendDriverOptions } from './driver-resend';
|
|
32
|
+
export { createResendDriver, RESEND_BASE_URL } from './driver-resend';
|
|
33
|
+
export type { SmtpDriverOptions } from './driver-smtp';
|
|
34
|
+
export { createSmtpDriver } from './driver-smtp';
|
|
35
|
+
export type { MailErrorCode, MailErrorInit, SendFailure, SendStage } from './errors';
|
|
36
|
+
export {
|
|
37
|
+
driverUnavailable,
|
|
38
|
+
layoutUnknown,
|
|
39
|
+
localeMissing,
|
|
40
|
+
MAIL_ERROR_CODES,
|
|
41
|
+
MAIL_ERROR_TITLES,
|
|
42
|
+
MailError,
|
|
43
|
+
mailDuplicate,
|
|
44
|
+
sendFailed,
|
|
45
|
+
templateUnknown,
|
|
46
|
+
textMissing,
|
|
47
|
+
} from './errors';
|
|
48
|
+
|
|
49
|
+
export { escapeHtml, safeUrl } from './html';
|
|
50
|
+
|
|
51
|
+
export { mailIdempotencyKey } from './idempotency';
|
|
52
|
+
export { mailMessageSchema, sendMailJob } from './job';
|
|
53
|
+
export type {
|
|
54
|
+
ColorScheme,
|
|
55
|
+
DarkRule,
|
|
56
|
+
LayoutInput,
|
|
57
|
+
MailLayout,
|
|
58
|
+
MailToken,
|
|
59
|
+
UnsubscribeSlot,
|
|
60
|
+
} from './layout';
|
|
61
|
+
export {
|
|
62
|
+
BASE_LAYOUT,
|
|
63
|
+
baseLayout,
|
|
64
|
+
DARK_RULES,
|
|
65
|
+
darkModeCss,
|
|
66
|
+
layoutFor,
|
|
67
|
+
MAIL_FONT_STACK,
|
|
68
|
+
MAIL_TOKENS,
|
|
69
|
+
MAIL_WIDTH_PX,
|
|
70
|
+
registeredLayouts,
|
|
71
|
+
registerLayout,
|
|
72
|
+
token,
|
|
73
|
+
} from './layout';
|
|
74
|
+
export type { AnyMailDefinition, MailDefinition, MailInit, SendOptions } from './mail';
|
|
75
|
+
export {
|
|
76
|
+
defineMail,
|
|
77
|
+
mailFor,
|
|
78
|
+
registeredMailIds,
|
|
79
|
+
registeredMails,
|
|
80
|
+
renderMessage,
|
|
81
|
+
resetMails,
|
|
82
|
+
send,
|
|
83
|
+
sendById,
|
|
84
|
+
} from './mail';
|
|
85
|
+
export type { RenderableMail, RenderedMail, RenderOptions } from './render';
|
|
86
|
+
export { FOOTER_KEYS, renderMail, textOf, UNSUBSCRIBE_KEY } from './render';
|
|
87
|
+
export type { SmtpConnector, SmtpStream } from './smtp-client';
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
FRAMEWORK_MAILS,
|
|
91
|
+
type InviteInput,
|
|
92
|
+
inviteInput,
|
|
93
|
+
inviteMail,
|
|
94
|
+
MFA_METHODS,
|
|
95
|
+
type MfaEnrolledInput,
|
|
96
|
+
mfaEnrolledInput,
|
|
97
|
+
mfaEnrolledMail,
|
|
98
|
+
type ResetPasswordInput,
|
|
99
|
+
resetPasswordInput,
|
|
100
|
+
resetPasswordMail,
|
|
101
|
+
type SecurityAlertInput,
|
|
102
|
+
securityAlertInput,
|
|
103
|
+
securityAlertMail,
|
|
104
|
+
type VerifyEmailInput,
|
|
105
|
+
verifyEmailInput,
|
|
106
|
+
verifyEmailMail,
|
|
107
|
+
type WelcomeInput,
|
|
108
|
+
welcomeInput,
|
|
109
|
+
welcomeMail,
|
|
110
|
+
} from './templates';
|
package/src/job.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type JobHandle } from '@ultimat3/jobs';
|
|
2
|
+
import { type StandardSchemaV1 } from '@ultimat3/schema';
|
|
3
|
+
import { type MailMessage } from './driver';
|
|
4
|
+
/** The queue payload is the already-rendered envelope: rendering happens once, at send time. */
|
|
5
|
+
export declare const mailMessageSchema: StandardSchemaV1<unknown, MailMessage>;
|
|
6
|
+
/**
|
|
7
|
+
* `(mailId, recipients, hash(rendered payload))`, or the caller's key when supplied.
|
|
8
|
+
* Content-derived on purpose: a retry of the same request produces the same key, while an
|
|
9
|
+
* intentional resend with different content produces a different one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mailIdempotencyKey(message: MailMessage): string;
|
|
12
|
+
/**
|
|
13
|
+
* Five attempts with exponential backoff: transient 4xx/greylisting clears in minutes, and a
|
|
14
|
+
* hard bounce still lands in the dead-letter queue where `x jobs dead --json` can show it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const sendMailJob: JobHandle<MailMessage>;
|
|
17
|
+
//# sourceMappingURL=job.d.ts.map
|
package/src/job.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job.d.ts","sourceRoot":"","sources":["job.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAO,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,KAAK,gBAAgB,EAAK,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,KAAK,WAAW,EAA+B,MAAM,UAAU,CAAC;AAEzE,gGAAgG;AAChG,eAAO,MAAM,iBAAiB,EAAE,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAanE,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAiB/D;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,CAAC,WAAW,CAM7C,CAAC"}
|
package/src/job.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Single responsibility: delivering a rendered message is background work, never request work.
|
|
2
|
+
// A slow SMTP host must not hold a request open, and at-least-once delivery must not mean two
|
|
3
|
+
// welcome emails — so the send is a `job` with a content-derived idempotency key.
|
|
4
|
+
import { job } from '@ultimat3/jobs';
|
|
5
|
+
import { t } from '@ultimat3/schema';
|
|
6
|
+
import { mailDriver } from './driver';
|
|
7
|
+
/** The queue payload is the already-rendered envelope: rendering happens once, at send time. */
|
|
8
|
+
export const mailMessageSchema = t.object({
|
|
9
|
+
mailId: t.string,
|
|
10
|
+
to: t.array(t.email),
|
|
11
|
+
subject: t.string,
|
|
12
|
+
html: t.string,
|
|
13
|
+
text: t.string,
|
|
14
|
+
locale: t.locale,
|
|
15
|
+
tz: t.timezone,
|
|
16
|
+
replyTo: t.email.optional(),
|
|
17
|
+
cc: t.array(t.email).optional(),
|
|
18
|
+
bcc: t.array(t.email).optional(),
|
|
19
|
+
unsubscribeUrl: t.url.optional(),
|
|
20
|
+
idempotencyKey: t.string.optional(),
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* `(mailId, recipients, hash(rendered payload))`, or the caller's key when supplied.
|
|
24
|
+
* Content-derived on purpose: a retry of the same request produces the same key, while an
|
|
25
|
+
* intentional resend with different content produces a different one.
|
|
26
|
+
*/
|
|
27
|
+
export function mailIdempotencyKey(message) {
|
|
28
|
+
const explicit = message.idempotencyKey;
|
|
29
|
+
if (explicit !== undefined && explicit !== '')
|
|
30
|
+
return `mail:${explicit}`;
|
|
31
|
+
const recipients = [...message.to].map((address) => address.toLowerCase()).sort();
|
|
32
|
+
const digest = fnv1a32(stableStringify({
|
|
33
|
+
subject: message.subject,
|
|
34
|
+
html: message.html,
|
|
35
|
+
text: message.text,
|
|
36
|
+
cc: message.cc ?? [],
|
|
37
|
+
bcc: message.bcc ?? [],
|
|
38
|
+
locale: message.locale,
|
|
39
|
+
tz: message.tz,
|
|
40
|
+
unsubscribeUrl: message.unsubscribeUrl ?? '',
|
|
41
|
+
}));
|
|
42
|
+
return `mail:${message.mailId}:${recipients.join(',')}:${digest}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Five attempts with exponential backoff: transient 4xx/greylisting clears in minutes, and a
|
|
46
|
+
* hard bounce still lands in the dead-letter queue where `x jobs dead --json` can show it.
|
|
47
|
+
*/
|
|
48
|
+
export const sendMailJob = job({
|
|
49
|
+
name: 'mail.send',
|
|
50
|
+
input: mailMessageSchema,
|
|
51
|
+
idempotencyKey: mailIdempotencyKey,
|
|
52
|
+
retry: { attempts: 5, backoff: 'exponential' },
|
|
53
|
+
run: ({ input }) => mailDriver().send(input),
|
|
54
|
+
});
|
|
55
|
+
/** Key order is normalised so two structurally equal payloads hash identically. */
|
|
56
|
+
function stableStringify(value) {
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
59
|
+
if (value !== null && typeof value === 'object') {
|
|
60
|
+
const entries = Object.entries(value)
|
|
61
|
+
.filter(([, entry]) => entry !== undefined)
|
|
62
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
63
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`);
|
|
64
|
+
return `{${entries.join(',')}}`;
|
|
65
|
+
}
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return 'null';
|
|
68
|
+
return JSON.stringify(value);
|
|
69
|
+
}
|
|
70
|
+
/** FNV-1a, 32-bit. Not a security hash — a short, dependency-free, stable content id. */
|
|
71
|
+
function fnv1a32(input) {
|
|
72
|
+
let hash = 0x811c9dc5;
|
|
73
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
74
|
+
hash ^= input.charCodeAt(index);
|
|
75
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
76
|
+
}
|
|
77
|
+
return hash.toString(16).padStart(8, '0');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=job.js.map
|
package/src/job.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job.js","sourceRoot":"","sources":["job.ts"],"names":[],"mappings":"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"}
|
package/src/job.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Single responsibility: delivering a rendered message is background work, never request work.
|
|
2
|
+
// A slow SMTP host must not hold a request open, and at-least-once delivery must not mean two
|
|
3
|
+
// welcome emails — so the send is a `job` keyed by the content-derived idempotency key.
|
|
4
|
+
|
|
5
|
+
import { type JobHandle, job } from '@ultimat3/jobs';
|
|
6
|
+
import { type StandardSchemaV1, t } from '@ultimat3/schema';
|
|
7
|
+
import { type MailMessage, mailDriver, type SendResult } from './driver';
|
|
8
|
+
import { mailIdempotencyKey } from './idempotency';
|
|
9
|
+
|
|
10
|
+
/** The queue payload is the already-rendered envelope: rendering happens once, at send time. */
|
|
11
|
+
export const mailMessageSchema: StandardSchemaV1<unknown, MailMessage> = t.object({
|
|
12
|
+
mailId: t.string,
|
|
13
|
+
to: t.array(t.email),
|
|
14
|
+
subject: t.string,
|
|
15
|
+
html: t.string,
|
|
16
|
+
text: t.string,
|
|
17
|
+
locale: t.locale,
|
|
18
|
+
tz: t.timezone,
|
|
19
|
+
replyTo: t.email.optional(),
|
|
20
|
+
cc: t.array(t.email).optional(),
|
|
21
|
+
bcc: t.array(t.email).optional(),
|
|
22
|
+
unsubscribeUrl: t.url.optional(),
|
|
23
|
+
idempotencyKey: t.string.optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Five attempts with exponential backoff: transient 4xx/greylisting clears in minutes, and a
|
|
28
|
+
* hard bounce still lands in the dead-letter queue where `x jobs dead --json` can show it.
|
|
29
|
+
*/
|
|
30
|
+
export const sendMailJob: JobHandle<MailMessage> = job<MailMessage>({
|
|
31
|
+
name: 'mail.send',
|
|
32
|
+
input: mailMessageSchema,
|
|
33
|
+
idempotencyKey: mailIdempotencyKey,
|
|
34
|
+
retry: { attempts: 5, backoff: 'exponential' },
|
|
35
|
+
run: ({ input }): Promise<SendResult> => mailDriver().send(input),
|
|
36
|
+
});
|
package/src/layout.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type ColorScheme = 'light' | 'dark';
|
|
2
|
+
export type MailToken = 'pageBg' | 'surfaceBg' | 'textPrimary' | 'textMuted' | 'borderSubtle' | 'accentBg' | 'accentText' | 'linkText' | 'calloutInfoBg' | 'calloutInfoText' | 'calloutDangerBg' | 'calloutDangerText';
|
|
3
|
+
export declare const MAIL_TOKENS: Readonly<Record<MailToken, Readonly<Record<ColorScheme, string>>>>;
|
|
4
|
+
/** Resolve a token to a hex value. The only function in the package that returns a colour. */
|
|
5
|
+
export declare function token(name: MailToken, scheme?: ColorScheme): string;
|
|
6
|
+
export declare const MAIL_FONT_STACK = "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif";
|
|
7
|
+
export declare const MAIL_WIDTH_PX = 600;
|
|
8
|
+
/** A node's dark-mode role: which CSS property takes which token when the client is dark. */
|
|
9
|
+
export type DarkRule = readonly [role: string, declarations: readonly [string, MailToken][]];
|
|
10
|
+
/**
|
|
11
|
+
* Dark overrides key off short `data-x` role codes rather than class names: base styling
|
|
12
|
+
* stays fully inlined for the clients that strip <style>, and the codes are deliberately
|
|
13
|
+
* not token names, so nothing leaks the design vocabulary into delivered HTML.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DARK_RULES: readonly DarkRule[];
|
|
16
|
+
/** The one `prefers-color-scheme` block. Clients that ignore it keep the inlined light theme. */
|
|
17
|
+
export declare function darkModeCss(): string;
|
|
18
|
+
export interface UnsubscribeSlot {
|
|
19
|
+
/** Already translated — the layout never touches a catalog. */
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly url: string;
|
|
22
|
+
}
|
|
23
|
+
export interface LayoutInput {
|
|
24
|
+
readonly subject: string;
|
|
25
|
+
/** Hidden preview text. Clients show it next to the subject in the inbox list. */
|
|
26
|
+
readonly preheader: string;
|
|
27
|
+
/** Rendered block HTML. */
|
|
28
|
+
readonly content: string;
|
|
29
|
+
readonly footer: readonly string[];
|
|
30
|
+
readonly unsubscribe?: UnsubscribeSlot | undefined;
|
|
31
|
+
readonly locale: string;
|
|
32
|
+
readonly direction: 'ltr' | 'rtl';
|
|
33
|
+
}
|
|
34
|
+
export type MailLayout = (input: LayoutInput) => string;
|
|
35
|
+
export declare const BASE_LAYOUT = "base";
|
|
36
|
+
/** The framework's only layout. Apps register their own with `registerLayout`. */
|
|
37
|
+
export declare const baseLayout: MailLayout;
|
|
38
|
+
export declare function registerLayout(name: string, layout: MailLayout): void;
|
|
39
|
+
export declare function layoutFor(name: string): MailLayout | undefined;
|
|
40
|
+
export declare function registeredLayouts(): readonly string[];
|
|
41
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["layout.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3C,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,WAAW,GACX,aAAa,GACb,WAAW,GACX,cAAc,GACd,UAAU,GACV,YAAY,GACZ,UAAU,GACV,eAAe,GACf,iBAAiB,GACjB,iBAAiB,GACjB,mBAAmB,CAAC;AAExB,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAcvF,CAAC;AAEL,8FAA8F;AAC9F,wBAAgB,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,GAAE,WAAqB,GAAG,MAAM,CAE5E;AAED,eAAO,MAAM,eAAe,gFACmD,CAAC;AAEhF,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,6FAA6F;AAC7F,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;AAE7F;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAmCzC,CAAC;AAEF,iGAAiG;AACjG,wBAAgB,WAAW,IAAI,MAAM,CAQpC;AAED,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2BAA2B;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;CACnC;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,MAAM,CAAC;AAExD,eAAO,MAAM,WAAW,SAAS,CAAC;AA6ClC,kFAAkF;AAClF,eAAO,MAAM,UAAU,EAAE,UAwBjB,CAAC;AAeT,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAErE;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAE9D;AAED,wBAAgB,iBAAiB,IAAI,SAAS,MAAM,EAAE,CAErD"}
|
package/src/layout.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Single responsibility: the base email layout and the semantic colour tokens every block
|
|
2
|
+
// resolves through. Colours are declared once here (light + dark) and inlined by the
|
|
3
|
+
// renderer, because email clients drop <style> and templates must never carry a raw hex.
|
|
4
|
+
// Table-based and 600px wide: Outlook still renders with Word's HTML engine.
|
|
5
|
+
import { escapeHtml, safeUrl, styleAttr } from './html';
|
|
6
|
+
export const MAIL_TOKENS = Object.freeze({
|
|
7
|
+
pageBg: { light: '#f4f5f7', dark: '#0b0d10' },
|
|
8
|
+
surfaceBg: { light: '#ffffff', dark: '#14181d' },
|
|
9
|
+
textPrimary: { light: '#16191d', dark: '#e7eaee' },
|
|
10
|
+
textMuted: { light: '#5c6470', dark: '#9aa4b1' },
|
|
11
|
+
borderSubtle: { light: '#e2e6ea', dark: '#262c33' },
|
|
12
|
+
accentBg: { light: '#2f6df6', dark: '#4f86ff' },
|
|
13
|
+
accentText: { light: '#ffffff', dark: '#0b0d10' },
|
|
14
|
+
linkText: { light: '#2f6df6', dark: '#7aa7ff' },
|
|
15
|
+
calloutInfoBg: { light: '#eef3ff', dark: '#141c2c' },
|
|
16
|
+
calloutInfoText: { light: '#1f3f8f', dark: '#b9cdfb' },
|
|
17
|
+
calloutDangerBg: { light: '#fdeceb', dark: '#2a1416' },
|
|
18
|
+
calloutDangerText: { light: '#8c2118', dark: '#f5b3ad' },
|
|
19
|
+
});
|
|
20
|
+
/** Resolve a token to a hex value. The only function in the package that returns a colour. */
|
|
21
|
+
export function token(name, scheme = 'light') {
|
|
22
|
+
return MAIL_TOKENS[name][scheme];
|
|
23
|
+
}
|
|
24
|
+
export const MAIL_FONT_STACK = '-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif';
|
|
25
|
+
export const MAIL_WIDTH_PX = 600;
|
|
26
|
+
/**
|
|
27
|
+
* Dark overrides key off short `data-x` role codes rather than class names: base styling
|
|
28
|
+
* stays fully inlined for the clients that strip <style>, and the codes are deliberately
|
|
29
|
+
* not token names, so nothing leaks the design vocabulary into delivered HTML.
|
|
30
|
+
*/
|
|
31
|
+
export const DARK_RULES = [
|
|
32
|
+
['page', [['background-color', 'pageBg']]],
|
|
33
|
+
[
|
|
34
|
+
'card',
|
|
35
|
+
[
|
|
36
|
+
['background-color', 'surfaceBg'],
|
|
37
|
+
['border-color', 'borderSubtle'],
|
|
38
|
+
],
|
|
39
|
+
],
|
|
40
|
+
['h', [['color', 'textPrimary']]],
|
|
41
|
+
['p', [['color', 'textPrimary']]],
|
|
42
|
+
['mut', [['color', 'textMuted']]],
|
|
43
|
+
[
|
|
44
|
+
'btn',
|
|
45
|
+
[
|
|
46
|
+
['background-color', 'accentBg'],
|
|
47
|
+
['color', 'accentText'],
|
|
48
|
+
],
|
|
49
|
+
],
|
|
50
|
+
['hr', [['border-top-color', 'borderSubtle']]],
|
|
51
|
+
['a', [['color', 'linkText']]],
|
|
52
|
+
[
|
|
53
|
+
'ci',
|
|
54
|
+
[
|
|
55
|
+
['background-color', 'calloutInfoBg'],
|
|
56
|
+
['color', 'calloutInfoText'],
|
|
57
|
+
],
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
'cd',
|
|
61
|
+
[
|
|
62
|
+
['background-color', 'calloutDangerBg'],
|
|
63
|
+
['color', 'calloutDangerText'],
|
|
64
|
+
],
|
|
65
|
+
],
|
|
66
|
+
];
|
|
67
|
+
/** The one `prefers-color-scheme` block. Clients that ignore it keep the inlined light theme. */
|
|
68
|
+
export function darkModeCss() {
|
|
69
|
+
const rules = DARK_RULES.map(([role, declarations]) => {
|
|
70
|
+
const body = declarations
|
|
71
|
+
.map(([property, name]) => `${property}:${token(name, 'dark')}!important`)
|
|
72
|
+
.join(';');
|
|
73
|
+
return `[data-x="${role}"]{${body}}`;
|
|
74
|
+
});
|
|
75
|
+
return `@media (prefers-color-scheme:dark){${rules.join('')}}`;
|
|
76
|
+
}
|
|
77
|
+
export const BASE_LAYOUT = 'base';
|
|
78
|
+
const BODY_STYLE = styleAttr([
|
|
79
|
+
'margin:0',
|
|
80
|
+
'padding:0',
|
|
81
|
+
'width:100%',
|
|
82
|
+
`background-color:${token('pageBg')}`,
|
|
83
|
+
`font-family:${MAIL_FONT_STACK}`,
|
|
84
|
+
`color:${token('textPrimary')}`,
|
|
85
|
+
'-webkit-font-smoothing:antialiased',
|
|
86
|
+
]);
|
|
87
|
+
const PREHEADER_STYLE = styleAttr([
|
|
88
|
+
'display:none',
|
|
89
|
+
'font-size:1px',
|
|
90
|
+
'line-height:1px',
|
|
91
|
+
'max-height:0',
|
|
92
|
+
'max-width:0',
|
|
93
|
+
'opacity:0',
|
|
94
|
+
'overflow:hidden',
|
|
95
|
+
'mso-hide:all',
|
|
96
|
+
]);
|
|
97
|
+
const PAGE_STYLE = styleAttr([`background-color:${token('pageBg')}`, 'width:100%']);
|
|
98
|
+
const GUTTER_STYLE = styleAttr(['padding:24px 12px']);
|
|
99
|
+
const SHELL_STYLE = styleAttr([`width:${MAIL_WIDTH_PX}px`, 'max-width:100%']);
|
|
100
|
+
const CARD_STYLE = styleAttr([
|
|
101
|
+
`background-color:${token('surfaceBg')}`,
|
|
102
|
+
`border:1px solid ${token('borderSubtle')}`,
|
|
103
|
+
'border-radius:12px',
|
|
104
|
+
'padding:32px',
|
|
105
|
+
]);
|
|
106
|
+
const FOOTER_STYLE = styleAttr([
|
|
107
|
+
`color:${token('textMuted')}`,
|
|
108
|
+
'font-size:12px',
|
|
109
|
+
'line-height:18px',
|
|
110
|
+
'padding:16px 8px 32px 8px',
|
|
111
|
+
]);
|
|
112
|
+
const UNSUBSCRIBE_STYLE = styleAttr([`color:${token('textMuted')}`, 'text-decoration:underline']);
|
|
113
|
+
const TABLE_ATTRS = 'role="presentation" cellpadding="0" cellspacing="0" border="0"';
|
|
114
|
+
/** The framework's only layout. Apps register their own with `registerLayout`. */
|
|
115
|
+
export const baseLayout = (input) => `<!doctype html>
|
|
116
|
+
<html lang="${escapeHtml(input.locale)}" dir="${input.direction}">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charset="utf-8">
|
|
119
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
120
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
121
|
+
<meta name="color-scheme" content="light dark">
|
|
122
|
+
<meta name="supported-color-schemes" content="light dark">
|
|
123
|
+
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no">
|
|
124
|
+
<meta name="x-apple-disable-message-reformatting">
|
|
125
|
+
<title>${escapeHtml(input.subject)}</title>
|
|
126
|
+
<style>${darkModeCss()}</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body ${BODY_STYLE} data-x="page">
|
|
129
|
+
<div ${PREHEADER_STYLE}>${escapeHtml(input.preheader)}</div>
|
|
130
|
+
<table ${TABLE_ATTRS} width="100%" data-x="page" ${PAGE_STYLE}>
|
|
131
|
+
<tr><td align="center" ${GUTTER_STYLE}>
|
|
132
|
+
<table ${TABLE_ATTRS} width="${MAIL_WIDTH_PX}" ${SHELL_STYLE}>
|
|
133
|
+
<tr><td data-x="card" ${CARD_STYLE}>${input.content}</td></tr>
|
|
134
|
+
<tr><td data-x="mut" ${FOOTER_STYLE}>${footerHtml(input)}</td></tr>
|
|
135
|
+
</table>
|
|
136
|
+
</td></tr>
|
|
137
|
+
</table>
|
|
138
|
+
</body>
|
|
139
|
+
</html>`;
|
|
140
|
+
function footerHtml(input) {
|
|
141
|
+
const lines = input.footer.map((line) => `<div>${escapeHtml(line)}</div>`);
|
|
142
|
+
const slot = input.unsubscribe;
|
|
143
|
+
if (slot !== undefined) {
|
|
144
|
+
const href = escapeHtml(safeUrl(slot.url));
|
|
145
|
+
const label = escapeHtml(slot.label);
|
|
146
|
+
lines.push(`<div><a data-x="a" href="${href}" ${UNSUBSCRIBE_STYLE}>${label}</a></div>`);
|
|
147
|
+
}
|
|
148
|
+
return lines.join('');
|
|
149
|
+
}
|
|
150
|
+
const layouts = new Map([[BASE_LAYOUT, baseLayout]]);
|
|
151
|
+
export function registerLayout(name, layout) {
|
|
152
|
+
layouts.set(name, layout);
|
|
153
|
+
}
|
|
154
|
+
export function layoutFor(name) {
|
|
155
|
+
return layouts.get(name);
|
|
156
|
+
}
|
|
157
|
+
export function registeredLayouts() {
|
|
158
|
+
return [...layouts.keys()].sort();
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["layout.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,qFAAqF;AACrF,yFAAyF;AACzF,6EAA6E;AAE7E,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAkBxD,MAAM,CAAC,MAAM,WAAW,GACtB,MAAM,CAAC,MAAM,CAAC;IACZ,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IAC7C,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IAChD,WAAW,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IAClD,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IAChD,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IACnD,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/C,UAAU,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IACjD,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/C,aAAa,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IACpD,eAAe,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IACtD,eAAe,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;IACtD,iBAAiB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;CACzD,CAAC,CAAC;AAEL,8FAA8F;AAC9F,MAAM,UAAU,KAAK,CAAC,IAAe,EAAE,MAAM,GAAgB,OAAO;IAClE,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAC1B,6EAA6E,CAAC;AAEhF,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AAKjC;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAwB;IAC7C,CAAC,MAAM,EAAE,CAAC,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC1C;QACE,MAAM;QACN;YACE,CAAC,kBAAkB,EAAE,WAAW,CAAC;YACjC,CAAC,cAAc,EAAE,cAAc,CAAC;SACjC;KACF;IACD,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IACjC,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IACjC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IACjC;QACE,KAAK;QACL;YACE,CAAC,kBAAkB,EAAE,UAAU,CAAC;YAChC,CAAC,OAAO,EAAE,YAAY,CAAC;SACxB;KACF;IACD,CAAC,IAAI,EAAE,CAAC,CAAC,kBAAkB,EAAE,cAAc,CAAC,CAAC,CAAC;IAC9C,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IAC9B;QACE,IAAI;QACJ;YACE,CAAC,kBAAkB,EAAE,eAAe,CAAC;YACrC,CAAC,OAAO,EAAE,iBAAiB,CAAC;SAC7B;KACF;IACD;QACE,IAAI;QACJ;YACE,CAAC,kBAAkB,EAAE,iBAAiB,CAAC;YACvC,CAAC,OAAO,EAAE,mBAAmB,CAAC;SAC/B;KACF;CACF,CAAC;AAEF,iGAAiG;AACjG,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,EAAE;QACpD,MAAM,IAAI,GAAG,YAAY;aACtB,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,GAAG,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,YAAY,CAAC;aACzE,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,OAAO,YAAY,IAAI,MAAM,IAAI,GAAG,CAAC;IACvC,CAAC,CAAC,CAAC;IACH,OAAO,sCAAsC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC;AACjE,CAAC;AAsBD,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC;AAElC,MAAM,UAAU,GAAG,SAAS,CAAC;IAC3B,UAAU;IACV,WAAW;IACX,YAAY;IACZ,oBAAoB,KAAK,CAAC,QAAQ,CAAC,EAAE;IACrC,eAAe,eAAe,EAAE;IAChC,SAAS,KAAK,CAAC,aAAa,CAAC,EAAE;IAC/B,oCAAoC;CACrC,CAAC,CAAC;AAEH,MAAM,eAAe,GAAG,SAAS,CAAC;IAChC,cAAc;IACd,eAAe;IACf,iBAAiB;IACjB,cAAc;IACd,aAAa;IACb,WAAW;IACX,iBAAiB;IACjB,cAAc;CACf,CAAC,CAAC;AAEH,MAAM,UAAU,GAAG,SAAS,CAAC,CAAC,oBAAoB,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC;AACpF,MAAM,YAAY,GAAG,SAAS,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC;AACtD,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,SAAS,aAAa,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAE9E,MAAM,UAAU,GAAG,SAAS,CAAC;IAC3B,oBAAoB,KAAK,CAAC,WAAW,CAAC,EAAE;IACxC,oBAAoB,KAAK,CAAC,cAAc,CAAC,EAAE;IAC3C,oBAAoB;IACpB,cAAc;CACf,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,SAAS,CAAC;IAC7B,SAAS,KAAK,CAAC,WAAW,CAAC,EAAE;IAC7B,gBAAgB;IAChB,kBAAkB;IAClB,2BAA2B;CAC5B,CAAC,CAAC;AAEH,MAAM,iBAAiB,GAAG,SAAS,CAAC,CAAC,SAAS,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,2BAA2B,CAAC,CAAC,CAAC;AAElG,MAAM,WAAW,GAAG,gEAAgE,CAAC;AAErF,kFAAkF;AAClF,MAAM,CAAC,MAAM,UAAU,GAAe,CAAC,KAAK,EAAE,EAAE,CAAC;cACnC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,KAAK,CAAC,SAAS;;;;;;;;;SAStD,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC;SACzB,WAAW,EAAE;;QAEd,UAAU;OACX,eAAe,IAAI,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC;SAC5C,WAAW,+BAA+B,UAAU;yBACpC,YAAY;SAC5B,WAAW,WAAW,aAAa,KAAK,WAAW;wBACpC,UAAU,IAAI,KAAK,CAAC,OAAO;uBAC5B,YAAY,IAAI,UAAU,CAAC,KAAK,CAAC;;;;;QAKhD,CAAC;AAET,SAAS,UAAU,CAAC,KAAkB;IACpC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,4BAA4B,IAAI,KAAK,iBAAiB,IAAI,KAAK,YAAY,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAqB,CAAC,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;AAEzE,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,MAAkB;IAC7D,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC"}
|