@shware/workflow 2.0.1 → 3.1.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.
Files changed (228) hide show
  1. package/README.md +106 -2
  2. package/dist/apns-relay.cjs +4 -17
  3. package/dist/apns-relay.cjs.map +1 -1
  4. package/dist/apns-relay.mjs +4 -17
  5. package/dist/apns-relay.mjs.map +1 -1
  6. package/dist/aws/engine-step.cjs +104 -0
  7. package/dist/aws/engine-step.cjs.map +1 -0
  8. package/dist/aws/engine-step.d.cts +28 -0
  9. package/dist/aws/engine-step.d.cts.map +1 -0
  10. package/dist/aws/engine-step.d.mts +28 -0
  11. package/dist/aws/engine-step.d.mts.map +1 -0
  12. package/dist/aws/engine-step.mjs +102 -0
  13. package/dist/aws/engine-step.mjs.map +1 -0
  14. package/dist/aws/function-url.cjs +62 -0
  15. package/dist/aws/function-url.cjs.map +1 -0
  16. package/dist/aws/function-url.d.cts +38 -0
  17. package/dist/aws/function-url.d.cts.map +1 -0
  18. package/dist/aws/function-url.d.mts +38 -0
  19. package/dist/aws/function-url.d.mts.map +1 -0
  20. package/dist/aws/function-url.mjs +59 -0
  21. package/dist/aws/function-url.mjs.map +1 -0
  22. package/dist/aws/index.cjs +160 -0
  23. package/dist/aws/index.cjs.map +1 -0
  24. package/dist/aws/index.d.cts +68 -0
  25. package/dist/aws/index.d.cts.map +1 -0
  26. package/dist/aws/index.d.mts +68 -0
  27. package/dist/aws/index.d.mts.map +1 -0
  28. package/dist/aws/index.mjs +148 -0
  29. package/dist/aws/index.mjs.map +1 -0
  30. package/dist/aws/instances.cjs +196 -0
  31. package/dist/aws/instances.cjs.map +1 -0
  32. package/dist/aws/instances.d.cts +94 -0
  33. package/dist/aws/instances.d.cts.map +1 -0
  34. package/dist/aws/instances.d.mts +94 -0
  35. package/dist/aws/instances.d.mts.map +1 -0
  36. package/dist/aws/instances.mjs +192 -0
  37. package/dist/aws/instances.mjs.map +1 -0
  38. package/dist/aws/lambda-api.cjs +89 -0
  39. package/dist/aws/lambda-api.cjs.map +1 -0
  40. package/dist/aws/lambda-api.d.cts +43 -0
  41. package/dist/aws/lambda-api.d.cts.map +1 -0
  42. package/dist/aws/lambda-api.d.mts +43 -0
  43. package/dist/aws/lambda-api.d.mts.map +1 -0
  44. package/dist/aws/lambda-api.mjs +88 -0
  45. package/dist/aws/lambda-api.mjs.map +1 -0
  46. package/dist/client.d.cts +1 -1
  47. package/dist/client.d.mts +1 -1
  48. package/dist/cloudflare/bindings.cjs +19 -0
  49. package/dist/cloudflare/bindings.cjs.map +1 -1
  50. package/dist/cloudflare/bindings.d.cts +39 -0
  51. package/dist/cloudflare/bindings.d.cts.map +1 -1
  52. package/dist/cloudflare/bindings.d.mts +39 -0
  53. package/dist/cloudflare/bindings.d.mts.map +1 -1
  54. package/dist/cloudflare/bindings.mjs +18 -1
  55. package/dist/cloudflare/bindings.mjs.map +1 -1
  56. package/dist/cloudflare/index.cjs +32 -28
  57. package/dist/cloudflare/index.d.cts +8 -6
  58. package/dist/cloudflare/index.d.mts +8 -6
  59. package/dist/cloudflare/index.mjs +12 -10
  60. package/dist/cloudflare/runner.cjs +27 -76
  61. package/dist/cloudflare/runner.cjs.map +1 -1
  62. package/dist/cloudflare/runner.d.cts +7 -2
  63. package/dist/cloudflare/runner.d.cts.map +1 -1
  64. package/dist/cloudflare/runner.d.mts +7 -2
  65. package/dist/cloudflare/runner.d.mts.map +1 -1
  66. package/dist/cloudflare/runner.mjs +26 -75
  67. package/dist/cloudflare/runner.mjs.map +1 -1
  68. package/dist/cloudflare/worker.cjs +22 -203
  69. package/dist/cloudflare/worker.cjs.map +1 -1
  70. package/dist/cloudflare/worker.d.cts +10 -56
  71. package/dist/cloudflare/worker.d.cts.map +1 -1
  72. package/dist/cloudflare/worker.d.mts +10 -56
  73. package/dist/cloudflare/worker.d.mts.map +1 -1
  74. package/dist/cloudflare/worker.mjs +20 -199
  75. package/dist/cloudflare/worker.mjs.map +1 -1
  76. package/dist/email-sender.cjs +62 -0
  77. package/dist/email-sender.cjs.map +1 -0
  78. package/dist/email-sender.d.cts +70 -0
  79. package/dist/email-sender.d.cts.map +1 -0
  80. package/dist/email-sender.d.mts +70 -0
  81. package/dist/email-sender.d.mts.map +1 -0
  82. package/dist/email-sender.mjs +60 -0
  83. package/dist/email-sender.mjs.map +1 -0
  84. package/dist/email.cjs +21 -0
  85. package/dist/email.cjs.map +1 -1
  86. package/dist/email.d.cts +6 -0
  87. package/dist/email.d.cts.map +1 -1
  88. package/dist/email.d.mts +6 -0
  89. package/dist/email.d.mts.map +1 -1
  90. package/dist/email.mjs +21 -1
  91. package/dist/email.mjs.map +1 -1
  92. package/dist/engine/index.cjs +1 -1
  93. package/dist/engine/index.mjs +1 -1
  94. package/dist/engine/interpreter.cjs +1 -1
  95. package/dist/engine/interpreter.cjs.map +1 -1
  96. package/dist/engine/interpreter.mjs +1 -1
  97. package/dist/engine/interpreter.mjs.map +1 -1
  98. package/dist/engine/ports.d.cts +6 -1
  99. package/dist/engine/ports.d.cts.map +1 -1
  100. package/dist/engine/ports.d.mts +6 -1
  101. package/dist/engine/ports.d.mts.map +1 -1
  102. package/dist/{cloudflare → host}/channels.cjs +13 -11
  103. package/dist/host/channels.cjs.map +1 -0
  104. package/dist/{cloudflare → host}/channels.d.cts +8 -3
  105. package/dist/host/channels.d.cts.map +1 -0
  106. package/dist/{cloudflare → host}/channels.d.mts +8 -3
  107. package/dist/host/channels.d.mts.map +1 -0
  108. package/dist/{cloudflare → host}/channels.mjs +9 -7
  109. package/dist/host/channels.mjs.map +1 -0
  110. package/dist/host/execute.cjs +81 -0
  111. package/dist/host/execute.cjs.map +1 -0
  112. package/dist/host/execute.d.cts +37 -0
  113. package/dist/host/execute.d.cts.map +1 -0
  114. package/dist/host/execute.d.mts +37 -0
  115. package/dist/host/execute.d.mts.map +1 -0
  116. package/dist/host/execute.mjs +80 -0
  117. package/dist/host/execute.mjs.map +1 -0
  118. package/dist/host/journey-host.cjs +214 -0
  119. package/dist/host/journey-host.cjs.map +1 -0
  120. package/dist/host/journey-host.d.cts +105 -0
  121. package/dist/host/journey-host.d.cts.map +1 -0
  122. package/dist/host/journey-host.d.mts +105 -0
  123. package/dist/host/journey-host.d.mts.map +1 -0
  124. package/dist/host/journey-host.mjs +210 -0
  125. package/dist/host/journey-host.mjs.map +1 -0
  126. package/dist/{cloudflare → host}/router.cjs +34 -13
  127. package/dist/host/router.cjs.map +1 -0
  128. package/dist/{cloudflare → host}/router.d.cts +2 -2
  129. package/dist/host/router.d.cts.map +1 -0
  130. package/dist/{cloudflare → host}/router.d.mts +2 -2
  131. package/dist/host/router.d.mts.map +1 -0
  132. package/dist/{cloudflare → host}/router.mjs +34 -13
  133. package/dist/host/router.mjs.map +1 -0
  134. package/dist/host/senders.cjs +63 -0
  135. package/dist/host/senders.cjs.map +1 -0
  136. package/dist/host/senders.d.cts +31 -0
  137. package/dist/host/senders.d.cts.map +1 -0
  138. package/dist/host/senders.d.mts +31 -0
  139. package/dist/host/senders.d.mts.map +1 -0
  140. package/dist/host/senders.mjs +59 -0
  141. package/dist/host/senders.mjs.map +1 -0
  142. package/dist/http2-client.cjs +90 -0
  143. package/dist/http2-client.cjs.map +1 -0
  144. package/dist/http2-client.d.cts +47 -0
  145. package/dist/http2-client.d.cts.map +1 -0
  146. package/dist/http2-client.d.mts +47 -0
  147. package/dist/http2-client.d.mts.map +1 -0
  148. package/dist/http2-client.mjs +88 -0
  149. package/dist/http2-client.mjs.map +1 -0
  150. package/dist/index.cjs +1 -0
  151. package/dist/index.d.cts +2 -2
  152. package/dist/index.d.mts +2 -2
  153. package/dist/index.mjs +2 -2
  154. package/dist/react-email.cjs.map +1 -1
  155. package/dist/react-email.d.cts +1 -2
  156. package/dist/react-email.d.cts.map +1 -1
  157. package/dist/react-email.d.mts +1 -2
  158. package/dist/react-email.d.mts.map +1 -1
  159. package/dist/react-email.mjs.map +1 -1
  160. package/dist/store/d1.cjs +1 -0
  161. package/dist/store/d1.cjs.map +1 -1
  162. package/dist/store/d1.d.cts.map +1 -1
  163. package/dist/store/d1.d.mts.map +1 -1
  164. package/dist/store/d1.mjs +1 -0
  165. package/dist/store/d1.mjs.map +1 -1
  166. package/dist/store/index.cjs.map +1 -1
  167. package/dist/store/index.d.cts +74 -0
  168. package/dist/store/index.d.cts.map +1 -1
  169. package/dist/store/index.d.mts +74 -0
  170. package/dist/store/index.d.mts.map +1 -1
  171. package/dist/store/index.mjs.map +1 -1
  172. package/dist/store/postgres.cjs +177 -12
  173. package/dist/store/postgres.cjs.map +1 -1
  174. package/dist/store/postgres.d.cts +10 -1
  175. package/dist/store/postgres.d.cts.map +1 -1
  176. package/dist/store/postgres.d.mts +10 -1
  177. package/dist/store/postgres.d.mts.map +1 -1
  178. package/dist/store/postgres.mjs +177 -12
  179. package/dist/store/postgres.mjs.map +1 -1
  180. package/dist/store/tables.cjs +31 -2
  181. package/dist/store/tables.cjs.map +1 -1
  182. package/dist/store/tables.d.cts +1 -0
  183. package/dist/store/tables.d.cts.map +1 -1
  184. package/dist/store/tables.d.mts +1 -0
  185. package/dist/store/tables.d.mts.map +1 -1
  186. package/dist/store/tables.mjs +31 -2
  187. package/dist/store/tables.mjs.map +1 -1
  188. package/dist/transports/cloudflare-email.cjs +59 -0
  189. package/dist/transports/cloudflare-email.cjs.map +1 -0
  190. package/dist/transports/cloudflare-email.d.cts +38 -0
  191. package/dist/transports/cloudflare-email.d.cts.map +1 -0
  192. package/dist/transports/cloudflare-email.d.mts +38 -0
  193. package/dist/transports/cloudflare-email.d.mts.map +1 -0
  194. package/dist/transports/cloudflare-email.mjs +57 -0
  195. package/dist/transports/cloudflare-email.mjs.map +1 -0
  196. package/dist/transports/ses.cjs +91 -0
  197. package/dist/transports/ses.cjs.map +1 -0
  198. package/dist/transports/ses.d.cts +39 -0
  199. package/dist/transports/ses.d.cts.map +1 -0
  200. package/dist/transports/ses.d.mts +39 -0
  201. package/dist/transports/ses.d.mts.map +1 -0
  202. package/dist/transports/ses.mjs +89 -0
  203. package/dist/transports/ses.mjs.map +1 -0
  204. package/dist/transports/sigv4.cjs +83 -0
  205. package/dist/transports/sigv4.cjs.map +1 -0
  206. package/dist/transports/sigv4.d.cts +27 -0
  207. package/dist/transports/sigv4.d.cts.map +1 -0
  208. package/dist/transports/sigv4.d.mts +27 -0
  209. package/dist/transports/sigv4.d.mts.map +1 -0
  210. package/dist/transports/sigv4.mjs +82 -0
  211. package/dist/transports/sigv4.mjs.map +1 -0
  212. package/package.json +53 -1
  213. package/dist/cloudflare/channels.cjs.map +0 -1
  214. package/dist/cloudflare/channels.d.cts.map +0 -1
  215. package/dist/cloudflare/channels.d.mts.map +0 -1
  216. package/dist/cloudflare/channels.mjs.map +0 -1
  217. package/dist/cloudflare/router.cjs.map +0 -1
  218. package/dist/cloudflare/router.d.cts.map +0 -1
  219. package/dist/cloudflare/router.d.mts.map +0 -1
  220. package/dist/cloudflare/router.mjs.map +0 -1
  221. package/dist/cloudflare/senders.cjs +0 -121
  222. package/dist/cloudflare/senders.cjs.map +0 -1
  223. package/dist/cloudflare/senders.d.cts +0 -72
  224. package/dist/cloudflare/senders.d.cts.map +0 -1
  225. package/dist/cloudflare/senders.d.mts +0 -72
  226. package/dist/cloudflare/senders.d.mts.map +0 -1
  227. package/dist/cloudflare/senders.mjs +0 -115
  228. package/dist/cloudflare/senders.mjs.map +0 -1
