tossinbox 0.1.1 → 0.1.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
@@ -68,11 +68,11 @@ Requires Node.js 18+.
68
68
  # Homebrew (macOS, Linux)
69
69
  brew install mohamed-khairy-5i/tap/tossinbox
70
70
 
71
- # npm from GitHub (npm registry publish coming soon)
72
- npm install -g github:mohamed-khairy-5i/tossinbox
71
+ # npm (npmjs.com)
72
+ npm install -g tossinbox
73
73
 
74
74
  # Or run without installing
75
- npx github:mohamed-khairy-5i/tossinbox spawn
75
+ npx tossinbox@latest spawn
76
76
  ```
77
77
 
78
78
  From source:
@@ -164,13 +164,14 @@ TossInbox ships with an MCP server exposing four tools:
164
164
  "mcpServers": {
165
165
  "tossinbox": {
166
166
  "command": "npx",
167
- "args": ["-y", "github:mohamed-khairy-5i/tossinbox", "mcp"]
167
+ "args": ["-y", "tossinbox", "mcp"]
168
168
  }
169
169
  }
170
170
  }
171
171
  ```
172
172
 
173
- Or after a global install, simply use `tossinbox-mcp` as the command.
173
+ Or after a global install, simply use `tossinbox-mcp` as the command
174
+ equivalent to `npx -y tossinbox mcp`.
174
175
 
175
176
  ### Works with any AI agent
176
177
 
@@ -229,7 +230,7 @@ No — receive-only by design. TossInbox exists for privacy and testing and
229
230
  ships no bulk-send or bulk-signup mode.
230
231
 
231
232
  **Does it work on Windows?**
232
- Yes, anywhere Node.js 18+ runs. `npx github:mohamed-khairy-5i/tossinbox spawn`
233
+ Yes, anywhere Node.js 18+ runs. `npx tossinbox@latest spawn`
233
234
  works in PowerShell exactly the same.
234
235
 
235
236
  **A site blocked my disposable address. What now?**
@@ -252,7 +253,7 @@ round.
252
253
  - [x] GitHub Action: `mohamed-khairy-5i/tossinbox@v1`
253
254
  - [x] Homebrew tap: `brew install mohamed-khairy-5i/tap/tossinbox`
