fastpass-cli 0.2.1 → 0.2.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # fastpass
2
2
 
3
- A CLI for rapdily 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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastpass-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Cloudflare Access in 60 seconds.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/auth.js CHANGED
@@ -35,6 +35,7 @@ export async function getCredentials() {
35
35
  }
36
36
 
37
37
  s.succeed('Credentials OK');
38
+ console.log('');
38
39
  return { token, accountId };
39
40
  }
40
41
 
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('0.2.1');
22
+ .version(version);
19
23
 
20
24
  // Default action (no subcommand) — run the protect wizard
21
25
  program
@@ -1,10 +1,10 @@
1
1
  import pc from 'picocolors';
2
- import { input, select } from '@inquirer/prompts';
2
+ import { input, 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
64
  const { include, includeType } = await resolveAccess(opts.allow);
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,28 @@ 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 = describeAccess(include, includeType);
88
+
89
+ // Show confirmation summary (skip when all CLI flags provided)
90
+ const allFlagsProvided = opts.domain && opts.auth && opts.allow;
91
+ if (!allFlagsProvided) {
92
+ console.log(` ${pc.bold('Domain:')} ${domain.trim()}`);
93
+ console.log(` ${pc.bold('Login:')} ${authMethods.map((m) => AUTH_CHOICES[m].label.split(' (')[0]).join(', ')}`);
94
+ console.log(` ${pc.bold('Access:')} ${accessLabel}`);
95
+ console.log('');
96
+
97
+ const ok = await confirm({
98
+ message: 'Create this Access application?',
99
+ default: true,
100
+ });
101
+
102
+ if (!ok) {
103
+ console.log(pc.dim(' Cancelled.\n'));
104
+ return;
105
+ }
106
+ }
107
+
74
108
  // Create the access application with an inline policy
75
109
  const s = spin(`Creating Access application for ${pc.bold(domain.trim())}...`);
76
110
 
@@ -91,7 +125,20 @@ export async function protect(api, opts = {}) {
91
125
  ],
92
126
  };
93
127
 
94
- const { result: app } = await api.post('/access/apps', appBody);
128
+ let app;
129
+ try {
130
+ const { result } = await api.post('/access/apps', appBody);
131
+ app = result;
132
+ } catch (err) {
133
+ s.fail(`Failed to create Access application for ${pc.bold(domain.trim())}`);
134
+ if (err instanceof ApiError && err.message.includes('application_already_exists')) {
135
+ console.log(`\n ${pc.yellow('This domain is already protected by Access.')}\n`);
136
+ console.log(` Run ${pc.cyan(`fastpass inspect ${domain.trim()}`)} to view its configuration.`);
137
+ console.log(` Run ${pc.cyan(`fastpass remove ${domain.trim()}`)} to remove it first.\n`);
138
+ return;
139
+ }
140
+ throw err;
141
+ }
95
142
 
96
143
  s.succeed(`Protected ${pc.bold(domain.trim())}`);
97
144
  console.log('');
@@ -182,6 +229,36 @@ export async function resolveAccess(allowFlag) {
182
229
  }
183
230
  }
184
231
 
232
+ export async function checkExistingApp(api, domain) {
233
+ const s = spin('Checking for existing application...');
234
+ try {
235
+ const { result } = await api.get('/access/apps');
236
+ const match = result?.find(
237
+ (app) => app.type === 'self_hosted' && app.domain === domain,
238
+ );
239
+ s.stop();
240
+ return match || null;
241
+ } catch {
242
+ s.stop();
243
+ return null;
244
+ }
245
+ }
246
+
247
+ function describeAccess(include, includeType) {
248
+ switch (includeType) {
249
+ case 'emails':
250
+ return include.join(', ');
251
+ case 'domain':
252
+ return `*@${include[0]}`;
253
+ case 'github_org':
254
+ return `GitHub org: ${include[0]}`;
255
+ case 'everyone':
256
+ return 'Everyone (any logged-in user)';
257
+ default:
258
+ return 'Everyone';
259
+ }
260
+ }
261
+
185
262
  export function buildIncludeRules(include, includeType) {
186
263
  switch (includeType) {
187
264
  case 'emails':