@aderis-erp/cli 1.0.13 → 1.0.15

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 (86) hide show
  1. package/dist/commands/access-groups.js +21 -21
  2. package/dist/commands/account-transaction-allocations.js +21 -21
  3. package/dist/commands/account-transactions.js +23 -23
  4. package/dist/commands/accounts.js +23 -23
  5. package/dist/commands/agent-platform.js +23 -22
  6. package/dist/commands/analytics.d.ts +40 -0
  7. package/dist/commands/{documents.js → analytics.js} +33 -43
  8. package/dist/commands/audit-log.js +24 -24
  9. package/dist/commands/auth/login.js +11 -2
  10. package/dist/commands/auth/logout.d.ts +2 -0
  11. package/dist/commands/auth/logout.js +19 -6
  12. package/dist/commands/auth/status.js +26 -12
  13. package/dist/commands/auth/tenant.js +12 -3
  14. package/dist/commands/auth.js +32 -17
  15. package/dist/commands/bank-accounts.js +21 -21
  16. package/dist/commands/bank-integrations.js +21 -21
  17. package/dist/commands/business-partners.js +29 -26
  18. package/dist/commands/chat.js +21 -21
  19. package/dist/commands/config/add.d.ts +9 -0
  20. package/dist/commands/config/add.js +64 -2
  21. package/dist/commands/config/current.js +9 -4
  22. package/dist/commands/config/list.js +9 -4
  23. package/dist/commands/config/set-api-key.d.ts +10 -0
  24. package/dist/commands/config/set-api-key.js +49 -0
  25. package/dist/commands/config/use.js +3 -4
  26. package/dist/commands/credit-card-accounts.js +21 -21
  27. package/dist/commands/delivery-notes.js +28 -25
  28. package/dist/commands/dunning-configs.js +21 -21
  29. package/dist/commands/dunning-reminders.js +21 -21
  30. package/dist/commands/dunning-runs.js +21 -21
  31. package/dist/commands/email-transactions.js +21 -21
  32. package/dist/commands/employees.js +23 -23
  33. package/dist/commands/entity-configurations.js +21 -21
  34. package/dist/commands/event-processor-mappings.js +21 -21
  35. package/dist/commands/{documents.d.ts → files.d.ts} +1 -1
  36. package/dist/commands/{files-internal.js → files.js} +39 -33
  37. package/dist/commands/health.js +21 -21
  38. package/dist/commands/inbox.d.ts +1 -0
  39. package/dist/commands/inbox.js +25 -23
  40. package/dist/commands/integrations.js +21 -21
  41. package/dist/commands/item-categories.js +21 -21
  42. package/dist/commands/items.js +23 -23
  43. package/dist/commands/journal-lines.js +21 -21
  44. package/dist/commands/journals.js +27 -24
  45. package/dist/commands/ledgers.js +25 -22
  46. package/dist/commands/office-locations.js +21 -21
  47. package/dist/commands/payment-methods.js +21 -21
  48. package/dist/commands/payment-runs.js +23 -22
  49. package/dist/commands/payment-terms.js +21 -21
  50. package/dist/commands/payments.js +21 -21
  51. package/dist/commands/price-lists.js +21 -21
  52. package/dist/commands/processor-mappings.js +21 -21
  53. package/dist/commands/processors.js +21 -21
  54. package/dist/commands/purchase-order-types.js +21 -21
  55. package/dist/commands/purchase-orders.d.ts +1 -0
  56. package/dist/commands/purchase-orders.js +32 -25
  57. package/dist/commands/reports.js +21 -21
  58. package/dist/commands/salary-types.js +21 -21
  59. package/dist/commands/sales-invoices.js +33 -31
  60. package/dist/commands/sales-orders.js +23 -23
  61. package/dist/commands/saved-views.js +21 -21
  62. package/dist/commands/scheduled-processor-mappings.d.ts +40 -0
  63. package/dist/commands/scheduled-processor-mappings.js +1178 -0
  64. package/dist/commands/schedules.js +21 -21
  65. package/dist/commands/switch.js +10 -3
  66. package/dist/commands/{files-internal.d.ts → tag-configurations.d.ts} +1 -1
  67. package/dist/commands/tag-configurations.js +1178 -0
  68. package/dist/commands/tags.js +21 -21
  69. package/dist/commands/tax-codes.js +21 -21
  70. package/dist/commands/tax-matrix.js +21 -21
  71. package/dist/commands/tenants.js +23 -23
  72. package/dist/commands/text-templates.js +21 -21
  73. package/dist/commands/tools.js +21 -21
  74. package/dist/commands/vat-declarations.js +21 -21
  75. package/dist/commands/vendor-invoices.js +25 -25
  76. package/dist/commands/webhooks.js +21 -21
  77. package/dist/commands/workflows.js +21 -21
  78. package/dist/lib/api-client.d.ts +8 -0
  79. package/dist/lib/api-client.js +31 -6
  80. package/dist/lib/auth.d.ts +8 -0
  81. package/dist/lib/auth.js +51 -7
  82. package/dist/lib/base-api-command.js +6 -4
  83. package/dist/lib/config.d.ts +31 -1
  84. package/dist/lib/config.js +31 -0
  85. package/oclif.manifest.json +1099 -443
  86. package/package.json +2 -2
