circleinbox 0.1.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.
@@ -0,0 +1,242 @@
1
+ // Tests for the 6 critical/major fixes in circleinbox-cli.cjs
2
+ // Run with: node --test cli/lib/cli/cli-fixes.test.cjs
3
+
4
+ const { describe, it } = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+ const fs = require('node:fs');
7
+ const path = require('node:path');
8
+
9
+ // Read the CLI source to verify structural fixes
10
+ const cliSource = fs.readFileSync(
11
+ path.join(__dirname, '..', '..', 'circleinbox-cli.cjs'),
12
+ 'utf8'
13
+ );
14
+
15
+ describe('Fix 1: --api-url must not persist to disk', () => {
16
+ it('should NOT call saveConfig when --api-url is set', () => {
17
+ // The old code had: saveConfig(config); after setting api-url
18
+ // The new code should set globalFlags.apiUrlOverride instead
19
+ const apiUrlBlock = cliSource.match(
20
+ /if \(gf\['api-url'\]\)[\s\S]*?}/
21
+ )?.[0];
22
+ assert.ok(apiUrlBlock, 'api-url handling block exists');
23
+ assert.ok(
24
+ !apiUrlBlock.includes('saveConfig'),
25
+ 'saveConfig must not be called in api-url block'
26
+ );
27
+ assert.ok(
28
+ apiUrlBlock.includes('apiUrlOverride'),
29
+ 'should set apiUrlOverride in memory'
30
+ );
31
+ });
32
+
33
+ it('getClient should use apiUrlOverride', () => {
34
+ const getClientBlock = cliSource.match(
35
+ /function getClient\(\)[\s\S]*?^}/m
36
+ )?.[0];
37
+ assert.ok(getClientBlock, 'getClient function exists');
38
+ assert.ok(
39
+ getClientBlock.includes('apiUrlOverride'),
40
+ 'getClient must check globalFlags.apiUrlOverride'
41
+ );
42
+ });
43
+ });
44
+
45
+ describe('Fix 2: readline rl.close() ordering', () => {
46
+ it('inboxDelete should close rl inside the question callback', () => {
47
+ const inboxDeleteBlock = cliSource.match(
48
+ /async function inboxDelete[\s\S]*?^}/m
49
+ )?.[0];
50
+ assert.ok(inboxDeleteBlock, 'inboxDelete function exists');
51
+
52
+ // rl.close() must appear INSIDE the callback, not after rl.question
53
+ // The pattern should be: rl.question('...', (answer) => { rl.close(); resolve(...) })
54
+ // NOT: rl.question('...', resolve); rl.close();
55
+ assert.ok(
56
+ !inboxDeleteBlock.match(/rl\.question\([^)]+,\s*resolve\s*\)\s*;\s*\n\s*rl\.close/),
57
+ 'rl.close() must NOT be on the line after rl.question with bare resolve'
58
+ );
59
+ // Positive check: rl.close() appears inside a callback with resolve
60
+ assert.ok(
61
+ inboxDeleteBlock.includes('rl.close();\n resolve('),
62
+ 'rl.close() should be inside the callback before resolve'
63
+ );
64
+ });
65
+
66
+ it('commandReset password prompt should close rl inside the callback', () => {
67
+ // Find the commandReset function's readline usage
68
+ const resetBlock = cliSource.match(
69
+ /async function commandReset[\s\S]*?^}/m
70
+ )?.[0];
71
+ assert.ok(resetBlock, 'commandReset function exists');
72
+
73
+ // Should NOT have the pattern: rl.question('...', resolve); rl.close();
74
+ assert.ok(
75
+ !resetBlock.match(/rl\.question\([^)]+,\s*resolve\s*\)\s*;\s*\n\s*rl\.close/),
76
+ 'rl.close() must NOT be after rl.question with bare resolve in commandReset'
77
+ );
78
+ });
79
+ });
80
+
81
+ describe('Fix 3: response.json() crash on non-JSON', () => {
82
+ it('should use response.text() + JSON.parse instead of response.json()', () => {
83
+ const requestBlock = cliSource.match(
84
+ /async request\(method[\s\S]*?^\s{2}}/m
85
+ )?.[0];
86
+ assert.ok(requestBlock, 'request method exists');
87
+
88
+ assert.ok(
89
+ !requestBlock.includes('response.json()'),
90
+ 'must NOT use response.json() directly'
91
+ );
92
+ assert.ok(
93
+ requestBlock.includes('response.text()'),
94
+ 'must use response.text() first'
95
+ );
96
+ assert.ok(
97
+ requestBlock.includes('JSON.parse(text)'),
98
+ 'must use JSON.parse on text'
99
+ );
100
+ });
101
+
102
+ it('should handle parse failures gracefully', () => {
103
+ const requestBlock = cliSource.match(
104
+ /async request\(method[\s\S]*?^\s{2}}/m
105
+ )?.[0];
106
+ // Should have error handling around JSON.parse
107
+ assert.ok(
108
+ requestBlock.includes("catch") && requestBlock.includes("INVALID_RESPONSE"),
109
+ 'should catch parse errors and throw with INVALID_RESPONSE code'
110
+ );
111
+ });
112
+ });
113
+
114
+ describe('Fix 4: fetch timeout with AbortController', () => {
115
+ it('ApiClient.request should use AbortController with 30s timeout', () => {
116
+ const requestBlock = cliSource.match(
117
+ /async request\(method[\s\S]*?^\s{2}}/m
118
+ )?.[0];
119
+ assert.ok(requestBlock, 'request method exists');
120
+
121
+ assert.ok(
122
+ requestBlock.includes('AbortController'),
123
+ 'must create AbortController'
124
+ );
125
+ assert.ok(
126
+ requestBlock.includes('30000'),
127
+ 'must set 30s timeout'
128
+ );
129
+ assert.ok(
130
+ requestBlock.includes('signal: controller.signal'),
131
+ 'must pass signal to fetch'
132
+ );
133
+ assert.ok(
134
+ requestBlock.includes('clearTimeout'),
135
+ 'must clear timeout after fetch'
136
+ );
137
+ });
138
+
139
+ it('resetClearAuth fetch calls should also have timeouts', () => {
140
+ const resetBlock = cliSource.match(
141
+ /async function resetClearAuth[\s\S]*?^}/m
142
+ )?.[0];
143
+ assert.ok(resetBlock, 'resetClearAuth function exists');
144
+
145
+ // Should have AbortController for both fetch calls
146
+ const controllerMatches = resetBlock.match(/new AbortController/g);
147
+ assert.ok(
148
+ controllerMatches && controllerMatches.length >= 2,
149
+ 'resetClearAuth must have at least 2 AbortControllers (one per fetch)'
150
+ );
151
+ });
152
+ });
153
+
154
+ describe('Fix 5: subArgs no longer uses fragile indexOf', () => {
155
+ it('should not use allArgs.indexOf(command) for subArgs', () => {
156
+ const mainBlock = cliSource.match(
157
+ /\/\/ Main[\s\S]*$/
158
+ )?.[0];
159
+ assert.ok(mainBlock, 'main block exists');
160
+
161
+ assert.ok(
162
+ !mainBlock.includes('allArgs.indexOf(command)'),
163
+ 'must NOT use allArgs.indexOf(command)'
164
+ );
165
+ });
166
+
167
+ it('should track command position by iterating and skipping global flags', () => {
168
+ const mainBlock = cliSource.match(
169
+ /\/\/ Main[\s\S]*$/
170
+ )?.[0];
171
+ assert.ok(mainBlock, 'main block exists');
172
+
173
+ assert.ok(
174
+ mainBlock.includes('commandIndex'),
175
+ 'should use a commandIndex variable'
176
+ );
177
+ assert.ok(
178
+ mainBlock.includes('GLOBAL_FLAGS'),
179
+ 'should define known global flags to skip'
180
+ );
181
+ });
182
+ });
183
+
184
+ describe('Fix 6: HTTPS validation for credentials', () => {
185
+ it('resetClearAuth should refuse HTTP URLs', () => {
186
+ const resetBlock = cliSource.match(
187
+ /async function resetClearAuth[\s\S]*?^}/m
188
+ )?.[0];
189
+ assert.ok(resetBlock, 'resetClearAuth function exists');
190
+
191
+ assert.ok(
192
+ resetBlock.includes("startsWith('https://')"),
193
+ 'must check for https:// protocol'
194
+ );
195
+ assert.ok(
196
+ resetBlock.includes('insecure HTTP') || resetBlock.includes('Refusing'),
197
+ 'must show error about insecure connection'
198
+ );
199
+ });
200
+ });
201
+
202
+ // ============================================================================
203
+ // PR #22 Review Fixes
204
+ // ============================================================================
205
+
206
+ describe('Fix 7: Generic password reset flow must also validate HTTPS', () => {
207
+ it('commandReset generic flow should check for HTTPS before proceeding', () => {
208
+ const resetBlock = cliSource.match(
209
+ /async function commandReset[\s\S]*?^}/m
210
+ )?.[0];
211
+ assert.ok(resetBlock, 'commandReset function exists');
212
+
213
+ // The generic flow starts after the ClearAuth branch (if flags.clearauth)
214
+ // It should have its own HTTPS check before the generic flow logic
215
+ const genericFlowSection = resetBlock.split('flags.clearauth')[1];
216
+ assert.ok(genericFlowSection, 'generic flow section exists after clearauth branch');
217
+
218
+ assert.ok(
219
+ genericFlowSection.includes("startsWith('https://')") ||
220
+ genericFlowSection.includes('startsWith("https://")'),
221
+ 'generic flow must check for https:// protocol on serviceUrl'
222
+ );
223
+ assert.ok(
224
+ genericFlowSection.includes('insecure') || genericFlowSection.includes('HTTPS'),
225
+ 'generic flow must warn about insecure connections'
226
+ );
227
+ });
228
+ });
229
+
230
+ describe('Fix 8: Retry jitter to prevent thundering herd', () => {
231
+ it('ApiClient.request should add jitter to retry delays', () => {
232
+ const requestBlock = cliSource.match(
233
+ /async request\(method[\s\S]*?^\s{2}}/m
234
+ )?.[0];
235
+ assert.ok(requestBlock, 'request method exists');
236
+
237
+ assert.ok(
238
+ requestBlock.includes('Math.random()'),
239
+ 'retry delay must include random jitter via Math.random()'
240
+ );
241
+ });
242
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "circleinbox",
3
+ "version": "0.1.0",
4
+ "description": "CLI for CircleInbox email infrastructure — manage inboxes, send email, store credentials, and more",
5
+ "type": "module",
6
+ "main": "circleinbox-cli.cjs",
7
+ "bin": {
8
+ "circleinbox": "./circleinbox-cli.cjs"
9
+ },
10
+ "engines": {
11
+ "node": ">=20.0.0"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Dundas",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "test": "node --test lib/**/*.test.cjs"
20
+ },
21
+ "files": [
22
+ "lib/",
23
+ "circleinbox-cli.cjs",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "keywords": [
28
+ "circleinbox",
29
+ "email",
30
+ "inbox",
31
+ "cli",
32
+ "credentials",
33
+ "vault",
34
+ "mailbox",
35
+ "agent",
36
+ "developer-tools"
37
+ ],
38
+ "dependencies": {}
39
+ }