@webamoki/web-svelte 0.7.2 → 0.7.3

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.
@@ -1,14 +1,13 @@
1
- import { DatabaseError } from 'pg';
2
1
  import { fail, message as superFormMessage } from 'sveltekit-superforms';
3
2
  /**
4
3
  * automatically handle database errors from catch.
5
4
  * used in form/action handling in page.server.ts
6
5
  */
7
6
  export function handleDbErrorForm(form, message, err) {
8
- if (err instanceof DatabaseError) {
9
- console.error(`Database Error ${message}:`, err);
10
- return fail(500, { form });
11
- }
7
+ // if (err instanceof DatabaseError) {
8
+ // console.error(`Database Error ${message}:`, err);
9
+ // return fail(500, { form });
10
+ // }
12
11
  console.error(`Unexpected Error ${message}:`, err);
13
12
  return fail(500, { form });
14
13
  }
@@ -16,7 +15,9 @@ export function handleDbErrorForm(form, message, err) {
16
15
  * check if an error returned by a try catch is a duplicate value error in postgre
17
16
  */
18
17
  export function isDuplicateDbError(err) {
19
- return err instanceof DatabaseError && err.code === '23505';
18
+ console.error('Checking for duplicate error:', err);
19
+ console.error('Error type:', err instanceof Error ? err.name : typeof err);
20
+ return false;
20
21
  }
21
22
  export function successMessage(form, options) {
22
23
  const message = {
@@ -1,10 +1,20 @@
1
1
  # Email Utility Package
2
2
 
3
- This package provides utilities for sending emails using AWS Simple Email Service (SES).
3
+ This package provides utilities for sending emails using AWS Simple Email Service (SES) API.
4
+
5
+ ## Key Features
6
+
7
+ - **Cloudflare Workers Compatible**: Uses direct AWS SES API calls with fetch instead of AWS SDK
8
+ - **AWS Signature V4**: Implements proper AWS authentication for secure API requests
9
+ - **Zero Heavy Dependencies**: No AWS SDK required, works in any JavaScript runtime with fetch support
4
10
 
5
11
  ## Installation
6
12
 
7
- The AWS SES SDK is already included as a dependency. You'll need to configure your AWS credentials and region.
13
+ No additional dependencies required beyond the standard JavaScript runtime. The implementation uses:
14
+
15
+ - Native `fetch` API for HTTP requests
16
+ - Web Crypto API for AWS Signature V4 signing
17
+ - Native `DOMParser` for XML response parsing
8
18
 
9
19
  ## Configuration
10
20
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * AWS Signature Version 4 signing utilities for SES API
3
+ * Compatible with Cloudflare Workers and other edge runtimes
4
+ */
5
+ interface AwsCredentials {
6
+ accessKeyId: string;
7
+ secretAccessKey: string;
8
+ region: string;
9
+ }
10
+ /**
11
+ * Create AWS Signature V4 authorization header
12
+ */
13
+ export declare function signRequest(method: string, host: string, path: string, body: string, credentials: AwsCredentials, service?: string): Promise<{
14
+ headers: Record<string, string>;
15
+ signedHeaders: string;
16
+ }>;
17
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * AWS Signature Version 4 signing utilities for SES API
3
+ * Compatible with Cloudflare Workers and other edge runtimes
4
+ */
5
+ /**
6
+ * Create a SHA-256 hash
7
+ */
8
+ async function sha256(message) {
9
+ const encoder = new TextEncoder();
10
+ const data = encoder.encode(message);
11
+ return await crypto.subtle.digest('SHA-256', data);
12
+ }
13
+ /**
14
+ * Create a SHA-256 HMAC
15
+ */
16
+ async function hmacSha256(key, message) {
17
+ const encoder = new TextEncoder();
18
+ const keyData = key instanceof ArrayBuffer ? new Uint8Array(key) : key;
19
+ const cryptoKey = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
20
+ return await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));
21
+ }
22
+ /**
23
+ * Convert ArrayBuffer to hex string
24
+ */
25
+ function bufferToHex(buffer) {
26
+ return Array.from(new Uint8Array(buffer))
27
+ .map((b) => b.toString(16).padStart(2, '0'))
28
+ .join('');
29
+ }
30
+ /**
31
+ * Get signing key for AWS Signature V4
32
+ */
33
+ async function getSignatureKey(key, dateStamp, regionName, serviceName) {
34
+ const encoder = new TextEncoder();
35
+ const kDate = await hmacSha256(encoder.encode('AWS4' + key), dateStamp);
36
+ const kRegion = await hmacSha256(kDate, regionName);
37
+ const kService = await hmacSha256(kRegion, serviceName);
38
+ const kSigning = await hmacSha256(kService, 'aws4_request');
39
+ return kSigning;
40
+ }
41
+ /**
42
+ * Create AWS Signature V4 authorization header
43
+ */
44
+ export async function signRequest(method, host, path, body, credentials, service = 'ses') {
45
+ const { accessKeyId, secretAccessKey, region } = credentials;
46
+ // Create timestamp
47
+ const now = new Date();
48
+ const amzDate = now
49
+ .toISOString()
50
+ .replace(/[:-]|\.\d{3}/g, '')
51
+ .slice(0, -1); // Format: YYYYMMDD'T'HHMMSS'Z'
52
+ const dateStamp = amzDate.slice(0, 8); // Format: YYYYMMDD
53
+ // Hash the request body
54
+ const payloadHash = bufferToHex(await sha256(body));
55
+ // Create canonical headers
56
+ const canonicalHeaders = `host:${host}\n` + `x-amz-date:${amzDate}\n`;
57
+ const signedHeaders = 'host;x-amz-date';
58
+ // Create canonical request
59
+ const canonicalRequest = `${method}\n` +
60
+ `${path}\n` +
61
+ `\n` + // Empty query string
62
+ `${canonicalHeaders}\n` +
63
+ `${signedHeaders}\n` +
64
+ `${payloadHash}`;
65
+ // Create string to sign
66
+ const algorithm = 'AWS4-HMAC-SHA256';
67
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
68
+ const canonicalRequestHash = bufferToHex(await sha256(canonicalRequest));
69
+ const stringToSign = `${algorithm}\n` + `${amzDate}\n` + `${credentialScope}\n` + `${canonicalRequestHash}`;
70
+ // Calculate signature
71
+ const signingKey = await getSignatureKey(secretAccessKey, dateStamp, region, service);
72
+ const signature = bufferToHex(await hmacSha256(signingKey, stringToSign));
73
+ // Create authorization header
74
+ const authorizationHeader = `${algorithm} ` +
75
+ `Credential=${accessKeyId}/${credentialScope}, ` +
76
+ `SignedHeaders=${signedHeaders}, ` +
77
+ `Signature=${signature}`;
78
+ return {
79
+ headers: {
80
+ Authorization: authorizationHeader,
81
+ 'X-Amz-Date': amzDate,
82
+ 'Content-Type': 'application/x-www-form-urlencoded'
83
+ },
84
+ signedHeaders
85
+ };
86
+ }
@@ -10,7 +10,8 @@ export interface SendEmailOptions {
10
10
  replyTo?: string | string[];
11
11
  }
12
12
  /**
13
- * Send an email using AWS SES.
13
+ * Send an email using AWS SES API.
14
+ * Uses AWS Signature V4 signing and fetch API for Cloudflare Workers compatibility.
14
15
  *
15
16
  * Environment variables required:
16
17
  * - AWS_REGION
@@ -1,8 +1,7 @@
1
- import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
2
- // Create SES client once at module level for reuse across function calls
3
- const sesClient = new SESClient({ region: process.env.AWS_REGION });
1
+ import { signRequest } from './aws-signer.js';
4
2
  /**
5
- * Send an email using AWS SES.
3
+ * Send an email using AWS SES API.
4
+ * Uses AWS Signature V4 signing and fetch API for Cloudflare Workers compatibility.
6
5
  *
7
6
  * Environment variables required:
8
7
  * - AWS_REGION
@@ -39,48 +38,105 @@ export async function sendEmail(options) {
39
38
  if (!toAddresses || toAddresses.length === 0) {
40
39
  throw new Error('sendEmail: at least one valid recipient is required (to)');
41
40
  }
41
+ // Get AWS credentials from environment
42
+ const region = process.env.AWS_REGION;
43
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
44
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
45
+ if (!region || !accessKeyId || !secretAccessKey) {
46
+ throw new Error('sendEmail: missing required environment variables (AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)');
47
+ }
42
48
  // Format source with optional fromName
43
49
  const source = fromName ? `${fromName} <${from}>` : from;
44
- // Build message body
45
- const Message = {
46
- Subject: {
47
- Charset: 'UTF-8',
48
- Data: subject
49
- },
50
- Body: {}
51
- };
50
+ // Build form-encoded request body for SES API
51
+ const params = new URLSearchParams();
52
+ params.append('Action', 'SendEmail');
53
+ params.append('Source', source);
54
+ // Add destination addresses
55
+ toAddresses.forEach((addr, i) => params.append(`Destination.ToAddresses.member.${i + 1}`, addr));
56
+ if (ccAddresses) {
57
+ ccAddresses.forEach((addr, i) => params.append(`Destination.CcAddresses.member.${i + 1}`, addr));
58
+ }
59
+ if (bccAddresses) {
60
+ bccAddresses.forEach((addr, i) => params.append(`Destination.BccAddresses.member.${i + 1}`, addr));
61
+ }
62
+ // Add reply-to addresses
63
+ if (replyToAddresses) {
64
+ replyToAddresses.forEach((addr, i) => params.append(`ReplyToAddresses.member.${i + 1}`, addr));
65
+ }
66
+ // Add message subject
67
+ params.append('Message.Subject.Data', subject);
68
+ params.append('Message.Subject.Charset', 'UTF-8');
69
+ // Add message body
52
70
  if (html) {
53
- Message.Body.Html = {
54
- Charset: 'UTF-8',
55
- Data: html
56
- };
71
+ params.append('Message.Body.Html.Data', html);
72
+ params.append('Message.Body.Html.Charset', 'UTF-8');
57
73
  }
58
74
  if (text) {
59
- Message.Body.Text = {
60
- Charset: 'UTF-8',
61
- Data: text
62
- };
75
+ params.append('Message.Body.Text.Data', text);
76
+ params.append('Message.Body.Text.Charset', 'UTF-8');
63
77
  }
64
- const params = {
65
- Source: source,
66
- Destination: {
67
- ToAddresses: toAddresses,
68
- CcAddresses: ccAddresses,
69
- BccAddresses: bccAddresses
70
- },
71
- Message,
72
- ReplyToAddresses: replyToAddresses
73
- };
78
+ const body = params.toString();
79
+ const host = `email.${region}.amazonaws.com`;
80
+ const path = '/';
74
81
  try {
75
- const command = new SendEmailCommand(params);
76
- const res = await sesClient.send(command);
77
- if (!res.MessageId) {
78
- throw new Error('sendEmail: SES response did not contain a MessageId');
82
+ // Sign the request
83
+ const { headers } = await signRequest('POST', host, path, body, {
84
+ accessKeyId,
85
+ secretAccessKey,
86
+ region
87
+ });
88
+ // Make the API request
89
+ const response = await fetch(`https://${host}${path}`, {
90
+ method: 'POST',
91
+ headers: {
92
+ ...headers,
93
+ Host: host,
94
+ 'Content-Length': body.length.toString()
95
+ },
96
+ body
97
+ });
98
+ const responseText = await response.text();
99
+ if (!response.ok) {
100
+ // Parse error response
101
+ let errorMessage = 'Unknown error';
102
+ let errorCode;
103
+ try {
104
+ const parser = new DOMParser();
105
+ const xmlDoc = parser.parseFromString(responseText, 'text/xml');
106
+ const errorNode = xmlDoc.querySelector('Error');
107
+ if (errorNode) {
108
+ errorCode = errorNode.querySelector('Code')?.textContent || undefined;
109
+ errorMessage = errorNode.querySelector('Message')?.textContent || errorMessage;
110
+ }
111
+ }
112
+ catch {
113
+ errorMessage = responseText || `HTTP ${response.status} ${response.statusText}`;
114
+ }
115
+ throw new Error(`sendEmail: failed to send email: ${JSON.stringify({ message: errorMessage, code: errorCode })}`);
116
+ }
117
+ // Parse success response for MessageId
118
+ try {
119
+ const parser = new DOMParser();
120
+ const xmlDoc = parser.parseFromString(responseText, 'text/xml');
121
+ const messageId = xmlDoc.querySelector('MessageId')?.textContent;
122
+ if (!messageId) {
123
+ throw new Error('sendEmail: SES response did not contain a MessageId');
124
+ }
125
+ return messageId;
126
+ }
127
+ catch (err) {
128
+ if (err instanceof Error && err.message.includes('MessageId')) {
129
+ throw err;
130
+ }
131
+ throw new Error(`sendEmail: failed to parse SES response: ${err instanceof Error ? err.message : String(err)}`);
79
132
  }
80
- return res.MessageId;
81
133
  }
82
134
  catch (err) {
83
- // Normalize error for callers
135
+ // Re-throw if already formatted
136
+ if (err instanceof Error && err.message.startsWith('sendEmail:')) {
137
+ throw err;
138
+ }
139
+ // Normalize other errors
84
140
  const message = err instanceof Error ? err.message : String(err);
85
141
  let code;
86
142
  if (err && typeof err === 'object') {
@@ -89,7 +145,6 @@ export async function sendEmail(options) {
89
145
  code = e['name'];
90
146
  }
91
147
  const details = { message, code };
92
- // Re-throw a clear error for the caller to handle
93
148
  throw new Error(`sendEmail: failed to send email: ${JSON.stringify(details)}`);
94
149
  }
95
150
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.7.2",
6
+ "version": "0.7.3",
7
7
  "license": "MIT",
8
8
  "repository": {
9
9
  "type": "git",
@@ -75,6 +75,7 @@
75
75
  "@changesets/cli": "^2.29.7",
76
76
  "@eslint/compat": "^1.4.1",
77
77
  "@eslint/js": "^9.39.1",
78
+ "@sveltejs/adapter-cloudflare": "^7.2.8",
78
79
  "@sveltejs/kit": "^2.48.4",
79
80
  "@sveltejs/package": "^2.5.4",
80
81
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
@@ -82,7 +83,6 @@
82
83
  "@tailwindcss/typography": "^0.5.19",
83
84
  "@tailwindcss/vite": "^4.1.17",
84
85
  "@types/node": "^22.19.1",
85
- "@types/pg": "^8.15.6",
86
86
  "@types/ramda": "^0.31.1",
87
87
  "@types/sorted-array-functions": "^1.3.3",
88
88
  "clsx": "^2.1.1",
@@ -110,7 +110,6 @@
110
110
  "svelte"
111
111
  ],
112
112
  "dependencies": {
113
- "@aws-sdk/client-ses": "^3.948.0",
114
113
  "@internationalized/date": "^3.10.0",
115
114
  "@lucide/svelte": "^0.553.0",
116
115
  "@sveltejs/adapter-auto": "^7.0.0",
@@ -119,7 +118,6 @@
119
118
  "devalue": "^5.5.0",
120
119
  "drizzle-orm": "^0.44.7",
121
120
  "formsnap": "^2.0.1",
122
- "pg": "^8.16.3",
123
121
  "ramda": "^0.31.3",
124
122
  "sorted-array-functions": "^1.3.0",
125
123
  "svelte-awesome-color-picker": "^4.1.0",