@credibledata/cred-cli 0.1.3 → 0.1.5

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 (172) hide show
  1. package/.prettierignore +2 -1
  2. package/README.md +0 -2
  3. package/bun.lock +182 -283
  4. package/dist/client/api.js +804 -111
  5. package/dist/client/api.js.map +1 -1
  6. package/dist/clientconfiguration.js +55 -13
  7. package/dist/clientconfiguration.js.map +1 -1
  8. package/dist/clientconfiguration.spec.js +305 -16
  9. package/dist/clientconfiguration.spec.js.map +1 -1
  10. package/dist/commands/add.js +6 -3
  11. package/dist/commands/add.js.map +1 -1
  12. package/dist/commands/connection/createconnections.js +18 -3
  13. package/dist/commands/connection/createconnections.js.map +1 -1
  14. package/dist/commands/connection/createconnections.spec.js +219 -0
  15. package/dist/commands/connection/createconnections.spec.js.map +1 -0
  16. package/dist/commands/connection/deleteconnection.spec.js +130 -0
  17. package/dist/commands/connection/deleteconnection.spec.js.map +1 -0
  18. package/dist/commands/connection/listconnections.js +9 -3
  19. package/dist/commands/connection/listconnections.js.map +1 -1
  20. package/dist/commands/connection/listconnections.spec.js +117 -0
  21. package/dist/commands/connection/listconnections.spec.js.map +1 -0
  22. package/dist/commands/group/addmember.js +2 -0
  23. package/dist/commands/group/addmember.js.map +1 -1
  24. package/dist/commands/group/addmember.spec.js +148 -0
  25. package/dist/commands/group/addmember.spec.js.map +1 -0
  26. package/dist/commands/group/creategroup.spec.js +122 -0
  27. package/dist/commands/group/creategroup.spec.js.map +1 -0
  28. package/dist/commands/group/deletegroup.spec.js +104 -0
  29. package/dist/commands/group/deletegroup.spec.js.map +1 -0
  30. package/dist/commands/group/getgroup.spec.js +111 -0
  31. package/dist/commands/group/getgroup.spec.js.map +1 -0
  32. package/dist/commands/group/listgroups.spec.js +118 -0
  33. package/dist/commands/group/listgroups.spec.js.map +1 -0
  34. package/dist/commands/group/removemember.js +1 -0
  35. package/dist/commands/group/removemember.js.map +1 -1
  36. package/dist/commands/group/removemember.spec.js +138 -0
  37. package/dist/commands/group/removemember.spec.js.map +1 -0
  38. package/dist/commands/groupaccesstoken/createToken.js +6 -2
  39. package/dist/commands/groupaccesstoken/createToken.js.map +1 -1
  40. package/dist/commands/groupaccesstoken/createToken.spec.js +127 -0
  41. package/dist/commands/groupaccesstoken/createToken.spec.js.map +1 -0
  42. package/dist/commands/groupaccesstoken/deleteToken.js.map +1 -1
  43. package/dist/commands/groupaccesstoken/deleteToken.spec.js +103 -0
  44. package/dist/commands/groupaccesstoken/deleteToken.spec.js.map +1 -0
  45. package/dist/commands/groupaccesstoken/groupContainer.js.map +1 -1
  46. package/dist/commands/groupaccesstoken/listTokens.js.map +1 -1
  47. package/dist/commands/groupaccesstoken/listTokens.spec.js +115 -0
  48. package/dist/commands/groupaccesstoken/listTokens.spec.js.map +1 -0
  49. package/dist/commands/list.js +10 -3
  50. package/dist/commands/list.js.map +1 -1
  51. package/dist/commands/misc/login.js +146 -22
  52. package/dist/commands/misc/login.js.map +1 -1
  53. package/dist/commands/misc/login.spec.js +111 -6
  54. package/dist/commands/misc/login.spec.js.map +1 -1
  55. package/dist/commands/misc/logout.spec.js +30 -0
  56. package/dist/commands/misc/logout.spec.js.map +1 -0
  57. package/dist/commands/misc/loopbackOAuth.js +110 -0
  58. package/dist/commands/misc/loopbackOAuth.js.map +1 -0
  59. package/dist/commands/misc/set-access-token.js.map +1 -1
  60. package/dist/commands/misc/set-access-token.spec.js +65 -0
  61. package/dist/commands/misc/set-access-token.spec.js.map +1 -0
  62. package/dist/commands/package/deletepackage.spec.js +136 -0
  63. package/dist/commands/package/deletepackage.spec.js.map +1 -0
  64. package/dist/commands/package/listpackages.js +10 -5
  65. package/dist/commands/package/listpackages.js.map +1 -1
  66. package/dist/commands/package/listpackages.spec.js +129 -0
  67. package/dist/commands/package/listpackages.spec.js.map +1 -0
  68. package/dist/commands/package/publish.js +23 -0
  69. package/dist/commands/package/publish.js.map +1 -1
  70. package/dist/commands/package/publish.spec.js +511 -0
  71. package/dist/commands/package/publish.spec.js.map +1 -0
  72. package/dist/commands/package/updatepackage.spec.js +50 -0
  73. package/dist/commands/package/updatepackage.spec.js.map +1 -1
  74. package/dist/commands/project/createproject.js +11 -1
  75. package/dist/commands/project/createproject.js.map +1 -1
  76. package/dist/commands/project/createproject.spec.js +154 -0
  77. package/dist/commands/project/createproject.spec.js.map +1 -0
  78. package/dist/commands/project/deleteproject.spec.js +106 -0
  79. package/dist/commands/project/deleteproject.spec.js.map +1 -0
  80. package/dist/commands/project/getproject.spec.js +99 -0
  81. package/dist/commands/project/getproject.spec.js.map +1 -0
  82. package/dist/commands/project/listprojects.js +13 -7
  83. package/dist/commands/project/listprojects.js.map +1 -1
  84. package/dist/commands/project/listprojects.spec.js +111 -0
  85. package/dist/commands/project/listprojects.spec.js.map +1 -0
  86. package/dist/commands/project/setproject.spec.js +58 -0
  87. package/dist/commands/project/setproject.spec.js.map +1 -0
  88. package/dist/commands/project/updateproject.js +68 -0
  89. package/dist/commands/project/updateproject.js.map +1 -0
  90. package/dist/commands/project/updateproject.spec.js +129 -0
  91. package/dist/commands/project/updateproject.spec.js.map +1 -0
  92. package/dist/commands/rm.js.map +1 -1
  93. package/dist/commands/update.js +13 -0
  94. package/dist/commands/update.js.map +1 -1
  95. package/dist/commands/version/listversions.spec.js +121 -0
  96. package/dist/commands/version/listversions.spec.js.map +1 -0
  97. package/dist/common/UserGroupId.spec.js +92 -0
  98. package/dist/common/UserGroupId.spec.js.map +1 -0
  99. package/dist/constants.js +49 -13
  100. package/dist/constants.js.map +1 -1
  101. package/dist/index.js +40 -18
  102. package/dist/index.js.map +1 -1
  103. package/dist/logger.spec.js +125 -0
  104. package/dist/logger.spec.js.map +1 -0
  105. package/dist/polyfills/file.polyfill.js.map +1 -1
  106. package/dist/utils.js +59 -0
  107. package/dist/utils.js.map +1 -1
  108. package/dist/utils.spec.js +158 -0
  109. package/dist/utils.spec.js.map +1 -0
  110. package/docker/Dockerfile.ci +11 -3
  111. package/eslint.config.mjs +45 -0
  112. package/generate-client.sh +5 -0
  113. package/package.json +24 -16
  114. package/src/client/.openapi-generator/FILES +1 -0
  115. package/src/client/api.ts +1999 -696
  116. package/src/clientconfiguration.spec.ts +394 -20
  117. package/src/clientconfiguration.ts +74 -15
  118. package/src/commands/add.ts +15 -6
  119. package/src/commands/connection/createconnections.spec.ts +403 -0
  120. package/src/commands/connection/createconnections.ts +30 -5
  121. package/src/commands/connection/deleteconnection.spec.ts +189 -0
  122. package/src/commands/connection/listconnections.spec.ts +132 -0
  123. package/src/commands/connection/listconnections.ts +22 -3
  124. package/src/commands/group/addmember.spec.ts +257 -0
  125. package/src/commands/group/addmember.ts +2 -0
  126. package/src/commands/group/creategroup.spec.ts +146 -0
  127. package/src/commands/group/deletegroup.spec.ts +120 -0
  128. package/src/commands/group/getgroup.spec.ts +129 -0
  129. package/src/commands/group/listgroups.spec.ts +137 -0
  130. package/src/commands/group/removemember.spec.ts +224 -0
  131. package/src/commands/group/removemember.ts +1 -0
  132. package/src/commands/groupaccesstoken/createToken.spec.ts +211 -0
  133. package/src/commands/groupaccesstoken/createToken.ts +55 -51
  134. package/src/commands/groupaccesstoken/deleteToken.spec.ts +147 -0
  135. package/src/commands/groupaccesstoken/deleteToken.ts +16 -11
  136. package/src/commands/groupaccesstoken/groupContainer.ts +51 -51
  137. package/src/commands/groupaccesstoken/listTokens.spec.ts +147 -0
  138. package/src/commands/groupaccesstoken/listTokens.ts +16 -11
  139. package/src/commands/list.ts +50 -9
  140. package/src/commands/misc/login.spec.ts +91 -6
  141. package/src/commands/misc/login.ts +208 -25
  142. package/src/commands/misc/logout.spec.ts +32 -0
  143. package/src/commands/misc/loopbackOAuth.ts +127 -0
  144. package/src/commands/misc/set-access-token.spec.ts +95 -0
  145. package/src/commands/misc/set-access-token.ts +16 -16
  146. package/src/commands/package/deletepackage.spec.ts +164 -0
  147. package/src/commands/package/listpackages.spec.ts +147 -0
  148. package/src/commands/package/listpackages.ts +28 -5
  149. package/src/commands/package/publish.spec.ts +597 -0
  150. package/src/commands/package/publish.ts +32 -0
  151. package/src/commands/package/updatepackage.spec.ts +98 -0
  152. package/src/commands/project/createproject.spec.ts +247 -0
  153. package/src/commands/project/createproject.ts +12 -1
  154. package/src/commands/project/deleteproject.spec.ts +123 -0
  155. package/src/commands/project/getproject.spec.ts +109 -0
  156. package/src/commands/project/listprojects.spec.ts +121 -0
  157. package/src/commands/project/listprojects.ts +33 -7
  158. package/src/commands/project/setproject.spec.ts +63 -0
  159. package/src/commands/project/updateproject.spec.ts +217 -0
  160. package/src/commands/project/updateproject.ts +82 -0
  161. package/src/commands/rm.ts +1 -6
  162. package/src/commands/update.ts +24 -1
  163. package/src/commands/version/listversions.spec.ts +138 -0
  164. package/src/common/UserGroupId.spec.ts +117 -0
  165. package/src/constants.ts +59 -13
  166. package/src/index.ts +46 -20
  167. package/src/logger.spec.ts +158 -0
  168. package/src/polyfills/file.polyfill.ts +1 -0
  169. package/src/utils.spec.ts +135 -0
  170. package/src/utils.ts +81 -0
  171. package/tsconfig.json +15 -18
  172. package/.eslintrc +0 -33