@@ -0,0 +1,39 @@
1
+ import { EmailTransport } from "../email-sender.mjs";
2
+ //#region src/transports/ses.d.ts
3
+ /**
4
+ * `@shware/workflow/transports/ses` — email through Amazon SES (the v2 `SendEmail` API) over
5
+ * plain `fetch` with a SigV4 signature, so the same transport runs in a Lambda and in a Worker.
6
+ * Nothing here loads the AWS SDK or a Node-only credential chain.
7
+ */
8
+ export interface AwsCredentials {
9
+ accessKeyId: string;
10
+ secretAccessKey: string;
11
+ /** Present for temporary credentials (a role's): signed with the request. */
12
+ sessionToken?: string | undefined;
13
+ }
14
+ /** Called on every send, so rotated temporary credentials are picked up rather than frozen. */
15
+ export type AwsCredentialProvider = () => AwsCredentials | Promise<AwsCredentials>;
16
+ export interface SesEmailTransportOptions {
17
+ region: string;
18
+ /**
19
+ * Fixed credentials, or a provider read on every send. Defaults to the process environment's
20
+ * `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, read at send time —
21
+ * a Lambda's execution role. A Worker passes them explicitly from its validated env.
22
+ */
23
+ credentials?: AwsCredentials | AwsCredentialProvider;
24
+ /** An SES configuration set (event publishing, a dedicated IP pool). */
25
+ configurationSetName?: string;
26
+ /** Defaults to `https://email.<region>.amazonaws.com`. */
27
+ endpoint?: string;
28
+ fetch?: typeof fetch;
29
+ }
30
+ /** An SES error response: `type` is SES's error code (`MessageRejected`, `TooManyRequestsException`, ...). */
31
+ export declare class SesSendError extends Error {
32
+ readonly status: number;
33
+ readonly type: string | undefined;
34
+ readonly name = "SesSendError";
35
+ constructor(status: number, type: string | undefined, message: string);
36
+ }
37
+ export declare function sesEmailTransport(options: SesEmailTransportOptions): EmailTransport;
38
+ //#endregion
39
+ //# sourceMappingURL=ses.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ses.d.mts","names":[],"sources":["../../src/transports/ses.ts"],"mappings":";;;;;;;iBAUiB;EACf;EACA;;EAEA;;;YAIU,8BAA8B,iBAAiB,QAAQ;iBAElD;EACf;;;;;;EAMA,cAAc,iBAAiB;;EAE/B;;EAEA;EACA,eAAe;;;qBAIJ,qBAAqB;WAGrB;WACA;WAHO;EAClB,YACW,gBACA,0BACT;;wBAoBY,kBAAkB,SAAS,2BAA2B"}
@@ -0,0 +1,89 @@
1
+ import { mimeAddress } from "../email.mjs";
2
+ import { signRequest } from "./sigv4.mjs";
3
+ //#region src/transports/ses.ts
4
+ /** An SES error response: `type` is SES's error code (`MessageRejected`, `TooManyRequestsException`, ...). */
5
+ var SesSendError = class extends Error {
6
+ status;
7
+ type;
8
+ name = "SesSendError";
9
+ constructor(status, type, message) {
10
+ super(`SES SendEmail failed (${status}${type === void 0 ? "" : ` ${type}`}): ${message}`);
11
+ this.status = status;
12
+ this.type = type;
13
+ }
14
+ };
15
+ function environmentCredentials() {
16
+ const env = globalThis.process?.env;
17
+ const accessKeyId = env?.AWS_ACCESS_KEY_ID;
18
+ const secretAccessKey = env?.AWS_SECRET_ACCESS_KEY;
19
+ if (accessKeyId === void 0 || secretAccessKey === void 0) throw new Error("sesEmailTransport: no credentials — pass `credentials`, or run where AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are set (a Lambda execution role)");
20
+ return {
21
+ accessKeyId,
22
+ secretAccessKey,
23
+ sessionToken: env?.AWS_SESSION_TOKEN
24
+ };
25
+ }
26
+ function sesEmailTransport(options) {
27
+ const endpoint = (options.endpoint ?? `https://email.${options.region}.amazonaws.com`).replace(/\/$/, "");
28
+ const credentials = async () => {
29
+ const configured = options.credentials;
30
+ if (configured === void 0) return environmentCredentials();
31
+ return typeof configured === "function" ? configured() : configured;
32
+ };
33
+ const fetchImpl = options.fetch ?? fetch;
34
+ return { async send(message) {
35
+ const body = JSON.stringify({
36
+ FromEmailAddress: mimeAddress(message.from),
37
+ Destination: { ToAddresses: [message.to] },
38
+ ...message.replyTo === void 0 ? {} : { ReplyToAddresses: [mimeAddress(message.replyTo)] },
39
+ Content: { Simple: {
40
+ Subject: {
41
+ Data: message.subject,
42
+ Charset: "UTF-8"
43
+ },
44
+ Body: { Html: {
45
+ Data: message.html,
46
+ Charset: "UTF-8"
47
+ } }
48
+ } },
49
+ ...options.configurationSetName === void 0 ? {} : { ConfigurationSetName: options.configurationSetName }
50
+ });
51
+ const url = `${endpoint}/v2/email/outbound-emails`;
52
+ const headers = await signRequest({
53
+ method: "POST",
54
+ url,
55
+ headers: { "content-type": "application/json" },
56
+ body,
57
+ credentials: await credentials(),
58
+ region: options.region,
59
+ service: "ses"
60
+ });
61
+ const response = await fetchImpl(url, {
62
+ method: "POST",
63
+ headers,
64
+ body
65
+ });
66
+ const text = await response.text();
67
+ if (!response.ok) {
68
+ let detail = text;
69
+ try {
70
+ const parsed = JSON.parse(text);
71
+ const said = parsed.message ?? parsed.Message;
72
+ if (typeof said === "string") detail = said;
73
+ } catch {}
74
+ const type = response.headers.get("x-amzn-errortype")?.split(":")[0];
75
+ throw new SesSendError(response.status, type, detail);
76
+ }
77
+ let messageId;
78
+ try {
79
+ messageId = JSON.parse(text).MessageId;
80
+ } catch {
81
+ messageId = void 0;
82
+ }
83
+ return typeof messageId === "string" ? { providerMessageId: messageId } : {};
84
+ } };
85
+ }
86
+ //#endregion
87
+ export { SesSendError, sesEmailTransport };
88
+
89
+ //# sourceMappingURL=ses.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ses.mjs","names":[],"sources":["../../src/transports/ses.ts"],"sourcesContent":["import { mimeAddress } from '../email';\nimport type { EmailMessage, EmailTransport } from '../email-sender';\nimport { signRequest } from './sigv4';\n\n/**\n * `@shware/workflow/transports/ses` — email through Amazon SES (the v2 `SendEmail` API) over\n * plain `fetch` with a SigV4 signature, so the same transport runs in a Lambda and in a Worker.\n * Nothing here loads the AWS SDK or a Node-only credential chain.\n */\n\nexport interface AwsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n /** Present for temporary credentials (a role's): signed with the request. */\n sessionToken?: string | undefined;\n}\n\n/** Called on every send, so rotated temporary credentials are picked up rather than frozen. */\nexport type AwsCredentialProvider = () => AwsCredentials | Promise<AwsCredentials>;\n\nexport interface SesEmailTransportOptions {\n region: string;\n /**\n * Fixed credentials, or a provider read on every send. Defaults to the process environment's\n * `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, read at send time —\n * a Lambda's execution role. A Worker passes them explicitly from its validated env.\n */\n credentials?: AwsCredentials | AwsCredentialProvider;\n /** An SES configuration set (event publishing, a dedicated IP pool). */\n configurationSetName?: string;\n /** Defaults to `https://email.<region>.amazonaws.com`. */\n endpoint?: string;\n fetch?: typeof fetch;\n}\n\n/** An SES error response: `type` is SES's error code (`MessageRejected`, `TooManyRequestsException`, ...). */\nexport class SesSendError extends Error {\n override readonly name = 'SesSendError';\n constructor(\n readonly status: number,\n readonly type: string | undefined,\n message: string\n ) {\n super(`SES SendEmail failed (${status}${type === undefined ? '' : ` ${type}`}): ${message}`);\n }\n}\n\nfunction environmentCredentials(): AwsCredentials {\n // Read through globalThis: a Worker has no `process`, and must pass credentials instead\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n const accessKeyId = env?.AWS_ACCESS_KEY_ID;\n const secretAccessKey = env?.AWS_SECRET_ACCESS_KEY;\n if (accessKeyId === undefined || secretAccessKey === undefined) {\n throw new Error(\n 'sesEmailTransport: no credentials — pass `credentials`, or run where AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are set (a Lambda execution role)'\n );\n }\n return { accessKeyId, secretAccessKey, sessionToken: env?.AWS_SESSION_TOKEN };\n}\n\nexport function sesEmailTransport(options: SesEmailTransportOptions): EmailTransport {\n const endpoint = (options.endpoint ?? `https://email.${options.region}.amazonaws.com`).replace(\n /\\/$/,\n ''\n );\n const credentials = async (): Promise<AwsCredentials> => {\n const configured = options.credentials;\n if (configured === undefined) return environmentCredentials();\n return typeof configured === 'function' ? configured() : configured;\n };\n const fetchImpl = options.fetch ?? fetch;\n\n return {\n async send(message: EmailMessage) {\n const body = JSON.stringify({\n FromEmailAddress: mimeAddress(message.from),\n Destination: { ToAddresses: [message.to] },\n ...(message.replyTo === undefined\n ? {}\n : { ReplyToAddresses: [mimeAddress(message.replyTo)] }),\n Content: {\n Simple: {\n Subject: { Data: message.subject, Charset: 'UTF-8' },\n Body: { Html: { Data: message.html, Charset: 'UTF-8' } },\n },\n },\n ...(options.configurationSetName === undefined\n ? {}\n : { ConfigurationSetName: options.configurationSetName }),\n });\n const url = `${endpoint}/v2/email/outbound-emails`;\n const headers = await signRequest({\n method: 'POST',\n url,\n headers: { 'content-type': 'application/json' },\n body,\n credentials: await credentials(),\n region: options.region,\n service: 'ses',\n });\n // No retries here: the durable step owns them, and counts them — SES cannot de-duplicate,\n // so every extra attempt of an accepted send is another chance to deliver twice\n const response = await fetchImpl(url, { method: 'POST', headers, body });\n const text = await response.text();\n if (!response.ok) {\n let detail = text;\n try {\n const parsed = JSON.parse(text) as { message?: unknown; Message?: unknown };\n const said = parsed.message ?? parsed.Message;\n if (typeof said === 'string') detail = said;\n } catch {\n // Not JSON: the raw body says what went wrong\n }\n const type = response.headers.get('x-amzn-errortype')?.split(':')[0];\n throw new SesSendError(response.status, type, detail);\n }\n let messageId: unknown;\n try {\n messageId = (JSON.parse(text) as { MessageId?: unknown }).MessageId;\n } catch {\n messageId = undefined;\n }\n return typeof messageId === 'string' ? { providerMessageId: messageId } : {};\n },\n };\n}\n"],"mappings":";;;;AAoCA,IAAa,eAAb,cAAkC,MAAM;CAG3B;CACA;CAHX,OAAyB;CACzB,YACE,QACA,MACA,SACA;EACA,MAAM,yBAAyB,SAAS,SAAS,KAAA,IAAY,KAAK,IAAI,OAAO,KAAK,SAAS;EAJlF,KAAA,SAAA;EACA,KAAA,OAAA;CAIX;AACF;AAEA,SAAS,yBAAyC;CAEhD,MAAM,MAAO,WAA0E,SACnF;CACJ,MAAM,cAAc,KAAK;CACzB,MAAM,kBAAkB,KAAK;CAC7B,IAAI,gBAAgB,KAAA,KAAa,oBAAoB,KAAA,GACnD,MAAM,IAAI,MACR,oJACF;CAEF,OAAO;EAAE;EAAa;EAAiB,cAAc,KAAK;CAAkB;AAC9E;AAEA,SAAgB,kBAAkB,SAAmD;CACnF,MAAM,YAAY,QAAQ,YAAY,iBAAiB,QAAQ,OAAO,gBAAA,CAAiB,QACrF,OACA,EACF;CACA,MAAM,cAAc,YAAqC;EACvD,MAAM,aAAa,QAAQ;EAC3B,IAAI,eAAe,KAAA,GAAW,OAAO,uBAAuB;EAC5D,OAAO,OAAO,eAAe,aAAa,WAAW,IAAI;CAC3D;CACA,MAAM,YAAY,QAAQ,SAAS;CAEnC,OAAO,EACL,MAAM,KAAK,SAAuB;EAChC,MAAM,OAAO,KAAK,UAAU;GAC1B,kBAAkB,YAAY,QAAQ,IAAI;GAC1C,aAAa,EAAE,aAAa,CAAC,QAAQ,EAAE,EAAE;GACzC,GAAI,QAAQ,YAAY,KAAA,IACpB,CAAC,IACD,EAAE,kBAAkB,CAAC,YAAY,QAAQ,OAAO,CAAC,EAAE;GACvD,SAAS,EACP,QAAQ;IACN,SAAS;KAAE,MAAM,QAAQ;KAAS,SAAS;IAAQ;IACnD,MAAM,EAAE,MAAM;KAAE,MAAM,QAAQ;KAAM,SAAS;IAAQ,EAAE;GACzD,EACF;GACA,GAAI,QAAQ,yBAAyB,KAAA,IACjC,CAAC,IACD,EAAE,sBAAsB,QAAQ,qBAAqB;EAC3D,CAAC;EACD,MAAM,MAAM,GAAG,SAAS;EACxB,MAAM,UAAU,MAAM,YAAY;GAChC,QAAQ;GACR;GACA,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C;GACA,aAAa,MAAM,YAAY;GAC/B,QAAQ,QAAQ;GAChB,SAAS;EACX,CAAC;EAGD,MAAM,WAAW,MAAM,UAAU,KAAK;GAAE,QAAQ;GAAQ;GAAS;EAAK,CAAC;EACvE,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,IAAI,CAAC,SAAS,IAAI;GAChB,IAAI,SAAS;GACb,IAAI;IACF,MAAM,SAAS,KAAK,MAAM,IAAI;IAC9B,MAAM,OAAO,OAAO,WAAW,OAAO;IACtC,IAAI,OAAO,SAAS,UAAU,SAAS;GACzC,QAAQ,CAER;GACA,MAAM,OAAO,SAAS,QAAQ,IAAI,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;GAClE,MAAM,IAAI,aAAa,SAAS,QAAQ,MAAM,MAAM;EACtD;EACA,IAAI;EACJ,IAAI;GACF,YAAa,KAAK,MAAM,IAAI,CAAC,CAA6B;EAC5D,QAAQ;GACN,YAAY,KAAA;EACd;EACA,OAAO,OAAO,cAAc,WAAW,EAAE,mBAAmB,UAAU,IAAI,CAAC;CAC7E,EACF;AACF"}
@@ -0,0 +1,83 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/transports/sigv4.ts
3
+ const utf8 = new TextEncoder();
4
+ function hex(bytes) {
5
+ return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
6
+ }
7
+ async function sha256(data) {
8
+ return hex(await crypto.subtle.digest("SHA-256", utf8.encode(data)));
9
+ }
10
+ async function hmac(key, data) {
11
+ const imported = await crypto.subtle.importKey("raw", key, {
12
+ name: "HMAC",
13
+ hash: "SHA-256"
14
+ }, false, ["sign"]);
15
+ return crypto.subtle.sign("HMAC", imported, utf8.encode(data));
16
+ }
17
+ /** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */
18
+ function encode(value) {
19
+ return encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
20
+ }
21
+ /** Each path segment encoded once (the URL already decoded it). */
22
+ function canonicalPath(pathname) {
23
+ return pathname.split("/").map((segment) => encode(decodeURIComponent(segment))).join("/");
24
+ }
25
+ /**
26
+ * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`
27
+ * as a space, which would sign a value other than the one on the wire.
28
+ */
29
+ function canonicalQuery(search) {
30
+ if (search === "" || search === "?") return "";
31
+ return search.slice(1).split("&").filter((pair) => pair !== "").map((pair) => {
32
+ const at = pair.indexOf("=");
33
+ const name = at === -1 ? pair : pair.slice(0, at);
34
+ const value = at === -1 ? "" : pair.slice(at + 1);
35
+ return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))];
36
+ }).sort(([a, av], [b, bv]) => a === b ? av < bv ? -1 : av > bv ? 1 : 0 : a < b ? -1 : 1).map(([name, value]) => `${name}=${value}`).join("&");
37
+ }
38
+ /** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */
39
+ async function signRequest(request) {
40
+ const url = new URL(request.url);
41
+ const datetime = request.datetime ?? (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
42
+ const date = datetime.slice(0, 8);
43
+ const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;
44
+ const headers = {};
45
+ for (const [name, value] of Object.entries(request.headers ?? {})) headers[name.toLowerCase()] = value.trim().replace(/\s+/g, " ");
46
+ headers.host = url.host;
47
+ headers["x-amz-date"] = datetime;
48
+ if (sessionToken !== void 0) headers["x-amz-security-token"] = sessionToken;
49
+ const names = Object.keys(headers).sort();
50
+ const signedHeaders = names.join(";");
51
+ const canonical = [
52
+ request.method.toUpperCase(),
53
+ canonicalPath(url.pathname),
54
+ canonicalQuery(url.search),
55
+ ...names.map((name) => `${name}:${headers[name]}`),
56
+ "",
57
+ signedHeaders,
58
+ await sha256(request.body ?? "")
59
+ ].join("\n");
60
+ const scope = `${date}/${request.region}/${request.service}/aws4_request`;
61
+ const toSign = [
62
+ "AWS4-HMAC-SHA256",
63
+ datetime,
64
+ scope,
65
+ await sha256(canonical)
66
+ ].join("\n");
67
+ let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);
68
+ for (const part of [
69
+ request.region,
70
+ request.service,
71
+ "aws4_request"
72
+ ]) key = await hmac(key, part);
73
+ const signature = hex(await hmac(key, toSign));
74
+ const { host: _host, ...sent } = headers;
75
+ return {
76
+ ...sent,
77
+ authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
78
+ };
79
+ }
80
+ //#endregion
81
+ exports.signRequest = signRequest;
82
+
83
+ //# sourceMappingURL=sigv4.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.cjs","names":[],"sources":["../../src/transports/sigv4.ts"],"sourcesContent":["/**\n * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign\n * the JSON API calls the transports make. Headers are signed as given (plus `host` and\n * `x-amz-date`); the payload hash is the body's SHA-256.\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n */\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string | undefined;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: string;\n credentials: SigV4Credentials;\n region: string;\n service: string;\n /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */\n datetime?: string;\n}\n\nconst utf8 = new TextEncoder();\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nasync function sha256(data: string): Promise<string> {\n return hex(await crypto.subtle.digest('SHA-256', utf8.encode(data)));\n}\n\nasync function hmac(\n key: ArrayBuffer | Uint8Array<ArrayBuffer>,\n data: string\n): Promise<ArrayBuffer> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return crypto.subtle.sign('HMAC', imported, utf8.encode(data));\n}\n\n/** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */\nfunction encode(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`\n );\n}\n\n/** Each path segment encoded once (the URL already decoded it). */\nfunction canonicalPath(pathname: string): string {\n return pathname\n .split('/')\n .map((segment) => encode(decodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`\n * as a space, which would sign a value other than the one on the wire.\n */\nfunction canonicalQuery(search: string): string {\n if (search === '' || search === '?') return '';\n return search\n .slice(1)\n .split('&')\n .filter((pair) => pair !== '')\n .map((pair) => {\n const at = pair.indexOf('=');\n const name = at === -1 ? pair : pair.slice(0, at);\n const value = at === -1 ? '' : pair.slice(at + 1);\n return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))] as const;\n })\n .sort(([a, av], [b, bv]) => (a === b ? (av < bv ? -1 : av > bv ? 1 : 0) : a < b ? -1 : 1))\n .map(([name, value]) => `${name}=${value}`)\n .join('&');\n}\n\n/** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */\nexport async function signRequest(request: SigV4Request): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const datetime = request.datetime ?? new Date().toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n const date = datetime.slice(0, 8);\n const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;\n\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers ?? {})) {\n headers[name.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n headers.host = url.host;\n headers['x-amz-date'] = datetime;\n if (sessionToken !== undefined) headers['x-amz-security-token'] = sessionToken;\n\n const names = Object.keys(headers).sort();\n const signedHeaders = names.join(';');\n const canonical = [\n request.method.toUpperCase(),\n canonicalPath(url.pathname),\n canonicalQuery(url.search),\n ...names.map((name) => `${name}:${headers[name]}`),\n '',\n signedHeaders,\n await sha256(request.body ?? ''),\n ].join('\\n');\n\n const scope = `${date}/${request.region}/${request.service}/aws4_request`;\n const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256(canonical)].join('\\n');\n\n let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);\n for (const part of [request.region, request.service, 'aws4_request']) {\n key = await hmac(key, part);\n }\n const signature = hex(await hmac(key, toSign));\n\n const { host: _host, ...sent } = headers;\n return {\n ...sent,\n authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ //#region src/transports/sigv4.d.ts
2
+ /**
3
+ * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign
4
+ * the JSON API calls the transports make. Headers are signed as given (plus `host` and
5
+ * `x-amz-date`); the payload hash is the body's SHA-256.
6
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
7
+ */
8
+ export interface SigV4Credentials {
9
+ accessKeyId: string;
10
+ secretAccessKey: string;
11
+ sessionToken?: string | undefined;
12
+ }
13
+ export interface SigV4Request {
14
+ method: string;
15
+ url: string;
16
+ headers?: Record<string, string>;
17
+ body?: string;
18
+ credentials: SigV4Credentials;
19
+ region: string;
20
+ service: string;
21
+ /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */
22
+ datetime?: string;
23
+ }
24
+ /** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */
25
+ export declare function signRequest(request: SigV4Request): Promise<Record<string, string>>;
26
+ //#endregion
27
+ //# sourceMappingURL=sigv4.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.cts","names":[],"sources":["../../src/transports/sigv4.ts"],"mappings":";;;;;;;iBAOiB;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA,UAAU;EACV;EACA,aAAa;EACb;EACA;;EAEA;;;wBAiEoB,YAAY,SAAS,eAAe,QAAQ"}
@@ -0,0 +1,27 @@
1
+ //#region src/transports/sigv4.d.ts
2
+ /**
3
+ * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign
4
+ * the JSON API calls the transports make. Headers are signed as given (plus `host` and
5
+ * `x-amz-date`); the payload hash is the body's SHA-256.
6
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
7
+ */
8
+ export interface SigV4Credentials {
9
+ accessKeyId: string;
10
+ secretAccessKey: string;
11
+ sessionToken?: string | undefined;
12
+ }
13
+ export interface SigV4Request {
14
+ method: string;
15
+ url: string;
16
+ headers?: Record<string, string>;
17
+ body?: string;
18
+ credentials: SigV4Credentials;
19
+ region: string;
20
+ service: string;
21
+ /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */
22
+ datetime?: string;
23
+ }
24
+ /** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */
25
+ export declare function signRequest(request: SigV4Request): Promise<Record<string, string>>;
26
+ //#endregion
27
+ //# sourceMappingURL=sigv4.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.mts","names":[],"sources":["../../src/transports/sigv4.ts"],"mappings":";;;;;;;iBAOiB;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA,UAAU;EACV;EACA,aAAa;EACb;EACA;;EAEA;;;wBAiEoB,YAAY,SAAS,eAAe,QAAQ"}
@@ -0,0 +1,82 @@
1
+ //#region src/transports/sigv4.ts
2
+ const utf8 = new TextEncoder();
3
+ function hex(bytes) {
4
+ return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
5
+ }
6
+ async function sha256(data) {
7
+ return hex(await crypto.subtle.digest("SHA-256", utf8.encode(data)));
8
+ }
9
+ async function hmac(key, data) {
10
+ const imported = await crypto.subtle.importKey("raw", key, {
11
+ name: "HMAC",
12
+ hash: "SHA-256"
13
+ }, false, ["sign"]);
14
+ return crypto.subtle.sign("HMAC", imported, utf8.encode(data));
15
+ }
16
+ /** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */
17
+ function encode(value) {
18
+ return encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
19
+ }
20
+ /** Each path segment encoded once (the URL already decoded it). */
21
+ function canonicalPath(pathname) {
22
+ return pathname.split("/").map((segment) => encode(decodeURIComponent(segment))).join("/");
23
+ }
24
+ /**
25
+ * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`
26
+ * as a space, which would sign a value other than the one on the wire.
27
+ */
28
+ function canonicalQuery(search) {
29
+ if (search === "" || search === "?") return "";
30
+ return search.slice(1).split("&").filter((pair) => pair !== "").map((pair) => {
31
+ const at = pair.indexOf("=");
32
+ const name = at === -1 ? pair : pair.slice(0, at);
33
+ const value = at === -1 ? "" : pair.slice(at + 1);
34
+ return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))];
35
+ }).sort(([a, av], [b, bv]) => a === b ? av < bv ? -1 : av > bv ? 1 : 0 : a < b ? -1 : 1).map(([name, value]) => `${name}=${value}`).join("&");
36
+ }
37
+ /** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */
38
+ async function signRequest(request) {
39
+ const url = new URL(request.url);
40
+ const datetime = request.datetime ?? (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
41
+ const date = datetime.slice(0, 8);
42
+ const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;
43
+ const headers = {};
44
+ for (const [name, value] of Object.entries(request.headers ?? {})) headers[name.toLowerCase()] = value.trim().replace(/\s+/g, " ");
45
+ headers.host = url.host;
46
+ headers["x-amz-date"] = datetime;
47
+ if (sessionToken !== void 0) headers["x-amz-security-token"] = sessionToken;
48
+ const names = Object.keys(headers).sort();
49
+ const signedHeaders = names.join(";");
50
+ const canonical = [
51
+ request.method.toUpperCase(),
52
+ canonicalPath(url.pathname),
53
+ canonicalQuery(url.search),
54
+ ...names.map((name) => `${name}:${headers[name]}`),
55
+ "",
56
+ signedHeaders,
57
+ await sha256(request.body ?? "")
58
+ ].join("\n");
59
+ const scope = `${date}/${request.region}/${request.service}/aws4_request`;
60
+ const toSign = [
61
+ "AWS4-HMAC-SHA256",
62
+ datetime,
63
+ scope,
64
+ await sha256(canonical)
65
+ ].join("\n");
66
+ let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);
67
+ for (const part of [
68
+ request.region,
69
+ request.service,
70
+ "aws4_request"
71
+ ]) key = await hmac(key, part);
72
+ const signature = hex(await hmac(key, toSign));
73
+ const { host: _host, ...sent } = headers;
74
+ return {
75
+ ...sent,
76
+ authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
77
+ };
78
+ }
79
+ //#endregion
80
+ export { signRequest };
81
+
82
+ //# sourceMappingURL=sigv4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.mjs","names":[],"sources":["../../src/transports/sigv4.ts"],"sourcesContent":["/**\n * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign\n * the JSON API calls the transports make. Headers are signed as given (plus `host` and\n * `x-amz-date`); the payload hash is the body's SHA-256.\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n */\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string | undefined;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: string;\n credentials: SigV4Credentials;\n region: string;\n service: string;\n /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */\n datetime?: string;\n}\n\nconst utf8 = new TextEncoder();\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nasync function sha256(data: string): Promise<string> {\n return hex(await crypto.subtle.digest('SHA-256', utf8.encode(data)));\n}\n\nasync function hmac(\n key: ArrayBuffer | Uint8Array<ArrayBuffer>,\n data: string\n): Promise<ArrayBuffer> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return crypto.subtle.sign('HMAC', imported, utf8.encode(data));\n}\n\n/** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */\nfunction encode(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`\n );\n}\n\n/** Each path segment encoded once (the URL already decoded it). */\nfunction canonicalPath(pathname: string): string {\n return pathname\n .split('/')\n .map((segment) => encode(decodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`\n * as a space, which would sign a value other than the one on the wire.\n */\nfunction canonicalQuery(search: string): string {\n if (search === '' || search === '?') return '';\n return search\n .slice(1)\n .split('&')\n .filter((pair) => pair !== '')\n .map((pair) => {\n const at = pair.indexOf('=');\n const name = at === -1 ? pair : pair.slice(0, at);\n const value = at === -1 ? '' : pair.slice(at + 1);\n return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))] as const;\n })\n .sort(([a, av], [b, bv]) => (a === b ? (av < bv ? -1 : av > bv ? 1 : 0) : a < b ? -1 : 1))\n .map(([name, value]) => `${name}=${value}`)\n .join('&');\n}\n\n/** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */\nexport async function signRequest(request: SigV4Request): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const datetime = request.datetime ?? new Date().toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n const date = datetime.slice(0, 8);\n const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;\n\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers ?? {})) {\n headers[name.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n headers.host = url.host;\n headers['x-amz-date'] = datetime;\n if (sessionToken !== undefined) headers['x-amz-security-token'] = sessionToken;\n\n const names = Object.keys(headers).sort();\n const signedHeaders = names.join(';');\n const canonical = [\n request.method.toUpperCase(),\n canonicalPath(url.pathname),\n canonicalQuery(url.search),\n ...names.map((name) => `${name}:${headers[name]}`),\n '',\n signedHeaders,\n await sha256(request.body ?? ''),\n ].join('\\n');\n\n const scope = `${date}/${request.region}/${request.service}/aws4_request`;\n const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256(canonical)].join('\\n');\n\n let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);\n for (const part of [request.region, request.service, 'aws4_request']) {\n key = await hmac(key, part);\n }\n const signature = hex(await hmac(key, toSign));\n\n const { host: _host, ...sent } = headers;\n return {\n ...sent,\n authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/workflow",
3
- "version": "2.0.1",
3
+ "version": "3.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ShwareHQ/shware-sdk.git"
@@ -43,6 +43,16 @@
43
43
  "default": "./dist/cloudflare/index.cjs"
