@aderis-erp/cli 1.0.17 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/commands/access-groups.js +49 -17
  2. package/dist/commands/account-transaction-allocations.js +46 -14
  3. package/dist/commands/account-transactions.js +75 -35
  4. package/dist/commands/accounts.js +54 -22
  5. package/dist/commands/agent-platform.js +53 -21
  6. package/dist/commands/{nango-integrations.d.ts → agent-runs.d.ts} +1 -1
  7. package/dist/commands/agent-runs.js +1155 -0
  8. package/dist/commands/analytics.js +47 -15
  9. package/dist/commands/api-keys.js +49 -16
  10. package/dist/commands/{text-templates.d.ts → async-jobs.d.ts} +1 -1
  11. package/dist/commands/async-jobs.js +1158 -0
  12. package/dist/commands/audit-log.js +46 -14
  13. package/dist/commands/bank-accounts.js +63 -30
  14. package/dist/commands/bank-integrations.d.ts +1 -0
  15. package/dist/commands/bank-integrations.js +66 -31
  16. package/dist/commands/business-partners.js +77 -44
  17. package/dist/commands/chat.js +52 -19
  18. package/dist/commands/credit-card-accounts.d.ts +1 -0
  19. package/dist/commands/credit-card-accounts.js +67 -29
  20. package/dist/commands/delivery-notes.js +78 -44
  21. package/dist/commands/dunning-configs.js +49 -17
  22. package/dist/commands/dunning-reminders.js +52 -20
  23. package/dist/commands/dunning-runs.js +45 -13
  24. package/dist/commands/email-transactions.js +47 -15
  25. package/dist/commands/employees.js +76 -44
  26. package/dist/commands/entity-configurations.js +47 -15
  27. package/dist/commands/event-processor-mappings.js +49 -17
  28. package/dist/commands/files.js +56 -24
  29. package/dist/commands/health.js +49 -17
  30. package/dist/commands/{tag-configurations.d.ts → imports.d.ts} +1 -1
  31. package/dist/commands/{tag-configurations.js → imports.js} +59 -31
  32. package/dist/commands/inbox.js +52 -19
  33. package/dist/commands/integration-identities.d.ts +40 -0
  34. package/dist/commands/integration-identities.js +1151 -0
  35. package/dist/commands/integrations.js +47 -15
  36. package/dist/commands/item-categories.js +49 -17
  37. package/dist/commands/items.js +49 -17
  38. package/dist/commands/journal-lines.js +45 -13
  39. package/dist/commands/journals.js +68 -36
  40. package/dist/commands/ledgers.d.ts +3 -1
  41. package/dist/commands/ledgers.js +96 -34
  42. package/dist/commands/my-account.d.ts +39 -0
  43. package/dist/commands/my-account.js +1160 -0
  44. package/dist/commands/office-locations.js +49 -17
  45. package/dist/commands/payment-methods.js +54 -22
  46. package/dist/commands/payment-runs.js +61 -29
  47. package/dist/commands/payment-terms.js +50 -18
  48. package/dist/commands/payments.js +51 -19
  49. package/dist/commands/payroll-runs.d.ts +43 -0
  50. package/dist/commands/payroll-runs.js +1188 -0
  51. package/dist/commands/permissions.d.ts +39 -0
  52. package/dist/commands/{text-templates.js → permissions.js} +54 -30
  53. package/dist/commands/price-lists.js +54 -22
  54. package/dist/commands/processor-mappings.js +48 -16
  55. package/dist/commands/processors.js +55 -24
  56. package/dist/commands/purchase-order-types.js +49 -17
  57. package/dist/commands/purchase-orders.js +69 -36
  58. package/dist/commands/reports.js +49 -17
  59. package/dist/commands/roles.d.ts +39 -0
  60. package/dist/commands/{nango-integrations.js → roles.js} +63 -26
  61. package/dist/commands/salary-types.js +49 -17
  62. package/dist/commands/sales-invoices.js +84 -51
  63. package/dist/commands/sales-orders.js +78 -44
  64. package/dist/commands/saved-views.js +47 -15
  65. package/dist/commands/scheduled-processor-mappings.js +49 -17
  66. package/dist/commands/schedules.js +53 -21
  67. package/dist/commands/support/end.d.ts +9 -0
  68. package/dist/commands/support/end.js +29 -0
  69. package/dist/commands/support/start.d.ts +14 -0
  70. package/dist/commands/support/start.js +50 -0
  71. package/dist/commands/support/status.d.ts +10 -0
  72. package/dist/commands/support/status.js +45 -0
  73. package/dist/commands/support/tenants.d.ts +10 -0
  74. package/dist/commands/support/tenants.js +39 -0
  75. package/dist/commands/tags.js +51 -18
  76. package/dist/commands/tax-codes.js +52 -20
  77. package/dist/commands/tax-matrix.js +49 -17
  78. package/dist/commands/templates.d.ts +39 -0
  79. package/dist/commands/templates.js +1157 -0
  80. package/dist/commands/tenants.js +52 -20
  81. package/dist/commands/tools.js +46 -14
  82. package/dist/commands/users.d.ts +42 -0
  83. package/dist/commands/users.js +1174 -0
  84. package/dist/commands/vat-declarations.d.ts +2 -0
  85. package/dist/commands/vat-declarations.js +72 -36
  86. package/dist/commands/vendor-invoices.js +81 -49
  87. package/dist/commands/webhooks.js +51 -19
  88. package/dist/commands/workflows.js +47 -15
  89. package/dist/lib/api-client.d.ts +6 -0
  90. package/dist/lib/api-client.js +13 -0
  91. package/dist/lib/auth/constants.d.ts +12 -0
  92. package/dist/lib/auth/constants.js +12 -0
  93. package/dist/lib/auth/index.d.ts +11 -1
  94. package/dist/lib/auth/index.js +105 -6
  95. package/dist/lib/auth/jwks.d.ts +19 -0
  96. package/dist/lib/auth/jwks.js +112 -0
  97. package/dist/lib/auth/keycloak-provider.d.ts +45 -7
  98. package/dist/lib/auth/keycloak-provider.js +161 -27
  99. package/dist/lib/auth/tokens.d.ts +8 -3
  100. package/dist/lib/auth/tokens.js +8 -7
  101. package/dist/lib/auth/types.d.ts +5 -1
  102. package/dist/lib/config.d.ts +18 -0
  103. package/dist/lib/config.js +10 -0
  104. package/oclif.manifest.json +2182 -744
  105. package/package.json +7 -4