@@ -1,28 +1,210 @@
1
1
  import axios, { isAxiosError } from "axios";
2
2
  import { jwtDecode } from "jwt-decode";
3
+ import { URLSearchParams } from "url";
4
+ import { OrganizationsApi } from "../../client/api";
5
+ import { Configuration } from "../../client/configuration";
3
6
  import { ClientConfiguration } from "../../clientconfiguration";
4
7
  import {
5
8
  auth0ConfigsByEnv,
9
+ AUTH_CLI_LOOPBACK_REDIRECT_URI,
6
10
  Environment,
11
+ getAuth0OrganizationIdForCliLogin,
7
12
  LOGIN_TIMEOUT_MS,
8
13
  } from "../../constants";
9
14
  import { logger } from "../../logger";
10
- import { prettifyJson, printOutError, sleep } from "../../utils";
15
+ import { printOutError, sleep } from "../../utils";
16
+ import * as loopbackOAuth from "./loopbackOAuth";
11
17
 
12
- export async function login(organizationName: string, configPath?: string) {
18
+ const organizationsApi = new OrganizationsApi(new Configuration());
19
+
20
+ export async function login(
21
+ organizationName: string,
22
+ controlPlaneBaseURL?: string,
23
+ configPath?: string,
24
+ ) {
13
25
  const config = new ClientConfiguration(configPath);
14
26
  const environment = config.getEnvironment();
15
27
 
16
28
  if (environment === "e2e-test") {
17
29
  return loginM2M(organizationName, config);
18
- } else {
19
- return loginWithDeviceCode(environment, organizationName, config);
30
+ }
31
+
32
+ const auth0OrganizationId =
33
+ getAuth0OrganizationIdForCliLogin(organizationName);
34
+ if (auth0OrganizationId) {
35
+ return loginWithAuthCodePkceOrganization(
36
+ environment,
37
+ organizationName,
38
+ controlPlaneBaseURL,
39
+ config,
40
+ auth0OrganizationId,
41
+ );
42
+ }
43
+
44
+ return loginWithDeviceCode(
45
+ environment,
46
+ organizationName,
47
+ controlPlaneBaseURL,
48
+ config,
49
+ );
50
+ }
51
+
52
+ async function persistTokensAndVerifyOrganization(
53
+ tokenPayload: {
54
+ id_token?: string;
55
+ access_token: string;
56
+ refresh_token?: string;
57
+ },
58
+ organizationName: string,
59
+ controlPlaneBaseURL: string,
60
+ config: ClientConfiguration,
61
+ ): Promise<void> {
62
+ if (tokenPayload.id_token) {
63
+ const decoded = jwtDecode(tokenPayload.id_token);
64
+ if (
65
+ typeof decoded === "object" &&
66
+ decoded !== null &&
67
+ "email" in decoded &&
68
+ "name" in decoded
69
+ ) {
70
+ const user = decoded as { email?: string; name?: string };
71
+ config.setVSCodeUser(user.name, user.email);
72
+ }
73
+ }
74
+
75
+ config.setAccessToken(true, tokenPayload.access_token);
76
+ config.setJwtAccessToken(tokenPayload.access_token);
77
+ if (tokenPayload.refresh_token) {
78
+ config.setRefreshToken(tokenPayload.refresh_token);
79
+ }
80
+
81
+ try {
82
+ const response = await organizationsApi.getOrganization(
83
+ organizationName,
84
+ {
85
+ baseURL: controlPlaneBaseURL,
86
+ headers: config.getHeaders(),
87
+ },
88
+ );
89
+
90
+ if (response.status === 200) {
91
+ if (organizationName !== config.getOrganization()) {
92
+ config.setOrganization(organizationName);
93
+ config.setProject(undefined);
94
+ }
95
+ logger.info("Login successful");
96
+ }
97
+ } catch (error) {
98
+ if (!controlPlaneBaseURL.startsWith("https://")) {
99
+ const errorMsg =
100
+ error instanceof Error ? error.message : String(error);
101
+ logger.warn(
102
+ "Error with organziation check- might be bootstrapping. Ignoring: " +
103
+ errorMsg,
104
+ );
105
+ config.setOrganization(organizationName);
106
+ config.setProject(undefined);
107
+ return;
108
+ }
109
+
110
+ if (isAxiosError(error)) {
111
+ if (error.response?.status === 403 || error.response?.status === 404) {
112
+ logger.error(
113
+ `The organization "${organizationName}" does not exist, or you do not have permission to access it.`,
114
+ );
115
+ process.exit(1);
116
+ }
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ async function loginWithAuthCodePkceOrganization(
123
+ environment: Environment,
124
+ organizationName: string,
125
+ controlPlaneBaseURL: string,
126
+ config: ClientConfiguration,
127
+ auth0OrganizationId: string,
128
+ ): Promise<void> {
129
+ const auth0Domain = auth0ConfigsByEnv[environment].domain;
130
+ const auth0ClientId = auth0ConfigsByEnv[environment].clientId;
131
+ const audience = auth0ConfigsByEnv[environment].audience;
132
+
133
+ const { codeVerifier, codeChallenge } = loopbackOAuth.generatePkcePair();
134
+ const state = loopbackOAuth.generateOAuthState();
135
+
136
+ const authorizeParams = new URLSearchParams({
137
+ response_type: "code",
138
+ client_id: auth0ClientId,
139
+ redirect_uri: AUTH_CLI_LOOPBACK_REDIRECT_URI,
140
+ scope: "openid profile email offline_access",
141
+ audience,
142
+ organization: auth0OrganizationId,
143
+ code_challenge: codeChallenge,
144
+ code_challenge_method: "S256",
145
+ state,
146
+ });
147
+ const authorizeUrl = `${auth0Domain}/authorize?${authorizeParams.toString()}`;
148
+
149
+ try {
150
+ const { code } = await loopbackOAuth.authorizationCodeViaLoopback({
151
+ redirectUri: AUTH_CLI_LOOPBACK_REDIRECT_URI,
152
+ expectedState: state,
153
+ authorizeUrl,
154
+ timeoutMs: LOGIN_TIMEOUT_MS,
155
+ onListening: () => {
156
+ logger.info(
157
+ `Please visit the following URL to login ${authorizeUrl}`,
158
+ );
159
+ logger.info("Waiting for login...");
160
+ },
161
+ });
162
+
163
+ const tokenResponse = await axios.request({
164
+ method: "POST",
165
+ url: `${auth0Domain}/oauth/token`,
166
+ headers: { "content-type": "application/x-www-form-urlencoded" },
167
+ data: new URLSearchParams({
168
+ grant_type: "authorization_code",
169
+ client_id: auth0ClientId,
170
+ code,
171
+ redirect_uri: AUTH_CLI_LOOPBACK_REDIRECT_URI,
172
+ code_verifier: codeVerifier,
173
+ }),
174
+ });
175
+
176
+ await persistTokensAndVerifyOrganization(
177
+ tokenResponse.data,
178
+ organizationName,
179
+ controlPlaneBaseURL,
180
+ config,
181
+ );
182
+ } catch (e) {
183
+ if (isAxiosError(e)) {
184
+ const d = e.response?.data;
185
+ if (
186
+ typeof d === "object" &&
187
+ d !== null &&
188
+ "error" in d &&
189
+ d.error === "redirect_uri_mismatch"
190
+ ) {
191
+ logger.error(
192
+ `Auth0 rejected the CLI callback URL. Add ${AUTH_CLI_LOOPBACK_REDIRECT_URI} to this application's Allowed Callback URLs in Auth0.`,
193
+ );
194
+ }
195
+ printOutError(e, false);
196
+ } else {
197
+ const msg = e instanceof Error ? e.message : String(e);
198
+ logger.error(msg);
199
+ }
200
+ process.exit(1);
20
201
  }
21
202
  }
22
203
 
23
204
  async function loginWithDeviceCode(
24
205
  environment: Environment,
25
206
  organizationName: string,
207
+ controlPlaneBaseURL: string,
26
208
  config: ClientConfiguration,
27
209
  ) {
28
210
  const auth0Domain = auth0ConfigsByEnv[environment].domain;
@@ -65,33 +247,34 @@ async function loginWithDeviceCode(
65
247
  client_id: auth0ClientId,
66
248
  }),
67
249
  });
68
- const decoded = jwtDecode(accessToken.data.id_token);
69
- if (
70
- typeof decoded === "object" &&
71
- decoded !== null &&
72
- "email" in decoded &&
73
- "name" in decoded
74
- ) {
75
- const user = decoded as { email?: string; name?: string };
76
- config.setVSCodeUser(user.name, user.email);
77
- }
78
-
79
- config.setAccessToken(true, accessToken.data.access_token);
80
- config.setJwtAccessToken(accessToken.data.access_token);
81
- config.setRefreshToken(accessToken.data.refresh_token);
82
- if (organizationName !== config.getOrganization()) {
83
- config.setOrganization(organizationName);
84
- config.setProject(undefined);
85
- }
86
-
87
- logger.info("Login successful");
250
+ await persistTokensAndVerifyOrganization(
251
+ accessToken.data,
252
+ organizationName,
253
+ controlPlaneBaseURL,
254
+ config,
255
+ );
88
256
  break;
89
257
  } catch (error) {
90
258
  if (
91
259
  isAxiosError(error) &&
92
260
  error.response?.data?.error !== "authorization_pending"
93
261
  ) {
94
- logger.error(prettifyJson(error.response?.data));
262
+ const errorData = error.response?.data;
263
+ if (errorData) {
264
+ if (typeof errorData === "string") {
265
+ logger.error(errorData);
266
+ } else if (errorData.error_description) {
267
+ logger.error(errorData.error_description);
268
+ } else if (errorData.error) {
269
+ logger.error(errorData.error);
270
+ } else {
271
+ logger.error(JSON.stringify(errorData));
272
+ }
273
+ } else {
274
+ const errorMsg =
275
+ error instanceof Error ? error.message : String(error);
276
+ logger.error(errorMsg || "Unknown error occurred");
277
+ }
95
278
  }
96
279
  if (Date.now() - startTime > LOGIN_TIMEOUT_MS) {
97
280
  logger.info("Login timed out after 5 minutes");
@@ -0,0 +1,32 @@
1
+ import { expect } from "chai";
2
+ import sinon from "sinon";
3
+ import { ClientConfiguration } from "../../clientconfiguration";
4
+ import { logger } from "../../logger";
5
+ import { logout } from "./logout";
6
+
7
+ describe("logout", () => {
8
+ let sandbox: sinon.SinonSandbox;
9
+ let destroyConfigStub: sinon.SinonStub;
10
+ let loggerStub: sinon.SinonStubbedInstance<typeof logger>;
11
+
12
+ beforeEach(() => {
13
+ sandbox = sinon.createSandbox();
14
+ loggerStub = sandbox.stub(logger);
15
+ destroyConfigStub = sandbox.stub(
16
+ ClientConfiguration.prototype,
17
+ "destroyConfig",
18
+ );
19
+ });
20
+
21
+ afterEach(() => {
22
+ sandbox.restore();
23
+ });
24
+
25
+ it("should destroy config and log success message", () => {
26
+ logout();
27
+
28
+ expect(destroyConfigStub.calledOnce).to.be.true;
29
+ expect(loggerStub.info.calledWith(sinon.match("Credentials reset"))).to.be
30
+ .true;
31
+ });
32
+ });
@@ -0,0 +1,127 @@
1
+ import { createHash, randomBytes } from "crypto";
2
+ import http from "http";
3
+ import { URL } from "url";
4
+
5
+ export function generatePkcePair(): {
6
+ codeVerifier: string;
7
+ codeChallenge: string;
8
+ } {
9
+ const codeVerifier = randomBytes(32).toString("base64url");
10
+ const codeChallenge = createHash("sha256")
11
+ .update(codeVerifier)
12
+ .digest("base64url");
13
+ return { codeVerifier, codeChallenge };
14
+ }
15
+
16
+ export function generateOAuthState(): string {
17
+ return randomBytes(24).toString("base64url");
18
+ }
19
+
20
+ /**
21
+ * Listens on loopback `redirectUri` for the OAuth redirect, then exchanges are done by the caller.
22
+ * Caller should print `authorizeUrl` as plain text (e.g. in {@link onListening}) — no browser auto-open.
23
+ * Used when the CLI must send `organization` on `/authorize` (Auth0 Organizations); the documented
24
+ * device-code endpoint does not list an `organization` parameter
25
+ * (https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow/call-your-api-using-the-device-authorization-flow#device-code-parameters).
26
+ */
27
+ export async function authorizationCodeViaLoopback(options: {
28
+ redirectUri: string;
29
+ expectedState: string;
30
+ authorizeUrl: string;
31
+ timeoutMs: number;
32
+ /** Invoked once the loopback server is ready (safe to tell the user to open the URL). */
33
+ onListening?: () => void;
34
+ }): Promise<{ code: string }> {
35
+ const {
36
+ redirectUri,
37
+ expectedState,
38
+ authorizeUrl: _authorizeUrl,
39
+ timeoutMs,
40
+ onListening,
41
+ } = options;
42
+ const parsed = new URL(redirectUri);
43
+ const port = Number(parsed.port);
44
+ const pathname = parsed.pathname;
45
+
46
+ if (!Number.isFinite(port) || port <= 0) {
47
+ throw new Error(
48
+ `Invalid redirect URI (need explicit port): ${redirectUri}`,
49
+ );
50
+ }
51
+
52
+ return new Promise((resolve, reject) => {
53
+ let settled = false;
54
+ // eslint-disable-next-line prefer-const -- assign after finish/timer (handler may run before `const` bindings exist)
55
+ let server: http.Server | undefined;
56
+
57
+ const finish = (err: Error | null, result?: { code: string }) => {
58
+ if (settled) {
59
+ return;
60
+ }
61
+ settled = true;
62
+ clearTimeout(timer);
63
+ server?.close();
64
+ if (err) {
65
+ reject(err);
66
+ } else {
67
+ resolve(result!);
68
+ }
69
+ };
70
+
71
+ const timer = setTimeout(() => {
72
+ finish(
73
+ new Error(
74
+ `Login timed out after ${Math.round(timeoutMs / 1000)} seconds`,
75
+ ),
76
+ );
77
+ }, timeoutMs);
78
+
79
+ server = http.createServer((req, res) => {
80
+ if (settled || !req.url) {
81
+ return;
82
+ }
83
+ try {
84
+ const reqUrl = new URL(req.url, `http://127.0.0.1:${port}`);
85
+ if (reqUrl.pathname !== pathname) {
86
+ res.writeHead(404);
87
+ res.end();
88
+ return;
89
+ }
90
+ const code = reqUrl.searchParams.get("code");
91
+ const state = reqUrl.searchParams.get("state");
92
+ const error = reqUrl.searchParams.get("error");
93
+
94
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
95
+ res.end(
96
+ "<!DOCTYPE html><html><body><p>You can close this tab and return to the terminal.</p></body></html>",
97
+ );
98
+
99
+ if (error) {
100
+ finish(
101
+ new Error(
102
+ reqUrl.searchParams.get("error_description") || error,
103
+ ),
104
+ );
105
+ return;
106
+ }
107
+ if (!code || state !== expectedState) {
108
+ finish(new Error("Invalid authorization response"));
109
+ return;
110
+ }
111
+ finish(null, { code });
112
+ } catch (e) {
113
+ finish(e instanceof Error ? e : new Error(String(e)));
114
+ }
115
+ });
116
+
117
+ server.on("error", (err) => finish(err));
118
+
119
+ server.listen(port, "127.0.0.1", () => {
120
+ try {
121
+ onListening?.();
122
+ } catch (e) {
123
+ finish(e instanceof Error ? e : new Error(String(e)));
124
+ }
125
+ });
126
+ });
127
+ }
@@ -0,0 +1,95 @@
1
+ import { expect } from "chai";
2
+ import sinon from "sinon";
3
+ import { ClientConfiguration } from "../../clientconfiguration";
4
+ import { logger } from "../../logger";
5
+ import { setAccessToken } from "./set-access-token";
6
+
7
+ describe("set-access-token", () => {
8
+ let sandbox: sinon.SinonSandbox;
9
+ let setAccessTokenStub: sinon.SinonStub;
10
+ let setJwtAccessTokenStub: sinon.SinonStub;
11
+ let setOrganizationStub: sinon.SinonStub;
12
+ let loggerStub: sinon.SinonStubbedInstance<typeof logger>;
13
+
14
+ beforeEach(() => {
15
+ sandbox = sinon.createSandbox();
16
+ loggerStub = sandbox.stub(logger);
17
+ setAccessTokenStub = sandbox.stub(
18
+ ClientConfiguration.prototype,
19
+ "setAccessToken",
20
+ );
21
+ setJwtAccessTokenStub = sandbox.stub(
22
+ ClientConfiguration.prototype,
23
+ "setJwtAccessToken",
24
+ );
25
+ setOrganizationStub = sandbox.stub(
26
+ ClientConfiguration.prototype,
27
+ "setOrganization",
28
+ );
29
+ });
30
+
31
+ afterEach(() => {
32
+ sandbox.restore();
33
+ });
34
+
35
+ it("should set access token successfully", () => {
36
+ const jwtToken = "test-jwt-token";
37
+
38
+ setAccessToken(jwtToken);
39
+
40
+ expect(setAccessTokenStub.calledOnce).to.be.true;
41
+ expect(setAccessTokenStub.firstCall.args).to.deep.equal([
42
+ false,
43
+ jwtToken,
44
+ ]);
45
+ expect(setJwtAccessTokenStub.calledOnce).to.be.true;
46
+ expect(setJwtAccessTokenStub.firstCall.args[0]).to.equal(jwtToken);
47
+ expect(
48
+ loggerStub.info.calledWith(
49
+ sinon.match("Access token set successfully"),
50
+ ),
51
+ ).to.be.true;
52
+ });
53
+
54
+ it("should set access token and organization when provided", () => {
55
+ const jwtToken = "test-jwt-token";
56
+ const organizationName = "test-org";
57
+
58
+ setAccessToken(jwtToken, organizationName);
59
+
60
+ expect(setAccessTokenStub.calledOnce).to.be.true;
61
+ expect(setJwtAccessTokenStub.calledOnce).to.be.true;
62
+ expect(setOrganizationStub.calledOnce).to.be.true;
63
+ expect(setOrganizationStub.firstCall.args[0]).to.equal(organizationName);
64
+ expect(
65
+ loggerStub.info.calledWith(
66
+ sinon.match("Access token set successfully"),
67
+ ),
68
+ ).to.be.true;
69
+ expect(
70
+ loggerStub.info.calledWith(
71
+ sinon.match(`Organization set to: ${organizationName}`),
72
+ ),
73
+ ).to.be.true;
74
+ });
75
+
76
+ it("should not set organization when not provided", () => {
77
+ const jwtToken = "test-jwt-token";
78
+
79
+ setAccessToken(jwtToken);
80
+
81
+ expect(setOrganizationStub.called).to.be.false;
82
+ expect(loggerStub.info.calledWith(sinon.match(/Organization set to:/))).to
83
+ .be.false;
84
+ });
85
+
86
+ it("should handle custom config path", () => {
87
+ const jwtToken = "test-jwt-token";
88
+ const configPath = "/custom/path/config.json";
89
+
90
+ setAccessToken(jwtToken, undefined, configPath);
91
+
92
+ expect(setAccessTokenStub.calledOnce).to.be.true;
93
+ expect(setJwtAccessTokenStub.calledOnce).to.be.true;
94
+ });
95
+ });
@@ -3,30 +3,30 @@ import { logger } from "../../logger";
3
3
 
4
4
  /**
5
5
  * Sets a service account JWT token for CLI authentication.
6
- *
6
+ *
7
7
  * @param jwtToken The JWT token to use for authentication
8
8
  * @param organizationName Optional organization name to set
9
9
  * @param configPath Optional custom config path
10
10
  */
11
11
  export function setAccessToken(
12
- jwtToken: string,
13
- organizationName?: string,
14
- configPath?: string
12
+ jwtToken: string,
13
+ organizationName?: string,
14
+ configPath?: string,
15
15
  ) {
16
- const config = new ClientConfiguration(configPath);
16
+ const config = new ClientConfiguration(configPath);
17
17
 
18
- // Set the JWT token as both access token and JWT access token
19
- config.setAccessToken(false, jwtToken);
20
- config.setJwtAccessToken(jwtToken);
18
+ // Set the JWT token as both access token and JWT access token
19
+ config.setAccessToken(false, jwtToken);
20
+ config.setJwtAccessToken(jwtToken);
21
21
 
22
- // Set organization if provided
23
- if (organizationName) {
24
- config.setOrganization(organizationName);
25
- }
22
+ // Set organization if provided
23
+ if (organizationName) {
24
+ config.setOrganization(organizationName);
25
+ }
26
26
 
27
- logger.info("Access token set successfully");
27
+ logger.info("Access token set successfully");
28
28
 
29
- if (organizationName) {
30
- logger.info(`Organization set to: ${organizationName}`);
31
- }
29
+ if (organizationName) {
30
+ logger.info(`Organization set to: ${organizationName}`);
31
+ }
32
32
  }