@credibledata/cred-cli 0.0.6

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 (259) hide show
  1. package/.github/workflows/build.yml +23 -0
  2. package/.mocharc.json +10 -0
  3. package/.prettierignore +2 -0
  4. package/.prettierrc +4 -0
  5. package/README-DEV.md +40 -0
  6. package/README.md +11 -0
  7. package/bun.lock +1158 -0
  8. package/dist/client/api.js +7715 -0
  9. package/dist/client/api.js.map +1 -0
  10. package/dist/client/base.js +69 -0
  11. package/dist/client/base.js.map +1 -0
  12. package/dist/client/common.js +162 -0
  13. package/dist/client/common.js.map +1 -0
  14. package/dist/client/configuration.js +44 -0
  15. package/dist/client/configuration.js.map +1 -0
  16. package/dist/client/index.js +32 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/clientconfiguration.js +205 -0
  19. package/dist/clientconfiguration.js.map +1 -0
  20. package/dist/clientconfiguration.spec.js +381 -0
  21. package/dist/clientconfiguration.spec.js.map +1 -0
  22. package/dist/commands/add.js +66 -0
  23. package/dist/commands/add.js.map +1 -0
  24. package/dist/commands/connection/createconnections.js +89 -0
  25. package/dist/commands/connection/createconnections.js.map +1 -0
  26. package/dist/commands/connection/createconnections.spec.js +219 -0
  27. package/dist/commands/connection/createconnections.spec.js.map +1 -0
  28. package/dist/commands/connection/deleteconnection.js +52 -0
  29. package/dist/commands/connection/deleteconnection.js.map +1 -0
  30. package/dist/commands/connection/deleteconnection.spec.js +130 -0
  31. package/dist/commands/connection/deleteconnection.spec.js.map +1 -0
  32. package/dist/commands/connection/listconnections.js +50 -0
  33. package/dist/commands/connection/listconnections.js.map +1 -0
  34. package/dist/commands/connection/listconnections.spec.js +117 -0
  35. package/dist/commands/connection/listconnections.spec.js.map +1 -0
  36. package/dist/commands/get.js +28 -0
  37. package/dist/commands/get.js.map +1 -0
  38. package/dist/commands/group/addmember.js +75 -0
  39. package/dist/commands/group/addmember.js.map +1 -0
  40. package/dist/commands/group/addmember.spec.js +148 -0
  41. package/dist/commands/group/addmember.spec.js.map +1 -0
  42. package/dist/commands/group/creategroup.js +55 -0
  43. package/dist/commands/group/creategroup.js.map +1 -0
  44. package/dist/commands/group/creategroup.spec.js +122 -0
  45. package/dist/commands/group/creategroup.spec.js.map +1 -0
  46. package/dist/commands/group/deletegroup.js +48 -0
  47. package/dist/commands/group/deletegroup.js.map +1 -0
  48. package/dist/commands/group/deletegroup.spec.js +104 -0
  49. package/dist/commands/group/deletegroup.spec.js.map +1 -0
  50. package/dist/commands/group/getgroup.js +50 -0
  51. package/dist/commands/group/getgroup.js.map +1 -0
  52. package/dist/commands/group/getgroup.spec.js +111 -0
  53. package/dist/commands/group/getgroup.spec.js.map +1 -0
  54. package/dist/commands/group/listgroups.js +56 -0
  55. package/dist/commands/group/listgroups.js.map +1 -0
  56. package/dist/commands/group/listgroups.spec.js +118 -0
  57. package/dist/commands/group/listgroups.spec.js.map +1 -0
  58. package/dist/commands/group/removemember.js +69 -0
  59. package/dist/commands/group/removemember.js.map +1 -0
  60. package/dist/commands/group/removemember.spec.js +138 -0
  61. package/dist/commands/group/removemember.spec.js.map +1 -0
  62. package/dist/commands/groupaccesstoken/createToken.js +53 -0
  63. package/dist/commands/groupaccesstoken/createToken.js.map +1 -0
  64. package/dist/commands/groupaccesstoken/createToken.spec.js +127 -0
  65. package/dist/commands/groupaccesstoken/createToken.spec.js.map +1 -0
  66. package/dist/commands/groupaccesstoken/deleteToken.js +24 -0
  67. package/dist/commands/groupaccesstoken/deleteToken.js.map +1 -0
  68. package/dist/commands/groupaccesstoken/deleteToken.spec.js +103 -0
  69. package/dist/commands/groupaccesstoken/deleteToken.spec.js.map +1 -0
  70. package/dist/commands/groupaccesstoken/groupContainer.js +71 -0
  71. package/dist/commands/groupaccesstoken/groupContainer.js.map +1 -0
  72. package/dist/commands/groupaccesstoken/listTokens.js +25 -0
  73. package/dist/commands/groupaccesstoken/listTokens.js.map +1 -0
  74. package/dist/commands/groupaccesstoken/listTokens.spec.js +115 -0
  75. package/dist/commands/groupaccesstoken/listTokens.spec.js.map +1 -0
  76. package/dist/commands/list.js +75 -0
  77. package/dist/commands/list.js.map +1 -0
  78. package/dist/commands/misc/login.js +290 -0
  79. package/dist/commands/misc/login.js.map +1 -0
  80. package/dist/commands/misc/login.spec.js +243 -0
  81. package/dist/commands/misc/login.spec.js.map +1 -0
  82. package/dist/commands/misc/logout.js +11 -0
  83. package/dist/commands/misc/logout.js.map +1 -0
  84. package/dist/commands/misc/logout.spec.js +30 -0
  85. package/dist/commands/misc/logout.spec.js.map +1 -0
  86. package/dist/commands/misc/loopbackOAuth.js +110 -0
  87. package/dist/commands/misc/loopbackOAuth.js.map +1 -0
  88. package/dist/commands/misc/set-access-token.js +27 -0
  89. package/dist/commands/misc/set-access-token.js.map +1 -0
  90. package/dist/commands/misc/set-access-token.spec.js +65 -0
  91. package/dist/commands/misc/set-access-token.spec.js.map +1 -0
  92. package/dist/commands/package/deletepackage.js +54 -0
  93. package/dist/commands/package/deletepackage.js.map +1 -0
  94. package/dist/commands/package/deletepackage.spec.js +136 -0
  95. package/dist/commands/package/deletepackage.spec.js.map +1 -0
  96. package/dist/commands/package/listpackages.js +57 -0
  97. package/dist/commands/package/listpackages.js.map +1 -0
  98. package/dist/commands/package/listpackages.spec.js +129 -0
  99. package/dist/commands/package/listpackages.spec.js.map +1 -0
  100. package/dist/commands/package/publish.js +157 -0
  101. package/dist/commands/package/publish.js.map +1 -0
  102. package/dist/commands/package/publish.spec.js +511 -0
  103. package/dist/commands/package/publish.spec.js.map +1 -0
  104. package/dist/commands/package/updatepackage.js +75 -0
  105. package/dist/commands/package/updatepackage.js.map +1 -0
  106. package/dist/commands/package/updatepackage.spec.js +189 -0
  107. package/dist/commands/package/updatepackage.spec.js.map +1 -0
  108. package/dist/commands/project/createproject.js +75 -0
  109. package/dist/commands/project/createproject.js.map +1 -0
  110. package/dist/commands/project/createproject.spec.js +154 -0
  111. package/dist/commands/project/createproject.spec.js.map +1 -0
  112. package/dist/commands/project/deleteproject.js +49 -0
  113. package/dist/commands/project/deleteproject.js.map +1 -0
  114. package/dist/commands/project/deleteproject.spec.js +106 -0
  115. package/dist/commands/project/deleteproject.spec.js.map +1 -0
  116. package/dist/commands/project/getproject.js +44 -0
  117. package/dist/commands/project/getproject.js.map +1 -0
  118. package/dist/commands/project/getproject.spec.js +99 -0
  119. package/dist/commands/project/getproject.spec.js.map +1 -0
  120. package/dist/commands/project/listprojects.js +52 -0
  121. package/dist/commands/project/listprojects.js.map +1 -0
  122. package/dist/commands/project/listprojects.spec.js +111 -0
  123. package/dist/commands/project/listprojects.spec.js.map +1 -0
  124. package/dist/commands/project/setproject.js +26 -0
  125. package/dist/commands/project/setproject.js.map +1 -0
  126. package/dist/commands/project/setproject.spec.js +58 -0
  127. package/dist/commands/project/setproject.spec.js.map +1 -0
  128. package/dist/commands/project/updateproject.js +68 -0
  129. package/dist/commands/project/updateproject.js.map +1 -0
  130. package/dist/commands/project/updateproject.spec.js +129 -0
  131. package/dist/commands/project/updateproject.spec.js.map +1 -0
  132. package/dist/commands/rm.js +68 -0
  133. package/dist/commands/rm.js.map +1 -0
  134. package/dist/commands/set.js +17 -0
  135. package/dist/commands/set.js.map +1 -0
  136. package/dist/commands/update.js +42 -0
  137. package/dist/commands/update.js.map +1 -0
  138. package/dist/commands/version/archive-unarchive-version.js +79 -0
  139. package/dist/commands/version/archive-unarchive-version.js.map +1 -0
  140. package/dist/commands/version/archive-unarchive-version.spec.js +244 -0
  141. package/dist/commands/version/archive-unarchive-version.spec.js.map +1 -0
  142. package/dist/commands/version/listversions.js +46 -0
  143. package/dist/commands/version/listversions.js.map +1 -0
  144. package/dist/commands/version/listversions.spec.js +121 -0
  145. package/dist/commands/version/listversions.spec.js.map +1 -0
  146. package/dist/common/UserGroupId.js +112 -0
  147. package/dist/common/UserGroupId.js.map +1 -0
  148. package/dist/common/UserGroupId.spec.js +92 -0
  149. package/dist/common/UserGroupId.spec.js.map +1 -0
  150. package/dist/constants.js +90 -0
  151. package/dist/constants.js.map +1 -0
  152. package/dist/index.js +159 -0
  153. package/dist/index.js.map +1 -0
  154. package/dist/logger.js +77 -0
  155. package/dist/logger.js.map +1 -0
  156. package/dist/logger.spec.js +125 -0
  157. package/dist/logger.spec.js.map +1 -0
  158. package/dist/polyfills/file.polyfill.js +6 -0
  159. package/dist/polyfills/file.polyfill.js.map +1 -0
  160. package/dist/utils.js +110 -0
  161. package/dist/utils.js.map +1 -0
  162. package/dist/utils.spec.js +158 -0
  163. package/dist/utils.spec.js.map +1 -0
  164. package/dist/validator.js +53 -0
  165. package/dist/validator.js.map +1 -0
  166. package/dist/validator.spec.js +41 -0
  167. package/dist/validator.spec.js.map +1 -0
  168. package/docker/Dockerfile.ci +18 -0
  169. package/docker-compose.yml +18 -0
  170. package/eslint.config.mjs +45 -0
  171. package/generate-client.sh +27 -0
  172. package/mocha.setup.ts +4 -0
  173. package/openapitools.json +7 -0
  174. package/package.json +75 -0
  175. package/src/client/.openapi-generator/FILES +9 -0
  176. package/src/client/.openapi-generator/VERSION +1 -0
  177. package/src/client/.openapi-generator-ignore +23 -0
  178. package/src/client/api.ts +9729 -0
  179. package/src/client/base.ts +86 -0
  180. package/src/client/common.ts +150 -0
  181. package/src/client/configuration.ts +110 -0
  182. package/src/client/git_push.sh +57 -0
  183. package/src/client/index.ts +18 -0
  184. package/src/clientconfiguration.spec.ts +490 -0
  185. package/src/clientconfiguration.ts +263 -0
  186. package/src/commands/add.ts +125 -0
  187. package/src/commands/connection/createconnections.spec.ts +403 -0
  188. package/src/commands/connection/createconnections.ts +109 -0
  189. package/src/commands/connection/deleteconnection.spec.ts +189 -0
  190. package/src/commands/connection/deleteconnection.ts +57 -0
  191. package/src/commands/connection/listconnections.spec.ts +132 -0
  192. package/src/commands/connection/listconnections.ts +58 -0
  193. package/src/commands/get.ts +31 -0
  194. package/src/commands/group/addmember.spec.ts +257 -0
  195. package/src/commands/group/addmember.ts +85 -0
  196. package/src/commands/group/creategroup.spec.ts +146 -0
  197. package/src/commands/group/creategroup.ts +54 -0
  198. package/src/commands/group/deletegroup.spec.ts +120 -0
  199. package/src/commands/group/deletegroup.ts +45 -0
  200. package/src/commands/group/getgroup.spec.ts +129 -0
  201. package/src/commands/group/getgroup.ts +47 -0
  202. package/src/commands/group/listgroups.spec.ts +137 -0
  203. package/src/commands/group/listgroups.ts +45 -0
  204. package/src/commands/group/removemember.spec.ts +224 -0
  205. package/src/commands/group/removemember.ts +75 -0
  206. package/src/commands/groupaccesstoken/createToken.spec.ts +211 -0
  207. package/src/commands/groupaccesstoken/createToken.ts +72 -0
  208. package/src/commands/groupaccesstoken/deleteToken.spec.ts +147 -0
  209. package/src/commands/groupaccesstoken/deleteToken.ts +29 -0
  210. package/src/commands/groupaccesstoken/groupContainer.ts +83 -0
  211. package/src/commands/groupaccesstoken/listTokens.spec.ts +147 -0
  212. package/src/commands/groupaccesstoken/listTokens.ts +29 -0
  213. package/src/commands/list.ts +131 -0
  214. package/src/commands/misc/login.spec.ts +213 -0
  215. package/src/commands/misc/login.ts +325 -0
  216. package/src/commands/misc/logout.spec.ts +32 -0
  217. package/src/commands/misc/logout.ts +9 -0
  218. package/src/commands/misc/loopbackOAuth.ts +127 -0
  219. package/src/commands/misc/set-access-token.spec.ts +95 -0
  220. package/src/commands/misc/set-access-token.ts +32 -0
  221. package/src/commands/package/deletepackage.spec.ts +164 -0
  222. package/src/commands/package/deletepackage.ts +58 -0
  223. package/src/commands/package/listpackages.spec.ts +147 -0
  224. package/src/commands/package/listpackages.ts +67 -0
  225. package/src/commands/package/publish.spec.ts +597 -0
  226. package/src/commands/package/publish.ts +211 -0
  227. package/src/commands/package/updatepackage.spec.ts +319 -0
  228. package/src/commands/package/updatepackage.ts +95 -0
  229. package/src/commands/project/createproject.spec.ts +247 -0
  230. package/src/commands/project/createproject.ts +70 -0
  231. package/src/commands/project/deleteproject.spec.ts +123 -0
  232. package/src/commands/project/deleteproject.ts +48 -0
  233. package/src/commands/project/getproject.spec.ts +109 -0
  234. package/src/commands/project/getproject.ts +43 -0
  235. package/src/commands/project/listprojects.spec.ts +121 -0
  236. package/src/commands/project/listprojects.ts +62 -0
  237. package/src/commands/project/setproject.spec.ts +63 -0
  238. package/src/commands/project/setproject.ts +14 -0
  239. package/src/commands/project/updateproject.spec.ts +217 -0
  240. package/src/commands/project/updateproject.ts +82 -0
  241. package/src/commands/rm.ts +101 -0
  242. package/src/commands/set.ts +16 -0
  243. package/src/commands/update.ts +64 -0
  244. package/src/commands/version/archive-unarchive-version.spec.ts +456 -0
  245. package/src/commands/version/archive-unarchive-version.ts +111 -0
  246. package/src/commands/version/listversions.spec.ts +138 -0
  247. package/src/commands/version/listversions.ts +45 -0
  248. package/src/common/UserGroupId.spec.ts +117 -0
  249. package/src/common/UserGroupId.ts +137 -0
  250. package/src/constants.ts +116 -0
  251. package/src/index.ts +209 -0
  252. package/src/logger.spec.ts +158 -0
  253. package/src/logger.ts +86 -0
  254. package/src/polyfills/file.polyfill.ts +6 -0
  255. package/src/utils.spec.ts +135 -0
  256. package/src/utils.ts +115 -0
  257. package/src/validator.spec.ts +51 -0
  258. package/src/validator.ts +44 -0
  259. package/tsconfig.json +17 -0
