emissions-api-sdk 1.0.9 → 1.0.10

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 (45) hide show
  1. package/dist/Client.js +69 -9
  2. package/dist/Constants.js +2 -1
  3. package/dist/coverage/clover.xml +72 -57
  4. package/dist/coverage/coverage-final.json +4 -4
  5. package/dist/coverage/lcov-report/index.html +18 -18
  6. package/dist/coverage/lcov-report/src/Client.ts.html +242 -50
  7. package/dist/coverage/lcov-report/src/Constants.ts.html +7 -4
  8. package/dist/coverage/lcov-report/src/api/Calculation.ts.html +1 -1
  9. package/dist/coverage/lcov-report/src/api/EconomicActivity.ts.html +1 -1
  10. package/dist/coverage/lcov-report/src/api/Factor.ts.html +1 -1
  11. package/dist/coverage/lcov-report/src/api/FactorSets.ts.html +1 -1
  12. package/dist/coverage/lcov-report/src/api/Fugitive.ts.html +1 -1
  13. package/dist/coverage/lcov-report/src/api/Location.ts.html +1 -1
  14. package/dist/coverage/lcov-report/src/api/Metadata.ts.html +1 -1
  15. package/dist/coverage/lcov-report/src/api/Mobile.ts.html +1 -1
  16. package/dist/coverage/lcov-report/src/api/RealEstate.ts.html +1 -1
  17. package/dist/coverage/lcov-report/src/api/Stationary.ts.html +1 -1
  18. package/dist/coverage/lcov-report/src/api/TransportationAndDistribution.ts.html +1 -1
  19. package/dist/coverage/lcov-report/src/api/TypeRecommender.ts.html +1 -1
  20. package/dist/coverage/lcov-report/src/api/Usage.ts.html +1 -1
  21. package/dist/coverage/lcov-report/src/api/index.html +1 -1
  22. package/dist/coverage/lcov-report/src/index.html +21 -21
  23. package/dist/coverage/lcov-report/src/request.ts.html +1 -1
  24. package/dist/coverage/lcov-report/src/utils.ts.html +5 -5
  25. package/dist/coverage/lcov-report/test/index.html +1 -1
  26. package/dist/coverage/lcov-report/test/mocks/CommonRequest.ts.html +1 -1
  27. package/dist/coverage/lcov-report/test/mocks/FactorRequest.ts.html +1 -1
  28. package/dist/coverage/lcov-report/test/mocks/GenericCalculationRequest.ts.html +1 -1
  29. package/dist/coverage/lcov-report/test/mocks/LocationRequest.ts.html +1 -1
  30. package/dist/coverage/lcov-report/test/mocks/SearchRequest.ts.html +1 -1
  31. package/dist/coverage/lcov-report/test/mocks/index.html +1 -1
  32. package/dist/coverage/lcov-report/test/testUtils.ts.html +4 -4
  33. package/dist/coverage/lcov.info +161 -121
  34. package/dist/types/Client.d.ts +26 -0
  35. package/dist/types/Constants.d.ts +1 -0
  36. package/dist/types/interfaces/Config.d.ts +6 -0
  37. package/docs/reference.html +8 -2
  38. package/docs/searchindex.js +1 -1
  39. package/package.json +2 -2
  40. package/src/Client.ts +79 -15
  41. package/src/Constants.ts +1 -0
  42. package/src/interfaces/Config.ts +7 -0
  43. package/test/client.test.ts +93 -1
  44. package/test/tokenRefresh.test.ts +91 -1
  45. package/.secrets.baseline +0 -314
package/src/Client.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import axios from "axios";
2
2
  import { ClientConfig } from "./interfaces/Config";
3
3
  import { findExpiryTime } from "./utils";
4
- import { API_DOMAIN, TOKEN_GENERATION_API, CLIENT_SOURCE_EXCEL, CLIENT_SOURCE_SDK } from "./Constants";
4
+ import { API_DOMAIN, TOKEN_GENERATION_API, PAT_TOKEN_EXCHANGE_API, CLIENT_SOURCE_EXCEL, CLIENT_SOURCE_SDK } from "./Constants";
5
5
 
