fastpass-cli 0.2.1 → 0.2.3
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 +1 -1
- package/package.json +1 -1
- package/src/auth.js +1 -0
- package/src/cli.js +6 -1
- package/src/commands/protect.js +88 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fastpass
|
|
2
2
|
|
|
3
|
-
A CLI for
|
|
3
|
+
A CLI for rapidly configuring [Cloudflare Access](https://www.cloudflare.com/products/zero-trust/access/) — add authentication to internal apps without touching your application code. Creates Access Applications, Identity Providers, and policies via the Cloudflare API.
|
|
4
4
|
|
|
5
5
|
**Requirements:** Node 18+, Cloudflare account with the domain on your account, Cloudflare Zero Trust (Access) enabled.
|
|
6
6
|
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import { Command } from 'commander';
|
|
2
3
|
import pc from 'picocolors';
|
|
3
4
|
import { getCredentials } from './auth.js';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { version } = require('../package.json');
|
|
4
8
|
import { createApi } from './api.js';
|
|
5
9
|
import { protect } from './commands/protect.js';
|
|
6
10
|
import { list } from './commands/list.js';
|
|
@@ -15,7 +19,7 @@ export function run() {
|
|
|
15
19
|
program
|
|
16
20
|
.name('fastpass-cli')
|
|
17
21
|
.description('Cloudflare Access in 60 seconds.')
|
|
18
|
-
.version(
|
|
22
|
+
.version(version);
|
|
19
23
|
|
|
20
24
|
// Default action (no subcommand) — run the protect wizard
|
|
21
25
|
program
|
|
@@ -23,6 +27,7 @@ export function run() {
|
|
|
23
27
|
.description('Protect a domain with Cloudflare Access')
|
|
24
28
|
.option('--auth <method>', 'Auth method(s): email, github, google (comma-separated for multiple)')
|
|
25
29
|
.option('--allow <rule>', 'Who can access: email, *@domain.com, or "everyone"')
|
|
30
|
+
.option('--hidden', 'Hide email addresses from terminal output')
|
|
26
31
|
.action(async (domain, opts) => {
|
|
27
32
|
printBanner();
|
|
28
33
|
const creds = await getCredentials();
|
package/src/commands/protect.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { input, select } from '@inquirer/prompts';
|
|
2
|
+
import { input, password, select, confirm } from '@inquirer/prompts';
|
|
3
3
|
import { getTeamName } from '../auth.js';
|
|
4
4
|
import { ensureEmailOtp } from '../idp/email-otp.js';
|
|
5
5
|
import { ensureGitHub } from '../idp/github.js';
|
|
6
6
|
import { ensureGoogle } from '../idp/google.js';
|
|
7
|
-
import { handleApiError } from '../api.js';
|
|
7
|
+
import { handleApiError, ApiError } from '../api.js';
|
|
8
8
|
import { spin, withSpinner } from '../ui.js';
|
|
9
9
|
|
|
10
10
|
const AUTH_CHOICES = {
|
|
@@ -33,6 +33,16 @@ export async function protect(api, opts = {}) {
|
|
|
33
33
|
|
|
34
34
|
// Validate domain exists in CF
|
|
35
35
|
await withSpinner('Verifying domain...', () => validateDomain(api, domain.trim()));
|
|
36
|
+
console.log('');
|
|
37
|
+
|
|
38
|
+
// Check if domain is already protected
|
|
39
|
+
const existing = await checkExistingApp(api, domain.trim());
|
|
40
|
+
if (existing) {
|
|
41
|
+
console.log(`\n ${pc.yellow('This domain is already protected by Access.')}\n`);
|
|
42
|
+
console.log(` Run ${pc.cyan(`fastpass inspect ${domain.trim()}`)} to view its configuration.`);
|
|
43
|
+
console.log(` Run ${pc.cyan(`fastpass remove ${domain.trim()}`)} to remove it first.\n`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
36
46
|
|
|
37
47
|
// Parse auth methods: comma-separated string from CLI, or single interactive choice
|
|
38
48
|
const authMethods = opts.auth
|
|
@@ -48,9 +58,11 @@ export async function protect(api, opts = {}) {
|
|
|
48
58
|
process.exit(1);
|
|
49
59
|
}
|
|
50
60
|
}
|
|
61
|
+
console.log('');
|
|
51
62
|
|
|
52
63
|
// Resolve who gets access
|
|
53
|
-
const { include, includeType } = await resolveAccess(opts.allow);
|
|
64
|
+
const { include, includeType } = await resolveAccess(opts.allow, opts.hidden);
|
|
65
|
+
console.log('');
|
|
54
66
|
|
|
55
67
|
// Get team name for OAuth callback URLs
|
|
56
68
|
const teamName = await withSpinner('Fetching team info', () => getTeamName(api));
|
|
@@ -71,6 +83,30 @@ export async function protect(api, opts = {}) {
|
|
|
71
83
|
// Build the policy include rules
|
|
72
84
|
const policyInclude = buildIncludeRules(include, includeType);
|
|
73
85
|
|
|
86
|
+
// Describe access for the summary
|
|
87
|
+
const accessLabel = (opts.hidden && includeType === 'emails')
|
|
88
|
+
? pc.dim('(hidden)')
|
|
89
|
+
: describeAccess(include, includeType);
|
|
90
|
+
|
|
91
|
+
// Show confirmation summary (skip when all CLI flags provided)
|
|
92
|
+
const allFlagsProvided = opts.domain && opts.auth && opts.allow;
|
|
93
|
+
if (!allFlagsProvided) {
|
|
94
|
+
console.log(` ${pc.bold('Domain:')} ${domain.trim()}`);
|
|
95
|
+
console.log(` ${pc.bold('Login:')} ${authMethods.map((m) => AUTH_CHOICES[m].label.split(' (')[0]).join(', ')}`);
|
|
96
|
+
console.log(` ${pc.bold('Access:')} ${accessLabel}`);
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
const ok = await confirm({
|
|
100
|
+
message: 'Create this Access application?',
|
|
101
|
+
default: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!ok) {
|
|
105
|
+
console.log(pc.dim(' Cancelled.\n'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
74
110
|
// Create the access application with an inline policy
|
|
75
111
|
const s = spin(`Creating Access application for ${pc.bold(domain.trim())}...`);
|
|
76
112
|
|
|
@@ -91,7 +127,20 @@ export async function protect(api, opts = {}) {
|
|
|
91
127
|
],
|
|
92
128
|
};
|
|
93
129
|
|
|
94
|
-
|
|
130
|
+
let app;
|
|
131
|
+
try {
|
|
132
|
+
const { result } = await api.post('/access/apps', appBody);
|
|
133
|
+
app = result;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
s.fail(`Failed to create Access application for ${pc.bold(domain.trim())}`);
|
|
136
|
+
if (err instanceof ApiError && err.message.includes('application_already_exists')) {
|
|
137
|
+
console.log(`\n ${pc.yellow('This domain is already protected by Access.')}\n`);
|
|
138
|
+
console.log(` Run ${pc.cyan(`fastpass inspect ${domain.trim()}`)} to view its configuration.`);
|
|
139
|
+
console.log(` Run ${pc.cyan(`fastpass remove ${domain.trim()}`)} to remove it first.\n`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
95
144
|
|
|
96
145
|
s.succeed(`Protected ${pc.bold(domain.trim())}`);
|
|
97
146
|
console.log('');
|
|
@@ -124,7 +173,7 @@ export async function validateDomain(api, domain) {
|
|
|
124
173
|
}
|
|
125
174
|
}
|
|
126
175
|
|
|
127
|
-
export async function resolveAccess(allowFlag) {
|
|
176
|
+
export async function resolveAccess(allowFlag, hidden) {
|
|
128
177
|
// If --allow flag was passed, parse it
|
|
129
178
|
if (allowFlag) {
|
|
130
179
|
if (allowFlag.startsWith('*@')) {
|
|
@@ -140,6 +189,8 @@ export async function resolveAccess(allowFlag) {
|
|
|
140
189
|
return { include: allowFlag.split(',').map((e) => e.trim()), includeType: 'emails' };
|
|
141
190
|
}
|
|
142
191
|
|
|
192
|
+
const emailPrompt = hidden ? password : input;
|
|
193
|
+
|
|
143
194
|
// Interactive
|
|
144
195
|
const accessType = await select({
|
|
145
196
|
message: 'Who should have access?',
|
|
@@ -148,7 +199,7 @@ export async function resolveAccess(allowFlag) {
|
|
|
148
199
|
|
|
149
200
|
switch (accessType) {
|
|
150
201
|
case 'me': {
|
|
151
|
-
const email = await
|
|
202
|
+
const email = await emailPrompt({
|
|
152
203
|
message: 'Your email address:',
|
|
153
204
|
validate: (v) => v.includes('@') || 'Enter a valid email',
|
|
154
205
|
});
|
|
@@ -169,7 +220,7 @@ export async function resolveAccess(allowFlag) {
|
|
|
169
220
|
return { include: [org.trim()], includeType: 'github_org' };
|
|
170
221
|
}
|
|
171
222
|
case 'emails': {
|
|
172
|
-
const emails = await
|
|
223
|
+
const emails = await emailPrompt({
|
|
173
224
|
message: 'Email addresses (comma-separated):',
|
|
174
225
|
validate: (v) => v.includes('@') || 'Enter at least one email',
|
|
175
226
|
});
|
|
@@ -182,6 +233,36 @@ export async function resolveAccess(allowFlag) {
|
|
|
182
233
|
}
|
|
183
234
|
}
|
|
184
235
|
|
|
236
|
+
export async function checkExistingApp(api, domain) {
|
|
237
|
+
const s = spin('Checking for existing application...');
|
|
238
|
+
try {
|
|
239
|
+
const { result } = await api.get('/access/apps');
|
|
240
|
+
const match = result?.find(
|
|
241
|
+
(app) => app.type === 'self_hosted' && app.domain === domain,
|
|
242
|
+
);
|
|
243
|
+
s.stop();
|
|
244
|
+
return match || null;
|
|
245
|
+
} catch {
|
|
246
|
+
s.stop();
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function describeAccess(include, includeType) {
|
|
252
|
+
switch (includeType) {
|
|
253
|
+
case 'emails':
|
|
254
|
+
return include.join(', ');
|
|
255
|
+
case 'domain':
|
|
256
|
+
return `*@${include[0]}`;
|
|
257
|
+
case 'github_org':
|
|
258
|
+
return `GitHub org: ${include[0]}`;
|
|
259
|
+
case 'everyone':
|
|
260
|
+
return 'Everyone (any logged-in user)';
|
|
261
|
+
default:
|
|
262
|
+
return 'Everyone';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
185
266
|
export function buildIncludeRules(include, includeType) {
|
|
186
267
|
switch (includeType) {
|
|
187
268
|
case 'emails':
|