@anonymilyhq/cli 1.2.0 → 1.3.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 +84 -110
- package/bin/cli.js +169 -2
- package/lib/forwarder.js +200 -26
- package/lib/trigger.js +76 -0
- package/lib/utils.js +6 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -14,17 +14,19 @@
|
|
|
14
14
|
|
|
15
15
|
## What is it?
|
|
16
16
|
|
|
17
|
-
The Anonymily CLI connects to your
|
|
17
|
+
The Anonymily CLI connects to your webhook endpoint 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. It also captures your server's response and streams it back to the dashboard in real time, so you can see status codes, latency, and error bodies without leaving the UI.
|
|
18
18
|
|
|
19
|
-
**No
|
|
19
|
+
**No account required for the free tier. One command.**
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
```bash
|
|
22
|
+
npx @anonymilyhq/cli listen 3000
|
|
23
|
+
```
|
|
22
24
|
|
|
23
25
|
---
|
|
24
26
|
|
|
25
27
|
## Installation
|
|
26
28
|
|
|
27
|
-
**Run instantly via npx (no install
|
|
29
|
+
**Run instantly via npx (no install):**
|
|
28
30
|
```bash
|
|
29
31
|
npx @anonymilyhq/cli listen 3000
|
|
30
32
|
```
|
|
@@ -38,31 +40,26 @@ npm install -g @anonymilyhq/cli
|
|
|
38
40
|
|
|
39
41
|
## Quick Start
|
|
40
42
|
|
|
41
|
-
**No account or signup required** — works immediately with free tier (random ID, 50 requests, 24h retention).
|
|
42
|
-
|
|
43
43
|
```bash
|
|
44
|
-
# Start listening — generates a random 8-char endpoint ID automatically
|
|
45
44
|
npx @anonymilyhq/cli listen 3000
|
|
46
45
|
|
|
47
46
|
# Output:
|
|
48
|
-
#
|
|
49
|
-
# Forwarding: https://api.anonymily.com/h/xk92bzte
|
|
50
|
-
# Tier:
|
|
51
|
-
#
|
|
52
|
-
# Waiting for requests to arrive...
|
|
47
|
+
# Anonymily CLI is running!
|
|
48
|
+
# Forwarding: https://api.anonymily.com/h/xk92bzte → http://127.0.0.1:3000
|
|
49
|
+
# Tier: FREE | Storage: 0/200 | Retention: 48 hours
|
|
50
|
+
# Waiting for requests...
|
|
53
51
|
```
|
|
54
52
|
|
|
55
53
|
Send a test payload:
|
|
56
54
|
```bash
|
|
57
55
|
curl -X POST https://api.anonymily.com/h/xk92bzte \
|
|
58
56
|
-H "Content-Type: application/json" \
|
|
59
|
-
-d '{"event": "payment.
|
|
57
|
+
-d '{"event": "payment.succeeded", "amount": 9900}'
|
|
60
58
|
```
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
CLI output:
|
|
63
61
|
```
|
|
64
|
-
[12:01:45]
|
|
65
|
-
└─ Forwarded to localhost | Status: 200
|
|
62
|
+
[12:01:45] Incoming POST → localhost:3000 | 200 OK | 11ms
|
|
66
63
|
```
|
|
67
64
|
|
|
68
65
|
---
|
|
@@ -71,170 +68,147 @@ You'll see in the CLI:
|
|
|
71
68
|
|
|
72
69
|
### `listen <port>`
|
|
73
70
|
|
|
74
|
-
Listen for incoming webhooks and forward them to
|
|
71
|
+
Listen for incoming webhooks and forward them to your local port.
|
|
75
72
|
|
|
76
73
|
```bash
|
|
77
|
-
|
|
74
|
+
npx @anonymilyhq/cli listen <port> [options]
|
|
78
75
|
```
|
|
79
76
|
|
|
80
77
|
| Option | Description |
|
|
81
|
-
|
|
82
|
-
| `<port>` | **Required.** Local port to forward to (e.g. `3000
|
|
83
|
-
| `-i, --id <id>` | **Pro
|
|
78
|
+
|---|---|
|
|
79
|
+
| `<port>` | **Required.** Local port to forward to (e.g. `3000`) |
|
|
80
|
+
| `-i, --id <id>` | **Pro.** Use a named endpoint ID (e.g. `stripe-test`) instead of a random one. |
|
|
81
|
+
| `-t, --token <token>` | **Pro.** Personal Access Token — authenticates to claimed endpoints. Also readable from `ANONYMILY_TOKEN` env var. |
|
|
84
82
|
|
|
85
83
|
**Examples:**
|
|
84
|
+
|
|
86
85
|
```bash
|
|
87
|
-
#
|
|
86
|
+
# Free tier — random endpoint, no auth
|
|
88
87
|
npx @anonymilyhq/cli listen 3000
|
|
89
88
|
|
|
90
|
-
#
|
|
91
|
-
npx @anonymilyhq/cli listen 3000 --id stripe-dev
|
|
89
|
+
# Pro — named endpoint with PAT
|
|
90
|
+
npx @anonymilyhq/cli listen 3000 --id stripe-dev --token pat_example123
|
|
92
91
|
|
|
93
|
-
#
|
|
94
|
-
|
|
92
|
+
# PAT via env var
|
|
93
|
+
export ANONYMILY_TOKEN=pat_example123
|
|
94
|
+
npx @anonymilyhq/cli listen 3000 --id stripe-dev
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
### `replay <hookId> <requestId>`
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Submit feedback about the Anonymily CLI directly from your terminal.
|
|
99
|
+
Re-fire a previously captured request back through your hook endpoint. Requires a PAT and Pro subscription (free users are rate-limited to 5 replays/day, enforced server-side).
|
|
102
100
|
|
|
103
101
|
```bash
|
|
104
|
-
|
|
102
|
+
npx @anonymilyhq/cli replay <hookId> <requestId> [options]
|
|
105
103
|
```
|
|
106
104
|
|
|
107
|
-
|
|
|
108
|
-
|
|
109
|
-
| `<
|
|
110
|
-
|
|
|
105
|
+
| Option | Description |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `<hookId>` | **Required.** The hook ID that captured the request. |
|
|
108
|
+
| `<requestId>` | **Required.** The request ID to replay (visible in the dashboard). |
|
|
109
|
+
| `-t, --token <token>` | **Required.** PAT — or set `ANONYMILY_TOKEN`. |
|
|
110
|
+
| `-m, --method <method>` | Override the HTTP method (e.g. `POST`, `GET`). |
|
|
111
|
+
| `-b, --body <json>` | Override the request body (JSON string). |
|
|
112
|
+
| `--resign` | Re-sign the payload using the hook's stored secret. |
|
|
111
113
|
|
|
112
114
|
**Examples:**
|
|
113
|
-
```bash
|
|
114
|
-
# Quick rating
|
|
115
|
-
anonymily feedback 5
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
```bash
|
|
117
|
+
# Basic replay
|
|
118
|
+
npx @anonymilyhq/cli replay stripe-dev abc-123 --token pat_example123
|
|
119
119
|
|
|
120
|
-
#
|
|
121
|
-
|
|
120
|
+
# Replay with body override and re-signing
|
|
121
|
+
npx @anonymilyhq/cli replay stripe-dev abc-123 \
|
|
122
|
+
--body '{"amount":5000}' --resign --token pat_example123
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
## Upgrading to Pro (₹750/month)
|
|
129
|
-
|
|
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)
|
|
125
|
+
### `feedback <rating> [message]`
|
|
135
126
|
|
|
136
|
-
|
|
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:
|
|
141
|
-
```bash
|
|
142
|
-
npx @anonymilyhq/cli listen 3000 --id my-webhook
|
|
143
|
-
```
|
|
144
|
-
5. Enjoy Pro limits — 500 requests, 30-day history
|
|
127
|
+
Submit feedback from the terminal.
|
|
145
128
|
|
|
146
|
-
|
|
129
|
+
```bash
|
|
130
|
+
npx @anonymilyhq/cli feedback 5 "Works perfectly with Shopify webhooks"
|
|
131
|
+
```
|
|
147
132
|
|
|
148
133
|
---
|
|
149
134
|
|
|
150
135
|
## How It Works
|
|
151
136
|
|
|
152
137
|
```
|
|
153
|
-
External Service (Stripe, GitHub,
|
|
138
|
+
External Service (Stripe, GitHub, Shopify, …)
|
|
154
139
|
│
|
|
155
140
|
│ POST https://api.anonymily.com/h/<hookId>
|
|
156
141
|
▼
|
|
157
|
-
Anonymily Backend
|
|
142
|
+
Anonymily Backend — captures, verifies signature, stores in Redis
|
|
158
143
|
│
|
|
159
144
|
│ SSE broadcast via /stream/<hookId>
|
|
160
145
|
▼
|
|
161
146
|
@anonymilyhq/cli ──forward──► http://localhost:<port>
|
|
147
|
+
│
|
|
148
|
+
│ Response (status, latency, body) streamed back to dashboard
|
|
149
|
+
▼
|
|
150
|
+
Anonymily Dashboard — shows full exchange in real time
|
|
162
151
|
```
|
|
163
152
|
|
|
164
|
-
1. The CLI opens a persistent SSE connection to
|
|
165
|
-
2. When a webhook arrives,
|
|
166
|
-
3. The CLI
|
|
167
|
-
4.
|
|
153
|
+
1. The CLI opens a persistent SSE connection to the backend.
|
|
154
|
+
2. When a webhook arrives, it is broadcast instantly.
|
|
155
|
+
3. The CLI re-issues the exact HTTP request to your local port, preserving method, headers, query params, and body.
|
|
156
|
+
4. The local response (status code, latency, body) is captured and streamed back to the dashboard so you can see the full exchange without leaving the UI.
|
|
168
157
|
|
|
169
158
|
---
|
|
170
159
|
|
|
171
|
-
##
|
|
160
|
+
## Tiers
|
|
172
161
|
|
|
173
162
|
| Feature | Free | Pro |
|
|
174
|
-
|
|
175
|
-
| Endpoint ID
|
|
176
|
-
| Requests
|
|
177
|
-
| History retention |
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
|
|
182
|
-
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| Endpoint ID | Random 8-char | Custom named (`--id`) |
|
|
165
|
+
| Requests per hook | 200 | 2,000 |
|
|
166
|
+
| History retention | 48 hours | 30 days |
|
|
167
|
+
| Named (persistent) endpoints | — | Unlimited |
|
|
168
|
+
| Replay (`replay` command) | 5/day | Unlimited |
|
|
169
|
+
| Signature verification UI | — | ✓ |
|
|
170
|
+
| AI diagnosis + handler gen | — | ✓ (dashboard) |
|
|
171
|
+
| AI edge-case events | — | ✓ (dashboard) |
|
|
172
|
+
| Price | Free | $9 / ₹750 per month |
|
|
173
|
+
|
|
174
|
+
Upgrade at [anonymily.com/upgrade](https://anonymily.com/upgrade).
|
|
183
175
|
|
|
184
176
|
---
|
|
185
177
|
|
|
186
|
-
##
|
|
187
|
-
|
|
188
|
-
A sample target server is included to test forwarding without a real local app:
|
|
178
|
+
## MCP Server
|
|
189
179
|
|
|
190
|
-
|
|
191
|
-
node sample-target-server.js
|
|
192
|
-
# Listens on http://localhost:4000 and logs all incoming requests
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Then in another terminal:
|
|
196
|
-
```bash
|
|
197
|
-
npx @anonymilyhq/cli listen 4000
|
|
198
|
-
```
|
|
180
|
+
If you use Claude Code, you can drive Anonymily entirely from your AI assistant using the `@anonymilyhq/mcp-server` package. See the [MCP Server README](../../packages/mcp-server/README.md) for setup.
|
|
199
181
|
|
|
200
182
|
---
|
|
201
183
|
|
|
202
184
|
## Troubleshooting
|
|
203
185
|
|
|
204
186
|
**`Connection refused` when forwarding:**
|
|
205
|
-
|
|
206
|
-
- Test with: `curl http://localhost:<port>`
|
|
187
|
+
Ensure your local server is running on the specified port. Test with `curl http://localhost:<port>`.
|
|
207
188
|
|
|
208
|
-
**Requests arrive
|
|
209
|
-
|
|
210
|
-
- Review your local server logs for the underlying cause
|
|
189
|
+
**Requests arrive but server returns errors:**
|
|
190
|
+
Check your server logs. The CLI shows `Status: 5xx` — look at the response body in the dashboard for the error detail.
|
|
211
191
|
|
|
212
192
|
**CLI doesn't reconnect after network drop:**
|
|
213
|
-
|
|
214
|
-
|
|
193
|
+
The CLI uses `eventsource` which auto-reconnects. You'll see: `Connection dropped. Reconnecting...`
|
|
194
|
+
|
|
195
|
+
**401 on stream connection:**
|
|
196
|
+
Named endpoints require a valid PAT. Pass `--token <your-pat>` or set `ANONYMILY_TOKEN`.
|
|
215
197
|
|
|
216
|
-
**
|
|
217
|
-
|
|
218
|
-
- Ensure you're sending requests to the correct endpoint URL shown in the CLI output
|
|
198
|
+
**403 when using `--id` on a free account:**
|
|
199
|
+
Custom named endpoint IDs (`--id`) are a Pro feature. Remove `--id` to use a random free endpoint, or upgrade at [anonymily.com/upgrade](https://anonymily.com/upgrade).
|
|
219
200
|
|
|
220
201
|
---
|
|
221
202
|
|
|
222
203
|
## Environment Variables
|
|
223
204
|
|
|
224
205
|
| Variable | Description |
|
|
225
|
-
|
|
206
|
+
|---|---|
|
|
207
|
+
| `ANONYMILY_TOKEN` | Personal Access Token for named endpoint authentication |
|
|
226
208
|
| `ANONYMILY_API_URL` | Override the backend URL (default: `https://api.anonymily.com`) |
|
|
227
209
|
|
|
228
210
|
---
|
|
229
211
|
|
|
230
|
-
## Changelog
|
|
231
|
-
|
|
232
|
-
See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes.
|
|
233
|
-
|
|
234
|
-
## Contributing
|
|
235
|
-
|
|
236
|
-
Issues and pull requests are welcome. Open an issue first to discuss any significant changes.
|
|
237
|
-
|
|
238
212
|
## License
|
|
239
213
|
|
|
240
214
|
MIT © [Anonymily](https://anonymily.com)
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { generateHookId } from '../lib/utils.js';
|
|
|
6
6
|
import { startForwarding } from '../lib/forwarder.js';
|
|
7
7
|
import { logger } from '../lib/logger.js';
|
|
8
8
|
import { submitFeedback } from '../lib/feedback.js';
|
|
9
|
+
import { listEvents, fireEvent, printEventList } from '../lib/trigger.js';
|
|
9
10
|
|
|
10
11
|
// Point this to your live production backend URL
|
|
11
12
|
const API_URL = process.env.ANONYMILY_API_URL || 'https://api.anonymily.com';
|
|
@@ -22,13 +23,179 @@ program
|
|
|
22
23
|
.command('listen')
|
|
23
24
|
.description('Listen for incoming webhooks and forward them to a local port')
|
|
24
25
|
.argument('<port>', 'Local port to forward to (e.g., 8080)')
|
|
25
|
-
.option('-i, --id <id>', '[Pro
|
|
26
|
+
.option('-i, --id <id>', '[Pro] Use a custom named endpoint ID (requires Pro subscription — anonymily.com/upgrade)')
|
|
27
|
+
// CLI-001: Accept a Personal Access Token for authenticating Pro hook streams
|
|
28
|
+
.option('-t, --token <token>', '[Pro only] Personal Access Token for authenticating to private hooks (or set ANONYMILY_TOKEN env var)')
|
|
26
29
|
.action(async (port, options) => {
|
|
27
30
|
const hookId = options.id || generateHookId();
|
|
28
|
-
|
|
31
|
+
// CLI-001: Resolve token from --token flag or ANONYMILY_TOKEN env var
|
|
32
|
+
const token = options.token || process.env.ANONYMILY_TOKEN || null;
|
|
33
|
+
startForwarding(API_URL, hookId, parseInt(port, 10), token);
|
|
29
34
|
});
|
|
30
35
|
|
|
31
36
|
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// trigger
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
program
|
|
41
|
+
.command('trigger')
|
|
42
|
+
.description('[Pro] Fire a synthetic provider event at your webhook endpoint')
|
|
43
|
+
.argument('[provider]', 'Provider name (e.g. github, stripe, shopify, razorpay, slack)')
|
|
44
|
+
.argument('[event]', 'Event name (e.g. push, payment_intent.succeeded, order.created)')
|
|
45
|
+
.option('-i, --hook <hookId>', 'Target hook ID (defaults to a random ID)')
|
|
46
|
+
.option('-t, --token <token>', 'Personal Access Token (or set ANONYMILY_TOKEN env var)')
|
|
47
|
+
.option('-l, --list', 'List all available provider/event combinations and exit')
|
|
48
|
+
.action(async (provider, event, options) => {
|
|
49
|
+
const token = options.token || process.env.ANONYMILY_TOKEN || null;
|
|
50
|
+
|
|
51
|
+
// --list: show all available events
|
|
52
|
+
if (options.list || (!provider && !event)) {
|
|
53
|
+
const events = await listEvents(API_URL);
|
|
54
|
+
if (events) printEventList(events);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!provider || !event) {
|
|
59
|
+
logger.error('Usage: anonymily trigger <provider> <event> --hook <hookId> [--token <pat>]');
|
|
60
|
+
logger.dim(' anonymily trigger --list');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hookId = options.hook || null;
|
|
65
|
+
if (!hookId) {
|
|
66
|
+
logger.error('Specify a hook ID with --hook <hookId>. Example: --hook abc12345');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!token) {
|
|
71
|
+
logger.error('A Personal Access Token is required for the trigger command.');
|
|
72
|
+
logger.dim(' Use --token <pat> or set the ANONYMILY_TOKEN environment variable.');
|
|
73
|
+
logger.dim(' Generate a PAT from your dashboard at https://anonymily.com/account');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logger.dim(`Firing ${pc.bold(provider)} ${pc.bold(event)} at ${pc.cyan(`${API_URL}/h/${hookId}`)}...`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await fireEvent(API_URL, hookId, provider, event, token);
|
|
81
|
+
logger.success(`\n✅ Event fired successfully!`);
|
|
82
|
+
logger.raw(` Provider: ${pc.bold(provider)}`);
|
|
83
|
+
logger.raw(` Event: ${pc.bold(event)}`);
|
|
84
|
+
logger.raw(` Hook: ${pc.cyan(`${API_URL}/h/${hookId}`)}`);
|
|
85
|
+
if (result.signatureAdded) {
|
|
86
|
+
logger.raw(` Signature: ${pc.green('✓ computed and attached')}`);
|
|
87
|
+
} else {
|
|
88
|
+
logger.dim(` Signature: not added (no signing secret configured for this hook)`);
|
|
89
|
+
}
|
|
90
|
+
logger.raw('');
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logger.error(`\n❌ Failed to fire event: ${err.message}`);
|
|
93
|
+
if (err.message?.includes('Pro')) {
|
|
94
|
+
logger.dim(' Upgrade to Pro at https://anonymily.com/upgrade to use the trigger command.');
|
|
95
|
+
}
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// replay
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
program
|
|
104
|
+
.command('replay')
|
|
105
|
+
.description('[Pro] Re-fire a previously captured request back through your hook endpoint')
|
|
106
|
+
.argument('<hookId>', 'The hook ID that captured the request')
|
|
107
|
+
.argument('<requestId>', 'The request ID to replay (from the dashboard or anonymily requests <hookId>)')
|
|
108
|
+
.option('-t, --token <token>', 'Personal Access Token (or set ANONYMILY_TOKEN env var)')
|
|
109
|
+
.option('-m, --method <method>', 'Override the HTTP method (e.g. POST, GET)')
|
|
110
|
+
.option('-b, --body <json>', 'Override the request body (JSON string)')
|
|
111
|
+
.option('--resign', "Re-sign the payload with the hook's stored secret")
|
|
112
|
+
.action(async (hookId, requestId, options) => {
|
|
113
|
+
const token = options.token || process.env.ANONYMILY_TOKEN || null;
|
|
114
|
+
if (!token) {
|
|
115
|
+
logger.error('A Personal Access Token is required for the replay command.');
|
|
116
|
+
logger.dim(' Use --token <pat> or set the ANONYMILY_TOKEN environment variable.');
|
|
117
|
+
logger.dim(' Generate a PAT from your dashboard at https://anonymily.com/account');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let body;
|
|
122
|
+
if (options.body) {
|
|
123
|
+
try {
|
|
124
|
+
body = JSON.parse(options.body);
|
|
125
|
+
} catch {
|
|
126
|
+
logger.error("--body must be valid JSON. Example: --body '{\"event\":\"payment\"}'");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.dim(`Replaying request ${pc.bold(requestId)} on hook ${pc.cyan(`${API_URL}/h/${hookId}`)}...`);
|
|
132
|
+
|
|
133
|
+
// BUG-022: Look up the original captured request so we can replay its actual
|
|
134
|
+
// method/body/headers. Without this, the replay POST body was empty.
|
|
135
|
+
let originalReq = null;
|
|
136
|
+
try {
|
|
137
|
+
const reqRes = await fetch(`${API_URL}/v1/hooks/${hookId}/requests/${requestId}`, {
|
|
138
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
139
|
+
});
|
|
140
|
+
if (reqRes.ok) {
|
|
141
|
+
originalReq = await reqRes.json();
|
|
142
|
+
} else {
|
|
143
|
+
logger.warn(`⚠️ Could not load original request (${reqRes.status}) — replay may have empty body.`);
|
|
144
|
+
}
|
|
145
|
+
} catch (fetchErr) {
|
|
146
|
+
logger.warn(`⚠️ Could not load original request: ${fetchErr.message} — replay may have empty body.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// CLI flags override stored values; stored values serve as defaults.
|
|
150
|
+
const replayMethod = options.method ?? originalReq?.method ?? undefined;
|
|
151
|
+
const replayBody = body !== undefined ? body : (originalReq?.body ?? undefined);
|
|
152
|
+
const replayHeaders = originalReq?.headers ?? undefined;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(`${API_URL}/v1/hooks/${hookId}/replay`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
Authorization: `Bearer ${token}`,
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
...(replayMethod ? { method: replayMethod } : {}),
|
|
163
|
+
...(replayBody !== undefined ? { body: replayBody } : {}),
|
|
164
|
+
...(replayHeaders ? { headers: replayHeaders } : {}),
|
|
165
|
+
resign: !!options.resign,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (res.status === 401) {
|
|
170
|
+
logger.error('❌ Unauthorized: Invalid or expired Personal Access Token.');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
if (res.status === 403) {
|
|
174
|
+
const data = await res.json().catch(() => ({}));
|
|
175
|
+
logger.error(`❌ ${data.message ?? 'Free tier allows 5 replays per day. Upgrade to Pro for unlimited replays.'}`);
|
|
176
|
+
logger.dim(' Upgrade at https://anonymily.com/upgrade');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const data = await res.json().catch(() => ({}));
|
|
181
|
+
logger.error(`❌ Replay failed: ${data.message ?? res.statusText}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result = await res.json();
|
|
186
|
+
logger.success(`\n✅ Request replayed successfully!`);
|
|
187
|
+
logger.raw(` Hook: ${pc.cyan(`${API_URL}/h/${hookId}`)}`);
|
|
188
|
+
logger.raw(` Request: ${requestId}`);
|
|
189
|
+
if (result.signatureAdded) {
|
|
190
|
+
logger.raw(` Signature: ${pc.green('✓ re-signed with hook secret')}`);
|
|
191
|
+
}
|
|
192
|
+
logger.raw('');
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger.error(`\n❌ Failed to replay request: ${err.message}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
32
199
|
// ---------------------------------------------------------------------------
|
|
33
200
|
// feedback
|
|
34
201
|
// ---------------------------------------------------------------------------
|
package/lib/forwarder.js
CHANGED
|
@@ -33,56 +33,163 @@ function ensureContentType(headers, body) {
|
|
|
33
33
|
return headers;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* P1-05: Fetch the stable signing secret for a claimed hook.
|
|
38
|
+
* Requires a valid PAT (token). Returns null on any error.
|
|
39
|
+
*/
|
|
40
|
+
async function fetchSigningSecret(apiUrl, hookId, token) {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(`${apiUrl}/users/hooks/${hookId}/secret`, {
|
|
43
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) return null;
|
|
46
|
+
const { secret } = await res.json();
|
|
47
|
+
return secret ?? null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
/**
|
|
37
54
|
* Fetch tier information for a hook ID from the backend.
|
|
55
|
+
* Limits (maxRequests, retention) are derived from the live /pricing endpoint
|
|
56
|
+
* so the CLI stays in sync with server-side config without a reinstall.
|
|
38
57
|
*/
|
|
39
|
-
async function fetchTierInfo(apiUrl, hookId) {
|
|
58
|
+
async function fetchTierInfo(apiUrl, hookId, token) {
|
|
40
59
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
// 1. Fetch hook history for current request count + tier.
|
|
61
|
+
// Custom hooks require auth after BUG-003 fix.
|
|
62
|
+
const historyHeaders = { 'Content-Type': 'application/json' };
|
|
63
|
+
if (token) historyHeaders['Authorization'] = `Bearer ${token}`;
|
|
64
|
+
|
|
65
|
+
const historyRes = await fetch(`${apiUrl}/history/${hookId}`, { headers: historyHeaders });
|
|
66
|
+
if (!historyRes.ok) return null;
|
|
67
|
+
const data = await historyRes.json();
|
|
68
|
+
|
|
69
|
+
// History returns a plain array; _tier is written per-item by the API.
|
|
70
|
+
const requestCount = Array.isArray(data) ? data.length : 0;
|
|
71
|
+
let tier = (Array.isArray(data) && data.length > 0 && data[0]._tier) || 'free';
|
|
72
|
+
|
|
73
|
+
// When history is empty, _tier can't be read from requests — fall back to /users/me
|
|
74
|
+
if (tier === 'free' && token && requestCount === 0) {
|
|
75
|
+
try {
|
|
76
|
+
const meRes = await fetch(`${apiUrl}/users/me`, {
|
|
77
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
78
|
+
});
|
|
79
|
+
if (meRes.ok) {
|
|
80
|
+
const me = await meRes.json();
|
|
81
|
+
if (me.isPro) tier = 'pro';
|
|
82
|
+
}
|
|
83
|
+
} catch { /* keep 'free' as safe fallback */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Fetch live limits from /pricing endpoint
|
|
87
|
+
let maxRequests = tier === 'pro' ? 2000 : 200; // conservative fallback
|
|
88
|
+
let retention = tier === 'pro' ? '30 days' : '48 hours';
|
|
89
|
+
let proMaxRequests = 2000; // used in upgrade messaging
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const pricingRes = await fetch(`${apiUrl}/pricing`);
|
|
93
|
+
if (pricingRes.ok) {
|
|
94
|
+
const pricing = await pricingRes.json();
|
|
95
|
+
proMaxRequests = pricing.pro?.features?.requestCap ?? proMaxRequests;
|
|
96
|
+
if (tier === 'pro') {
|
|
97
|
+
maxRequests = proMaxRequests;
|
|
98
|
+
const hours = pricing.pro?.features?.retentionHours ?? 720;
|
|
99
|
+
retention = `${hours / 24} days`;
|
|
100
|
+
} else {
|
|
101
|
+
maxRequests = pricing.free?.features?.requestCap ?? maxRequests;
|
|
102
|
+
const hours = pricing.free?.features?.retentionHours ?? 48;
|
|
103
|
+
retention = `${hours} hours`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch { /* silently keep fallback values */ }
|
|
107
|
+
|
|
108
|
+
return { tier, requestCount, maxRequests, retention, proMaxRequests };
|
|
51
109
|
} catch {
|
|
52
110
|
return null;
|
|
53
111
|
}
|
|
54
112
|
}
|
|
55
113
|
|
|
56
|
-
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* CLI-001: Build EventSource constructor options from an optional PAT.
|
|
117
|
+
*
|
|
118
|
+
* Returns an options object with an `Authorization: Bearer <token>` header
|
|
119
|
+
* when `token` is a non-empty string, or an empty object otherwise.
|
|
120
|
+
* Exported for unit testing.
|
|
121
|
+
*
|
|
122
|
+
* @param {string|null} token - Personal Access Token, or null/undefined.
|
|
123
|
+
* @returns {object} EventSource options (may have a `headers` field).
|
|
124
|
+
*/
|
|
125
|
+
export function buildEventSourceOptions(token) {
|
|
126
|
+
if (token) {
|
|
127
|
+
return { headers: { Authorization: `Bearer ${token}` } };
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* CLI-001: Start forwarding webhooks from the Anonymily SSE stream to a local port.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} apiUrl - Base URL of the Anonymily backend API.
|
|
136
|
+
* @param {string} hookId - The webhook endpoint ID to listen on.
|
|
137
|
+
* @param {number} port - Local port to forward received requests to.
|
|
138
|
+
* @param {string|null} token - Optional Personal Access Token for Pro hook authentication.
|
|
139
|
+
* When provided, sent as `Authorization: Bearer <token>`.
|
|
140
|
+
* Required for privately-claimed Pro hooks; optional for public hooks.
|
|
141
|
+
*/
|
|
142
|
+
export async function startForwarding(apiUrl, hookId, port, token = null) {
|
|
57
143
|
const webhookUrl = `${apiUrl}/h/${hookId}`;
|
|
58
144
|
const localUrl = `http://127.0.0.1:${port}`;
|
|
59
145
|
|
|
60
146
|
logger.success(pc.bold(`\n🚀 Anonymily CLI is running!`));
|
|
61
147
|
logger.raw(`\nForwarding: ${pc.cyan(webhookUrl)} ➔ ${pc.cyan(localUrl)}`);
|
|
62
148
|
|
|
149
|
+
// CLI-001: Inform user about authentication status
|
|
150
|
+
if (token) {
|
|
151
|
+
logger.dim(`🔐 Authenticated with Personal Access Token`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// P1-05: Display stable signing secret for claimed (custom) hooks
|
|
155
|
+
const isCustomEndpoint = !/^[a-z0-9]{8}$/.test(hookId);
|
|
156
|
+
if (token && isCustomEndpoint) {
|
|
157
|
+
const secret = await fetchSigningSecret(apiUrl, hookId, token);
|
|
158
|
+
if (secret) {
|
|
159
|
+
logger.raw(`\n${pc.bold('Signing Secret')} (use in your app to verify requests):`);
|
|
160
|
+
logger.raw(` ${pc.cyan(`whsec_${secret}`)}\n`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
63
164
|
// Fetch and display tier information
|
|
64
|
-
const tierInfo = await fetchTierInfo(apiUrl, hookId);
|
|
165
|
+
const tierInfo = await fetchTierInfo(apiUrl, hookId, token);
|
|
65
166
|
if (tierInfo) {
|
|
66
167
|
const tierColor = tierInfo.tier === 'pro' ? pc.green : pc.yellow;
|
|
67
168
|
const tierBadge = tierInfo.tier === 'pro' ? '✨ PRO' : '🆓 FREE';
|
|
68
169
|
logger.raw(`Tier: ${tierColor(pc.bold(tierBadge))} | Storage: ${tierInfo.requestCount}/${tierInfo.maxRequests} | Retention: ${tierInfo.retention}`);
|
|
69
170
|
|
|
70
171
|
if (tierInfo.tier === 'free') {
|
|
71
|
-
logger.dim(`\n💡 Upgrade to Pro for custom endpoint IDs +
|
|
172
|
+
logger.dim(`\n💡 Upgrade to Pro for custom endpoint IDs + ${tierInfo.maxRequests} req/${tierInfo.retention} at https://anonymily.com/upgrade`);
|
|
72
173
|
}
|
|
73
174
|
}
|
|
74
175
|
|
|
75
176
|
logger.dim(`Waiting for requests to arrive...`);
|
|
76
177
|
logger.dim(`💬 Enjoying the CLI? Run 'anonymily feedback 5' to let us know!\n`);
|
|
77
178
|
|
|
78
|
-
|
|
179
|
+
// CLI-001: Build EventSource options — attach Authorization header when token is provided
|
|
180
|
+
const esOptions = buildEventSourceOptions(token);
|
|
181
|
+
|
|
182
|
+
const es = new EventSourceConstructor(`${apiUrl}/stream/${hookId}`, esOptions);
|
|
79
183
|
|
|
80
184
|
let requestCount = tierInfo?.requestCount || 0;
|
|
81
185
|
const maxRequests = tierInfo?.maxRequests || 50;
|
|
186
|
+
const proMaxRequests = tierInfo?.proMaxRequests ?? 2000;
|
|
82
187
|
|
|
83
188
|
es.onmessage = async (event) => {
|
|
84
189
|
try {
|
|
85
190
|
const reqData = JSON.parse(event.data);
|
|
191
|
+
// Skip internal SSE events (e.g. response_update from P1-03 capture)
|
|
192
|
+
if (reqData._type) return;
|
|
86
193
|
const { method, headers, query, body } = reqData;
|
|
87
194
|
|
|
88
195
|
// Strip hop-by-hop headers that must not be forwarded
|
|
@@ -105,30 +212,97 @@ export async function startForwarding(apiUrl, hookId, port) {
|
|
|
105
212
|
|
|
106
213
|
logger.warn(`[${new Date().toLocaleTimeString()}] ⚡ Incoming ${method} request ${progressColor(progress)}...`);
|
|
107
214
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
215
|
+
const t0 = Date.now();
|
|
216
|
+
let response;
|
|
217
|
+
let captureError = null;
|
|
218
|
+
try {
|
|
219
|
+
response = await fetch(fullLocalUrl, {
|
|
220
|
+
method,
|
|
221
|
+
headers: finalHeaders,
|
|
222
|
+
body: serializedBody,
|
|
223
|
+
signal: AbortSignal.timeout(30_000),
|
|
224
|
+
});
|
|
225
|
+
} catch (fetchErr) {
|
|
226
|
+
captureError = fetchErr.name === 'TimeoutError' ? 'timeout' : 'unreachable';
|
|
227
|
+
}
|
|
228
|
+
const latencyMs = Date.now() - t0;
|
|
229
|
+
|
|
230
|
+
if (captureError) {
|
|
231
|
+
logger.error(` └─ ${captureError === 'timeout' ? 'Timeout' : 'Unreachable'} after ${latencyMs}ms. Make sure your local server is running on port ${port}\n`);
|
|
232
|
+
// P1-03: report connection failure
|
|
233
|
+
if (reqData.id) {
|
|
234
|
+
fetch(`${apiUrl}/response/${reqData.id}`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
237
|
+
body: JSON.stringify({ error: captureError, latencyMs }),
|
|
238
|
+
}).catch(() => {});
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
const statusColor = response.ok ? pc.green : pc.red;
|
|
242
|
+
logger.raw(` └─ Forwarded to localhost | Status: ${statusColor(response.status)} | ${latencyMs}ms\n`);
|
|
243
|
+
|
|
244
|
+
// P1-03: capture response details and send back to API
|
|
245
|
+
if (reqData.id) {
|
|
246
|
+
let responseBody = '';
|
|
247
|
+
try {
|
|
248
|
+
const clone = response.clone();
|
|
249
|
+
const buf = await clone.arrayBuffer();
|
|
250
|
+
const bytes = buf.byteLength;
|
|
251
|
+
if (bytes <= 100_000) {
|
|
252
|
+
const ct = response.headers.get('content-type') ?? '';
|
|
253
|
+
responseBody = ct.includes('text') || ct.includes('json')
|
|
254
|
+
? await response.text()
|
|
255
|
+
: `[binary ${bytes} bytes]`;
|
|
256
|
+
} else {
|
|
257
|
+
responseBody = `[truncated — ${bytes} bytes]`;
|
|
258
|
+
}
|
|
259
|
+
} catch {}
|
|
113
260
|
|
|
114
|
-
|
|
115
|
-
|
|
261
|
+
const responseHeaders = {};
|
|
262
|
+
response.headers.forEach((v, k) => { responseHeaders[k] = v; });
|
|
263
|
+
|
|
264
|
+
fetch(`${apiUrl}/response/${reqData.id}`, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: { 'Content-Type': 'application/json' },
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
status: response.status,
|
|
269
|
+
headers: responseHeaders,
|
|
270
|
+
body: responseBody,
|
|
271
|
+
latencyMs,
|
|
272
|
+
}),
|
|
273
|
+
}).catch(() => {});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
116
276
|
|
|
117
277
|
// Warn when approaching tier limit
|
|
118
278
|
if (requestCount === maxRequests) {
|
|
119
279
|
logger.warn(`⚠️ Tier limit reached (${maxRequests} requests). Older requests will be purged.\n`);
|
|
120
280
|
} else if (requestCount === Math.floor(maxRequests * 0.9)) {
|
|
121
|
-
logger.warn(`⚠️ 90% of tier limit reached. Upgrade to Pro for
|
|
281
|
+
logger.warn(`⚠️ 90% of tier limit reached. Upgrade to Pro for ${proMaxRequests} requests at https://anonymily.com/upgrade\n`);
|
|
122
282
|
}
|
|
123
283
|
} catch (err) {
|
|
124
|
-
logger.error(` └─
|
|
125
|
-
logger.dim(` Make sure your local server is actually running on port ${port}\n`);
|
|
284
|
+
logger.error(` └─ Unexpected error: ${err.message}\n`);
|
|
126
285
|
}
|
|
127
286
|
};
|
|
128
287
|
|
|
129
|
-
es.onerror = () => {
|
|
288
|
+
es.onerror = (err) => {
|
|
289
|
+
// eventsource v4 uses err.code for HTTP status, not err.status
|
|
290
|
+
if (err && err.code === 401) {
|
|
291
|
+
logger.error(`\n❌ Unauthorized: This hook requires a valid Personal Access Token.`);
|
|
292
|
+
logger.dim(` Use --token <your-pat> or set the ANONYMILY_TOKEN environment variable.`);
|
|
293
|
+
logger.dim(` Generate a PAT from your dashboard at https://anonymily.com/account\n`);
|
|
294
|
+
es.close();
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
if (err && err.code === 403) {
|
|
298
|
+
logger.error(`\n❌ Forbidden: Custom endpoint IDs require a Pro subscription.`);
|
|
299
|
+
logger.dim(` Remove --id to use a random free endpoint, or upgrade at https://anonymily.com/upgrade\n`);
|
|
300
|
+
es.close();
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
130
303
|
logger.dim(`\n[Network] Connection dropped. Automatically reconnecting...`);
|
|
131
304
|
};
|
|
132
305
|
|
|
133
306
|
return es;
|
|
134
307
|
}
|
|
308
|
+
|
package/lib/trigger.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* P2-02: List available synthetic event templates from the API.
|
|
6
|
+
* @returns {Promise<Array<{provider: string, event: string, description: string}>>}
|
|
7
|
+
*/
|
|
8
|
+
export async function listEvents(apiUrl) {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(`${apiUrl}/trigger/events`);
|
|
11
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
12
|
+
return await res.json();
|
|
13
|
+
} catch (err) {
|
|
14
|
+
logger.error(`Failed to fetch event list: ${err.message}`);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* P2-02: Fire a synthetic event at a hook endpoint.
|
|
21
|
+
* @param {string} apiUrl
|
|
22
|
+
* @param {string} hookId
|
|
23
|
+
* @param {string} provider e.g. 'github'
|
|
24
|
+
* @param {string} event e.g. 'push'
|
|
25
|
+
* @param {string|null} token PAT for Pro auth
|
|
26
|
+
*/
|
|
27
|
+
export async function fireEvent(apiUrl, hookId, provider, event, token) {
|
|
28
|
+
const res = await fetch(`${apiUrl}/trigger/${hookId}`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({ provider, event }),
|
|
35
|
+
signal: AbortSignal.timeout(20_000),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
let errMsg = `HTTP ${res.status}`;
|
|
40
|
+
try {
|
|
41
|
+
const body = await res.json();
|
|
42
|
+
errMsg = body.message ?? errMsg;
|
|
43
|
+
} catch {}
|
|
44
|
+
throw new Error(errMsg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pretty-print the event list in a table grouped by provider.
|
|
52
|
+
*/
|
|
53
|
+
export function printEventList(events) {
|
|
54
|
+
if (!events || events.length === 0) {
|
|
55
|
+
logger.error('No events available.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Group by provider
|
|
60
|
+
const byProvider = {};
|
|
61
|
+
for (const e of events) {
|
|
62
|
+
(byProvider[e.provider] ??= []).push(e);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.raw(`\n${pc.bold('Available synthetic events')}\n`);
|
|
66
|
+
logger.raw(` Usage: ${pc.cyan('anonymily trigger <provider> <event> --hook <hookId> --token <pat>')}\n`);
|
|
67
|
+
|
|
68
|
+
for (const [provider, evts] of Object.entries(byProvider)) {
|
|
69
|
+
logger.raw(` ${pc.bold(pc.green(provider))}`);
|
|
70
|
+
for (const e of evts) {
|
|
71
|
+
const paddedEvent = e.event.padEnd(36);
|
|
72
|
+
logger.raw(` ${pc.cyan(paddedEvent)} ${pc.dim(e.description)}`);
|
|
73
|
+
}
|
|
74
|
+
logger.raw('');
|
|
75
|
+
}
|
|
76
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export function generateHookId() {
|
|
2
|
-
|
|
2
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
3
|
+
let id = '';
|
|
4
|
+
for (let i = 0; i < 8; i++) {
|
|
5
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
6
|
+
}
|
|
7
|
+
return id;
|
|
3
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonymilyhq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "CLI for Anonymily platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node ./bin/cli.js",
|
|
11
|
+
"build": "echo 'CLI built successfully' && ls ./bin/cli.js",
|
|
11
12
|
"test": "node --experimental-vm-modules node_modules/.bin/jest --no-coverage",
|
|
12
13
|
"test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage"
|
|
13
14
|
},
|