@anonymilyhq/cli 1.0.2 → 1.0.4
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 +177 -19
- package/bin/cli.js +81 -13
- package/lib/config.js +70 -0
- package/lib/forwarder.js +41 -4
- package/lib/proRegister.js +100 -0
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -1,54 +1,212 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @anonymilyhq/cli
|
|
2
4
|
|
|
3
|
-
The official CLI for [Anonymily](https://anonymily.com)
|
|
5
|
+
**The official CLI for [Anonymily](https://anonymily.com) — forward webhooks from the cloud straight to your localhost.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@anonymilyhq/cli)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What is it?
|
|
16
|
+
|
|
17
|
+
The Anonymily CLI connects to your live webhook endpoint on `api.anonymily.com` via a Server-Sent Events (SSE) stream and forwards every incoming request — with the exact same method, headers, query params, and body — to a port on your local machine.
|
|
18
|
+
|
|
19
|
+
No tunnels, no sign-ups required to start. Just one command.
|
|
20
|
+
|
|
21
|
+
> **Pro tip:** Create a free account at [anonymily.com](https://anonymily.com) and claim your endpoint to keep the same URL across sessions and unlock 500 requests / 30-day history retention with a Pro subscription.
|
|
4
22
|
|
|
5
|
-
|
|
23
|
+
---
|
|
6
24
|
|
|
7
25
|
## Installation
|
|
8
26
|
|
|
9
|
-
|
|
27
|
+
**Run instantly via npx (no install required):**
|
|
28
|
+
```bash
|
|
29
|
+
npx @anonymilyhq/cli listen 3000
|
|
30
|
+
```
|
|
10
31
|
|
|
32
|
+
**Or install globally:**
|
|
11
33
|
```bash
|
|
12
34
|
npm install -g @anonymilyhq/cli
|
|
13
35
|
```
|
|
14
36
|
|
|
15
|
-
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
16
40
|
|
|
17
41
|
```bash
|
|
18
|
-
|
|
42
|
+
# Start listening — generates a random endpoint ID automatically
|
|
43
|
+
npx @anonymilyhq/cli listen 3000
|
|
44
|
+
|
|
45
|
+
# Output:
|
|
46
|
+
# 🚀 Anonymily CLI is running!
|
|
47
|
+
# Forwarding: https://api.anonymily.com/h/xk92bzte ➔ http://127.0.0.1:3000
|
|
48
|
+
# Waiting for requests to arrive...
|
|
19
49
|
```
|
|
20
50
|
|
|
21
|
-
|
|
51
|
+
Send a test payload:
|
|
52
|
+
```bash
|
|
53
|
+
curl -X POST https://api.anonymily.com/h/xk92bzte \
|
|
54
|
+
-H "Content-Type: application/json" \
|
|
55
|
+
-d '{"event": "payment.success"}'
|
|
56
|
+
```
|
|
22
57
|
|
|
23
|
-
|
|
58
|
+
You'll see in the CLI:
|
|
59
|
+
```
|
|
60
|
+
[12:01:45] ⚡ Incoming POST request...
|
|
61
|
+
└─ Forwarded to localhost | Status: 200
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
### `listen <port>`
|
|
69
|
+
|
|
70
|
+
Listen for incoming webhooks and forward them to a local port.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
anonymily listen <port> [options]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Option | Description |
|
|
77
|
+
|--------|-------------|
|
|
78
|
+
| `<port>` | **Required.** Local port to forward to (e.g. `3000`, `8080`) |
|
|
79
|
+
| `-i, --id <id>` | Use a custom, memorable endpoint name (e.g. `stripe-test`) |
|
|
80
|
+
| `--pro` | Register the hook as Pro-tier (legacy). Requires `--id` and a configured Pro API key |
|
|
24
81
|
|
|
82
|
+
**Examples:**
|
|
25
83
|
```bash
|
|
26
|
-
|
|
84
|
+
# Random endpoint ID (ephemeral)
|
|
85
|
+
npx @anonymilyhq/cli listen 3000
|
|
86
|
+
|
|
87
|
+
# Custom named endpoint → api.anonymily.com/h/stripe-dev
|
|
88
|
+
npx @anonymilyhq/cli listen 3000 --id stripe-dev
|
|
89
|
+
|
|
90
|
+
# Pro-tier named endpoint (legacy hook-based Pro)
|
|
91
|
+
npx @anonymilyhq/cli listen 3000 --id stripe-dev --pro
|
|
27
92
|
```
|
|
28
93
|
|
|
29
|
-
|
|
94
|
+
---
|
|
30
95
|
|
|
31
|
-
|
|
96
|
+
### `config set-key <key>`
|
|
97
|
+
|
|
98
|
+
Save your Pro API key locally so you don't need to export it every session.
|
|
32
99
|
|
|
33
100
|
```bash
|
|
34
|
-
anonymily
|
|
101
|
+
anonymily config set-key <your-pro-api-key>
|
|
35
102
|
```
|
|
36
103
|
|
|
37
|
-
|
|
104
|
+
The key is stored in `~/.anonymily/config.json`. You can also set it via environment variable:
|
|
38
105
|
|
|
39
106
|
```bash
|
|
40
|
-
|
|
107
|
+
export PRO_API_KEY=<your-pro-api-key>
|
|
41
108
|
```
|
|
42
109
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Account-Based Pro (Recommended)
|
|
113
|
+
|
|
114
|
+
The modern way to get Pro limits is via an **account subscription** at [anonymily.com/upgrade](https://anonymily.com/upgrade). This gives your entire account Pro limits (₹750/month) — no per-hook configuration needed.
|
|
115
|
+
|
|
116
|
+
**Workflow:**
|
|
117
|
+
1. Sign up at [anonymily.com](https://anonymily.com)
|
|
118
|
+
2. Subscribe to Pro at [anonymily.com/upgrade](https://anonymily.com/upgrade)
|
|
119
|
+
3. Open the dashboard, copy your endpoint URL, and claim it to your account
|
|
120
|
+
4. Run the CLI with the same hook ID:
|
|
121
|
+
```bash
|
|
122
|
+
npx @anonymilyhq/cli listen 3000 --id your-claimed-hook
|
|
123
|
+
```
|
|
124
|
+
5. All Pro limits apply automatically — 500 requests, 30-day history
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## How It Works
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
External Service (Stripe, GitHub, etc.)
|
|
132
|
+
│
|
|
133
|
+
│ POST https://api.anonymily.com/h/<hookId>
|
|
134
|
+
▼
|
|
135
|
+
Anonymily Backend (NestJS + Redis + Supabase)
|
|
136
|
+
│
|
|
137
|
+
│ SSE broadcast via /stream/<hookId>
|
|
138
|
+
▼
|
|
139
|
+
@anonymilyhq/cli ──forward──► http://localhost:<port>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
1. The CLI opens a persistent SSE connection to `/stream/<hookId>`
|
|
143
|
+
2. When a webhook arrives, the backend broadcasts it instantly
|
|
144
|
+
3. The CLI receives the event and re-issues the exact same HTTP request to your local port
|
|
145
|
+
4. Method, headers, query params, and body are all preserved 1:1
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Tier Comparison
|
|
150
|
+
|
|
151
|
+
| Feature | Free | Pro |
|
|
152
|
+
|---------|------|-----|
|
|
153
|
+
| Requests stored per hook | 50 | 500 |
|
|
154
|
+
| History retention | 24 hours | 30 days |
|
|
155
|
+
| Custom endpoint ID | ✅ | ✅ |
|
|
156
|
+
| Persistent history in dashboard | ✅ | ✅ |
|
|
157
|
+
| Price | Free | ₹750/month |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Testing Locally
|
|
162
|
+
|
|
163
|
+
A sample target server is included to test forwarding without a real local app:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
node sample-target-server.js
|
|
167
|
+
# Listens on http://localhost:4000 and logs all incoming requests
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Then in another terminal:
|
|
171
|
+
```bash
|
|
172
|
+
npx @anonymilyhq/cli listen 4000
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Troubleshooting
|
|
178
|
+
|
|
179
|
+
**`Connection refused` when forwarding:**
|
|
180
|
+
- Make sure your local server is actually running on the specified port
|
|
181
|
+
- Test with: `curl http://localhost:<port>`
|
|
182
|
+
|
|
183
|
+
**Requests arrive in CLI but local server returns errors:**
|
|
184
|
+
- Check the status code logged: `└─ Forwarded to localhost | Status: 500`
|
|
185
|
+
- Review your local server logs for the underlying cause
|
|
186
|
+
|
|
187
|
+
**CLI doesn't reconnect after network drop:**
|
|
188
|
+
- The CLI uses `eventsource` which auto-reconnects on drops
|
|
189
|
+
- You'll see: `[Network] Connection dropped. Automatically reconnecting...`
|
|
190
|
+
|
|
191
|
+
**SSE stream appears stuck (no events):**
|
|
192
|
+
- Confirm the hook ID exists: `curl https://api.anonymily.com/history/<hookId>`
|
|
193
|
+
- Ensure you're sending requests to the correct endpoint URL shown in the CLI output
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Environment Variables
|
|
198
|
+
|
|
199
|
+
| Variable | Description |
|
|
200
|
+
|----------|-------------|
|
|
201
|
+
| `PRO_API_KEY` | Legacy Pro API key (overrides `~/.anonymily/config.json`) |
|
|
202
|
+
| `ANONYMILY_API_URL` | Override the backend URL (default: `https://api.anonymily.com`) |
|
|
46
203
|
|
|
204
|
+
---
|
|
47
205
|
|
|
48
|
-
##
|
|
206
|
+
## Contributing
|
|
49
207
|
|
|
50
|
-
|
|
208
|
+
Issues and pull requests are welcome. Open an issue first to discuss any significant changes.
|
|
51
209
|
|
|
52
210
|
## License
|
|
53
211
|
|
|
54
|
-
MIT
|
|
212
|
+
MIT © [Anonymily](https://anonymily.com)
|
package/bin/cli.js
CHANGED
|
@@ -1,25 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander';
|
|
4
|
+
import pc from 'picocolors';
|
|
4
5
|
import { generateHookId } from '../lib/utils.js';
|
|
5
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
|
+
import { logger } from '../lib/logger.js';
|
|
6
15
|
|
|
7
16
|
// Point this to your live production backend URL
|
|
8
17
|
const API_URL = process.env.ANONYMILY_API_URL || 'https://api.anonymily.com';
|
|
9
18
|
|
|
10
19
|
program
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
.name('anonymily')
|
|
21
|
+
.description('Forward webhooks from Anonymily directly to your local machine.')
|
|
22
|
+
.version('1.0.3');
|
|
14
23
|
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// listen
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
15
27
|
program
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
.command('listen')
|
|
29
|
+
.description('Listen for incoming webhooks and forward them to a local port')
|
|
30
|
+
.argument('<port>', 'Local port to forward to (e.g., 8080)')
|
|
31
|
+
.option('-i, --id <id>', 'Provide a custom endpoint ID to listen to')
|
|
32
|
+
.option(
|
|
33
|
+
'--pro',
|
|
34
|
+
'Register the hook as Pro-tier (requires --id and PRO_API_KEY)',
|
|
35
|
+
)
|
|
36
|
+
.action(async (port, options) => {
|
|
37
|
+
// --pro requires --id
|
|
38
|
+
if (options.pro && !options.id) {
|
|
39
|
+
logger.error(
|
|
40
|
+
pc.bold(
|
|
41
|
+
'--pro requires --id to specify a persistent hook name.',
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hookId = options.id || generateHookId();
|
|
48
|
+
|
|
49
|
+
if (options.pro) {
|
|
50
|
+
try {
|
|
51
|
+
await registerProHook(API_URL, hookId);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (
|
|
54
|
+
err instanceof ProApiKeyMissingError ||
|
|
55
|
+
err instanceof ProApiAuthError ||
|
|
56
|
+
err instanceof ProApiError
|
|
57
|
+
) {
|
|
58
|
+
logger.error(err.message);
|
|
59
|
+
} else {
|
|
60
|
+
logger.error(`Unexpected error during Pro registration: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
startForwarding(API_URL, hookId, parseInt(port, 10));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// config
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
const configCmd = program
|
|
73
|
+
.command('config')
|
|
74
|
+
.description('Manage CLI configuration');
|
|
75
|
+
|
|
76
|
+
configCmd
|
|
77
|
+
.command('set-key <key>')
|
|
78
|
+
.description(
|
|
79
|
+
'Save your Pro API key to ~/.anonymily/config.json',
|
|
80
|
+
)
|
|
81
|
+
.action((key) => {
|
|
82
|
+
if (!key || !key.trim()) {
|
|
83
|
+
logger.error('API key cannot be empty.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
writeConfig({ proApiKey: key.trim() });
|
|
87
|
+
logger.success(
|
|
88
|
+
`Pro API key saved to ${CONFIG_PATH}`,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
program.parse();
|
|
93
|
+
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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/forwarder.js
CHANGED
|
@@ -2,9 +2,40 @@ import { EventSource as EventSourceConstructor } from 'eventsource';
|
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Serialize the body for the forwarded request.
|
|
7
|
+
*
|
|
8
|
+
* The backend stores the body as a parsed JSON object (even for non-JSON
|
|
9
|
+
* payloads, it gets wrapped). We need to re-serialize appropriately:
|
|
10
|
+
* - No body for GET/HEAD
|
|
11
|
+
* - null/undefined body → no body sent
|
|
12
|
+
* - Object/array body → JSON.stringify (with matching Content-Type)
|
|
13
|
+
* - Primitive (string/number) → convert to string (e.g. plain text bodies)
|
|
14
|
+
*/
|
|
15
|
+
function serializeBody(method, body, headers) {
|
|
16
|
+
if (['GET', 'HEAD'].includes(method)) return undefined;
|
|
17
|
+
if (body === null || body === undefined) return undefined;
|
|
18
|
+
if (typeof body === 'string') return body;
|
|
19
|
+
if (typeof body === 'object') return JSON.stringify(body);
|
|
20
|
+
return String(body);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure Content-Type is set for bodies that need it.
|
|
25
|
+
* If the original request had application/json, preserve it.
|
|
26
|
+
*/
|
|
27
|
+
function ensureContentType(headers, body) {
|
|
28
|
+
if (body === undefined || body === null) return headers;
|
|
29
|
+
if (headers['content-type']) return headers;
|
|
30
|
+
if (typeof body === 'object') {
|
|
31
|
+
return { ...headers, 'content-type': 'application/json' };
|
|
32
|
+
}
|
|
33
|
+
return headers;
|
|
34
|
+
}
|
|
35
|
+
|
|
5
36
|
export function startForwarding(apiUrl, hookId, port) {
|
|
6
37
|
const webhookUrl = `${apiUrl}/h/${hookId}`;
|
|
7
|
-
const localUrl = `http://
|
|
38
|
+
const localUrl = `http://127.0.0.1:${port}`;
|
|
8
39
|
|
|
9
40
|
logger.success(pc.bold(`\n🚀 Anonymily CLI is running!`));
|
|
10
41
|
logger.raw(`\nForwarding: ${pc.cyan(webhookUrl)} ➔ ${pc.cyan(localUrl)}`);
|
|
@@ -17,9 +48,15 @@ export function startForwarding(apiUrl, hookId, port) {
|
|
|
17
48
|
const reqData = JSON.parse(event.data);
|
|
18
49
|
const { method, headers, query, body } = reqData;
|
|
19
50
|
|
|
51
|
+
// Strip hop-by-hop headers that must not be forwarded
|
|
20
52
|
const forwardHeaders = { ...headers };
|
|
21
53
|
delete forwardHeaders.host;
|
|
22
54
|
delete forwardHeaders.connection;
|
|
55
|
+
delete forwardHeaders['content-length'];
|
|
56
|
+
delete forwardHeaders['transfer-encoding'];
|
|
57
|
+
|
|
58
|
+
const serializedBody = serializeBody(method, body, forwardHeaders);
|
|
59
|
+
const finalHeaders = ensureContentType(forwardHeaders, body);
|
|
23
60
|
|
|
24
61
|
const queryString = new URLSearchParams(query || {}).toString();
|
|
25
62
|
const fullLocalUrl = queryString ? `${localUrl}?${queryString}` : localUrl;
|
|
@@ -27,9 +64,9 @@ export function startForwarding(apiUrl, hookId, port) {
|
|
|
27
64
|
logger.warn(`[${new Date().toLocaleTimeString()}] ⚡ Incoming ${method} request...`);
|
|
28
65
|
|
|
29
66
|
const response = await fetch(fullLocalUrl, {
|
|
30
|
-
method
|
|
31
|
-
headers:
|
|
32
|
-
body:
|
|
67
|
+
method,
|
|
68
|
+
headers: finalHeaders,
|
|
69
|
+
body: serializedBody,
|
|
33
70
|
});
|
|
34
71
|
|
|
35
72
|
const statusColor = response.ok ? pc.green : pc.red;
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonymilyhq/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "CLI for Anonymily platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"anonymily": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node ./bin/cli.js"
|
|
10
|
+
"start": "node ./bin/cli.js",
|
|
11
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest --no-coverage",
|
|
12
|
+
"test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage"
|
|
11
13
|
},
|
|
12
14
|
"keywords": [
|
|
13
15
|
"cli",
|
|
@@ -27,5 +29,12 @@
|
|
|
27
29
|
"commander": "^14.0.3",
|
|
28
30
|
"eventsource": "^4.1.0",
|
|
29
31
|
"picocolors": "^1.1.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"jest": "^30.3.0"
|
|
35
|
+
},
|
|
36
|
+
"jest": {
|
|
37
|
+
"testEnvironment": "node",
|
|
38
|
+
"transform": {}
|
|
30
39
|
}
|
|
31
|
-
}
|
|
40
|
+
}
|