@@ -1,9 +1,10 @@
1
1
  import { URL } from 'node:url';
2
- import { REDIRECT_URI } from './constants.js';
2
+ import { FORM_POST_HEADERS, REDIRECT_URI, SUPPORT_CLIENT_ID, SUPPORT_SESSION_GRANT_TYPE } from './constants.js';
3
3
  import { AuthError } from './errors.js';
4
4
  import { requireClientId } from './guards.js';
5
- import { authHttp, toAuthError } from './http.js';
6
- import { decodeToken, exchangeAuthorizationCode, redeemRefreshToken, tenantIdOf, tenantsOf, toTokenData, } from './tokens.js';
5
+ import { authHttp, httpStatusOf, toAuthError } from './http.js';
6
+ import { verifyToken } from './jwks.js';
7
+ import { exchangeAuthorizationCode, redeemRefreshToken, tenantIdOf, tenantsOf, toTokenData, } from './tokens.js';
7
8
  import { buildValidatedUrl } from './validated-url.js';
8
9
  /**
9
10
  * Default Keycloak origin and realm per environment. A context may override
@@ -83,13 +84,25 @@ function validateDiscovery(url, data) {
83
84
  throw new AuthError(`Keycloak discovery document at ${url} advertises a ${field} outside the allowed hosts: ${value}`);
84
85
  }
85
86
  };
86
- // `end_session_endpoint` is unused: logout is local-only.
87
87
  return {
88
88
  authorization_endpoint: requireEndpoint('authorization_endpoint'),
89
+ end_session_endpoint: requireEndpoint('end_session_endpoint'),
89
90
  issuer: requireString('issuer'),
91
+ jwks_uri: requireEndpoint('jwks_uri'),
90
92
  token_endpoint: requireEndpoint('token_endpoint'),
91
93
  };
92
94
  }
95
+ /** `{id, name}` entries from the realm's support-tenant list; an entry without a usable id is dropped. */
96
+ function parseSupportTenants(body) {
97
+ if (!Array.isArray(body))
98
+ return [];
99
+ return body.flatMap((entry) => {
100
+ const { id, name } = (entry ?? {});
101
+ if (typeof id !== 'string' || !id)
102
+ return [];
103
+ return [{ name: typeof name === 'string' && name ? name : id, tenantId: id }];
104
+ });
105
+ }
93
106
  /**
94
107
  * Authorization Code + PKCE S256 with `state` and `nonce` against the public
95
108
  * `aderis-cli-login` client, endpoints from OIDC discovery. Tenants come from
@@ -129,15 +142,55 @@ export class KeycloakProvider {
129
142
  this.discoveryCache.set(key, pending);
130
143
  return pending;
131
144
  }
145
+ /**
146
+ * Ends exactly the support session. The operator's own login is a different
147
+ * Keycloak session and is left running.
148
+ */
149
+ async endSupportSession(context, refreshToken) {
150
+ const discovery = await this.discover(context);
151
+ const params = new URLSearchParams({ client_id: SUPPORT_CLIENT_ID, refresh_token: refreshToken });
152
+ try {
153
+ await authHttp.post(discovery.end_session_endpoint, params.toString(), { headers: FORM_POST_HEADERS });
154
+ }
155
+ catch (error) {
156
+ // A session Keycloak has already forgotten is the state we wanted.
157
+ const status = httpStatusOf(error);
158
+ if (status !== 400 && status !== 401 && status !== 404)
159
+ throw toAuthError(error, 'Support session logout');
160
+ }
161
+ }
132
162
  async exchangeCode(context, { code, codeVerifier, nonce }) {
133
163
  const discovery = await this.discover(context);
134
164
  const response = await exchangeAuthorizationCode(discovery.token_endpoint, context, { code, codeVerifier });
135
- this.verifyNonce(response, nonce);
136
- return this.requireTenant(this.requireIssuer(discovery, toTokenData(response)));
165
+ await this.verifyNonce(context, discovery, response, nonce);
166
+ const claims = await this.verifyAccessToken(discovery, response.access_token);
167
+ return this.requireTenant(toTokenData(response, claims), claims);
168
+ }
169
+ /**
170
+ * Customers this operator may act for. Served by the realm's tenant-claims
171
+ * mapper against the operator's own Bearer, so Keycloak — not the CLI —
172
+ * decides who is an operator and which customers they may pick.
173
+ */
174
+ async listSupportTenants(context, operatorAccessToken) {
175
+ const discovery = await this.discover(context);
176
+ const url = buildValidatedUrl(new URL(discovery.issuer).origin, `${new URL(discovery.issuer).pathname}/aderis-tenant-claims/me/support/tenants`);
177
+ let response;
178
+ try {
179
+ response = await authHttp.get(url, {
180
+ headers: { Accept: 'application/json', Authorization: `Bearer ${operatorAccessToken}` },
181
+ });
182
+ }
183
+ catch (error) {
184
+ if (httpStatusOf(error) === 403) {
185
+ throw new AuthError('Your account is not an Aderis Support operator, so it cannot act for a customer.');
186
+ }
187
+ throw toAuthError(error, 'Support tenant list');
188
+ }
189
+ return parseSupportTenants(response.data);
137
190
  }
