groundmail 0.16.0 → 0.17.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 CHANGED
@@ -25,6 +25,27 @@ const email = await tx.emails.send({
25
25
  console.log(email.id, email.status)
26
26
  ```
27
27
 
28
+ ## Testing
29
+
30
+ Use a test key (`gm_test_…`, create one with `tx.apiKeys.create({ test: true })` or in the
31
+ dashboard) to run sends through validation, templates and webhooks without delivering anything.
32
+ Simulator addresses produce a specific outcome with any key and never leave Groundmail:
33
+
34
+ ```ts
35
+ import { simulatorAddress } from 'groundmail'
36
+
37
+ await tx.emails.send({
38
+ from: 'ops@yourdomain.com',
39
+ to: simulatorAddress('bounce', 'signup-42'), // bounce+signup-42@simulator.groundmail.dev
40
+ subject: 'Bounce handling test',
41
+ html: '<p>Simulated.</p>',
42
+ })
43
+ // status `bounced`, then an `email.bounced` webhook with `data.test: true`
44
+ ```
45
+
46
+ Outcomes: `delivered`, `bounce`, `block`, `complaint`, `suppressed`, `delayed`. Details:
47
+ https://docs.groundmail.dev/testing
48
+
28
49
  ## Adapters
29
50
 
30
51
  Optional entry points (install peer deps as needed):
@@ -41,6 +62,7 @@ Optional entry points (install peer deps as needed):
41
62
 
42
63
  - Quickstart: https://docs.groundmail.dev/quickstart
43
64
  - Node / Nodemailer: https://docs.groundmail.dev/frameworks/node
65
+ - Testing (test keys, simulator): https://docs.groundmail.dev/testing
44
66
  - API reference: https://docs.groundmail.dev/api/emails
45
67
  - Product: https://groundmail.dev
46
68
 
package/dist/authjs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Groundmail } from './client-DXYsWgOT.js';
1
+ import { G as Groundmail } from './client-DLz4rpD9.js';
2
2
 
3
3
  /** Subset of Auth.js theme used for the default magic-link email. */
4
4
  type AuthjsTheme = {
package/dist/authjs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-JYLRUBEJ.js";
4
+ } from "./chunk-MJKAVFFN.js";
5
5
 
6
6
  // src/authjs.ts
7
7
  function escapeHtml(value) {
@@ -1,4 +1,4 @@
1
- import { S as SendEmailResult, G as Groundmail } from './client-DXYsWgOT.js';
1
+ import { S as SendEmailResult, G as Groundmail } from './client-DLz4rpD9.js';
2
2
 
3
3
  type BetterAuthEmailUser = {
4
4
  email: string;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-JYLRUBEJ.js";
4
+ } from "./chunk-MJKAVFFN.js";
5
5
 
6
6
  // src/better-auth.ts
7
7
  function linkHtml(label, url) {
@@ -1008,6 +1008,7 @@ var EmailsResource = class {
1008
1008
  const search = new URLSearchParams();
1009
1009
  if (params.limit != null) search.set("limit", String(params.limit));
1010
1010
  if (params.cursor) search.set("cursor", params.cursor);
1011
+ if (params.test != null) search.set("test", String(params.test));
1011
1012
  const query = search.toString();
1012
1013
  return this.client.request(`/emails${query ? `?${query}` : ""}`, {
1013
1014
  method: "GET"
@@ -1192,7 +1193,10 @@ var KeysResource = class {
1192
1193
  method: "POST",
1193
1194
  body: JSON.stringify({
1194
1195
  ...params.name ? { name: params.name } : {},
1195
- ...params.scope ? { scope: params.scope } : {}
1196
+ ...params.scope && !params.scopes?.length ? { scope: params.scope } : {},
1197
+ ...params.scopes?.length ? { scopes: params.scopes } : {},
1198
+ ...params.allowedDomains?.length ? { allowed_domains: params.allowedDomains } : {},
1199
+ ...params.test ? { test: true } : {}
1196
1200
  })
1197
1201
  });
1198
1202
  }
package/dist/clerk.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Groundmail, S as SendEmailResult } from './client-DXYsWgOT.js';
1
+ import { G as Groundmail, S as SendEmailResult } from './client-DLz4rpD9.js';
2
2
 
3
3
  /**
4
4
  * Subset of Clerk `email.created` webhook `data` used for delivery.
package/dist/clerk.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-JYLRUBEJ.js";
4
+ } from "./chunk-MJKAVFFN.js";
5
5
 
6
6
  // src/clerk.ts
7
7
  function resolveApiKey(options) {
@@ -78,6 +78,8 @@ type SendEmailResult = {
78
78
  status: string;
79
79
  /** Present when some recipients were held back and the email went to the rest. */
80
80
  held_back?: HeldBackRecipient[];
81
+ /** `true` on a test send (a `gm_test_…` key or only simulator addresses): nothing was delivered. */
82
+ test?: true;
81
83
  };
82
84
  /** `details` on `POST /emails` 422 when Groundmail validation held back every `to` recipient. */
83
85
  type HeldBackErrorDetails = {
@@ -111,6 +113,7 @@ type BatchSendEmailResult = {
111
113
  id?: string;
112
114
  status?: string;
113
115
  held_back?: HeldBackRecipient[];
116
+ test?: true;
114
117
  error?: {
115
118
  message: string;
116
119
  code?: string;
@@ -148,6 +151,11 @@ type EmailListItem = {
148
151
  bounce_type: BounceType | null;
149
152
  /** Recipients Groundmail validation held back before sending, with why. Empty when none were. */
150
153
  held_back: HeldBackRecipient[];
154
+ /**
155
+ * `true` when the email never reached the mail server: sent with a test key or only to
156
+ * simulator addresses. Its status and events are simulated.
157
+ */
158
+ test: boolean;
151
159
  tags: unknown;
152
160
  scheduled_at: string | null;
153
161
  cancelled_at: string | null;
@@ -207,6 +215,8 @@ type ListReceivedEmailsResult = {
207
215
  type ListEmailsParams = {
208
216
  limit?: number;
209
217
  cursor?: string;
218
+ /** `true`: only test emails; `false`: only live ones. Omit for both. */
219
+ test?: boolean;
210
220
  };
211
221
  type ListEmailsResult = {
212
222
  emails: EmailListItem[];
@@ -261,6 +271,12 @@ type ApiKey = {
261
271
  id: string;
262
272
  name: string;
263
273
  scope: ApiKeyScope;
274
+ /** Fine-grained scopes (`emails:send`, `domains:read`, …). */
275
+ scopes?: string[];
276
+ /** Domains the key may send from; empty means any verified domain. */
277
+ allowed_domains?: string[];
278
+ /** A test key (`gm_test_…`): its sends are simulated and never delivered. */
279
+ test?: boolean;
264
280
  prefix: string;
265
281
  created_at: string;
266
282
  last_used_at: string | null;
@@ -277,11 +293,24 @@ type ListApiKeysResult = {
277
293
  type CreateApiKeyParams = {
278
294
  name?: string;
279
295
  scope?: ApiKeyScope;
296
+ /** Fine-grained scopes, e.g. `['emails:send']`. Takes precedence over `scope`. */
297
+ scopes?: string[];
298
+ /** Verified domains the key may send from (needs `emails:send`, not `full`). */
299
+ allowedDomains?: string[];
300
+ /**
301
+ * Create a test key (`gm_test_…`): same scopes and checks as a live key, but sends are
302
+ * never delivered. They get simulated events and webhooks and count toward nothing.
303
+ */
304
+ test?: boolean;
280
305
  };
281
306
  type CreateApiKeyResult = {
282
307
  id: string;
283
308
  name: string;
284
309
  scope: ApiKeyScope;
310
+ scopes?: string[];
311
+ allowed_domains?: string[];
312
+ /** A test key (`gm_test_…`). */
313
+ test?: boolean;
285
314
  prefix: string;
286
315
  token: string;
287
316
  created_at: string;
@@ -326,6 +355,8 @@ type EmailWebhookEventPayload = {
326
355
  bounce_reason?: string | null;
327
356
  /** Set on `email.bounced` and `email.failed` when the email bounced: `hard` or `block`. */
328
357
  bounce_type?: BounceType | null;
358
+ /** `true` on simulated events (test keys, simulator addresses, `POST /emails/test`). Absent on real mail. */
359
+ test?: true;
329
360
  };
330
361
  };
331
362
  /** `contact.unsubscribed` webhook payload (`scope` distinguishes global vs category opt-out). */
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as ApiErrorBody, D as DnsRecord } from './client-DXYsWgOT.js';
2
- export { a as ApiErrorCode, b as ApiKey, c as ApiKeyScope, d as ApplyDnsParams, e as ApplyDnsRecordResult, f as ApplyDnsResult, g as Asset, h as Automation, i as AutomationResult, j as AutomationRun, k as AutomationRunResponse, l as AutomationRunStatus, m as AutomationRunStep, n as AutomationRunStepStatus, o as AutomationStatus, p as AutomationStep, q as AutomationTrigger, B as BatchSendEmailResult, r as BounceType, C as CategoryUnsubscribedErrorDetails, s as ConnectTwilioParams, t as ContactWebhookEventPayload, u as CreateApiKeyParams, v as CreateApiKeyResult, w as CreateAutomationParams, x as CreateDomainParams, y as CreateDomainResult, z as CreateMigrationParams, E as CreateSuppressionParams, F as CreateTemplateParams, H as CreateWebhookParams, I as DEFAULT_API_BASE_URL, J as DailyRampLimitErrorDetails, K as DeliverabilityBucket, L as DeliverabilityDomainInsight, M as DeliverabilityInsightsResult, N as DeliverabilityMailServerRelay, O as DeliverabilityWindow, P as DnsProviderName, Q as Domain, R as DomainBimiAnalysis, T as DomainDetailResult, U as DomainDmarcAnalysis, V as DomainDnsAnalysis, W as DomainStatus, X as DomainTlsMode, Y as DomainTrackingDns, Z as DomainTrackingStatus, _ as EmailListItem, $ as EmailWebhookEventPayload, a0 as EmailWebhookEventType, a1 as EnrollmentSkip, a2 as EventRecord, G as Groundmail, a3 as GroundmailOptions, a4 as HeldBackErrorDetails, a5 as HeldBackReason, a6 as HeldBackRecipient, a7 as ListApiKeysParams, a8 as ListApiKeysResult, a9 as ListAutomationRunsParams, aa as ListAutomationRunsResult, ab as ListAutomationsResult, ac as ListDomainsParams, ad as ListDomainsResult, ae as ListEmailsParams, af as ListEmailsResult, ag as ListReceivedEmailsParams, ah as ListReceivedEmailsResult, ai as ListSuppressionsParams, aj as ListSuppressionsResult, ak as ListTemplatesParams, al as ListTemplatesResult, am as MailServerSigningStatus, an as Migration, ao as MigrationDnsChange, ap as MigrationDnsPlan, aq as MigrationDomainItem, ar as MigrationItemStatus, as as MigrationProvider, at as MigrationResource, au as MigrationSummary, av as MigrationTemplateItem, aw as MigrationTotals, ax as MigrationWebhookItem, ay as ReceivedEmail, az as ReceivedEmailAttachment, aA as ReceivedEmailAttachmentMeta, aB as RemoteApplyDnsCredentials, aC as RunMigrationOptions, aD as SendEmailParams, S as SendEmailResult, aE as SuppressedErrorDetails, aF as Suppression, aG as Template, aH as TemplateFormat, aI as TemplateLintWarning, aJ as TemplateStarter, aK as TemplateStatus, aL as TemplateVersion, aM as TestEmailWebhookParams, aN as TestEmailWebhookResult, aO as TriggerEventParams, aP as TriggerEventResult, aQ as TwilioIntegrationResponse, aR as TwilioIntegrationStatus, aS as UpdateAutomationParams, aT as UpdateDomainParams, aU as UpdateEmailParams, aV as UpdateTemplateParams, aW as UpdateWebhookParams, aX as ValidateEmailResult, aY as ValidationReason, aZ as ValidationVerdict, a_ as VerifyDomainResult, a$ as WebhookEndpoint, b0 as WebhookEndpointWithSecret, b1 as WebhookEventType, b2 as mergeMigrations } from './client-DXYsWgOT.js';
1
+ import { A as ApiErrorBody, D as DnsRecord } from './client-DLz4rpD9.js';
2
+ export { a as ApiErrorCode, b as ApiKey, c as ApiKeyScope, d as ApplyDnsParams, e as ApplyDnsRecordResult, f as ApplyDnsResult, g as Asset, h as Automation, i as AutomationResult, j as AutomationRun, k as AutomationRunResponse, l as AutomationRunStatus, m as AutomationRunStep, n as AutomationRunStepStatus, o as AutomationStatus, p as AutomationStep, q as AutomationTrigger, B as BatchSendEmailResult, r as BounceType, C as CategoryUnsubscribedErrorDetails, s as ConnectTwilioParams, t as ContactWebhookEventPayload, u as CreateApiKeyParams, v as CreateApiKeyResult, w as CreateAutomationParams, x as CreateDomainParams, y as CreateDomainResult, z as CreateMigrationParams, E as CreateSuppressionParams, F as CreateTemplateParams, H as CreateWebhookParams, I as DEFAULT_API_BASE_URL, J as DailyRampLimitErrorDetails, K as DeliverabilityBucket, L as DeliverabilityDomainInsight, M as DeliverabilityInsightsResult, N as DeliverabilityMailServerRelay, O as DeliverabilityWindow, P as DnsProviderName, Q as Domain, R as DomainBimiAnalysis, T as DomainDetailResult, U as DomainDmarcAnalysis, V as DomainDnsAnalysis, W as DomainStatus, X as DomainTlsMode, Y as DomainTrackingDns, Z as DomainTrackingStatus, _ as EmailListItem, $ as EmailWebhookEventPayload, a0 as EmailWebhookEventType, a1 as EnrollmentSkip, a2 as EventRecord, G as Groundmail, a3 as GroundmailOptions, a4 as HeldBackErrorDetails, a5 as HeldBackReason, a6 as HeldBackRecipient, a7 as ListApiKeysParams, a8 as ListApiKeysResult, a9 as ListAutomationRunsParams, aa as ListAutomationRunsResult, ab as ListAutomationsResult, ac as ListDomainsParams, ad as ListDomainsResult, ae as ListEmailsParams, af as ListEmailsResult, ag as ListReceivedEmailsParams, ah as ListReceivedEmailsResult, ai as ListSuppressionsParams, aj as ListSuppressionsResult, ak as ListTemplatesParams, al as ListTemplatesResult, am as MailServerSigningStatus, an as Migration, ao as MigrationDnsChange, ap as MigrationDnsPlan, aq as MigrationDomainItem, ar as MigrationItemStatus, as as MigrationProvider, at as MigrationResource, au as MigrationSummary, av as MigrationTemplateItem, aw as MigrationTotals, ax as MigrationWebhookItem, ay as ReceivedEmail, az as ReceivedEmailAttachment, aA as ReceivedEmailAttachmentMeta, aB as RemoteApplyDnsCredentials, aC as RunMigrationOptions, aD as SendEmailParams, S as SendEmailResult, aE as SuppressedErrorDetails, aF as Suppression, aG as Template, aH as TemplateFormat, aI as TemplateLintWarning, aJ as TemplateStarter, aK as TemplateStatus, aL as TemplateVersion, aM as TestEmailWebhookParams, aN as TestEmailWebhookResult, aO as TriggerEventParams, aP as TriggerEventResult, aQ as TwilioIntegrationResponse, aR as TwilioIntegrationStatus, aS as UpdateAutomationParams, aT as UpdateDomainParams, aU as UpdateEmailParams, aV as UpdateTemplateParams, aW as UpdateWebhookParams, aX as ValidateEmailResult, aY as ValidationReason, aZ as ValidationVerdict, a_ as VerifyDomainResult, a$ as WebhookEndpoint, b0 as WebhookEndpointWithSecret, b1 as WebhookEventType, b2 as mergeMigrations } from './client-DLz4rpD9.js';
3
3
 
4
4
  type GroundmailErrorOptions = {
5
5
  code?: string;
@@ -43,6 +43,40 @@ declare function buildSignatureHeader(secret: string, payload: string, timestamp
43
43
  */
44
44
  declare function verifyWebhookSignature(secret: string, payload: string, header: string | null | undefined, toleranceSec?: number): boolean;
45
45
 
46
+ /**
47
+ * Test mode helpers.
48
+ *
49
+ * Simulator addresses at `simulator.groundmail.dev` produce one outcome each, with
50
+ * the same status and webhooks as real mail, and never leave Groundmail. They work
51
+ * with live and test keys. Plus addressing tells sends apart:
52
+ *
53
+ * ```ts
54
+ * import { simulatorAddress } from 'groundmail'
55
+ * await groundmail.emails.send({ from, to: simulatorAddress('bounce', 'signup-42'), subject, html })
56
+ * // → bounce+signup-42@simulator.groundmail.dev: status `bounced`, `email.bounced` webhook
57
+ * ```
58
+ *
59
+ * Docs: https://docs.groundmail.dev/testing
60
+ */
61
+ declare const SIMULATOR_DOMAIN = "simulator.groundmail.dev";
62
+ declare const SIMULATOR_OUTCOMES: readonly ["delivered", "bounce", "block", "complaint", "suppressed", "delayed"];
63
+ /**
64
+ * - `delivered`: `email.delivered`
65
+ * - `bounce`: hard bounce, `email.bounced` with `bounce_type: "hard"`
66
+ * - `block`: block bounce, `email.bounced` with `bounce_type: "block"`
67
+ * - `complaint`: `email.delivered`, then `email.complained`
68
+ * - `suppressed`: refused like a suppressed recipient (422), `email.suppressed`
69
+ * - `delayed`: `email.delivery_delayed`
70
+ */
71
+ type SimulatorOutcome = (typeof SIMULATOR_OUTCOMES)[number];
72
+ /** `simulatorAddress('bounce')` → `bounce@simulator.groundmail.dev`; a tag adds `+tag`. */
73
+ declare function simulatorAddress(outcome: SimulatorOutcome, tag?: string): string;
74
+ /** True for any address at the simulator domain. */
75
+ declare function isSimulatorAddress(address: string): boolean;
76
+ /** Test keys start with `gm_test_`. */
77
+ declare const TEST_API_KEY_PREFIX = "gm_test_";
78
+ declare function isTestApiKey(key: string | null | undefined): boolean;
79
+
46
80
  /** Merge `include:host` into an existing SPF record (or create a minimal one). */
47
81
  declare function mergeSpfInclude(existing: string | null | undefined, includeHost: string): string;
48
82
 
@@ -75,4 +109,4 @@ declare function applyDnsRecords(input: {
75
109
  fetchImpl?: typeof fetch;
76
110
  }): Promise<ApplyDnsResult>;
77
111
 
78
- export { DnsRecord, GroundmailError, type GroundmailErrorOptions, applyDnsRecords, buildSignatureHeader, mergeSpfInclude, signWebhookPayload, verifyWebhookSignature };
112
+ export { DnsRecord, GroundmailError, type GroundmailErrorOptions, SIMULATOR_DOMAIN, SIMULATOR_OUTCOMES, type SimulatorOutcome, TEST_API_KEY_PREFIX, applyDnsRecords, buildSignatureHeader, isSimulatorAddress, isTestApiKey, mergeSpfInclude, signWebhookPayload, simulatorAddress, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -8,15 +8,41 @@ import {
8
8
  mergeSpfInclude,
9
9
  signWebhookPayload,
10
10
  verifyWebhookSignature
11
- } from "./chunk-JYLRUBEJ.js";
11
+ } from "./chunk-MJKAVFFN.js";
12
+
13
+ // src/simulator.ts
14
+ var SIMULATOR_DOMAIN = "simulator.groundmail.dev";
15
+ var SIMULATOR_OUTCOMES = ["delivered", "bounce", "block", "complaint", "suppressed", "delayed"];
16
+ function simulatorAddress(outcome, tag) {
17
+ if (!SIMULATOR_OUTCOMES.includes(outcome)) {
18
+ throw new Error(`Unknown simulator outcome: ${outcome}`);
19
+ }
20
+ const cleanTag = tag?.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
21
+ return `${outcome}${cleanTag ? `+${cleanTag}` : ""}@${SIMULATOR_DOMAIN}`;
22
+ }
23
+ function isSimulatorAddress(address) {
24
+ const match = address.match(/<([^>]+)>/);
25
+ const bare = (match ? match[1] : address).trim().toLowerCase();
26
+ return bare.endsWith(`@${SIMULATOR_DOMAIN}`);
27
+ }
28
+ var TEST_API_KEY_PREFIX = "gm_test_";
29
+ function isTestApiKey(key) {
30
+ return Boolean(key?.startsWith(TEST_API_KEY_PREFIX));
31
+ }
12
32
  export {
13
33
  DEFAULT_API_BASE_URL,
14
34
  Groundmail,
15
35
  GroundmailError,
36
+ SIMULATOR_DOMAIN,
37
+ SIMULATOR_OUTCOMES,
38
+ TEST_API_KEY_PREFIX,
16
39
  applyDnsRecords,
17
40
  buildSignatureHeader,
41
+ isSimulatorAddress,
42
+ isTestApiKey,
18
43
  mergeMigrations,
19
44
  mergeSpfInclude,
20
45
  signWebhookPayload,
46
+ simulatorAddress,
21
47
  verifyWebhookSignature
22
48
  };
package/dist/payload.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Groundmail } from './client-DXYsWgOT.js';
1
+ import { G as Groundmail } from './client-DLz4rpD9.js';
2
2
 
3
3
  /**
4
4
  * Minimal Payload `SendEmailOptions` / address shapes used by this adapter.
package/dist/payload.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-JYLRUBEJ.js";
4
+ } from "./chunk-MJKAVFFN.js";
5
5
 
6
6
  // src/payload.ts
7
7
  function resolveApiKey(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundmail",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Groundmail transactional email API client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,7 +89,7 @@
89
89
  "prepublishOnly": "npm run build && npm run build:nodemailer-bridge"
90
90
  },
91
91
  "dependencies": {
92
- "groundmail-nodemailer": "0.15.2"
92
+ "groundmail-nodemailer": "0.15.3"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@types/nodemailer": "^8.0.1",