@aderis-erp/cli 1.0.14 → 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 (83) hide show
  1. package/dist/commands/access-groups.js +17 -17
  2. package/dist/commands/account-transaction-allocations.js +17 -17
  3. package/dist/commands/account-transactions.js +19 -19
  4. package/dist/commands/accounts.js +19 -19
  5. package/dist/commands/agent-platform.js +19 -18
  6. package/dist/commands/analytics.d.ts +40 -0
  7. package/dist/commands/{documents.js → analytics.js} +29 -39
  8. package/dist/commands/audit-log.js +17 -17
  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 +17 -17
  16. package/dist/commands/bank-integrations.js +17 -17
  17. package/dist/commands/business-partners.js +25 -22
  18. package/dist/commands/chat.js +17 -17
  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 +17 -17
  27. package/dist/commands/delivery-notes.js +22 -21
  28. package/dist/commands/dunning-configs.js +17 -17
  29. package/dist/commands/dunning-reminders.js +17 -17
  30. package/dist/commands/dunning-runs.js +17 -17
  31. package/dist/commands/email-transactions.js +17 -17
  32. package/dist/commands/employees.js +19 -19
  33. package/dist/commands/entity-configurations.js +17 -17
  34. package/dist/commands/event-processor-mappings.js +17 -17
  35. package/dist/commands/{documents.d.ts → files.d.ts} +1 -1
  36. package/dist/commands/{files-internal.js → files.js} +35 -29
  37. package/dist/commands/health.js +17 -17
  38. package/dist/commands/inbox.js +17 -17
  39. package/dist/commands/integrations.js +17 -17
  40. package/dist/commands/item-categories.js +17 -17
  41. package/dist/commands/items.js +19 -19
  42. package/dist/commands/journal-lines.js +17 -17
  43. package/dist/commands/journals.js +21 -20
  44. package/dist/commands/ledgers.js +21 -18
  45. package/dist/commands/office-locations.js +17 -17
  46. package/dist/commands/payment-methods.js +17 -17
  47. package/dist/commands/payment-runs.js +19 -18
  48. package/dist/commands/payment-terms.js +17 -17
  49. package/dist/commands/payments.js +17 -17
  50. package/dist/commands/price-lists.js +17 -17
  51. package/dist/commands/processor-mappings.js +17 -17
  52. package/dist/commands/processors.js +17 -17
  53. package/dist/commands/purchase-order-types.js +17 -17
  54. package/dist/commands/purchase-orders.d.ts +1 -0
  55. package/dist/commands/purchase-orders.js +28 -21
  56. package/dist/commands/reports.js +17 -17
  57. package/dist/commands/salary-types.js +17 -17
  58. package/dist/commands/sales-invoices.js +27 -25
  59. package/dist/commands/sales-orders.js +19 -19
  60. package/dist/commands/saved-views.js +17 -17
  61. package/dist/commands/scheduled-processor-mappings.js +17 -17
  62. package/dist/commands/schedules.js +17 -17
  63. package/dist/commands/switch.js +10 -3
  64. package/dist/commands/{files-internal.d.ts → tag-configurations.d.ts} +1 -1
  65. package/dist/commands/tag-configurations.js +1178 -0
  66. package/dist/commands/tags.js +17 -17
  67. package/dist/commands/tax-codes.js +17 -17
  68. package/dist/commands/tax-matrix.js +17 -17
  69. package/dist/commands/tenants.js +17 -17
  70. package/dist/commands/text-templates.js +17 -17
  71. package/dist/commands/tools.js +17 -17
  72. package/dist/commands/vat-declarations.js +17 -17
  73. package/dist/commands/vendor-invoices.js +21 -21
  74. package/dist/commands/webhooks.js +17 -17
  75. package/dist/commands/workflows.js +17 -17
  76. package/dist/lib/api-client.js +11 -6
  77. package/dist/lib/auth.d.ts +8 -0
  78. package/dist/lib/auth.js +51 -7
  79. package/dist/lib/base-api-command.js +6 -4
  80. package/dist/lib/config.d.ts +31 -1
  81. package/dist/lib/config.js +31 -0
  82. package/oclif.manifest.json +729 -247
  83. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
