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 +15 -1
- package/dist/commands/deploy.d.ts +2 -1
- package/dist/commands/deploy.js +43 -7
- package/dist/components/DevPropertiesForm.d.ts +2 -1
- package/dist/components/DevPropertiesForm.js +46 -13
- package/dist/components/MainMenu.js +5 -0
- package/dist/utils/oauth2-auth.d.ts +12 -0
- package/dist/utils/oauth2-auth.js +31 -0
- package/package.json +1 -1
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 {};
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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 (
|
|
32
|
-
|
|
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 && (
|
|
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
|
-
'
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 && (
|
|
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';
|