@sasjs/adapter 4.17.3 → 4.19.0
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.
- package/SASViyaApiClient.d.ts +6 -7
- package/SASjs.d.ts +6 -2
- package/api/viya/executeOnComputeApi.d.ts +3 -1
- package/api/viya/pollJobState.d.ts +3 -2
- package/auth/getTokens.d.ts +13 -1
- package/index-dev.js +145 -38
- package/index.d.ts +1 -0
- package/index.js +145 -38
- package/minified_sas9.js +39 -5
- package/node/SASViyaApiClient.d.ts +6 -7
- package/node/SASjs.d.ts +6 -2
- package/node/api/viya/executeOnComputeApi.d.ts +3 -1
- package/node/api/viya/pollJobState.d.ts +3 -2
- package/node/auth/getTokens.d.ts +13 -1
- package/node/index.d.ts +1 -0
- package/node/index.js +145 -38
- package/package.json +2 -2
- package/spec/SASjs.startComputeJob.spec.d.ts +1 -0
- package/src/SASViyaApiClient.ts +44 -16
- package/src/SASjs.ts +17 -4
- package/src/api/viya/executeOnComputeApi.ts +19 -6
- package/src/api/viya/pollJobState.ts +21 -9
- package/src/auth/getTokens.ts +82 -9
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -92,12 +92,12 @@
|
|
|
92
92
|
},
|
|
93
93
|
"main": "index.js",
|
|
94
94
|
"dependencies": {
|
|
95
|
-
"@sasjs/utils": "^3.
|
|
95
|
+
"@sasjs/utils": "^3.6.0",
|
|
96
96
|
"axios": "1.18.1",
|
|
97
97
|
"axios-cookiejar-support": "5.0.5",
|
|
98
98
|
"form-data": "4.0.6",
|
|
99
99
|
"https": "1.0.0",
|
|
100
100
|
"tough-cookie": "4.1.3"
|
|
101
101
|
},
|
|
102
|
-
"version": "4.
|
|
102
|
+
"version": "4.19.0"
|
|
103
103
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/SASViyaApiClient.ts
CHANGED
|
@@ -18,12 +18,17 @@ import {
|
|
|
18
18
|
} from './types/errors'
|
|
19
19
|
import { SessionManager } from './SessionManager'
|
|
20
20
|
import { ContextManager } from './ContextManager'
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
SasAuthResponse,
|
|
23
|
+
MacroVar,
|
|
24
|
+
AuthConfig,
|
|
25
|
+
ServerType
|
|
26
|
+
} from '@sasjs/utils/types'
|
|
22
27
|
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
|
23
28
|
import { RequestClient } from './request/RequestClient'
|
|
24
29
|
import { prefixMessage } from '@sasjs/utils/error'
|
|
25
30
|
import { pollJobState } from './api/viya/pollJobState'
|
|
26
|
-
import { getTokens } from './auth/getTokens'
|
|
31
|
+
import { getTokens, OnTokensRefreshed } from './auth/getTokens'
|
|
27
32
|
import { uploadTables } from './api/viya/uploadTables'
|
|
28
33
|
import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
|
|
29
34
|
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
|
@@ -337,6 +342,7 @@ export class SASViyaApiClient {
|
|
|
337
342
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
|
338
343
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
|
339
344
|
* @param variables - an object that represents macro variables.
|
|
345
|
+
* @param onTokensRefreshed - optional callback invoked with the rotated token pair after a successful internal refresh, so consumers that persist tokens can store the new pair.
|
|
340
346
|
*/
|
|
341
347
|
public async executeScript(
|
|
342
348
|
jobPath: string,
|
|
@@ -349,7 +355,8 @@ export class SASViyaApiClient {
|
|
|
349
355
|
waitForResult = true,
|
|
350
356
|
pollOptions?: PollOptions,
|
|
351
357
|
printPid = false,
|
|
352
|
-
variables?: MacroVar
|
|
358
|
+
variables?: MacroVar,
|
|
359
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
353
360
|
): Promise<any> {
|
|
354
361
|
return executeOnComputeApi(
|
|
355
362
|
this.requestClient,
|
|
@@ -365,7 +372,8 @@ export class SASViyaApiClient {
|
|
|
365
372
|
waitForResult,
|
|
366
373
|
pollOptions,
|
|
367
374
|
printPid,
|
|
368
|
-
variables
|
|
375
|
+
variables,
|
|
376
|
+
onTokensRefreshed
|
|
369
377
|
)
|
|
370
378
|
}
|
|
371
379
|
|
|
@@ -864,11 +872,17 @@ export class SASViyaApiClient {
|
|
|
864
872
|
expectWebout = false,
|
|
865
873
|
pollOptions?: PollOptions,
|
|
866
874
|
printPid = false,
|
|
867
|
-
variables?: MacroVar
|
|
875
|
+
variables?: MacroVar,
|
|
876
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
868
877
|
) {
|
|
869
878
|
let access_token = (authConfig || {}).access_token
|
|
870
879
|
if (authConfig) {
|
|
871
|
-
;({ access_token } = await getTokens(
|
|
880
|
+
;({ access_token } = await getTokens(
|
|
881
|
+
this.requestClient,
|
|
882
|
+
authConfig,
|
|
883
|
+
ServerType.SasViya,
|
|
884
|
+
onTokensRefreshed
|
|
885
|
+
))
|
|
872
886
|
}
|
|
873
887
|
|
|
874
888
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
|
@@ -943,7 +957,8 @@ export class SASViyaApiClient {
|
|
|
943
957
|
waitForResult,
|
|
944
958
|
pollOptions,
|
|
945
959
|
printPid,
|
|
946
|
-
variables
|
|
960
|
+
variables,
|
|
961
|
+
onTokensRefreshed
|
|
947
962
|
)
|
|
948
963
|
}
|
|
949
964
|
|
|
@@ -954,18 +969,25 @@ export class SASViyaApiClient {
|
|
|
954
969
|
* @param debug - sets the _debug flag in the job arguments.
|
|
955
970
|
* @param data - any data to be passed in as input to the job.
|
|
956
971
|
* @param accessToken - an optional access token for an authorized user.
|
|
972
|
+
* @param onTokensRefreshed - optional callback invoked with the rotated token pair after a successful internal refresh, so consumers that persist tokens can store the new pair.
|
|
957
973
|
*/
|
|
958
974
|
public async executeJob(
|
|
959
975
|
sasJob: string,
|
|
960
976
|
contextName: string,
|
|
961
977
|
debug: boolean,
|
|
962
978
|
data?: any,
|
|
963
|
-
authConfig?: AuthConfig
|
|
979
|
+
authConfig?: AuthConfig,
|
|
980
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
964
981
|
): Promise<JobExecutionResult> {
|
|
965
982
|
let access_token = (authConfig || {}).access_token
|
|
966
983
|
|
|
967
984
|
if (authConfig) {
|
|
968
|
-
;({ access_token } = await getTokens(
|
|
985
|
+
;({ access_token } = await getTokens(
|
|
986
|
+
this.requestClient,
|
|
987
|
+
authConfig,
|
|
988
|
+
ServerType.SasViya,
|
|
989
|
+
onTokensRefreshed
|
|
990
|
+
))
|
|
969
991
|
}
|
|
970
992
|
|
|
971
993
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
|
@@ -1050,11 +1072,14 @@ export class SASViyaApiClient {
|
|
|
1050
1072
|
access_token
|
|
1051
1073
|
)
|
|
1052
1074
|
|
|
1053
|
-
const jobStatus = await this.pollJobState(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1075
|
+
const jobStatus = await this.pollJobState(
|
|
1076
|
+
postedJob,
|
|
1077
|
+
authConfig,
|
|
1078
|
+
undefined,
|
|
1079
|
+
onTokensRefreshed
|
|
1080
|
+
).catch((err) => {
|
|
1081
|
+
throw prefixMessage(err, 'Error while polling job status. ')
|
|
1082
|
+
})
|
|
1058
1083
|
|
|
1059
1084
|
const { result: currentJob } = await this.requestClient.get<Job>(
|
|
1060
1085
|
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
|
@@ -1138,14 +1163,17 @@ export class SASViyaApiClient {
|
|
|
1138
1163
|
private async pollJobState(
|
|
1139
1164
|
postedJob: Job,
|
|
1140
1165
|
authConfig?: AuthConfig,
|
|
1141
|
-
pollOptions?: PollOptions
|
|
1166
|
+
pollOptions?: PollOptions,
|
|
1167
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
1142
1168
|
) {
|
|
1143
1169
|
return pollJobState(
|
|
1144
1170
|
this.requestClient,
|
|
1145
1171
|
postedJob,
|
|
1146
1172
|
this.debug,
|
|
1147
1173
|
authConfig,
|
|
1148
|
-
pollOptions
|
|
1174
|
+
pollOptions,
|
|
1175
|
+
undefined,
|
|
1176
|
+
onTokensRefreshed
|
|
1149
1177
|
)
|
|
1150
1178
|
}
|
|
1151
1179
|
|
package/src/SASjs.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { SASViyaApiClient } from './SASViyaApiClient'
|
|
|
16
16
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
|
17
17
|
import { SASjsApiClient, SASjsAuthResponse } from './SASjsApiClient'
|
|
18
18
|
import { AuthManager } from './auth'
|
|
19
|
+
import { OnTokensRefreshed } from './auth/getTokens'
|
|
19
20
|
import {
|
|
20
21
|
ServerType,
|
|
21
22
|
MacroVar,
|
|
@@ -45,6 +46,7 @@ interface ExecuteScriptParams {
|
|
|
45
46
|
authConfig?: AuthConfig
|
|
46
47
|
authConfigSas9?: AuthConfigSas9
|
|
47
48
|
debug?: boolean
|
|
49
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
const defaultConfig: SASjsConfig = {
|
|
@@ -101,6 +103,7 @@ export default class SASjs {
|
|
|
101
103
|
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute scripts.
|
|
102
104
|
* @param authConfigSas9 - (required for server type sas9) a valid username and password that are authorised to execute scripts.
|
|
103
105
|
* @param debug - (optional) if true, global debug config will be overriden
|
|
106
|
+
* @param onTokensRefreshed - (optional, SAS Viya only) callback invoked with the rotated token pair after a successful internal refresh, so consumers that persist tokens can store the new pair.
|
|
104
107
|
*/
|
|
105
108
|
public async executeScript({
|
|
106
109
|
linesOfCode,
|
|
@@ -109,7 +112,8 @@ export default class SASjs {
|
|
|
109
112
|
runTime,
|
|
110
113
|
authConfig,
|
|
111
114
|
authConfigSas9,
|
|
112
|
-
debug
|
|
115
|
+
debug,
|
|
116
|
+
onTokensRefreshed
|
|
113
117
|
}: ExecuteScriptParams) {
|
|
114
118
|
this.isMethodSupported('executeScript', [
|
|
115
119
|
ServerType.Sas9,
|
|
@@ -157,7 +161,13 @@ export default class SASjs {
|
|
|
157
161
|
contextName,
|
|
158
162
|
authConfig,
|
|
159
163
|
null,
|
|
160
|
-
debug ? debug : this.sasjsConfig.debug
|
|
164
|
+
debug ? debug : this.sasjsConfig.debug,
|
|
165
|
+
false,
|
|
166
|
+
true,
|
|
167
|
+
undefined,
|
|
168
|
+
false,
|
|
169
|
+
undefined,
|
|
170
|
+
onTokensRefreshed
|
|
161
171
|
)
|
|
162
172
|
}
|
|
163
173
|
}
|
|
@@ -927,6 +937,7 @@ export default class SASjs {
|
|
|
927
937
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
|
928
938
|
* @param variables - an object that represents macro variables.
|
|
929
939
|
* @param verboseMode - boolean or a string equal to 'bleached' to enable verbose mode (log every HTTP response).
|
|
940
|
+
* @param onTokensRefreshed - (optional, SAS Viya only) callback invoked with the rotated token pair after a successful internal refresh, so consumers that persist tokens can store the new pair.
|
|
930
941
|
*/
|
|
931
942
|
public async startComputeJob(
|
|
932
943
|
sasJob: string,
|
|
@@ -937,7 +948,8 @@ export default class SASjs {
|
|
|
937
948
|
pollOptions?: PollOptions,
|
|
938
949
|
printPid = false,
|
|
939
950
|
variables?: MacroVar,
|
|
940
|
-
verboseMode?: VerboseMode
|
|
951
|
+
verboseMode?: VerboseMode,
|
|
952
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
941
953
|
) {
|
|
942
954
|
config = {
|
|
943
955
|
...this.sasjsConfig,
|
|
@@ -966,7 +978,8 @@ export default class SASjs {
|
|
|
966
978
|
false,
|
|
967
979
|
pollOptions,
|
|
968
980
|
printPid,
|
|
969
|
-
variables
|
|
981
|
+
variables,
|
|
982
|
+
onTokensRefreshed
|
|
970
983
|
)
|
|
971
984
|
}
|
|
972
985
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
|
2
|
-
import { AuthConfig, MacroVar } from '@sasjs/utils/types'
|
|
2
|
+
import { AuthConfig, MacroVar, ServerType } from '@sasjs/utils/types'
|
|
3
3
|
import { prefixMessage } from '@sasjs/utils/error'
|
|
4
4
|
import {
|
|
5
5
|
PollOptions,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ComputeJobExecutionError,
|
|
8
8
|
NotFoundError
|
|
9
9
|
} from '../..'
|
|
10
|
-
import { getTokens } from '../../auth/getTokens'
|
|
10
|
+
import { getTokens, OnTokensRefreshed } from '../../auth/getTokens'
|
|
11
11
|
import { RequestClient } from '../../request/RequestClient'
|
|
12
12
|
import { SessionManager } from '../../SessionManager'
|
|
13
13
|
import { isRelativePath, fetchLogByChunks } from '../../utils'
|
|
@@ -32,6 +32,7 @@ interface JobRequestBody {
|
|
|
32
32
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
|
33
33
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
|
34
34
|
* @param variables - an object that represents macro variables.
|
|
35
|
+
* @param onTokensRefreshed - optional callback invoked with the rotated token pair after a successful internal refresh, so consumers that persist tokens can store the new pair.
|
|
35
36
|
*/
|
|
36
37
|
export async function executeOnComputeApi(
|
|
37
38
|
requestClient: RequestClient,
|
|
@@ -47,12 +48,18 @@ export async function executeOnComputeApi(
|
|
|
47
48
|
waitForResult = true,
|
|
48
49
|
pollOptions?: PollOptions,
|
|
49
50
|
printPid = false,
|
|
50
|
-
variables?: MacroVar
|
|
51
|
+
variables?: MacroVar,
|
|
52
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
51
53
|
): Promise<any> {
|
|
52
54
|
let access_token = (authConfig || {}).access_token
|
|
53
55
|
|
|
54
56
|
if (authConfig) {
|
|
55
|
-
;({ access_token } = await getTokens(
|
|
57
|
+
;({ access_token } = await getTokens(
|
|
58
|
+
requestClient,
|
|
59
|
+
authConfig,
|
|
60
|
+
ServerType.SasViya,
|
|
61
|
+
onTokensRefreshed
|
|
62
|
+
))
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
const logger = process.logger || console
|
|
@@ -173,7 +180,8 @@ export async function executeOnComputeApi(
|
|
|
173
180
|
{
|
|
174
181
|
session,
|
|
175
182
|
sessionManager
|
|
176
|
-
}
|
|
183
|
+
},
|
|
184
|
+
onTokensRefreshed
|
|
177
185
|
).catch(async (err) => {
|
|
178
186
|
const error = err?.response?.data
|
|
179
187
|
const result = /err=[0-9]*,/.exec(error)
|
|
@@ -196,7 +204,12 @@ export async function executeOnComputeApi(
|
|
|
196
204
|
})
|
|
197
205
|
|
|
198
206
|
if (authConfig) {
|
|
199
|
-
;({ access_token } = await getTokens(
|
|
207
|
+
;({ access_token } = await getTokens(
|
|
208
|
+
requestClient,
|
|
209
|
+
authConfig,
|
|
210
|
+
ServerType.SasViya,
|
|
211
|
+
onTokensRefreshed
|
|
212
|
+
))
|
|
200
213
|
}
|
|
201
214
|
|
|
202
215
|
const { result: currentJob } = await requestClient
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthConfig } from '@sasjs/utils/types'
|
|
1
|
+
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
|
2
2
|
import { Job, PollOptions, PollStrategy } from '../..'
|
|
3
|
-
import { getTokens } from '../../auth/getTokens'
|
|
3
|
+
import { getTokens, OnTokensRefreshed } from '../../auth/getTokens'
|
|
4
4
|
import { RequestClient } from '../../request/RequestClient'
|
|
5
5
|
import { JobStatePollError } from '../../types/errors'
|
|
6
6
|
import { Link, WriteStream, SessionState, JobSessionManager } from '../../types'
|
|
@@ -46,7 +46,8 @@ export async function pollJobState(
|
|
|
46
46
|
debug: boolean,
|
|
47
47
|
authConfig?: AuthConfig,
|
|
48
48
|
pollOptions?: PollOptions,
|
|
49
|
-
jobSessionManager?: JobSessionManager
|
|
49
|
+
jobSessionManager?: JobSessionManager,
|
|
50
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
50
51
|
): Promise<JobState> {
|
|
51
52
|
const logger = process.logger || console
|
|
52
53
|
|
|
@@ -130,7 +131,8 @@ export async function pollJobState(
|
|
|
130
131
|
authConfig,
|
|
131
132
|
streamLog,
|
|
132
133
|
logFileStream,
|
|
133
|
-
jobSessionManager
|
|
134
|
+
jobSessionManager,
|
|
135
|
+
onTokensRefreshed
|
|
134
136
|
)
|
|
135
137
|
|
|
136
138
|
currentState = result.state
|
|
@@ -162,7 +164,8 @@ export async function pollJobState(
|
|
|
162
164
|
authConfig,
|
|
163
165
|
streamLog,
|
|
164
166
|
logFileStream,
|
|
165
|
-
jobSessionManager
|
|
167
|
+
jobSessionManager,
|
|
168
|
+
onTokensRefreshed
|
|
166
169
|
)
|
|
167
170
|
|
|
168
171
|
currentState = result.state
|
|
@@ -179,14 +182,21 @@ const getJobState = async (
|
|
|
179
182
|
job: Job,
|
|
180
183
|
currentState: string,
|
|
181
184
|
debug: boolean,
|
|
182
|
-
authConfig?: AuthConfig
|
|
185
|
+
authConfig?: AuthConfig,
|
|
186
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
183
187
|
): Promise<JobState> => {
|
|
184
188
|
const stateLink = job.links.find((l: any) => l.rel === 'state')!
|
|
185
189
|
|
|
186
190
|
if (needsRetry(currentState)) {
|
|
187
191
|
let tokens
|
|
188
192
|
|
|
189
|
-
if (authConfig)
|
|
193
|
+
if (authConfig)
|
|
194
|
+
tokens = await getTokens(
|
|
195
|
+
requestClient,
|
|
196
|
+
authConfig,
|
|
197
|
+
ServerType.SasViya,
|
|
198
|
+
onTokensRefreshed
|
|
199
|
+
)
|
|
190
200
|
|
|
191
201
|
const { result: jobState } = await requestClient
|
|
192
202
|
.get<string>(
|
|
@@ -236,7 +246,8 @@ export const doPoll = async (
|
|
|
236
246
|
authConfig?: AuthConfig,
|
|
237
247
|
streamLog?: boolean,
|
|
238
248
|
logStream?: WriteStream,
|
|
239
|
-
jobSessionManager?: JobSessionManager
|
|
249
|
+
jobSessionManager?: JobSessionManager,
|
|
250
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
240
251
|
): Promise<{ state: JobState; pollCount: number }> => {
|
|
241
252
|
const { maxPollCount, pollInterval } = pollOptions
|
|
242
253
|
const logger = process.logger || console
|
|
@@ -287,7 +298,8 @@ export const doPoll = async (
|
|
|
287
298
|
postedJob,
|
|
288
299
|
state,
|
|
289
300
|
debug,
|
|
290
|
-
authConfig
|
|
301
|
+
authConfig,
|
|
302
|
+
onTokensRefreshed
|
|
291
303
|
).catch((err) => {
|
|
292
304
|
errorCount++
|
|
293
305
|
|
package/src/auth/getTokens.ts
CHANGED
|
@@ -8,26 +8,76 @@ import { RequestClient } from '../request/RequestClient'
|
|
|
8
8
|
import { refreshTokensForViya } from './refreshTokensForViya'
|
|
9
9
|
import { refreshTokensForSasjs } from './refreshTokensForSasjs'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Callback invoked with the rotated token pair after a successful internal
|
|
13
|
+
* refresh. Viya refresh tokens are single-use/rotating, so consumers that
|
|
14
|
+
* persist tokens (e.g. the CLI writing `.env.{target}`) should pass a handler
|
|
15
|
+
* wherever this is accepted to avoid the persisted pair going stale.
|
|
16
|
+
*/
|
|
17
|
+
export type OnTokensRefreshed = (tokens: {
|
|
18
|
+
access_token: string
|
|
19
|
+
refresh_token: string
|
|
20
|
+
}) => void | Promise<void>
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* Returns the auth configuration, refreshing the tokens if necessary.
|
|
13
24
|
* This function can only be used by Node, if a server type is SASVIYA.
|
|
14
25
|
* @param requestClient - the pre-configured HTTP request client
|
|
15
26
|
* @param authConfig - an object containing a client ID, secret, access token and refresh token
|
|
16
27
|
* @param serverType - server type for which refreshing the tokens, defaults to SASVIYA
|
|
28
|
+
* @param onTokensRefreshed - optional callback invoked with the rotated token
|
|
29
|
+
* pair after a successful internal refresh.
|
|
17
30
|
*/
|
|
18
31
|
export async function getTokens(
|
|
19
32
|
requestClient: RequestClient,
|
|
20
33
|
authConfig: AuthConfig,
|
|
21
|
-
serverType: ServerType = ServerType.SasViya
|
|
34
|
+
serverType: ServerType = ServerType.SasViya,
|
|
35
|
+
onTokensRefreshed?: OnTokensRefreshed
|
|
22
36
|
): Promise<AuthConfig> {
|
|
23
37
|
const logger = process.logger || console
|
|
24
|
-
|
|
38
|
+
// Note: client/secret are NOT mutated. The sas.cli public-client fallback
|
|
39
|
+
// is applied only to the refresh call itself, so a caller that round-trips
|
|
40
|
+
// the returned AuthConfig into persistent storage never ends up saving
|
|
41
|
+
// fabricated CLIENT/SECRET values.
|
|
42
|
+
let { access_token, refresh_token } = authConfig
|
|
43
|
+
const { client, secret } = authConfig
|
|
44
|
+
|
|
45
|
+
// Tokens are not always decodable JWTs - some servers issue opaque tokens.
|
|
46
|
+
// jwt-decode throws InvalidTokenError for these; only in that case is the
|
|
47
|
+
// token treated as still valid (the server is the authority on expiry and
|
|
48
|
+
// will reject a genuinely expired token on use/refresh).
|
|
49
|
+
// Use a 300 s (5 min) safety margin instead of the isAccessTokenExpiring
|
|
50
|
+
// default of 3600 s (1 h). Some Viya estates issue access tokens with a
|
|
51
|
+
// 1-hour TTL; with the 3600 s default a brand-new 1 h token is immediately
|
|
52
|
+
// considered "expiring", causing a redundant refresh even when the CLI has
|
|
53
|
+
// already refreshed the token moments earlier. 300 s is short enough that a
|
|
54
|
+
// fresh 1 h token (TTL ≈ 3600 ≫ 300) is NOT considered expiring, yet long
|
|
55
|
+
// enough to let a single API call complete before the token actually expires.
|
|
56
|
+
// Long-running jobs are protected because pollJobState calls getTokens on
|
|
57
|
+
// every poll, so a token that does expire mid-job is refreshed in-place.
|
|
58
|
+
let isAccessTokenExpiringSoon = false
|
|
59
|
+
try {
|
|
60
|
+
isAccessTokenExpiringSoon = isAccessTokenExpiring(access_token, 300)
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if ((e as Error).name !== 'InvalidTokenError') throw e
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let isRefreshTokenExpired = false
|
|
66
|
+
try {
|
|
67
|
+
isRefreshTokenExpired = hasTokenExpired(refresh_token)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if ((e as Error).name !== 'InvalidTokenError') throw e
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let isRefreshTokenExpiringSoon = false
|
|
73
|
+
try {
|
|
74
|
+
isRefreshTokenExpiringSoon = isRefreshTokenExpiring(refresh_token)
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if ((e as Error).name !== 'InvalidTokenError') throw e
|
|
77
|
+
}
|
|
25
78
|
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
isRefreshTokenExpiring(refresh_token)
|
|
29
|
-
) {
|
|
30
|
-
if (hasTokenExpired(refresh_token)) {
|
|
79
|
+
if (isAccessTokenExpiringSoon || isRefreshTokenExpiringSoon) {
|
|
80
|
+
if (isRefreshTokenExpired) {
|
|
31
81
|
const error =
|
|
32
82
|
'Unable to obtain new access token. Your refresh token has expired.'
|
|
33
83
|
|
|
@@ -38,16 +88,39 @@ export async function getTokens(
|
|
|
38
88
|
|
|
39
89
|
logger.info('Refreshing access and refresh tokens.')
|
|
40
90
|
|
|
91
|
+
// Tokens minted without a registered OAuth client (e.g. via the
|
|
92
|
+
// password grant against the built-in public client) can still be
|
|
93
|
+
// refreshed using that same secret-less client. A configured client
|
|
94
|
+
// with a missing secret is treated as a public client (empty secret).
|
|
41
95
|
const tokens =
|
|
42
96
|
serverType === ServerType.SasViya
|
|
43
97
|
? await refreshTokensForViya(
|
|
44
98
|
requestClient,
|
|
45
|
-
client,
|
|
46
|
-
secret,
|
|
99
|
+
client || 'sas.cli',
|
|
100
|
+
secret || '',
|
|
47
101
|
refresh_token
|
|
48
102
|
)
|
|
49
103
|
: await refreshTokensForSasjs(requestClient, refresh_token)
|
|
50
104
|
;({ access_token, refresh_token } = tokens)
|
|
105
|
+
|
|
106
|
+
// Viya refresh tokens are single-use/rotating. Callers throughout the
|
|
107
|
+
// compute-execution chain (executeOnComputeApi, pollJobState's poll
|
|
108
|
+
// loop, executeComputeJob -> executeJob, etc.) share this same
|
|
109
|
+
// authConfig object by reference and call getTokens again later in the
|
|
110
|
+
// same execution - update it in place so a later call in the same chain
|
|
111
|
+
// refreshes with the token that was just issued, not the one that was
|
|
112
|
+
// just consumed. client/secret are deliberately left untouched (see
|
|
113
|
+
// note above).
|
|
114
|
+
authConfig.access_token = access_token
|
|
115
|
+
authConfig.refresh_token = refresh_token
|
|
116
|
+
|
|
117
|
+
// A failing persistence handler must not turn a successful refresh into
|
|
118
|
+
// an auth failure - log the error and return the fresh pair regardless.
|
|
119
|
+
try {
|
|
120
|
+
await onTokensRefreshed?.({ access_token, refresh_token })
|
|
121
|
+
} catch (e) {
|
|
122
|
+
logger.error('onTokensRefreshed handler failed:', e)
|
|
123
|
+
}
|
|
51
124
|
}
|
|
52
125
|
|
|
53
126
|
return { access_token, refresh_token, client, secret }
|