sitevision-cli 1.0.0-beta.10 → 1.0.0-beta.12

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/dist/app.js CHANGED
@@ -4,6 +4,7 @@ import { detectProject } from './utils/project-detection.js';
4
4
  import { MainMenu } from './components/MainMenu.js';
5
5
  import { InfoScreen } from './components/InfoScreen.js';
6
6
  import { SetupFlow } from './components/SetupFlow.js';
7
+ import { DevPropertiesForm } from './components/DevPropertiesForm.js';
7
8
  import { PasswordInput } from './components/PasswordInput.js';
8
9
  import { KeychainPasswordChoice } from './components/KeychainPasswordChoice.js';
9
10
  import { decideSigningStep } from './utils/signing-step.js';
@@ -135,6 +136,13 @@ export default function App({ project: initialProject }) {
135
136
  case 'setup-signing':
136
137
  setState('setup-signing');
137
138
  break;
139
+ case 'change-auth':
140
+ if (!project.hasDevProperties || !project.devProperties) {
141
+ console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
142
+ return;
143
+ }
144
+ setState('change-auth-method');
145
+ break;
138
146
  case 'dev':
139
147
  if (!project.hasDevProperties || !project.devProperties) {
140
148
  console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
@@ -221,6 +229,12 @@ export default function App({ project: initialProject }) {
221
229
  break;
222
230
  }
223
231
  };
232
+ if (state === 'change-auth-method') {
233
+ return (_jsx(DevPropertiesForm, { projectRoot: project.root, initialProperties: project.devProperties, packageJson: project.packageJson, authOnly: true, onComplete: () => {
234
+ reloadProject();
235
+ setState('menu');
236
+ }, onCancel: () => setState('menu') }));
237
+ }
224
238
  if (state === 'setup') {
225
239
  return (_jsx(SetupFlow, { project: project, onReload: reloadProject, onComplete: () => setState('menu') }));
226
240
  }
@@ -293,7 +307,7 @@ export default function App({ project: initialProject }) {
293
307
  if (project.devProperties)
294
308
  project.devProperties.password = undefined;
295
309
  setState('dev-password-input');
296
- } }));
310
+ }, onChangeAuthMethod: () => setState('change-auth-method') }));
297
311
  }
