@edgestore/cli 1.0.0-next.2 → 1.0.0-next.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 +4 -3
- package/dist/bin.mjs +156 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,9 +14,10 @@ edgestore login
|
|
|
14
14
|
edgestore init
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Use `edgestore login --
|
|
18
|
-
|
|
19
|
-
system credential store and are never
|
|
17
|
+
Use `edgestore login --device` when a local browser callback is unavailable.
|
|
18
|
+
Use `edgestore login --token` or `EDGESTORE_TOKEN` for automation. Persisted
|
|
19
|
+
credentials are stored in the operating system credential store and are never
|
|
20
|
+
written to a plaintext config file.
|
|
20
21
|
|
|
21
22
|
The CLI manages accounts, projects and their keys, management tokens, buckets,
|
|
22
23
|
files, uploads, team members, and invitations. Secrets are returned only when
|
package/dist/bin.mjs
CHANGED
|
@@ -414,12 +414,21 @@ const oauthCredentialSchema = z.object({
|
|
|
414
414
|
resource: z.string().url(),
|
|
415
415
|
scope: z.string().optional()
|
|
416
416
|
}).strict();
|
|
417
|
-
const oauthClientRegistrationSchema = z.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
417
|
+
const oauthClientRegistrationSchema = z.discriminatedUnion('flow', [
|
|
418
|
+
z.object({
|
|
419
|
+
version: z.literal(2),
|
|
420
|
+
flow: z.literal('browser'),
|
|
421
|
+
clientId: z.string().min(1),
|
|
422
|
+
issuer: z.string().url(),
|
|
423
|
+
redirectUri: z.string().url()
|
|
424
|
+
}).strict(),
|
|
425
|
+
z.object({
|
|
426
|
+
version: z.literal(2),
|
|
427
|
+
flow: z.literal('device'),
|
|
428
|
+
clientId: z.string().min(1),
|
|
429
|
+
issuer: z.string().url()
|
|
430
|
+
}).strict()
|
|
431
|
+
]);
|
|
423
432
|
class KeyringCredentialStore {
|
|
424
433
|
async get(apiOrigin) {
|
|
425
434
|
const entry = await createEntry(CREDENTIAL_NAME, apiOrigin);
|
|
@@ -735,6 +744,7 @@ const resourceMetadataSchema = z.object({
|
|
|
735
744
|
scopes_supported: z.array(z.string().min(1)).min(1),
|
|
736
745
|
bearer_methods_supported: z.array(z.string()).optional()
|
|
737
746
|
}).passthrough();
|
|
747
|
+
const deviceGrantType = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
738
748
|
class DefaultOAuthService {
|
|
739
749
|
fetchImplementation;
|
|
740
750
|
constructor(fetchImplementation = fetch){
|
|
@@ -753,6 +763,19 @@ class DefaultOAuthService {
|
|
|
753
763
|
});
|
|
754
764
|
}
|
|
755
765
|
}
|
|
766
|
+
async loginWithDeviceCode(input) {
|
|
767
|
+
try {
|
|
768
|
+
return await this.performDeviceLoginWithRecovery(input);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (input.signal.aborted || error instanceof CliError) throw error;
|
|
771
|
+
throw new CliError('oauth_login_failed', oauthErrorMessage(error), {
|
|
772
|
+
suggestions: [
|
|
773
|
+
'edgestore login --device',
|
|
774
|
+
'edgestore login --token'
|
|
775
|
+
]
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
756
779
|
async performLoginWithRecovery(input) {
|
|
757
780
|
try {
|
|
758
781
|
return await this.performLogin(input);
|
|
@@ -764,6 +787,17 @@ class DefaultOAuthService {
|
|
|
764
787
|
});
|
|
765
788
|
}
|
|
766
789
|
}
|
|
790
|
+
async performDeviceLoginWithRecovery(input) {
|
|
791
|
+
try {
|
|
792
|
+
return await this.performDeviceLogin(input);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
if (!input.client || !isInvalidClientError(error)) throw error;
|
|
795
|
+
return await this.performDeviceLogin({
|
|
796
|
+
...input,
|
|
797
|
+
client: undefined
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
767
801
|
async refresh(credential, signal) {
|
|
768
802
|
try {
|
|
769
803
|
const config = await this.discover(new URL(credential.issuer), {
|
|
@@ -782,10 +816,11 @@ class DefaultOAuthService {
|
|
|
782
816
|
});
|
|
783
817
|
} catch (error) {
|
|
784
818
|
if (signal.aborted || error instanceof CliError) throw error;
|
|
785
|
-
throw new CliError('oauth_refresh_failed', 'The
|
|
819
|
+
throw new CliError('oauth_refresh_failed', 'The OAuth login could not be refreshed.', {
|
|
786
820
|
details: oauthErrorMessage(error),
|
|
787
821
|
suggestions: [
|
|
788
822
|
'edgestore login',
|
|
823
|
+
'edgestore login --device',
|
|
789
824
|
'edgestore login --token'
|
|
790
825
|
]
|
|
791
826
|
});
|
|
@@ -819,7 +854,8 @@ class DefaultOAuthService {
|
|
|
819
854
|
throw new CliError('oauth_registration_failed', 'The OAuth server did not return a client ID.');
|
|
820
855
|
}
|
|
821
856
|
const clientRegistration = {
|
|
822
|
-
version:
|
|
857
|
+
version: 2,
|
|
858
|
+
flow: 'browser',
|
|
823
859
|
clientId,
|
|
824
860
|
issuer: issuerIdentifier,
|
|
825
861
|
redirectUri: callback.redirectUri
|
|
@@ -864,13 +900,69 @@ class DefaultOAuthService {
|
|
|
864
900
|
await callback.close();
|
|
865
901
|
}
|
|
866
902
|
}
|
|
903
|
+
async performDeviceLogin(input) {
|
|
904
|
+
const metadata = await this.protectedResourceMetadata(input.apiOrigin, input.resource, input.signal);
|
|
905
|
+
const issuer = new URL(metadata.authorization_servers[0]);
|
|
906
|
+
const issuerIdentifier = normalizeUrl(issuer.toString());
|
|
907
|
+
const reusableClient = reusableDeviceClientRegistration(input.client, issuer);
|
|
908
|
+
const config = reusableClient ? await this.discover(issuer, {
|
|
909
|
+
clientId: reusableClient.clientId,
|
|
910
|
+
signal: input.signal
|
|
911
|
+
}) : await this.registerDeviceClient(issuer, input.signal);
|
|
912
|
+
const clientId = config.clientMetadata().client_id;
|
|
913
|
+
if (!clientId) {
|
|
914
|
+
throw new CliError('oauth_registration_failed', 'The OAuth server did not return a client ID.');
|
|
915
|
+
}
|
|
916
|
+
const clientRegistration = reusableClient ?? {
|
|
917
|
+
version: 2,
|
|
918
|
+
flow: 'device',
|
|
919
|
+
clientId,
|
|
920
|
+
issuer: issuerIdentifier
|
|
921
|
+
};
|
|
922
|
+
if (!reusableClient) {
|
|
923
|
+
await input.onClientRegistered?.(clientRegistration);
|
|
924
|
+
}
|
|
925
|
+
const authorization = await oauth.initiateDeviceAuthorization(config, {
|
|
926
|
+
resource: metadata.resource,
|
|
927
|
+
scope: [
|
|
928
|
+
...new Set(metadata.scopes_supported)
|
|
929
|
+
].join(' ')
|
|
930
|
+
});
|
|
931
|
+
input.onDeviceAuthorization?.({
|
|
932
|
+
userCode: authorization.user_code,
|
|
933
|
+
verificationUri: authorization.verification_uri,
|
|
934
|
+
...authorization.verification_uri_complete ? {
|
|
935
|
+
verificationUriComplete: authorization.verification_uri_complete
|
|
936
|
+
} : {},
|
|
937
|
+
expiresIn: authorization.expires_in
|
|
938
|
+
});
|
|
939
|
+
const verificationUrl = authorization.verification_uri_complete ?? authorization.verification_uri;
|
|
940
|
+
try {
|
|
941
|
+
await input.openUrl(verificationUrl);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
input.onBrowserOpenFailed?.(verificationUrl, error);
|
|
944
|
+
}
|
|
945
|
+
const tokens = await oauth.pollDeviceAuthorizationGrant(config, authorization, {
|
|
946
|
+
resource: metadata.resource
|
|
947
|
+
}, {
|
|
948
|
+
signal: input.signal
|
|
949
|
+
});
|
|
950
|
+
return {
|
|
951
|
+
credential: credentialFromTokens(tokens, {
|
|
952
|
+
clientId,
|
|
953
|
+
issuer: issuerIdentifier,
|
|
954
|
+
resource: metadata.resource
|
|
955
|
+
}),
|
|
956
|
+
client: clientRegistration
|
|
957
|
+
};
|
|
958
|
+
}
|
|
867
959
|
async protectedResourceMetadata(apiOrigin, expectedResource, signal) {
|
|
868
960
|
const metadataUrl = new URL('/.well-known/oauth-protected-resource/v2', apiOrigin);
|
|
869
961
|
const response = await this.fetchImplementation(metadataUrl, {
|
|
870
962
|
signal
|
|
871
963
|
});
|
|
872
964
|
if (!response.ok) {
|
|
873
|
-
throw new CliError('oauth_metadata_unavailable', `The EdgeStore API does not advertise
|
|
965
|
+
throw new CliError('oauth_metadata_unavailable', `The EdgeStore API does not advertise OAuth login (${response.status}).`, {
|
|
874
966
|
suggestions: [
|
|
875
967
|
'edgestore login --token'
|
|
876
968
|
]
|
|
@@ -904,6 +996,19 @@ class DefaultOAuthService {
|
|
|
904
996
|
]
|
|
905
997
|
}, oauth.None(), this.discoveryOptions(issuer, signal));
|
|
906
998
|
}
|
|
999
|
+
async registerDeviceClient(issuer, signal) {
|
|
1000
|
+
return await oauth.dynamicClientRegistration(issuer, {
|
|
1001
|
+
application_type: 'native',
|
|
1002
|
+
client_name: 'EdgeStore CLI',
|
|
1003
|
+
redirect_uris: [],
|
|
1004
|
+
token_endpoint_auth_method: 'none',
|
|
1005
|
+
grant_types: [
|
|
1006
|
+
deviceGrantType,
|
|
1007
|
+
'refresh_token'
|
|
1008
|
+
],
|
|
1009
|
+
response_types: []
|
|
1010
|
+
}, oauth.None(), this.discoveryOptions(issuer, signal));
|
|
1011
|
+
}
|
|
907
1012
|
async discover(issuer, options) {
|
|
908
1013
|
return await oauth.discovery(issuer, options.clientId, {
|
|
909
1014
|
token_endpoint_auth_method: 'none',
|
|
@@ -950,7 +1055,13 @@ async function openCallback(state, signal, preferredRedirectUri) {
|
|
|
950
1055
|
}
|
|
951
1056
|
}
|
|
952
1057
|
function reusableClientRegistration(client, issuer) {
|
|
953
|
-
|
|
1058
|
+
if (client?.flow !== 'browser' || normalizeUrl(client.issuer) !== normalizeUrl(issuer.toString()) || !isReusableOAuthRedirectUri(client.redirectUri)) {
|
|
1059
|
+
return undefined;
|
|
1060
|
+
}
|
|
1061
|
+
return client;
|
|
1062
|
+
}
|
|
1063
|
+
function reusableDeviceClientRegistration(client, issuer) {
|
|
1064
|
+
return client?.flow === 'device' && normalizeUrl(client.issuer) === normalizeUrl(issuer.toString()) ? client : undefined;
|
|
954
1065
|
}
|
|
955
1066
|
function credentialFromTokens(tokens, context) {
|
|
956
1067
|
const refreshToken = tokens.refresh_token ?? context.fallbackRefreshToken;
|
|
@@ -973,7 +1084,7 @@ function normalizeUrl(value) {
|
|
|
973
1084
|
return new URL(value).toString().replace(/\/$/, '');
|
|
974
1085
|
}
|
|
975
1086
|
function isInvalidClientError(error) {
|
|
976
|
-
return (error instanceof oauth.AuthorizationResponseError || error instanceof oauth.ResponseBodyError) && error.error === 'invalid_client';
|
|
1087
|
+
return (error instanceof oauth.AuthorizationResponseError || error instanceof oauth.ResponseBodyError) && (error.error === 'invalid_client' || error.error === 'unauthorized_client');
|
|
977
1088
|
}
|
|
978
1089
|
function isLoopback(url) {
|
|
979
1090
|
return url.hostname === 'localhost' || url.hostname === '127.0.0.1';
|
|
@@ -983,7 +1094,7 @@ function isAddressInUse(error) {
|
|
|
983
1094
|
}
|
|
984
1095
|
function oauthErrorMessage(error) {
|
|
985
1096
|
if (error instanceof Error && error.message) return error.message;
|
|
986
|
-
return '
|
|
1097
|
+
return 'OAuth login failed.';
|
|
987
1098
|
}
|
|
988
1099
|
|
|
989
1100
|
class DefaultCliPrompts {
|
|
@@ -1470,10 +1581,21 @@ async function exists(candidate) {
|
|
|
1470
1581
|
}
|
|
1471
1582
|
|
|
1472
1583
|
async function loginCommand(runtime, flags, options) {
|
|
1473
|
-
if (
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1584
|
+
if (options.device && options.token) {
|
|
1585
|
+
throw usageError('conflicting_login_modes', '--device and --token cannot be used together.');
|
|
1586
|
+
}
|
|
1587
|
+
const machineOutputFlag = flags.json ? '--json' : flags.plain ? '--plain' : undefined;
|
|
1588
|
+
if (!options.token && machineOutputFlag) {
|
|
1589
|
+
throw usageError('interactive_input_disabled', `OAuth login is interactive and cannot be used with ${machineOutputFlag}.`, [
|
|
1590
|
+
'edgestore login',
|
|
1591
|
+
'edgestore login --device'
|
|
1592
|
+
]);
|
|
1593
|
+
}
|
|
1594
|
+
if (options.device) return await oauthLogin(runtime, flags, 'device');
|
|
1595
|
+
if (!options.token) return await oauthLogin(runtime, flags, 'browser');
|
|
1596
|
+
if (machineOutputFlag && runtime.io.inputIsTty) {
|
|
1597
|
+
throw usageError('interactive_input_disabled', `Interactive token input is disabled with ${machineOutputFlag}.`, [
|
|
1598
|
+
`printf %s "$EDGESTORE_TOKEN" | edgestore login --token ${machineOutputFlag}`
|
|
1477
1599
|
]);
|
|
1478
1600
|
}
|
|
1479
1601
|
const token = await runtime.prompts.readToken(runtime.io.stdin, runtime.io.inputIsTty);
|
|
@@ -1633,24 +1755,34 @@ function actorLabel(actor) {
|
|
|
1633
1755
|
}
|
|
1634
1756
|
return actor.user.email;
|
|
1635
1757
|
}
|
|
1636
|
-
async function
|
|
1758
|
+
async function oauthLogin(runtime, flags, mode) {
|
|
1637
1759
|
const apiUrl = apiUrlFor(runtime, flags);
|
|
1638
1760
|
const output = outputFor(runtime, flags);
|
|
1639
|
-
const
|
|
1761
|
+
const sharedInput = {
|
|
1640
1762
|
apiOrigin: apiUrl.displayUrl,
|
|
1641
1763
|
resource: apiUrl.sdkBaseUrl,
|
|
1642
1764
|
client: await runtime.credentials.getCachedOAuthClient(apiUrl.displayUrl),
|
|
1643
1765
|
signal: runtime.signal,
|
|
1644
1766
|
openUrl: (url)=>runtime.openUrl(url),
|
|
1645
|
-
onAuthorizationUrl: (url)=>{
|
|
1646
|
-
if (output.options.mode === 'human') {
|
|
1647
|
-
runtime.io.stderr.write(`Opening a browser to continue login...\nIf it does not open, visit:\n ${url}\n`);
|
|
1648
|
-
}
|
|
1649
|
-
},
|
|
1650
1767
|
onBrowserOpenFailed: (url)=>{
|
|
1651
1768
|
output.warning(`Could not open a browser automatically. Open this URL:\n ${url}`);
|
|
1652
1769
|
},
|
|
1653
1770
|
onClientRegistered: (client)=>runtime.credentials.setCachedOAuthClient(apiUrl.displayUrl, client)
|
|
1771
|
+
};
|
|
1772
|
+
const result = mode === 'device' ? await runtime.oauth.loginWithDeviceCode({
|
|
1773
|
+
...sharedInput,
|
|
1774
|
+
onDeviceAuthorization: (authorization)=>{
|
|
1775
|
+
if (output.options.mode !== 'human') return;
|
|
1776
|
+
const url = authorization.verificationUriComplete ?? authorization.verificationUri;
|
|
1777
|
+
runtime.io.stderr.write(`Confirm this one-time code in your browser:\n ${authorization.userCode}\nOpening a browser to continue login...\nIf it does not open, visit:\n ${url}\n`);
|
|
1778
|
+
}
|
|
1779
|
+
}) : await runtime.oauth.login({
|
|
1780
|
+
...sharedInput,
|
|
1781
|
+
onAuthorizationUrl: (url)=>{
|
|
1782
|
+
if (output.options.mode === 'human') {
|
|
1783
|
+
runtime.io.stderr.write(`Opening a browser to continue login...\nIf it does not open, visit:\n ${url}\n`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1654
1786
|
});
|
|
1655
1787
|
let identity;
|
|
1656
1788
|
let credentialStored = false;
|
|
@@ -5068,7 +5200,7 @@ Common workflows:
|
|
|
5068
5200
|
}
|
|
5069
5201
|
outputFor(runtime, globalFlags(program));
|
|
5070
5202
|
});
|
|
5071
|
-
program.command('login').description('Log in to EdgeStore').option('--token', 'read and securely store a management token').action(async (options)=>{
|
|
5203
|
+
program.command('login').description('Log in to EdgeStore').option('--device', 'log in with a device code').option('--token', 'read and securely store a management token').action(async (options)=>{
|
|
5072
5204
|
await loginCommand(runtime, globalFlags(program), options);
|
|
5073
5205
|
});
|
|
5074
5206
|
program.command('logout').description('Remove the stored login').action(async ()=>{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgestore/cli",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.3",
|
|
4
4
|
"description": "Command-line interface for EdgeStore accounts and projects",
|
|
5
5
|
"homepage": "https://edgestore.dev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@clack/prompts": "1.7.0",
|
|
39
|
-
"@edgestore/sdk": "1.0.0-next.
|
|
39
|
+
"@edgestore/sdk": "1.0.0-next.3",
|
|
40
40
|
"@manypkg/find-root": "3.1.0",
|
|
41
41
|
"@manypkg/get-packages": "3.1.0",
|
|
42
42
|
"@napi-rs/keyring": "1.3.0",
|