@@ -0,0 +1,213 @@
1
+ import axios from "axios";
2
+ import { expect } from "chai";
3
+ import fs from "fs";
4
+ import os from "os";
5
+ import { join } from "path";
6
+ import sinon from "sinon";
7
+ import * as credConstants from "../../constants";
8
+ import { ClientConfiguration } from "../../clientconfiguration";
9
+ import { login } from "./login";
10
+ import { OrganizationsApi } from "../../client/api";
11
+ import * as loopbackOAuth from "./loopbackOAuth";
12
+
13
+ function createFakeIdToken(payload: Record<string, unknown>): string {
14
+ const header = Buffer.from(
15
+ JSON.stringify({ alg: "none", typ: "JWT" }),
16
+ ).toString("base64url");
17
+ const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
18
+ return `${header}.${body}.`;
19
+ }
20
+
21
+ describe("login", () => {
22
+ let tmpConfigFilePath: string;
23
+ let axiosStub: sinon.SinonStub;
24
+ let orgPermissionsStub: sinon.SinonStub;
25
+
26
+ const mockDeviceCodeResponse = {
27
+ data: {
28
+ verification_uri_complete: "https://example.com",
29
+ user_code: "123456",
30
+ device_code: "device-code-123",
31
+ interval: 1,
32
+ },
33
+ };
34
+
35
+ const fakeJwtPayload = {
36
+ name: "Test User",
37
+ email: "test@example.com",
38
+ };
39
+
40
+ const mockedAccessTokenResponse = {
41
+ data: {
42
+ id_token: createFakeIdToken(fakeJwtPayload),
43
+ access_token: "mock-access-token",
44
+ refresh_token: "mock-refresh-token",
45
+ },
46
+ };
47
+
48
+ const mockOrgPermissionsResponse = {
49
+ status: 200,
50
+ data: {},
51
+ };
52
+
53
+ beforeEach(() => {
54
+ tmpConfigFilePath = join(os.tmpdir(), "cred.json");
55
+ fs.writeFileSync(tmpConfigFilePath, "{}");
56
+ axiosStub = sinon.stub(axios, "request");
57
+ orgPermissionsStub = sinon
58
+ .stub(OrganizationsApi.prototype, "getOrganization")
59
+ .resolves(mockOrgPermissionsResponse as any);
60
+ });
61
+
62
+ afterEach(() => {
63
+ fs.rmSync(tmpConfigFilePath);
64
+ sinon.restore();
65
+ });
66
+
67
+ it("should not delete config properties on login", async function () {
68
+ this.timeout(2000);
69
+ const config = new ClientConfiguration(tmpConfigFilePath);
70
+ config.setEnvironment("localhost");
71
+ config.setOrganization("foo");
72
+ config.setProject("bar");
73
+ axiosStub
74
+ .onFirstCall()
75
+ .resolves(mockDeviceCodeResponse)
76
+ .onSecondCall()
77
+ .resolves(mockedAccessTokenResponse);
78
+ await login("foo", "http://localhost", tmpConfigFilePath);
79
+ expect(config.getOrganization()).to.equal("foo");
80
+ expect(config.getProject()).to.equal("bar");
81
+ });
82
+
83
+ it("should change the active organization when login succeeds", async function () {
84
+ this.timeout(2000);
85
+ axiosStub
86
+ .onFirstCall()
87
+ .resolves(mockDeviceCodeResponse)
88
+ .onSecondCall()
89
+ .resolves(mockedAccessTokenResponse);
90
+ const config = new ClientConfiguration(tmpConfigFilePath);
91
+ config.setEnvironment("localhost");
92
+ config.setOrganization("foo");
93
+ config.setProject("bar");
94
+ await login("test", "http://localhost", tmpConfigFilePath);
95
+ expect(config.getOrganization()).to.equal("test");
96
+ });
97
+
98
+ it("should set an access token when login succeeds", async function () {
99
+ this.timeout(2000);
100
+ axiosStub
101
+ .onFirstCall()
102
+ .resolves(mockDeviceCodeResponse)
103
+ .onSecondCall()
104
+ .resolves(mockedAccessTokenResponse);
105
+ const config = new ClientConfiguration(tmpConfigFilePath);
106
+ config.setEnvironment("localhost");
107
+ await login("test", "http://localhost", tmpConfigFilePath);
108
+ expect(config.getAccessToken()).to.equal(
109
+ mockedAccessTokenResponse.data.access_token,
110
+ );
111
+ });
112
+
113
+ it("should unset the project field when switching organizations", async function () {
114
+ this.timeout(2000);
115
+ axiosStub
116
+ .onFirstCall()
117
+ .resolves(mockDeviceCodeResponse)
118
+ .onSecondCall()
119
+ .resolves(mockedAccessTokenResponse);
120
+ const config = new ClientConfiguration(tmpConfigFilePath);
121
+ config.setEnvironment("localhost");
122
+ config.setOrganization("foo");
123
+ config.setProject("bar");
124
+ await login("not-foo", "http://localhost", tmpConfigFilePath);
125
+ expect(config.getProject()).to.equal(undefined);
126
+ });
127
+
128
+ it("should not unset the project field when logging in with the same organization", async function () {
129
+ this.timeout(2000);
130
+ axiosStub
131
+ .onFirstCall()
132
+ .resolves(mockDeviceCodeResponse)
133
+ .onSecondCall()
134
+ .resolves(mockedAccessTokenResponse);
135
+ const config = new ClientConfiguration(tmpConfigFilePath);
136
+ config.setEnvironment("localhost");
137
+ config.setOrganization("foo");
138
+ config.setProject("bar");
139
+ await login("foo", "http://localhost", tmpConfigFilePath);
140
+ expect(config.getProject()).to.equal("bar");
141
+ });
142
+
143
+ it("uses auth-code + PKCE with organization in authorize URL for mapped CLI orgs", async function () {
144
+ this.timeout(2000);
145
+ const orgIdStub = sinon
146
+ .stub(credConstants, "getAuth0OrganizationIdForCliLogin")
147
+ .returns("org_nMRKafeSuxOiuJeG");
148
+ const loopbackStub = sinon
149
+ .stub(loopbackOAuth, "authorizationCodeViaLoopback")
150
+ .callsFake(async (opts) => {
151
+ opts.onListening?.();
152
+ return { code: "test-pkce-auth-code" };
153
+ });
154
+ axiosStub.onFirstCall().resolves(mockedAccessTokenResponse);
155
+ const config = new ClientConfiguration(tmpConfigFilePath);
156
+ config.setEnvironment("videoamp");
157
+ await login(
158
+ "videoamp",
159
+ "https://admin.videoamp-aws-us-east-1.credibledata.com/api/v0",
160
+ tmpConfigFilePath,
161
+ );
162
+ expect(orgIdStub.calledOnceWithExactly("videoamp")).to.be.true;
163
+ expect(loopbackStub.calledOnce).to.be.true;
164
+ const call = loopbackStub.getCall(0);
165
+ expect(call.args[0].authorizeUrl).to.contain(
166
+ "organization=org_nMRKafeSuxOiuJeG",
167
+ );
168
+ });
169
+
170
+ it("uses mapped org id for PKCE flow regardless of org name casing", async function () {
171
+ this.timeout(2000);
172
+ const orgIdStub = sinon
173
+ .stub(credConstants, "getAuth0OrganizationIdForCliLogin")
174
+ .callsFake((name: string) =>
175
+ name.toLowerCase() === "videoamp"
176
+ ? "org_nMRKafeSuxOiuJeG"
177
+ : undefined,
178
+ );
179
+ const loopbackStub = sinon
180
+ .stub(loopbackOAuth, "authorizationCodeViaLoopback")
181
+ .callsFake(async (opts) => {
182
+ opts.onListening?.();
183
+ return { code: "test-pkce-auth-code" };
184
+ });
185
+ axiosStub.onFirstCall().resolves(mockedAccessTokenResponse);
186
+ const config = new ClientConfiguration(tmpConfigFilePath);
187
+ config.setEnvironment("videoamp");
188
+ await login(
189
+ "VideoAmp",
190
+ "https://admin.videoamp-aws-us-east-1.credibledata.com/api/v0",
191
+ tmpConfigFilePath,
192
+ );
193
+ expect(orgIdStub.calledOnceWithExactly("VideoAmp")).to.be.true;
194
+ expect(loopbackStub.getCall(0).args[0].authorizeUrl).to.contain(
195
+ "organization=org_nMRKafeSuxOiuJeG",
196
+ );
197
+ });
198
+
199
+ it("should not pass Auth0 organization for unmapped org names", async function () {
200
+ this.timeout(2000);
201
+ axiosStub
202
+ .onFirstCall()
203
+ .resolves(mockDeviceCodeResponse)
204
+ .onSecondCall()
205
+ .resolves(mockedAccessTokenResponse);
206
+ const config = new ClientConfiguration(tmpConfigFilePath);
207
+ config.setEnvironment("localhost");
208
+ await login("test", "http://localhost", tmpConfigFilePath);
209
+ expect(axiosStub.getCall(0).args[0].data).to.not.have.property(
210
+ "organization",
211
+ );
212
+ });
213
+ });
@@ -0,0 +1,325 @@
1
+ import axios, { isAxiosError } from "axios";
2
+ import { jwtDecode } from "jwt-decode";
3
+ import { URLSearchParams } from "url";
4
+ import { OrganizationsApi } from "../../client/api";
5
+ import { Configuration } from "../../client/configuration";
6
+ import { ClientConfiguration } from "../../clientconfiguration";
7
+ import {
8
+ auth0ConfigsByEnv,
9
+ AUTH_CLI_LOOPBACK_REDIRECT_URI,
10
+ Environment,
11
+ getAuth0OrganizationIdForCliLogin,
12
+ LOGIN_TIMEOUT_MS,
13
+ } from "../../constants";
14
+ import { logger } from "../../logger";
15
+ import { printOutError, sleep } from "../../utils";
16
+ import * as loopbackOAuth from "./loopbackOAuth";
17
+
18
+ const organizationsApi = new OrganizationsApi(new Configuration());
19
+
20
+ export async function login(
21
+ organizationName: string,
22
+ controlPlaneBaseURL?: string,
23
+ configPath?: string,
24
+ ) {
25
+ const config = new ClientConfiguration(configPath);
26
+ const environment = config.getEnvironment();
27
+
28
+ if (environment === "e2e-test") {
29
+ return loginM2M(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);
201
+ }
202
+ }
203
+
204
+ async function loginWithDeviceCode(
205
+ environment: Environment,
206
+ organizationName: string,
207
+ controlPlaneBaseURL: string,
208
+ config: ClientConfiguration,
209
+ ) {
210
+ const auth0Domain = auth0ConfigsByEnv[environment].domain;
211
+ const auth0ClientId = auth0ConfigsByEnv[environment].clientId;
212
+ let deviceCodeData = null;
213
+
214
+ try {
215
+ const deviceCode = await axios.request({
216
+ method: "POST",
217
+ url: `${auth0Domain}/oauth/device/code`,
218
+ headers: { "content-type": "application/x-www-form-urlencoded" },
219
+ data: {
220
+ client_id: auth0ClientId,
221
+ audience: auth0ConfigsByEnv[environment].audience,
222
+ scope: "openid profile email offline_access",
223
+ },
224
+ });
225
+ deviceCodeData = deviceCode.data;
226
+
227
+ logger.info(
228
+ `Please visit the following URL to login ${deviceCode.data.verification_uri_complete} and verify user code ${deviceCode.data.user_code}.`,
229
+ );
230
+ logger.info("Waiting for login...");
231
+ } catch (e) {
232
+ printOutError(e, false);
233
+ process.exit(1);
234
+ }
235
+
236
+ const startTime = Date.now();
237
+
238
+ while (true) {
239
+ try {
240
+ const accessToken = await axios.request({
241
+ method: "POST",
242
+ url: `${auth0Domain}/oauth/token`,
243
+ headers: { "content-type": "application/x-www-form-urlencoded" },
244
+ data: new URLSearchParams({
245
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
246
+ device_code: deviceCodeData.device_code,
247
+ client_id: auth0ClientId,
248
+ }),
249
+ });
250
+ await persistTokensAndVerifyOrganization(
251
+ accessToken.data,
252
+ organizationName,
253
+ controlPlaneBaseURL,
254
+ config,
255
+ );
256
+ break;
257
+ } catch (error) {
258
+ if (
259
+ isAxiosError(error) &&
260
+ error.response?.data?.error !== "authorization_pending"
261
+ ) {
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
+ }
278
+ }
279
+ if (Date.now() - startTime > LOGIN_TIMEOUT_MS) {
280
+ logger.info("Login timed out after 5 minutes");
281
+ process.exit(1);
282
+ }
283
+
284
+ await sleep(deviceCodeData.interval * 1000);
285
+ }
286
+ }
287
+ }
288
+
289
+ async function loginM2M(
290
+ organizationName: string,
291
+ config: ClientConfiguration,
292
+ ): Promise<void> {
293
+ const auth0Config = auth0ConfigsByEnv["e2e-test"];
294
+
295
+ if (!auth0Config) {
296
+ throw new Error("No environment variables present for e2e");
297
+ }
298
+
299
+ try {
300
+ const response = await axios.request({
301
+ method: "POST",
302
+ url: `${auth0Config.domain}/oauth/token`,
303
+ headers: { "content-type": "application/x-www-form-urlencoded" },
304
+ data: {
305
+ grant_type: "client_credentials",
306
+ client_id: auth0Config.clientId,
307
+ client_secret: auth0Config.clientSecret,
308
+ audience: auth0Config.audience,
309
+ },
310
+ });
311
+
312
+ const token = response.data.access_token;
313
+
314
+ if (!token) {
315
+ throw new Error("No access token received");
316
+ }
317
+
318
+ config.destroyConfig();
319
+ config.setAccessToken(true, token);
320
+ config.setOrganization(organizationName);
321
+ } catch (e) {
322
+ printOutError(e, false);
323
+ process.exit(1);
324
+ }
325
+ }
@@ -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,9 @@
1
+ import { ClientConfiguration } from "../../clientconfiguration";
2
+ import { logger } from "../../logger";
3
+
4
+ export function logout() {
5
+ const config = new ClientConfiguration();
6
+ config.destroyConfig();
7
+
8
+ logger.info("Credentials reset");
9
+ }
@@ -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
+ }