298
312
  if (state === 'setup-signing') {
299
313
  return (_jsx(SigningPropertiesForm, { projectRoot: project.root, onComplete: () => {
@@ -10,7 +10,8 @@ interface DeployScreenProps {
10
10
  activate: boolean;
11
11
  onBack?: () => void;
12
12
  onRetryCredentials?: () => void;
13
+ onChangeAuthMethod?: () => void;
13
14
  }
14
- export declare function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, onBack, onRetryCredentials, }: DeployScreenProps): React.JSX.Element;
15
+ export declare function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, onBack, onRetryCredentials, onChangeAuthMethod, }: DeployScreenProps): React.JSX.Element;
15
16
  export declare const deployCommand: Command;
16
17
  export {};
@@ -6,10 +6,10 @@ import { deployApp, deployProduction } from '../utils/sitevision-api.js';
6
6
  import { getZipPath, getSignedZipPath, getAppType, } from '../utils/project-detection.js';
7
7
  import { zipExists } from '../utils/zip.js';
8
8
  import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
9
- import { setDeployPassword, deleteSessionCookie } from '../utils/keychain.js';
9
+ import { setDeployPassword, deleteSessionCookie, deleteOAuth2RefreshToken, } from '../utils/keychain.js';
10
10
  import { resolveOAuth2AccessToken } from '../utils/oauth2-auth.js';
11
11
  import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
12
- export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, onBack, onRetryCredentials, }) {
12
+ export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, onBack, onRetryCredentials, onChangeAuthMethod, }) {
13
13
  const [state, setState] = React.useState({
14
14
  status: 'deploying',
15
15
  message: production ? 'Deploying to production...' : 'Deploying to dev...',
@@ -23,19 +23,56 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
23
23
  sessionCookie: devProperties.sessionCookie,
24
24
  });
25
25
  const deployStartedRef = React.useRef(false);
26
+ const authMethod = devProperties.authMethod ?? 'basic';
27
+ // OAuth2 and cookie can re-authenticate in-place; basic re-prompts via the
28
+ // parent (TUI password entry).
29
+ const canRelogin = authMethod === 'oauth2' || authMethod === 'cookie';
30
+ // Discard the stored credential and force a fresh login. This is the
31
+ // "retry with new credentials" action for token/cookie auth — the usual fix
32
+ // when a session/token has expired (Sitevision reports that as a 400, not a
33
+ // 401, so it isn't auto-cleared).
34
+ const retryWithFreshLogin = () => {
35
+ const { domain, username } = devProperties;
36
+ if (authMethod === 'cookie' && domain && username) {
37
+ deleteSessionCookie(domain, username);
38
+ devProperties.sessionCookie = undefined;
39
+ }
40
+ else if (authMethod === 'oauth2' &&
41
+ domain &&
42
+ devProperties.oauth2?.clientId) {
43
+ deleteOAuth2RefreshToken(domain, devProperties.oauth2.clientId);
44
+ devProperties.accessToken = undefined;
45
+ }
46
+ setCredential({});
47
+ deployStartedRef.current = false;
48
+ setState({
49
+ status: 'deploying',
50
+ message: production
51
+ ? 'Deploying to production...'
52
+ : 'Deploying to dev...',
53
+ });
54
+ setPhase('login');
55
+ };
26
56
  useInput((input, key) => {
27
57
  if (state.status !== 'deploying') {
28
58
  if (onBack && (key.escape || input === 'q')) {
29
59
  onBack();
30
60
  }
31
- if (onRetryCredentials && state.status === 'error' && input === 'r') {
32
- onRetryCredentials();
61
+ if (state.status === 'error' && input === 'r') {
62
+ if (canRelogin) {
63
+ retryWithFreshLogin();
64
+ }
65
+ else if (onRetryCredentials) {
66
+ onRetryCredentials();
67
+ }
68
+ }
69
+ if (state.status === 'error' && input === 'm' && onChangeAuthMethod) {
70
+ onChangeAuthMethod();
33
71
  }
34
72
  }
35
73
  });
36
74
  // Decide once whether we can deploy straight away or must log in first.
37
75
  React.useEffect(() => {
38
- const authMethod = devProperties.authMethod ?? 'basic';
39
76
  if (authMethod === 'basic' || devProperties.sessionCookie) {
40
77
  setPhase('deploy');
41
78
  return;
@@ -72,7 +109,6 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
72
109
  async function runDeploy() {
73
110
  try {
74
111
  const appType = getAppType(manifest);
75
- const authMethod = devProperties.authMethod ?? 'basic';
76
112
  // Credential resolved in the init effect / login screen.
77
113
  const { accessToken, sessionCookie } = credential;
78
114
  // A stale session fails without a clean 401 — clear the stored cookie
@@ -191,7 +227,7 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
191
227
  ? 'Deploying'
192
228
  : state.status === 'success'
193
229
  ? 'Deployed'
194
- : 'Failed', message: state.message }) }), state.status === 'success' && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: [production ? 'Production deployment' : 'Dev deployment', " complete"] }), state.executableId && (_jsxs(Text, { dimColor: true, children: ["Executable ID: ", state.executableId] })), force && _jsx(Text, { dimColor: true, children: "(Force mode - overwrote existing)" }), activate && production && _jsx(Text, { dimColor: true, children: "(Activated)" })] })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), state.status !== 'deploying' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), onBack && _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" })] }))] }));
230
+ : 'Failed', message: state.message }) }), state.status === 'success' && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: [production ? 'Production deployment' : 'Dev deployment', " complete"] }), state.executableId && (_jsxs(Text, { dimColor: true, children: ["Executable ID: ", state.executableId] })), force && _jsx(Text, { dimColor: true, children: "(Force mode - overwrote existing)" }), activate && production && _jsx(Text, { dimColor: true, children: "(Activated)" })] })), state.status === 'error' && state.error && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "red", children: state.error }), canRelogin && (_jsx(Text, { color: "yellow", children: "This can happen when your session or token has expired \u2014 log in again to get fresh credentials." }))] })), state.status !== 'deploying' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && canRelogin && (_jsx(Text, { dimColor: true, children: "Press r to log in again with fresh credentials" })), state.status === 'error' && !canRelogin && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), state.status === 'error' && onChangeAuthMethod && (_jsx(Text, { dimColor: true, children: "Press m to change auth method" })), onBack && _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" })] }))] }));
195
231
  }
196
232
  export const deployCommand = {
197
233
  name: 'deploy',
@@ -3,8 +3,9 @@ interface Props {
3
3
  projectRoot: string;
4
4
  initialProperties?: DevProperties;
5
5
  packageJson: PackageJson;
6
+ authOnly?: boolean;
6
7
  onComplete: () => void;
7
8
  onCancel: () => void;
8
9
  }
9
- export declare function DevPropertiesForm({ projectRoot, initialProperties, packageJson, onComplete, onCancel, }: Props): import("react").JSX.Element;
10
+ export declare function DevPropertiesForm({ projectRoot, initialProperties, packageJson, authOnly, onComplete, onCancel, }: Props): import("react").JSX.Element;
10
11
  export {};
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useState, useEffect } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { TextInput } from './TextInput.js';
5
5
  import { writeDevProperties } from '../utils/project-detection.js';