6
6
  export class Client {
7
7
  private static instance: Client | null = null;
8
8
 
9
9
  private token: string;
10
10
  private readonly apiKey: string;
11
+ private readonly patToken: string;
11
12
  private readonly clientId: string;
12
13
  private readonly orgId: string;
13
14
  private expiresAt: number;
@@ -21,28 +22,30 @@ export class Client {
21
22
  config: ClientConfig,
22
23
  isUserProvidedToken = false
23
24
  ) {
24
- const { apiKey="", clientId, orgId="", host, authUrl, isExcelAddIn=false } = config;
25
+ const { apiKey="", patToken="", clientId, orgId="", host, authUrl, isExcelAddIn=false } = config;
25
26
  this.token = token;
26
27
  this.clientId = clientId;
27
28
  if (isUserProvidedToken) {
28
29
  this.apiKey = "";
30
+ this.patToken = "";
29
31
  this.orgId = "";
30
32
  } else {
31
33
  this.apiKey = apiKey;
34
+ this.patToken = patToken;
32
35
  this.orgId = orgId;
33
36
  }
34
37
 
35
38
  const exp = findExpiryTime(token);
36
39
  this.expiresAt = exp;
37
40
  this.domain = host ?? API_DOMAIN;
38
- this.tokenDomain = authUrl ?? TOKEN_GENERATION_API;
41
+ this.tokenDomain = authUrl ?? (patToken ? PAT_TOKEN_EXCHANGE_API : TOKEN_GENERATION_API);
39
42
  this.isUserProvidedToken = isUserProvidedToken;
40
43
  this.clientSource = isExcelAddIn === true ? CLIENT_SOURCE_EXCEL : CLIENT_SOURCE_SDK;
41
44
  }
42
45
 
43
46
  /**
44
47
  * Initializes and returns a Client instance with the provided configuration.
45
- *
48
+ *
46
49
  * @static
47
50
  * @param {ClientConfig} config - The client configuration object
48
51
  * @return {Promise<void>} A promise that resolves when the client is initialized
@@ -50,7 +53,8 @@ export class Client {
50
53
  * - custom "host" is provided without "authUrl"
51
54
  * - token is provided without "clientId"
52
55
  * - apiKey is provided without "clientId" or "orgId"
53
- *
56
+ * - patToken is provided without "clientId" or with "orgId"
57
+ *
54
58
  * @example
55
59
  * // Initialize with authentication URL
56
60
  * await Client.getClient({
@@ -59,19 +63,25 @@ export class Client {
59
63
  * clientId: 'client123',
60
64
  * orgId: 'org456'
61
65
  * });
62
- *
66
+ *
63
67
  * // Initialize with existing token
64
68
  * await Client.getClient({
65
69
  * token: 'existing-jwt-token',
66
70
  * clientId: 'client123'
67
71
  * });
68
- *
72
+ *
69
73
  * // Initialize with API key
70
74
  * await Client.getClient({
71
75
  * apiKey: 'your-api-key',
72
76
  * clientId: 'client123',
73
77
  * orgId: 'org456'
74
78
  * });
79
+ *
80
+ * // Initialize with PAT token
81
+ * await Client.getClient({
82
+ * patToken: 'your-personal-access-token',
83
+ * clientId: 'client123'
84
+ * });
75
85
  */
76
86
 
77
87
  public static async getClient(config: ClientConfig): Promise<void> {
@@ -90,14 +100,28 @@ export class Client {
90
100
  'If apiKey is provided , "clientId" and "OrgId" must also be provided.'
91
101
  );
92
102
  }
103
+ if(config.patToken && !config.clientId){
104
+ throw new Error(
105
+ 'If patToken is provided, "clientId" must also be provided.'
106
+ );
107
+ }
108
+ if(config.patToken && config.orgId){
109
+ throw new Error(
110
+ 'orgId should not be provided when using patToken.'
111
+ );
112
+ }
93
113
  let token: string;
94
114
  let isUserProvidedToken = false;
95
115
 
96
116
  if (config.token && config.clientId) {
97
117
  token = config.token;
98
118
  isUserProvidedToken = true;
99
- } else token = await Client.requestToken(config);
100
- Client.instance = new Client(token, config,isUserProvidedToken);
119
+ } else if (config.patToken && config.clientId) {
120
+ token = await Client.requestTokenFromPAT(config);
121
+ } else {
122
+ token = await Client.requestToken(config);
123
+ }
124
+ Client.instance = new Client(token, config, isUserProvidedToken);
101
125
  }
102
126
 
