From cec32125d35ef0d4c2e6bfbc41ef9e5028345824 Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Thu, 19 Feb 2026 11:02:40 -0600 Subject: [PATCH] feat: add support for china environment --- .changeset/late-peaches-ring.md | 5 + package-lock.json | 69 +++++--- package.json | 6 +- src/__tests__/lib/command/api-command.test.ts | 159 +++++++++++++++--- src/__tests__/lib/login-authenticator.test.ts | 33 ++-- src/commands/apps/create.ts | 10 +- src/commands/edge/drivers.ts | 14 +- src/commands/locations/modes/getcurrent.ts | 2 +- src/index.ts | 7 +- src/lib/command/api-command.ts | 98 +++++++++-- src/lib/command/api-organization-command.ts | 8 +- src/lib/command/format.ts | 35 ++-- src/lib/command/input-and-output-item.ts | 11 +- src/lib/command/output-builder.ts | 24 +-- src/lib/login-authenticator.ts | 32 ++-- src/run.ts | 4 +- 16 files changed, 385 insertions(+), 132 deletions(-) create mode 100644 .changeset/late-peaches-ring.md diff --git a/.changeset/late-peaches-ring.md b/.changeset/late-peaches-ring.md new file mode 100644 index 00000000..1cb1a097 --- /dev/null +++ b/.changeset/late-peaches-ring.md @@ -0,0 +1,5 @@ +--- +"@smartthings/cli": minor +--- + +add support for China environment diff --git a/package-lock.json b/package-lock.json index 793fb884..8c074823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@aws-sdk/client-lambda": "^3.899.0", "@inquirer/prompts": "^7.8.6", - "@smartthings/core-sdk": "^8.4.1", + "@smartthings/core-sdk": "^8.5.0", "axios": "1.12.2", "chalk": "^5.6.2", "env-paths": "^3.0.0", @@ -4008,23 +4008,34 @@ } }, "node_modules/@smartthings/core-sdk": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@smartthings/core-sdk/-/core-sdk-8.4.1.tgz", - "integrity": "sha512-1cFSRd45mdn43IerUMg9O55uxjkxVY/kdTcZ2b1H04XO0FwpDfZpgB0NAbU+yfBwnK7/uQ0yvNpEDZ8FOkJARg==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@smartthings/core-sdk/-/core-sdk-8.5.0.tgz", + "integrity": "sha512-0PH39onvLjc9hNwivQLXz9FTa13zdYAPKzZDbYmfbd7ZRILRWfUoSl79xUg+Q4yabdZyiJY+TFLf7pK9YL6W7A==", "license": "Apache-2.0", "dependencies": { "async-mutex": "^0.5.0", - "axios": "^1.8.3", + "axios": "^1.13.5", "http-signature": "^1.4.0", "lodash.isdate": "^4.0.1", "lodash.isstring": "^4.0.1", - "qs": "^6.14.0", + "qs": "^6.15.0", "sshpk": "^1.18.0" }, "engines": { "node": ">=22" } }, + "node_modules/@smartthings/core-sdk/node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@smithy/abort-controller": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", @@ -6148,6 +6159,7 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } @@ -6156,6 +6168,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", "engines": { "node": ">=0.8" } @@ -6435,6 +6448,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } @@ -7966,6 +7980,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" }, @@ -8950,6 +8965,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -10017,7 +10033,8 @@ "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" - ] + ], + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -10361,9 +10378,9 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -10427,9 +10444,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -10729,6 +10746,7 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" } @@ -11175,6 +11193,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", @@ -13853,7 +13872,8 @@ "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" }, "node_modules/jsdom": { "version": "20.0.3", @@ -13927,7 +13947,8 @@ "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -13994,6 +14015,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -14239,7 +14261,8 @@ "node_modules/lodash.isdate": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isdate/-/lodash.isdate-4.0.1.tgz", - "integrity": "sha512-hg5B1GD+R9egsBgMwmAhk+V53Us03TVvXT4dnyKugEfsD4QKuG9Wlyvxq8OGy2nu7qVGsh4DRSnMk33hoWBq/Q==" + "integrity": "sha512-hg5B1GD+R9egsBgMwmAhk+V53Us03TVvXT4dnyKugEfsD4QKuG9Wlyvxq8OGy2nu7qVGsh4DRSnMk33hoWBq/Q==", + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", @@ -14250,7 +14273,8 @@ "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" }, "node_modules/lodash.kebabcase": { "version": "4.1.1", @@ -16177,9 +16201,9 @@ ] }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -17307,6 +17331,7 @@ "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -18427,7 +18452,8 @@ "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", @@ -18898,6 +18924,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", diff --git a/package.json b/package.json index edd35349..329e02b1 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "smartthings": "dist/src/run.js" }, "exports": { - ".": "./dist/src/index.js" + ".": "./dist/src/index.js", + "./*": "./dist/src/lib/*.js", + "./commands/list": "./dist/src/commands/index.js" }, "engines": { "node": ">=24.8.0", @@ -44,7 +46,7 @@ "dependencies": { "@aws-sdk/client-lambda": "^3.899.0", "@inquirer/prompts": "^7.8.6", - "@smartthings/core-sdk": "^8.4.1", + "@smartthings/core-sdk": "^8.5.0", "axios": "1.12.2", "chalk": "^5.6.2", "env-paths": "^3.0.0", diff --git a/src/__tests__/lib/command/api-command.test.ts b/src/__tests__/lib/command/api-command.test.ts index 3fe140c0..e14ab47b 100644 --- a/src/__tests__/lib/command/api-command.test.ts +++ b/src/__tests__/lib/command/api-command.test.ts @@ -2,12 +2,12 @@ import { jest } from '@jest/globals' import type { osLocale } from 'os-locale' -import type { - Authenticator, - Logger, - RESTClientConfig, - SmartThingsClient, - WarningFromHeader, +import { + type Authenticator, + type Logger, + type RESTClientConfig, + type SmartThingsClient, + type WarningFromHeader, } from '@smartthings/core-sdk' import type { @@ -19,8 +19,9 @@ import type { import type { newBearerTokenAuthenticator, newSmartThingsClient } from '../../../lib/command/util/st-client-wrapper.js' import type { CLIConfig } from '../../../lib/cli-config.js' import type { coreSDKLoggerFromLog4JSLogger } from '../../../lib/log-utils.js' -import { defaultClientIdProvider, type loginAuthenticator } from '../../../lib/login-authenticator.js' +import { globalClientIdProvider, type loginAuthenticator } from '../../../lib/login-authenticator.js' import type { TableGenerator } from '../../../lib/table-generator.js' +import type { fatalError } from '../../../lib/util.js' import { buildArgvMock } from '../../test-lib/builder-mock.js' @@ -74,7 +75,7 @@ const loginAuthenticatorMock = jest.fn() const mockAuthenticator = { mock: 'authenticator' } as unknown as Authenticator loginAuthenticatorMock.mockReturnValue(mockAuthenticator) jest.unstable_mockModule('../../../lib/login-authenticator.js', () => ({ - defaultClientIdProvider, + globalClientIdProvider, loginAuthenticator: loginAuthenticatorMock, })) @@ -87,6 +88,11 @@ jest.unstable_mockModule('../../../lib/command/util/st-client-wrapper.js', () => newSmartThingsClient: newSmartThingsClientMock, })) +const fatalErrorMock = jest.fn().mockImplementation(() => { throw Error('fatal error')}) +jest.unstable_mockModule('../../../lib/util.js', () => ({ + fatalError: fatalErrorMock, +})) + const { apiCommand, @@ -103,7 +109,7 @@ test('apiCommandBuilder', () => { expect(smartThingsCommandBuilderMock).toHaveBeenCalledTimes(1) expect(smartThingsCommandBuilderMock).toHaveBeenCalledWith(argvMock) - expect(optionMock).toHaveBeenCalledTimes(2) + expect(optionMock).toHaveBeenCalledTimes(3) }) describe('apiCommand', () => { @@ -120,41 +126,89 @@ describe('apiCommand', () => { expect(smartThingsCommandMock).toHaveBeenCalledWith(flags) }) - describe('token handling', () => { - it('leaves token undefined when not specified anywhere', async () => { + describe('environment and token handling', () => { + it('environment is "global" and leaves token undefined when not specified anywhere', async () => { stringConfigValueMock.mockReturnValueOnce(undefined) const result = await apiCommand(flags) + expect(result.environment).toBe('global') expect(result.token).toBeUndefined() - expect(stringConfigValueMock).toHaveBeenCalledTimes(1) + expect(stringConfigValueMock).toHaveBeenCalledTimes(2) expect(stringConfigValueMock).toHaveBeenCalledWith('token') + expect(stringConfigValueMock).toHaveBeenCalledWith('environment') expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledTimes(0) expect(loginAuthenticatorMock).toHaveBeenCalledTimes(1) expect(loginAuthenticatorMock).toHaveBeenCalledWith( 'test-data-dir/credentials.json', 'profile-from-parent', - defaultClientIdProvider, userAgent) + globalClientIdProvider, + userAgent, + ) + }) + + it('uses environment from command line', async () => { + const result = await apiCommand({ profile: 'default', environment: 'china', token: 'token-from-cmd-line' }) + + expect(result.environment).toBe('china') + expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledExactlyOnceWith('token-from-cmd-line') + expect(loginAuthenticatorMock).toHaveBeenCalledTimes(0) + }) + + it('errors out when token required and not provided', async () => { + await expect(apiCommand({ profile: 'default', environment: 'china' })).rejects.toThrow('fatal error') + + expect(fatalErrorMock).toHaveBeenCalledExactlyOnceWith('a token is required for the china environment') + }) + + it('errors out when token required and not provided', async () => { + const clientIdProvider = { + baseURL: 'https://api.example.com', + } + smartThingsCommandMock.mockResolvedValueOnce({ + ...stCommandMock, + 'profile': { + clientIdProvider, + }, + }) + + await expect(apiCommand(flags)).rejects.toThrow('fatal error') + + expect(fatalErrorMock).toHaveBeenCalledExactlyOnceWith('no authentication method available') }) it('uses token from command line', async () => { const result = await apiCommand({ profile: 'default', token: 'token-from-cmd-line' }) expect(result.token).toBe('token-from-cmd-line') - expect(stringConfigValueMock).toHaveBeenCalledTimes(0) + expect(stringConfigValueMock).toHaveBeenCalledTimes(1) + expect(stringConfigValueMock).toHaveBeenCalledWith('environment') expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledTimes(1) expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledWith('token-from-cmd-line') expect(loginAuthenticatorMock).toHaveBeenCalledTimes(0) }) + it('uses environment from config file', async () => { + stringConfigValueMock.mockReturnValueOnce('token-from-config-file') + stringConfigValueMock.mockReturnValueOnce('china') + + const result = await apiCommand(flags) + + expect(result.environment).toBe('china') + expect(stringConfigValueMock).toHaveBeenCalledTimes(2) + expect(stringConfigValueMock).toHaveBeenCalledWith('token') + expect(stringConfigValueMock).toHaveBeenCalledWith('environment') + }) + it('uses token from config file', async () => { stringConfigValueMock.mockReturnValueOnce('token-from-config-file') const result = await apiCommand(flags) expect(result.token).toBe('token-from-config-file') - expect(stringConfigValueMock).toHaveBeenCalledTimes(1) + expect(stringConfigValueMock).toHaveBeenCalledTimes(2) expect(stringConfigValueMock).toHaveBeenCalledWith('token') + expect(stringConfigValueMock).toHaveBeenCalledWith('environment') expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledTimes(1) expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledWith('token-from-config-file') expect(loginAuthenticatorMock).toHaveBeenCalledTimes(0) @@ -164,7 +218,8 @@ describe('apiCommand', () => { const result = await apiCommand({ profile: 'default', token: 'token-from-cmd-line' }) expect(result.token).toBe('token-from-cmd-line') - expect(stringConfigValueMock).toHaveBeenCalledTimes(0) + expect(stringConfigValueMock).toHaveBeenCalledTimes(1) + expect(stringConfigValueMock).toHaveBeenCalledWith('environment') expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledTimes(1) expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledWith('token-from-cmd-line') expect(loginAuthenticatorMock).toHaveBeenCalledTimes(0) @@ -175,39 +230,92 @@ describe('apiCommand', () => { const result = await apiCommand(flags) expect(result.token).toBe(undefined) - expect(stringConfigValueMock).toHaveBeenCalledTimes(1) + expect(stringConfigValueMock).toHaveBeenCalledTimes(2) expect(stringConfigValueMock).toHaveBeenCalledWith('token') + expect(stringConfigValueMock).toHaveBeenCalledWith('environment') expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledTimes(0) expect(loginAuthenticatorMock).toHaveBeenCalledTimes(1) expect(loginAuthenticatorMock).toHaveBeenCalledWith( 'test-data-dir/credentials.json', 'profile-from-parent', - defaultClientIdProvider, userAgent) + globalClientIdProvider, userAgent) + }) + }) + + describe('environment handling', () => { + it('uses environment from command line over environment from config file', async () => { + smartThingsCommandMock.mockResolvedValueOnce({ + ...stCommandMock, + profile: { + environment: 'global', + }, + }) + + const result = await apiCommand({ profile: 'default', environment: 'china', token: 'token-from-cmd-line' }) + + expect(result.environment).toBe('china') + expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledExactlyOnceWith('token-from-cmd-line') + expect(stringConfigValueMock).toHaveBeenCalledTimes(0) + expect(loginAuthenticatorMock).toHaveBeenCalledTimes(0) + }) + + it('normalizes empty environment to "global"', async () => { + stringConfigValueMock.mockReturnValueOnce('') + const result = await apiCommand(flags) + + expect(result.environment).toBe('global') + expect(stringConfigValueMock).toHaveBeenCalledTimes(2) + expect(stringConfigValueMock).toHaveBeenCalledWith('token') + expect(stringConfigValueMock).toHaveBeenCalledWith('environment') + expect(loginAuthenticatorMock).toHaveBeenCalledExactlyOnceWith( + 'test-data-dir/credentials.json', + 'profile-from-parent', + globalClientIdProvider, userAgent) + expect(newBearerTokenAuthenticatorMock).toHaveBeenCalledTimes(0) }) }) describe('clientIdProvider handling', () => { - it('uses defaultClientIdProvider when none provided in configuration', async () => { + it('uses globalClientIdProvider when none provided in configuration', async () => { const result = await apiCommand(flags) - expect(result.clientIdProvider).toBe(defaultClientIdProvider) + expect(result.urlProvider).toBe(globalClientIdProvider) }) it('uses value from config', async () => { const clientIdProvider = { - notReally: 'valid', - needToFix: 'this test when we do more input checking', + baseURL: 'https://api.smartthings.com', } smartThingsCommandMock.mockResolvedValueOnce({ ...stCommandMock, - profile: { + 'profile': { + clientIdProvider, + }, + }) + stringConfigValueMock.mockReturnValueOnce('token-from-config-file') + + const result = await apiCommand(flags) + + expect(result.urlProvider).toStrictEqual(clientIdProvider) + expect(result.environment).toBe('global') + }) + + it('calculates environment based on clientIdProvider.baseURL', async () => { + const clientIdProvider = { + baseURL: 'https://api.samsungiotcloud.cn', + } + smartThingsCommandMock.mockResolvedValueOnce({ + ...stCommandMock, + 'profile': { clientIdProvider, }, }) + stringConfigValueMock.mockReturnValueOnce('token-from-config-file') const result = await apiCommand(flags) - expect(result.clientIdProvider).toStrictEqual(clientIdProvider) + expect(result.urlProvider).toStrictEqual(clientIdProvider) + expect(result.environment).toBe('china') }) it('logs error and uses default when config is not an object', async () => { @@ -221,7 +329,8 @@ describe('apiCommand', () => { const result = await apiCommand(flags) expect(errorMock).toHaveBeenCalledWith('ignoring invalid configClientIdProvider') - expect(result.clientIdProvider).toBe(defaultClientIdProvider) + expect(result.urlProvider).toBe(globalClientIdProvider) + expect(result.environment).toBe('global') }) }) diff --git a/src/__tests__/lib/login-authenticator.test.ts b/src/__tests__/lib/login-authenticator.test.ts index 2d9c005c..efc56d53 100644 --- a/src/__tests__/lib/login-authenticator.test.ts +++ b/src/__tests__/lib/login-authenticator.test.ts @@ -60,8 +60,9 @@ const { loginAuthenticator } = await import('../../lib/login-authenticator.js') const credentialsFilename = '/full/path/to/file/credentials.json' const profileName = 'myProfile' +const profileEnvKey = `${profileName}:example.com/base-url` const clientIdProvider = { - baseURL: 'https://example.com/unused-here', + baseURL: 'https://example.com/base-url', authURL: 'https://example.com/unused-here', keyApiURL: 'https://example.com/unused-here', baseOAuthInURL: 'https://example.com/oauth-in-url', @@ -72,7 +73,7 @@ const userAgent = 'userAgent' const accessToken = 'db3d92f1-0000-0000-0000-000000000000' const refreshToken = '3f3fb859-0000-0000-0000-000000000000' const credentialsFileData = { - [profileName]: { + [profileEnvKey]: { accessToken: accessToken, refreshToken: refreshToken, expires: '2020-10-15T13:26:39.966Z', @@ -92,8 +93,8 @@ const otherCredentialsFileData = { }, } const refreshableCredentialsFileData = { - [profileName]: { - ...credentialsFileData[profileName], + [profileEnvKey]: { + ...credentialsFileData[profileEnvKey], expires: new Date().toISOString(), }, } @@ -190,11 +191,13 @@ const mockBrowser = async (finishHandlerCaller = finishHappy, closeError?: Error describe('loginAuthenticator', () => { it('creates Authenticator without errors', () => { - expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)).toBeDefined() + expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)) + .toBeDefined() }) it('makes sure directories exist', () => { - expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)).toBeDefined() + expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)) + .toBeDefined() expect(mkdirSyncMock).toHaveBeenCalledTimes(1) expect(mkdirSyncMock).toHaveBeenCalledWith('/full/path/to/file', { recursive: true }) @@ -203,7 +206,8 @@ describe('loginAuthenticator', () => { it('reads auth from credentials file', () => { readFileSyncMock.mockReturnValueOnce(Buffer.from(JSON.stringify(credentialsFileData))) - expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)).toBeDefined() + expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)) + .toBeDefined() expect(traceMock).toHaveBeenCalledWith('constructing a LoginAuthenticator') expect(traceMock).toHaveBeenCalledWith(expect.stringContaining('authentication info from file')) @@ -213,7 +217,8 @@ describe('loginAuthenticator', () => { it('partially redacts token values in logs', async () => { readFileSyncMock.mockReturnValueOnce(Buffer.from(JSON.stringify(credentialsFileData))) - expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)).toBeDefined() + expect(loginAuthenticator(credentialsFilename, profileName, clientIdProvider, userAgent)) + .toBeDefined() expect(traceMock).not.toHaveBeenCalledWith(expect.stringContaining(accessToken)) expect(traceMock).not.toHaveBeenCalledWith(expect.stringContaining(refreshToken)) @@ -282,7 +287,8 @@ describe('login', () => { expect(readFileSyncMock).toHaveBeenCalledTimes(2) expect(readFileSyncMock).toHaveBeenCalledWith(credentialsFilename) expect(writeFileSyncMock).toHaveBeenCalledTimes(1) - expect(writeFileSyncMock).toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile":/)) + expect(writeFileSyncMock) + .toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile:example.com\/base-url":/)) expect(chmodMock).toHaveBeenCalledTimes(1) expect(chmodMock).toHaveBeenCalledWith(credentialsFilename, 0o600, expect.any(Function)) @@ -481,7 +487,8 @@ describe('authenticate', () => { expect(readFileSyncMock).toHaveBeenCalledTimes(2) expect(readFileSyncMock).toHaveBeenCalledWith(credentialsFilename) expect(writeFileSyncMock).toHaveBeenCalledTimes(1) - expect(writeFileSyncMock).toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile":/)) + expect(writeFileSyncMock) + .toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile:example.com\/base-url":/)) }) it('includes User-Agent on refresh', async () => { @@ -536,7 +543,8 @@ describe('authenticate', () => { expect(readFileSyncMock).toHaveBeenCalledTimes(2) expect(readFileSyncMock).toHaveBeenCalledWith(credentialsFilename) expect(writeFileSyncMock).toHaveBeenCalledTimes(1) - expect(writeFileSyncMock).toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile":/)) + expect(writeFileSyncMock) + .toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile:example.com\/base-url":/)) }) it('logs in not logged in', async () => { @@ -580,7 +588,8 @@ describe('authenticate', () => { expect(readFileSyncMock).toHaveBeenCalledTimes(2) expect(readFileSyncMock).toHaveBeenCalledWith(credentialsFilename) expect(writeFileSyncMock).toHaveBeenCalledTimes(1) - expect(writeFileSyncMock).toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile":/)) + expect(writeFileSyncMock) + .toHaveBeenCalledWith(credentialsFilename, expect.stringMatching(/"myProfile:example.com\/base-url":/)) }) it('logs errors with trying to refresh token', async () => { diff --git a/src/commands/apps/create.ts b/src/commands/apps/create.ts index 7005a3b4..a8e04de0 100644 --- a/src/commands/apps/create.ts +++ b/src/commands/apps/create.ts @@ -22,9 +22,13 @@ import { import { getAppCreateRequestFromUser } from '../../lib/command/util/apps-user-input-create.js' -export type CommandArgs = APICommandFlags & InputAndOutputItemFlags & LambdaAuthFlags & { - authorize: boolean -} +export type CommandArgs = + & APICommandFlags + & InputAndOutputItemFlags + & LambdaAuthFlags + & { + authorize: boolean + } const command = 'apps:create' diff --git a/src/commands/edge/drivers.ts b/src/commands/edge/drivers.ts index 2f57fc42..94f3294b 100644 --- a/src/commands/edge/drivers.ts +++ b/src/commands/edge/drivers.ts @@ -3,6 +3,7 @@ import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' import { type EdgeDriver } from '@smartthings/core-sdk' import { type WithOrganization } from '../../lib/api-helpers.js' +import { buildEpilog } from '../../lib/help.js' import { apiOrganizationCommand, apiOrganizationCommandBuilder, @@ -23,13 +24,16 @@ import { listDrivers, listTableFieldDefinitions, } from '../../lib/command/util/edge-drivers.js' -import { buildEpilog } from '../../lib/help.js' -export type CommandArgs = APIOrganizationCommandFlags & AllOrganizationFlags & OutputItemOrListFlags & { - driverVersion?: string - idOrIndex?: string -} +export type CommandArgs = + & APIOrganizationCommandFlags + & AllOrganizationFlags + & OutputItemOrListFlags + & { + driverVersion?: string + idOrIndex?: string + } const command = 'edge:drivers [id-or-index]' diff --git a/src/commands/locations/modes/getcurrent.ts b/src/commands/locations/modes/getcurrent.ts index 8f38639c..a3e74edb 100644 --- a/src/commands/locations/modes/getcurrent.ts +++ b/src/commands/locations/modes/getcurrent.ts @@ -62,7 +62,7 @@ const handler = async (argv: ArgumentsCamelCase): Promise => const locationId = await chooseLocation(command, argv.location, { autoChoose: true }) const currentMode = await command.client.modes.getCurrent(locationId) const mode = argv.verbose ? await withLocation(command.client, { ...currentMode, locationId }) : currentMode - await formatAndWriteItem(command, config, mode) + await formatAndWriteItem(command, config, mode) } const cmd: CommandModule = { command, describe, builder, handler } diff --git a/src/index.ts b/src/index.ts index df5f5955..dffdf6ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,14 @@ import fs from 'node:fs' import path from 'node:path' -import yargs, { Argv } from 'yargs' +import yargs, { type Argv, type CommandModule } from 'yargs' import { hideBin } from 'yargs/helpers' -import { commands } from './commands/index.js' - const pkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '..', 'package.json'), 'utf8')) -export const buildInstance = (): Argv => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const buildInstance = (commands: CommandModule[]): Argv => { const instance: Argv = yargs(hideBin(process.argv)) instance .scriptName('smartthings') diff --git a/src/lib/command/api-command.ts b/src/lib/command/api-command.ts index 3efa8181..9018ff21 100644 --- a/src/lib/command/api-command.ts +++ b/src/lib/command/api-command.ts @@ -1,24 +1,51 @@ import log4js from 'log4js' import { osLocale } from 'os-locale' -import { Argv } from 'yargs' +import { type Argv } from 'yargs' -import { Authenticator, HttpClientHeaders, SmartThingsClient, WarningFromHeader } from '@smartthings/core-sdk' +import { + type Authenticator, + chinaSmartThingsURLProvider, + type HttpClientHeaders, + type SmartThingsClient, + type SmartThingsURLProvider, + type WarningFromHeader, +} from '@smartthings/core-sdk' import { coreSDKLoggerFromLog4JSLogger } from '../log-utils.js' -import { ClientIdProvider, defaultClientIdProvider, loginAuthenticator } from '../login-authenticator.js' -import { SmartThingsCommand, SmartThingsCommandFlags, smartThingsCommand, smartThingsCommandBuilder } from './smartthings-command.js' +import { type ClientIdProvider, globalClientIdProvider, loginAuthenticator } from '../login-authenticator.js' +import { fatalError } from '../util.js' +import { + type SmartThingsCommand, + type SmartThingsCommandFlags, + smartThingsCommand, + smartThingsCommandBuilder, +} from './smartthings-command.js' import { newBearerTokenAuthenticator, newSmartThingsClient } from './util/st-client-wrapper.js' export const userAgent = '@smartthings/cli' -export type APICommandFlags = SmartThingsCommandFlags & { - token?: string - language?: string +export type URLProvider = SmartThingsURLProvider & Partial +export const urlProvidersByEnvironment: { [key: string]: URLProvider } = { + global: globalClientIdProvider, + china: chinaSmartThingsURLProvider, } +export type APICommandFlags = + & SmartThingsCommandFlags + & { + environment?: string + token?: string + language?: string + } + export const apiCommandBuilder = (yargs: Argv): Argv => smartThingsCommandBuilder(yargs) + .option('environment', { + desc: 'the environment to use', + type: 'string', + choices: Object.keys(urlProvidersByEnvironment), + }) .option('token', { alias: 't', desc: 'the auth token to use', @@ -32,8 +59,9 @@ export const apiCommandBuilder = (yargs: Argv): Argv = SmartThingsCommand & { + environment: string token?: string - clientIdProvider: ClientIdProvider + urlProvider: SmartThingsURLProvider authenticator: Authenticator client: SmartThingsClient } @@ -50,20 +78,37 @@ export const apiCommand = async ( // The `|| undefined` at then end of this line is to normalize falsy values to `undefined`. const token = (flags.token ?? stCommand.cliConfig.stringConfigValue('token')) || undefined - const calculateClientIdProvider = (): ClientIdProvider => { + // Calculate the environment and clientIdProvider. In old configs, the clientIdProvider had to + // be specified manually. Now we support specifying the environment, which determines the + // clientIdProvider. If both are specified, environment wins. If only one is specified, calculate + // the other based on it. If neither is specified, use the globalClientIdProvider. + const calculateEnvironment = (): [string, SmartThingsURLProvider] => { + const environment = (flags.environment ?? stCommand.cliConfig.stringConfigValue('environment')) || undefined + // first look for environment; then fall back to the old config style + if (environment) { + if (environment in urlProvidersByEnvironment) { + return [environment, urlProvidersByEnvironment[environment]] + } + return fatalError(`unknown environment: ${environment}`) + } + const configClientIdProvider = stCommand.profile.clientIdProvider if (configClientIdProvider) { if (typeof configClientIdProvider !== 'object') { stCommand.logger.error('ignoring invalid configClientIdProvider') } else { - // TODO: do more type checking here eventually - return configClientIdProvider as ClientIdProvider + const clientIdProvider = configClientIdProvider as ClientIdProvider + // Look up the environment based on the clientIdProvider.baseURL. + const environment = Object.entries(urlProvidersByEnvironment) + .find(([, provider]) => provider.baseURL === clientIdProvider.baseURL)?.[0] + ?? 'unknown' + return [environment, clientIdProvider] } } - return defaultClientIdProvider + return ['global', globalClientIdProvider] } - const clientIdProvider = calculateClientIdProvider() + const [environment, urlProvider] = calculateEnvironment() const logger = coreSDKLoggerFromLog4JSLogger(log4js.getLogger('rest-client')) const buildHeaders = async (): Promise => { @@ -85,9 +130,25 @@ export const apiCommand = async ( addAdditionalHeaders(stCommand, headers) } - const authenticator = token - ? newBearerTokenAuthenticator(token) - : loginAuthenticator(`${stCommand.dataDir}/credentials.json`, stCommand.profileName, clientIdProvider, userAgent) + const buildAuthenticator = (): Authenticator => { + if (token) { + return newBearerTokenAuthenticator(token) + } + if ('clientId' in urlProvider) { + return loginAuthenticator( + `${stCommand.dataDir}/credentials.json`, + stCommand.profileName, + urlProvider as ClientIdProvider, + userAgent, + ) + } + return fatalError( + environment === 'china' + ? 'a token is required for the china environment' + : 'no authentication method available', + ) + } + const authenticator = buildAuthenticator() const warningLogger = (warnings: WarningFromHeader[] | string): void => { const message = 'Warnings from API:\n' + (typeof(warnings) === 'string' @@ -97,12 +158,13 @@ export const apiCommand = async ( console.warn(message) } const client = newSmartThingsClient(authenticator, - { urlProvider: clientIdProvider, logger, headers, warningLogger }) + { urlProvider: urlProvider, logger, headers, warningLogger }) return { ...stCommand, + environment, token, - clientIdProvider, + urlProvider, authenticator, client, } diff --git a/src/lib/command/api-organization-command.ts b/src/lib/command/api-organization-command.ts index 8913f65f..af024e78 100644 --- a/src/lib/command/api-organization-command.ts +++ b/src/lib/command/api-organization-command.ts @@ -6,9 +6,11 @@ import { type APICommand, type APICommandFlags, apiCommand, apiCommandBuilder } import { type SmartThingsCommand } from './smartthings-command.js' -export type APIOrganizationCommandFlags = APICommandFlags & { - organization?: string -} +export type APIOrganizationCommandFlags = + & APICommandFlags + & { + organization?: string + } export const apiOrganizationCommandBuilder = (yargs: Argv): Argv => apiCommandBuilder(yargs) diff --git a/src/lib/command/format.ts b/src/lib/command/format.ts index 11bfe224..3f08e9ee 100644 --- a/src/lib/command/format.ts +++ b/src/lib/command/format.ts @@ -1,9 +1,9 @@ -import { Naming, Sorting } from './io-defs.js' -import { IOFormat } from '../io-util.js' -import { itemTableFormatter, listTableFormatter, writeOutput, OutputFormatter } from './output.js' -import { buildOutputFormatter, buildOutputFormatterBuilder, BuildOutputFormatterFlags } from './output-builder.js' -import { SmartThingsCommand } from './smartthings-command.js' -import { TableFieldDefinition } from '../table-generator.js' +import { type IOFormat } from '../io-util.js' +import { type TableFieldDefinition } from '../table-generator.js' +import { type Naming, type Sorting } from './io-defs.js' +import { itemTableFormatter, listTableFormatter, writeOutput, type OutputFormatter } from './output.js' +import { buildOutputFormatter, buildOutputFormatterBuilder, type BuildOutputFormatterFlags } from './output-builder.js' +import { type SmartThingsCommand } from './smartthings-command.js' export type TableCommonOutputProducer = { @@ -12,19 +12,28 @@ export type TableCommonOutputProducer = { export type CustomCommonOutputProducer = { buildTableOutput(data: O): string } -export type CommonOutputProducer = TableCommonOutputProducer | CustomCommonOutputProducer +export type CommonOutputProducer = + TableCommonOutputProducer | CustomCommonOutputProducer + +export type TableCommonListOutputProducer = + & Sorting + & { + listTableFieldDefinitions: TableFieldDefinition[] + } + + +export type CustomCommonListOutputProducer = + & Sorting + & { + buildListTableOutput(data: L[]): string + } -export type TableCommonListOutputProducer = Sorting & { - listTableFieldDefinitions: TableFieldDefinition[] -} -export type CustomCommonListOutputProducer = Sorting & { - buildListTableOutput(data: L[]): string -} export type CommonListOutputProducer = | Sorting | TableCommonListOutputProducer | CustomCommonListOutputProducer + export type FormatAndWriteItemFlags = BuildOutputFormatterFlags export const formatAndWriteItemBuilder = buildOutputFormatterBuilder export type FormatAndWriteItemConfig = CommonOutputProducer diff --git a/src/lib/command/input-and-output-item.ts b/src/lib/command/input-and-output-item.ts index deeed19a..78245cfd 100644 --- a/src/lib/command/input-and-output-item.ts +++ b/src/lib/command/input-and-output-item.ts @@ -14,15 +14,20 @@ import { import { type SmartThingsCommand, type SmartThingsCommandFlags } from './smartthings-command.js' -export type InputAndOutputItemFlags = InputProcessorFlags & BuildOutputFormatterFlags & { - dryRun?: boolean -} +export type InputAndOutputItemFlags = + & InputProcessorFlags + & BuildOutputFormatterFlags + & { + dryRun?: boolean + } + export type InputAndOutputItemConfig = FormatAndWriteItemConfig export const inputAndOutputItemBuilder = ( yargs: Argv, ): Argv => inputItemBuilder(buildOutputFormatterBuilder(yargs)) .option('dry-run', { alias: 'd', describe: "produce JSON but don't actually submit", type: 'boolean' }) + /** * This is the main function used in most create and update commands. It parses input and passes it * on to the executeAction function parameter. diff --git a/src/lib/command/output-builder.ts b/src/lib/command/output-builder.ts index 49ffdb54..3b57354b 100644 --- a/src/lib/command/output-builder.ts +++ b/src/lib/command/output-builder.ts @@ -1,22 +1,26 @@ -import { Argv } from 'yargs' +import { type Argv } from 'yargs' -import { IOFormat } from '../io-util.js' +import { type CLIConfig } from '../cli-config.js' +import { type IOFormat } from '../io-util.js' import { calculateOutputFormat, calculateOutputFormatBuilder, - CalculateOutputFormatFlags, + type CalculateOutputFormatFlags, jsonFormatter, - OutputFormatter, + type OutputFormatter, yamlFormatter, } from './output.js' -import { SmartThingsCommandFlags } from './smartthings-command.js' -import { CLIConfig } from '../cli-config.js' +import { type SmartThingsCommandFlags } from './smartthings-command.js' -export type BuildOutputFormatterFlags = SmartThingsCommandFlags & CalculateOutputFormatFlags & { - indent?: number - groupRows?: boolean -} +export type BuildOutputFormatterFlags = + & SmartThingsCommandFlags + & CalculateOutputFormatFlags + & { + indent?: number + groupRows?: boolean + } + export const buildOutputFormatterBuilder = (yargs: Argv): Argv => calculateOutputFormatBuilder(yargs) .option('indent', { diff --git a/src/lib/login-authenticator.ts b/src/lib/login-authenticator.ts index ebd022d7..09a917ef 100644 --- a/src/lib/login-authenticator.ts +++ b/src/lib/login-authenticator.ts @@ -11,7 +11,7 @@ import open from 'open' import ora from 'ora' import qs from 'qs' -import { SmartThingsURLProvider, defaultSmartThingsURLProvider, Authenticator, HttpClientHeaders } from '@smartthings/core-sdk' +import { SmartThingsURLProvider, globalSmartThingsURLProvider, Authenticator, HttpClientHeaders } from '@smartthings/core-sdk' import { delay } from './util.js' @@ -22,8 +22,8 @@ export type ClientIdProvider = SmartThingsURLProvider & { oauthAuthTokenRefreshURL: string } -export const defaultClientIdProvider: ClientIdProvider = { - ...defaultSmartThingsURLProvider, +export const globalClientIdProvider: ClientIdProvider = { + ...globalSmartThingsURLProvider, baseOAuthInURL: 'https://oauthin-regional.api.smartthings.com/oauth', oauthAuthTokenRefreshURL: 'https://auth-global.api.smartthings.com/oauth/token', clientId: 'd18cf96e-c626-4433-bf51-ddbb10c5d1ed', @@ -43,9 +43,9 @@ type AuthenticationInfo = { deviceId: string } +// The key to this map is a combination of the profile base URL. type CredentialsFileData = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [profileName: string]: any + [profileEnvKey: string]: Omit & { expires: string } } /** @@ -58,7 +58,12 @@ function scrubAuthInfo(authInfo?: AuthenticationInfo): string { return message.replace(tokenRegex, '"$1-xxxx-xxxx-xxxx-xxxxxxxxxxxx"') } -export const loginAuthenticator = (credentialsFile: string, profileName: string, clientIdProvider: ClientIdProvider, userAgent: string): Authenticator => { +export const loginAuthenticator = ( + credentialsFile: string, + profileName: string, + clientIdProvider: ClientIdProvider, + userAgent: string, +): Authenticator => { let authenticationInfo: AuthenticationInfo | undefined const logger = log4js.getLogger('login-authenticator') @@ -79,8 +84,9 @@ export const loginAuthenticator = (credentialsFile: string, profileName: string, const clientId = clientIdProvider.clientId const credentialsFileData = readCredentialsFile() - if (profileName in credentialsFileData) { - const authInfo = credentialsFileData[profileName] + const profileEnvKey = `${profileName}:${clientIdProvider.baseURL.replace(/https:\/\//, '')}` + if (profileEnvKey in credentialsFileData) { + const authInfo = credentialsFileData[profileEnvKey] authenticationInfo = { ...authInfo, expires: new Date(authInfo.expires), @@ -110,16 +116,20 @@ export const loginAuthenticator = (credentialsFile: string, profileName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any const updateTokenFromResponse = (response: AxiosResponse): void => { + const expires = new Date(Date.now() + response.data.expires_in * 1000) const updatedAuthenticationInfo = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token, - expires: new Date(Date.now() + response.data.expires_in * 1000), + expires, scope: response.data.scope, installedAppId: response.data.installed_app_id, deviceId: response.data.device_id, } const credentialsFileData = readCredentialsFile() - credentialsFileData[profileName] = updatedAuthenticationInfo + credentialsFileData[profileEnvKey] = { + ...updatedAuthenticationInfo, + expires: updatedAuthenticationInfo.expires.toISOString(), + } writeCredentialsFile(credentialsFileData) authenticationInfo = updatedAuthenticationInfo } @@ -235,7 +245,7 @@ export const loginAuthenticator = (credentialsFile: string, profileName: string, const logout = async (): Promise => { const credentialsFileData = readCredentialsFile() - delete credentialsFileData[profileName] + delete credentialsFileData[profileEnvKey] writeCredentialsFile(credentialsFileData) } diff --git a/src/run.ts b/src/run.ts index 0254cf6a..0288de13 100644 --- a/src/run.ts +++ b/src/run.ts @@ -2,8 +2,10 @@ import { buildInstance } from './index.js' +import { commands } from './commands/index.js' + // After bundling with ncc, we get deprecation warnings from axios. Turn them off for now. (process as unknown as { noDeprecation: boolean }).noDeprecation = true -await buildInstance().parse() +await buildInstance(commands).parse()