@webamoki/web-svelte 0.6.3 → 0.7.1

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,74 +1,3 @@
1
- <!-- <script lang="ts" generics="S extends type.Any<Record<string, unknown>>">
2
- import { toast } from 'svelte-sonner';
3
- import { dateTransport } from '../../utils/index.js';
4
- import { type } from 'arktype';
5
- import type { Snippet } from 'svelte';
6
- import type { Readable } from 'svelte/store';
7
- import { defaults, superForm, type SuperForm, type SuperValidated } from 'sveltekit-superforms';
8
- import { arktype, arktypeClient } from 'sveltekit-superforms/adapters';
9
- import type { SuperFormData, SuperFormErrors } from 'sveltekit-superforms/client';
10
-
11
- interface Props {
12
- validated?: SuperValidated<S['infer']> | S['infer'];
13
- schema: S;
14
- onSuccess?: (
15
- form: Readonly<SuperValidated<S['infer'], App.Superforms.Message, S['infer']>>
16
- ) => void;
17
- invalidateAll?: boolean;
18
- children: Snippet<
19
- [
20
- {
21
- form: SuperForm<S['infer'], App.Superforms.Message>;
22
- data: SuperFormData<S['infer']>;
23
- delayed: Readable<boolean>;
24
- errors: SuperFormErrors<S['infer']>;
25
- }
26
- ]
27
- >;
28
- // TODO: Enforce use of resolve
29
- action: string;
30
- actionName?: string;
31
- class?: string;
32
- }
33
-
34
- let {
35
- validated: _validated,
36
- schema,
37
- onSuccess,
38
- invalidateAll = false,
39
- children,
40
- action: _action,
41
- actionName,
42
- class: className
43
- }: Props = $props();
44
-
45
- let validated = _validated ?? defaults(arktype(schema));
46
- const form = superForm(validated, {
47
- validators: arktypeClient(schema),
48
- dataType: 'json',
49
- invalidateAll,
50
- transport: dateTransport,
51
- onUpdated({ form }) {
52
- const text = form.message?.text;
53
- if (text === undefined) return;
54
-
55
- if (form.message?.success) {
56
- toast.success(text);
57
- } else {
58
- toast.error(text);
59
- }
60
-
61
- if (form.valid) {
62
- onSuccess?.(form);
63
- }
64
- }
65
- });
66
- const { form: data, delayed, errors } = form;
67
- </script>
68
-
69
- <form class={className} action="{_action}?/{actionName}" method="POST" use:form.enhance>
70
- {@render children({ form, data, delayed, errors })}
71
- </form> -->
72
1
  <script lang="ts" generics="T extends Record<string, unknown>, M">
73
2
  import type { FsSuperForm } from 'formsnap';
74
3
  import type { Snippet } from 'svelte';