254
255
  - [x] Project website at [tossinbox.pages.dev](https://tossinbox.pages.dev/)
255
- - [ ] Publish `tossinbox` + `tossinbox-mcp` to the npm registry
256
+ - [x] Publish `tossinbox` + `tossinbox-mcp` to the [npm registry](https://www.npmjs.com/package/tossinbox)
256
257
  - [ ] `mail.gw` provider (mail.tm-compatible API — small lift)
257
258
  - [ ] `tempmail.lol` provider (free API)
258
259
  - [ ] Provider failover: auto-switch when a provider is down
@@ -264,6 +265,9 @@ round.
264
265
  - [CLI reference](https://tossinbox.pages.dev/cli) — every command, flag, and exit code
265
266
  - [Agents & MCP](https://tossinbox.pages.dev/agents) — setup for every MCP client
266
267
  - [FAQ](https://tossinbox.pages.dev/faq) — privacy, providers, troubleshooting
268
+ - [Examples](https://tossinbox.pages.dev/examples) — copy-paste recipes: shell, CI, Node.js, MCP
269
+ - [Guide](https://tossinbox.pages.dev/guide) — providers, state, flags, exit codes, troubleshooting
270
+ - [Roadmap](https://tossinbox.pages.dev/roadmap) — what shipped and what is next
267
271
  - [Changelog](./CHANGELOG.md) · [Contributing](./CONTRIBUTING.md) · [Security](./SECURITY.md)
268
272
 
269
273
  ## License
@@ -0,0 +1,46 @@
1
+ import { ProviderError } from "./types.js";
2
+ /** Translate Node's raw fetch failures into a ProviderError that says what
3
+ * actually happened and what to do next. Without this, a DNS blip or a dead
4
+ * upstream surfaces as Node's bare "fetch failed" — useless to humans and
5
+ * unparsable for agents. Messages stay short, actionable, and stable. */
6
+ export function networkError(err, provider, host, timeoutMs) {
7
+ const failure = (err ?? {});
8
+ const cause = failure.cause ?? {};
9
+ const code = cause.code ?? "";
10
+ // AbortSignal.timeout rejects with a DOMException named "TimeoutError"
11
+ // (sometimes wrapped as the fetch cause).
12
+ if (failure.name === "TimeoutError" ||
13
+ cause.name === "TimeoutError" ||
14
+ code === "UND_ERR_HEADERS_TIMEOUT") {
15
+ return new ProviderError(provider, `no response from ${host} within ${Math.round(timeoutMs / 1000)}s — ` +
16
+ "provider is down or unreachable; retry, or switch with --provider");
17
+ }
18
+ switch (code) {
19
+ case "ENOTFOUND":
20
+ case "EAI_AGAIN":
21
+ return new ProviderError(provider, `DNS lookup failed for ${host} — no internet, bad DNS, or a blocked ` +
22
+ "network; check connectivity and retry");
23
+ case "ECONNREFUSED":
24
+ return new ProviderError(provider, `connection refused by ${host} — provider may be down, or a ` +
25
+ "firewall/proxy is blocking it; retry later");
26
+ case "ECONNRESET":
27
+ case "EPIPE":
28
+ return new ProviderError(provider, `connection to ${host} dropped mid-request — usually transient; retry`);
29
+ case "ENETUNREACH":
30
+ case "ENETDOWN":
31
+ case "EHOSTUNREACH":
32
+ return new ProviderError(provider, "network unreachable — you appear to be offline; check your connection and retry");
33
+ case "ECONNABORTED":
34
+ return new ProviderError(provider, `connection to ${host} aborted — usually transient; retry`);
35
+ case "CERT_HAS_EXPIRED":
36
+ case "DEPTH_ZERO_SELF_SIGNED_CERT":
37
+ case "SELF_SIGNED_CERT_IN_CHAIN":
38
+ case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
39
+ return new ProviderError(provider, `TLS certificate problem with ${host} — if a corporate proxy ` +
40
+ "intercepts TLS, point NODE_EXTRA_CA_CERTS at its CA bundle");
41
+ default: {
42
+ const detail = cause.message || failure.message || String(err);
43
+ return new ProviderError(provider, `cannot reach ${host} (${detail}) — check connectivity and retry`);
44
+ }
45
+ }
46
+ }
@@ -1,5 +1,6 @@
1
1
  import { ProviderError } from "../types.js";
2
2
  import { extractCode } from "../otp.js";
3
+ import { networkError } from "../net.js";
3
4
  import { VERSION } from "../../version.js";
4
5
  const BASE = "https://api.guerrillamail.com/ajax.php";
5
6
  const REQUEST_TIMEOUT_MS = 20_000;
@@ -13,10 +14,13 @@ async function call(params) {
13
14
  const url = new URL(BASE);
14
15
  for (const [k, v] of Object.entries(params))
15
16
  url.searchParams.set(k, v);
16
- // Hard timeout on every request — an agent must never hang forever.
17
+ // Hard timeout on every request — an agent must never hang forever. Raw
18
+ // network failures are translated into a readable, actionable ProviderError.
17
19
  const res = await fetch(url, {
18
20
  headers: { Accept: "application/json", "User-Agent": `tossinbox/${VERSION}` },
19
21
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
22
+ }).catch((err) => {
23
+ throw networkError(err, "guerrillamail", "api.guerrillamail.com", REQUEST_TIMEOUT_MS);
20
24
  });
21
25
  if (!res.ok)
22
26
  throw new ProviderError("guerrillamail", `HTTP ${res.status}`, res.status);
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { ProviderError } from "../types.js";
3
3
  import { extractCode } from "../otp.js";
4
+ import { networkError } from "../net.js";
4
5
  import { VERSION } from "../../version.js";
5
6
  const BASE = "https://api.mail.tm";
6
7
  const REQUEST_TIMEOUT_MS = 20_000;
@@ -27,9 +28,13 @@ function headers(token) {
27
28
  h.Authorization = `Bearer ${token}`;
28
29
  return h;
29
30
  }
30
- /** Hard timeout on every request — an agent must never hang forever. */
31
+ /** Hard timeout on every request — an agent must never hang forever. Raw
32
+ * network failures (DNS, refused, timeout) are translated into a readable,
33
+ * actionable ProviderError instead of Node's bare "fetch failed". */
31
34
  function fetchJson(url, init) {
32
- return fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
35
+ return fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }).catch((err) => {
36
+ throw networkError(err, "mailtm", "api.mail.tm", REQUEST_TIMEOUT_MS);
37
+ });
33
38
  }
34
39
  /** mail.tm allows ~8 requests per second; retry once on 429. */
35
40
  async function request(method, url, options = {}) {
@@ -61,9 +61,28 @@ export async function resolveInbox(address) {
61
61
  }
62
62
  async function writeState(state) {
63
63
  const file = statePath();
64
+ const dir = path.dirname(file);
64
65
  // The state file contains provider tokens — keep it private.
65
- await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
66
- await fs.writeFile(file, JSON.stringify(state, null, 2) + "\n", "utf8");
67
- // writeFile's mode option only applies at creation enforce on every write.
68
- await fs.chmod(file, 0o600);
66
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
67
+ // Atomic save: write a sibling temp file, fsync it, then rename it over the
68
+ // real one. A crash mid-write can no longer truncate state.json and destroy
69
+ // saved inboxes — readers always see either the old file or the new one.
70
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
71
+ const handle = await fs.open(tmp, "w", 0o600);
72
+ try {
73
+ await handle.writeFile(JSON.stringify(state, null, 2) + "\n", "utf8");
74
+ // open()'s mode is filtered by umask — enforce 0600 before the rename.
75
+ await handle.chmod(0o600);
76
+ await handle.sync(); // flush to disk before it becomes the real file
77
+ }
78
+ finally {
79
+ await handle.close();
80
+ }
81
+ try {
82
+ await fs.rename(tmp, file); // atomic on POSIX; replaces the target on Windows too
83
+ }
84
+ catch (err) {
85
+ await fs.rm(tmp, { force: true }).catch(() => { });
86
+ throw err;
87
+ }
69
88
  }
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Single source of truth for the runtime version string.
2
2
  * Keep in sync with package.json — bump both on release. */
3
- export const VERSION = "0.1.1";
3
+ export const VERSION = "0.1.2";
package/llms.txt CHANGED
@@ -7,6 +7,21 @@ an MCP server, collects verification codes (OTP) from them, and deletes the
7
7
  inboxes when done. It is designed agent-first: every CLI command supports
8
8
  `--json`, exit codes are documented, and there are no interactive prompts.
9
9
 
10
+ ## Site
11
+
12
+ - [Home](https://tossinbox.pages.dev/): overview, install options, comparison table
13
+ - [Quickstart](https://tossinbox.pages.dev/quickstart): first inbox in four commands
14
+ - [CLI reference](https://tossinbox.pages.dev/cli): all commands, flags, exit codes
15
+ - [Agents & MCP](https://tossinbox.pages.dev/agents): MCP server setup for AI agents
16
+ - [Examples](https://tossinbox.pages.dev/examples): copy-paste recipes (shell, CI, Node.js, MCP)
17
+ - [Guide](https://tossinbox.pages.dev/guide): providers, state, flags, exit codes, troubleshooting
18
+ - [FAQ](https://tossinbox.pages.dev/faq): privacy, providers, troubleshooting
19
+ - [Roadmap](https://tossinbox.pages.dev/roadmap): shipped and planned
20
+ - [Changelog](https://tossinbox.pages.dev/changelog): every change by version
21
+ - [Arabic home](https://tossinbox.pages.dev/ar/): الصفحة الرئيسية بالعربية
22
+ - [Arabic quickstart](https://tossinbox.pages.dev/ar/quickstart): البداية السريعة بالعربية
23
+ - [Source repository](https://github.com/mohamed-khairy-5i/tossinbox): code, issues, releases
24
+
10
25
  ## CLI (binary: `tossinbox`)
11
26
 
12
27
  - `tossinbox spawn` — create a new disposable inbox (flags: `-p provider`, `-l label`)
@@ -37,9 +52,9 @@ Typical agent flow: `create_inbox` -> use the address in a signup form ->
37
52
  ## Installation
38
53
 
39
54
  ```bash
40
- npm install -g github:mohamed-khairy-5i/tossinbox
55
+ npm install -g tossinbox
41
56
  # or run without installing:
42
- npx github:mohamed-khairy-5i/tossinbox spawn
57
+ npx tossinbox@latest spawn
43
58
  ```
44
59
 
45
60
  ## Notes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tossinbox",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Disposable email inboxes for humans and AI agents. Spawn an inbox, wait for the OTP, toss it.",
5
5
  "type": "module",
6
6
  "license": "MIT",