@q32/core 0.1.28 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/aws.d.ts +24 -0
- package/dist/aws.d.ts.map +1 -0
- package/dist/aws.js +109 -0
- package/dist/aws.js.map +1 -0
- package/dist/email.d.ts +8 -0
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +87 -0
- package/dist/email.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/ops-events.d.ts +4 -4
- package/dist/ops-events.d.ts.map +1 -1
- package/dist/ops-events.js +4 -4
- package/dist/ops-events.js.map +1 -1
- package/dist/ses-sns.d.ts +60 -0
- package/dist/ses-sns.d.ts.map +1 -0
- package/dist/ses-sns.js +96 -0
- package/dist/ses-sns.js.map +1 -0
- package/dist/ses.d.ts +53 -0
- package/dist/ses.d.ts.map +1 -0
- package/dist/ses.js +125 -0
- package/dist/ses.js.map +1 -0
- package/package.json +13 -1
- package/src/aws.ts +152 -0
- package/src/email.ts +99 -0
- package/src/index.ts +3 -0
- package/src/ops-events.ts +4 -4
- package/src/ses-sns.ts +144 -0
- package/src/ses.ts +180 -0
- package/docs/commonality-adgiro-relin-dirtsignal.md +0 -28
- package/docs/evaluation.md +0 -76
- package/docs/goal-audit.md +0 -28
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ses-sns.js","sourceRoot":"","sources":["../src/ses-sns.ts"],"names":[],"mappings":"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"}
|
package/dist/ses.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type AwsConfig, type AwsEnv } from "./aws.js";
|
|
2
|
+
import { type EmailAddress, type SendEmailInput } from "./email.js";
|
|
3
|
+
export type SesEmailInput = Omit<SendEmailInput, "from"> & {
|
|
4
|
+
from?: EmailAddress;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export type SesSendResult = {
|
|
8
|
+
messageId: string;
|
|
9
|
+
provider: "ses";
|
|
10
|
+
};
|
|
11
|
+
export type SesMailer = {
|
|
12
|
+
readonly enabled: boolean;
|
|
13
|
+
send(input: SesEmailInput): Promise<SesSendResult>;
|
|
14
|
+
};
|
|
15
|
+
export type SesMailerEnv = AwsEnv & {
|
|
16
|
+
FROM_EMAIL?: string;
|
|
17
|
+
EMAIL_FROM?: string;
|
|
18
|
+
NOTIFICATION_FROM_EMAIL?: string;
|
|
19
|
+
POSTMARK_FROM_EMAIL?: string;
|
|
20
|
+
SES_FROM_EMAIL?: string;
|
|
21
|
+
EMAIL_REPLY_TO?: string;
|
|
22
|
+
NOTIFICATION_REPLY_TO?: string;
|
|
23
|
+
};
|
|
24
|
+
export type SesSendOptions = {
|
|
25
|
+
fetcher?: typeof fetch;
|
|
26
|
+
};
|
|
27
|
+
export declare class NoopSesMailer implements SesMailer {
|
|
28
|
+
readonly enabled = false;
|
|
29
|
+
send(): Promise<SesSendResult>;
|
|
30
|
+
}
|
|
31
|
+
export declare class WorkerSesMailer implements SesMailer {
|
|
32
|
+
private readonly config;
|
|
33
|
+
private readonly defaults;
|
|
34
|
+
readonly enabled = true;
|
|
35
|
+
constructor(config: AwsConfig, defaults: {
|
|
36
|
+
from: EmailAddress;
|
|
37
|
+
replyTo?: EmailAddress[];
|
|
38
|
+
fetcher?: typeof fetch;
|
|
39
|
+
});
|
|
40
|
+
send(input: SesEmailInput): Promise<SesSendResult>;
|
|
41
|
+
}
|
|
42
|
+
export declare function createSesMailerFromEnv(env: SesMailerEnv, options?: {
|
|
43
|
+
fromEmail?: string;
|
|
44
|
+
fromName?: string;
|
|
45
|
+
replyTo?: string | string[] | null;
|
|
46
|
+
fetcher?: typeof fetch;
|
|
47
|
+
}): SesMailer;
|
|
48
|
+
export declare function sendSesEmail(config: AwsConfig, input: SendEmailInput & {
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
}, options?: SesSendOptions): Promise<SesSendResult>;
|
|
51
|
+
export declare function sesQuery(config: AwsConfig, params: Record<string, string>, options?: SesSendOptions): Promise<string>;
|
|
52
|
+
export declare function inboundSesMxHost(region?: string): string;
|
|
53
|
+
//# sourceMappingURL=ses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ses.d.ts","sourceRoot":"","sources":["../src/ses.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AACzF,OAAO,EAA2C,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE7G,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG;IACzD,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACpD,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,CAAC;AAEF,qBAAa,aAAc,YAAW,SAAS;IAC7C,QAAQ,CAAC,OAAO,SAAS;IAEnB,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC;CAGrC;AAED,qBAAa,eAAgB,YAAW,SAAS;IAI7C,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ3B,QAAQ,CAAC,OAAO,QAAQ;gBAGL,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE;QACzB,IAAI,EAAE,YAAY,CAAC;QACnB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;QACzB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;KACxB;IAGH,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;CAOnD;AAED,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,YAAY,EACjB,OAAO,GAAE;IACP,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACnB,GACL,SAAS,CAcX;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,cAAc,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,EAC5D,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAaxB;AAED,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED,wBAAgB,gBAAgB,CAAC,MAAM,SAAc,GAAG,MAAM,CAE7D"}
|
package/dist/ses.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { awsFetch, createAwsConfigFromEnv } from "./aws.js";
|
|
2
|
+
import { buildRawMimeMessage, formatEmailAddress } from "./email.js";
|
|
3
|
+
export class NoopSesMailer {
|
|
4
|
+
enabled = false;
|
|
5
|
+
async send() {
|
|
6
|
+
throw new Error("SES mailer is not configured.");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class WorkerSesMailer {
|
|
10
|
+
config;
|
|
11
|
+
defaults;
|
|
12
|
+
enabled = true;
|
|
13
|
+
constructor(config, defaults) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.defaults = defaults;
|
|
16
|
+
}
|
|
17
|
+
send(input) {
|
|
18
|
+
return sendSesEmail(this.config, {
|
|
19
|
+
...input,
|
|
20
|
+
from: input.from ?? this.defaults.from,
|
|
21
|
+
replyTo: input.replyTo ?? this.defaults.replyTo,
|
|
22
|
+
}, { fetcher: this.defaults.fetcher });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function createSesMailerFromEnv(env, options = {}) {
|
|
26
|
+
const config = createAwsConfigFromEnv(env);
|
|
27
|
+
const fromEmail = options.fromEmail ?? env.FROM_EMAIL ?? env.EMAIL_FROM ?? env.NOTIFICATION_FROM_EMAIL ?? env.POSTMARK_FROM_EMAIL ?? env.SES_FROM_EMAIL;
|
|
28
|
+
if (!config || !fromEmail)
|
|
29
|
+
return new NoopSesMailer();
|
|
30
|
+
const replyTo = options.replyTo ?? env.NOTIFICATION_REPLY_TO ?? env.EMAIL_REPLY_TO;
|
|
31
|
+
return new WorkerSesMailer(config, {
|
|
32
|
+
from: { email: fromEmail, name: options.fromName },
|
|
33
|
+
replyTo: typeof replyTo === "string"
|
|
34
|
+
? [{ email: replyTo }]
|
|
35
|
+
: Array.isArray(replyTo)
|
|
36
|
+
? replyTo.map((email) => ({ email }))
|
|
37
|
+
: undefined,
|
|
38
|
+
fetcher: options.fetcher,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export async function sendSesEmail(config, input, options = {}) {
|
|
42
|
+
const hasRawFeatures = Boolean(input.headers && Object.keys(input.headers).length > 0) || Boolean(input.attachments?.length);
|
|
43
|
+
const body = hasRawFeatures ? rawSesBody(input) : simpleSesBody(input);
|
|
44
|
+
const response = await awsFetch(config, `https://email.${config.region}.amazonaws.com/v2/email/outbound-emails`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "content-type": "application/json" },
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
}, { service: "ses", fetcher: options.fetcher });
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
throw new Error(`SES send failed: ${response.status} ${text.slice(0, 300)}`);
|
|
52
|
+
const payload = parseJsonObject(text);
|
|
53
|
+
const messageId = stringValue(payload.MessageId) ?? stringValue(payload.messageId) ?? "";
|
|
54
|
+
return { provider: "ses", messageId };
|
|
55
|
+
}
|
|
56
|
+
export async function sesQuery(config, params, options = {}) {
|
|
57
|
+
const body = new URLSearchParams({ Version: "2010-12-01", ...params }).toString();
|
|
58
|
+
const response = await awsFetch(config, `https://email.${config.region}.amazonaws.com/`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
61
|
+
body,
|
|
62
|
+
}, { service: "ses", fetcher: options.fetcher });
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
if (!response.ok)
|
|
65
|
+
throw new Error(`SES ${params.Action ?? "query"} failed: ${response.status} ${text.slice(0, 300)}`);
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
export function inboundSesMxHost(region = "us-east-1") {
|
|
69
|
+
return `inbound-smtp.${region}.amazonaws.com`;
|
|
70
|
+
}
|
|
71
|
+
function simpleSesBody(input) {
|
|
72
|
+
return {
|
|
73
|
+
FromEmailAddress: formatEmailAddress(input.from),
|
|
74
|
+
Destination: {
|
|
75
|
+
ToAddresses: input.to.map(formatEmailAddress),
|
|
76
|
+
...(input.cc?.length ? { CcAddresses: input.cc.map(formatEmailAddress) } : {}),
|
|
77
|
+
...(input.bcc?.length ? { BccAddresses: input.bcc.map(formatEmailAddress) } : {}),
|
|
78
|
+
},
|
|
79
|
+
...(input.replyTo?.length ? { ReplyToAddresses: input.replyTo.map(formatEmailAddress) } : {}),
|
|
80
|
+
Content: {
|
|
81
|
+
Simple: {
|
|
82
|
+
Subject: { Data: input.subject, Charset: "UTF-8" },
|
|
83
|
+
Body: {
|
|
84
|
+
...(input.text ? { Text: { Data: input.text, Charset: "UTF-8" } } : {}),
|
|
85
|
+
...(input.html ? { Html: { Data: input.html, Charset: "UTF-8" } } : {}),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function rawSesBody(input) {
|
|
92
|
+
return {
|
|
93
|
+
FromEmailAddress: formatEmailAddress(input.from),
|
|
94
|
+
Destination: {
|
|
95
|
+
ToAddresses: input.to.map(formatEmailAddress),
|
|
96
|
+
...(input.cc?.length ? { CcAddresses: input.cc.map(formatEmailAddress) } : {}),
|
|
97
|
+
...(input.bcc?.length ? { BccAddresses: input.bcc.map(formatEmailAddress) } : {}),
|
|
98
|
+
},
|
|
99
|
+
Content: {
|
|
100
|
+
Raw: {
|
|
101
|
+
Data: textToBase64(buildRawMimeMessage(input)),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function textToBase64(value) {
|
|
107
|
+
const bytes = new TextEncoder().encode(value);
|
|
108
|
+
let binary = "";
|
|
109
|
+
for (const byte of bytes)
|
|
110
|
+
binary += String.fromCharCode(byte);
|
|
111
|
+
return btoa(binary);
|
|
112
|
+
}
|
|
113
|
+
function parseJsonObject(value) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(value);
|
|
116
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function stringValue(value) {
|
|
123
|
+
return typeof value === "string" ? value : null;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=ses.js.map
|
package/dist/ses.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ses.js","sourceRoot":"","sources":["../src/ses.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q32/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared TypeScript primitives for Q32 Cloudflare Worker projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"types": "./dist/auth.d.ts",
|
|
33
33
|
"default": "./dist/auth.js"
|
|
34
34
|
},
|
|
35
|
+
"./aws": {
|
|
36
|
+
"types": "./dist/aws.d.ts",
|
|
37
|
+
"default": "./dist/aws.js"
|
|
38
|
+
},
|
|
35
39
|
"./billing": {
|
|
36
40
|
"types": "./dist/billing.d.ts",
|
|
37
41
|
"default": "./dist/billing.js"
|
|
@@ -120,6 +124,14 @@
|
|
|
120
124
|
"types": "./dist/session.d.ts",
|
|
121
125
|
"default": "./dist/session.js"
|
|
122
126
|
},
|
|
127
|
+
"./ses": {
|
|
128
|
+
"types": "./dist/ses.d.ts",
|
|
129
|
+
"default": "./dist/ses.js"
|
|
130
|
+
},
|
|
131
|
+
"./ses-sns": {
|
|
132
|
+
"types": "./dist/ses-sns.d.ts",
|
|
133
|
+
"default": "./dist/ses-sns.js"
|
|
134
|
+
},
|
|
123
135
|
"./testing": {
|
|
124
136
|
"types": "./dist/testing.d.ts",
|
|
125
137
|
"default": "./dist/testing.js"
|
package/src/aws.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export type AwsCredentials = {
|
|
2
|
+
accessKeyId: string;
|
|
3
|
+
secretAccessKey: string;
|
|
4
|
+
sessionToken?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type AwsConfig = AwsCredentials & {
|
|
8
|
+
region: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AwsEnv = {
|
|
12
|
+
AWS_ACCESS_KEY?: string;
|
|
13
|
+
AWS_SECRET_KEY?: string;
|
|
14
|
+
AWS_ACCESS_KEY_ID?: string;
|
|
15
|
+
AWS_SECRET_ACCESS_KEY?: string;
|
|
16
|
+
AWS_SESSION_TOKEN?: string;
|
|
17
|
+
AWS_REGION?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createAwsConfigFromEnv(env: AwsEnv, defaultRegion = "us-east-1"): AwsConfig | null {
|
|
21
|
+
const accessKeyId = env.AWS_ACCESS_KEY_ID ?? env.AWS_ACCESS_KEY;
|
|
22
|
+
const secretAccessKey = env.AWS_SECRET_ACCESS_KEY ?? env.AWS_SECRET_KEY;
|
|
23
|
+
if (!accessKeyId || !secretAccessKey) return null;
|
|
24
|
+
return {
|
|
25
|
+
accessKeyId,
|
|
26
|
+
secretAccessKey,
|
|
27
|
+
sessionToken: env.AWS_SESSION_TOKEN,
|
|
28
|
+
region: env.AWS_REGION ?? defaultRegion,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type AwsFetchOptions = {
|
|
33
|
+
service: string;
|
|
34
|
+
region?: string;
|
|
35
|
+
fetcher?: typeof fetch;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function awsFetch(
|
|
39
|
+
config: AwsConfig,
|
|
40
|
+
input: string | URL,
|
|
41
|
+
init: RequestInit,
|
|
42
|
+
options: AwsFetchOptions,
|
|
43
|
+
): Promise<Response> {
|
|
44
|
+
const url = new URL(input.toString());
|
|
45
|
+
const region = options.region ?? config.region;
|
|
46
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
47
|
+
const body = bodyToString(init.body);
|
|
48
|
+
const payloadHash = await sha256Hex(body);
|
|
49
|
+
const headers = new Headers(init.headers);
|
|
50
|
+
headers.set("host", url.host);
|
|
51
|
+
headers.set("x-amz-content-sha256", payloadHash);
|
|
52
|
+
const now = new Date();
|
|
53
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
54
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
55
|
+
headers.set("x-amz-date", amzDate);
|
|
56
|
+
if (config.sessionToken) headers.set("x-amz-security-token", config.sessionToken);
|
|
57
|
+
|
|
58
|
+
const signedHeaderNames = headerNames(headers).map((key) => key.toLowerCase()).sort();
|
|
59
|
+
const canonicalHeaders = signedHeaderNames.map((key) => `${key}:${normalizeHeaderValue(headers.get(key) ?? "")}\n`).join("");
|
|
60
|
+
const canonicalRequest = [
|
|
61
|
+
method,
|
|
62
|
+
canonicalPath(url),
|
|
63
|
+
canonicalQuery(url),
|
|
64
|
+
canonicalHeaders,
|
|
65
|
+
signedHeaderNames.join(";"),
|
|
66
|
+
payloadHash,
|
|
67
|
+
].join("\n");
|
|
68
|
+
const credentialScope = `${dateStamp}/${region}/${options.service}/aws4_request`;
|
|
69
|
+
const stringToSign = [
|
|
70
|
+
"AWS4-HMAC-SHA256",
|
|
71
|
+
amzDate,
|
|
72
|
+
credentialScope,
|
|
73
|
+
await sha256Hex(canonicalRequest),
|
|
74
|
+
].join("\n");
|
|
75
|
+
const signature = await hmacHex(await signingKey(config.secretAccessKey, dateStamp, region, options.service), stringToSign);
|
|
76
|
+
headers.set(
|
|
77
|
+
"authorization",
|
|
78
|
+
`AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames.join(";")}, Signature=${signature}`,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (options.fetcher ?? fetch)(url, {
|
|
82
|
+
...init,
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
body: init.body,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function bodyToString(body: BodyInit | null | undefined): string {
|
|
90
|
+
if (!body) return "";
|
|
91
|
+
if (typeof body === "string") return body;
|
|
92
|
+
throw new Error("awsFetch currently supports string request bodies.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function canonicalPath(url: URL): string {
|
|
96
|
+
return url.pathname
|
|
97
|
+
.split("/")
|
|
98
|
+
.map((part) => encodeURIComponent(decodeURIComponent(part)).replace(/[!'()*]/g, pctEncode))
|
|
99
|
+
.join("/");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function canonicalQuery(url: URL): string {
|
|
103
|
+
const entries: Array<[string, string]> = [];
|
|
104
|
+
url.searchParams.forEach((value, key) => entries.push([key, value]));
|
|
105
|
+
return entries
|
|
106
|
+
.map(([key, value]) => [encodeRfc3986(key), encodeRfc3986(value)] as const)
|
|
107
|
+
.sort(([leftKey, leftValue], [rightKey, rightValue]) => leftKey.localeCompare(rightKey) || leftValue.localeCompare(rightValue))
|
|
108
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
109
|
+
.join("&");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function headerNames(headers: Headers): string[] {
|
|
113
|
+
const names: string[] = [];
|
|
114
|
+
headers.forEach((_value, key) => names.push(key));
|
|
115
|
+
return names;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeHeaderValue(value: string): string {
|
|
119
|
+
return value.trim().replace(/\s+/g, " ");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function encodeRfc3986(value: string): string {
|
|
123
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, pctEncode);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pctEncode(char: string): string {
|
|
127
|
+
return `%${char.charCodeAt(0).toString(16).toUpperCase()}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function signingKey(secret: string, date: string, region: string, service: string): Promise<ArrayBuffer> {
|
|
131
|
+
const kDate = await hmacRaw(new TextEncoder().encode(`AWS4${secret}`), date);
|
|
132
|
+
const kRegion = await hmacRaw(kDate, region);
|
|
133
|
+
const kService = await hmacRaw(kRegion, service);
|
|
134
|
+
return hmacRaw(kService, "aws4_request");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function hmacRaw(key: BufferSource, value: string): Promise<ArrayBuffer> {
|
|
138
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
139
|
+
return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(value));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function hmacHex(key: BufferSource, value: string): Promise<string> {
|
|
143
|
+
return bytesToHex(new Uint8Array(await hmacRaw(key, value)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function sha256Hex(value: string): Promise<string> {
|
|
147
|
+
return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
151
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
152
|
+
}
|
package/src/email.ts
CHANGED
|
@@ -47,3 +47,102 @@ export function normalizeEmailAddress(email: string): string {
|
|
|
47
47
|
export function appendUnsubscribeFooter(text: string, unsubscribeUrl: string): string {
|
|
48
48
|
return `${text.trim()}\n\nUnsubscribe: ${unsubscribeUrl}\n`;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
export function sanitizeEmailHeaderValue(value: string): string {
|
|
52
|
+
return value.replace(/[\r\n]+/g, " ").trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function encodeMimeHeader(value: string): string {
|
|
56
|
+
if (/^[\x20-\x7e]*$/.test(value)) return sanitizeEmailHeaderValue(value);
|
|
57
|
+
return `=?UTF-8?B?${base64Utf8(value)}?=`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildListUnsubscribeHeaders(url: string): Record<string, string> {
|
|
61
|
+
return {
|
|
62
|
+
"List-Unsubscribe": `<${sanitizeEmailHeaderValue(url)}>`,
|
|
63
|
+
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type RawMimeMessageInput = Omit<SendEmailInput, "tags"> & {
|
|
68
|
+
headers?: Record<string, string>;
|
|
69
|
+
boundaryPrefix?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function buildRawMimeMessage(input: RawMimeMessageInput): string {
|
|
73
|
+
const mixedBoundary = `${input.boundaryPrefix ?? "q32"}-mixed-${crypto.randomUUID()}`;
|
|
74
|
+
const bodyBoundary = `${input.boundaryPrefix ?? "q32"}-body-${crypto.randomUUID()}`;
|
|
75
|
+
const attachments = input.attachments ?? [];
|
|
76
|
+
const headers = [
|
|
77
|
+
`From: ${formatEmailAddress(input.from)}`,
|
|
78
|
+
`To: ${input.to.map(formatEmailAddress).join(", ")}`,
|
|
79
|
+
...(input.cc?.length ? [`Cc: ${input.cc.map(formatEmailAddress).join(", ")}`] : []),
|
|
80
|
+
`Subject: ${encodeMimeHeader(input.subject)}`,
|
|
81
|
+
"MIME-Version: 1.0",
|
|
82
|
+
...(input.replyTo?.length ? [`Reply-To: ${input.replyTo.map(formatEmailAddress).join(", ")}`] : []),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const [name, value] of Object.entries(input.headers ?? {})) {
|
|
86
|
+
const headerName = sanitizeHeaderName(name);
|
|
87
|
+
const headerValue = sanitizeEmailHeaderValue(value);
|
|
88
|
+
if (headerName && headerValue) headers.push(`${headerName}: ${headerValue}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parts = [...headers, `Content-Type: multipart/mixed; boundary="${mixedBoundary}"`, ""];
|
|
92
|
+
parts.push(`--${mixedBoundary}`);
|
|
93
|
+
|
|
94
|
+
if (input.text && input.html) {
|
|
95
|
+
parts.push(`Content-Type: multipart/alternative; boundary="${bodyBoundary}"`, "");
|
|
96
|
+
parts.push(`--${bodyBoundary}`);
|
|
97
|
+
parts.push("Content-Type: text/plain; charset=UTF-8");
|
|
98
|
+
parts.push("Content-Transfer-Encoding: base64", "", wrapBase64(base64Utf8(input.text)), "");
|
|
99
|
+
parts.push(`--${bodyBoundary}`);
|
|
100
|
+
parts.push("Content-Type: text/html; charset=UTF-8");
|
|
101
|
+
parts.push("Content-Transfer-Encoding: base64", "", wrapBase64(base64Utf8(input.html)), "");
|
|
102
|
+
parts.push(`--${bodyBoundary}--`, "");
|
|
103
|
+
} else if (input.html) {
|
|
104
|
+
parts.push("Content-Type: text/html; charset=UTF-8");
|
|
105
|
+
parts.push("Content-Transfer-Encoding: base64", "", wrapBase64(base64Utf8(input.html)), "");
|
|
106
|
+
} else {
|
|
107
|
+
parts.push("Content-Type: text/plain; charset=UTF-8");
|
|
108
|
+
parts.push("Content-Transfer-Encoding: base64", "", wrapBase64(base64Utf8(input.text ?? "")), "");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const attachment of attachments) {
|
|
112
|
+
parts.push(`--${mixedBoundary}`);
|
|
113
|
+
parts.push(`Content-Type: ${sanitizeEmailHeaderValue(attachment.contentType)}; name=${quoteHeaderParam(attachment.filename)}`);
|
|
114
|
+
parts.push("Content-Transfer-Encoding: base64");
|
|
115
|
+
parts.push(`Content-Disposition: attachment; filename=${quoteHeaderParam(attachment.filename)}`);
|
|
116
|
+
parts.push("", wrapBase64(attachmentDataToBase64(attachment.data)), "");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
parts.push(`--${mixedBoundary}--`, "");
|
|
120
|
+
return parts.join("\r\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sanitizeHeaderName(value: string): string {
|
|
124
|
+
const name = value.trim();
|
|
125
|
+
return /^[A-Za-z0-9-]+$/.test(name) ? name : "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function quoteHeaderParam(value: string): string {
|
|
129
|
+
return `"${sanitizeEmailHeaderValue(value).replace(/(["\\])/g, "\\$1")}"`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function attachmentDataToBase64(value: Uint8Array | string): string {
|
|
133
|
+
if (typeof value === "string") return base64Utf8(value);
|
|
134
|
+
let binary = "";
|
|
135
|
+
for (const byte of value) binary += String.fromCharCode(byte);
|
|
136
|
+
return btoa(binary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function base64Utf8(value: string): string {
|
|
140
|
+
const bytes = new TextEncoder().encode(value);
|
|
141
|
+
let binary = "";
|
|
142
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
143
|
+
return btoa(binary);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function wrapBase64(value: string): string {
|
|
147
|
+
return value.replace(/.{1,76}/g, "$&\r\n").trimEnd();
|
|
148
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./api.js";
|
|
2
2
|
export * from "./ai.js";
|
|
3
3
|
export * from "./auth.js";
|
|
4
|
+
export * from "./aws.js";
|
|
4
5
|
export * from "./billing.js";
|
|
5
6
|
export * from "./cloudflare.js";
|
|
6
7
|
export * from "./crypto.js";
|
|
@@ -21,5 +22,7 @@ export * from "./rate-limit.js";
|
|
|
21
22
|
export * from "./react-router.js";
|
|
22
23
|
export * from "./seo.js";
|
|
23
24
|
export * from "./session.js";
|
|
25
|
+
export * from "./ses.js";
|
|
26
|
+
export * from "./ses-sns.js";
|
|
24
27
|
export * from "./testing.js";
|
|
25
28
|
export * from "./time.js";
|
package/src/ops-events.ts
CHANGED
|
@@ -258,7 +258,7 @@ export const DEFAULT_D1_OPS_EVENT_COLUMNS: OpsEventColumn[] = [
|
|
|
258
258
|
{ column: "payload_json", value: "payloadJson" },
|
|
259
259
|
];
|
|
260
260
|
|
|
261
|
-
export const
|
|
261
|
+
export const GRAPH_SCOPE_EVENT_COLUMNS: OpsEventColumn[] = [
|
|
262
262
|
{ column: "event_id", value: "eventId" },
|
|
263
263
|
{ column: "graph_id", value: "scopeId" },
|
|
264
264
|
{ column: "event_name", value: "eventName" },
|
|
@@ -273,7 +273,7 @@ export const GRAPHILIZE_GRAPH_EVENTS_COLUMNS: OpsEventColumn[] = [
|
|
|
273
273
|
{ column: "occurred_at", value: "occurredAt", cast: "timestamptz" },
|
|
274
274
|
];
|
|
275
275
|
|
|
276
|
-
export const
|
|
276
|
+
export const RUN_SCOPED_OPS_EVENT_COLUMNS: OpsEventColumn[] = [
|
|
277
277
|
{ column: "event_id", value: "eventId" },
|
|
278
278
|
{ column: "run_id", value: "runId" },
|
|
279
279
|
{ column: "event_name", value: "eventName" },
|
|
@@ -288,7 +288,7 @@ export const DIRT_SIGNAL_OPS_EVENTS_COLUMNS: OpsEventColumn[] = [
|
|
|
288
288
|
{ column: "occurred_at", value: "occurredAt", cast: "timestamptz" },
|
|
289
289
|
];
|
|
290
290
|
|
|
291
|
-
export const
|
|
291
|
+
export const OPERATIONAL_EVENT_COLUMNS: OpsEventColumn[] = [
|
|
292
292
|
{ column: "operational_event_id", value: "eventId" },
|
|
293
293
|
{ column: "name", value: "eventName" },
|
|
294
294
|
{ column: "status", value: "status" },
|
|
@@ -300,7 +300,7 @@ export const RELIN_OPERATIONAL_EVENTS_COLUMNS: OpsEventColumn[] = [
|
|
|
300
300
|
{ column: "metadata_json", value: "metadataJsonOrNull" },
|
|
301
301
|
];
|
|
302
302
|
|
|
303
|
-
export const
|
|
303
|
+
export const REQUEST_JOB_OPS_EVENT_COLUMNS: OpsEventColumn[] = [
|
|
304
304
|
{ column: "ops_event_id", value: "eventId" },
|
|
305
305
|
{ column: "level", value: "severity" },
|
|
306
306
|
{ column: "event_type", value: "eventName" },
|