@ewyn/client 0.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.
package/dist/index.js ADDED
@@ -0,0 +1,211 @@
1
+ import { MailerApiError } from './errors.js';
2
+ /**
3
+ * Mailer SDK Client
4
+ *
5
+ * @example Basic usage without template config
6
+ * ```ts
7
+ * const client = new Mailer({
8
+ * workspaceId: 'your-workspace-id',
9
+ * apiKey: 'your-api-key',
10
+ * });
11
+ *
12
+ * await client.send({
13
+ * to: 'user@example.com',
14
+ * templateId: 'version-uuid',
15
+ * variables: { firstName: 'John' }
16
+ * });
17
+ * ```
18
+ *
19
+ * @example Type-safe usage with template config
20
+ * ```ts
21
+ * const config = {
22
+ * welcome: {
23
+ * id: 'version-uuid',
24
+ * name: 'Welcome Email',
25
+ * version: 1,
26
+ * vars: {
27
+ * firstName: { required: true },
28
+ * plan: { required: false }
29
+ * }
30
+ * }
31
+ * } as const;
32
+ *
33
+ * const client = new Mailer({
34
+ * workspaceId: 'your-workspace-id',
35
+ * apiKey: 'your-api-key',
36
+ * templates: config
37
+ * });
38
+ *
39
+ * // Type-safe sending
40
+ * await client.send({
41
+ * to: 'user@example.com',
42
+ * template: 'welcome', // Autocomplete available
43
+ * variables: {
44
+ * firstName: 'John' // TypeScript enforces required vars
45
+ * }
46
+ * });
47
+ * ```
48
+ */
49
+ export class Mailer {
50
+ workspaceId;
51
+ apiKey;
52
+ baseUrl;
53
+ templates;
54
+ maxRetries;
55
+ timeout;
56
+ constructor(options) {
57
+ this.workspaceId = options.workspaceId;
58
+ this.apiKey = options.apiKey;
59
+ this.baseUrl = options.baseUrl || 'https://www.ewyn.ai/api/v1';
60
+ this.templates = options.templates;
61
+ this.maxRetries = options.maxRetries ?? 3;
62
+ this.timeout = options.timeout ?? 30000;
63
+ }
64
+ /**
65
+ * Send an email using a template
66
+ *
67
+ * @param options - Email sending options
68
+ * @returns Promise resolving to the send response
69
+ * @throws {MailerApiError} If the API request fails
70
+ *
71
+ * @example With template ID
72
+ * ```ts
73
+ * await client.send({
74
+ * to: 'user@example.com',
75
+ * templateId: 'version-uuid',
76
+ * variables: { firstName: 'John' }
77
+ * });
78
+ * ```
79
+ *
80
+ * @example With template name (requires config)
81
+ * ```ts
82
+ * await client.send({
83
+ * to: 'user@example.com',
84
+ * template: 'welcome',
85
+ * version: 2, // Optional: defaults to latest
86
+ * variables: { firstName: 'John' }
87
+ * });
88
+ * ```
89
+ */
90
+ async send(options) {
91
+ // Validate and resolve template
92
+ const templateId = await this.resolveTemplateId(options);
93
+ // Validate variables if template config is provided
94
+ if (this.templates && options.template) {
95
+ this.validateVariables(options.template, options.variables || {});
96
+ }
97
+ // Build request body
98
+ const body = {
99
+ to: options.to,
100
+ templateId,
101
+ };
102
+ if (options.variables) {
103
+ body.variables = options.variables;
104
+ }
105
+ if (options.metadata) {
106
+ body.metadata = options.metadata;
107
+ }
108
+ if (options.idempotencyKey) {
109
+ body.idempotencyKey = options.idempotencyKey;
110
+ }
111
+ // Add version if specified
112
+ if ('version' in options && options.version !== undefined) {
113
+ body.version = options.version;
114
+ }
115
+ // Make request with retries
116
+ return this.requestWithRetry(`/workspaces/${this.workspaceId}/send`, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ 'Authorization': `Bearer ${this.apiKey}`,
121
+ },
122
+ body: JSON.stringify(body),
123
+ });
124
+ }
125
+ /**
126
+ * Resolve template ID from either templateId or template name
127
+ */
128
+ async resolveTemplateId(options) {
129
+ if (options.templateId) {
130
+ return options.templateId;
131
+ }
132
+ if (options.template) {
133
+ if (!this.templates) {
134
+ throw new Error('Template name provided but no template config was provided to the client');
135
+ }
136
+ const template = this.templates[options.template];
137
+ if (!template) {
138
+ throw new Error(`Template "${options.template}" not found in template config`);
139
+ }
140
+ return template.id;
141
+ }
142
+ throw new Error('Either templateId or template must be provided');
143
+ }
144
+ /**
145
+ * Validate that all required variables are provided
146
+ */
147
+ validateVariables(templateName, variables) {
148
+ if (!this.templates) {
149
+ return;
150
+ }
151
+ const template = this.templates[templateName];
152
+ if (!template) {
153
+ return;
154
+ }
155
+ const missing = [];
156
+ for (const [varName, varDef] of Object.entries(template.vars)) {
157
+ if (varDef?.required && !(varName in variables)) {
158
+ missing.push(varName);
159
+ }
160
+ }
161
+ if (missing.length > 0) {
162
+ throw new Error(`Missing required variables for template "${templateName}": ${missing.join(', ')}`);
163
+ }
164
+ }
165
+ /**
166
+ * Make HTTP request with retry logic
167
+ */
168
+ async requestWithRetry(path, init, attempt = 1) {
169
+ const url = `${this.baseUrl}${path}`;
170
+ const controller = new AbortController();
171
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
172
+ try {
173
+ const response = await fetch(url, {
174
+ ...init,
175
+ signal: controller.signal,
176
+ });
177
+ clearTimeout(timeoutId);
178
+ const data = await response.json().catch(() => ({}));
179
+ if (!response.ok) {
180
+ const error = new MailerApiError(response.status, data.code, data.details || data, data.error || data.message);
181
+ // Retry on retryable errors
182
+ if (error.isRetryable() && attempt < this.maxRetries) {
183
+ // Exponential backoff: 1s, 2s, 4s
184
+ const delay = Math.pow(2, attempt - 1) * 1000;
185
+ await new Promise((resolve) => setTimeout(resolve, delay));
186
+ return this.requestWithRetry(path, init, attempt + 1);
187
+ }
188
+ throw error;
189
+ }
190
+ return data;
191
+ }
192
+ catch (error) {
193
+ clearTimeout(timeoutId);
194
+ if (error instanceof MailerApiError) {
195
+ throw error;
196
+ }
197
+ // Handle network errors or timeouts
198
+ if (error instanceof Error && error.name === 'AbortError') {
199
+ throw new MailerApiError(408, 'TIMEOUT', undefined, `Request timed out after ${this.timeout}ms`);
200
+ }
201
+ // Retry on network errors if we have attempts left
202
+ if (attempt < this.maxRetries) {
203
+ const delay = Math.pow(2, attempt - 1) * 1000;
204
+ await new Promise((resolve) => setTimeout(resolve, delay));
205
+ return this.requestWithRetry(path, init, attempt + 1);
206
+ }
207
+ throw new MailerApiError(500, 'NETWORK_ERROR', undefined, error instanceof Error ? error.message : 'Unknown network error');
208
+ }
209
+ }
210
+ }
211
+ export { MailerApiError } from './errors.js';
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Template variable definition
3
+ */
4
+ export interface TemplateVariable {
5
+ required: boolean;
6
+ }
7
+ /**
8
+ * Template configuration entry
9
+ */
10
+ export interface TemplateConfigEntry {
11
+ /** Template version ID (UUID) - used for sending */
12
+ id: string;
13
+ /** Human-readable template name */
14
+ name: string;
15
+ /** Major version number */
16
+ version: number;
17
+ /** Variable definitions */
18
+ vars: Record<string, TemplateVariable>;
19
+ }
20
+ /**
21
+ * Template configuration object
22
+ * Maps template names to their configuration
23
+ */
24
+ export type TemplateConfig = Record<string, TemplateConfigEntry>;
25
+ /**
26
+ * Extract required variable names from a template config entry
27
+ */
28
+ type RequiredVars<T extends TemplateConfigEntry> = {
29
+ [K in keyof T['vars']]: T['vars'][K] extends {
30
+ required: true;
31
+ } ? K : never;
32
+ }[keyof T['vars']];
33
+ /**
34
+ * Extract optional variable names from a template config entry
35
+ */
36
+ type OptionalVars<T extends TemplateConfigEntry> = {
37
+ [K in keyof T['vars']]: T['vars'][K] extends {
38
+ required: false;
39
+ } ? K : never;
40
+ }[keyof T['vars']];
41
+ /**
42
+ * Build a variables object type from template config
43
+ */
44
+ type VariablesFromConfig<T extends TemplateConfigEntry> = RequiredVars<T> extends never ? {
45
+ [K in OptionalVars<T>]?: string;
46
+ } : {
47
+ [K in RequiredVars<T>]: string;
48
+ } & {
49
+ [K in OptionalVars<T>]?: string;
50
+ };
51
+ /**
52
+ * Options for sending an email without template config
53
+ */
54
+ export interface SendEmailOptionsBase {
55
+ /** Recipient email address */
56
+ to: string;
57
+ /** Template version ID (UUID) - required if template not provided */
58
+ templateId?: string;
59
+ /** Template name (requires template config to be provided) */
60
+ template?: string;
61
+ /** Major version number (use with template name) */
62
+ version?: number;
63
+ /** Variables to substitute in the template */
64
+ variables?: Record<string, string>;
65
+ /** Additional metadata to attach to the message */
66
+ metadata?: Record<string, unknown>;
67
+ /** Idempotency key to prevent duplicate sends */
68
+ idempotencyKey?: string;
69
+ }
70
+ /**
71
+ * Type-safe options when using template config
72
+ */
73
+ export type SendEmailOptionsTyped<TConfig extends TemplateConfig, TName extends keyof TConfig = keyof TConfig> = {
74
+ /** Recipient email address */
75
+ to: string;
76
+ /** Template name from config */
77
+ template: TName;
78
+ /** Major version number (optional, defaults to latest) */
79
+ version?: TConfig[TName]['version'];
80
+ /** Type-safe variables based on template config */
81
+ variables: VariablesFromConfig<TConfig[TName]>;
82
+ /** Additional metadata to attach to the message */
83
+ metadata?: Record<string, unknown>;
84
+ /** Idempotency key to prevent duplicate sends */
85
+ idempotencyKey?: string;
86
+ };
87
+ /**
88
+ * Options for sending an email (generic version)
89
+ */
90
+ export type SendEmailOptions = SendEmailOptionsBase;
91
+ /**
92
+ * Response from sending an email
93
+ */
94
+ export interface SendEmailResponse {
95
+ messageId: string;
96
+ status: 'queued';
97
+ queuedAt: string;
98
+ }
99
+ /**
100
+ * Configuration options for the Mailer client (base)
101
+ */
102
+ export interface MailerOptionsBase {
103
+ /** Workspace ID (UUID) */
104
+ workspaceId: string;
105
+ /** API key secret */
106
+ apiKey: string;
107
+ /** Base URL for the API (defaults to production) */
108
+ baseUrl?: string;
109
+ /** Maximum number of retries for retryable errors (default: 3) */
110
+ maxRetries?: number;
111
+ /** Request timeout in milliseconds (default: 30000) */
112
+ timeout?: number;
113
+ }
114
+ /**
115
+ * Configuration options with typed template config
116
+ */
117
+ export type MailerOptionsTyped<TConfig extends TemplateConfig> = MailerOptionsBase & {
118
+ /** Template configuration for type-safe sending */
119
+ templates: TConfig;
120
+ };
121
+ /**
122
+ * Configuration options for the Mailer client (generic version)
123
+ */
124
+ export type MailerOptions = MailerOptionsBase & {
125
+ /** Template configuration for name-based sending and validation */
126
+ templates?: TemplateConfig;
127
+ };
128
+ export {};
129
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ewyn/client",
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript SDK for Ewyn email service with full type safety",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsc --watch",
20
+ "lint": "eslint . --max-warnings 0",
21
+ "test": "vitest run --typecheck",
22
+ "test:watch": "vitest"
23
+ },
24
+ "keywords": [
25
+ "ewyn",
26
+ "email",
27
+ "sdk"
28
+ ],
29
+ "author": "",
30
+ "license": "MIT",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "prepublishOnly": "pnpm build",
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "dependencies": {},
39
+ "devDependencies": {
40
+ "@types/node": "^20.11.0",
41
+ "@workspace/eslint-config": "workspace:*",
42
+ "@workspace/typescript-config": "workspace:*",
43
+ "typescript": "^5.7.3",
44
+ "vitest": "^1.2.0"
45
+ }
46
+ }