sitevision-cli 1.0.0-beta.5 → 1.0.0-beta.7
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 +22 -1
- package/dist/cli.js +22 -0
- package/dist/commands/deploy.js +81 -3
- package/dist/commands/dev.js +6 -0
- package/dist/components/DevPropertiesForm.js +160 -31
- package/dist/components/SetupFlow.js +35 -2
- package/dist/types/index.d.ts +24 -1
- package/dist/utils/keychain.d.ts +9 -0
- package/dist/utils/keychain.js +54 -0
- package/dist/utils/oauth2-auth.d.ts +20 -0
- package/dist/utils/oauth2-auth.js +160 -0
- package/dist/utils/password-prompt.d.ts +5 -0
- package/dist/utils/password-prompt.js +28 -0
- package/dist/utils/project-detection.d.ts +3 -2
- package/dist/utils/project-detection.js +24 -4
- package/dist/utils/session-cookie-auth.d.ts +9 -0
- package/dist/utils/session-cookie-auth.js +96 -0
- package/dist/utils/sitevision-api.d.ts +29 -5
- package/dist/utils/sitevision-api.js +90 -23
- package/package.json +2 -1
package/dist/app.js
CHANGED
|
@@ -13,6 +13,7 @@ import { DeployScreen } from './commands/deploy.js';
|
|
|
13
13
|
import { SignScreen } from './commands/sign.js';
|
|
14
14
|
import { SigningPropertiesForm } from './components/SigningPropertiesForm.js';
|
|
15
15
|
import { getSigningPassword, setDeployPassword as saveDeployPassword, setSigningPassword as saveSigningPassword, } from './utils/keychain.js';
|
|
16
|
+
const NONBASIC_DEV_UNSUPPORTED = '\x1b[33mdev/watch support only basic auth. Use `svc deploy` for OAuth2/cookie.\x1b[0m';
|
|
16
17
|
export default function App({ project: initialProject }) {
|
|
17
18
|
// The project is loaded once at startup, but setup flows write new values to
|
|
18
19
|
// disk and the OS keychain. Hold it in state so we can re-detect after setup
|
|
@@ -70,10 +71,18 @@ export default function App({ project: initialProject }) {
|
|
|
70
71
|
};
|
|
71
72
|
// Check if dev password is available (either from file or session)
|
|
72
73
|
const hasDevPassword = Boolean(project.devProperties?.password || devPassword);
|
|
74
|
+
// OAuth2 / cookie configs authenticate with a token or session resolved at
|
|
75
|
+
// deploy time, so they need no basic password. `svc dev`/`watch` stay basic.
|
|
76
|
+
const authMethod = project.devProperties?.authMethod ?? 'basic';
|
|
77
|
+
const isTokenAuth = authMethod === 'oauth2' || authMethod === 'cookie';
|
|
78
|
+
const deployAuthReady = isTokenAuth || hasDevPassword;
|
|
73
79
|
// Get effective dev properties with session password if needed
|
|
74
80
|
const getEffectiveDevProperties = () => {
|
|
75
81
|
if (!project.devProperties)
|
|
76
82
|
return undefined;
|
|
83
|
+
// Non-basic configs authenticate by token/cookie — never graft a password.
|
|
84
|
+
if (isTokenAuth)
|
|
85
|
+
return project.devProperties;
|
|
77
86
|
if (project.devProperties.password)
|
|
78
87
|
return project.devProperties;
|
|
79
88
|
return { ...project.devProperties, password: devPassword };
|
|
@@ -131,6 +140,10 @@ export default function App({ project: initialProject }) {
|
|
|
131
140
|
console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
|
|
132
141
|
return;
|
|
133
142
|
}
|
|
143
|
+
if (isTokenAuth) {
|
|
144
|
+
console.log(NONBASIC_DEV_UNSUPPORTED);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
134
147
|
if (!hasDevPassword) {
|
|
135
148
|
setState('dev-password-input');
|
|
136
149
|
}
|
|
@@ -147,6 +160,10 @@ export default function App({ project: initialProject }) {
|
|
|
147
160
|
console.log('\x1b[31mSigning credentials not configured. Run svc setup-signing first.\x1b[0m');
|
|
148
161
|
return;
|
|
149
162
|
}
|
|
163
|
+
if (isTokenAuth) {
|
|
164
|
+
console.log(NONBASIC_DEV_UNSUPPORTED);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
150
167
|
// Need both dev password and signing password
|
|
151
168
|
if (!hasDevPassword) {
|
|
152
169
|
setState('dev-password-input');
|
|
@@ -168,6 +185,10 @@ export default function App({ project: initialProject }) {
|
|
|
168
185
|
console.log('\x1b[31mSigning credentials not configured. Run svc setup-signing first.\x1b[0m');
|
|
169
186
|
return;
|
|
170
187
|
}
|
|
188
|
+
if (isTokenAuth) {
|
|
189
|
+
console.log(NONBASIC_DEV_UNSUPPORTED);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
171
192
|
routeToSigningStep(command);
|
|
172
193
|
break;
|
|
173
194
|
case 'sign':
|
|
@@ -191,7 +212,7 @@ export default function App({ project: initialProject }) {
|
|
|
191
212
|
console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
|
|
192
213
|
return;
|
|
193
214
|
}
|
|
194
|
-
if (!
|
|
215
|
+
if (!deployAuthReady) {
|
|
195
216
|
setState('dev-password-input');
|
|
196
217
|
}
|
|
197
218
|
else {
|
package/dist/cli.js
CHANGED
|
@@ -58,6 +58,12 @@ const cli = meow(`
|
|
|
58
58
|
shortFlag: 'p',
|
|
59
59
|
default: false,
|
|
60
60
|
},
|
|
61
|
+
token: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
},
|
|
64
|
+
cookie: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
},
|
|
61
67
|
},
|
|
62
68
|
});
|
|
63
69
|
const [commandName, ...args] = cli.input;
|
|
@@ -149,6 +155,22 @@ async function main() {
|
|
|
149
155
|
process.exit(1);
|
|
150
156
|
}
|
|
151
157
|
})();
|
|
158
|
+
// --token / --cookie override the resolved bearer token / session cookie for
|
|
159
|
+
// this run (manual / CI path, alongside SITEVISION_ACCESS_TOKEN and
|
|
160
|
+
// SITEVISION_SESSION_COOKIE).
|
|
161
|
+
if (cli.flags.token || cli.flags.cookie) {
|
|
162
|
+
if (project.devProperties) {
|
|
163
|
+
if (cli.flags.token) {
|
|
164
|
+
project.devProperties.accessToken = cli.flags.token;
|
|
165
|
+
}
|
|
166
|
+
if (cli.flags.cookie) {
|
|
167
|
+
project.devProperties.sessionCookie = cli.flags.cookie;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log('\x1b[33m--token/--cookie needs a .dev_properties.json (domain, site, addon) to deploy against.\x1b[0m');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
152
174
|
// First run: show the welcome (branding + optional signing-password save),
|
|
153
175
|
// then continue to the normal flow once the user dismisses it.
|
|
154
176
|
if (firstRun) {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -6,7 +6,9 @@ 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 } from '../utils/keychain.js';
|
|
9
|
+
import { setDeployPassword, deleteSessionCookie } from '../utils/keychain.js';
|
|
10
|
+
import { resolveOAuth2AccessToken } from '../utils/oauth2-auth.js';
|
|
11
|
+
import { resolveSessionCookie } from '../utils/session-cookie-auth.js';
|
|
10
12
|
export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, signingPassword, onBack, onRetryCredentials, }) {
|
|
11
13
|
const [state, setState] = React.useState({
|
|
12
14
|
status: 'deploying',
|
|
@@ -26,6 +28,52 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
|
|
|
26
28
|
async function runDeploy() {
|
|
27
29
|
try {
|
|
28
30
|
const appType = getAppType(manifest);
|
|
31
|
+
// Resolve an OAuth2 token silently (env/keychain refresh). A fresh
|
|
32
|
+
// browser login happens only in the direct `svc deploy` command, which
|
|
33
|
+
// sets accessToken before rendering — the Ink menu can't own the
|
|
34
|
+
// terminal for a login, so it resolves refresh-only here.
|
|
35
|
+
const authMethod = devProperties.authMethod ?? 'basic';
|
|
36
|
+
let accessToken = devProperties.accessToken;
|
|
37
|
+
if (authMethod === 'oauth2' && !accessToken) {
|
|
38
|
+
accessToken =
|
|
39
|
+
(await resolveOAuth2AccessToken(devProperties, {
|
|
40
|
+
interactive: false,
|
|
41
|
+
})) ?? undefined;
|
|
42
|
+
if (!accessToken) {
|
|
43
|
+
setState({
|
|
44
|
+
status: 'error',
|
|
45
|
+
error: 'No OAuth2 access token. Run `svc deploy` from a terminal to log in, or set SITEVISION_ACCESS_TOKEN.',
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let sessionCookie = devProperties.sessionCookie;
|
|
51
|
+
if (authMethod === 'cookie' && !sessionCookie) {
|
|
52
|
+
sessionCookie =
|
|
53
|
+
(await resolveSessionCookie(devProperties, {
|
|
54
|
+
interactive: false,
|
|
55
|
+
})) ?? undefined;
|
|
56
|
+
if (!sessionCookie) {
|
|
57
|
+
setState({
|
|
58
|
+
status: 'error',
|
|
59
|
+
error: 'No session cookie. Run `svc deploy` from a terminal to log in, or set SITEVISION_SESSION_COOKIE.',
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// A stale session fails without a clean 401 — clear the stored cookie
|
|
65
|
+
// so the next run re-authenticates. Skip when SITEVISION_SESSION_COOKIE
|
|
66
|
+
// is set: detection re-reads it first, so clearing would just replay
|
|
67
|
+
// the same dead cookie in a loop.
|
|
68
|
+
const clearStaleCookie = (result) => {
|
|
69
|
+
if (result.authExpired &&
|
|
70
|
+
authMethod === 'cookie' &&
|
|
71
|
+
!process.env['SITEVISION_SESSION_COOKIE'] &&
|
|
72
|
+
devProperties.domain &&
|
|
73
|
+
devProperties.username) {
|
|
74
|
+
deleteSessionCookie(devProperties.domain, devProperties.username);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
29
77
|
if (production) {
|
|
30
78
|
// Production deployment requires a signed zip
|
|
31
79
|
const signedZipPath = getSignedZipPath(projectRoot, manifest);
|
|
@@ -42,11 +90,14 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
|
|
|
42
90
|
addonName: devProperties.addonName,
|
|
43
91
|
username: devProperties.username,
|
|
44
92
|
password: devProperties.password,
|
|
93
|
+
accessToken,
|
|
94
|
+
sessionCookie,
|
|
45
95
|
useHTTP: devProperties.useHTTPForDevDeploy,
|
|
46
96
|
activate,
|
|
47
97
|
};
|
|
48
98
|
const result = await deployProduction(signedZipPath, config, appType);
|
|
49
99
|
if (!result.success) {
|
|
100
|
+
clearStaleCookie(result);
|
|
50
101
|
setState({
|
|
51
102
|
status: 'error',
|
|
52
103
|
error: result.error || 'Deployment failed',
|
|
@@ -75,10 +126,13 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
|
|
|
75
126
|
addonName: devProperties.addonName,
|
|
76
127
|
username: devProperties.username,
|
|
77
128
|
password: devProperties.password,
|
|
129
|
+
accessToken,
|
|
130
|
+
sessionCookie,
|
|
78
131
|
useHTTP: devProperties.useHTTPForDevDeploy,
|
|
79
132
|
};
|
|
80
133
|
const result = await deployApp(zipPath, config, appType, force);
|
|
81
134
|
if (!result.success) {
|
|
135
|
+
clearStaleCookie(result);
|
|
82
136
|
setState({
|
|
83
137
|
status: 'error',
|
|
84
138
|
error: result.error || 'Deployment failed',
|
|
@@ -146,8 +200,32 @@ export const deployCommand = {
|
|
|
146
200
|
console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
|
|
147
201
|
return;
|
|
148
202
|
}
|
|
149
|
-
// Resolve deploy
|
|
150
|
-
|
|
203
|
+
// Resolve deploy credentials. A token/cookie from env/--flag (already
|
|
204
|
+
// loaded in detectProject / cli) short-circuits everything.
|
|
205
|
+
const authMethod = project.devProperties.authMethod ?? 'basic';
|
|
206
|
+
if (project.devProperties.accessToken ||
|
|
207
|
+
project.devProperties.sessionCookie) {
|
|
208
|
+
// Nothing to acquire — a bearer token or session cookie is in hand.
|
|
209
|
+
}
|
|
210
|
+
else if (authMethod === 'oauth2') {
|
|
211
|
+
const token = await resolveOAuth2AccessToken(project.devProperties);
|
|
212
|
+
if (!token) {
|
|
213
|
+
console.log('\n\x1b[31mError: No OAuth2 access token available.\x1b[0m');
|
|
214
|
+
console.log('Set SITEVISION_ACCESS_TOKEN, pass --token, or configure the oauth2 endpoints in .dev_properties.json to log in.\n');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
project.devProperties.accessToken = token;
|
|
218
|
+
}
|
|
219
|
+
else if (authMethod === 'cookie') {
|
|
220
|
+
const cookie = await resolveSessionCookie(project.devProperties);
|
|
221
|
+
if (!cookie) {
|
|
222
|
+
console.log('\n\x1b[31mError: No session cookie available.\x1b[0m');
|
|
223
|
+
console.log('Log in when the browser opens, or set SITEVISION_SESSION_COOKIE / pass --cookie.\n');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
project.devProperties.sessionCookie = cookie;
|
|
227
|
+
}
|
|
228
|
+
else if (!project.devProperties.password) {
|
|
151
229
|
const { domain, username } = project.devProperties;
|
|
152
230
|
console.log('');
|
|
153
231
|
const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
|
package/dist/commands/dev.js
CHANGED
|
@@ -394,6 +394,12 @@ export const devCommand = {
|
|
|
394
394
|
console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
|
|
395
395
|
return;
|
|
396
396
|
}
|
|
397
|
+
// dev/watch continuously redeploy and only support basic auth. Fail clearly
|
|
398
|
+
// on an OAuth2/cookie config instead of prompting for an unusable password.
|
|
399
|
+
if ((project.devProperties.authMethod ?? 'basic') !== 'basic') {
|
|
400
|
+
console.log('\n\x1b[33mdev/watch support only basic auth. Use `svc deploy` for OAuth2/cookie.\x1b[0m\n');
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
397
403
|
// Resolve deploy password (already loaded from keychain/env in detectProject — prompt if missing)
|
|
398
404
|
if (!project.devProperties.password) {
|
|
399
405
|
const { domain, username } = project.devProperties;
|
|
@@ -3,15 +3,35 @@ import { useState } 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
|
-
import { setDeployPassword, deleteDeployPassword } from '../utils/keychain.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { setDeployPassword, deleteDeployPassword, setOAuth2ClientSecret, } from '../utils/keychain.js';
|
|
7
|
+
import { DEFAULT_REDIRECT_PORT } from '../utils/oauth2-auth.js';
|
|
8
|
+
const LABELS = {
|
|
9
|
+
domain: 'Domain',
|
|
10
|
+
siteName: 'Site Name',
|
|
11
|
+
addonName: 'Addon Name',
|
|
12
|
+
username: 'Username',
|
|
13
|
+
authMethod: 'Auth Method',
|
|
14
|
+
password: 'Password',
|
|
15
|
+
oauthClientId: 'Client ID',
|
|
16
|
+
oauthAuthEndpoint: 'Authorization Endpoint',
|
|
17
|
+
oauthTokenEndpoint: 'Token Endpoint',
|
|
18
|
+
oauthScopes: 'Scopes',
|
|
19
|
+
oauthClientSecret: 'Client Secret',
|
|
20
|
+
sessionLoginUrl: 'Login URL',
|
|
21
|
+
useHTTP: 'Use HTTP',
|
|
22
|
+
};
|
|
23
|
+
const AUTH_METHODS = [
|
|
24
|
+
{ value: 'basic', label: 'Basic auth (username + password)' },
|
|
25
|
+
{ value: 'oauth2', label: 'OAuth2 bearer token (PKCE)' },
|
|
26
|
+
{ value: 'cookie', label: 'Session cookie (SAML / SSO login)' },
|
|
14
27
|
];
|
|
28
|
+
function parseScopes(raw) {
|
|
29
|
+
const scopes = raw
|
|
30
|
+
.split(/[\s,]+/)
|
|
31
|
+
.map(s => s.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
return scopes.length > 0 ? scopes : undefined;
|
|
34
|
+
}
|
|
15
35
|
export function DevPropertiesForm({ projectRoot, initialProperties, packageJson, onComplete, onCancel, }) {
|
|
16
36
|
const [stepIndex, setStepIndex] = useState(0);
|
|
17
37
|
const [properties, setProperties] = useState(() => {
|
|
@@ -19,49 +39,158 @@ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson,
|
|
|
19
39
|
domain: packageJson.developmentDomain || '',
|
|
20
40
|
addonName: packageJson.addonName || '',
|
|
21
41
|
siteName: packageJson.siteName || '',
|
|
42
|
+
authMethod: 'basic',
|
|
22
43
|
};
|
|
23
44
|
return { ...defaults, ...initialProperties };
|
|
24
45
|
});
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
const [oauth, setOauth] = useState(() => ({
|
|
47
|
+
authorizationEndpoint: initialProperties?.oauth2?.authorizationEndpoint ?? '',
|
|
48
|
+
tokenEndpoint: initialProperties?.oauth2?.tokenEndpoint ?? '',
|
|
49
|
+
clientId: initialProperties?.oauth2?.clientId ?? '',
|
|
50
|
+
scopes: initialProperties?.oauth2?.scopes?.join(' ') ?? '',
|
|
51
|
+
clientSecret: '',
|
|
52
|
+
}));
|
|
53
|
+
const method = properties.authMethod ?? 'basic';
|
|
54
|
+
const isOAuth = method === 'oauth2';
|
|
55
|
+
const methodSteps = method === 'oauth2'
|
|
56
|
+
? [
|
|
57
|
+
'oauthClientId',
|
|
58
|
+
'oauthAuthEndpoint',
|
|
59
|
+
'oauthTokenEndpoint',
|
|
60
|
+
'oauthScopes',
|
|
61
|
+
'oauthClientSecret',
|
|
62
|
+
]
|
|
63
|
+
: method === 'cookie'
|
|
64
|
+
? ['sessionLoginUrl']
|
|
65
|
+
: ['password'];
|
|
66
|
+
const steps = [
|
|
67
|
+
'domain',
|
|
68
|
+
'siteName',
|
|
69
|
+
'addonName',
|
|
70
|
+
'username',
|
|
71
|
+
'authMethod',
|
|
72
|
+
...methodSteps,
|
|
73
|
+
'useHTTP',
|
|
74
|
+
];
|
|
75
|
+
const currentStep = steps[stepIndex];
|
|
76
|
+
const redirectPort = initialProperties?.oauth2?.redirectPort ?? DEFAULT_REDIRECT_PORT;
|
|
77
|
+
const advance = () => {
|
|
78
|
+
if (stepIndex < steps.length - 1) {
|
|
30
79
|
setStepIndex(stepIndex + 1);
|
|
31
80
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
};
|
|
82
|
+
const finalize = (props, fields) => {
|
|
83
|
+
const authMethod = props.authMethod ?? 'basic';
|
|
84
|
+
const domain = props.domain ?? '';
|
|
85
|
+
const username = props.username ?? '';
|
|
86
|
+
const finalProps = {
|
|
87
|
+
domain,
|
|
88
|
+
siteName: props.siteName ?? '',
|
|
89
|
+
addonName: props.addonName ?? '',
|
|
90
|
+
username,
|
|
91
|
+
authMethod,
|
|
92
|
+
useHTTPForDevDeploy: props.useHTTPForDevDeploy ?? false,
|
|
93
|
+
};
|
|
94
|
+
if (authMethod === 'oauth2') {
|
|
95
|
+
finalProps.oauth2 = {
|
|
96
|
+
authorizationEndpoint: fields.authorizationEndpoint,
|
|
97
|
+
tokenEndpoint: fields.tokenEndpoint,
|
|
98
|
+
clientId: fields.clientId,
|
|
99
|
+
scopes: parseScopes(fields.scopes),
|
|
100
|
+
...(initialProperties?.oauth2?.redirectPort && {
|
|
101
|
+
redirectPort: initialProperties.oauth2.redirectPort,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
if (fields.clientSecret && domain && fields.clientId) {
|
|
105
|
+
setOAuth2ClientSecret(domain, fields.clientId, fields.clientSecret);
|
|
37
106
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
107
|
+
}
|
|
108
|
+
else if (authMethod === 'cookie') {
|
|
109
|
+
if (props.sessionLoginUrl) {
|
|
110
|
+
finalProps.sessionLoginUrl = props.sessionLoginUrl;
|
|
41
111
|
}
|
|
42
|
-
|
|
43
|
-
|
|
112
|
+
}
|
|
113
|
+
else if (props.password && domain && username) {
|
|
114
|
+
setDeployPassword(domain, username, props.password);
|
|
115
|
+
}
|
|
116
|
+
else if (domain && username) {
|
|
117
|
+
// Empty password — clear any stale keychain entry so deploy prompts.
|
|
118
|
+
deleteDeployPassword(domain, username);
|
|
119
|
+
}
|
|
120
|
+
writeDevProperties(projectRoot, finalProps);
|
|
121
|
+
onComplete();
|
|
122
|
+
};
|
|
123
|
+
// Update a top-level DevProperties field, then advance or finalize.
|
|
124
|
+
const submitProperty = (key, value) => {
|
|
125
|
+
const next = { ...properties, [key]: value };
|
|
126
|
+
setProperties(next);
|
|
127
|
+
if (steps[stepIndex] === steps[steps.length - 1]) {
|
|
128
|
+
finalize(next, oauth);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
advance();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
// Update an OAuth field, then advance or finalize.
|
|
135
|
+
const submitOAuth = (key, value) => {
|
|
136
|
+
const next = { ...oauth, [key]: value };
|
|
137
|
+
setOauth(next);
|
|
138
|
+
if (steps[stepIndex] === steps[steps.length - 1]) {
|
|
139
|
+
finalize(properties, next);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
advance();
|
|
44
143
|
}
|
|
45
144
|
};
|
|
46
145
|
const renderInput = () => {
|
|
47
|
-
switch (currentStep
|
|
146
|
+
switch (currentStep) {
|
|
48
147
|
case 'domain':
|
|
49
|
-
return (_jsx(TextInput, { label: "Development Domain (e.g. www.sitevision.se)", defaultValue: properties.domain, placeholder: "sitevision.se", onSubmit:
|
|
148
|
+
return (_jsx(TextInput, { label: "Development Domain (e.g. www.sitevision.se)", defaultValue: properties.domain, placeholder: "sitevision.se", onSubmit: value => submitProperty('domain', value), onCancel: onCancel }, "domain"));
|
|
50
149
|
case 'siteName':
|
|
51
|
-
return (_jsx(TextInput, { label: "Site Name (Root node name)", defaultValue: properties.siteName, onSubmit:
|
|
150
|
+
return (_jsx(TextInput, { label: "Site Name (Root node name)", defaultValue: properties.siteName, onSubmit: value => submitProperty('siteName', value), onCancel: onCancel }, "siteName"));
|
|
52
151
|
case 'addonName':
|
|
53
|
-
return (_jsx(TextInput, { label: "Addon Name", defaultValue: properties.addonName, onSubmit:
|
|
152
|
+
return (_jsx(TextInput, { label: "Addon Name", defaultValue: properties.addonName, onSubmit: value => submitProperty('addonName', value), onCancel: onCancel }, "addonName"));
|
|
54
153
|
case 'username':
|
|
55
|
-
return (_jsx(TextInput, { label: "Username (usually your Sitevision Cloud email)", defaultValue: properties.username, onSubmit:
|
|
154
|
+
return (_jsx(TextInput, { label: "Username (usually your Sitevision Cloud email)", defaultValue: properties.username, onSubmit: value => submitProperty('username', value), onCancel: onCancel }, "username"));
|
|
155
|
+
case 'authMethod':
|
|
156
|
+
return (_jsx(MethodSelect, { defaultValue: method, onSubmit: value => submitProperty('authMethod', value) }, "authMethod"));
|
|
56
157
|
case 'password':
|
|
57
|
-
return (_jsx(TextInput, { label: "Password (saved in OS keychain \u2014 leave empty to prompt on each run)", type: "password", defaultValue: properties.password, onSubmit:
|
|
158
|
+
return (_jsx(TextInput, { label: "Password (saved in OS keychain \u2014 leave empty to prompt on each run)", type: "password", defaultValue: properties.password, onSubmit: value => submitProperty('password', value), onCancel: onCancel }, "password"));
|
|
159
|
+
case 'oauthClientId':
|
|
160
|
+
return (_jsx(TextInput, { label: "OAuth2 Client ID", defaultValue: oauth.clientId, onSubmit: value => submitOAuth('clientId', value), onCancel: onCancel }, "oauthClientId"));
|
|
161
|
+
case 'oauthAuthEndpoint':
|
|
162
|
+
return (_jsx(TextInput, { label: "Authorization Endpoint URL", defaultValue: oauth.authorizationEndpoint, onSubmit: value => submitOAuth('authorizationEndpoint', value), onCancel: onCancel }, "oauthAuthEndpoint"));
|
|
163
|
+
case 'oauthTokenEndpoint':
|
|
164
|
+
return (_jsx(TextInput, { label: "Token Endpoint URL", defaultValue: oauth.tokenEndpoint, onSubmit: value => submitOAuth('tokenEndpoint', value), onCancel: onCancel }, "oauthTokenEndpoint"));
|
|
165
|
+
case 'oauthScopes':
|
|
166
|
+
return (_jsx(TextInput, { label: "Scopes (space-separated, optional)", defaultValue: oauth.scopes, onSubmit: value => submitOAuth('scopes', value), onCancel: onCancel }, "oauthScopes"));
|
|
167
|
+
case 'oauthClientSecret':
|
|
168
|
+
return (_jsx(TextInput, { label: "Client Secret (OS keychain \u2014 leave empty for a public/PKCE client)", type: "password", defaultValue: oauth.clientSecret, onSubmit: value => submitOAuth('clientSecret', value), onCancel: onCancel }, "oauthClientSecret"));
|
|
169
|
+
case 'sessionLoginUrl':
|
|
170
|
+
return (_jsx(TextInput, { label: "Login URL (opened in a browser; blank = site root)", defaultValue: properties.sessionLoginUrl ??
|
|
171
|
+
(properties.domain ? `https://${properties.domain}/` : ''), onSubmit: value => submitProperty('sessionLoginUrl', value), onCancel: onCancel }, "sessionLoginUrl"));
|
|
58
172
|
case 'useHTTP':
|
|
59
|
-
return (_jsx(BooleanInput, { label: "Use HTTP for deployment? (y/n)", defaultValue: properties.useHTTPForDevDeploy ?? false, onSubmit:
|
|
173
|
+
return (_jsx(BooleanInput, { label: "Use HTTP for deployment? (y/n)", defaultValue: properties.useHTTPForDevDeploy ?? false, onSubmit: value => submitProperty('useHTTPForDevDeploy', value) }, "useHTTP"));
|
|
60
174
|
default:
|
|
61
175
|
return null;
|
|
62
176
|
}
|
|
63
177
|
};
|
|
64
|
-
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 ",
|
|
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() })] }));
|
|
179
|
+
}
|
|
180
|
+
function MethodSelect({ defaultValue, onSubmit, }) {
|
|
181
|
+
const [index, setIndex] = useState(() => Math.max(0, AUTH_METHODS.findIndex(m => m.value === defaultValue)));
|
|
182
|
+
useInput((_input, key) => {
|
|
183
|
+
if (key.upArrow) {
|
|
184
|
+
setIndex(p => (p === 0 ? AUTH_METHODS.length - 1 : p - 1));
|
|
185
|
+
}
|
|
186
|
+
else if (key.downArrow) {
|
|
187
|
+
setIndex(p => (p === AUTH_METHODS.length - 1 ? 0 : p + 1));
|
|
188
|
+
}
|
|
189
|
+
else if (key.return) {
|
|
190
|
+
onSubmit(AUTH_METHODS[index].value);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Authentication method" }) }), AUTH_METHODS.map((m, i) => (_jsxs(Text, { color: i === index ? 'green' : undefined, children: [i === index ? '❯ ' : ' ', m.label] }, m.value))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move, Enter to select" }) })] }));
|
|
65
194
|
}
|
|
66
195
|
function BooleanInput({ label, defaultValue, onSubmit, }) {
|
|
67
196
|
useInput(input => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { getAppType, localizedText, migrateLegacyPassword, getPackageJsonSyncChanges, syncDevPropertiesToPackageJson, readSvcConfig, writeSvcConfig, } from '../utils/project-detection.js';
|
|
4
|
+
import { getAppType, localizedText, migrateLegacyPassword, getPackageJsonSyncChanges, syncDevPropertiesToPackageJson, readSvcConfig, writeSvcConfig, writeDevProperties, } from '../utils/project-detection.js';
|
|
5
5
|
import { ProcessRunner } from '../utils/process-runner.js';
|
|
6
6
|
import { ProcessOutputComponent } from './ProcessOutput.js';
|
|
7
7
|
import { StatusIndicator } from './StatusIndicator.js';
|
|
@@ -13,6 +13,9 @@ export function SetupFlow({ project, onReload, onComplete }) {
|
|
|
13
13
|
const [commandStatus, setCommandStatus] = useState('running');
|
|
14
14
|
const [syncChanges, setSyncChanges] = useState([]);
|
|
15
15
|
const [syncDecision, setSyncDecision] = useState(false);
|
|
16
|
+
// Set when the user picks OAuth2 for an existing config, so the form opens in
|
|
17
|
+
// the OAuth2 branch without mutating the shared project object.
|
|
18
|
+
const [pendingAuthMethod, setPendingAuthMethod] = useState(undefined);
|
|
16
19
|
const appType = getAppType(project.manifest);
|
|
17
20
|
// Auto-advance through checks
|
|
18
21
|
useEffect(() => {
|
|
@@ -29,6 +32,9 @@ export function SetupFlow({ project, onReload, onComplete }) {
|
|
|
29
32
|
if (project.hasLegacyPassword) {
|
|
30
33
|
setStep('confirm-password-migration');
|
|
31
34
|
}
|
|
35
|
+
else if (project.devProperties?.authMethod === undefined) {
|
|
36
|
+
setStep('confirm-auth-method');
|
|
37
|
+
}
|
|
32
38
|
else {
|
|
33
39
|
setStep('check-package-sync');
|
|
34
40
|
}
|
|
@@ -142,10 +148,33 @@ export function SetupFlow({ project, onReload, onComplete }) {
|
|
|
142
148
|
setStep('show-info');
|
|
143
149
|
}
|
|
144
150
|
}
|
|
151
|
+
else if (step === 'confirm-auth-method') {
|
|
152
|
+
if (input === 'o' || input === 'O') {
|
|
153
|
+
// Choosing OAuth2 needs endpoints — collect them in the full form.
|
|
154
|
+
setPendingAuthMethod('oauth2');
|
|
155
|
+
setStep('setup-dev-properties');
|
|
156
|
+
}
|
|
157
|
+
else if (input === 'c' || input === 'C') {
|
|
158
|
+
setPendingAuthMethod('cookie');
|
|
159
|
+
setStep('setup-dev-properties');
|
|
160
|
+
}
|
|
161
|
+
else if (['b', 'B', '\r'].includes(input)) {
|
|
162
|
+
if (project.devProperties) {
|
|
163
|
+
writeDevProperties(project.root, {
|
|
164
|
+
...project.devProperties,
|
|
165
|
+
authMethod: 'basic',
|
|
166
|
+
});
|
|
167
|
+
onReload();
|
|
168
|
+
}
|
|
169
|
+
setStep('check-package-sync');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
145
172
|
});
|
|
146
173
|
// Setup Dev Properties Form
|
|
147
174
|
if (step === 'setup-dev-properties') {
|
|
148
|
-
return (_jsx(DevPropertiesForm, { projectRoot: project.root, initialProperties:
|
|
175
|
+
return (_jsx(DevPropertiesForm, { projectRoot: project.root, initialProperties: pendingAuthMethod && project.devProperties
|
|
176
|
+
? { ...project.devProperties, authMethod: pendingAuthMethod }
|
|
177
|
+
: project.devProperties, packageJson: project.packageJson, onComplete: () => {
|
|
149
178
|
// Re-detect from disk/keychain so devProperties (incl. the keychain
|
|
150
179
|
// password) populate in memory — otherwise the rest of this flow and
|
|
151
180
|
// the menu would see stale state until the CLI is restarted.
|
|
@@ -178,6 +207,10 @@ export function SetupFlow({ project, onReload, onComplete }) {
|
|
|
178
207
|
if (step === 'confirm-password-migration') {
|
|
179
208
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 Plaintext password found in .dev_properties.json" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Move it to the OS keychain and remove it from the file? (y/n)" }), _jsx(Text, { dimColor: true, children: "Recommended \u2014 storing passwords in project files is insecure." })] })] }));
|
|
180
209
|
}
|
|
210
|
+
// Ask which auth method an existing config should use (setting is missing)
|
|
211
|
+
if (step === 'confirm-auth-method') {
|
|
212
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 Deploy authentication method not set" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Which method should deploys use? [B]asic / [O]Auth2 / [C]ookie" }), _jsx(Text, { dimColor: true, children: "B = Basic (default), O = OAuth2 bearer, C = session cookie (SSO)." })] })] }));
|
|
213
|
+
}
|
|
181
214
|
// Confirm package.json sync
|
|
182
215
|
if (step === 'confirm-package-sync') {
|
|
183
216
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 package.json is out of sync with .dev_properties.json" }) }), _jsx(Box, { marginBottom: 1, flexDirection: "column", marginLeft: 2, children: syncChanges.map(change => (_jsxs(Box, { children: [_jsx(Text, { color: change.from === undefined ? 'green' : 'yellow', children: change.from === undefined ? '+ ' : '~ ' }), _jsxs(Text, { bold: true, children: [change.key, ": "] }), change.from !== undefined && (_jsxs(Text, { dimColor: true, children: [change.from, " \u2192 "] })), _jsx(Text, { children: change.to })] }, change.key))) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Update package.json from .dev_properties.json? (y/n)" }), _jsx(Text, { dimColor: true, children: "sitevision-scripts reads these fields from package.json." })] })] }));
|
package/dist/types/index.d.ts
CHANGED
|
@@ -48,6 +48,24 @@ export interface DevProperties {
|
|
|
48
48
|
useHTTPForDevDeploy?: boolean;
|
|
49
49
|
signingUsername?: string;
|
|
50
50
|
certificateName?: string;
|
|
51
|
+
authMethod?: 'basic' | 'oauth2' | 'cookie';
|
|
52
|
+
oauth2?: OAuth2Config;
|
|
53
|
+
sessionLoginUrl?: string;
|
|
54
|
+
accessToken?: string;
|
|
55
|
+
sessionCookie?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* OAuth2 provider config for bearer-token deploys.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately secret-free: the client secret and refresh token live in the OS
|
|
61
|
+
* keychain, so nothing here is unsafe to write to .dev_properties.json.
|
|
62
|
+
*/
|
|
63
|
+
export interface OAuth2Config {
|
|
64
|
+
authorizationEndpoint: string;
|
|
65
|
+
tokenEndpoint: string;
|
|
66
|
+
clientId: string;
|
|
67
|
+
scopes?: string[];
|
|
68
|
+
redirectPort?: number;
|
|
51
69
|
}
|
|
52
70
|
/**
|
|
53
71
|
* Signing credentials (password is runtime-only, not persisted)
|
|
@@ -65,7 +83,9 @@ export interface DeployConfig {
|
|
|
65
83
|
siteName: string;
|
|
66
84
|
addonName: string;
|
|
67
85
|
username: string;
|
|
68
|
-
password
|
|
86
|
+
password?: string;
|
|
87
|
+
accessToken?: string;
|
|
88
|
+
sessionCookie?: string;
|
|
69
89
|
useHTTP?: boolean;
|
|
70
90
|
}
|
|
71
91
|
/**
|
|
@@ -149,6 +169,7 @@ export interface DeployResponse {
|
|
|
149
169
|
executableId?: string;
|
|
150
170
|
message?: string;
|
|
151
171
|
error?: string;
|
|
172
|
+
authExpired?: boolean;
|
|
152
173
|
}
|
|
153
174
|
/**
|
|
154
175
|
* API response from addon creation
|
|
@@ -157,6 +178,7 @@ export interface CreateAddonResponse {
|
|
|
157
178
|
success: boolean;
|
|
158
179
|
addonId?: string;
|
|
159
180
|
error?: string;
|
|
181
|
+
authExpired?: boolean;
|
|
160
182
|
}
|
|
161
183
|
/**
|
|
162
184
|
* API response from activation
|
|
@@ -164,6 +186,7 @@ export interface CreateAddonResponse {
|
|
|
164
186
|
export interface ActivationResponse {
|
|
165
187
|
success: boolean;
|
|
166
188
|
error?: string;
|
|
189
|
+
authExpired?: boolean;
|
|
167
190
|
}
|
|
168
191
|
/**
|
|
169
192
|
* Build mode
|
package/dist/utils/keychain.d.ts
CHANGED
|
@@ -4,3 +4,12 @@ export declare function deleteDeployPassword(domain: string, username: string):
|
|
|
4
4
|
export declare function getSigningPassword(username: string): string | null;
|
|
5
5
|
export declare function setSigningPassword(username: string, password: string): boolean;
|
|
6
6
|
export declare function deleteSigningPassword(username: string): void;
|
|
7
|
+
export declare function getOAuth2RefreshToken(domain: string, clientId: string): string | null;
|
|
8
|
+
export declare function setOAuth2RefreshToken(domain: string, clientId: string, token: string): boolean;
|
|
9
|
+
export declare function deleteOAuth2RefreshToken(domain: string, clientId: string): void;
|
|
10
|
+
export declare function getOAuth2ClientSecret(domain: string, clientId: string): string | null;
|
|
11
|
+
export declare function setOAuth2ClientSecret(domain: string, clientId: string, secret: string): boolean;
|
|
12
|
+
export declare function deleteOAuth2ClientSecret(domain: string, clientId: string): void;
|
|
13
|
+
export declare function getSessionCookie(domain: string, username: string): string | null;
|
|
14
|
+
export declare function setSessionCookie(domain: string, username: string, cookie: string): boolean;
|
|
15
|
+
export declare function deleteSessionCookie(domain: string, username: string): void;
|