6
6
  import { setDeployPassword, deleteDeployPassword, setOAuth2ClientSecret, } from '../utils/keychain.js';
7
- import { DEFAULT_REDIRECT_PORT } from '../utils/oauth2-auth.js';
7
+ import { DEFAULT_REDIRECT_PORT, discoverOAuth2Config, } from '../utils/oauth2-auth.js';
8
8
  const LABELS = {
9
9
  domain: 'Domain',
10
10
  siteName: 'Site Name',
@@ -32,7 +32,7 @@ function parseScopes(raw) {
32
32
  .filter(Boolean);
33
33
  return scopes.length > 0 ? scopes : undefined;
34
34
  }
35
- export function DevPropertiesForm({ projectRoot, initialProperties, packageJson, onComplete, onCancel, }) {
35
+ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson, authOnly, onComplete, onCancel, }) {
36
36
  const [stepIndex, setStepIndex] = useState(0);
37
37
  const [properties, setProperties] = useState(() => {
38
38
  const defaults = {
@@ -50,8 +50,39 @@ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson,
50
50
  scopes: initialProperties?.oauth2?.scopes?.join(' ') ?? '',
51
51
  clientSecret: '',
52
52
  }));
53
+ const [discoveryNote, setDiscoveryNote] = useState('');
53
54
  const method = properties.authMethod ?? 'basic';
54
55
  const isOAuth = method === 'oauth2';
56
+ // When OAuth2 is chosen, auto-fill the endpoints from the site's OpenID
57
+ // configuration (unauthenticated) so the user doesn't type them in.
58
+ useEffect(() => {
59
+ if (method !== 'oauth2' || !properties.domain)
60
+ return;
61
+ if (oauth.authorizationEndpoint && oauth.tokenEndpoint)
62
+ return;
63
+ let cancelled = false;
64
+ setDiscoveryNote('Looking up OAuth2 endpoints…');
65
+ void discoverOAuth2Config(properties.domain, properties.useHTTPForDevDeploy).then(discovered => {
66
+ if (cancelled)
67
+ return;
68
+ if (discovered) {
69
+ setOauth(previous => ({
70
+ ...previous,
71
+ authorizationEndpoint: previous.authorizationEndpoint || discovered.authorizationEndpoint,
72
+ tokenEndpoint: previous.tokenEndpoint || discovered.tokenEndpoint,
73
+ scopes: previous.scopes || (discovered.scopesSupported?.join(' ') ?? ''),
74
+ }));
75
+ setDiscoveryNote('Endpoints auto-filled from the site OpenID config.');
76
+ }
77
+ else {
78
+ setDiscoveryNote('Could not auto-discover endpoints — enter them below.');
79
+ }
80
+ });
81
+ return () => {
82
+ cancelled = true;
83
+ };
84
+ // eslint-disable-next-line react-hooks/exhaustive-deps
85
+ }, [method, properties.domain]);
55
86
  const methodSteps = method === 'oauth2'
56
87
  ? [
57
88
  'oauthClientId',
@@ -63,15 +94,17 @@ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson,
63
94
  : method === 'cookie'
64
95
  ? ['sessionLoginUrl']
65
96
  : ['password'];
66
- const steps = [
67
- 'domain',
68
- 'siteName',
69
- 'addonName',
70
- 'username',
71
- 'authMethod',
72
- ...methodSteps,
73
- 'useHTTP',
74
- ];
97
+ const steps = authOnly
98
+ ? ['authMethod', ...methodSteps]
99
+ : [
100
+ 'domain',
101
+ 'siteName',
102
+ 'addonName',
103
+ 'username',
104
+ 'authMethod',
105
+ ...methodSteps,
106
+ 'useHTTP',
107
+ ];
75
108
  const currentStep = steps[stepIndex];
76
109
  const redirectPort = initialProperties?.oauth2?.redirectPort ?? DEFAULT_REDIRECT_PORT;
77
110
  const advance = () => {
@@ -175,7 +208,7 @@ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson,
175
208
  return null;
176
209
  }
177
210
  };
178
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Setup Development Properties" }), _jsxs(Text, { children: [' ', "Step ", stepIndex + 1, " of ", steps.length, ":", ' ', currentStep ? LABELS[currentStep] : ''] })] }), isOAuth && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Whitelist this redirect URI on the OAuth2 client: http://127.0.0.1:", redirectPort, "/callback"] }) })), _jsx(Box, { marginBottom: 1, children: steps.map((s, i) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: i <= stepIndex ? 'green' : 'gray', children: i < stepIndex ? '✓' : i === stepIndex ? '●' : '○' }) }, s))) }), _jsx(Box, { borderStyle: "single", borderColor: "gray", padding: 1, children: renderInput() })] }));
211
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Setup Development Properties" }), _jsxs(Text, { children: [' ', "Step ", stepIndex + 1, " of ", steps.length, ":", ' ', currentStep ? LABELS[currentStep] : ''] })] }), isOAuth && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Whitelist this redirect URI on the OAuth2 client: http://127.0.0.1:", redirectPort, "/callback"] }), discoveryNote && _jsx(Text, { color: "yellow", children: discoveryNote })] })), _jsx(Box, { marginBottom: 1, children: steps.map((s, i) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: i <= stepIndex ? 'green' : 'gray', children: i < stepIndex ? '✓' : i === stepIndex ? '●' : '○' }) }, s))) }), _jsx(Box, { borderStyle: "single", borderColor: "gray", padding: 1, children: renderInput() })] }));
179
212
  }