@@ -0,0 +1,209 @@
1
+ # Email Utility Package
2
+
3
+ This package provides utilities for sending emails using AWS Simple Email Service (SES).
4
+
5
+ ## Installation
6
+
7
+ The AWS SES SDK is already included as a dependency. You'll need to configure your AWS credentials and region.
8
+
9
+ ## Configuration
10
+
11
+ ### Environment Variables
12
+
13
+ The email utility requires the following environment variables:
14
+
15
+ - `AWS_REGION` - AWS region where SES is configured (e.g., `us-east-1`)
16
+ - `AWS_ACCESS_KEY_ID` - AWS access key ID
17
+ - `AWS_SECRET_ACCESS_KEY` - AWS secret access key
18
+
19
+ ### SES Setup
20
+
21
+ Before using this utility, ensure:
22
+
23
+ 1. Your AWS account has SES enabled in your chosen region
24
+ 2. Your sender email address(es) are verified in SES
25
+ 3. If in SES sandbox mode, recipient addresses must also be verified
26
+ 4. Your account has the necessary IAM permissions to send emails via SES
27
+
28
+ ## Usage
29
+
30
+ ### Basic Example
31
+
32
+ ```typescript
33
+ import { sendEmail } from '@webamoki/web-svelte/utils/email';
34
+
35
+ // Send a simple text email
36
+ const messageId = await sendEmail({
37
+ to: 'recipient@example.com',
38
+ subject: 'Hello from SES',
39
+ text: 'This is a plain text email.',
40
+ from: 'sender@example.com'
41
+ });
42
+
43
+ console.log('Email sent with message ID:', messageId);
44
+ ```
45
+
46
+ ### HTML Email
47
+
48
+ ```typescript
49
+ await sendEmail({
50
+ to: 'recipient@example.com',
51
+ subject: 'Welcome!',
52
+ html: '<h1>Welcome to our service!</h1><p>Thanks for signing up.</p>',
53
+ text: 'Welcome to our service! Thanks for signing up.', // Fallback for email clients that don't support HTML
54
+ from: 'noreply@example.com',
55
+ fromName: 'My Application'
56
+ });
57
+ ```
58
+
59
+ ### Multiple Recipients
60
+
61
+ ```typescript
62
+ await sendEmail({
63
+ to: ['user1@example.com', 'user2@example.com'],
64
+ cc: 'manager@example.com',
65
+ bcc: ['archive@example.com', 'backup@example.com'],
66
+ subject: 'Team Update',
67
+ text: 'This is a team-wide announcement.',
68
+ from: 'team@example.com'
69
+ });
70
+ ```
71
+
72
+ ### With Reply-To
73
+
74
+ ```typescript
75
+ await sendEmail({
76
+ to: 'customer@example.com',
77
+ subject: 'Your order confirmation',
78
+ html: '<p>Your order has been confirmed!</p>',
79
+ from: 'noreply@example.com',
80
+ fromName: 'Order System',
81
+ replyTo: 'support@example.com'
82
+ });
83
+ ```
84
+
85
+ ## API Reference
86
+
87
+ ### `sendEmail(options: SendEmailOptions): Promise<string>`
88
+
89
+ Sends an email via AWS SES.
90
+
91
+ **Returns:** Promise that resolves to the SES message ID (string) on success.
92
+
93
+ **Throws:** Error with descriptive message if:
94
+
95
+ - Validation fails (missing required fields, invalid recipients, etc.)
96
+ - AWS SES returns an error (credentials, permissions, service issues)
97
+ - SES response does not contain a MessageId (unlikely but handled explicitly)
98
+
99
+ ### `SendEmailOptions`
100
+
101
+ ```typescript
102
+ interface SendEmailOptions {
103
+ to: string | string[]; // Required: recipient email address(es)
104
+ cc?: string | string[]; // Optional: CC recipients
105
+ bcc?: string | string[]; // Optional: BCC recipients
106
+ subject: string; // Required: email subject
107
+ text?: string; // Optional: plain text body
108
+ html?: string; // Optional: HTML body
109
+ from: string; // Required: sender email
110
+ fromName?: string; // Optional: sender display name
111
+ replyTo?: string | string[]; // Optional: reply-to address(es)
112
+ }
113
+ ```
114
+
115
+ ## Error Handling
116
+
117
+ The `sendEmail` function validates inputs and will throw errors for:
118
+
119
+ - Missing required fields (`to`, `from`, `subject`, at least one of `text` or `html`)
120
+ - Empty recipient list (no valid email addresses after filtering)
121
+ - AWS SES errors (credentials, permissions, service issues)
122
+
123
+ **Note:** The function automatically filters out empty strings and whitespace-only values from recipient arrays (`to`, `cc`, `bcc`, `replyTo`). For example, `['', 'valid@example.com', ' ']` will be processed as `['valid@example.com']`.
124
+
125
+ Always wrap calls in try-catch:
126
+
127
+ ```typescript
128
+ try {
129
+ const messageId = await sendEmail({
130
+ to: 'user@example.com',
131
+ subject: 'Test',
132
+ text: 'Hello!',
133
+ from: 'sender@example.com'
134
+ });
135
+ console.log('Success:', messageId);
136
+ } catch (error) {
137
+ console.error('Failed to send email:', error.message);
138
+ // Handle error appropriately
139
+ }
140
+ ```
141
+
142
+ ## SvelteKit Integration
143
+
144
+ ### Server-Only Usage
145
+
146
+ ⚠️ **Important:** This utility should only be used in server-side code (e.g., `+page.server.ts`, `+server.ts`, or server-side functions) as it requires AWS credentials.
147
+
148
+ ### Example: Contact Form Handler
149
+
150
+ ```typescript
151
+ // src/routes/contact/+page.server.ts
152
+ import { sendEmail } from '@webamoki/web-svelte/utils/email';
153
+ import type { Actions } from './$types';
154
+
155
+ export const actions: Actions = {
156
+ default: async ({ request }) => {
157
+ const formData = await request.formData();
158
+ const email = formData.get('email') as string;
159
+ const message = formData.get('message') as string;
160
+
161
+ try {
162
+ await sendEmail({
163
+ to: 'admin@example.com',
164
+ subject: `Contact form submission from ${email}`,
165
+ text: message,
166
+ from: 'noreply@example.com',
167
+ replyTo: email
168
+ });
169
+
170
+ return { success: true };
171
+ } catch (error) {
172
+ console.error('Email error:', error);
173
+ return { success: false, error: 'Failed to send message' };
174
+ }
175
+ }
176
+ };
177
+ ```
178
+
179
+ ## Best Practices
180
+
181
+ 1. **Never expose AWS credentials in client-side code**
182
+ 2. **Use environment variables for configuration**
183
+ 3. **Verify sender addresses in SES before deploying**
184
+ 4. **Always provide both `text` and `html` versions when sending HTML emails**
185
+ 5. **Handle errors gracefully and log failures for monitoring**
186
+ 6. **Consider rate limits** - SES has sending limits based on your account status
187
+ 7. **Use BCC for bulk emails** to avoid exposing all recipients
188
+ 8. **Set appropriate Reply-To addresses** for better user experience
189
+
190
+ ## Troubleshooting
191
+
192
+ ### "Sender not verified" error
193
+
194
+ Verify your sender email address in the AWS SES console for your region.
195
+
196
+ ### "Access Denied" error
197
+
198
+ Ensure your AWS credentials have the `ses:SendEmail` permission.
199
+
200
+ ### Emails not arriving
201
+
202
+ - Check SES sending statistics in AWS console
203
+ - Verify you're not in sandbox mode (or that recipients are verified)
204
+ - Check spam folders
205
+ - Review bounce and complaint notifications in SES
206
+
207
+ ## License
208
+
209
+ MIT
@@ -0,0 +1 @@
1
+ export { sendEmail, type SendEmailOptions } from './ses.js';
@@ -0,0 +1 @@
1
+ export { sendEmail } from './ses.js';
@@ -0,0 +1,22 @@
1
+ export interface SendEmailOptions {
2
+ to: string | string[];
3
+ cc?: string | string[];
4
+ bcc?: string | string[];
5
+ subject: string;
6
+ text?: string;
7
+ html?: string;
8
+ from: string;
9
+ fromName?: string;
10
+ replyTo?: string | string[];
11
+ }
12
+ /**
13
+ * Send an email using AWS SES.
14
+ *
15
+ * Environment variables required:
16
+ * - AWS_REGION
17
+ * - AWS_ACCESS_KEY_ID
18
+ * - AWS_SECRET_ACCESS_KEY
19
+ *
20
+ * @returns messageId returned by SES
21
+ */
22
+ export declare function sendEmail(options: SendEmailOptions): Promise<string>;
@@ -0,0 +1,95 @@
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 });
4
+ /**
5
+ * Send an email using AWS SES.
6
+ *
7
+ * Environment variables required:
8
+ * - AWS_REGION
9
+ * - AWS_ACCESS_KEY_ID
10
+ * - AWS_SECRET_ACCESS_KEY
11
+ *
12
+ * @returns messageId returned by SES
13
+ */
14
+ export async function sendEmail(options) {
15
+ if (!options)
16
+ throw new Error('sendEmail: options is required');
17
+ const { to, cc, bcc, subject, text, html, from, fromName, replyTo } = options;
18
+ if (!subject) {
19
+ throw new Error('sendEmail: subject is required');
20
+ }
21
+ if (!text && !html) {
22
+ throw new Error('sendEmail: at least one of text or html body must be provided');
23
+ }
24
+ if (!from) {
25
+ throw new Error('sendEmail: sender `from` is required');
26
+ }
27
+ // Normalize and validate addresses: convert to array and filter out empty/whitespace strings
28
+ const normalizeAddresses = (addr) => {
29
+ if (addr === undefined)
30
+ return undefined;
31
+ const addresses = Array.isArray(addr) ? addr : [addr];
32
+ const filtered = addresses.filter((a) => a && a.trim() !== '');
33
+ return filtered.length > 0 ? filtered : undefined;
34
+ };
35
+ const toAddresses = normalizeAddresses(to);
36
+ const ccAddresses = normalizeAddresses(cc);
37
+ const bccAddresses = normalizeAddresses(bcc);
38
+ const replyToAddresses = normalizeAddresses(replyTo);
39
+ if (!toAddresses || toAddresses.length === 0) {
40
+ throw new Error('sendEmail: at least one valid recipient is required (to)');
41
+ }
42
+ // Format source with optional fromName
43
+ 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
+ };
52
+ if (html) {
53
+ Message.Body.Html = {
54
+ Charset: 'UTF-8',
55
+ Data: html
56
+ };
57
+ }
58
+ if (text) {
59
+ Message.Body.Text = {
60
+ Charset: 'UTF-8',
61
+ Data: text
62
+ };
63
+ }
64
+ const params = {
65
+ Source: source,
66
+ Destination: {
67
+ ToAddresses: toAddresses,
68
+ CcAddresses: ccAddresses,
69
+ BccAddresses: bccAddresses
70
+ },
71
+ Message,
72
+ ReplyToAddresses: replyToAddresses
73
+ };
74
+ 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');
79
+ }
80
+ return res.MessageId;
81
+ }
82
+ catch (err) {
83
+ // Normalize error for callers
84
+ const message = err instanceof Error ? err.message : String(err);
85
+ let code;
86
+ if (err && typeof err === 'object') {
87
+ const e = err;
88
+ if (typeof e['name'] === 'string')
89
+ code = e['name'];
90
+ }
91
+ const details = { message, code };
92
+ // Re-throw a clear error for the caller to handle
93
+ throw new Error(`sendEmail: failed to send email: ${JSON.stringify(details)}`);
94
+ }
95
+ }
@@ -1,7 +1,7 @@
1
1
  import { type } from 'arktype';
