tm1npm 1.5.3 → 2.0.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/lib/index.d.ts +1 -1
  3. package/lib/index.d.ts.map +1 -1
  4. package/lib/services/ApplicationService.d.ts +19 -3
  5. package/lib/services/ApplicationService.d.ts.map +1 -1
  6. package/lib/services/ApplicationService.js +232 -6
  7. package/lib/services/AsyncOperationService.d.ts +8 -1
  8. package/lib/services/AsyncOperationService.d.ts.map +1 -1
  9. package/lib/services/AsyncOperationService.js +69 -26
  10. package/lib/services/ElementService.d.ts +67 -1
  11. package/lib/services/ElementService.d.ts.map +1 -1
  12. package/lib/services/ElementService.js +214 -0
  13. package/lib/services/FileService.d.ts.map +1 -1
  14. package/lib/services/HierarchyService.d.ts +26 -0
  15. package/lib/services/HierarchyService.d.ts.map +1 -1
  16. package/lib/services/HierarchyService.js +306 -0
  17. package/lib/services/ProcessService.d.ts +40 -22
  18. package/lib/services/ProcessService.d.ts.map +1 -1
  19. package/lib/services/ProcessService.js +118 -111
  20. package/lib/services/RestService.d.ts +213 -25
  21. package/lib/services/RestService.d.ts.map +1 -1
  22. package/lib/services/RestService.js +841 -263
  23. package/lib/services/SubsetService.d.ts +2 -0
  24. package/lib/services/SubsetService.d.ts.map +1 -1
  25. package/lib/services/SubsetService.js +33 -0
  26. package/lib/services/TM1Service.d.ts +44 -1
  27. package/lib/services/TM1Service.d.ts.map +1 -1
  28. package/lib/services/TM1Service.js +96 -4
  29. package/lib/services/index.d.ts +1 -1
  30. package/lib/services/index.d.ts.map +1 -1
  31. package/lib/tests/100PercentParityCheck.test.js +23 -6
  32. package/lib/tests/applicationService.issue38.test.d.ts +5 -0
  33. package/lib/tests/applicationService.issue38.test.d.ts.map +1 -0
  34. package/lib/tests/applicationService.issue38.test.js +237 -0
  35. package/lib/tests/asyncOperationService.test.js +51 -45
  36. package/lib/tests/bugfix28.test.js +12 -4
  37. package/lib/tests/elementService.issue37.test.d.ts +5 -0
  38. package/lib/tests/elementService.issue37.test.d.ts.map +1 -0
  39. package/lib/tests/elementService.issue37.test.js +413 -0
  40. package/lib/tests/elementService.issue38.test.d.ts +5 -0
  41. package/lib/tests/elementService.issue38.test.d.ts.map +1 -0
  42. package/lib/tests/elementService.issue38.test.js +79 -0
  43. package/lib/tests/hierarchyService.issue38.test.d.ts +5 -0
  44. package/lib/tests/hierarchyService.issue38.test.d.ts.map +1 -0
  45. package/lib/tests/hierarchyService.issue38.test.js +460 -0
  46. package/lib/tests/processService.comprehensive.test.js +9 -9
  47. package/lib/tests/processService.test.js +234 -0
  48. package/lib/tests/restService.test.d.ts +0 -4
  49. package/lib/tests/restService.test.d.ts.map +1 -1
  50. package/lib/tests/restService.test.js +1558 -143
  51. package/lib/tests/subsetService.issue38.test.d.ts +5 -0
  52. package/lib/tests/subsetService.issue38.test.d.ts.map +1 -0
  53. package/lib/tests/subsetService.issue38.test.js +113 -0
  54. package/lib/tests/tm1Service.test.js +80 -8
  55. package/package.json +1 -1
  56. package/src/index.ts +1 -1
  57. package/src/services/ApplicationService.ts +282 -10
  58. package/src/services/AsyncOperationService.ts +76 -29
  59. package/src/services/ElementService.ts +322 -1
  60. package/src/services/FileService.ts +3 -3
  61. package/src/services/HierarchyService.ts +419 -1
  62. package/src/services/ProcessService.ts +185 -142
  63. package/src/services/RestService.ts +1021 -267
  64. package/src/services/SubsetService.ts +48 -0
  65. package/src/services/TM1Service.ts +127 -6
  66. package/src/services/index.ts +1 -1
  67. package/src/tests/100PercentParityCheck.test.ts +29 -8
  68. package/src/tests/applicationService.issue38.test.ts +293 -0
  69. package/src/tests/asyncOperationService.test.ts +52 -48
  70. package/src/tests/bugfix28.test.ts +12 -4
  71. package/src/tests/elementService.issue37.test.ts +571 -0
  72. package/src/tests/elementService.issue38.test.ts +103 -0
  73. package/src/tests/hierarchyService.issue38.test.ts +599 -0
  74. package/src/tests/processService.comprehensive.test.ts +10 -10
  75. package/src/tests/processService.test.ts +295 -3
  76. package/src/tests/restService.test.ts +1844 -139
  77. package/src/tests/subsetService.issue38.test.ts +182 -0
  78. package/src/tests/tm1Service.test.ts +95 -11
@@ -1,11 +1,48 @@
1
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 () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.RestService = exports.AuthenticationMode = void 0;
7
40
  const axios_1 = __importDefault(require("axios"));
41
+ const https = __importStar(require("https"));
42
+ const fs = __importStar(require("fs"));
8
43
  const TM1Exception_1 = require("../exceptions/TM1Exception");
44
+ const Utils_1 = require("../utils/Utils");
45
+ const PRODUCT_VERSION_AUTH_SUFFIX = '/Configuration/ProductVersion/$value';
9
46
  var AuthenticationMode;
