@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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/bin.mjs +156 -24
  3. 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 --token` or `EDGESTORE_TOKEN` for automation and other
18
- non-browser environments. Persisted credentials are stored in the operating
19
- system credential store and are never written to a plaintext config file.
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.object({
418
- version: z.literal(1),
419
- clientId: z.string().min(1),
420
- issuer: z.string().url(),
421
- redirectUri: z.string().url()
422
- }).strict();
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 browser login could not be refreshed.', {
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: 1,
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 browser login (${response.status}).`, {
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
- return client && normalizeUrl(client.issuer) === normalizeUrl(issuer.toString()) && isReusableOAuthRedirectUri(client.redirectUri) ? client : undefined;
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 'Browser login failed.';
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 (!options.token) return await browserLogin(runtime, flags);
1474
- if (flags.json && runtime.io.inputIsTty) {
1475
- throw usageError('interactive_input_disabled', 'Interactive token input is disabled with --json.', [
1476
- 'printf %s "$EDGESTORE_TOKEN" | edgestore login --token --json'
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 browserLogin(runtime, flags) {
1758
+ async function oauthLogin(runtime, flags, mode) {
1637
1759
  const apiUrl = apiUrlFor(runtime, flags);
1638
1760
  const output = outputFor(runtime, flags);
1639
- const result = await runtime.oauth.login({
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.2",
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.2",
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",