3
- import { getCurrentContext, getContext, getBaseUrls } from '../lib/config.js';
3
+ import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
@@ -9,12 +9,12 @@ const NESTED_IMPORTS = ["line-items", "notes", "files", "payments"];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "submit", "approve", "decline", "book", "rebuild-journal", "cancel", "confirm-cancellation", "change-vendor", "recalculate-payment-status", "validate", "create-line-items", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-payments", "create-payments", "get-payments", "update-payments", "delete-payments", "payments-approve", "payments-cancel"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
11
  'list': { method: 'GET', path: '/v1/vendor-invoices', summary: 'Get all vendor invoices with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/vendor-invoices', summary: 'Create a new vendor invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'payments'] },
12
+ 'create': { method: 'POST', path: '/v1/vendor-invoices', summary: 'Create a new vendor invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'payments'] },
13
13
  'get': { method: 'GET', path: '/v1/vendor-invoices/{id}', summary: 'Get a vendor invoice by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/vendor-invoices/{id}', summary: 'Update a vendor invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'removeFileIds'] },
14
+ 'update': { method: 'PATCH', path: '/v1/vendor-invoices/{id}', summary: 'Update a vendor invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'removeFileIds'] },
15
15
  'delete': { method: 'DELETE', path: '/v1/vendor-invoices/{id}', summary: 'Delete a vendor invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
16
  'submit': { method: 'POST', path: '/v1/vendor-invoices/{id}/submit', summary: 'Submit a vendor invoice for approval (transitions DRAFT to PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'approve': { method: 'POST', path: '/v1/vendor-invoices/{id}/approve', summary: 'Approve a vendor invoice (transitions DRAFT/PENDING_APPROVAL to APPROVED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'removeFileIds'] },
17
+ 'approve': { method: 'POST', path: '/v1/vendor-invoices/{id}/approve', summary: 'Approve a vendor invoice (transitions DRAFT/PENDING_APPROVAL to APPROVED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'removeFileIds'] },
18
18
  'decline': { method: 'POST', path: '/v1/vendor-invoices/{id}/decline', summary: 'Decline a vendor invoice (transitions PENDING_APPROVAL to DECLINED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
19
  'book': { method: 'POST', path: '/v1/vendor-invoices/{id}/book', summary: 'Manually book a vendor invoice (dispatch to ledger-booking-agent)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
20
  'rebuild-journal': { method: 'POST', path: '/v1/vendor-invoices/{id}/rebuild-journal', summary: 'Rebuild a vendor invoice booking journal (dispatch to ledger-booking-agent)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
@@ -22,7 +22,7 @@ const ENDPOINTS = {
22
22
  'confirm-cancellation': { method: 'POST', path: '/v1/vendor-invoices/{id}/confirm-cancellation', summary: 'Confirm bank cancellation for a vendor invoice (PENDING_CANCELLATION to CANCELLED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
23
  'change-vendor': { method: 'POST', path: '/v1/vendor-invoices/{id}/change-vendor', summary: 'Change the vendor on a vendor invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
24
  'recalculate-payment-status': { method: 'POST', path: '/v1/vendor-invoices/{id}/recalculate-payment-status', summary: 'Recalculate a vendor invoice\'s open amount and payment status', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
25
- 'validate': { method: 'POST', path: '/v1/vendor-invoices/{id}/validate', summary: 'Validate a vendor invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'removeFileIds'] },
25
+ 'validate': { method: 'POST', path: '/v1/vendor-invoices/{id}/validate', summary: 'Validate a vendor invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'removeFileIds'] },
26
26
  'create-line-items': { method: 'POST', path: '/v1/vendor-invoices/{invoiceId}/line-items', summary: 'Add a line item to a vendor invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
27
  'update-line-items': { method: 'PATCH', path: '/v1/vendor-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a vendor invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
28
28
  'delete-line-items': { method: 'DELETE', path: '/v1/vendor-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Remove a line item from a vendor invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
@@ -60,7 +60,7 @@ export default class VendorInvoices extends Command {
60
60
  id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
61
61
  };
62
62
  static flags = {
63
- context: Flags.string({ char: 'c', description: 'Context to use' }),
63
+ context: Flags.string({ char: 'c', aliases: ['config'], env: 'ADERIS_CONTEXT', description: 'Context to use for this command, overriding the current context (alias: --config)' }),
64
64
  output: Flags.string({ char: 'o', description: 'Output format (default: table for import, json for other commands)', options: ['json', 'table', 'csv'] }),
65
65
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
66
66
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
@@ -136,9 +136,9 @@ export default class VendorInvoices extends Command {
136
136
  }
137
137
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
138
138
  if (!context)
139
- this.error('No context configured. Run: aderis auth login --new');
140
- if (!context.tokens)
141
- this.error('Not logged in. Run: aderis auth login');
139
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
140
+ if (!hasCredentials(context))
141
+ this.error(missingCredentialsMessage(context));
142
142
  const queryParams = {};
143
143
  if (flags.page)
144
144
  queryParams.page = flags.page;
@@ -381,9 +381,9 @@ export default class VendorInvoices extends Command {
381
381
  async runImport(flags) {
382
382
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
383
383
  if (!context)
384
- this.error('No context configured. Run: aderis auth login --new');
385
- if (!context.tokens)
386
- this.error('Not logged in. Run: aderis auth login');
384
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
385
+ if (!hasCredentials(context))
386
+ this.error(missingCredentialsMessage(context));
387
387
  // Get data from file, --data, or --set flags
388
388
  let items = [];
389
389
  if (flags.file) {
@@ -593,9 +593,9 @@ export default class VendorInvoices extends Command {
593
593
  async runExport(flags) {
594
594
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
595
595
  if (!context)
596
- this.error('No context configured. Run: aderis auth login --new');
597
- if (!context.tokens)
598
- this.error('Not logged in. Run: aderis auth login');
596
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
597
+ if (!hasCredentials(context))
598
+ this.error(missingCredentialsMessage(context));
599
599
  const listEndpoint = ENDPOINTS['list'];
600
600
  if (!listEndpoint)
601
601
  this.error('Export requires a list endpoint');
@@ -687,9 +687,9 @@ export default class VendorInvoices extends Command {
687
687
  async runCamt053Import(flags) {
688
688
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
689
689
  if (!context)
690
- this.error('No context configured. Run: aderis auth login --new');
691
- if (!context.tokens)
692
- this.error('Not logged in. Run: aderis auth login');
690
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
691
+ if (!hasCredentials(context))
692
+ this.error(missingCredentialsMessage(context));
693
693
  if (!flags.file) {
694
694
  this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
695
695
  }
@@ -777,9 +777,9 @@ export default class VendorInvoices extends Command {
777
777
  }
778
778
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
779
779
  if (!context)
780
- this.error('No context configured. Run: aderis auth login --new');
781
- if (!context.tokens)
782
- this.error('Not logged in. Run: aderis auth login');
780
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
781
+ if (!hasCredentials(context))
782
+ this.error(missingCredentialsMessage(context));
783
783
  // Get data from file, --data, or --set flags
784
784
  let items = [];
785
785
  if (flags.file) {
@@ -1,6 +1,6 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
3
- import { getCurrentContext, getContext, getBaseUrls } from '../lib/config.js';
3
+ import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
@@ -31,7 +31,7 @@ export default class Webhooks extends Command {
31
31
  id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
32
32
  };
33
33
  static flags = {
34
- context: Flags.string({ char: 'c', description: 'Context to use' }),
34
+ context: Flags.string({ char: 'c', aliases: ['config'], env: 'ADERIS_CONTEXT', description: 'Context to use for this command, overriding the current context (alias: --config)' }),
35
35
  output: Flags.string({ char: 'o', description: 'Output format (default: table for import, json for other commands)', options: ['json', 'table', 'csv'] }),
36
36
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
37
37
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
@@ -103,9 +103,9 @@ export default class Webhooks extends Command {
103
103
  }
104
104
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
105
105
  if (!context)
106
- this.error('No context configured. Run: aderis auth login --new');
107
- if (!context.tokens)
108
- this.error('Not logged in. Run: aderis auth login');
106
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
107
+ if (!hasCredentials(context))
108
+ this.error(missingCredentialsMessage(context));
109
109
  const queryParams = {};
110
110
  if (flags.page)
111
111
  queryParams.page = flags.page;
@@ -348,9 +348,9 @@ export default class Webhooks extends Command {
348
348
  async runImport(flags) {
349
349
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
350
350
  if (!context)
351
- this.error('No context configured. Run: aderis auth login --new');
352
- if (!context.tokens)
353
- this.error('Not logged in. Run: aderis auth login');
351
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
352
+ if (!hasCredentials(context))
353
+ this.error(missingCredentialsMessage(context));
354
354
  // Get data from file, --data, or --set flags
355
355
  let items = [];
356
356
  if (flags.file) {
@@ -560,9 +560,9 @@ export default class Webhooks extends Command {
560
560
  async runExport(flags) {
561
561
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
562
562
  if (!context)
563
- this.error('No context configured. Run: aderis auth login --new');
564
- if (!context.tokens)
565
- this.error('Not logged in. Run: aderis auth login');
563
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
564
+ if (!hasCredentials(context))
565
+ this.error(missingCredentialsMessage(context));
566
566
  const listEndpoint = ENDPOINTS['list'];
567
567
  if (!listEndpoint)
568
568
  this.error('Export requires a list endpoint');
@@ -654,9 +654,9 @@ export default class Webhooks extends Command {
654
654
  async runCamt053Import(flags) {
655
655
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
656
656
  if (!context)
657
- this.error('No context configured. Run: aderis auth login --new');
658
- if (!context.tokens)
659
- this.error('Not logged in. Run: aderis auth login');
657
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
658
+ if (!hasCredentials(context))
659
+ this.error(missingCredentialsMessage(context));
660
660
  if (!flags.file) {
661
661
  this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
662
662
  }
@@ -744,9 +744,9 @@ export default class Webhooks extends Command {
744
744
  }
745
745
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
746
746
  if (!context)
747
- this.error('No context configured. Run: aderis auth login --new');
748
- if (!context.tokens)
749
- this.error('Not logged in. Run: aderis auth login');
747
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
748
+ if (!hasCredentials(context))
749
+ this.error(missingCredentialsMessage(context));
750
750
  // Get data from file, --data, or --set flags
751
751
  let items = [];
752
752
  if (flags.file) {
@@ -1,6 +1,6 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
3
- import { getCurrentContext, getContext, getBaseUrls } from '../lib/config.js';
3
+ import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = false;
6
6
  const HAS_EXPORT = true;
@@ -23,7 +23,7 @@ export default class Workflows extends Command {
23
23
  id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
24
24
  };
25
25
  static flags = {
26
- context: Flags.string({ char: 'c', description: 'Context to use' }),
26
+ context: Flags.string({ char: 'c', aliases: ['config'], env: 'ADERIS_CONTEXT', description: 'Context to use for this command, overriding the current context (alias: --config)' }),
27
27
  output: Flags.string({ char: 'o', description: 'Output format (default: table for import, json for other commands)', options: ['json', 'table', 'csv'] }),
28
28
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
29
29
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
@@ -95,9 +95,9 @@ export default class Workflows extends Command {
95
95
  }
96
96
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
97
97
  if (!context)
98
- this.error('No context configured. Run: aderis auth login --new');
99
- if (!context.tokens)
100
- this.error('Not logged in. Run: aderis auth login');
98
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
99
+ if (!hasCredentials(context))
100
+ this.error(missingCredentialsMessage(context));
101
101
  const queryParams = {};
102
102
  if (flags.page)
103
103
  queryParams.page = flags.page;
@@ -340,9 +340,9 @@ export default class Workflows extends Command {
340
340
  async runImport(flags) {
341
341
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
342
342
  if (!context)
343
- this.error('No context configured. Run: aderis auth login --new');
344
- if (!context.tokens)
345
- this.error('Not logged in. Run: aderis auth login');
343
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
344
+ if (!hasCredentials(context))
345
+ this.error(missingCredentialsMessage(context));
346
346
  // Get data from file, --data, or --set flags
347
347
  let items = [];
348
348
  if (flags.file) {
@@ -552,9 +552,9 @@ export default class Workflows extends Command {
552
552
  async runExport(flags) {
553
553
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
554
554
  if (!context)
555
- this.error('No context configured. Run: aderis auth login --new');
556
- if (!context.tokens)
557
- this.error('Not logged in. Run: aderis auth login');
555
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
556
+ if (!hasCredentials(context))
557
+ this.error(missingCredentialsMessage(context));
558
558
  const listEndpoint = ENDPOINTS['list'];
559
559
  if (!listEndpoint)
560
560
  this.error('Export requires a list endpoint');
@@ -646,9 +646,9 @@ export default class Workflows extends Command {
646
646
  async runCamt053Import(flags) {
647
647
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
648
648
  if (!context)
649
- this.error('No context configured. Run: aderis auth login --new');
650
- if (!context.tokens)
651
- this.error('Not logged in. Run: aderis auth login');
649
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
650
+ if (!hasCredentials(context))
651
+ this.error(missingCredentialsMessage(context));
652
652
  if (!flags.file) {
653
653
  this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
654
654
  }
@@ -736,9 +736,9 @@ export default class Workflows extends Command {
736
736
  }
737
737
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
738
738
  if (!context)
739
- this.error('No context configured. Run: aderis auth login --new');
740
- if (!context.tokens)
741
- this.error('Not logged in. Run: aderis auth login');
739
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
740
+ if (!hasCredentials(context))
741
+ this.error(missingCredentialsMessage(context));
742
742
  // Get data from file, --data, or --set flags
743
743
  let items = [];
744
744
  if (flags.file) {
@@ -1,6 +1,6 @@
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
4
  /**
5
5
  * Normalize a --sort-by value: comma-separated fields, each optionally suffixed
6
6
  * with `:ASC` or `:DESC`. Fields without an explicit direction default to DESC
@@ -39,14 +39,15 @@ export async function createApiClient(contextName, options = false) {
39
39
  if (!context) {
40
40
  throw new Error('No context configured. Run: aderis auth login --new');
41
41
  }
42
- const token = await getValidToken(context.name);
42
+ const authHeaders = await getAuthHeaders(context.name);
43
43
  const { api: baseURL } = getBaseUrls(context.environment);
44
44
  const client = axios.create({
45
45
  baseURL,
46
46
  headers: {
47
47
  Accept: binary ? '*/*' : (acceptAny ? '*/*' : 'application/json'),
48
- Authorization: `Bearer ${token}`,
49
48
  'Content-Type': 'application/json',
49
+ // Either `Authorization: Bearer <token>` or `x-api-key: <key>`.
50
+ ...authHeaders,
50
51
  },
51
52
  // Serialize arrays as repeated params (key=v1&key=v2) instead of bracket format (key[]=v1)
52
53
  paramsSerializer: { indexes: null },
@@ -56,9 +57,13 @@ export async function createApiClient(contextName, options = false) {
56
57
  // Response interceptor for error handling
57
58
  client.interceptors.response.use((response) => response, (error) => {
58
59
  if (error.response) {
59
- // 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.
60
62
  if (error.response.status === 401) {
61
- 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);
62
67
  }
63
68
  const message = extractErrorMessage(error.response.data);
64
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
  }