10
47
  (function (AuthenticationMode) {
11
48
  AuthenticationMode[AuthenticationMode["BASIC"] = 1] = "BASIC";
@@ -22,37 +59,262 @@ class RestService {
22
59
  get version() {
23
60
  return this._serverVersion;
24
61
  }
62
+ // Sync accessors for cached role flags. Return false before the first
63
+ // is_*_admin() call resolves — callers that need the real value must
64
+ // await is_admin() / is_data_admin() / etc. first.
65
+ get isAdmin() { var _a; return (_a = this._isAdmin) !== null && _a !== void 0 ? _a : false; }
66
+ get isDataAdmin() { var _a; return (_a = this._isDataAdmin) !== null && _a !== void 0 ? _a : false; }
67
+ get isSecurityAdmin() { var _a; return (_a = this._isSecurityAdmin) !== null && _a !== void 0 ? _a : false; }
68
+ get isOpsAdmin() { var _a; return (_a = this._isOpsAdmin) !== null && _a !== void 0 ? _a : false; }
25
69
  constructor(config) {
70
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
71
+ this.sessionCookies = new Map();
26
72
  this.isConnected = false;
27
73
  this.config = { ...config };
74
+ this._asyncRequestsMode = (_a = config.asyncRequestsMode) !== null && _a !== void 0 ? _a : false;
75
+ this._cancelAtTimeout = (_b = config.cancelAtTimeout) !== null && _b !== void 0 ? _b : false;
76
+ this._timeout = (_c = config.timeout) !== null && _c !== void 0 ? _c : 60;
77
+ this._asyncPollingInitialDelay = (_d = config.asyncPollingInitialDelay) !== null && _d !== void 0 ? _d : 0.1;
78
+ this._asyncPollingMaxDelay = (_e = config.asyncPollingMaxDelay) !== null && _e !== void 0 ? _e : 1.0;
79
+ this._asyncPollingBackoffFactor = (_f = config.asyncPollingBackoffFactor) !== null && _f !== void 0 ? _f : 2.0;
80
+ this._reConnectOnSessionTimeout = (_g = config.reConnectOnSessionTimeout) !== null && _g !== void 0 ? _g : true;
81
+ this._reConnectOnRemoteDisconnect = (_h = config.reConnectOnRemoteDisconnect) !== null && _h !== void 0 ? _h : true;
82
+ this._remoteDisconnectMaxRetries = (_j = config.remoteDisconnectMaxRetries) !== null && _j !== void 0 ? _j : 5;
83
+ this._remoteDisconnectRetryDelay = (_k = config.remoteDisconnectRetryDelay) !== null && _k !== void 0 ? _k : 1;
84
+ this._remoteDisconnectMaxDelay = (_l = config.remoteDisconnectMaxDelay) !== null && _l !== void 0 ? _l : 30;
85
+ // Pre-populate admin flags for the built-in ADMIN user (mirrors tm1py)
86
+ if (config.user && (0, Utils_1.caseAndSpaceInsensitiveEquals)(config.user, 'ADMIN')) {
87
+ this._isAdmin = true;
88
+ this._isDataAdmin = true;
89
+ this._isSecurityAdmin = true;
90
+ this._isOpsAdmin = true;
91
+ }
28
92
  this.setupAxiosInstance();
93
+ if (this.config.sessionId) {
94
+ // Mirror tm1py's _set_session_id_cookie: v12 topologies use paSession,
95
+ // v11 and baseUrl overrides use TM1SessionId.
96
+ const topo = this.determineTopology();
97
+ const cookieName = (topo === 'ibm_cloud' || topo === 'pa_proxy' || topo === 's2s')
98
+ ? 'paSession'
99
+ : 'TM1SessionId';
100
+ this.sessionCookies.set(cookieName, this.config.sessionId);
101
+ }
102
+ }
103
+ getSessionCookieValue() {
104
+ for (const name of RestService.SESSION_COOKIE_NAMES) {
105
+ const value = this.sessionCookies.get(name);
106
+ if (value)
107
+ return value;
108
+ }
109
+ return undefined;
110
+ }
111
+ buildCookieHeader() {
112
+ if (this.sessionCookies.size === 0)
113
+ return undefined;
114
+ const parts = [];
115
+ for (const [name, value] of this.sessionCookies) {
116
+ parts.push(`${name}=${value}`);
117
+ }
118
+ return parts.join('; ');
119
+ }
120
+ parseSetCookieHeaders(setCookie) {
121
+ if (!setCookie)
122
+ return;
123
+ const list = RestService.normaliseSetCookie(setCookie);
124
+ for (const raw of list) {
125
+ const firstSegment = raw.split(';')[0];
126
+ const eqIdx = firstSegment.indexOf('=');
127
+ if (eqIdx <= 0)
128
+ continue;
129
+ // Strip CR/LF/NUL defensively to block header-injection via compromised response
130
+ const sanitize = (s) => s.replace(/[\r\n\0]/g, '').trim();
131
+ const name = sanitize(firstSegment.slice(0, eqIdx));
132
+ const value = sanitize(firstSegment.slice(eqIdx + 1));
133
+ if (!RestService.SESSION_COOKIE_NAMES.includes(name))
134
+ continue;
135
+ if (value === '') {
136
+ this.sessionCookies.delete(name);
137
+ }
138
+ else {
139
+ this.sessionCookies.set(name, value);
140
+ }
141
+ }
142
+ }
143
+ removeAuthorizationHeader() {
144
+ delete this.axiosInstance.defaults.headers.common['Authorization'];
145
+ }
146
+ deleteHeaderCaseInsensitive(headers, name) {
147
+ if (!headers)
148
+ return;
149
+ // axios 1.x may supply an AxiosHeaders instance with case-insensitive lookup; plain objects
150
+ // (common in retry paths and test mocks) are case-sensitive and require explicit iteration
151
+ const target = name.toLowerCase();
152
+ for (const key of Object.keys(headers)) {
153
+ if (key.toLowerCase() === target) {
154
+ delete headers[key];
155
+ }
156
+ }
29
157
  }
30
158
  setupAxiosInstance() {
31
159
  const baseURL = this.buildBaseUrl();
32
- this.axiosInstance = axios_1.default.create({
160
+ const axiosConfig = {
33
161
  baseURL,
34
- timeout: (this.config.timeout || 60) * 1000,
162
+ timeout: this._timeout * 1000,
35
163
  headers: {
36
164
  ...RestService.HEADERS,
37
165
  ...(this.config.sessionContext && { 'TM1-SessionContext': this.config.sessionContext })
38
166
  }
39
- });
167
+ };
168
+ if (this.config.proxies) {
169
+ const proxyUrl = this.config.proxies.https || this.config.proxies.http;
170
+ if (proxyUrl) {
171
+ const parsed = new URL(proxyUrl);
172
+ axiosConfig.proxy = {
173
+ host: parsed.hostname,
174
+ port: parsed.port
175
+ ? parseInt(parsed.port, 10)
176
+ : (parsed.protocol === 'https:' ? 443 : 80),
177
+ protocol: parsed.protocol.replace(':', ''),
178
+ ...(parsed.username && {
179
+ auth: {
180
+ username: decodeURIComponent(parsed.username),
181
+ password: decodeURIComponent(parsed.password)
182
+ }
183
+ })
184
+ };
185
+ }
186
+ }
187
+ if (this.config.sslContext) {
188
+ axiosConfig.httpsAgent = this.config.sslContext;
189
+ }
190
+ else if (this.config.cert) {
191
+ const [certPath, keyPath] = Array.isArray(this.config.cert)
192
+ ? this.config.cert
193
+ : [this.config.cert, undefined];
194
+ axiosConfig.httpsAgent = new https.Agent({
195
+ cert: fs.readFileSync(certPath),
196
+ key: keyPath ? fs.readFileSync(keyPath) : undefined
197
+ });
198
+ }
199
+ this.axiosInstance = axios_1.default.create(axiosConfig);
40
200
  this.setupInterceptors();
41
201
  }
42
202
  buildBaseUrl() {
43
- if (this.config.baseUrl) {
44
- return this.config.baseUrl;
203
+ return this.resolveRoots().serviceRoot;
204
+ }
205
+ /**
206
+ * Pick the deployment topology based on the provided config, mirroring
207
+ * tm1py's _determine_auth_mode + _construct_service_and_auth_root dispatch.
208
+ *
209
+ * Note: authUrl is intentionally excluded from the v12 signal set because
210
+ * tm1npm historically uses authUrl for CAM SSO (unlike tm1py, where auth_url
211
+ * is a v12-only field). apiKey is also excluded to avoid collision with the
212
+ * existing BASIC_API_KEY auth flow.
213
+ */
214
+ determineTopology() {
215
+ const c = this.config;
216
+ const hasV12Signal = !!(c.instance || c.database || c.iamUrl || c.paUrl || c.tenant);
217
+ // tm1py's _construct_service_and_auth_root routes v12 modes (IBM Cloud / PA
218
+ // Proxy / S2S) through their dedicated constructors even if base_url is
219
+ // supplied. Only non-v12 configs fall through to the base_url override.
220
+ if (!hasV12Signal)
221
+ return c.baseUrl ? 'base_url' : 'v11';
222
+ if (c.iamUrl)
223
+ return 'ibm_cloud';
224
+ if (c.address && c.user && !c.instance)
225
+ return 'pa_proxy';
226
+ return 's2s';
227
+ }
228
+ /**
229
+ * Resolve the TM1 service root and auth root URLs for the configured topology.
230
+ * Mirrors tm1py's _construct_service_and_auth_root return tuple.
231
+ */
232
+ resolveRoots() {
233
+ switch (this.determineTopology()) {
234
+ case 'base_url': return this.rootsFromBaseUrl();
235
+ case 'ibm_cloud': return this.rootsIbmCloud();
236
+ case 'pa_proxy': return this.rootsPaProxy();
237
+ case 's2s': return this.rootsS2s();
238
+ case 'v11':
239
+ default: return this.rootsV11();
45
240
  }
241
+ }
242
+ rootsV11() {
243
+ var _a;
46
244
  const protocol = this.config.ssl ? 'https' : 'http';
47
245
  const address = this.config.address || 'localhost';
48
- const port = this.config.port || 8001;
49
- return `${protocol}://${address}:${port}/api/v1`;
246
+ const port = (_a = this.config.port) !== null && _a !== void 0 ? _a : 8001;
247
+ const serviceRoot = `${protocol}://${address}:${port}/api/v1`;
248
+ return { serviceRoot, authRoot: serviceRoot + PRODUCT_VERSION_AUTH_SUFFIX };
249
+ }
250
+ rootsIbmCloud() {
251
+ const { address, tenant, database, ssl } = this.config;
252
+ if (!address || !tenant || !database) {
253
+ throw new Error("'address', 'tenant' and 'database' must be provided to connect to TM1 > v12 in IBM Cloud");
254
+ }
255
+ if (!ssl) {
256
+ throw new Error("'ssl' must be true to connect to TM1 > v12 in IBM Cloud");
257
+ }
258
+ const t = encodeURIComponent(tenant);
259
+ const d = encodeURIComponent(database);
260
+ const serviceRoot = `https://${address}/api/${t}/v0/tm1/${d}`;
261
+ return { serviceRoot, authRoot: serviceRoot + PRODUCT_VERSION_AUTH_SUFFIX };
262
+ }
263
+ rootsPaProxy() {
264
+ const { address, database, ssl } = this.config;
265
+ if (!address || !database) {
266
+ throw new Error("'address' and 'database' must be provided to connect to TM1 > v12 using PA Proxy");
267
+ }
268
+ const protocol = ssl ? 'https' : 'http';
269
+ const d = encodeURIComponent(database);
270
+ const serviceRoot = `${protocol}://${address}/tm1/${d}/api/v1`;
271
+ const authRoot = `${protocol}://${address}/login`;
272
+ return { serviceRoot, authRoot };
273
+ }
274
+ rootsS2s() {
275
+ const { instance, database, ssl, port } = this.config;
276
+ if (!instance || !database) {
277
+ throw new Error("'instance' and 'database' arguments are required for v12 authentication with 'address'");
278
+ }
279
+ const protocol = ssl ? 'https' : 'http';
280
+ const address = this.config.address && this.config.address.length > 0
281
+ ? this.config.address
282
+ : 'localhost';
283
+ const portPart = port != null ? `:${port}` : '';
284
+ const i = encodeURIComponent(instance);
285
+ const d = encodeURIComponent(database);
286
+ const serviceRoot = `${protocol}://${address}${portPart}/${i}/api/v1/Databases('${d}')`;
287
+ const authRoot = `${protocol}://${address}${portPart}/${i}/auth/v1/session`;
288
+ return { serviceRoot, authRoot };
289
+ }
290
+ rootsFromBaseUrl() {
291
+ const base = this.config.baseUrl;
292
+ if (this.config.address) {
293
+ throw new Error("Base URL and Address cannot be specified at the same time");
294
+ }
295
+ if (/api\/v1\/Databases/.test(base)) {
296
+ if (!this.config.authUrl) {
297
+ throw new Error("Auth_url missing — when connecting to planning analytics engine using base_url, you must specify a corresponding auth_url");
298
+ }
299
+ return { serviceRoot: base, authRoot: this.config.authUrl };
300
+ }
301
+ // Recognize baseUrl shapes documented in docs/connection-guide.md
302
+ // (TM1 11 IBM Cloud `/tm1/api/tm1`, TM1 12 PaaS/access-token `/v0/tm1/...`)
303
+ // and use them verbatim. Only fall through to /api/v1 suffixing when the
304
+ // URL clearly lacks any TM1 API path — matching tm1py's fallback.
305
+ const trimmed = base.replace(/\/+$/, '');
306
+ // Each alternative is $-anchored (after trailing-slash trim above) so the
307
+ // match intent is explicit: the URL already ends in a TM1 API suffix.
308
+ const hasApiSuffix = /\/api\/v1$|\/v0\/tm1\/[^/]+$|\/tm1\/api\/tm1$/.test(trimmed);
309
+ const serviceRoot = hasApiSuffix ? trimmed : `${trimmed}/api/v1`;
310
+ return { serviceRoot, authRoot: serviceRoot + PRODUCT_VERSION_AUTH_SUFFIX };
50
311
  }
51
312
  setupInterceptors() {
52
313
  // Request interceptor
53
314
  this.axiosInstance.interceptors.request.use((config) => {
54
- if (this.sessionId) {
55
- config.headers['TM1SessionId'] = this.sessionId;
315
+ const cookieHeader = this.buildCookieHeader();
316
+ if (cookieHeader) {
317
+ config.headers['Cookie'] = cookieHeader;
56
318
  }
57
319
  if (this.sandboxName) {
58
320
  config.headers['TM1-Sandbox'] = this.sandboxName;
@@ -60,24 +322,30 @@ class RestService {
60
322
  return config;
61
323
  }, (error) => Promise.reject(error));
62
324
  // Response interceptor with retry logic
63
- this.axiosInstance.interceptors.response.use((response) => response, async (error) => {
325
+ this.axiosInstance.interceptors.response.use((response) => {
64
326
  var _a;
327
+ this.parseSetCookieHeaders((_a = response.headers) === null || _a === void 0 ? void 0 : _a['set-cookie']);
328
+ return response;
329
+ }, async (error) => {
330
+ var _a, _b, _c, _d;
331
+ if (error.response) {
332
+ this.parseSetCookieHeaders((_a = error.response.headers) === null || _a === void 0 ? void 0 : _a['set-cookie']);
333
+ }
65
334
  const originalRequest = error.config;
66
335
  // Handle timeout errors
67
- if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
336
+ if (error.code === 'ECONNABORTED' || ((_c = (_b = error.message) === null || _b === void 0 ? void 0 : _b.includes) === null || _c === void 0 ? void 0 : _c.call(_b, 'timeout'))) {
68
337
  throw new TM1Exception_1.TM1TimeoutException(`Request timeout: ${error.message}`);
69
338
  }
70
- // Handle authentication errors with retry
71
- if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 && !originalRequest._retry) {
339
+ // Handle authentication errors with retry. Guarded by this.isConnected so a 401
340
+ // during disconnect()'s tm1.Close POST cannot recurse back into reAuthenticate().
341
+ if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.status) === 401 && originalRequest && !originalRequest._retry
342
+ && this.isConnected && this._reConnectOnSessionTimeout) {
72
343
  originalRequest._retry = true;
73
344
  try {
74
- // Attempt re-authentication
75
345
  await this.reAuthenticate();
76
- // Update the request with new session/token
77
- if (this.sessionId) {
78
- originalRequest.headers['TM1SessionId'] = this.sessionId;
79
- }
80
- // Retry the original request
346
+ // Stale values would defeat the rebuild by the request interceptor on replay
347
+ this.deleteHeaderCaseInsensitive(originalRequest.headers, 'Cookie');
348
+ this.deleteHeaderCaseInsensitive(originalRequest.headers, 'Authorization');
81
349
  return this.axiosInstance(originalRequest);
82
350
  }
83
351
  catch (reAuthError) {
@@ -86,13 +354,13 @@ class RestService {
86
354
  }
87
355
  }
88
356
  // Handle connection errors with retry
89
- if (this.shouldRetryRequest(error) && this.canRetryRequest(originalRequest)) {
357
+ if (originalRequest && this.shouldRetryRequest(error) && this.canRetryRequest(originalRequest)) {
90
358
  return this.retryRequest(originalRequest);
91
359
  }
92
360
  const response = error.response;
93
361
  if (response) {
94
362
  const message = this.extractErrorMessage(response);
95
- throw new TM1Exception_1.TM1RestException(message, response);
363
+ throw new TM1Exception_1.TM1RestException(message, response.status, response);
96
364
  }
97
365
  throw new TM1Exception_1.TM1RestException(error.message);
98
366
  });
@@ -101,6 +369,8 @@ class RestService {
101
369
  * Determine if a request should be retried
102
370
  */
103
371
  shouldRetryRequest(error) {
372
+ if (!this._reConnectOnRemoteDisconnect)
373
+ return false;
104
374
  // Retry on network errors, timeouts, and 5xx server errors
105
375
  return !error.response ||
106
376
  error.code === 'ECONNRESET' ||
@@ -112,18 +382,40 @@ class RestService {
112
382
  * Check if a request can be retried
113
383
  */
114
384
  canRetryRequest(config) {
385
+ if (config._idempotent === false) {
386
+ return false;
387
+ }
115
388
  // Don't retry if already retried maximum times
116
389
  config._retryCount = config._retryCount || 0;
117
- return config._retryCount < 3;
390
+ return config._retryCount < this._remoteDisconnectMaxRetries;
118
391
  }
119
392
  /**
120
- * Retry a failed request with exponential backoff
393
+ * Retry a failed request with exponential backoff, reconnecting the
394
+ * session before replay. Mirrors tm1py's _handle_remote_disconnect,
395
+ * which calls _manage_http_adapter() + connect() prior to retrying
396
+ * so a dropped session is re-established rather than replayed dead.
121
397
  */
122
398
  async retryRequest(config) {
123
399
  config._retryCount = config._retryCount || 0;
124
400
  config._retryCount++;
125
- const retryDelay = Math.pow(2, config._retryCount) * 1000; // Exponential backoff
401
+ const baseDelay = this._remoteDisconnectRetryDelay * Math.pow(2, config._retryCount - 1);
402
+ const retryDelay = Math.min(baseDelay, this._remoteDisconnectMaxDelay) * 1000;
126
403
  await new Promise(resolve => setTimeout(resolve, retryDelay));
404
+ // Re-establish session before replay. Clear sessionCookies first so
405
+ // connect() runs a full setupAuthentication (mirrors tm1py, whose
406
+ // connect() always re-runs _start_session / _set_session_id_cookie
407
+ // regardless of prior cookie state). Without this, a stale cookie
408
+ // from a server-invalidated session would be reused, the probe GET
409
+ // would 401, and the whole retry path would silently fail.
410
+ // Flip isConnected=false so the 401 re-auth branch cannot recurse
411
+ // through the probe.
412
+ this.sessionCookies.clear();
413
+ this.isConnected = false;
414
+ await this.connect();
415
+ // Drop stale Cookie/Authorization on the original config — the request
416
+ // interceptor rebuilds Cookie from sessionCookies on replay.
417
+ this.deleteHeaderCaseInsensitive(config.headers, 'Cookie');
418
+ this.deleteHeaderCaseInsensitive(config.headers, 'Authorization');
127
419
  return this.axiosInstance(config);
128
420
  }
129
421
  extractErrorMessage(response) {
@@ -140,22 +432,120 @@ class RestService {
140
432
  return `HTTP ${response.status}: ${response.statusText}`;
141
433
  }
142
434
  }
435
+ *waitTimeGenerator(timeout) {
436
+ let delay = this._asyncPollingInitialDelay;
437
+ let elapsed = 0;
438
+ if (timeout) {
439
+ while (elapsed < timeout) {
440
+ yield delay;
441
+ elapsed += delay;
442
+ delay = Math.min(delay * this._asyncPollingBackoffFactor, this._asyncPollingMaxDelay);
443
+ }
444
+ }
445
+ else {
446
+ while (true) {
447
+ yield delay;
448
+ delay = Math.min(delay * this._asyncPollingBackoffFactor, this._asyncPollingMaxDelay);
449
+ }
450
+ }
451
+ }
452
+ async _executeSyncRequest(method, url, data, timeout, idempotent, axiosExtras) {
453
+ const config = {
454
+ method: method,
455
+ url,
456
+ data,
457
+ ...axiosExtras
458
+ };
459
+ if (timeout !== undefined) {
460
+ config.timeout = timeout * 1000;
461
+ }
462
+ config._idempotent = idempotent !== null && idempotent !== void 0 ? idempotent : false;
463
+ return this.axiosInstance.request(config);
464
+ }
465
+ async _executeAsyncRequest(method, url, data, timeout, cancelAtTimeout, returnAsyncId, idempotent, axiosExtras) {
466
+ const preferValue = returnAsyncId ? 'respond-async' : 'respond-async,wait=55';
467
+ const config = {
468
+ method: method,
469
+ url,
470
+ data,
471
+ ...axiosExtras,
472
+ headers: {
473
+ ...axiosExtras === null || axiosExtras === void 0 ? void 0 : axiosExtras.headers,
474
+ Prefer: preferValue
475
+ }
476
+ };
477
+ if (timeout !== undefined) {
478
+ config.timeout = timeout * 1000;
479
+ }
480
+ config._idempotent = idempotent !== null && idempotent !== void 0 ? idempotent : false;
481
+ const response = await this.axiosInstance.request(config);
482
+ if (response.status !== 202) {
483
+ // Server completed synchronously. If the caller asked for an async
484
+ // ID (returnAsyncId: true) there is none to return — hand back the
485
+ // full response so the caller can inspect the result directly.
486
+ return response;
487
+ }
488
+ const location = response.headers['location'] || '';
489
+ const match = typeof location === 'string' ? location.match(/\('([^']+)'\)/) : null;
490
+ const asyncId = match ? match[1] : undefined;
491
+ if (!asyncId) {
492
+ throw new TM1Exception_1.TM1RestException(`Async request returned 202 but no valid async ID in Location header: ${location}`);
493
+ }
494
+ if (returnAsyncId) {
495
+ return asyncId;
496
+ }
497
+ return this._pollAsyncResponse(asyncId, timeout !== null && timeout !== void 0 ? timeout : this._timeout, cancelAtTimeout !== null && cancelAtTimeout !== void 0 ? cancelAtTimeout : this._cancelAtTimeout);
498
+ }
499
+ async _pollAsyncResponse(asyncId, timeout, cancelAtTimeout) {
500
+ for (const wait of this.waitTimeGenerator(timeout)) {
501
+ const response = await this.retrieve_async_response(asyncId);
502
+ if (response.status === 200 || response.status === 201) {
503
+ this.verifyAsyncResultHeader(response);
504
+ return response;
505
+ }
506
+ await new Promise(resolve => setTimeout(resolve, wait * 1000));
507
+ }
508
+ if (cancelAtTimeout) {
509
+ try {
510
+ await this.cancel_async_operation(asyncId);
511
+ }
512
+ catch (cancelError) {
513
+ console.warn(`Failed to cancel async operation ${asyncId} at timeout:`, cancelError);
514
+ }
515
+ }
516
+ throw new TM1Exception_1.TM1TimeoutException(`Async operation ${asyncId} timed out after ${timeout} seconds`, timeout);
517
+ }
518
+ async _request(method, url, data, options) {
519
+ var _a, _b, _c, _d, _e;
520
+ const timeout = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : this._timeout;
521
+ const cancelAtTimeout = (_b = options === null || options === void 0 ? void 0 : options.cancelAtTimeout) !== null && _b !== void 0 ? _b : this._cancelAtTimeout;
522
+ const asyncMode = (options === null || options === void 0 ? void 0 : options.returnAsyncId) || ((_c = options === null || options === void 0 ? void 0 : options.asyncRequestsMode) !== null && _c !== void 0 ? _c : this._asyncRequestsMode);
523
+ const verifyResponse = (_d = options === null || options === void 0 ? void 0 : options.verifyResponse) !== null && _d !== void 0 ? _d : true;
524
+ const idempotent = (_e = options === null || options === void 0 ? void 0 : options.idempotent) !== null && _e !== void 0 ? _e : false;
525
+ const { asyncRequestsMode: _asyncModeOpt, returnAsyncId, cancelAtTimeout: _cancelAtTimeoutOpt, idempotent: _idempotentOpt, verifyResponse: _verifyResponseOpt, timeout: _timeoutOpt, ...axiosExtras } = options !== null && options !== void 0 ? options : {};
526
+ if (!verifyResponse && axiosExtras.validateStatus === undefined) {
527
+ axiosExtras.validateStatus = () => true;
528
+ }
529
+ if (asyncMode) {
530
+ return this._executeAsyncRequest(method, url, data, timeout, cancelAtTimeout, returnAsyncId, idempotent, axiosExtras);
531
+ }
532
+ return this._executeSyncRequest(method, url, data, timeout, idempotent, axiosExtras);
533
+ }
143
534
  async connect() {
144
535
  try {
145
- // Set up authentication based on configuration
146
- await this.setupAuthentication();
147
- // Test connection
148
- const response = await this.axiosInstance.get('/Configuration/ServerName');
149
- // Extract session ID from response headers
150
- const setCookie = response.headers['set-cookie'];
151
- if (setCookie) {
152
- for (const cookie of setCookie) {
153
- const match = cookie.match(/TM1SessionId=([^;]+)/);
154
- if (match) {
155
- this.sessionId = match[1];
156
- break;
157
- }
158
- }
536
+ if (this.getSessionCookieValue() === undefined) {
537
+ await this.setupAuthentication();
538
+ }
539
+ // Mark probe non-idempotent so the response interceptor's retry
540
+ // branch skips it. Without this, a retry-triggered connect() whose
541
+ // probe also fails would spawn another connect(), recursively,
542
+ // bypassing remoteDisconnectMaxRetries (each probe has its own
543
+ // fresh _retryCount).
544
+ await this.axiosInstance.get('/Configuration/ServerName', { _idempotent: false });
545
+ // Strip Authorization only if the session cookie is established; Bearer/API-key
546
+ // modes that never issue a cookie must keep Authorization to stay authenticated
547
+ if (this.getSessionCookieValue()) {
548
+ this.removeAuthorizationHeader();
159
549
  }
160
550
  this.isConnected = true;
161
551
  }
@@ -164,34 +554,36 @@ class RestService {
164
554
  }
165
555
  }
166
556
  async disconnect() {
167
- if (this.isConnected && this.sessionId) {
557
+ const shouldClose = this.isConnected;
558
+ // Flip isConnected first so a 401 on tm1.Close cannot trigger reAuthenticate recursion
559
+ this.isConnected = false;
560
+ if (shouldClose) {
168
561
  try {
169
562
  await this.axiosInstance.post('/ActiveSession/tm1.Close', {});
170
563
  }
171
564
  catch (error) {
172
565
  // Ignore errors during disconnect
173
566
  }
174
- this.isConnected = false;
175
- this.sessionId = undefined;
176
567
  }
568
+ this.sessionCookies.clear();
177
569
  }
178
- async get(url, config) {
179
- return this.axiosInstance.get(url, config);
570
+ async get(url, options) {
571
+ return this._request('GET', url, undefined, { idempotent: true, ...options });
180
572
  }
181
- async post(url, data, config) {
182
- return this.axiosInstance.post(url, data, config);
573
+ async post(url, data, options) {
574
+ return this._request('POST', url, data, options);
183
575
  }
184
- async patch(url, data, config) {
185
- return this.axiosInstance.patch(url, data, config);
576
+ async patch(url, data, options) {
577
+ return this._request('PATCH', url, data, options);
186
578
  }
187
- async put(url, data, config) {
188
- return this.axiosInstance.put(url, data, config);
579
+ async put(url, data, options) {
580
+ return this._request('PUT', url, data, options);
189
581
  }
190
- async delete(url, config) {
191
- return this.axiosInstance.delete(url, config);
582
+ async delete(url, options) {
583
+ return this._request('DELETE', url, undefined, options);
192
584
  }
193
585
  getSessionId() {
194
- return this.sessionId;
586
+ return this.getSessionCookieValue();
195
587
  }
196
588
  setSandbox(sandboxName) {
197
589
  this.sandboxName = sandboxName;
@@ -200,175 +592,298 @@ class RestService {
200
592
  return this.sandboxName;
201
593
  }
202
594
  isLoggedIn() {
203
- return this.isConnected && !!this.sessionId;
595
+ return this.isConnected && (!!this.getSessionCookieValue() ||
596
+ !!this.axiosInstance.defaults.headers.common['Authorization']);
204
597
  }
205
598
  async getApiMetadata() {
206
599
  const response = await this.get("/$metadata");
207
600
  return response.data;
208
601
  }
602
+ // =========================================================================
603
+ // Authentication helpers — mirror tm1py's _build_authorization_token*,
604
+ // _generate_*_access_token, and _start_session flows
605
+ // =========================================================================
209
606
  /**
210
- * Set up authentication based on configuration
607
+ * Build an httpsAgent option that skips TLS verification when verify is false.
211
608
  */
212
- async setupAuthentication() {
213
- // Access Token authentication (TM1 12+ with JWT tokens)
214
- if (this.config.accessToken) {
215
- this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${this.config.accessToken}`;
216
- return;
217
- }
218
- // API Key authentication (TM1 12+ PAaaS)
219
- if (this.config.apiKey) {
220
- if (this.config.user === 'apikey') {
221
- // IBM Cloud API Key style
222
- this.axiosInstance.defaults.headers.common['Authorization'] = `Basic ${Buffer.from(`apikey:${this.config.apiKey}`).toString('base64')}`;
223
- }
224
- else {
225
- // Basic API Key style
226
- this.axiosInstance.defaults.headers.common['API-Key'] = this.config.apiKey;
609
+ static insecureAgentOption(verify) {
610
+ return verify === false
611
+ ? { httpsAgent: new https.Agent({ rejectUnauthorized: false }) }
612
+ : {};
613
+ }
614
+ /**
615
+ * Normalise a Set-Cookie header value (string | string[] | undefined) into a string[].
616
+ */
617
+ static normaliseSetCookie(raw) {
618
+ if (!raw)
619
+ return [];
620
+ return Array.isArray(raw) ? raw : [raw];
621
+ }
622
+ /**
623
+ * Extract a named cookie value from raw Set-Cookie headers.
624
+ */
625
+ static extractCookieValue(raw, name) {
626
+ const prefix = name + '=';
627
+ for (const header of RestService.normaliseSetCookie(raw)) {
628
+ const segment = header.split(';')[0];
629
+ if (segment.startsWith(prefix)) {
630
+ return segment.slice(prefix.length);
227
631
  }
228
- return;
229
- }
230
- // CAM (Cognos Access Manager) authentication
231
- if (this.config.authUrl && this.config.camPassport) {
232
- await this.setupCamAuthentication();
233
- return;
234
632
  }
235
- // CAM SSO authentication
236
- if (this.config.authUrl && this.config.user && this.config.password && this.config.namespace) {
237
- await this.setupCamSsoAuthentication();
238
- return;
239
- }
240
- // Service-to-Service authentication
241
- if (this.config.applicationClientId && this.config.applicationClientSecret) {
242
- await this.setupServiceToServiceAuthentication();
243
- return;
633
+ return undefined;
634
+ }
635
+ /**
636
+ * Build Basic Authorization header.
637
+ * Mirrors tm1py's _build_authorization_token_basic.
638
+ */
639
+ static _buildAuthorizationTokenBasic(user, password) {
640
+ return 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64');
641
+ }
642
+ /**
643
+ * Build CAMNamespace Authorization header.
644
+ * Mirrors tm1py's _build_authorization_token_cam (non-gateway path).
645
+ */
646
+ static _buildAuthorizationTokenCam(user, password, namespace) {
647
+ return 'CAMNamespace ' + Buffer.from(`${user}:${password}:${namespace}`).toString('base64');
648
+ }
649
+ /**
650
+ * Build CAMPassport Authorization token via gateway SSO.
651
+ * Mirrors tm1py's _build_authorization_token_cam (gateway path).
652
+ * Makes a GET request to the gateway URL with CAMNamespace as a query
653
+ * parameter and extracts the cam_passport cookie from the response.
654
+ *
655
+ * Note: tm1py uses HttpNegotiateAuth (NTLM/Kerberos) for gateway requests,
656
+ * which is Windows-only. This implementation sends a plain GET and relies on
657
+ * the gateway being accessible without NTLM. For environments requiring NTLM,
658
+ * pass a pre-obtained cam_passport via config.camPassport instead.
659
+ */
660
+ static async _buildAuthorizationTokenCamSso(gateway, namespace, verify) {
661
+ const response = await axios_1.default.get(gateway, {
662
+ params: { CAMNamespace: namespace },
663
+ ...RestService.insecureAgentOption(verify)
664
+ });
665
+ if (response.status !== 200) {
666
+ throw new Error('Failed to authenticate through CAM. Expected status_code 200, received status_code: ' +
667
+ response.status);
244
668
  }
245
- // Basic authentication (default)
246
- if (this.config.user && this.config.password) {
247
- const credentials = Buffer.from(`${this.config.user}:${this.config.password}`).toString('base64');
248
- this.axiosInstance.defaults.headers.common['Authorization'] = `Basic ${credentials}`;
249
- // Add namespace for TM1 Cloud
250
- if (this.config.namespace) {
251
- this.axiosInstance.defaults.headers.common['TM1-Namespace'] = this.config.namespace;
252
- }
253
- return;
669
+ const passport = RestService.extractCookieValue(response.headers['set-cookie'], 'cam_passport');
670
+ if (!passport) {
671
+ throw new Error("Failed to authenticate through CAM. HTTP response does not contain 'cam_passport' cookie");
254
672
  }
255
- throw new Error('No valid authentication configuration provided');
673
+ return 'CAMPassport ' + passport;
256
674
  }
257
675
  /**
258
- * Set up CAM (Cognos Access Manager) authentication
676
+ * Generate IBM IAM Cloud access token.
677
+ * Mirrors tm1py's _generate_ibm_iam_cloud_access_token.
259
678
  */
260
- async setupCamAuthentication() {
261
- if (!this.config.authUrl || !this.config.camPassport) {
262
- throw new Error('CAM authentication requires authUrl and camPassport');
263
- }
264
- try {
265
- const authResponse = await axios_1.default.post(this.config.authUrl, {
266
- parameters: [{
267
- name: 'CAMPassport',
268
- value: this.config.camPassport
269
- }]
270
- }, {
271
- headers: {
272
- 'Content-Type': 'application/json'
273
- }
274
- });
275
- if (authResponse.data && authResponse.data.sessionId) {
276
- this.sessionId = authResponse.data.sessionId;
277
- this.axiosInstance.defaults.headers.common['TM1SessionId'] = this.sessionId;
278
- }
279
- else {
280
- throw new Error('CAM authentication failed: No session ID returned');
281
- }
679
+ async _generateIbmIamCloudAccessToken() {
680
+ var _a;
681
+ const { iamUrl, apiKey } = this.config;
682
+ if (!iamUrl || !apiKey) {
683
+ throw new Error("'iamUrl' and 'apiKey' must be provided to generate access token from IBM Cloud");
282
684
  }
283
- catch (error) {
284
- throw new Error(`CAM authentication failed: ${error}`);
685
+ const payload = `grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey&apikey=${encodeURIComponent(apiKey)}`;
686
+ const headers = {
687
+ 'Accept': 'application/json',
688
+ 'Content-Type': 'application/x-www-form-urlencoded'
689
+ };
690
+ const response = await axios_1.default.post(iamUrl, payload, {
691
+ headers,
692
+ ...RestService.insecureAgentOption(this.config.verify)
693
+ });
694
+ if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.access_token)) {
695
+ throw new Error(`Failed to generate access_token from URL: '${iamUrl}'`);
285
696
  }
697
+ return response.data.access_token;
286
698
  }
287
699
  /**
288
- * Set up CAM SSO authentication
700
+ * Generate CPD (Cloud Pak for Data) access token.
701
+ * Mirrors tm1py's _generate_cpd_access_token.
289
702
  */
290
- async setupCamSsoAuthentication() {
291
- if (!this.config.authUrl || !this.config.user || !this.config.password || !this.config.namespace) {
292
- throw new Error('CAM SSO authentication requires authUrl, user, password, and namespace');
703
+ async _generateCpdAccessToken(credentials) {
704
+ var _a;
705
+ const { cpdUrl } = this.config;
706
+ if (!cpdUrl) {
707
+ throw new Error("'cpdUrl' must be provided to authenticate via CPD/Cloud Pak for Data");
293
708
  }
294
- try {
295
- const authPayload = {
296
- username: this.config.user,
297
- password: this.config.password,
298
- namespace: this.config.namespace
299
- };
300
- const authResponse = await axios_1.default.post(this.config.authUrl, authPayload, {
301
- headers: {
302
- 'Content-Type': 'application/json'
303
- }
304
- });
305
- if (authResponse.data && authResponse.data.token) {
306
- this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${authResponse.data.token}`;
307
- }
308
- else if (authResponse.data && authResponse.data.sessionId) {
309
- this.sessionId = authResponse.data.sessionId;
310
- this.axiosInstance.defaults.headers.common['TM1SessionId'] = this.sessionId;
311
- }
312
- else {
313
- throw new Error('CAM SSO authentication failed: No token or session ID returned');
314
- }
709
+ const url = `${cpdUrl}/v1/preauth/signin`;
710
+ const headers = { 'Content-Type': 'application/json;charset=UTF-8' };
711
+ const response = await axios_1.default.post(url, credentials, {
712
+ headers,
713
+ ...RestService.insecureAgentOption(this.config.verify)
714
+ });
715
+ if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.token)) {
716
+ throw new Error(`Failed to generate CPD access token from URL: '${url}'`);
315
717
  }
316
- catch (error) {
317
- throw new Error(`CAM SSO authentication failed: ${error}`);
718
+ return response.data.token;
719
+ }
720
+ /**
721
+ * Authenticate with PA Proxy using a CPD JWT token.
722
+ * Mirrors tm1py's PA_PROXY flow in _start_session.
723
+ */
724
+ async _authenticateWithPaProxy(jwt) {
725
+ const authRoot = this.resolveRoots().authRoot;
726
+ const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
727
+ const payload = `jwt=${jwt}`;
728
+ const response = await axios_1.default.post(authRoot, payload, {
729
+ headers,
730
+ ...RestService.insecureAgentOption(this.config.verify)
731
+ });
732
+ const setCookie = response.headers['set-cookie'];
733
+ const csrfValue = RestService.extractCookieValue(setCookie, 'ba-sso-csrf');
734
+ if (csrfValue) {
735
+ this.axiosInstance.defaults.headers.common['ba-sso-authenticity'] = csrfValue;
318
736
  }
737
+ this.parseSetCookieHeaders(setCookie);
319
738
  }
320
739
  /**
321
- * Set up Service-to-Service authentication
740
+ * Authenticate Service-to-Service (v12).
741
+ * Mirrors tm1py's SERVICE_TO_SERVICE flow in _start_session:
742
+ * Uses Basic auth with applicationClientId:applicationClientSecret,
743
+ * then POSTs {"User": user} to the auth endpoint.
322
744
  */
323
- async setupServiceToServiceAuthentication() {
324
- if (!this.config.applicationClientId || !this.config.applicationClientSecret) {
745
+ async _authenticateServiceToService() {
746
+ var _a;
747
+ const { applicationClientId, applicationClientSecret, user } = this.config;
748
+ if (!applicationClientId || !applicationClientSecret) {
325
749
  throw new Error('Service-to-Service authentication requires applicationClientId and applicationClientSecret');
326
750
  }
327
- try {
328
- const tokenEndpoint = this.config.authUrl || `${this.buildBaseUrl()}/oauth/token`;
329
- const tokenPayload = {
330
- grant_type: 'client_credentials',
331
- client_id: this.config.applicationClientId,
332
- client_secret: this.config.applicationClientSecret
333
- };
334
- const tokenResponse = await axios_1.default.post(tokenEndpoint, tokenPayload, {
335
- headers: {
336
- 'Content-Type': 'application/x-www-form-urlencoded'
337
- }
338
- });
339
- if (tokenResponse.data && tokenResponse.data.access_token) {
340
- this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${tokenResponse.data.access_token}`;
751
+ // Guard: v11 and plain baseUrl topologies resolve authRoot to a metadata
752
+ // probe URL, not a token endpoint. Require explicit authUrl in those cases.
753
+ if (!this.config.authUrl) {
754
+ const topo = this.determineTopology();
755
+ const baseUrlIsV12 = topo === 'base_url'
756
+ && /api\/v1\/Databases/.test((_a = this.config.baseUrl) !== null && _a !== void 0 ? _a : '');
757
+ if (topo === 'v11' || (topo === 'base_url' && !baseUrlIsV12)) {
758
+ throw new Error("'authUrl' is required for Service-to-Service authentication on v11 topology");
341
759
  }
342
- else {
343
- throw new Error('Service-to-Service authentication failed: No access token returned');
344
- }
345
- }
346
- catch (error) {
347
- throw new Error(`Service-to-Service authentication failed: ${error}`);
348
760
  }
761
+ const authRoot = this.config.authUrl || this.resolveRoots().authRoot;
762
+ const basicAuth = Buffer.from(`${applicationClientId}:${applicationClientSecret}`).toString('base64');
763
+ this.axiosInstance.defaults.headers.common['Authorization'] = `Basic ${basicAuth}`;
764
+ const response = await axios_1.default.post(authRoot, JSON.stringify({ User: user }), {
765
+ headers: {
766
+ ...RestService.HEADERS,
767
+ 'Authorization': `Basic ${basicAuth}`
768
+ },
769
+ ...RestService.insecureAgentOption(this.config.verify)
770
+ });
771
+ this.parseSetCookieHeaders(response.headers['set-cookie']);
349
772
  }
350
773
  /**
351
- * Get the authentication mode being used
774
+ * Determine the authentication mode from config.
775
+ * Mirrors tm1py's _determine_auth_mode, using the URL topology as the
776
+ * primary discriminator for v12 modes.
352
777
  */
353
778
  getAuthenticationMode() {
354
- if (this.config.accessToken) {
355
- return AuthenticationMode.ACCESS_TOKEN;
356
- }
357
- if (this.config.apiKey) {
358
- return this.config.user === 'apikey' ?
359
- AuthenticationMode.IBM_CLOUD_API_KEY :
360
- AuthenticationMode.BASIC_API_KEY;
361
- }
362
- if (this.config.authUrl && this.config.camPassport) {
363
- return AuthenticationMode.CAM;
364
- }
365
- if (this.config.authUrl && this.config.user && this.config.password && this.config.namespace) {
366
- return AuthenticationMode.CAM_SSO;
779
+ const topo = this.determineTopology();
780
+ const c = this.config;
781
+ switch (topo) {
782
+ case 'ibm_cloud':
783
+ return AuthenticationMode.IBM_CLOUD_API_KEY;
784
+ case 'pa_proxy':
785
+ return AuthenticationMode.PA_PROXY;
786
+ case 's2s':
787
+ return AuthenticationMode.SERVICE_TO_SERVICE;
788
+ case 'v11':
789
+ case 'base_url':
790
+ default: {
791
+ // v11 / base_url: check auth-specific config flags
792
+ if (c.accessToken)
793
+ return AuthenticationMode.ACCESS_TOKEN;
794
+ if (c.apiKey)
795
+ return AuthenticationMode.BASIC_API_KEY;
796
+ if (c.applicationClientId && c.applicationClientSecret) {
797
+ return AuthenticationMode.SERVICE_TO_SERVICE;
798
+ }
799
+ if (c.camPassport)
800
+ return AuthenticationMode.CAM;
801
+ if (c.gateway && c.namespace)
802
+ return AuthenticationMode.CAM_SSO;
803
+ if (c.integratedLogin)
804
+ return AuthenticationMode.WIA;
805
+ if (c.namespace)
806
+ return AuthenticationMode.CAM;
807
+ return AuthenticationMode.BASIC;
808
+ }
367
809
  }
368
- if (this.config.applicationClientId && this.config.applicationClientSecret) {
369
- return AuthenticationMode.SERVICE_TO_SERVICE;
810
+ }
811
+ /**
812
+ * Set up authentication based on configuration.
813
+ * Mirrors tm1py's _start_session routing.
814
+ */
815
+ async setupAuthentication() {
816
+ const authMode = this.getAuthenticationMode();
817
+ const password = this.config.decodeB64 && this.config.password
818
+ ? RestService.b64_decode_password(this.config.password)
819
+ : this.config.password;
820
+ switch (authMode) {
821
+ case AuthenticationMode.ACCESS_TOKEN:
822
+ this.axiosInstance.defaults.headers.common['Authorization'] =
823
+ `Bearer ${this.config.accessToken}`;
824
+ break;
825
+ case AuthenticationMode.BASIC_API_KEY:
826
+ if (this.config.user === 'apikey') {
827
+ this.axiosInstance.defaults.headers.common['Authorization'] =
828
+ RestService._buildAuthorizationTokenBasic('apikey', this.config.apiKey);
829
+ }
830
+ else {
831
+ this.axiosInstance.defaults.headers.common['API-Key'] = this.config.apiKey;
832
+ }
833
+ break;
834
+ case AuthenticationMode.IBM_CLOUD_API_KEY: {
835
+ const accessToken = await this._generateIbmIamCloudAccessToken();
836
+ this.axiosInstance.defaults.headers.common['Authorization'] =
837
+ `Bearer ${accessToken}`;
838
+ break;
839
+ }
840
+ case AuthenticationMode.PA_PROXY: {
841
+ if (!this.config.user || !password) {
842
+ throw new Error('PA Proxy authentication requires user and password');
843
+ }
844
+ const jwt = await this._generateCpdAccessToken({
845
+ username: this.config.user,
846
+ password
847
+ });
848
+ await this._authenticateWithPaProxy(jwt);
849
+ break;
850
+ }
851
+ case AuthenticationMode.SERVICE_TO_SERVICE:
852
+ await this._authenticateServiceToService();
853
+ break;
854
+ case AuthenticationMode.CAM_SSO: {
855
+ // CAM_SSO is only reached when gateway is set (see getAuthenticationMode)
856
+ const token = await RestService._buildAuthorizationTokenCamSso(this.config.gateway, this.config.namespace, this.config.verify);
857
+ this.axiosInstance.defaults.headers.common['Authorization'] = token;
858
+ break;
859
+ }
860
+ case AuthenticationMode.CAM: {
861
+ if (this.config.camPassport) {
862
+ this.axiosInstance.defaults.headers.common['Authorization'] =
863
+ 'CAMPassport ' + this.config.camPassport;
864
+ }
865
+ else if (this.config.namespace && this.config.user && password) {
866
+ this.axiosInstance.defaults.headers.common['Authorization'] =
867
+ RestService._buildAuthorizationTokenCam(this.config.user, password, this.config.namespace);
868
+ }
869
+ else {
870
+ throw new Error('CAM authentication requires either camPassport or user/password/namespace');
871
+ }
872
+ break;
873
+ }
874
+ case AuthenticationMode.WIA:
875
+ throw new Error('Windows Integrated Authentication (WIA) is not supported in Node.js. ' +
876
+ 'Use CAM or Basic authentication instead.');
877
+ case AuthenticationMode.BASIC:
878
+ default: {
879
+ if (!this.config.user || !password) {
880
+ throw new Error('No valid authentication configuration provided');
881
+ }
882
+ this.axiosInstance.defaults.headers.common['Authorization'] =
883
+ RestService._buildAuthorizationTokenBasic(this.config.user, password);
884
+ break;
885
+ }
370
886
  }
371
- return AuthenticationMode.BASIC;
372
887
  }
373
888
  /**
374
889
  * Re-authenticate using stored configuration
@@ -408,10 +923,10 @@ class RestService {
408
923
  getConnectionStats() {
409
924
  return {
410
925
  isConnected: this.isConnected,
411
- sessionId: this.sessionId,
926
+ sessionId: this.getSessionCookieValue(),
412
927
  authMode: this.getAuthenticationMode(),
413
928
  baseUrl: this.buildBaseUrl(),
414
- timeout: (this.config.timeout || 60) * 1000,
929
+ timeout: this._timeout,
415
930
  sandbox: this.sandboxName
416
931
  };
417
932
  }
@@ -485,13 +1000,14 @@ class RestService {
485
1000
  * Check if currently connected to TM1
486
1001
  */
487
1002
  is_connected() {
488
- return this.isConnected && !!this.sessionId;
1003
+ return this.isConnected && (!!this.getSessionCookieValue() ||
1004
+ !!this.axiosInstance.defaults.headers.common['Authorization']);
489
1005
  }
490
1006
  /**
491
1007
  * Get the current session ID
492
1008
  */
493
1009
  session_id() {
494
- return this.sessionId;
1010
+ return this.getSessionCookieValue();
495
1011
  }
496
1012
  /**
497
1013
  * Get TM1 server version (async method for tm1py compatibility)
@@ -514,56 +1030,69 @@ class RestService {
514
1030
  this._serverVersion = version;
515
1031
  }
516
1032
  /**
517
- * Check if current user is admin
1033
+ * Fetch the active user's group names as a CaseAndSpaceInsensitiveSet so
1034
+ * membership tests are case- and whitespace-insensitive (mirrors tm1py).
1035
+ * Concurrent callers (e.g. Promise.all([is_admin(), is_data_admin(), ...]))
1036
+ * coalesce onto a single in-flight request.
1037
+ */
1038
+ fetchActiveUserGroupNames() {
1039
+ if (this._activeUserGroupsPromise)
1040
+ return this._activeUserGroupsPromise;
1041
+ const clear = () => { this._activeUserGroupsPromise = undefined; };
1042
+ const fetch = this.get('/ActiveUser/Groups').then((response) => {
1043
+ var _a, _b;
1044
+ clear();
1045
+ const set = new Utils_1.CaseAndSpaceInsensitiveSet();
1046
+ const value = (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : [];
1047
+ for (const group of value) {
1048
+ if (group === null || group === void 0 ? void 0 : group.Name)
1049
+ set.add(group.Name);
1050
+ }
1051
+ return set;
1052
+ }, (err) => { clear(); throw err; });
1053
+ this._activeUserGroupsPromise = fetch;
1054
+ return fetch;
1055
+ }
1056
+ /**
1057
+ * Check if current user is admin. Result is cached after the first
1058
+ * computation, and pre-populated when the configured user is ADMIN.
518
1059
  */
519
1060
  async is_admin() {
520
- try {
521
- const response = await this.get('/ActiveUser/Groups');
522
- const groups = response.data.value || [];
523
- return groups.some((g) => g.Name === 'ADMIN');
524
- }
525
- catch (error) {
526
- return false;
527
- }
1061
+ if (this._isAdmin !== undefined)
1062
+ return this._isAdmin;
1063
+ const groups = await this.fetchActiveUserGroupNames();
1064
+ this._isAdmin = groups.has('ADMIN');
1065
+ return this._isAdmin;
528
1066
  }
529
1067
  /**
530
- * Check if current user is data admin
1068
+ * Check if current user is data admin (member of Admin or DataAdmin).
531
1069
  */
532
1070
  async is_data_admin() {
533
- try {
534
- const response = await this.get('/ActiveUser/Groups');
535
- const groups = response.data.value || [];
536
- return groups.some((g) => g.Name === 'ADMIN' || g.Name === 'DataAdmin');
537
- }
538
- catch (error) {
539
- return false;
540
- }
1071
+ if (this._isDataAdmin !== undefined)
1072
+ return this._isDataAdmin;
1073
+ const groups = await this.fetchActiveUserGroupNames();
1074
+ this._isDataAdmin = groups.has('Admin') || groups.has('DataAdmin');
1075
+ return this._isDataAdmin;
541
1076
  }
542
1077
  /**
543
- * Check if current user is ops admin
1078
+ * Check if current user is ops admin (member of Admin or OperationsAdmin).
544
1079
  */
545
1080
  async is_ops_admin() {
546
- try {
547
- const response = await this.get('/ActiveUser/Groups');
548
- const groups = response.data.value || [];
549
- return groups.some((g) => g.Name === 'ADMIN' || g.Name === 'OperationsAdmin');
550
- }
551
- catch (error) {
552
- return false;
553
- }
1081
+ if (this._isOpsAdmin !== undefined)
1082
+ return this._isOpsAdmin;
1083
+ const groups = await this.fetchActiveUserGroupNames();
1084
+ this._isOpsAdmin = groups.has('Admin') || groups.has('OperationsAdmin');
1085
+ return this._isOpsAdmin;
554
1086
  }
555
1087
  /**
556
- * Check if current user is security admin
1088
+ * Check if current user is security admin (member of Admin or SecurityAdmin).
557
1089
  */
558
1090
  async is_security_admin() {
559
- try {
560
- const response = await this.get('/ActiveUser/Groups');
561
- const groups = response.data.value || [];
562
- return groups.some((g) => g.Name === 'ADMIN' || g.Name === 'SecurityAdmin');
563
- }
564
- catch (error) {
565
- return false;
566
- }
1091
+ if (this._isSecurityAdmin !== undefined)
1092
+ return this._isSecurityAdmin;
1093
+ const groups = await this.fetchActiveUserGroupNames();
1094
+ this._isSecurityAdmin = groups.has('Admin') || groups.has('SecurityAdmin');
1095
+ return this._isSecurityAdmin;
567
1096
  }
568
1097
  /**
569
1098
  * Add custom HTTP header to all requests
@@ -591,61 +1120,109 @@ class RestService {
591
1120
  }
592
1121
  return headers;
593
1122
  }
1123
+ /**
1124
+ * Insert `tm1.compact=v0` into the Accept header (after the
1125
+ * `application/json` segment) and return the previous header value.
1126
+ * Mirrors tm1py's add_compact_json_header.
1127
+ */
1128
+ add_compact_json_header() {
1129
+ var _a;
1130
+ const original = (_a = this.axiosInstance.defaults.headers.common['Accept']) !== null && _a !== void 0 ? _a : '';
1131
+ const parts = original.split(';');
1132
+ // Insertion point matters: must come immediately after `application/json`
1133
+ parts.splice(1, 0, 'tm1.compact=v0');
1134
+ this.axiosInstance.defaults.headers.common['Accept'] = parts.join(';');
1135
+ return original;
1136
+ }
1137
+ /**
1138
+ * Decode a Base64-encoded password to its UTF-8 plaintext form
1139
+ * (mirrors tm1py's b64_decode_password).
1140
+ */
1141
+ static b64_decode_password(encryptedPassword) {
1142
+ return Buffer.from(encryptedPassword, 'base64').toString('utf-8');
1143
+ }
1144
+ /**
1145
+ * Coerce a boolean/number/string config value to a boolean. Strings
1146
+ * are stripped of whitespace and lowercased before comparison with
1147
+ * `'true'` (mirrors tm1py's translate_to_boolean).
1148
+ */
1149
+ static translate_to_boolean(value) {
1150
+ if (typeof value === 'boolean')
1151
+ return value;
1152
+ if (typeof value === 'number')
1153
+ return Boolean(value);
1154
+ if (typeof value === 'string') {
1155
+ return value.replace(/\s+/g, '').toLowerCase() === 'true';
1156
+ }
1157
+ throw new Error(`Invalid argument: '${String(value)}'. Must be type 'boolean', 'number', or 'string'`);
1158
+ }
594
1159
  /**
595
1160
  * Cancel an async operation by ID
596
1161
  */
597
1162
  async cancel_async_operation(async_id) {
598
- try {
599
- await this.post(`/AsyncOperations('${async_id}')/tm1.Cancel`);
600
- }
601
- catch (error) {
602
- throw new TM1Exception_1.TM1RestException(`Failed to cancel async operation ${async_id}: ${error}`);
603
- }
1163
+ await this.delete((0, Utils_1.formatUrl)("/_async('{}')", async_id), { asyncRequestsMode: false });
604
1164
  }
605
1165
  /**
606
1166
  * Retrieve async operation response
607
1167
  */
608
1168
  async retrieve_async_response(async_id) {
609
- try {
610
- const response = await this.get(`/AsyncOperations('${async_id}')`);
611
- return response.data;
612
- }
613
- catch (error) {
614
- throw new TM1Exception_1.TM1RestException(`Failed to retrieve async response ${async_id}: ${error}`);
615
- }
1169
+ // tm1py's retrieve_async_response returns the raw response without
1170
+ // raising on non-2xx because its caller (_poll_async_response) gates
1171
+ // on status_code in [200, 201]. Mirror that: accept all statuses so
1172
+ // transient 404s (resource not yet materialized) or 202s (still
1173
+ // running) flow through to the polling loop rather than aborting it.
1174
+ return this.get((0, Utils_1.formatUrl)("/_async('{}')", async_id), {
1175
+ asyncRequestsMode: false,
1176
+ verifyResponse: false,
1177
+ validateStatus: () => true
1178
+ });
616
1179
  }
617
1180
  /**
618
- * Get async operation status
1181
+ * TM1 v12 returns completed async results with HTTP 200 and encodes
1182
+ * the true operation status in the `asyncresult` header (e.g.
1183
+ * "500 Internal Server Error"). Mirror tm1py's
1184
+ * `_transform_async_response` by throwing on any embedded non-2xx
1185
+ * status so callers are not handed a 500 as "success".
619
1186
  */
620
- async get_async_operation_status(async_id) {
621
- try {
622
- const response = await this.get(`/AsyncOperations('${async_id}')/Status/$value`);
623
- return response.data;
624
- }
625
- catch (error) {
626
- throw new TM1Exception_1.TM1RestException(`Failed to get async operation status ${async_id}: ${error}`);
627
- }
1187
+ verifyAsyncResultHeader(response) {
1188
+ var _a;
1189
+ const headerValue = (_a = response.headers) === null || _a === void 0 ? void 0 : _a['asyncresult'];
1190
+ if (typeof headerValue !== 'string')
1191
+ return;
1192
+ const embeddedStatus = parseInt(headerValue.trim().split(/\s+/)[0], 10);
1193
+ if (Number.isNaN(embeddedStatus))
1194
+ return;
1195
+ if (embeddedStatus >= 200 && embeddedStatus < 300)
1196
+ return;
1197
+ throw new TM1Exception_1.TM1RestException(`Async operation failed with status ${headerValue}`, embeddedStatus, response);
628
1198
  }
629
1199
  /**
630
- * Wait for async operation to complete
1200
+ * Wait for async operation to complete using a fixed polling cadence.
1201
+ *
1202
+ * Unlike the internal dispatcher's {@link waitTimeGenerator} (capped
1203
+ * exponential backoff), this public helper polls every
1204
+ * {@link poll_interval_seconds} seconds so existing callers who tuned
1205
+ * the cadence keep their original behavior.
631
1206
  */
632
- async wait_for_async_operation(async_id, timeout_seconds = 300, poll_interval_seconds = 1) {
633
- const start_time = Date.now();
634
- const timeout_ms = timeout_seconds * 1000;
635
- const poll_interval_ms = poll_interval_seconds * 1000;
636
- while (Date.now() - start_time < timeout_ms) {
637
- const status = await this.get_async_operation_status(async_id);
638
- if (status === 'Completed' || status === 'CompletedSuccessfully') {
639
- return await this.retrieve_async_response(async_id);
1207
+ async wait_for_async_operation(async_id, timeout_seconds = 300, poll_interval_seconds = 1, cancel_at_timeout = false) {
1208
+ const deadline = Date.now() + timeout_seconds * 1000;
1209
+ while (Date.now() < deadline) {
1210
+ const response = await this.retrieve_async_response(async_id);
1211
+ if (response.status === 200 || response.status === 201) {
1212
+ this.verifyAsyncResultHeader(response);
1213
+ return response.data;
1214
+ }
1215
+ await new Promise(resolve => setTimeout(resolve, poll_interval_seconds * 1000));
1216
+ }
1217
+ if (cancel_at_timeout) {
1218
+ try {
1219
+ await this.cancel_async_operation(async_id);
640
1220
  }
641
- if (status === 'Failed' || status === 'CompletedWithError') {
642
- const response = await this.retrieve_async_response(async_id);
643
- throw new TM1Exception_1.TM1RestException(`Async operation failed: ${JSON.stringify(response)}`);
1221
+ catch (cancelError) {
1222
+ console.warn(`Failed to cancel async operation ${async_id} at timeout:`, cancelError);
644
1223
  }
645
- // Wait before polling again
646
- await new Promise(resolve => setTimeout(resolve, poll_interval_ms));
647
1224
  }
648
- throw new TM1Exception_1.TM1TimeoutException(`Async operation ${async_id} timed out after ${timeout_seconds} seconds`);
1225
+ throw new TM1Exception_1.TM1TimeoutException(`Async operation ${async_id} timed out after ${timeout_seconds} seconds`, timeout_seconds);
649
1226
  }
650
1227
  /**
651
1228
  * Get active user name
@@ -730,4 +1307,5 @@ RestService.HEADERS = {
730
1307
  };
731
1308
  RestService.DEFAULT_CONNECTION_POOL_SIZE = 10;
732
1309
  RestService.DEFAULT_POOL_CONNECTIONS = 1;
1310
+ RestService.SESSION_COOKIE_NAMES = ['TM1SessionId', 'paSession'];
733
1311
  //# sourceMappingURL=RestService.js.map