@forklaunch/core 3.1.0 → 3.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.
@@ -18,7 +18,9 @@
18
18
  * The quota counts recipients, as SES does.
19
19
  *
20
20
  * Vendor route (unauthenticated): GET /__mock/email/messages[?to=address]
21
- * lists what was "sent", with its from address and configuration set.
21
+ * lists what was "sent", with its from address and configuration set; a
22
+ * message sent with `listUnsubscribe` also shows it and the `headers` the
23
+ * platform would add (List-Unsubscribe, List-Unsubscribe-Post).
22
24
  *
23
25
  * MOCK_EMAIL_DAILY_QUOTA recipients per UTC day, default 200
24
26
  * MOCK_EMAIL_SENDING_DOMAIN default mail.forklaunch.test; the from address
@@ -36,11 +38,18 @@ const LIMITS = {
36
38
  maxRecipients: 50,
37
39
  maxSubjectLength: 998,
38
40
  maxBodyBytes: 512 * 1024,
39
- maxTags: 10
41
+ maxTags: 10,
42
+ maxUnsubscribeUrlLength: 512
40
43
  };
41
44
  const ADDRESS =
42
45
  /^[^\s@<>()[\]\\,;:"]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/;
43
46
  const TAG_PART = /^[A-Za-z0-9_-]{1,256}$/;
47
+ // List-Unsubscribe: the platform's rules (list-unsubscribe.util.ts there).
48
+ const HEADER_SAFE = /^[\x21-\x7e]+$/;
49
+ const URL_DELIMITERS = /[<>,"\\]/;
50
+ const MAILBOX =
51
+ /^[A-Za-z0-9._+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/;
52
+ const UNSUBSCRIBE_FIELDS = new Set(['url', 'mailto', 'oneClick']);
44
53
 
45
54
  const isAddress = (a) =>
46
55
  typeof a === 'string' && a.length <= 254 && ADDRESS.test(a);
@@ -74,6 +83,76 @@ function init(state) {
74
83
  return state;
75
84
  }
76
85
 
86
+ function unsubscribeUrlProblem(url) {
87
+ if (typeof url !== 'string' || url === '') return 'url must be a string';
88
+ if (url.length > LIMITS.maxUnsubscribeUrlLength)
89
+ return `url is longer than ${LIMITS.maxUnsubscribeUrlLength} characters`;
90
+ if (!HEADER_SAFE.test(url))
91
+ return 'url must be printable ASCII with no spaces or line breaks; percent-encode anything else';
92
+ if (URL_DELIMITERS.test(url))
93
+ return 'url must not contain < > , " or \\; percent-encode them';
94
+ let parsed;
95
+ try {
96
+ parsed = new URL(url);
97
+ } catch {
98
+ return 'url is not a URL';
99
+ }
100
+ if (parsed.protocol !== 'https:') return 'url must be https';
101
+ if (parsed.username || parsed.password)
102
+ return 'url must not carry a user name or password';
103
+ return null;
104
+ }
105
+
106
+ /** `listUnsubscribe`: `{ value }` (undefined when absent) or `{ error }`. */
107
+ function checkListUnsubscribe(value) {
108
+ if (value === undefined || value === null) return { value: undefined };
109
+ const fail = (problem) => ({ error: `listUnsubscribe: ${problem}` });
110
+ if (typeof value !== 'object' || Array.isArray(value))
111
+ return fail('must be an object');
112
+ const unknown = Object.keys(value).filter((k) => !UNSUBSCRIBE_FIELDS.has(k));
113
+ if (unknown.length)
114
+ return fail(`unknown field ${unknown.map((k) => `'${k}'`).join(', ')}`);
115
+ const { url, mailto, oneClick } = value;
116
+ if (url === undefined && mailto === undefined)
117
+ return fail('needs a url or a mailto');
118
+ if (url !== undefined) {
119
+ const problem = unsubscribeUrlProblem(url);
120
+ if (problem) return fail(problem);
121
+ }
122
+ if (
123
+ mailto !== undefined &&
124
+ (typeof mailto !== 'string' || mailto.length > 254 || !MAILBOX.test(mailto))
125
+ )
126
+ return fail(
127
+ `mailto: '${String(mailto).slice(0, 80)}' is not a plain mailbox (letters, digits, . _ + - before the @)`
128
+ );
129
+ if (oneClick !== undefined && typeof oneClick !== 'boolean')
130
+ return fail('oneClick must be true or false');
131
+ if (oneClick === true && url === undefined)
132
+ return fail('oneClick needs a url (RFC 8058 posts to it)');
133
+ return {
134
+ value: {
135
+ ...(url !== undefined ? { url } : {}),
136
+ ...(mailto !== undefined ? { mailto } : {}),
137
+ ...(oneClick === true ? { oneClick: true } : {})
138
+ }
139
+ };
140
+ }
141
+
142
+ /** The headers the platform adds to the SES message for it. */
143
+ function listUnsubscribeHeaders({ url, mailto, oneClick }) {
144
+ const targets = [
145
+ ...(url ? [`<${url}>`] : []),
146
+ ...(mailto ? [`<mailto:${mailto}>`] : [])
147
+ ];
148
+ return {
149
+ 'List-Unsubscribe': targets.join(', '),
150
+ ...(oneClick
151
+ ? { 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click' }
152
+ : {})
153
+ };
154
+ }
155
+
77
156
  /** The gateway's validation: a message, or the reason it is refused. */
78
157
  function validate(body) {
79
158
  const list = (v) =>
@@ -109,7 +188,9 @@ function validate(body) {
109
188
  if (name.startsWith('fl-') || name.startsWith('ses:'))
110
189
  return `tag '${name}' is reserved`;
111
190
  }
112
- return { to, replyTo, tags };
191
+ const unsubscribe = checkListUnsubscribe(body.listUnsubscribe);
192
+ if (unsubscribe.error) return unsubscribe.error;
193
+ return { to, replyTo, tags, listUnsubscribe: unsubscribe.value };
113
194
  }
114
195
 
115
196
  export default {
@@ -122,7 +203,7 @@ export default {
122
203
  const state = init(ctx.state);
123
204
  const checked = validate(ctx.body);
124
205
  if (typeof checked === 'string') return ctx.send(400, checked);
125
- const { to, replyTo, tags } = checked;
206
+ const { to, replyTo, tags, listUnsubscribe } = checked;
126
207
  const refused = to.filter((a) => state.suppressed.has(a.toLowerCase()));
127
208
  if (refused.length) {
128
209
  return ctx.send(
@@ -151,6 +232,12 @@ export default {
151
232
  text: ctx.body.text,
152
233
  html: ctx.body.html,
153
234
  tags,
235
+ ...(listUnsubscribe
236
+ ? {
237
+ listUnsubscribe,
238
+ headers: listUnsubscribeHeaders(listUnsubscribe)
239
+ }
240
+ : {}),
154
241
  at: new Date().toISOString()
155
242
  });
156
243
 
@@ -33,6 +33,19 @@ import { InstanceGatewayRequestError } from './instanceGateway';
33
33
  * text: 'See you on Tuesday at 10:00.'
34
34
  * });
35
35
  * if (await email.suppressed('pat@example.com')) { … ask for another address }
36
+ *
37
+ * @example One-click unsubscribe (RFC 8058), which Gmail and Yahoo require of
38
+ * bulk senders: the platform adds `List-Unsubscribe` and
39
+ * `List-Unsubscribe-Post: List-Unsubscribe=One-Click`.
40
+ * await email.send({
41
+ * to: member.email,
42
+ * subject: 'Your weekly recap',
43
+ * html,
44
+ * listUnsubscribe: {
45
+ * url: `https://app.example.com/unsubscribe?token=${token}`, // accepts POST
46
+ * oneClick: true
47
+ * }
48
+ * });
36
49
  */
37
50
  export interface EmailClientOptions {
38
51
  /** Platform base URL. Default: `PLATFORM_GATEWAY_URL`. */
@@ -57,7 +70,33 @@ export interface EmailSendRequest {
57
70
  * letters, digits, `_` and `-`. Names starting `fl-` or `ses:` are reserved.
58
71
  */
59
72
  tags?: Record<string, string>;
73
+ /** `List-Unsubscribe` targets; see {@link EmailListUnsubscribe}. */
74
+ listUnsubscribe?: EmailListUnsubscribe;
60
75
  }
76
+ /**
77
+ * Where a recipient unsubscribes: the `List-Unsubscribe` header (RFC 2369),
78
+ * plus `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058) with
79
+ * `oneClick`. The platform writes the headers; these are targets, not header
80
+ * text.
81
+ *
82
+ * - `url`: https only, printable ASCII up to 512 characters, no `< > , " \`
83
+ * (percent-encode them). With `oneClick`, mailbox providers POST
84
+ * `List-Unsubscribe=One-Click` to it (form-encoded, no cookies): it must
85
+ * unsubscribe from that alone, without a confirmation page.
86
+ * - `mailto`: a plain mailbox (letters, digits, `. _ + -` before the `@`).
87
+ *
88
+ * The headers go to every address in `to`; for a per-recipient link, send
89
+ * one message per recipient.
90
+ */
91
+ export type EmailListUnsubscribe = {
92
+ url: string;
93
+ mailto?: string;
94
+ oneClick?: boolean;
95
+ } | {
96
+ url?: undefined;
97
+ mailto: string;
98
+ oneClick?: false;
99
+ };
61
100
  export interface EmailSendResult {
62
101
  /** The provider's message id; delivery events carry it. */
63
102
  messageId: string;
@@ -93,9 +132,16 @@ export declare const EMAIL_LIMITS: {
93
132
  /** text + html, UTF-8 bytes. */
94
133
  readonly maxBodyBytes: number;
95
134
  readonly maxTags: 10;
135
+ /** `listUnsubscribe.url`; keeps the header within SES's limit. */
136
+ readonly maxUnsubscribeUrlLength: 512;
96
137
  };
97
138
  /** Whether `address` is a plain mailbox address (`local@domain.tld`). */
98
139
  export declare function isEmailAddress(address: unknown): address is string;
140
+ /**
141
+ * Check `listUnsubscribe` the way the gateway does; returns its wire form.
142
+ * At least one of `url` and `mailto`; `oneClick` only with `url`.
143
+ */
144
+ export declare function validateListUnsubscribe(value: unknown): EmailListUnsubscribe | undefined;
99
145
  /** Validate a send request the way the gateway does; returns the wire body. */
100
146
  export declare function validateEmailSendRequest(request: EmailSendRequest): Record<string, unknown>;
101
147
  export declare function createEmailClient(options?: EmailClientOptions): EmailClient;
@@ -1 +1 @@
1
- {"version":3,"file":"emailClient.d.ts","sourceRoot":"","sources":["../../src/http/emailClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,2BAA2B,EAE5B,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,+BAA+B;IAC/B,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACtB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,gFAAgF;AAChF,qBAAa,iBAAkB,SAAQ,2BAA2B;IAChE,SAAkB,IAAI,EAAG,mBAAmB,CAAU;CACvD;AAED,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,IAAI,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC1D,4EAA4E;IAC5E,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,+EAA+E;AAC/E,eAAO,MAAM,YAAY;aACvB,aAAa,EAAE,EAAE;aACjB,gBAAgB,EAAE,GAAG;aACrB,gBAAgB,EAAE,GAAG;IACrB,gCAAgC;aAChC,YAAY;aACZ,OAAO,EAAE,EAAE;CACH,CAAC;AAMX,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,MAAM,CAMlE;AAyBD,+EAA+E;AAC/E,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyCzB;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CAuDb"}
1
+ {"version":3,"file":"emailClient.d.ts","sourceRoot":"","sources":["../../src/http/emailClient.ts"],"names":[],"mappings":"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"}
@@ -35,6 +35,7 @@ export * from './openApiV3Generator/openApiV3Generator';
35
35
  export * from './router/discriminateBody';
36
36
  export * from './router/expressLikeRouter';
37
37
  export * from './telemetry/auditLogger';
38
+ export * from './telemetry/requestAudit';
38
39
  export * from './telemetry/constants';
39
40
  export * from './telemetry/evaluateTelemetryOptions';
40
41
  export * from './telemetry/metricsDefinition';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,cAAc,sCAAsC,CAAC;AACrD,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0CAA0C,CAAC;AACzD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,wDAAwD,CAAC;AACvE,cAAc,yCAAyC,CAAC;AACxD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AACrD,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,cAAc,sCAAsC,CAAC;AACrD,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0CAA0C,CAAC;AACzD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,wDAAwD,CAAC;AACvE,cAAc,yCAAyC,CAAC;AACxD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AACrD,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}