@@ -1,6 +1,26 @@
1
1
  import axios from 'axios';
2
- import { getValidToken } from './auth.js';
3
- import { getBaseUrls, getCurrentContext } from './config.js';
2
+ import { getAuthHeaders } from './auth.js';
3
+ import { getAuthType, getBaseUrls, getCurrentContext } from './config.js';
4
+ /**
5
+ * Normalize a --sort-by value: comma-separated fields, each optionally suffixed
6
+ * with `:ASC` or `:DESC`. Fields without an explicit direction default to DESC
7
+ * when the leaf name ends in `At`, `Date`, or `Time` (dates read newest-first);
8
+ * everything else defaults to ASC. Returned as an array so axios expands it
9
+ * into repeated `sortBy=a:ASC&sortBy=b:DESC` params.
10
+ */
11
+ export function normalizeSortBy(input) {
12
+ return input
13
+ .split(',')
14
+ .map((part) => part.trim())
15
+ .filter(Boolean)
16
+ .map((part) => {
17
+ if (part.includes(':'))
18
+ return part;
19
+ const leaf = part.split('.').pop() ?? part;
20
+ const dateLike = /(?:At|Date|Time)$/.test(leaf);
21
+ return `${part}:${dateLike ? 'DESC' : 'ASC'}`;
22
+ });
23
+ }
4
24
  export class ApiError extends Error {
5
25
  status;
6
26
  data;
@@ -19,14 +39,15 @@ export async function createApiClient(contextName, options = false) {
19
39
  if (!context) {
20
40
  throw new Error('No context configured. Run: aderis auth login --new');
21
41
  }
22
- const token = await getValidToken(context.name);
42
+ const authHeaders = await getAuthHeaders(context.name);
23
43
  const { api: baseURL } = getBaseUrls(context.environment);
24
44
  const client = axios.create({
25
45
  baseURL,
26
46
  headers: {
27
47
  Accept: binary ? '*/*' : (acceptAny ? '*/*' : 'application/json'),
28
- Authorization: `Bearer ${token}`,
29
48
  'Content-Type': 'application/json',
49
+ // Either `Authorization: Bearer <token>` or `x-api-key: <key>`.
50
+ ...authHeaders,
30
51
  },
31
52
  // Serialize arrays as repeated params (key=v1&key=v2) instead of bracket format (key[]=v1)
32
53
  paramsSerializer: { indexes: null },
@@ -36,9 +57,13 @@ export async function createApiClient(contextName, options = false) {
36
57
  // Response interceptor for error handling
37
58
  client.interceptors.response.use((response) => response, (error) => {
38
59
  if (error.response) {
39
- // Handle 401 with a clear message
60
+ // Handle 401 with a clear message, pointing at the right remedy for the
61
+ // context's auth type — re-login only helps OAuth contexts.
40
62
  if (error.response.status === 401) {
41
- throw new ApiError('Session expired. Please run: aderis auth login', 401, error.response.data);
63
+ const message = getAuthType(context) === 'api_key'
64
+ ? `API key for context "${context.name}" was rejected. Check the key with: aderis config set-api-key --context ${context.name}`
65
+ : 'Session expired. Please run: aderis auth login';
66
+ throw new ApiError(message, 401, error.response.data);
42
67
  }
43
68
  const message = extractErrorMessage(error.response.data);
44
69
  throw new ApiError(message, error.response.status, error.response.data);
@@ -4,6 +4,14 @@ export declare function login(context: Context, options?: {
4
4
  }): Promise<TokenData>;
5
5
  export declare function refreshTokens(contextName: string): Promise<TokenData>;
6
6
  export declare function getValidToken(contextName: string): Promise<string>;
7
+ /**
8
+ * Build the auth headers for a context: a bearer token for `oauth2` contexts
9
+ * (refreshing it first if needed) or `x-api-key` for `api_key` contexts.
10
+ *
11
+ * This is the single place that decides how credentials reach the API — callers
12
+ * should never read `tokens` or `api_key` off a context to build headers.
13
+ */
14
+ export declare function getAuthHeaders(contextName: string): Promise<Record<string, string>>;
7
15
  export declare function testAuth(contextName: string): Promise<{
8
16
  email: string;
9
17
  name?: string;
package/dist/lib/auth.js CHANGED
@@ -4,7 +4,7 @@ import * as crypto from 'node:crypto';
4
4
  import * as http from 'node:http';
5
5
  import { URL, URLSearchParams } from 'node:url';
6
6
  import open from 'open';
7
- import { getBaseUrls, getContext, saveCachedTenants, saveTokens, } from './config.js';
7
+ import { getAuthType, getBaseUrls, getContext, missingCredentialsMessage, saveCachedTenants, saveTokens, } from './config.js';
8
8
  function buildValidatedUrl(baseUrl, path) {
9
9
  try {
10
10
  // Minimal path validation
@@ -132,6 +132,23 @@ function renderAuthPage(type, title, message) {
132
132
  </body>
133
133
  </html>`;
134
134
  }
135
+ /**
136
+ * Guard for flows that only exist for OAuth contexts (browser login, token
137
+ * refresh, Frontegg tenant switching). API keys are issued per tenant, so there
138
+ * is nothing to log into or switch.
139
+ */
140
+ function requireOAuthContext(context, action) {
141
+ if (getAuthType(context) === 'api_key') {
142
+ throw new Error(`${action} requires OAuth. Context "${context.name}" authenticates with an API key, which is already scoped to one tenant.`);
143
+ }
144
+ }
145
+ /** `client_id` is optional on Context (API-key contexts have none), so OAuth flows must assert it. */
146
+ function requireClientId(context) {
147
+ if (!context.client_id) {
148
+ throw new Error(`Context "${context.name}" has no OAuth client ID. Recreate it with: aderis config add`);
149
+ }
150
+ return context.client_id;
151
+ }
135
152
  function generatePKCE() {
136
153
  const verifier = crypto.randomBytes(32).toString('base64url');
137
154
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
@@ -143,7 +160,7 @@ function generateState() {
143
160
  function buildAuthorizeUrl(context, pkce, state, options = {}) {
144
161
  const { auth: authUrl } = getBaseUrls(context.environment);
145
162
  const url = new URL(`${authUrl}/oauth/authorize`);
146
- url.searchParams.set('client_id', context.client_id);
163
+ url.searchParams.set('client_id', requireClientId(context));
147
164
  url.searchParams.set('redirect_uri', REDIRECT_URI);
148
165
  url.searchParams.set('response_type', 'code');
149
166
  url.searchParams.set('state', state);
@@ -155,6 +172,7 @@ function buildAuthorizeUrl(context, pkce, state, options = {}) {
155
172
  return url.toString();
156
173
  }
157
174
  export async function login(context, options = {}) {
175
+ requireOAuthContext(context, 'Browser login');
158
176
  const pkce = generatePKCE();
159
177
  const state = generateState();
160
178
  const authorizationUrl = buildAuthorizeUrl(context, pkce, state, options);
@@ -252,7 +270,7 @@ async function exchangeCodeForTokens(context, code, codeVerifier) {
252
270
  const { auth: authUrl } = getBaseUrls(context.environment);
253
271
  const params = new URLSearchParams();
254
272
  params.set('grant_type', 'authorization_code');
255
- params.set('client_id', context.client_id);
273
+ params.set('client_id', requireClientId(context));
256
274
  params.set('code', code);
257
275
  params.set('redirect_uri', REDIRECT_URI);
258
276
  params.set('code_verifier', codeVerifier);
@@ -279,13 +297,14 @@ export async function refreshTokens(contextName) {
279
297
  if (!context) {
280
298
  throw new Error(`Context "${contextName}" not found`);
281
299
  }
300
+ requireOAuthContext(context, 'Token refresh');
282
301
  if (!context.tokens?.refresh_token) {
283
302
  throw new Error('No refresh token available. Please login again.');
284
303
  }
285
304
  const { auth: authUrl } = getBaseUrls(context.environment);
286
305
  const params = new URLSearchParams();
287
306
  params.set('grant_type', 'refresh_token');
288
- params.set('client_id', context.client_id);
307
+ params.set('client_id', requireClientId(context));
289
308
  params.set('refresh_token', context.tokens.refresh_token);
290
309
  if (context.client_secret) {
291
310
  params.set('client_secret', context.client_secret);
@@ -321,6 +340,9 @@ export async function getValidToken(contextName) {
321
340
  if (!context) {
322
341
  throw new Error(`Context "${contextName}" not found`);
323
342
  }
343
+ if (getAuthType(context) === 'api_key') {
344
+ throw new Error(`Context "${contextName}" authenticates with an API key and has no access token. Use getAuthHeaders() instead.`);
345
+ }
324
346
  if (!context.tokens?.access_token) {
325
347
  throw new Error('Not logged in. Please run: aderis auth login');
326
348
  }
@@ -334,17 +356,36 @@ export async function getValidToken(contextName) {
334
356
  }
335
357
  return context.tokens.access_token;
336
358
  }
359
+ /**
360
+ * Build the auth headers for a context: a bearer token for `oauth2` contexts
361
+ * (refreshing it first if needed) or `x-api-key` for `api_key` contexts.
362
+ *
363
+ * This is the single place that decides how credentials reach the API — callers
364
+ * should never read `tokens` or `api_key` off a context to build headers.
365
+ */
366
+ export async function getAuthHeaders(contextName) {
367
+ const context = getContext(contextName);
368
+ if (!context) {
369
+ throw new Error(`Context "${contextName}" not found`);
370
+ }
371
+ if (getAuthType(context) === 'api_key') {
372
+ if (!context.api_key) {
373
+ throw new Error(missingCredentialsMessage(context));
374
+ }
375
+ return { 'x-api-key': context.api_key };
376
+ }
377
+ return { Authorization: `Bearer ${await getValidToken(contextName)}` };
378
+ }
337
379
  export async function testAuth(contextName) {
338
380
  const context = getContext(contextName);
339
381
  if (!context) {
340
382
  throw new Error(`Context "${contextName}" not found`);
341
383
  }
342
- const token = await getValidToken(contextName);
343
384
  const { api: apiUrl } = getBaseUrls(context.environment);
344
385
  const response = await axios.get(buildValidatedUrl(apiUrl, '/v1/auth/me'), {
345
386
  headers: {
346
387
  Accept: 'application/json',
347
- Authorization: `Bearer ${token}`,
388
+ ...(await getAuthHeaders(contextName)),
348
389
  },
349
390
  });
350
391
  return response.data;
@@ -354,6 +395,7 @@ export async function listTenants(contextName) {
354
395
  if (!context) {
355
396
  throw new Error(`Context "${contextName}" not found`);
356
397
  }
398
+ requireOAuthContext(context, 'Listing tenants');
357
399
  const token = await getValidToken(contextName);
358
400
  const { auth: authUrl } = getBaseUrls(context.environment);
359
401
  let response;
@@ -389,7 +431,8 @@ export async function getCurrentTenant(contextName) {
389
431
  if (!context) {
390
432
  throw new Error(`Context "${contextName}" not found`);
391
433
  }
392
- if (!context.tokens?.access_token) {
434
+ // API-key contexts carry no JWT, so there is no tenant to decode locally.
435
+ if (getAuthType(context) === 'api_key' || !context.tokens?.access_token) {
393
436
  return null;
394
437
  }
395
438
  const decoded = jwt.decode(context.tokens.access_token);
@@ -415,6 +458,7 @@ export async function switchTenant(contextName, tenantId) {
415
458
  if (!context) {
416
459
  throw new Error(`Context "${contextName}" not found`);
417
460
  }
461
+ requireOAuthContext(context, 'Switching tenants');
418
462
  if (!context.tokens?.refresh_token) {
419
463
  throw new Error('No refresh token available. Please login again.');
420
464
  }
@@ -1,11 +1,13 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import { ApiError, apiRequest } from './api-client.js';
3
- import { getCurrentContext } from './config.js';
3
+ import { getCurrentContext, hasCredentials, missingCredentialsMessage } from './config.js';
4
4
  export class BaseApiCommand extends Command {
5
5
  static globalFlags = {
6
6
  context: Flags.string({
7
+ aliases: ['config'],
7
8
  char: 'c',
8
- description: 'Context to use (defaults to current context)',
9
+ description: 'Context to use, overriding the current context (alias: --config)',
10
+ env: 'ADERIS_CONTEXT',
9
11
  }),
10
12
  output: Flags.string({
11
13
  char: 'o',
@@ -66,8 +68,8 @@ export class BaseApiCommand extends Command {
66
68
  if (!context) {
67
69
  this.error('No context configured. Run: aderis auth login --new');
68
70
  }
69
- if (!context.tokens) {
70
- this.error(`Not logged in to context "${context.name}". Run: aderis auth login`);
71
+ if (!hasCredentials(context)) {
72
+ this.error(missingCredentialsMessage(context));
71
73
  }
72
74
  }
73
75
  printTable(data) {
@@ -1,5 +1,15 @@
1
1
  import Conf from 'conf';
2
2
  export type Environment = 'dev' | 'live' | 'staging';
3
+ /**
4
+ * How a context authenticates against the API.
5
+ *
6
+ * - `oauth2`: browser PKCE login, sends `Authorization: Bearer <access_token>`
7
+ * - `api_key`: a long-lived key, sends `x-api-key: <key>`
8
+ *
9
+ * Contexts created before API-key support have no `auth_type` field; they are
10
+ * treated as `oauth2`, so always read it through {@link getAuthType}.
11
+ */
12
+ export type AuthType = 'api_key' | 'oauth2';
3
13
  export interface BaseUrls {
4
14
  api: string;
5
15
  auth: string;
@@ -17,8 +27,11 @@ export interface CachedTenant {
17
27
  tenantId: string;
18
28
  }
19
29
  export interface Context {
30
+ api_key?: string;
31
+ auth_type?: AuthType;
20
32
  cachedTenants?: CachedTenant[];
21
- client_id: string;
33
+ /** Only required for `oauth2` contexts; API-key contexts have no OAuth client. */
34
+ client_id?: string;
22
35
  client_secret?: string;
23
36
  environment: Environment;
24
37
  name: string;
@@ -44,5 +57,22 @@ export declare function saveCachedTenants(contextName: string, tenants: CachedTe
44
57
  export declare function getCachedTenants(contextName: string): CachedTenant[] | undefined;
45
58
  export declare function getTokens(contextName: string): null | TokenData;
46
59
  export declare function clearTokens(contextName: string): void;
60
+ export declare function saveApiKey(contextName: string, apiKey: string): void;
61
+ export declare function clearApiKey(contextName: string): void;
62
+ /**
63
+ * Auth type of a context, defaulting to `oauth2` for contexts stored before
64
+ * API-key support existed.
65
+ */
66
+ export declare function getAuthType(context: Context): AuthType;
67
+ /** Whether a context holds the credential its auth type needs to call the API. */
68
+ export declare function hasCredentials(context: Context): boolean;
69
+ /**
70
+ * The error to show when a context can't authenticate. Kept here so every
71
+ * command — including the ~60 generated ones — points at the right remedy for
72
+ * the context's auth type instead of always suggesting `auth login`.
73
+ */
74
+ export declare function missingCredentialsMessage(context: Context): string;
75
+ /** Redact a key for display: first and last 4 characters only. */
76
+ export declare function maskApiKey(apiKey: string): string;
47
77
  export declare function getBaseUrls(environment: Environment): BaseUrls;
48
78
  export { config };
@@ -133,6 +133,37 @@ export function getTokens(contextName) {
133
133
  export function clearTokens(contextName) {
134
134
  updateContext(contextName, { tokens: undefined });
135
135
  }
136
+ export function saveApiKey(contextName, apiKey) {
137
+ updateContext(contextName, { api_key: apiKey, auth_type: 'api_key' });
138
+ }
139
+ export function clearApiKey(contextName) {
140
+ updateContext(contextName, { api_key: undefined });
141
+ }
142
+ /**
143
+ * Auth type of a context, defaulting to `oauth2` for contexts stored before
144
+ * API-key support existed.
145
+ */
146
+ export function getAuthType(context) {
147
+ return context.auth_type ?? 'oauth2';
148
+ }
149
+ /** Whether a context holds the credential its auth type needs to call the API. */
150
+ export function hasCredentials(context) {
151
+ return getAuthType(context) === 'api_key' ? Boolean(context.api_key) : Boolean(context.tokens);
152
+ }
153
+ /**
154
+ * The error to show when a context can't authenticate. Kept here so every
155
+ * command — including the ~60 generated ones — points at the right remedy for
156
+ * the context's auth type instead of always suggesting `auth login`.
157
+ */
158
+ export function missingCredentialsMessage(context) {
159
+ return getAuthType(context) === 'api_key'
160
+ ? `No API key set for context "${context.name}". Run: aderis config set-api-key --context ${context.name}`
161
+ : `Not logged in to context "${context.name}". Run: aderis auth login --context ${context.name}`;
162
+ }
163
+ /** Redact a key for display: first and last 4 characters only. */
164
+ export function maskApiKey(apiKey) {
165
+ return apiKey.length <= 8 ? '*'.repeat(apiKey.length) : `${apiKey.slice(0, 4)}${'*'.repeat(8)}${apiKey.slice(-4)}`;
166
+ }
136
167
  export function getBaseUrls(environment) {
137
168
  return BASE_URLS[environment];
138
169
  }