@anonymilyhq/cli 1.1.1 → 1.2.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 +27 -38
- package/bin/cli.js +2 -63
- package/lib/forwarder.js +2 -2
- package/package.json +1 -1
- package/lib/config.js +0 -70
- package/lib/proRegister.js +0 -100
package/README.md
CHANGED
|
@@ -38,16 +38,17 @@ npm install -g @anonymilyhq/cli
|
|
|
38
38
|
|
|
39
39
|
## Quick Start
|
|
40
40
|
|
|
41
|
-
**No account or signup required** — works immediately with free tier (50 requests, 24h retention).
|
|
41
|
+
**No account or signup required** — works immediately with free tier (random ID, 50 requests, 24h retention).
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
# Start listening — generates a random endpoint ID automatically
|
|
44
|
+
# Start listening — generates a random 8-char endpoint ID automatically
|
|
45
45
|
npx @anonymilyhq/cli listen 3000
|
|
46
46
|
|
|
47
47
|
# Output:
|
|
48
48
|
# 🚀 Anonymily CLI is running!
|
|
49
49
|
# Forwarding: https://api.anonymily.com/h/xk92bzte ➔ http://127.0.0.1:3000
|
|
50
50
|
# Tier: 🆓 FREE | Storage: 0/50 | Retention: 24 hours
|
|
51
|
+
# 💡 Upgrade to Pro for custom endpoint IDs + 500 req/30d at https://anonymily.com/upgrade
|
|
51
52
|
# Waiting for requests to arrive...
|
|
52
53
|
```
|
|
53
54
|
|
|
@@ -79,35 +80,18 @@ anonymily listen <port> [options]
|
|
|
79
80
|
| Option | Description |
|
|
80
81
|
|--------|-------------|
|
|
81
82
|
| `<port>` | **Required.** Local port to forward to (e.g. `3000`, `8080`) |
|
|
82
|
-
| `-i, --id <id>` | Use a custom
|
|
83
|
-
| `--pro` | Register the hook as Pro-tier (legacy). Requires `--id` and a configured Pro API key |
|
|
83
|
+
| `-i, --id <id>` | **Pro only.** Use a custom endpoint ID (e.g. `stripe-test`) instead of a random one. Requires Pro subscription (₹750/month). |
|
|
84
84
|
|
|
85
85
|
**Examples:**
|
|
86
86
|
```bash
|
|
87
|
-
# Random endpoint ID (
|
|
87
|
+
# Random endpoint ID (Free tier: 50 requests, 24h retention)
|
|
88
88
|
npx @anonymilyhq/cli listen 3000
|
|
89
89
|
|
|
90
|
-
# Custom named endpoint
|
|
90
|
+
# Custom named endpoint (Pro only: requires subscription + claiming via dashboard)
|
|
91
91
|
npx @anonymilyhq/cli listen 3000 --id stripe-dev
|
|
92
92
|
|
|
93
|
-
# Pro-
|
|
94
|
-
npx @anonymilyhq/cli listen 3000 --id
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
### `config set-key <key>`
|
|
100
|
-
|
|
101
|
-
Save your Pro API key locally so you don't need to export it every session.
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
anonymily config set-key <your-pro-api-key>
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
The key is stored in `~/.anonymily/config.json`. You can also set it via environment variable:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
export PRO_API_KEY=<your-pro-api-key>
|
|
93
|
+
# Use your Pro-claimed endpoint (500 requests, 30 days)
|
|
94
|
+
npx @anonymilyhq/cli listen 3000 --id my-claimed-hook
|
|
111
95
|
```
|
|
112
96
|
|
|
113
97
|
---
|
|
@@ -143,20 +127,23 @@ Your feedback helps us improve Anonymily. All submissions are anonymous unless y
|
|
|
143
127
|
|
|
144
128
|
## Upgrading to Pro (₹750/month)
|
|
145
129
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
130
|
+
**What Pro gives you:**
|
|
131
|
+
- **Custom named endpoints** - Use memorable IDs like `stripe-prod` or `github-webhooks`
|
|
132
|
+
- **Claim & manage** - Link endpoints to your account permanently via dashboard
|
|
133
|
+
- **500 requests** per endpoint (vs 50 for Free random IDs)
|
|
134
|
+
- **30-day retention** (vs 24 hours for Free tier)
|
|
135
|
+
|
|
136
|
+
**How to upgrade and use custom endpoints:**
|
|
137
|
+
1. Subscribe to Pro at [anonymily.com/upgrade](https://anonymily.com/upgrade) (₹750/month)
|
|
138
|
+
2. Log in to your dashboard at [anonymily.com/dashboard](https://anonymily.com/dashboard)
|
|
139
|
+
3. Enter your custom endpoint ID (e.g. `my-webhook`) and click "Claim"
|
|
140
|
+
4. Run the CLI with your claimed hook ID:
|
|
154
141
|
```bash
|
|
155
|
-
npx @anonymilyhq/cli listen 3000 --id
|
|
142
|
+
npx @anonymilyhq/cli listen 3000 --id my-webhook
|
|
156
143
|
```
|
|
157
|
-
|
|
144
|
+
5. Enjoy Pro limits — 500 requests, 30-day history
|
|
158
145
|
|
|
159
|
-
**
|
|
146
|
+
**Important:** Custom endpoint IDs are a **Pro-only feature**. Free users can only use randomly-generated 8-character IDs with 50 requests and 24h retention.
|
|
160
147
|
|
|
161
148
|
---
|
|
162
149
|
|
|
@@ -185,12 +172,15 @@ External Service (Stripe, GitHub, etc.)
|
|
|
185
172
|
|
|
186
173
|
| Feature | Free | Pro |
|
|
187
174
|
|---------|------|-----|
|
|
175
|
+
| Endpoint ID format | Random 8-char (e.g. `xk92bzte`) | Custom named (e.g. `stripe-prod`) |
|
|
188
176
|
| Requests stored per hook | 50 | 500 |
|
|
189
177
|
| History retention | 24 hours | 30 days |
|
|
190
|
-
|
|
|
191
|
-
|
|
|
178
|
+
| Hook claiming & management | ❌ | ✅ |
|
|
179
|
+
| Dashboard access | ✅ | ✅ |
|
|
192
180
|
| Price | Free | ₹750/month |
|
|
193
181
|
|
|
182
|
+
**Note:** Custom endpoint IDs (via `--id` flag) are a **Pro-only feature**. Free users receive randomly-generated IDs.
|
|
183
|
+
|
|
194
184
|
---
|
|
195
185
|
|
|
196
186
|
## Testing Locally
|
|
@@ -233,7 +223,6 @@ npx @anonymilyhq/cli listen 4000
|
|
|
233
223
|
|
|
234
224
|
| Variable | Description |
|
|
235
225
|
|----------|-------------|
|
|
236
|
-
| `PRO_API_KEY` | Legacy Pro API key (overrides `~/.anonymily/config.json`) |
|
|
237
226
|
| `ANONYMILY_API_URL` | Override the backend URL (default: `https://api.anonymily.com`) |
|
|
238
227
|
|
|
239
228
|
---
|
package/bin/cli.js
CHANGED
|
@@ -4,13 +4,6 @@ import { program } from 'commander';
|
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import { generateHookId } from '../lib/utils.js';
|
|
6
6
|
import { startForwarding } from '../lib/forwarder.js';
|
|
7
|
-
import { writeConfig, CONFIG_PATH } from '../lib/config.js';
|
|
8
|
-
import {
|
|
9
|
-
registerProHook,
|
|
10
|
-
ProApiKeyMissingError,
|
|
11
|
-
ProApiAuthError,
|
|
12
|
-
ProApiError,
|
|
13
|
-
} from '../lib/proRegister.js';
|
|
14
7
|
import { logger } from '../lib/logger.js';
|
|
15
8
|
import { submitFeedback } from '../lib/feedback.js';
|
|
16
9
|
|
|
@@ -20,7 +13,7 @@ const API_URL = process.env.ANONYMILY_API_URL || 'https://api.anonymily.com';
|
|
|
20
13
|
program
|
|
21
14
|
.name('anonymily')
|
|
22
15
|
.description('Forward webhooks from Anonymily directly to your local machine.')
|
|
23
|
-
.version('1.
|
|
16
|
+
.version('1.2.0');
|
|
24
17
|
|
|
25
18
|
// ---------------------------------------------------------------------------
|
|
26
19
|
// listen
|
|
@@ -29,66 +22,12 @@ program
|
|
|
29
22
|
.command('listen')
|
|
30
23
|
.description('Listen for incoming webhooks and forward them to a local port')
|
|
31
24
|
.argument('<port>', 'Local port to forward to (e.g., 8080)')
|
|
32
|
-
.option('-i, --id <id>', '
|
|
33
|
-
.option(
|
|
34
|
-
'--pro',
|
|
35
|
-
'Register the hook as Pro-tier (requires --id and PRO_API_KEY)',
|
|
36
|
-
)
|
|
25
|
+
.option('-i, --id <id>', '[Pro only] Use a custom endpoint ID instead of random (requires ₹750/month subscription)')
|
|
37
26
|
.action(async (port, options) => {
|
|
38
|
-
// --pro requires --id
|
|
39
|
-
if (options.pro && !options.id) {
|
|
40
|
-
logger.error(
|
|
41
|
-
pc.bold(
|
|
42
|
-
'--pro requires --id to specify a persistent hook name.',
|
|
43
|
-
),
|
|
44
|
-
);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
27
|
const hookId = options.id || generateHookId();
|
|
49
|
-
|
|
50
|
-
if (options.pro) {
|
|
51
|
-
try {
|
|
52
|
-
await registerProHook(API_URL, hookId);
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (
|
|
55
|
-
err instanceof ProApiKeyMissingError ||
|
|
56
|
-
err instanceof ProApiAuthError ||
|
|
57
|
-
err instanceof ProApiError
|
|
58
|
-
) {
|
|
59
|
-
logger.error(err.message);
|
|
60
|
-
} else {
|
|
61
|
-
logger.error(`Unexpected error during Pro registration: ${err.message}`);
|
|
62
|
-
}
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
28
|
startForwarding(API_URL, hookId, parseInt(port, 10));
|
|
68
29
|
});
|
|
69
30
|
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// config
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
const configCmd = program
|
|
74
|
-
.command('config')
|
|
75
|
-
.description('Manage CLI configuration');
|
|
76
|
-
|
|
77
|
-
configCmd
|
|
78
|
-
.command('set-key <key>')
|
|
79
|
-
.description(
|
|
80
|
-
'Save your Pro API key to ~/.anonymily/config.json',
|
|
81
|
-
)
|
|
82
|
-
.action((key) => {
|
|
83
|
-
if (!key || !key.trim()) {
|
|
84
|
-
logger.error('API key cannot be empty.');
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
writeConfig({ proApiKey: key.trim() });
|
|
88
|
-
logger.success(
|
|
89
|
-
`Pro API key saved to ${CONFIG_PATH}`,
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
31
|
|
|
93
32
|
// ---------------------------------------------------------------------------
|
|
94
33
|
// feedback
|
package/lib/forwarder.js
CHANGED
|
@@ -68,7 +68,7 @@ export async function startForwarding(apiUrl, hookId, port) {
|
|
|
68
68
|
logger.raw(`Tier: ${tierColor(pc.bold(tierBadge))} | Storage: ${tierInfo.requestCount}/${tierInfo.maxRequests} | Retention: ${tierInfo.retention}`);
|
|
69
69
|
|
|
70
70
|
if (tierInfo.tier === 'free') {
|
|
71
|
-
logger.dim(`\n💡 Upgrade to Pro for
|
|
71
|
+
logger.dim(`\n💡 Upgrade to Pro for custom endpoint IDs + 500 req/30d at https://anonymily.com/upgrade`);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -118,7 +118,7 @@ export async function startForwarding(apiUrl, hookId, port) {
|
|
|
118
118
|
if (requestCount === maxRequests) {
|
|
119
119
|
logger.warn(`⚠️ Tier limit reached (${maxRequests} requests). Older requests will be purged.\n`);
|
|
120
120
|
} else if (requestCount === Math.floor(maxRequests * 0.9)) {
|
|
121
|
-
logger.warn(`⚠️ 90% of tier limit reached.
|
|
121
|
+
logger.warn(`⚠️ 90% of tier limit reached. Upgrade to Pro for 500 requests at https://anonymily.com/upgrade\n`);
|
|
122
122
|
}
|
|
123
123
|
} catch (err) {
|
|
124
124
|
logger.error(` └─ Error forwarding: ${err.message}`);
|
package/package.json
CHANGED
package/lib/config.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/config.js
|
|
3
|
-
*
|
|
4
|
-
* Read and write the Anonymily CLI config file at ~/.anonymily/config.json.
|
|
5
|
-
* The config file stores user-scoped settings such as the Pro API key.
|
|
6
|
-
*
|
|
7
|
-
* Schema:
|
|
8
|
-
* {
|
|
9
|
-
* "proApiKey": "<string>" // Pro subscriber API key
|
|
10
|
-
* }
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
14
|
-
import { homedir } from 'node:os';
|
|
15
|
-
import { join, dirname } from 'node:path';
|
|
16
|
-
|
|
17
|
-
/** Absolute path to the config directory. */
|
|
18
|
-
export const CONFIG_DIR = join(homedir(), '.anonymily');
|
|
19
|
-
|
|
20
|
-
/** Absolute path to the config JSON file. */
|
|
21
|
-
export const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Read and parse the config file.
|
|
25
|
-
* Returns an empty object if the file doesn't exist yet.
|
|
26
|
-
*
|
|
27
|
-
* @returns {Record<string, unknown>}
|
|
28
|
-
*/
|
|
29
|
-
export function readConfig() {
|
|
30
|
-
if (!existsSync(CONFIG_PATH)) return {};
|
|
31
|
-
try {
|
|
32
|
-
const raw = readFileSync(CONFIG_PATH, 'utf8');
|
|
33
|
-
return JSON.parse(raw);
|
|
34
|
-
} catch {
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Write (merge) key-value pairs into the config file.
|
|
41
|
-
* Creates the config directory and file if they don't exist.
|
|
42
|
-
*
|
|
43
|
-
* @param {Record<string, unknown>} updates
|
|
44
|
-
*/
|
|
45
|
-
export function writeConfig(updates) {
|
|
46
|
-
const existing = readConfig();
|
|
47
|
-
const merged = { ...existing, ...updates };
|
|
48
|
-
|
|
49
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
50
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Retrieve the user's Pro API key.
|
|
58
|
-
*
|
|
59
|
-
* Resolution order (highest precedence first):
|
|
60
|
-
* 1. PRO_API_KEY environment variable
|
|
61
|
-
* 2. proApiKey field in ~/.anonymily/config.json
|
|
62
|
-
*
|
|
63
|
-
* @returns {string | undefined}
|
|
64
|
-
*/
|
|
65
|
-
export function getProApiKey() {
|
|
66
|
-
if (process.env.PRO_API_KEY) return process.env.PRO_API_KEY;
|
|
67
|
-
const cfg = readConfig();
|
|
68
|
-
return typeof cfg.proApiKey === 'string' ? cfg.proApiKey : undefined;
|
|
69
|
-
}
|
|
70
|
-
|
package/lib/proRegister.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/proRegister.js
|
|
3
|
-
*
|
|
4
|
-
* Registers a hook ID as Pro-tier by calling the backend's
|
|
5
|
-
* POST /hooks/pro/register endpoint.
|
|
6
|
-
*
|
|
7
|
-
* Acceptance criteria (ANO-47):
|
|
8
|
-
* - Uses the PRO_API_KEY from env or ~/.anonymily/config.json
|
|
9
|
-
* - If the key is missing → throws ProApiKeyMissingError
|
|
10
|
-
* - If the API returns 401 → throws ProApiAuthError
|
|
11
|
-
* - If the API returns any other non-2xx → throws ProApiError
|
|
12
|
-
* - On success → prints confirmation message and returns the response body
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import pc from 'picocolors';
|
|
16
|
-
import { logger } from './logger.js';
|
|
17
|
-
import { getProApiKey } from './config.js';
|
|
18
|
-
|
|
19
|
-
/** Thrown when no Pro API key is configured. */
|
|
20
|
-
export class ProApiKeyMissingError extends Error {
|
|
21
|
-
constructor() {
|
|
22
|
-
super(
|
|
23
|
-
'Pro API key not configured. Run: anonymily config set-key <key>',
|
|
24
|
-
);
|
|
25
|
-
this.name = 'ProApiKeyMissingError';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Thrown when the backend returns 401 Unauthorized. */
|
|
30
|
-
export class ProApiAuthError extends Error {
|
|
31
|
-
constructor() {
|
|
32
|
-
super(
|
|
33
|
-
'Pro API key is invalid or revoked. Please verify your key with: anonymily config set-key <key>',
|
|
34
|
-
);
|
|
35
|
-
this.name = 'ProApiAuthError';
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Thrown when the backend returns any other non-2xx status. */
|
|
40
|
-
export class ProApiError extends Error {
|
|
41
|
-
/** @param {number} status @param {string} [body] */
|
|
42
|
-
constructor(status, body = '') {
|
|
43
|
-
super(`Pro registration failed (HTTP ${status}): ${body}`);
|
|
44
|
-
this.name = 'ProApiError';
|
|
45
|
-
this.status = status;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Register a hook ID as Pro-tier via the backend API.
|
|
51
|
-
*
|
|
52
|
-
* @param {string} apiUrl Base URL, e.g. https://api.anonymily.com
|
|
53
|
-
* @param {string} hookId The hook ID to register
|
|
54
|
-
* @returns {Promise<object>} Parsed response body from the API
|
|
55
|
-
*
|
|
56
|
-
* @throws {ProApiKeyMissingError} No API key available
|
|
57
|
-
* @throws {ProApiAuthError} API returned 401
|
|
58
|
-
* @throws {ProApiError} API returned other non-2xx
|
|
59
|
-
*/
|
|
60
|
-
export async function registerProHook(apiUrl, hookId) {
|
|
61
|
-
const apiKey = getProApiKey();
|
|
62
|
-
|
|
63
|
-
if (!apiKey) {
|
|
64
|
-
throw new ProApiKeyMissingError();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let response;
|
|
68
|
-
try {
|
|
69
|
-
response = await fetch(`${apiUrl}/hooks/pro/register`, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
'Content-Type': 'application/json',
|
|
73
|
-
'x-api-key': apiKey,
|
|
74
|
-
},
|
|
75
|
-
body: JSON.stringify({ hookId }),
|
|
76
|
-
});
|
|
77
|
-
} catch (networkErr) {
|
|
78
|
-
throw new Error(`Network error contacting Pro API: ${networkErr.message}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (response.status === 401) {
|
|
82
|
-
throw new ProApiAuthError();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
const body = await response.text().catch(() => '');
|
|
87
|
-
throw new ProApiError(response.status, body);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const data = await response.json().catch(() => ({}));
|
|
91
|
-
|
|
92
|
-
logger.success(
|
|
93
|
-
pc.bold(
|
|
94
|
-
`[Pro] Hook registered: persists for 30 days, up to 500 requests stored.`,
|
|
95
|
-
),
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
return data;
|
|
99
|
-
}
|
|
100
|
-
|