138
- /** No network call: the access token's `availableTenants` claim lists every tenant the user may act in. */
139
- async listTenants(_context, accessToken) {
140
- const decoded = decodeToken(accessToken);
191
+ /** The access token's `availableTenants` claim lists every tenant the user may act in, read once its signature checks out. */
192
+ async listTenants(context, accessToken) {
193
+ const decoded = await this.readClaims(context, accessToken);
141
194
  const current = tenantIdOf(decoded);
142
195
  const tenants = tenantsOf(decoded);
143
196
  if (tenants === undefined) {
@@ -145,9 +198,81 @@ export class KeycloakProvider {
145
198
  }
146
199
  return tenants.map(({ id, name }) => ({ isDefault: id === current, name, tenantId: id }));
147
200
  }
201
+ /**
202
+ * The claims of an access token, once its signature has been checked against
203
+ * the realm's keys. Every caller that needs a claim goes through here: reading
204
+ * one off an unverified token would believe whatever the token says
205
+ * (AderisERP/cli#25).
206
+ */
207
+ async readClaims(context, accessToken) {
208
+ const discovery = await this.discover(context);
209
+ // A stored token may well be past its five-minute expiry; that says nothing
210
+ // about who issued it. The signature and issuer still have to hold, and the
211
+ // API enforces expiry on its own.
212
+ return verifyToken(accessToken, {
213
+ ignoreExpiration: true,
214
+ issuer: discovery.issuer,
215
+ jwksUri: discovery.jwks_uri,
216
+ });
217
+ }
148
218
  refresh(context, refreshToken) {
149
219
  return this.redeem(context, refreshToken);
150
220
  }
221
+ /** Refreshes a support session. No tenant is named: the session never moves customer. */
222
+ async refreshSupportSession(context, refreshToken) {
223
+ const discovery = await this.discover(context);
224
+ const params = new URLSearchParams({
225
+ client_id: SUPPORT_CLIENT_ID,
226
+ grant_type: 'refresh_token',
227
+ refresh_token: refreshToken,
228
+ });
229
+ let response;
230
+ try {
231
+ response = await authHttp.post(discovery.token_endpoint, params.toString(), {
232
+ headers: FORM_POST_HEADERS,
233
+ });
234
+ }
235
+ catch (error) {
236
+ const status = httpStatusOf(error);
237
+ if (status === 400 || status === 401) {
238
+ throw new AuthError('The support session has ended. Start a new one with: aderis support start <tenant-id>', {
239
+ status,
240
+ });
241
+ }
242
+ throw toAuthError(error, 'Support session refresh');
243
+ }
244
+ const claims = await this.verifyAccessToken(discovery, response.data.access_token);
245
+ return toTokenData(response.data, claims);
246
+ }
247
+ /**
248
+ * Starts a support session for `tenantId`, authorised by the operator's own
249
+ * access token. The returned tokens belong to the shared support account and
250
+ * name the operator in `impersonatorId`; they are a second login, so the
251
+ * operator's own tokens stay valid and untouched.
252
+ */
253
+ async startSupportSession(context, operatorAccessToken, tenantId) {
254
+ const discovery = await this.discover(context);
255
+ const params = new URLSearchParams({
256
+ client_id: SUPPORT_CLIENT_ID,
257
+ grant_type: SUPPORT_SESSION_GRANT_TYPE,
258
+ tenant_id: tenantId,
259
+ });
260
+ let response;
261
+ try {
262
+ response = await authHttp.post(discovery.token_endpoint, params.toString(), {
263
+ headers: { ...FORM_POST_HEADERS, Authorization: `Bearer ${operatorAccessToken}` },
264
+ });
265
+ }
266
+ catch (error) {
267
+ const status = httpStatusOf(error);
268
+ if (status === 400 || status === 401 || status === 403) {
269
+ throw new AuthError(`Keycloak refused a support session for tenant ${tenantId}. Check that your account is a support operator and that the customer is one you may act for.`, { status });
270
+ }
271
+ throw toAuthError(error, 'Support session');
272
+ }
273
+ const claims = await this.verifyAccessToken(discovery, response.data.access_token);
274
+ return toTokenData(response.data, claims);
275
+ }
151
276
  /**
152
277
  * One refresh-token grant with the `tenant_id` form field, read by the
153
278
  * realm's tenant-claims mapper: the minted token's `tenantId` becomes this
@@ -176,37 +301,46 @@ export class KeycloakProvider {
176
301
  }
177
302
  async redeem(context, refreshToken, tenantId) {
178
303
  const discovery = await this.discover(context);
179
- const tokens = toTokenData(await redeemRefreshToken(discovery.token_endpoint, context, refreshToken, { tenantId }));
180
- return this.requireTenant(this.requireIssuer(discovery, tokens));
181
- }
182
- /** Rejects a token whose `iss` differs from the discovery issuer; the API compares `iss` exactly. */
183
- requireIssuer(discovery, tokens) {
184
- const issuer = decodeToken(tokens.access_token)?.iss;
185
- if (issuer !== discovery.issuer) {
186
- throw new AuthError(`Keycloak issued a token for issuer ${issuer ?? '(none)'}, but the realm's discovery document names ${discovery.issuer}. Aborting.`);
187
- }
188
- return tokens;
304
+ const response = await redeemRefreshToken(discovery.token_endpoint, context, refreshToken, { tenantId });
305
+ const claims = await this.verifyAccessToken(discovery, response.access_token);
306
+ return this.requireTenant(toTokenData(response, claims), claims);
189
307
  }
190
308
  /**
191
- * The mapper is fail-closed: with several reachable tenants and no `tenant_id`
192
- * the token has no `tenantId`. That token is stored and needs a tenant picked
193
- * (`completeTenantSelection`); a token reaching no tenant is refused.
309
+ * A token may legitimately carry no `tenantId`: the mapper is fail-closed for
310
+ * a `tenant_id` the session cannot reach, and a service account reaching
311
+ * several tenants gets none. That token is stored and needs a tenant named
312
+ * (`completeTenantSelection`); a token reaching no tenant at all is refused.
194
313
  */
195
- requireTenant(tokens) {
314
+ requireTenant(tokens, claims) {
196
315
  if (tokens.tenant_id)
197
316
  return tokens;
198
- const tenants = tenantsOf(decodeToken(tokens.access_token));
317
+ const tenants = tenantsOf(claims);
199
318
  if (tenants !== undefined && tenants.length > 0)
200
319
  return tokens;
201
320
  throw new AuthError('Keycloak issued a token with no tenants, so you have reached no tenant. Your user is not a member of any tenant in this realm. Contact your administrator.');
202
321
  }
322
+ /**
323
+ * Checks an access token's signature against the realm's published keys, and
324
+ * its issuer along with it. Every claim the CLI reads later — the tenant, the
325
+ * reachable tenants — is taken from the claims this returns, so no caller can
326
+ * read a claim that was not checked first (AderisERP/cli#25).
327
+ */
328
+ verifyAccessToken(discovery, accessToken) {
329
+ return verifyToken(accessToken, { issuer: discovery.issuer, jwksUri: discovery.jwks_uri });
330
+ }
203
331
  /** Consistency check only: the id_token signature is not checked (the API checks the access token). */
204
- verifyNonce(response, expectedNonce) {
332
+ async verifyNonce(context, discovery, response, expectedNonce) {
205
333
  if (!response.id_token) {
206
334
  throw new AuthError('Keycloak returned no id_token; the openid scope is missing from the client configuration.');
207
335
  }
208
- const claims = decodeToken(response.id_token);
209
- if (claims?.nonce !== expectedNonce) {
336
+ // Verified rather than decoded: a nonce read off an unverified token proves
337
+ // nothing, since anyone able to tamper with it can set the nonce too.
338
+ const claims = await verifyToken(response.id_token, {
339
+ audience: requireClientId(context),
340
+ issuer: discovery.issuer,
341
+ jwksUri: discovery.jwks_uri,
342
+ });
343
+ if (claims.nonce !== expectedNonce) {
210
344
  throw new AuthError('The id_token nonce does not match the login request. Aborting — please try again.');
211
345
  }
212
346
  }
@@ -19,9 +19,8 @@ export interface DecodedToken {
19
19
  tenantId?: string;
20
20
  tenants?: unknown;
21
21
  }
22
- export declare function decodeToken(token: string): DecodedToken | null;
23
22
  /** The tenant an access token is scoped to: `tenantId`, or the legacy `tenant_id`. */
24
- export declare function tenantIdOf(decoded: DecodedToken | null): string | undefined;
23
+ export declare function tenantIdOf(decoded: DecodedToken | null | undefined): string | undefined;
25
24
  /** One entry of the `availableTenants` claim, as the realm's tenant-claims mapper writes it. */
26
25
  export interface TenantClaim {
27
26
  id: string;
@@ -32,7 +31,13 @@ export interface TenantClaim {
32
31
  */
33
32
  export declare function tenantsOf(decoded: DecodedToken | null): TenantClaim[] | undefined;
34
33
  /** Map a token endpoint response onto the stored {@link TokenData} shape. */
35
- export declare function toTokenData(response: TokenResponse): TokenData;
34
+ /**
35
+ * Builds the stored token record from a token response and the claims that were
36
+ * verified from its access token. The claims are passed in rather than decoded
37
+ * here: a tenant read off an unverified token is attacker-controlled
38
+ * (AderisERP/cli#25).
39
+ */
40
+ export declare function toTokenData(response: TokenResponse, claims: DecodedToken): TokenData;
36
41
  /**
37
42
  * POSTs a form to a token endpoint (RFC 6749 §4.1.3 / §6). Failures surface as
38
43
  * {@link AuthError} with the HTTP status; the form never leaves this function inside an error.
@@ -1,14 +1,9 @@
1
- import jwt from 'jsonwebtoken';
2
1
  import { URLSearchParams } from 'node:url';
3
2
  import { SESSION_EXPIRED_MESSAGE } from '../config.js';
4
3
  import { FORM_POST_HEADERS, REDIRECT_URI } from './constants.js';
5
4
  import { AuthError } from './errors.js';
6
5
  import { requireClientId } from './guards.js';
7
6
  import { authHttp, toAuthError } from './http.js';
8
- export function decodeToken(token) {
9
- const decoded = jwt.decode(token);
10
- return decoded && typeof decoded === 'object' ? decoded : null;
11
- }
12
7
  /** The tenant an access token is scoped to: `tenantId`, or the legacy `tenant_id`. */
13
8
  export function tenantIdOf(decoded) {
14
9
  return decoded?.tenantId || decoded?.tenant_id;
@@ -45,12 +40,18 @@ export function tenantsOf(decoded) {
45
40
  return claim.map((entry, index) => parseTenantClaim(entry, index + 1));
46
41
  }
47
42
  /** Map a token endpoint response onto the stored {@link TokenData} shape. */
48
- export function toTokenData(response) {
43
+ /**
44
+ * Builds the stored token record from a token response and the claims that were
45
+ * verified from its access token. The claims are passed in rather than decoded
46
+ * here: a tenant read off an unverified token is attacker-controlled
47
+ * (AderisERP/cli#25).
48
+ */
49
+ export function toTokenData(response, claims) {
49
50
  return {
50
51
  access_token: response.access_token,
51
52
  expires_at: response.expires_in ? Date.now() + response.expires_in * 1000 : undefined,
52
53
  refresh_token: response.refresh_token,
53
- tenant_id: tenantIdOf(decodeToken(response.access_token)),
54
+ tenant_id: tenantIdOf(claims),
54
55
  };
55
56
  }
56
57
  /**
@@ -42,7 +42,11 @@ export interface AuthProvider {
42
42
  exchangeCode(context: Context, params: ExchangeParams): Promise<TokenData>;
43
43
  /** Tenants the user can switch to, given a valid access token. */
44
44
  listTenants(context: Context, accessToken: string): Promise<Tenant[]>;
45
- /** Refresh-token grant. A rejected refresh must surface as the "Session expired" error. */
45
+ /**
46
+ * Refresh-token grant. The tenant of the new token is the realm's to decide;
47
+ * only an explicit {@link switchTenant} names one. A rejected refresh must
48
+ * surface as the "Session expired" error.
49
+ */
46
50
  refresh(context: Context, refreshToken: string): Promise<TokenData>;
47
51
  /**
48
52
  * Re-scopes a live session to `tenantId` with one token request and no
@@ -26,6 +26,18 @@ export interface CachedTenant {
26
26
  name: string;
27
27
  tenantId: string;
28
28
  }
29
+ /**
30
+ * A running "act as Aderis Support" session, held beside the operator's own
31
+ * login rather than replacing it: ending it returns to the operator without a
32
+ * new browser login, exactly as the web app does.
33
+ */
34
+ export interface SupportSession {
35
+ /** The operator's Keycloak `sub`; a session started by someone else is not reused. */
36
+ operator_sub: string;
37
+ tenant_id: string;
38
+ tenant_name: string;
39
+ tokens: TokenData;
40
+ }
29
41
  export interface Context {
30
42
  api_key?: string;
31
43
  auth_type?: AuthType;
@@ -41,6 +53,8 @@ export interface Context {
41
53
  */
42
54
  keycloak_base_url?: string;
43
55
  name: string;
56
+ /** Set while acting as Aderis Support; the operator's own tokens stay in `tokens`. */
57
+ support?: SupportSession;
44
58
  tenant_id?: string;
45
59
  tokens?: TokenData;
46
60
  }
@@ -63,6 +77,10 @@ export declare function saveCachedTenants(contextName: string, tenants: CachedTe
63
77
  export declare function getCachedTenants(contextName: string): CachedTenant[] | undefined;
64
78
  export declare function getTokens(contextName: string): null | TokenData;
65
79
  export declare function clearTokens(contextName: string): void;
80
+ export declare function saveSupportSession(contextName: string, support: SupportSession): void;
81
+ export declare function clearSupportSession(contextName: string): void;
82
+ /** The running support session of a context, if any. */
83
+ export declare function getSupportSession(context: Context): SupportSession | undefined;
66
84
  export declare function saveApiKey(contextName: string, apiKey: string): void;
67
85
  export declare function clearApiKey(contextName: string): void;
68
86
  /**
@@ -128,6 +128,16 @@ export function getTokens(contextName) {
128
128
  export function clearTokens(contextName) {
129
129
  updateContext(contextName, { tokens: undefined });
130
130
  }
131
+ export function saveSupportSession(contextName, support) {
132
+ updateContext(contextName, { support });
133
+ }
134
+ export function clearSupportSession(contextName) {
135
+ updateContext(contextName, { support: undefined });
136
+ }
137
+ /** The running support session of a context, if any. */
138
+ export function getSupportSession(context) {
139
+ return context.support;
140
+ }
131
141
  export function saveApiKey(contextName, apiKey) {
132
142
  updateContext(contextName, { api_key: apiKey, auth_type: 'api_key' });
133
143
  }