@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.
- package/dist/commands/access-groups.js +49 -17
- package/dist/commands/account-transaction-allocations.js +46 -14
- package/dist/commands/account-transactions.js +75 -35
- package/dist/commands/accounts.js +54 -22
- package/dist/commands/agent-platform.js +53 -21
- package/dist/commands/{nango-integrations.d.ts → agent-runs.d.ts} +1 -1
- package/dist/commands/agent-runs.js +1155 -0
- package/dist/commands/analytics.js +47 -15
- package/dist/commands/api-keys.js +49 -16
- package/dist/commands/{text-templates.d.ts → async-jobs.d.ts} +1 -1
- package/dist/commands/async-jobs.js +1158 -0
- package/dist/commands/audit-log.js +46 -14
- package/dist/commands/bank-accounts.js +63 -30
- package/dist/commands/bank-integrations.d.ts +1 -0
- package/dist/commands/bank-integrations.js +66 -31
- package/dist/commands/business-partners.js +77 -44
- package/dist/commands/chat.js +52 -19
- package/dist/commands/credit-card-accounts.d.ts +1 -0
- package/dist/commands/credit-card-accounts.js +67 -29
- package/dist/commands/delivery-notes.js +78 -44
- package/dist/commands/dunning-configs.js +49 -17
- package/dist/commands/dunning-reminders.js +52 -20
- package/dist/commands/dunning-runs.js +45 -13
- package/dist/commands/email-transactions.js +47 -15
- package/dist/commands/employees.js +76 -44
- package/dist/commands/entity-configurations.js +47 -15
- package/dist/commands/event-processor-mappings.js +49 -17
- package/dist/commands/files.js +56 -24
- package/dist/commands/health.js +49 -17
- package/dist/commands/{tag-configurations.d.ts → imports.d.ts} +1 -1
- package/dist/commands/{tag-configurations.js → imports.js} +59 -31
- package/dist/commands/inbox.js +52 -19
- package/dist/commands/integration-identities.d.ts +40 -0
- package/dist/commands/integration-identities.js +1151 -0
- package/dist/commands/integrations.js +47 -15
- package/dist/commands/item-categories.js +49 -17
- package/dist/commands/items.js +49 -17
- package/dist/commands/journal-lines.js +45 -13
- package/dist/commands/journals.js +68 -36
- package/dist/commands/ledgers.d.ts +3 -1
- package/dist/commands/ledgers.js +96 -34
- package/dist/commands/my-account.d.ts +39 -0
- package/dist/commands/my-account.js +1160 -0
- package/dist/commands/office-locations.js +49 -17
- package/dist/commands/payment-methods.js +54 -22
- package/dist/commands/payment-runs.js +61 -29
- package/dist/commands/payment-terms.js +50 -18
- package/dist/commands/payments.js +51 -19
- package/dist/commands/payroll-runs.d.ts +43 -0
- package/dist/commands/payroll-runs.js +1188 -0
- package/dist/commands/permissions.d.ts +39 -0
- package/dist/commands/{text-templates.js → permissions.js} +54 -30
- package/dist/commands/price-lists.js +54 -22
- package/dist/commands/processor-mappings.js +48 -16
- package/dist/commands/processors.js +55 -24
- package/dist/commands/purchase-order-types.js +49 -17
- package/dist/commands/purchase-orders.js +69 -36
- package/dist/commands/reports.js +49 -17
- package/dist/commands/roles.d.ts +39 -0
- package/dist/commands/{nango-integrations.js → roles.js} +63 -26
- package/dist/commands/salary-types.js +49 -17
- package/dist/commands/sales-invoices.js +84 -51
- package/dist/commands/sales-orders.js +78 -44
- package/dist/commands/saved-views.js +47 -15
- package/dist/commands/scheduled-processor-mappings.js +49 -17
- package/dist/commands/schedules.js +53 -21
- package/dist/commands/support/end.d.ts +9 -0
- package/dist/commands/support/end.js +29 -0
- package/dist/commands/support/start.d.ts +14 -0
- package/dist/commands/support/start.js +50 -0
- package/dist/commands/support/status.d.ts +10 -0
- package/dist/commands/support/status.js +45 -0
- package/dist/commands/support/tenants.d.ts +10 -0
- package/dist/commands/support/tenants.js +39 -0
- package/dist/commands/tags.js +51 -18
- package/dist/commands/tax-codes.js +52 -20
- package/dist/commands/tax-matrix.js +49 -17
- package/dist/commands/templates.d.ts +39 -0
- package/dist/commands/templates.js +1157 -0
- package/dist/commands/tenants.js +52 -20
- package/dist/commands/tools.js +46 -14
- package/dist/commands/users.d.ts +42 -0
- package/dist/commands/users.js +1174 -0
- package/dist/commands/vat-declarations.d.ts +2 -0
- package/dist/commands/vat-declarations.js +72 -36
- package/dist/commands/vendor-invoices.js +81 -49
- package/dist/commands/webhooks.js +51 -19
- package/dist/commands/workflows.js +47 -15
- package/dist/lib/api-client.d.ts +6 -0
- package/dist/lib/api-client.js +13 -0
- package/dist/lib/auth/constants.d.ts +12 -0
- package/dist/lib/auth/constants.js +12 -0
- package/dist/lib/auth/index.d.ts +11 -1
- package/dist/lib/auth/index.js +105 -6
- package/dist/lib/auth/jwks.d.ts +19 -0
- package/dist/lib/auth/jwks.js +112 -0
- package/dist/lib/auth/keycloak-provider.d.ts +45 -7
- package/dist/lib/auth/keycloak-provider.js +161 -27
- package/dist/lib/auth/tokens.d.ts +8 -3
- package/dist/lib/auth/tokens.js +8 -7
- package/dist/lib/auth/types.d.ts +5 -1
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.js +10 -0
- package/oclif.manifest.json +2182 -744
- 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 {
|
|
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
|
-
|
|
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
|
-
/**
|
|
139
|
-
async listTenants(
|
|
140
|
-
const decoded =
|
|
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
|
|
180
|
-
|
|
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
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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.
|
package/dist/lib/auth/tokens.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
54
|
+
tenant_id: tenantIdOf(claims),
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
/**
|
package/dist/lib/auth/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/lib/config.js
CHANGED
|
@@ -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
|
}
|