@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
@@ -4,6 +4,19 @@ import os from "os";
4
4
  import { join } from "path";
5
5
  import sinon from "sinon";
6
6
  import { ClientConfiguration } from "./clientconfiguration";
7
+ import { ALL_ENVIRONMENTS, type Environment } from "./constants";
8
+ import { logger } from "./logger";
9
+
10
+ /** Expected `configPerEnvironment` shape after `loadConfig()` (every env has an object). */
11
+ function fullPerEnv(
12
+ filled: Partial<Record<Environment, Record<string, unknown>>> = {},
13
+ ): Record<string, Record<string, unknown>> {
14
+ const out: Record<string, Record<string, unknown>> = {};
15
+ for (const env of ALL_ENVIRONMENTS) {
16
+ out[env] = filled[env] != null ? { ...filled[env]! } : {};
17
+ }
18
+ return out;
19
+ }
7
20
 
8
21
  describe("ClientConfiguration", () => {
9
22
  let clientConfig: ClientConfiguration;
@@ -33,11 +46,7 @@ describe("ClientConfiguration", () => {
33
46
  readFileSyncStub.throws(new Error("File not found"));
34
47
  clientConfig.loadConfig();
35
48
  expect(clientConfig["config"]).to.deep.equal({
36
- configPerEnvironment: {
37
- production: {},
38
- staging: {},
39
- localhost: {},
40
- },
49
+ configPerEnvironment: fullPerEnv(),
41
50
  activeEnvironment: "production",
42
51
  });
43
52
  });
@@ -53,7 +62,33 @@ describe("ClientConfiguration", () => {
53
62
  };
54
63
  readFileSyncStub.returns(JSON.stringify(mockConfig));
55
64
  clientConfig.loadConfig();
56
- expect(clientConfig["config"]).to.deep.equal(mockConfig);
65
+ expect(clientConfig["config"]).to.deep.equal({
66
+ configPerEnvironment: fullPerEnv({
67
+ production: { project: "test-project" },
68
+ }),
69
+ activeEnvironment: "production",
70
+ });
71
+ });
72
+
73
+ it("should allow setAccessToken when activeEnvironment had no bucket on disk (e.g. videoamp)", () => {
74
+ const mockConfig = {
75
+ configPerEnvironment: {
76
+ production: {},
77
+ staging: {},
78
+ dev: {},
79
+ localhost: {},
80
+ "e2e-test": {},
81
+ },
82
+ activeEnvironment: "videoamp",
83
+ };
84
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
85
+ clientConfig.setAccessToken(true, "user-jwt");
86
+ const written = JSON.parse(writeFileSyncStub.lastCall.args[1] as string);
87
+ expect(written.configPerEnvironment.videoamp.accessToken).to.equal(
88
+ "user-jwt",
89
+ );
90
+ expect(written.configPerEnvironment.videoamp.isServiceAccountToken).to.be
91
+ .false;
57
92
  });
58
93
 
59
94
  it("should set and get the project correctly", () => {
@@ -64,11 +99,9 @@ describe("ClientConfiguration", () => {
64
99
  writeFileSyncStub.calledWith(
65
100
  customConfigPath,
66
101
  JSON.stringify({
67
- configPerEnvironment: {
102
+ configPerEnvironment: fullPerEnv({
68
103
  production: { project: "new-project" },
69
- staging: {},
70
- localhost: {},
71
- },
104
+ }),
72
105
  activeEnvironment: "production",
73
106
  }),
74
107
  ),
@@ -83,11 +116,12 @@ describe("ClientConfiguration", () => {
83
116
  writeFileSyncStub.calledWith(
84
117
  customConfigPath,
85
118
  JSON.stringify({
86
- configPerEnvironment: {
87
- production: { isServiceAccountToken: true, accessToken: "token-123" },
88
- staging: {},
89
- localhost: {},
90
- },
119
+ configPerEnvironment: fullPerEnv({
120
+ production: {
121
+ isServiceAccountToken: true,
122
+ accessToken: "token-123",
123
+ },
124
+ }),
91
125
  activeEnvironment: "production",
92
126
  }),
93
127
  ),
@@ -103,14 +137,354 @@ describe("ClientConfiguration", () => {
103
137
  writeFileSyncStub.calledWith(
104
138
  customConfigPath,
105
139
  JSON.stringify({
106
- configPerEnvironment: {
107
- production: { isServiceAccountToken: false, accessToken: "new-token" },
108
- staging: {},
109
- localhost: {},
110
- },
140
+ configPerEnvironment: fullPerEnv({
141
+ production: {
142
+ isServiceAccountToken: false,
143
+ accessToken: "new-token",
144
+ },
145
+ }),
146
+ activeEnvironment: "production",
147
+ }),
148
+ ),
149
+ ).to.be.true;
150
+ });
151
+
152
+ it("should set and get organization", () => {
153
+ readFileSyncStub.throws(new Error("File not found"));
154
+ clientConfig.setOrganization("my-org");
155
+
156
+ expect(
157
+ writeFileSyncStub.calledWith(
158
+ customConfigPath,
159
+ JSON.stringify({
160
+ configPerEnvironment: fullPerEnv({
161
+ production: { organization: "my-org" },
162
+ }),
163
+ activeEnvironment: "production",
164
+ }),
165
+ ),
166
+ ).to.be.true;
167
+ });
168
+
169
+ it("should set and get refresh token", () => {
170
+ readFileSyncStub.throws(new Error("File not found"));
171
+ clientConfig.setRefreshToken("refresh-token-123");
172
+
173
+ expect(
174
+ writeFileSyncStub.calledWith(
175
+ customConfigPath,
176
+ JSON.stringify({
177
+ configPerEnvironment: fullPerEnv({
178
+ production: { refreshToken: "refresh-token-123" },
179
+ }),
180
+ activeEnvironment: "production",
181
+ }),
182
+ ),
183
+ ).to.be.true;
184
+ });
185
+
186
+ it("should set and get VSCode user", () => {
187
+ readFileSyncStub.throws(new Error("File not found"));
188
+ clientConfig.setVSCodeUser("testuser", "test@example.com");
189
+
190
+ expect(
191
+ writeFileSyncStub.calledWith(
192
+ customConfigPath,
193
+ JSON.stringify({
194
+ configPerEnvironment: fullPerEnv({
195
+ production: {
196
+ vsCodeUser: {
197
+ username: "testuser",
198
+ email: "test@example.com",
199
+ },
200
+ },
201
+ }),
111
202
  activeEnvironment: "production",
112
203
  }),
113
204
  ),
114
205
  ).to.be.true;
115
206
  });
207
+
208
+ it("should set and get JWT access token", () => {
209
+ readFileSyncStub.throws(new Error("File not found"));
210
+ clientConfig.setJwtAccessToken("jwt-token-abc");
211
+
212
+ expect(
213
+ writeFileSyncStub.calledWith(
214
+ customConfigPath,
215
+ JSON.stringify({
216
+ configPerEnvironment: fullPerEnv({
217
+ production: { jwtAccessToken: "jwt-token-abc" },
218
+ }),
219
+ activeEnvironment: "production",
220
+ }),
221
+ ),
222
+ ).to.be.true;
223
+ });
224
+
225
+ it("should return true for doesConfigExists when environment config exists", () => {
226
+ const mockConfig = {
227
+ configPerEnvironment: {
228
+ production: { project: "test" },
229
+ staging: {},
230
+ localhost: {},
231
+ },
232
+ activeEnvironment: "production",
233
+ };
234
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
235
+
236
+ expect(clientConfig.doesConfigExists()).to.be.true;
237
+ });
238
+
239
+ it("should destroy config by resetting environment config to empty", () => {
240
+ const mockConfig = {
241
+ configPerEnvironment: {
242
+ production: { organization: "my-org", project: "my-proj" },
243
+ staging: {},
244
+ localhost: {},
245
+ },
246
+ activeEnvironment: "production",
247
+ };
248
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
249
+ clientConfig.destroyConfig();
250
+
251
+ const lastWrite = writeFileSyncStub.lastCall.args[1] as string;
252
+ expect(JSON.parse(lastWrite)).to.deep.equal({
253
+ configPerEnvironment: fullPerEnv(),
254
+ activeEnvironment: "production",
255
+ });
256
+ });
257
+
258
+ it("should set and get environment", () => {
259
+ readFileSyncStub.throws(new Error("File not found"));
260
+ clientConfig.setEnvironment("staging");
261
+
262
+ expect(
263
+ writeFileSyncStub.calledWith(
264
+ customConfigPath,
265
+ JSON.stringify({
266
+ configPerEnvironment: fullPerEnv(),
267
+ activeEnvironment: "staging",
268
+ }),
269
+ ),
270
+ ).to.be.true;
271
+ });
272
+
273
+ it("should return headers with Bearer token for user tokens", () => {
274
+ const mockConfig = {
275
+ configPerEnvironment: {
276
+ production: {
277
+ accessToken: "user-token",
278
+ isServiceAccountToken: false,
279
+ },
280
+ staging: {},
281
+ localhost: {},
282
+ },
283
+ activeEnvironment: "production",
284
+ };
285
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
286
+ clientConfig.loadConfig();
287
+
288
+ const headers = clientConfig.getHeaders();
289
+ expect(headers.Authorization).to.equal("Bearer user-token");
290
+ });
291
+
292
+ it("should return headers with ApiKey for service account tokens", () => {
293
+ const mockConfig = {
294
+ configPerEnvironment: {
295
+ production: {
296
+ accessToken: "service-token",
297
+ isServiceAccountToken: true,
298
+ },
299
+ staging: {},
300
+ localhost: {},
301
+ },
302
+ activeEnvironment: "production",
303
+ };
304
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
305
+ clientConfig.loadConfig();
306
+
307
+ const headers = clientConfig.getHeaders();
308
+ expect(headers.Authorization).to.equal("ApiKey service-token");
309
+ });
310
+
311
+ it("should reset to defaults when config file is corrupted (missing activeEnvironment)", () => {
312
+ readFileSyncStub.returns(JSON.stringify({ configPerEnvironment: {} }));
313
+ clientConfig.loadConfig();
314
+
315
+ expect(clientConfig["config"].activeEnvironment).to.equal("production");
316
+ });
317
+
318
+ describe("isTokenValid", () => {
319
+ it("should return invalid when no token exists", () => {
320
+ const mockConfig = {
321
+ configPerEnvironment: {
322
+ production: {},
323
+ staging: {},
324
+ localhost: {},
325
+ },
326
+ activeEnvironment: "production",
327
+ };
328
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
329
+
330
+ const result = clientConfig.isTokenValid();
331
+ expect(result.valid).to.be.false;
332
+ expect(result.reason).to.equal("No token found. Please login.");
333
+ });
334
+
335
+ it("should return valid for service account tokens", () => {
336
+ const mockConfig = {
337
+ configPerEnvironment: {
338
+ production: {
339
+ accessToken: "some-api-key",
340
+ isServiceAccountToken: true,
341
+ },
342
+ staging: {},
343
+ localhost: {},
344
+ },
345
+ activeEnvironment: "production",
346
+ };
347
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
348
+
349
+ const result = clientConfig.isTokenValid();
350
+ expect(result.valid).to.be.true;
351
+ });
352
+
353
+ it("should return invalid for expired JWT", () => {
354
+ // Create a JWT with an expired exp claim
355
+ const payload = { exp: Math.floor(Date.now() / 1000) - 3600 };
356
+ const fakeJwt =
357
+ Buffer.from(JSON.stringify({ alg: "HS256" })).toString(
358
+ "base64url",
359
+ ) +
360
+ "." +
361
+ Buffer.from(JSON.stringify(payload)).toString("base64url") +
362
+ ".signature";
363
+
364
+ const mockConfig = {
365
+ configPerEnvironment: {
366
+ production: {
367
+ accessToken: fakeJwt,
368
+ isServiceAccountToken: false,
369
+ },
370
+ staging: {},
371
+ localhost: {},
372
+ },
373
+ activeEnvironment: "production",
374
+ };
375
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
376
+
377
+ const result = clientConfig.isTokenValid();
378
+ expect(result.valid).to.be.false;
379
+ expect(result.reason).to.equal("Token expired. Please login again.");
380
+ });
381
+
382
+ it("should return valid for non-expired JWT", () => {
383
+ const payload = { exp: Math.floor(Date.now() / 1000) + 3600 };
384
+ const fakeJwt =
385
+ Buffer.from(JSON.stringify({ alg: "HS256" })).toString(
386
+ "base64url",
387
+ ) +
388
+ "." +
389
+ Buffer.from(JSON.stringify(payload)).toString("base64url") +
390
+ ".signature";
391
+
392
+ const mockConfig = {
393
+ configPerEnvironment: {
394
+ production: {
395
+ accessToken: fakeJwt,
396
+ isServiceAccountToken: false,
397
+ },
398
+ staging: {},
399
+ localhost: {},
400
+ },
401
+ activeEnvironment: "production",
402
+ };
403
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
404
+
405
+ const result = clientConfig.isTokenValid();
406
+ expect(result.valid).to.be.true;
407
+ });
408
+
409
+ it("should return invalid for malformed token", () => {
410
+ const mockConfig = {
411
+ configPerEnvironment: {
412
+ production: {
413
+ accessToken: "not-a-valid-jwt",
414
+ isServiceAccountToken: false,
415
+ },
416
+ staging: {},
417
+ localhost: {},
418
+ },
419
+ activeEnvironment: "production",
420
+ };
421
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
422
+
423
+ const result = clientConfig.isTokenValid();
424
+ expect(result.valid).to.be.false;
425
+ expect(result.reason).to.equal(
426
+ "Invalid token format. Please login again.",
427
+ );
428
+ });
429
+ });
430
+
431
+ describe("printStatus", () => {
432
+ let loggerStub: sinon.SinonStubbedInstance<typeof logger>;
433
+ let consoleLogStub: sinon.SinonStub;
434
+
435
+ beforeEach(() => {
436
+ loggerStub = sinon.stub(logger);
437
+ consoleLogStub = sinon.stub(console, "log");
438
+ });
439
+
440
+ it("should print org and project when token is valid", () => {
441
+ const payload = { exp: Math.floor(Date.now() / 1000) + 3600 };
442
+ const fakeJwt =
443
+ Buffer.from(JSON.stringify({ alg: "HS256" })).toString(
444
+ "base64url",
445
+ ) +
446
+ "." +
447
+ Buffer.from(JSON.stringify(payload)).toString("base64url") +
448
+ ".signature";
449
+
450
+ const mockConfig = {
451
+ configPerEnvironment: {
452
+ production: {
453
+ accessToken: fakeJwt,
454
+ isServiceAccountToken: false,
455
+ organization: "my-org",
456
+ project: "my-project",
457
+ },
458
+ staging: {},
459
+ localhost: {},
460
+ },
461
+ activeEnvironment: "production",
462
+ };
463
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
464
+
465
+ clientConfig.printStatus();
466
+
467
+ expect(loggerStub.info.calledWith(sinon.match("Organization:"))).to.be
468
+ .true;
469
+ expect(loggerStub.info.calledWith(sinon.match("Project:"))).to.be.true;
470
+ });
471
+
472
+ it("should print error when token is invalid", () => {
473
+ const mockConfig = {
474
+ configPerEnvironment: {
475
+ production: {},
476
+ staging: {},
477
+ localhost: {},
478
+ },
479
+ activeEnvironment: "production",
480
+ };
481
+ readFileSyncStub.returns(JSON.stringify(mockConfig));
482
+
483
+ clientConfig.printStatus();
484
+
485
+ expect(
486
+ loggerStub.error.calledWith(sinon.match("Failed to get status")),
487
+ ).to.be.true;
488
+ });
489
+ });
116
490
  });
@@ -1,7 +1,8 @@
1
1
  import fs from "fs";
2
+ import { jwtDecode } from "jwt-decode";
2
3
  import kleur from "kleur";
3
4
  import os from "os";
4
- import { ASCII_TITLE, Environment } from "./constants";
5
+ import { ALL_ENVIRONMENTS, ASCII_TITLE, Environment } from "./constants";
5
6
  import { logger } from "./logger";
6
7
 
7
8
  interface ConfigFile {
@@ -26,6 +27,12 @@ interface ConfigFile {
26
27
  activeEnvironment: Environment;
27
28
  }
28
29
 
30
+ interface JwtPayload {
31
+ exp: number;
32
+ iat?: number;
33
+ sub?: string;
34
+ }
35
+
29
36
  export class ClientConfiguration {
30
37
  private configPath = `${os.homedir()}/.cred`;
31
38
  private config: ConfigFile;
@@ -50,18 +57,23 @@ export class ClientConfiguration {
50
57
  }
51
58
  } catch (error) {
52
59
  logger.warn(error);
60
+ const configPerEnvironment: ConfigFile["configPerEnvironment"] = {};
61
+ for (const env of ALL_ENVIRONMENTS) {
62
+ configPerEnvironment[env] = {};
63
+ }
53
64
  this.config = {
54
- configPerEnvironment: {
55
- production: {},
56
- staging: {},
57
- localhost: {},
58
- },
65
+ configPerEnvironment,
59
66
  activeEnvironment: "production",
60
67
  };
61
- // Ensure the config file exists with default settings
62
68
  fs.writeFileSync(this.configPath, JSON.stringify(this.config));
63
69
  }
64
70
 
71
+ for (const env of ALL_ENVIRONMENTS) {
72
+ if (!this.config.configPerEnvironment[env]) {
73
+ this.config.configPerEnvironment[env] = {};
74
+ }
75
+ }
76
+
65
77
  return this.config;
66
78
  }
67
79
 
@@ -97,7 +109,8 @@ export class ClientConfiguration {
97
109
  public setAccessToken(userToken: boolean, accessToken: string) {
98
110
  this.loadConfig();
99
111
  const environment = this.getEnvironment();
100
- this.config.configPerEnvironment[environment].isServiceAccountToken = !userToken;
112
+ this.config.configPerEnvironment[environment].isServiceAccountToken =
113
+ !userToken;
101
114
  this.config.configPerEnvironment[environment].accessToken = accessToken;
102
115
  fs.writeFileSync(this.configPath, JSON.stringify(this.config));
103
116
  }
@@ -105,7 +118,8 @@ export class ClientConfiguration {
105
118
  public getAuthenticationHeader() {
106
119
  const environment = this.getEnvironment();
107
120
  const jwt = this.config.configPerEnvironment[environment].accessToken;
108
- const isServiceAccountToken = this.config.configPerEnvironment[environment].isServiceAccountToken;
121
+ const isServiceAccountToken =
122
+ this.config.configPerEnvironment[environment].isServiceAccountToken;
109
123
  const authType = isServiceAccountToken ? "ApiKey" : "Bearer";
110
124
  return `${authType} ${jwt}`;
111
125
  }
@@ -192,13 +206,58 @@ export class ClientConfiguration {
192
206
  return this.config.activeEnvironment || "production";
193
207
  }
194
208
 
209
+ public isTokenValid(): { valid: boolean; reason?: string } {
210
+ this.loadConfig();
211
+ const environment = this.getEnvironment();
212
+ const accessToken =
213
+ this.config.configPerEnvironment[environment]?.accessToken;
214
+
215
+ if (!accessToken) {
216
+ return { valid: false, reason: "No token found. Please login." };
217
+ }
218
+
219
+ const isServiceAccountToken =
220
+ this.config.configPerEnvironment[environment]?.isServiceAccountToken;
221
+ if (isServiceAccountToken) {
222
+ return { valid: true };
223
+ }
224
+
225
+ try {
226
+ const decoded = jwtDecode<JwtPayload>(accessToken);
227
+ const currentTime = Math.floor(Date.now() / 1000);
228
+
229
+ if (decoded.exp && decoded.exp < currentTime) {
230
+ return {
231
+ valid: false,
232
+ reason: "Token expired. Please login again.",
233
+ };
234
+ }
235
+
236
+ return { valid: true };
237
+ } catch {
238
+ return {
239
+ valid: false,
240
+ reason: "Invalid token format. Please login again.",
241
+ };
242
+ }
243
+ }
244
+
195
245
  public printStatus() {
196
246
  this.loadConfig();
197
- const organization = this.getOrganization();
198
- const project = this.getProject();
199
- // eslint-disable-next-line no-console
200
- console.log(kleur.green(ASCII_TITLE));
201
- logger.info(`Organization: ${kleur.green(organization || "Not set")}`);
202
- logger.info(`Project: ${kleur.green(project || "Not set")}`);
247
+ const tokenStatus = this.isTokenValid();
248
+
249
+ if (tokenStatus.valid) {
250
+ const organization = this.getOrganization();
251
+ const project = this.getProject();
252
+
253
+ console.log(kleur.green(ASCII_TITLE));
254
+ logger.info(`Organization: ${kleur.green(organization || "Not set")}`);
255
+ logger.info(`Project: ${kleur.green(project || "Not set")}`);
256
+ } else {
257
+ logger.error("Failed to get status");
258
+ logger.error(
259
+ tokenStatus.reason || "Please, make sure that you are logged in",
260
+ );
261
+ }
203
262
  }
204
263
  }
@@ -21,11 +21,17 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
21
21
  "--readmeFile <readmeFile>",
22
22
  "The path to the readme file for the project.",
23
23
  )
24
+ .option(
25
+ "--replication <replicationCount>",
26
+ "The replication count (must be at least 1).",
27
+ parseInt,
28
+ )
24
29
  .action((projectName, options) => {
25
30
  createProject(
26
31
  controlPlanesByEnv[environment],
27
32
  projectName,
28
33
  options.readmeFile,
34
+ options.replication,
29
35
  options.debug,
30
36
  options.yes,
31
37
  );
@@ -44,6 +50,11 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
44
50
  "--exclude-tables <excludeTables>",
45
51
  "The list of tables to exclude. Ex: {dataset/schema}.{table}",
46
52
  )
53
+ .option(
54
+ "--skip-indexing",
55
+ "Disable automatic indexing for this connection.",
56
+ false,
57
+ )
47
58
  .option("--debug")
48
59
  .option("-y, --yes", "Skip confirmation.")
49
60
  .action((connectionFileName, options) => {
@@ -52,6 +63,7 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
52
63
  connectionFileName,
53
64
  options.includeTables,
54
65
  options.excludeTables,
66
+ options.skipIndexing,
55
67
  options.debug,
56
68
  options.yes,
57
69
  );
@@ -90,27 +102,24 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
90
102
  );
91
103
  });
92
104
 
93
-
94
105
  add.command("group-access-token")
95
106
  .description("Create an access token for a group.")
96
- .argument(
97
- "<groupName>",
98
- "The name of the group",
99
- )
107
+ .argument("<groupName>", "The name of the group")
100
108
  .argument("<tokenName>", "The name of the token to create")
101
109
  .option(
102
110
  "-e, --expires-at <expiresAt>",
103
111
  "Token expiration date in ISO 8601 format (e.g., 2025-12-31T23:59:59Z). If not provided, token will expire in 99 years.",
104
112
  )
105
113
  .option("--debug")
114
+ .option("-j, --json-output", "Output only JSON (for use in scripts)")
106
115
  .action((groupName, tokenName, options) => {
107
116
  createGroupAccessToken(
108
117
  controlPlanesByEnv[environment],
109
118
  groupName,
110
119
  tokenName,
111
120
  options.expiresAt,
121
+ options.jsonOutput === true,
112
122
  options.debug || false,
113
123
  );
114
124
  });
115
-
116
125
  };