2
2
  import { type SuperValidated } from 'sveltekit-superforms';
3
3
  export * from './virtual-form.js';
4
- export declare function prepareForm<S extends type.Any<Record<string, unknown>>>(validated: SuperValidated<S['infer']> | S['infer'], schema: S, options?: Partial<{
4
+ export declare function prepareForm<S extends type.Any<Record<string, unknown>>>(schema: S, validated: SuperValidated<S['infer']> | S['infer'], options?: Partial<{
5
5
  invalidateAll: boolean;
6
6
  resetForm: boolean;
7
7
  onSuccess: (form: Readonly<SuperValidated<S['infer'], App.Superforms.Message, S['infer']>>) => void;
@@ -9,7 +9,7 @@ export declare function prepareForm<S extends type.Any<Record<string, unknown>>>
9
9
  }>): {
10
10
  form: import("sveltekit-superforms").SuperForm<S["infer"], any>;
11
11
  data: import("sveltekit-superforms/client").SuperFormData<S["infer"]>;
12
- delayed: import("svelte/store").Readable<boolean>;
12
+ isProcessing: import("svelte/store").Readable<boolean>;
13
13
  errors: import("sveltekit-superforms/client").SuperFormErrors<S["infer"]>;
14
14
  };
15
15
  export declare function prepareEmptyForm<S extends type.Any<Record<string, unknown>>>(schema: S, options?: Partial<{
@@ -20,6 +20,6 @@ export declare function prepareEmptyForm<S extends type.Any<Record<string, unkno
20
20
  }>): {
21
21
  form: import("sveltekit-superforms").SuperForm<S["infer"], any>;
22
22
  data: import("sveltekit-superforms/client").SuperFormData<S["infer"]>;
23
- delayed: import("svelte/store").Readable<boolean>;
23
+ isProcessing: import("svelte/store").Readable<boolean>;
24
24
  errors: import("sveltekit-superforms/client").SuperFormErrors<S["infer"]>;
25
25
  };
@@ -4,7 +4,7 @@ import { defaults, superForm } from 'sveltekit-superforms';
4
4
  import { arktype, arktypeClient } from 'sveltekit-superforms/adapters';
5
5
  import { dateTransport } from '../datetime/index.js';
6
6
  export * from './virtual-form.js';
7
- export function prepareForm(validated, schema, options) {
7
+ export function prepareForm(schema, validated, options) {
8
8
  const form = superForm(validated, {
9
9
  validators: arktypeClient(schema),
10
10
  dataType: 'json',
@@ -31,9 +31,9 @@ export function prepareForm(validated, schema, options) {
31
31
  toast.error(`${status} - ${message}`);
32
32
  }
33
33
  });
34
- const delayed = form.delayed;
34
+ const isProcessing = form.delayed;
35
35
  const errors = form.errors;
36
- return { form, data: form.form, delayed, errors };
36
+ return { form, data: form.form, isProcessing, errors };
37
37
  }
38
38
  export function prepareEmptyForm(schema, options) {
39
39
  const form = superForm(defaults(arktype(schema)), {
@@ -62,7 +62,7 @@ export function prepareEmptyForm(schema, options) {
62
62
  toast.error(`${status} - ${message}`);
63
63
  }
64
64
  });
65
- const delayed = form.delayed;
65
+ const isProcessing = form.delayed;
66
66
  const errors = form.errors;
67
- return { form, data: form.form, delayed, errors };
67
+ return { form, data: form.form, isProcessing, errors };
68
68
  }
@@ -10,5 +10,5 @@ export declare class VirtualForm<S extends type.Any<Record<string, unknown>>> {
10
10
  onError?: (message: App.Superforms.Message) => void;
11
11
  });
12
12
  submit(data: S['infer']): Promise<void>;
13
- get isLoading(): boolean;
13
+ get isProcessing(): boolean;
14
14
  }
@@ -5,7 +5,7 @@ import { parse, stringify } from 'devalue';
5
5
  import { dateTransport } from '../datetime/index.js';
6
6
  export class VirtualForm {
7
7
  // state storage
8
- #isLoading = false;
8
+ #isProcessing = false;
9
9
  #url = '';
10
10
  #schema;
11
11
  #transport;
@@ -70,7 +70,7 @@ export class VirtualForm {
70
70
  return result;
71
71
  }
72
72
  async submit(data) {
73
- this.#isLoading = true;
73
+ this.#isProcessing = true;
74
74
  this.#update();
75
75
  // Validate data against schema
76
76
  const validated = this.#schema(data);
@@ -99,7 +99,7 @@ export class VirtualForm {
99
99
  if (!res.ok || result.status === 400) {
100
100
  console.error('Request failed:', result);
101
101
  this.#onError?.(result);
102
- this.#isLoading = false;
102
+ this.#isProcessing = false;
103
103
  this.#update();
104
104
  return;
105
105
  }
@@ -124,11 +124,11 @@ export class VirtualForm {
124
124
  console.error(err);
125
125
  this.#onError?.({ text: 'Network error', data: err, success: false, showToast: false });
126
126
  }
127
- this.#isLoading = false;
127
+ this.#isProcessing = false;
128
128
  this.#update();
129
129
  }
130
- get isLoading() {
130
+ get isProcessing() {
131
131
  this.#subscribe();
132
- return this.#isLoading;
132
+ return this.#isProcessing;
133
133
  }
134
134
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.3",
6
+ "version": "0.7.1",
7
7
  "license": "MIT",
8
8
  "files": [
9
9
  "dist",
@@ -43,6 +43,10 @@
43
43
  "types": "./dist/utils/form/index.d.ts",
44
44
  "import": "./dist/utils/form/index.js"
45
45
  },
46
+ "./utils/email": {
47
+ "types": "./dist/utils/email/index.d.ts",
48
+ "import": "./dist/utils/email/index.js"
49
+ },
46
50
  "./server/form-handler": {
47
51
  "types": "./dist/server/form-handler.d.ts",
48
52
  "import": "./dist/server/form-handler.js"
@@ -102,6 +106,7 @@
102
106
  "svelte"
103
107
  ],
104
108
  "dependencies": {
109
+ "@aws-sdk/client-ses": "^3.948.0",
105
110
  "@internationalized/date": "^3.10.0",
106
111
  "@lucide/svelte": "^0.553.0",
107
112
  "@sveltejs/adapter-auto": "^7.0.0",