@sasjs/cli 4.18.0 → 4.18.1

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.
@@ -255,13 +255,13 @@ describe('sasjs cbd with Viya', function () {
255
255
  return __generator(this, function (_a) {
256
256
  switch (_a.label) {
257
257
  case 0:
258
- jest.spyOn(configUtils, 'getAccessToken').mockImplementation(function () {
258
+ jest.spyOn(configUtils, 'getAuthConfig').mockImplementation(function () {
259
259
  return Promise.reject('Token error');
260
260
  });
261
261
  return [4 /*yield*/, (0, build_1.build)(target)];
262
262
  case 1:
263
263
  _a.sent();
264
- return [4 /*yield*/, expect((0, deploy_1.deploy)(target, true)).rejects.toThrow(new Error("Deployment failed. Request is not authenticated.\nPlease add the following variables to your .env.".concat(target.name, " file:\nCLIENT, SECRET, ACCESS_TOKEN, REFRESH_TOKEN")))];
264
+ return [4 /*yield*/, expect((0, deploy_1.deploy)(target, true)).rejects.toThrow(new Error("Deployment failed. Request is not authenticated.\nToken error"))];
265
265
  case 2:
266
266
  _a.sent();
267
267
  jest.restoreAllMocks();
@@ -55,12 +55,11 @@ function deployToSasViyaWithServicePack(jsonFilePath, target, isLocal, isForced)
55
55
  throw new Error('Provided data file must be valid json.');
56
56
  }
57
57
  populateCodeInServicePack(jsonObject);
58
- return [4 /*yield*/, (0, utils_2.getAccessToken)(target).catch(function (e) { return ''; })];
58
+ return [4 /*yield*/, (0, utils_2.getAuthConfig)(target).catch(function (err) {
59
+ throw new Error("Deployment failed. Request is not authenticated.\n".concat((err === null || err === void 0 ? void 0 : err.message) || err));
60
+ })];
59
61
  case 2:
60
- access_token = _a.sent();
61
- if (!access_token) {
62
- throw new Error("Deployment failed. Request is not authenticated.\nPlease add the following variables to your .env".concat(isLocal ? ".".concat(target.name) : '', " file:\nCLIENT, SECRET, ACCESS_TOKEN, REFRESH_TOKEN"));
63
- }
62
+ access_token = (_a.sent()).access_token;
64
63
  sasjs = (0, utils_2.getSASjs)(target);
65
64
  return [4 /*yield*/, sasjs
66
65
  .deployServicePack(jsonObject, undefined, undefined, access_token, isForced)
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __generator = (this && this.__generator) || function (thisArg, body) {
35
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
36
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
37
+ function verb(n) { return function (v) { return step([n, v]); }; }
38
+ function step(op) {
39
+ if (f) throw new TypeError("Generator is already executing.");
40
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
41
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
42
+ if (y = 0, t) op = [op[0] & 2, t.value];
43
+ switch (op[0]) {
44
+ case 0: case 1: t = op; break;
45
+ case 4: _.label++; return { value: op[1], done: false };
46
+ case 5: _.label++; y = op[1]; op = [0]; continue;
47
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
48
+ default:
49
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
50
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
51
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
52
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
53
+ if (t[2]) _.ops.pop();
54
+ _.trys.pop(); continue;
55
+ }
56
+ op = body.call(thisArg, _);
57
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
58
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
59
+ }
60
+ };
61
+ var __importDefault = (this && this.__importDefault) || function (mod) {
62
+ return (mod && mod.__esModule) ? mod : { "default": mod };
63
+ };
64
+ Object.defineProperty(exports, "__esModule", { value: true });
65
+ var path_1 = __importDefault(require("path"));
66
+ var os_1 = __importDefault(require("os"));
67
+ var utils_1 = require("@sasjs/utils");
68
+ var configUtils = __importStar(require("../../../utils/config"));
69
+ var deployToSasViyaWithServicePack_1 = require("../deployToSasViyaWithServicePack");
70
+ var target = new utils_1.Target({
71
+ name: 'test',
72
+ appLoc: '/Public/test/',
73
+ serverType: utils_1.ServerType.SasViya,
74
+ serverUrl: 'https://server.com',
75
+ contextName: 'test context'
76
+ });
77
+ var mockAuthConfig = {
78
+ client: 'cl13nt',
79
+ secret: '53cr3t',
80
+ access_token: 'acc355',
81
+ refresh_token: 'r3fr35h'
82
+ };
83
+ describe('deployToSasViyaWithServicePack', function () {
84
+ var jsonFilePath;
85
+ var deployServicePack;
86
+ beforeEach(function () { return __awaiter(void 0, void 0, void 0, function () {
87
+ return __generator(this, function (_a) {
88
+ switch (_a.label) {
89
+ case 0:
90
+ jsonFilePath = path_1.default.join(os_1.default.tmpdir(), "servicepack-".concat(Date.now(), ".json"));
91
+ return [4 /*yield*/, (0, utils_1.createFile)(jsonFilePath, JSON.stringify({ members: [] }))];
92
+ case 1:
93
+ _a.sent();
94
+ deployServicePack = jest.fn().mockResolvedValue({});
95
+ jest
96
+ .spyOn(configUtils, 'getSASjs')
97
+ .mockImplementation(function () { return ({ deployServicePack: deployServicePack }); });
98
+ return [2 /*return*/];
99
+ }
100
+ });
101
+ }); });
102
+ afterEach(function () { return __awaiter(void 0, void 0, void 0, function () {
103
+ return __generator(this, function (_a) {
104
+ switch (_a.label) {
105
+ case 0: return [4 /*yield*/, (0, utils_1.deleteFile)(jsonFilePath)];
106
+ case 1:
107
+ _a.sent();
108
+ jest.restoreAllMocks();
109
+ return [2 /*return*/];
110
+ }
111
+ });
112
+ }); });
113
+ it('refreshes and persists the access token via getAuthConfig instead of the non-persisting getAccessToken', function () { return __awaiter(void 0, void 0, void 0, function () {
114
+ var getAuthConfigSpy;
115
+ return __generator(this, function (_a) {
116
+ switch (_a.label) {
117
+ case 0:
118
+ getAuthConfigSpy = jest
119
+ .spyOn(configUtils, 'getAuthConfig')
120
+ .mockImplementation(function () { return Promise.resolve(mockAuthConfig); });
121
+ return [4 /*yield*/, expect((0, deployToSasViyaWithServicePack_1.deployToSasViyaWithServicePack)(jsonFilePath, target, true, false)).toResolve()];
122
+ case 1:
123
+ _a.sent();
124
+ expect(getAuthConfigSpy).toHaveBeenCalledWith(target);
125
+ expect(deployServicePack).toHaveBeenCalledWith({ members: [] }, undefined, undefined, mockAuthConfig.access_token, false);
126
+ return [2 /*return*/];
127
+ }
128
+ });
129
+ }); });
130
+ it('surfaces the real auth/refresh error instead of a generic "add these variables" message when credentials already exist', function () { return __awaiter(void 0, void 0, void 0, function () {
131
+ var realError;
132
+ return __generator(this, function (_a) {
133
+ switch (_a.label) {
134
+ case 0:
135
+ realError = new Error('invalid_grant: refresh token has already been used');
136
+ jest.spyOn(configUtils, 'getAuthConfig').mockImplementation(function () {
137
+ return Promise.reject(realError);
138
+ });
139
+ return [4 /*yield*/, expect((0, deployToSasViyaWithServicePack_1.deployToSasViyaWithServicePack)(jsonFilePath, target, true, false)).rejects.toThrow(/invalid_grant: refresh token has already been used/)];
140
+ case 1:
141
+ _a.sent();
142
+ return [2 /*return*/];
143
+ }
144
+ });
145
+ }); });
146
+ });
@@ -18,6 +18,14 @@ export declare function isAccessTokenExpiring(token: string): boolean;
18
18
  * @param {string} token- token string that will be evaluated
19
19
  */
20
20
  export declare function isRefreshTokenExpiring(token?: string): boolean;
21
+ /**
22
+ * Exchanges a refresh token for a new access/refresh token pair.
23
+ * SAS Viya's refresh tokens are single-use and rotate on every call: the
24
+ * `refresh_token` returned here supersedes the one passed in, which becomes
25
+ * invalid immediately. Callers must persist the returned pair (see
26
+ * `saveTokens` in config.ts) or a later refresh attempt with the old token
27
+ * will be rejected by the server.
28
+ */
21
29
  export declare function refreshTokens(sasjsInstance: SASjs, clientId: string, clientSecret: string, refreshToken: string): Promise<{
22
30
  access_token: string;
23
31
  refresh_token: string;
@@ -101,6 +101,14 @@ function isRefreshTokenExpiring(token) {
101
101
  return timeToLive <= 30; // 30 seconds
102
102
  }
103
103
  exports.isRefreshTokenExpiring = isRefreshTokenExpiring;
104
+ /**
105
+ * Exchanges a refresh token for a new access/refresh token pair.
106
+ * SAS Viya's refresh tokens are single-use and rotate on every call: the
107
+ * `refresh_token` returned here supersedes the one passed in, which becomes
108
+ * invalid immediately. Callers must persist the returned pair (see
109
+ * `saveTokens` in config.ts) or a later refresh attempt with the old token
110
+ * will be rejected by the server.
111
+ */
104
112
  function refreshTokens(sasjsInstance, clientId, clientSecret, refreshToken) {
105
113
  return __awaiter(this, void 0, void 0, function () {
106
114
  var _a, access_token, refresh_token;
@@ -61,11 +61,29 @@ export declare function getStreamConfig(target?: Target): Promise<StreamConfig>;
61
61
  export declare function sanitizeAppLoc(appLoc: string): string;
62
62
  export declare function getProjectRoot(): Promise<string>;
63
63
  /**
64
- * Gets the auth config for the specified target.
64
+ * Gets the auth config for the specified target, refreshing the access token
65
+ * (via the refresh token) if it is expiring.
66
+ *
67
+ * Unlike {@link getAccessToken}, this persists any newly issued access/refresh
68
+ * token pair via {@link saveTokens}. That persistence is not optional: SAS Viya
69
+ * issues rotating, single-use refresh tokens, so once a refresh token has been
70
+ * used to mint a new pair, the old one is void. If the new pair isn't written
71
+ * back to `.env.{target}` / the global config, the *next* CLI invocation reads
72
+ * the now-stale refresh token from disk and fails to authenticate - which is
73
+ * indistinguishable, from the user's point of view, from never having
74
+ * refreshed at all. This is why every command that needs an authenticated
75
+ * request should call `getAuthConfig`, not `getAccessToken`.
65
76
  * @param {Target} target - the target to get an access token for.
66
77
  * @returns {AuthConfig} - an object containing an access token, refresh token, client ID and secret.
67
78
  */
68
79
  export declare function getAuthConfig(target: Target): Promise<AuthConfig>;
80
+ /**
81
+ * Persists an access/refresh token pair to wherever the target's credentials
82
+ * live: `.env.{targetName}` for a local target, or `authConfig` in the global
83
+ * `~/.sasjsrc` otherwise. Called by {@link getAuthConfig} after every refresh
84
+ * so that a rotated (single-use) refresh token from SAS Viya is never reused -
85
+ * reusing it on a subsequent CLI invocation would be rejected by the server.
86
+ */
69
87
  export declare const saveTokens: (targetName: string, client: string, secret: string, access_token: string, refresh_token: string) => Promise<void>;
70
88
  /**
71
89
  * Gets the auth config for the specified SAS9 target.
@@ -84,6 +102,16 @@ export declare function getAuthConfigSAS9(target: Target): AuthConfigSas9;
84
102
  * If a refresh token is unavailable, we will use the client ID and secret
85
103
  * to obtain a new access token. Manual intervention is required in this case
86
104
  * to navigate to the URL shown and type in an authorization code.
105
+ *
106
+ * IMPORTANT: unlike {@link getAuthConfig}, this does NOT persist a refreshed
107
+ * access/refresh token pair back to disk - the new tokens only live for the
108
+ * lifetime of this process. Since SAS Viya refresh tokens are single-use and
109
+ * rotate on every refresh, calling this from a command whose CLI invocation
110
+ * ends shortly after (leaving the stale refresh token on disk) will cause the
111
+ * *next* invocation to fail to authenticate. Only use this where that's
112
+ * acceptable - e.g. one-shot, same-process cleanup calls such as
113
+ * `removeTestServerFolder` in test support code. Any user-facing command
114
+ * should use `getAuthConfig` instead.
87
115
  * @param {object} target - the target to get an access token for.
88
116
  * @param {string} checkIfExpiring - flag that indicates whether to do an expiry check.
89
117
  */
@@ -790,7 +790,18 @@ function getProjectRoot() {
790
790
  }
791
791
  exports.getProjectRoot = getProjectRoot;
792
792
  /**
793
- * Gets the auth config for the specified target.
793
+ * Gets the auth config for the specified target, refreshing the access token
794
+ * (via the refresh token) if it is expiring.
795
+ *
796
+ * Unlike {@link getAccessToken}, this persists any newly issued access/refresh
797
+ * token pair via {@link saveTokens}. That persistence is not optional: SAS Viya
798
+ * issues rotating, single-use refresh tokens, so once a refresh token has been
799
+ * used to mint a new pair, the old one is void. If the new pair isn't written
800
+ * back to `.env.{target}` / the global config, the *next* CLI invocation reads
801
+ * the now-stale refresh token from disk and fails to authenticate - which is
802
+ * indistinguishable, from the user's point of view, from never having
803
+ * refreshed at all. This is why every command that needs an authenticated
804
+ * request should call `getAuthConfig`, not `getAccessToken`.
794
805
  * @param {Target} target - the target to get an access token for.
795
806
  * @returns {AuthConfig} - an object containing an access token, refresh token, client ID and secret.
796
807
  */
@@ -874,6 +885,13 @@ function getAuthConfig(target) {
874
885
  });
875
886
  }
876
887
  exports.getAuthConfig = getAuthConfig;
888
+ /**
889
+ * Persists an access/refresh token pair to wherever the target's credentials
890
+ * live: `.env.{targetName}` for a local target, or `authConfig` in the global
891
+ * `~/.sasjsrc` otherwise. Called by {@link getAuthConfig} after every refresh
892
+ * so that a rotated (single-use) refresh token from SAS Viya is never reused -
893
+ * reusing it on a subsequent CLI invocation would be rejected by the server.
894
+ */
877
895
  var saveTokens = function (targetName, client, secret, access_token, refresh_token) { return __awaiter(void 0, void 0, void 0, function () {
878
896
  var isLocalTarget, VERBOSE, envFileContent, envFilePath, globalConfig, target, targetJson;
879
897
  var _a, _b, _c;
@@ -952,6 +970,16 @@ exports.getAuthConfigSAS9 = getAuthConfigSAS9;
952
970
  * If a refresh token is unavailable, we will use the client ID and secret
953
971
  * to obtain a new access token. Manual intervention is required in this case
954
972
  * to navigate to the URL shown and type in an authorization code.
973
+ *
974
+ * IMPORTANT: unlike {@link getAuthConfig}, this does NOT persist a refreshed
975
+ * access/refresh token pair back to disk - the new tokens only live for the
976
+ * lifetime of this process. Since SAS Viya refresh tokens are single-use and
977
+ * rotate on every refresh, calling this from a command whose CLI invocation
978
+ * ends shortly after (leaving the stale refresh token on disk) will cause the
979
+ * *next* invocation to fail to authenticate. Only use this where that's
980
+ * acceptable - e.g. one-shot, same-process cleanup calls such as
981
+ * `removeTestServerFolder` in test support code. Any user-facing command
982
+ * should use `getAuthConfig` instead.
955
983
  * @param {object} target - the target to get an access token for.
956
984
  * @param {string} checkIfExpiring - flag that indicates whether to do an expiry check.
957
985
  */
package/package.json CHANGED
@@ -122,5 +122,5 @@
122
122
  "overrides": {
123
123
  "ws": "8.21.0"
124
124
  },
125
- "version": "4.18.0"
125
+ "version": "4.18.1"
126
126
  }