44
44
  }
45
45
  },
46
+ "./aws": {
47
+ "import": {
48
+ "types": "./dist/aws/index.d.mts",
49
+ "default": "./dist/aws/index.mjs"
50
+ },
51
+ "require": {
52
+ "types": "./dist/aws/index.d.cts",
53
+ "default": "./dist/aws/index.cjs"
54
+ }
55
+ },
46
56
  "./store": {
47
57
  "import": {
48
58
  "types": "./dist/store/index.d.mts",
@@ -93,6 +103,36 @@
93
103
  "default": "./dist/email-context.cjs"
94
104
  }
95
105
  },
106
+ "./email-sender": {
107
+ "import": {
108
+ "types": "./dist/email-sender.d.mts",
109
+ "default": "./dist/email-sender.mjs"
110
+ },
111
+ "require": {
112
+ "types": "./dist/email-sender.d.cts",
113
+ "default": "./dist/email-sender.cjs"
114
+ }
115
+ },
116
+ "./transports/ses": {
117
+ "import": {
118
+ "types": "./dist/transports/ses.d.mts",
119
+ "default": "./dist/transports/ses.mjs"
120
+ },
121
+ "require": {
122
+ "types": "./dist/transports/ses.d.cts",
123
+ "default": "./dist/transports/ses.cjs"
124
+ }
125
+ },
126
+ "./transports/cloudflare-email": {
127
+ "import": {
128
+ "types": "./dist/transports/cloudflare-email.d.mts",
129
+ "default": "./dist/transports/cloudflare-email.mjs"
130
+ },
131
+ "require": {
132
+ "types": "./dist/transports/cloudflare-email.d.cts",
133
+ "default": "./dist/transports/cloudflare-email.cjs"
134
+ }
135
+ },
96
136
  "./push": {
97
137
  "import": {
98
138
  "types": "./dist/push.d.mts",
@@ -163,8 +203,12 @@
163
203
  "ms": "^2.1.3"
164
204
  },
165
205
  "devDependencies": {
206
+ "@aws-sdk/client-lambda": "^3.1152.0",
207
+ "@aws/durable-execution-sdk-js": "^2.7.0",
208
+ "@aws/durable-execution-sdk-js-testing": "^1.3.1",
166
209
  "@electric-sql/pglite": "^0.5.10",
167
210
  "@repo/typescript-config": "0.0.0",
211
+ "@types/aws-lambda": "^8.10.164",
168
212
  "@types/ms": "^2.1.0",
169
213
  "@types/node": "^26.6.5",
170
214
  "@types/react": "^19.3.0",
@@ -177,12 +221,20 @@
177
221
  "zod": "^4.6.7"
178
222
  },
179
223
  "peerDependencies": {
224
+ "@aws-sdk/client-lambda": "^3.1152.0",
225
+ "@aws/durable-execution-sdk-js": "^2.7.0",
180
226
  "postgres": ">=3.4.0",
181
227
  "react": ">=19",
182
228
  "react-dom": ">=19",
183
229
  "zod": "^4.6.5"
184
230
  },
185
231
  "peerDependenciesMeta": {
232
+ "@aws-sdk/client-lambda": {
233
+ "optional": true
234
+ },
235
+ "@aws/durable-execution-sdk-js": {
236
+ "optional": true
237
+ },
186
238
  "postgres": {
187
239
  "optional": true
188
240
  },
@@ -1 +0,0 @@
1
- {"version":3,"file":"channels.cjs","names":["resolveRuntime","CfEmailSender","registryRenderer","registryPushRenderer","removeDevice","ApnsPushSender","apnsKeyFromBase64","apnsEnvironment","FcmPushSender","serviceAccountFromBase64","WebPushSender","routeByPlatform","indexTemplates","DiscordWebhookSender","lookupTemplate","renderDiscordContent","SlackWebhookSender","renderSlackContent","routeByChannel"],"sources":["../../src/cloudflare/channels.ts"],"sourcesContent":["import { ApnsPushSender, apnsKeyFromBase64 } from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport type { MessageSender, OutboundMessage } from '../engine/ports';\nimport { FcmPushSender, serviceAccountFromBase64 } from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport { WebPushSender } from '../webpush';\nimport type { JourneyEnv } from './bindings';\nimport { removeDevice } from './router';\nimport { CfEmailSender, routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the `EMAIL` binding with the configured addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/** A sender that fails every message with the reason the channel cannot deliver. */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new Error(`cannot send '${message.template}' on ${message.channel}: ${reason}`);\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n throw new Error(`${name} is not set: the environment must hold it for the configured channel`);\n }\n return value;\n}\n\n/**\n * A sender built on its first message. A platform's secret is only required\n * when something is actually sent there: a journey that only emails must not\n * fail because the APNs key is missing from a local `.dev.vars`.\n */\nfunction lazy(build: () => MessageSender): MessageSender {\n let sender: MessageSender | undefined;\n return {\n async send(message) {\n sender ??= build();\n return sender.send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n fetch?: typeof fetch;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n if (env.EMAIL === undefined) return unavailable('no EMAIL (send_email) binding');\n // Each template names its own sender: there is no global one to require\n return new CfEmailSender({\n binding: env.EMAIL,\n render: registryRenderer(registry, config.email),\n });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n // A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n // waits that read devices, as a removal through the API does\n const onUnregistered = async (message: OutboundMessage) => {\n if (message.device !== undefined && options.store !== undefined) {\n await removeDevice(env, message.device.token, options.store);\n }\n };\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n platforms.ios = lazy(\n () =>\n new ApnsPushSender({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(requireSecret(env, 'APNS_PRIVATE_KEY_BASE64')),\n topic: apns.topic,\n environment: apnsEnvironment(apns, mode),\n render,\n onUnregistered,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = lazy(\n () =>\n new FcmPushSender({\n serviceAccount: serviceAccountFromBase64(\n requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64')\n ),\n render,\n onUnregistered,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = lazy(\n () =>\n new WebPushSender({\n vapid: {\n subject: webPush.subject,\n publicKey: requireSecret(env, 'VAPID_PUBLIC_KEY'),\n privateKey: requireSecret(env, 'VAPID_PRIVATE_KEY'),\n },\n render,\n onUnregistered,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new Error(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"channels.d.cts","names":[],"sources":["../../src/cloudflare/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;wBAkCgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;YA4CA,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;EACR,eAAe;IAEhB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"channels.d.mts","names":[],"sources":["../../src/cloudflare/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;wBAkCgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;YA4CA,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;EACR,eAAe;IAEhB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"channels.mjs","names":[],"sources":["../../src/cloudflare/channels.ts"],"sourcesContent":["import { ApnsPushSender, apnsKeyFromBase64 } from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport type { MessageSender, OutboundMessage } from '../engine/ports';\nimport { FcmPushSender, serviceAccountFromBase64 } from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport { WebPushSender } from '../webpush';\nimport type { JourneyEnv } from './bindings';\nimport { removeDevice } from './router';\nimport { CfEmailSender, routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the `EMAIL` binding with the configured addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/** A sender that fails every message with the reason the channel cannot deliver. */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new Error(`cannot send '${message.template}' on ${message.channel}: ${reason}`);\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n throw new Error(`${name} is not set: the environment must hold it for the configured channel`);\n }\n return value;\n}\n\n/**\n * A sender built on its first message. A platform's secret is only required\n * when something is actually sent there: a journey that only emails must not\n * fail because the APNs key is missing from a local `.dev.vars`.\n */\nfunction lazy(build: () => MessageSender): MessageSender {\n let sender: MessageSender | undefined;\n return {\n async send(message) {\n sender ??= build();\n return sender.send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n fetch?: typeof fetch;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n if (env.EMAIL === undefined) return unavailable('no EMAIL (send_email) binding');\n // Each template names its own sender: there is no global one to require\n return new CfEmailSender({\n binding: env.EMAIL,\n render: registryRenderer(registry, config.email),\n });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n // A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n // waits that read devices, as a removal through the API does\n const onUnregistered = async (message: OutboundMessage) => {\n if (message.device !== undefined && options.store !== undefined) {\n await removeDevice(env, message.device.token, options.store);\n }\n };\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n platforms.ios = lazy(\n () =>\n new ApnsPushSender({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(requireSecret(env, 'APNS_PRIVATE_KEY_BASE64')),\n topic: apns.topic,\n environment: apnsEnvironment(apns, mode),\n render,\n onUnregistered,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = lazy(\n () =>\n new FcmPushSender({\n serviceAccount: serviceAccountFromBase64(\n requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64')\n ),\n render,\n onUnregistered,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = lazy(\n () =>\n new WebPushSender({\n vapid: {\n subject: webPush.subject,\n publicKey: requireSecret(env, 'VAPID_PUBLIC_KEY'),\n privateKey: requireSecret(env, 'VAPID_PRIVATE_KEY'),\n },\n render,\n onUnregistered,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new Error(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,YAAY,KAA8B;CACxD,OAAO,IAAI,eAAe;AAC5B;;AAGA,SAAgB,kBACd,QACA,KACiC;CACjC,OAAO,eAAe,QAAQ,SAAS,YAAY,GAAG,CAAC;AACzD;;AAGA,SAAS,YAAY,QAA+B;CAClD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,MAAM,IAAI,MAAM,gBAAgB,QAAQ,SAAS,OAAO,QAAQ,QAAQ,IAAI,QAAQ;CACtF,EACF;AACF;AAEA,SAAS,cACP,KACA,MAKQ;CACR,MAAM,QAAQ,IAAI;CAClB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAChD,MAAM,IAAI,MAAM,GAAG,KAAK,qEAAqE;CAE/F,OAAO;AACT;;;;;;AAOA,SAAS,KAAK,OAA2C;CACvD,IAAI;CACJ,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,WAAW,MAAM;EACjB,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAQA,SAAgB,mBACd,QACA,KACA,UAII,CAAC,GACU;CACf,MAAM,OAAO,YAAY,GAAG;CAE5B,MAAM,cAA6B;EACjC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,mCAAmC;EAClF,IAAI,IAAI,UAAU,KAAA,GAAW,OAAO,YAAY,+BAA+B;EAE/E,OAAO,IAAI,cAAc;GACvB,SAAS,IAAI;GACb,QAAQ,iBAAiB,UAAU,OAAO,KAAK;EACjD,CAAC;CACH;CAEA,MAAM,aAA4B;EAChC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,kCAAkC;EACjF,MAAM,SAAS,qBAAqB,QAAQ;EAG5C,MAAM,iBAAiB,OAAO,YAA6B;GACzD,IAAI,QAAQ,WAAW,KAAA,KAAa,QAAQ,UAAU,KAAA,GACpD,MAAM,aAAa,KAAK,QAAQ,OAAO,OAAO,QAAQ,KAAK;EAE/D;EACA,MAAM,YAA2C,CAAC;EAClD,MAAM,OAAO,OAAO;EACpB,IAAI,SAAS,KAAA,GACX,UAAU,MAAM,WAEZ,IAAI,eAAe;GACjB,OAAO,KAAK;GACZ,QAAQ,KAAK;GACb,YAAY,kBAAkB,cAAc,KAAK,yBAAyB,CAAC;GAC3E,OAAO,KAAK;GACZ,aAAa,gBAAgB,MAAM,IAAI;GACvC;GACA;GACA,GAAI,IAAI,sBAAsB,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,IAAI,kBAAkB;GAC/E,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC,CACL;EAEF,MAAM,MAAM,OAAO;EACnB,IAAI,QAAQ,KAAA,GACV,UAAU,UAAU,WAEhB,IAAI,cAAc;GAChB,gBAAgB,yBACd,cAAc,KAAK,uCAAuC,CAC5D;GACA;GACA;GACA,GAAI,IAAI,qBAAqB,KAAA,IACzB,CAAC,IACD,EAAE,SAAS,EAAE,YAAY,IAAI,iBAAiB,EAAE;GACpD,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC,CACL;EAEF,MAAM,UAAU,OAAO;EACvB,IAAI,YAAY,KAAA,GACd,UAAU,MAAM,WAEZ,IAAI,cAAc;GAChB,OAAO;IACL,SAAS,QAAQ;IACjB,WAAW,cAAc,KAAK,kBAAkB;IAChD,YAAY,cAAc,KAAK,mBAAmB;GACpD;GACA;GACA;GACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC,CACL;EAEF,IAAI,OAAO,KAAK,SAAS,CAAC,CAAC,WAAW,GACpC,OAAO,YAAY,4CAA4C;EACjE,OAAO,gBAAgB,SAAS;CAClC;;;;;CAMA,MAAM,cAAc,SAA8B,aAA8C;EAC9F,MAAM,MAAM,SAAS,QAAQ,GAAyC;EACtE,IAAI,OAAO,QAAQ,YAAY,QAAQ,IACrC,MAAM,IAAI,MACR,GAAG,QAAQ,aAAa,SAAS,GAAG,iDACtC;EAEF,OAAO;CACT;CAEA,MAAM,gBAA+B;EACnC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,qCAAqC;EACpF,MAAM,QAAQ,eAAe,WAAW,QAAQ;EAChD,OAAO,IAAI,qBAAqB;GAC9B,SAAS,IAAI,UAAU;IACrB,MAAM,WAAW,eAAe,OAAO,WAAW,EAAE;IACpD,OAAO;KACL,KAAK,WAAW,WAAW,QAAQ;KACnC,SAAS,qBAAqB,UAAU,KAAK;IAC/C;GACF;GACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC;CACH;CAEA,MAAM,cAA6B;EACjC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,mCAAmC;EAClF,MAAM,QAAQ,eAAe,SAAS,QAAQ;EAC9C,OAAO,IAAI,mBAAmB;GAC5B,SAAS,IAAI,UAAU;IACrB,MAAM,WAAW,eAAe,OAAO,SAAS,EAAE;IAClD,OAAO;KAAE,KAAK,WAAW,SAAS,QAAQ;KAAG,SAAS,mBAAmB,UAAU,KAAK;IAAE;GAC5F;GACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC;CACH;CAEA,OAAO,eAAe;EAAE,OAAO,MAAM;EAAG,MAAM,KAAK;EAAG,SAAS,QAAQ;EAAG,OAAO,MAAM;CAAE,CAAC;AAC5F"}