103
127
  /**
@@ -158,12 +182,21 @@ export class Client {
158
182
  const now = Math.floor(Date.now() / 1000);
159
183
  if (this.expiresAt - now < 60) {
160
184
  console.log("[SDK] Refreshing token...");
161
- const token = await Client.requestToken({
162
- apiKey: this.apiKey,
163
- clientId: this.clientId,
164
- orgId: this.orgId,
165
- authUrl: this.tokenDomain,
166
- });
185
+ let token: string;
186
+ if (this.patToken) {
187
+ token = await Client.requestTokenFromPAT({
188
+ patToken: this.patToken,
189
+ clientId: this.clientId,
190
+ authUrl: this.tokenDomain,
191
+ });
192
+ } else {
193
+ token = await Client.requestToken({
194
+ apiKey: this.apiKey,
195
+ clientId: this.clientId,
196
+ orgId: this.orgId,
197
+ authUrl: this.tokenDomain,
198
+ });
199
+ }
167
200
  this.token = token;
168
201
  this.expiresAt = findExpiryTime(token);
169
202
  }
@@ -249,4 +282,35 @@ export class Client {
249
282
  if (!res.data) throw new Error("Token response is empty");
250
283
  return String(res.data).trim();
251
284
  }
285
+
286
+ /**
287
+ * Requests an authentication token by exchanging a Personal Access Token (PAT).
288
+ *
289
+ * @private
290
+ * @static
291
+ * @param {ClientConfig} config - The client configuration containing PAT and client details
292
+ * @return {Promise<string>} A promise that resolves to the authentication token string
293
+ * @throws {Error} Throws an error if the token response is empty
294
+ *
295
+ * @example
296
+ * // Internal usage within the Client class
297
+ * const token = await Client.requestTokenFromPAT({
298
+ * patToken: 'your-personal-access-token',
299
+ * clientId: 'client123',
300
+ * authUrl: 'https://custom-auth.example.com/exchange'
301
+ * });
302
+ */
303
+ private static async requestTokenFromPAT(config: ClientConfig): Promise<string> {
304
+ const tokenUrl = config.authUrl ?? PAT_TOKEN_EXCHANGE_API;
305
+ const res = await axios.post(tokenUrl, null, {
306
+ headers: {
307
+ "X-IBM-Client-Id": `saascore-${config.clientId}`,
308
+ "X-IBM-Envizi-Pat": config.patToken,
309
+ accept: "application/json",
310
+ }
311
+ });
312
+
313
+ if (!res.data) throw new Error("Token response is empty");
314
+ return String(res.data).trim();
315
+ }
252
316
  }
package/src/Constants.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export const API_DOMAIN = "https://api.ibm.com/ghgemissions/run";
2
2
  export const TOKEN_GENERATION_API = "https://api.ibm.com/saascore/run/authentication-retrieve/api-key";
3
+ export const PAT_TOKEN_EXCHANGE_API = "https://api.ibm.com/saascore/run/envizi-auth/exchange";
3
4
  export const MOBILE_API_PATH = "/v3/carbon/mobile";
4
5
  export const LOCATION_API_PATH = "/v3/carbon/location";
5
6
  export const FUGITIVE_API_PATH = "/v3/carbon/fugitive";