180
213
  function MethodSelect({ defaultValue, onSubmit, }) {
181
214
  const [index, setIndex] = useState(() => Math.max(0, AUTH_METHODS.findIndex(m => m.value === defaultValue)));
@@ -51,6 +51,11 @@ export function MainMenu({ project, onSelect }) {
51
51
  value: 'deploy-production',
52
52
  description: 'Deploy to production server',
53
53
  },
54
+ {
55
+ label: '🔑 Auth Method',
56
+ value: 'change-auth',
57
+ description: 'Change the deploy authentication method',
58
+ },
54
59
  {
55
60
  label: 'ℹ️ Info',
56
61
  value: 'info',
@@ -6,6 +6,18 @@ export declare function createPkcePair(): {
6
6
  verifier: string;
7
7
  challenge: string;
8
8
  };
9
+ export interface DiscoveredOAuth2 {
10
+ authorizationEndpoint: string;
11
+ tokenEndpoint: string;
12
+ scopesSupported?: string[];
13
+ }
14
+ /**
15
+ * Fetch the site's OpenID configuration (unauthenticated) to auto-fill the
16
+ * authorization/token endpoints. Returns null if it isn't published (provider
17
+ * not enabled) or the response isn't a valid config, so callers fall back to
18
+ * manual entry.
19
+ */
20
+ export declare function discoverOAuth2Config(domain: string, useHTTP?: boolean): Promise<DiscoveredOAuth2 | null>;
9
21
  export declare function openBrowser(url: string): void;
10
22
  /**
11
23
  * Start an interactive OAuth2 login. Returns the authorize URL to open and a
@@ -44,6 +44,37 @@ async function postToken(config, params, secret) {
44
44
  return null;
45
45
  }
46
46
  }
47
+ /** OpenID configuration path (published at the issuer root once the provider is saved). */
48
+ const DISCOVERY_PATH = '/.well-known/openid-configuration';
49
+ /**
50
+ * Fetch the site's OpenID configuration (unauthenticated) to auto-fill the
51
+ * authorization/token endpoints. Returns null if it isn't published (provider
52
+ * not enabled) or the response isn't a valid config, so callers fall back to
53
+ * manual entry.
54
+ */
55
+ export async function discoverOAuth2Config(domain, useHTTP = false) {
56
+ if (!domain)
57
+ return null;
58
+ const protocol = useHTTP ? 'http' : 'https';
59
+ try {
60
+ const response = await makeRequest(`${protocol}://${domain}${DISCOVERY_PATH}`, { method: 'GET' });
61
+ if (response.statusCode !== 200)
62
+ return null;
63
+ const doc = JSON.parse(response.body.toString());
64
+ if (!doc.authorization_endpoint || !doc.token_endpoint)
65
+ return null;
66
+ return {
67
+ authorizationEndpoint: doc.authorization_endpoint,
68
+ tokenEndpoint: doc.token_endpoint,
69
+ scopesSupported: Array.isArray(doc.scopes_supported)
70
+ ? doc.scopes_supported
71
+ : undefined,
72
+ };
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
47
78
  export function openBrowser(url) {
48
79
  const isWin = process.platform === 'win32';
49
80
  const cmd = process.platform === 'darwin' ? 'open' : isWin ? 'cmd' : 'xdg-open';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "1.0.0-beta.10",
3
+ "version": "1.0.0-beta.12",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "svc": "dist/cli.js"