@mailto.foo/sdk 1.5.1 → 1.5.2

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
@@ -87,5 +87,15 @@ try {
87
87
  Your publish key (`pk_xxx`) is tied to your mailto.foo account and the domain you registered. It is safe to expose in frontend code — it only allows submissions, not reads.
88
88
  Get your publish key from your mailto.foo dashboard.
89
89
 
90
+ ## Claude Code
91
+
92
+ If you use [Claude Code](https://claude.com/claude-code), run:
93
+
94
+ ```bash
95
+ npx @mailto.foo/sdk init
96
+ ```
97
+
98
+ This adds a `/mailto-foo` slash command to your project (`.claude/commands/mailto-foo.md`) with everything Claude needs to know to wire up subscribe/contact forms with this SDK.
99
+
90
100
  ## License
91
101
  MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { copyFileSync, mkdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const command = process.argv[2];
7
+ const init = () => {
8
+ const src = join(__dirname, "..", "commands", "mailto-foo.md");
9
+ const destDir = join(process.cwd(), ".claude", "commands");
10
+ const dest = join(destDir, "mailto-foo.md");
11
+ mkdirSync(destDir, { recursive: true });
12
+ copyFileSync(src, dest);
13
+ console.log(`Created ${join(".claude", "commands", "mailto-foo.md")}`);
14
+ console.log("Run /mailto-foo in Claude Code to integrate @mailto.foo/sdk.");
15
+ };
16
+ switch (command) {
17
+ case "init":
18
+ init();
19
+ break;
20
+ default:
21
+ console.log("Usage: npx @mailto.foo/sdk init");
22
+ process.exit(command === undefined ? 0 : 1);
23
+ }
24
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAEhC,MAAM,IAAI,GAAG,GAAG,EAAE;IACjB,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IAE5C,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACxB,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,SAAS,EAAE,UAAU,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IACvE,OAAO,CAAC,GAAG,CAAC,8DAA8D,CAAC,CAAC;AAC7E,CAAC,CAAC;AAEF,QAAQ,OAAO,EAAE,CAAC;IACjB,KAAK,MAAM;QACV,IAAI,EAAE,CAAC;QACP,MAAM;IACP;QACC,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,97 @@
1
+ ---
2
+ description: Integrate @mailto.foo/sdk for email subscription/contact forms
3
+ ---
4
+
5
+ # @mailto.foo/sdk integration
6
+
7
+ Headless email subscription/contact-form SDK. Two entry points: `src/index.ts` (the `Mailto` class, published as ESM/CJS) and `src/cdn.ts` (auto-enhancement script for the CDN bundle, built on top of `Mailto`).
8
+
9
+ ## Core API
10
+
11
+ ```js
12
+ import { Mailto } from '@mailto.foo/sdk'
13
+
14
+ const mailto = new Mailto({ publishKey: 'pk_your_publish_key' })
15
+ await mailto.subscribe({ email: 'user@example.com', /* ...any extra string fields */ })
16
+ ```
17
+
18
+ `subscribe(data)` accepts `{ email: string; [key: string]: string }` — `email` is the only required field. Any additional string key/value pairs (name, company, message, plan, source, etc.) are passed straight through to the API and show up alongside the subscriber in the mailto.foo dashboard. There's no schema to register; just add the fields you need to the object (or to the `<form>` as extra `<input name="...">` elements when using the CDN auto-enhancement).
19
+
20
+ - `subscribe(data)` first calls `GET {API_BASE}/config/:publishKey/config` to fetch publisher config (5s timeout via `AbortController`).
21
+ - If the publisher has `turnstileSiteKey` set, it loads Cloudflare Turnstile, renders an invisible widget, and obtains a token before submitting — falling back to a visible modal overlay if the invisible challenge doesn't resolve to a token. This whole flow is internal; callers don't need to do anything extra.
22
+ - It then `POST`s `{ data, config: { publishKey, token } }` to `{API_BASE}/subscribe`.
23
+ - On a non-OK response it throws an `Error` whose message is the API's `error` field (JSON responses) or raw text body (non-JSON responses).
24
+ - On success it resolves with `void` — there is no return payload to inspect.
25
+
26
+ `API_BASE` is hardcoded to `https://api-mailto-foo.dan-7bc.workers.dev`.
27
+
28
+ ## CDN auto-enhancement (`src/cdn.ts`)
29
+
30
+ The CDN bundle scans the DOM for `form[action]` elements whose action matches `/((?:pk_|ph_public_)[A-Za-z0-9_-]+)\/subscribe\/?(?:[?#].*)?$/` and wires each one up automatically — no manual JS needed. Forms pointing at a mailto.foo subscribe path with an unrecognized key prefix have their submit blocked (with a `console.warn`) instead of letting the browser navigate to a 404.
31
+
32
+ 1. Calls `mailto.injectHoneypot(form)` (see Bot protection below) before attaching the submit handler.
33
+ 2. Intercepts `submit`, calls `preventDefault()`.
34
+ 3. Disables and marks loading (`data-loading="true"`) on every element matching `.mailto-foo-button`, `button[type="submit"]`, or `input[type="submit"]`.
35
+ 4. Dispatches `mailto:submitting` (bubbling `CustomEvent`, no detail) on the form.
36
+ 5. Calls `mailto.subscribe()` with all form fields collected via `FormData` — including any extra fields you've added (see "Extra/custom fields" above) and the injected honeypot field.
37
+ 6. On success: resets the form, dispatches `mailto:success`. If `data-verbose` is present, also clears `.mailto-foo-error` and sets `.mailto-foo-success` text to "Thanks for subscribing!".
38
+ 7. On failure: dispatches `mailto:error` with `detail: { error: <Error> }`. If `data-verbose` is present, also clears `.mailto-foo-success` and sets `.mailto-foo-error` text to `err.message`.
39
+ 8. Always re-enables buttons in a `finally` block (only if `data-verbose`).
40
+
41
+ By default the CDN script is **silent** — it only fires events and resets the form; it does not touch any status elements or button states. Add `data-verbose` to the `<form>` to opt into automatic UI management: status `<div>`s are auto-created if absent (appended as the last children), buttons are disabled during submission, and success/error text is written automatically. Exposes `window.Mailto = Mailto`.
42
+
43
+ ```html
44
+ <!-- Default: silent mode — only events fire, no automatic UI changes -->
45
+ <form action="https://api.mailto.foo/pk_xxx/subscribe">
46
+ <input type="email" name="email" required>
47
+ <button type="submit">Subscribe</button>
48
+ </form>
49
+
50
+ <script>
51
+ form.addEventListener('mailto:submitting', () => showLoading())
52
+ form.addEventListener('mailto:success', () => showThankYou())
53
+ form.addEventListener('mailto:error', (e) => showError(e.detail.error.message))
54
+ </script>
55
+
56
+ <!-- data-verbose: auto-manages status divs and button loading state -->
57
+ <form action="https://api.mailto.foo/pk_xxx/subscribe" data-verbose>
58
+ <input type="email" name="email" required>
59
+ <div class="mailto-foo-error"></div>
60
+ <div class="mailto-foo-success"></div>
61
+ <button type="submit" class="mailto-foo-button">Subscribe</button>
62
+ </form>
63
+ ```
64
+
65
+ ## Bot protection / honeypot
66
+
67
+ `Mailto#injectHoneypot(form)` fetches `GET {API_BASE}/config/:publishKey/hint?fields=<comma-separated existing field names>` and, if the publisher has it enabled, appends a hidden `<input>` (visually hidden, `tabindex="-1"`, `aria-hidden="true"`, `autocomplete="off"`) to the form using a field name returned by the API. The returned `field` name and an optional `sessionId` are cached on the `Mailto` instance and sent as `f` / `sid` on the next `subscribe()` call.
68
+
69
+ - The CDN auto-enhancement calls this automatically for every enhanced form — no setup needed.
70
+ - For manual/headless usage, call `await mailto.injectHoneypot(form)` once before the form can be submitted if you want this protection (then call `subscribe()` as usual — the honeypot field will be included via `FormData`/your data object).
71
+ - Don't strip unexpected hidden input fields from a mailto.foo form — they may be the honeypot field used for bot detection.
72
+
73
+ ## Error handling
74
+
75
+ Always wrap manual `subscribe()` calls in try/catch — it throws a plain `Error`:
76
+
77
+ ```js
78
+ try {
79
+ await mailto.subscribe({ email })
80
+ } catch (err) {
81
+ console.error(err.message) // e.g. "Email already subscribed"
82
+ }
83
+ ```
84
+
85
+ ## Gotchas / non-obvious details
86
+
87
+ - The publish key (`pk_xxx`) is meant to be exposed client-side — it only authorizes submissions, not reads.
88
+ - `subscribe()` makes **two** network round-trips per call: a config fetch, then the actual subscribe POST (plus a Turnstile challenge round-trip when bot protection is enabled). Don't assume it's a single fetch when debugging latency or mocking requests in tests.
89
+ - Turnstile script loading is memoized via the module-level `turnstileScriptPromise` — it's only ever injected once per page, even across multiple forms/instances.
90
+ - The form-action regex requires the action URL to end in `/<publishKey>/subscribe` (optionally with a trailing slash or query/hash) and the key to start with `pk_` or `ph_public_` — forms with other URL shapes are silently skipped, but forms pointing at *some* `<key>/subscribe` path on a mailto.foo host with an unrecognized prefix have submission blocked with a console warning.
91
+ - Extra form fields beyond `email` are passed straight through to the API as arbitrary string key/value pairs (`SubscribeData` is `{ email: string; [key: string]: string }`).
92
+
93
+ ## Task
94
+
95
+ $ARGUMENTS
96
+
97
+ Use the reference above to add or debug a mailto.foo subscribe/contact form in this project.
package/package.json CHANGED
@@ -2,9 +2,12 @@
2
2
  "name": "@mailto.foo/sdk",
3
3
  "type": "module",
4
4
  "description": "Headless email subscription and contact form SDK for mailto.foo",
5
- "version": "1.5.1",
5
+ "version": "1.5.2",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "sdk": "./dist/bin/cli.js"
10
+ },
8
11
  "repository": {
9
12
  "type": "git",
10
13
  "url": "git+https://gitlab.com/mailto.foo/sdk.git"
@@ -19,6 +22,7 @@
19
22
  "@biomejs/biome": "1.8.3",
20
23
  "@changesets/cli": "^2.27.7",
21
24
  "@total-typescript/tsconfig": "^1.0.4",
25
+ "@types/node": "^25.9.3",
22
26
  "esbuild": "^0.28.0",
23
27
  "typescript": "^5.5.3"
24
28
  },
@@ -26,8 +30,9 @@
26
30
  "url": "https://gitlab.com/mailto.foo/sdk/-/boards"
27
31
  },
28
32
  "scripts": {
29
- "build": "tsc && pnpm run build:cdn",
33
+ "build": "tsc && pnpm run build:cdn && pnpm run build:commands && chmod +x dist/bin/cli.js",
30
34
  "build:cdn": "esbuild src/cdn.ts --bundle --minify --format=iife --outfile=dist/sdk.min.js",
35
+ "build:commands": "mkdir -p dist/commands && cp src/commands/mailto-foo.md dist/commands/mailto-foo.md",
31
36
  "check": "biome check --write ./src",
32
37
  "workflow:check": "biome check ./src",
33
38
  "release": "pnpm run build && changeset publish"