@@ -70,6 +70,13 @@ export interface ClientConfig {
70
70
  */
71
71
  token?: string;
72
72
 
73
+ /**
74
+ * Optional Personal Access Token (PAT) for authentication.
75
+ * If provided, it will be exchanged for a JWT token.
76
+ * Requires clientId to be provided.
77
+ */
78
+ patToken?: string;
79
+
73
80
  /**
74
81
  * Optional flag indicating whether the client is running as an Excel Add-In.
75
82
  * This may affect certain behaviors like authentication flows.
@@ -1,7 +1,7 @@
1
1
  import { Client } from "../src/Client";
2
2
  import axios from "axios";
3
3
  import MockAdapter from "axios-mock-adapter";
4
- import { TOKEN_GENERATION_API } from "../src/Constants";
4
+ import { TOKEN_GENERATION_API, PAT_TOKEN_EXCHANGE_API } from "../src/Constants";
5
5
  import { ClientConfig } from "../src/interfaces/Config";
6
6
 
7
7
  const mock = new MockAdapter(axios);
@@ -57,6 +57,27 @@ describe("Client initialization and Header Authorization", () => {
57
57
  'If apiKey is provided , "clientId" and "OrgId" must also be provided.'
58
58
  );
59
59
  });
60
+
61
+ it("Should throw validation error for missing clientId with patToken", async () => {
62
+ await expect(
63
+ Client.getClient({ patToken: "pat-token" } as unknown as ClientConfig)
64
+ ).rejects.toThrow(
65
+ 'If patToken is provided, "clientId" must also be provided.'
66
+ );
67
+ });
68
+
69
+ it("Should throw validation error when orgId is provided with patToken", async () => {
70
+ await expect(
71
+ Client.getClient({
72
+ patToken: "pat-token",
73
+ clientId: "client123",
74
+ orgId: "org456"
75
+ } as ClientConfig)
76
+ ).rejects.toThrow(
77
+ 'orgId should not be provided when using patToken.'
78
+ );
79
+ });
80
+
60
81
  it("should not call refresh token if userProvidedToken is set and set token to userProvided", async () => {
61
82
  const base64Header = Buffer.from(
62
83
  JSON.stringify({ alg: "HS256", typ: "JWT" })
@@ -79,4 +100,75 @@ describe("Client initialization and Header Authorization", () => {
79
100
  });
80
101
  expect(spy).not.toHaveBeenCalled();
81
102
  });
103
+
104
+ it("should initialize client with PAT token and return auth header", async () => {
105
+ const base64Header = Buffer.from(
106
+ JSON.stringify({ alg: "HS256", typ: "JWT" })
107
+ ).toString("base64url");
108
+ const base64Payload = Buffer.from(
109
+ JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 600 })
110
+ ).toString("base64url");
111
+ const mockToken = `${base64Header}.${base64Payload}.Signature`;
112
+
113
+ mock.onPost(PAT_TOKEN_EXCHANGE_API).reply(200, mockToken);
114
+
115
+ await Client.getClient({
116
+ patToken: "test-pat-token",
117
+ clientId: "test-client-id",
118
+ });
119
+
120
+ const instance = Client.getInstance();
121
+ expect(instance.getAuthHeader()).toEqual({
122
+ Authorization: `Bearer ${mockToken}`,
123
+ });
124
+ });
125
+
126
+ it("should initialize client with PAT token and custom authUrl", async () => {
127
+ const base64Header = Buffer.from(
128
+ JSON.stringify({ alg: "HS256", typ: "JWT" })
129
+ ).toString("base64url");
130
+ const base64Payload = Buffer.from(
131
+ JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 600 })
132
+ ).toString("base64url");
133
+ const mockToken = `${base64Header}.${base64Payload}.Signature`;
134
+
135
+ const customAuthUrl = "https://custom-auth.example.com/exchange";
136
+ mock.onPost(customAuthUrl).reply(200, mockToken);
137
+
138
+ await Client.getClient({
139
+ patToken: "test-pat-token",
140
+ clientId: "test-client-id",
141
+ authUrl: customAuthUrl,
142
+ });
143
+
144
+ const instance = Client.getInstance();
145
+ expect(instance.getAuthHeader()).toEqual({
146
+ Authorization: `Bearer ${mockToken}`,
147
+ });
148
+ });
149
+
150
+ it("should call requestTokenFromPAT with correct headers", async () => {
151
+ const base64Header = Buffer.from(
152
+ JSON.stringify({ alg: "HS256", typ: "JWT" })
153
+ ).toString("base64url");
154
+ const base64Payload = Buffer.from(
155
+ JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 600 })
156
+ ).toString("base64url");
157
+ const mockToken = `${base64Header}.${base64Payload}.Signature`;
158
+
159
+ mock.onPost(PAT_TOKEN_EXCHANGE_API).reply((config) => {
160
+ expect(config.headers?.["X-IBM-Client-Id"]).toBe("saascore-test-client-id");
161
+ expect(config.headers?.["X-IBM-Envizi-Pat"]).toBe("test-pat-token");
162
+ expect(config.headers?.["Accept"]).toBe("application/json");
163
+ return [200, mockToken];
164
+ });
165
+
166
+ await Client.getClient({
167
+ patToken: "test-pat-token",
168
+ clientId: "test-client-id",
169
+ });
170
+
171
+ const instance = Client.getInstance();
172
+ expect(instance).toBeDefined();
173
+ });
82
174
  });
@@ -3,7 +3,7 @@ import * as utils from "../src/utils";
3
3
  import mockAxios from "axios";
4
4
  import MockAdapter from "axios-mock-adapter";
5
5
  import { generateMockjwt } from "./testUtils";
6
- import { TOKEN_GENERATION_API } from "../src/Constants";
6
+ import { TOKEN_GENERATION_API, PAT_TOKEN_EXCHANGE_API } from "../src/Constants";
7
7
 
8
8
  const mock = new MockAdapter(mockAxios);
9
9
  describe("Token refreshing", () => {
@@ -49,4 +49,94 @@ describe("Token refreshing", () => {
49
49
  await instance.refreshToken();
50
50
  expect(spy).not.toHaveBeenCalledTimes(1);
51
51
  });
52
+
53
+ it("should refresh PAT token when it is about to expire", async () => {
54
+ const shortLiveToken = generateMockjwt(1);
55
+ const refreshedToken = generateMockjwt(600);
56
+
57
+ mock
58
+ .onPost(PAT_TOKEN_EXCHANGE_API)
59
+ .replyOnce(200, shortLiveToken)
60
+ .onPost(PAT_TOKEN_EXCHANGE_API)
61
+ .replyOnce(200, refreshedToken);
62
+
63
+ jest
64
+ .spyOn(utils, "findExpiryTime")
65
+ .mockReturnValueOnce(Math.floor(Date.now() / 1000) + 1)
66
+ .mockReturnValueOnce(Math.floor(Date.now() / 1000) + 600);
67
+
68
+ await Client.getClient({
69
+ patToken: "test-pat-token",
70
+ clientId: "test-client-id",
71
+ });
72
+ const instance = Client.getInstance();
73
+
74
+ const spy = jest.spyOn(Client as any, "requestTokenFromPAT");
75
+
76
+ await instance.refreshToken();
77
+ expect(spy).toHaveBeenCalledTimes(1);
78
+ expect(spy).toHaveBeenCalledWith({
79
+ patToken: "test-pat-token",
80
+ clientId: "test-client-id",
81
+ authUrl: PAT_TOKEN_EXCHANGE_API,
82
+ });
83
+ });
84
+
85
+ it("should not refresh PAT token if it is still valid", async () => {
86
+ const freshToken = generateMockjwt(600);
87
+ mock.onPost(PAT_TOKEN_EXCHANGE_API).reply(200, freshToken);
88
+
89
+ const spy = jest.spyOn(Client as any, "requestTokenFromPAT");
90
+
91
+ jest
92
+ .spyOn(utils, "findExpiryTime")
93
+ .mockReturnValue(Math.floor(Date.now() / 1000) + 600);
94
+
95
+ await Client.getClient({
96
+ patToken: "test-pat-token",
97
+ clientId: "test-client-id",
98
+ });
99
+ const instance = Client.getInstance();
100
+
101
+ spy.mockClear(); // Clear the initial call from getClient
102
+
103
+ await instance.refreshToken();
104
+ expect(spy).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it("should refresh PAT token with custom authUrl", async () => {
108
+ const shortLiveToken = generateMockjwt(1);
109
+ const refreshedToken = generateMockjwt(600);
110
+ const customAuthUrl = "https://custom-auth.example.com/exchange";
111
+
112
+ mock
113
+ .onPost(customAuthUrl)
114
+ .replyOnce(200, shortLiveToken)
115
+ .onPost(customAuthUrl)
116
+ .replyOnce(200, refreshedToken);
117
+
118
+ const spy = jest.spyOn(Client as any, "requestTokenFromPAT");
119
+
120
+ jest
121
+ .spyOn(utils, "findExpiryTime")
122
+ .mockReturnValueOnce(Math.floor(Date.now() / 1000) + 1)
123
+ .mockReturnValueOnce(Math.floor(Date.now() / 1000) + 600);
124
+
125
+ await Client.getClient({
126
+ patToken: "test-pat-token",
127
+ clientId: "test-client-id",
128
+ authUrl: customAuthUrl,
129
+ });
130
+ const instance = Client.getInstance();
131
+
132
+ spy.mockClear(); // Clear the initial call from getClient
133
+
134
+ await instance.refreshToken();
135
+ expect(spy).toHaveBeenCalledTimes(1);
136
+ expect(spy).toHaveBeenCalledWith({
137
+ patToken: "test-pat-token",
138
+ clientId: "test-client-id",
139
+ authUrl: customAuthUrl,
140
+ });
141
+ });
52
142
  });