@aderis-erp/cli 1.0.14 → 1.0.17

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 (167) hide show
  1. package/README.md +30 -8
  2. package/dist/commands/access-groups.d.ts +0 -1
  3. package/dist/commands/access-groups.js +22 -78
  4. package/dist/commands/account-transaction-allocations.d.ts +0 -1
  5. package/dist/commands/account-transaction-allocations.js +22 -78
  6. package/dist/commands/account-transactions.d.ts +0 -1
  7. package/dist/commands/account-transactions.js +28 -82
  8. package/dist/commands/accounts.d.ts +0 -1
  9. package/dist/commands/accounts.js +24 -80
  10. package/dist/commands/agent-platform.d.ts +0 -1
  11. package/dist/commands/agent-platform.js +24 -79
  12. package/dist/commands/analytics.d.ts +39 -0
  13. package/dist/commands/analytics.js +1113 -0
  14. package/dist/commands/{documents.d.ts → api-keys.d.ts} +1 -2
  15. package/dist/commands/api-keys.js +1117 -0
  16. package/dist/commands/audit-log.d.ts +0 -1
  17. package/dist/commands/audit-log.js +22 -78
  18. package/dist/commands/auth/login.d.ts +9 -0
  19. package/dist/commands/auth/login.js +83 -21
  20. package/dist/commands/auth/logout.d.ts +6 -0
  21. package/dist/commands/auth/logout.js +23 -6
  22. package/dist/commands/auth/status.js +46 -23
  23. package/dist/commands/auth/tenant.js +15 -9
  24. package/dist/commands/auth.d.ts +0 -1
  25. package/dist/commands/auth.js +37 -51
  26. package/dist/commands/bank-accounts.d.ts +0 -1
  27. package/dist/commands/bank-accounts.js +22 -78
  28. package/dist/commands/bank-integrations.d.ts +0 -1
  29. package/dist/commands/bank-integrations.js +22 -78
  30. package/dist/commands/business-partners.d.ts +1 -1
  31. package/dist/commands/business-partners.js +38 -84
  32. package/dist/commands/chat.d.ts +0 -1
  33. package/dist/commands/chat.js +22 -78
  34. package/dist/commands/config/add.d.ts +12 -0
  35. package/dist/commands/config/add.js +97 -17
  36. package/dist/commands/config/current.js +12 -4
  37. package/dist/commands/config/list.js +12 -4
  38. package/dist/commands/config/set-api-key.d.ts +10 -0
  39. package/dist/commands/config/set-api-key.js +49 -0
  40. package/dist/commands/config/use.js +3 -4
  41. package/dist/commands/credit-card-accounts.d.ts +0 -1
  42. package/dist/commands/credit-card-accounts.js +22 -78
  43. package/dist/commands/delivery-notes.d.ts +0 -1
  44. package/dist/commands/delivery-notes.js +29 -84
  45. package/dist/commands/dunning-configs.d.ts +0 -1
  46. package/dist/commands/dunning-configs.js +22 -78
  47. package/dist/commands/dunning-reminders.d.ts +0 -1
  48. package/dist/commands/dunning-reminders.js +22 -78
  49. package/dist/commands/dunning-runs.d.ts +0 -1
  50. package/dist/commands/dunning-runs.js +22 -78
  51. package/dist/commands/email-transactions.d.ts +0 -1
  52. package/dist/commands/email-transactions.js +22 -78
  53. package/dist/commands/employees.d.ts +0 -1
  54. package/dist/commands/employees.js +24 -80
  55. package/dist/commands/entity-configurations.d.ts +0 -1
  56. package/dist/commands/entity-configurations.js +22 -78
  57. package/dist/commands/event-processor-mappings.d.ts +0 -1
  58. package/dist/commands/event-processor-mappings.js +22 -78
  59. package/dist/commands/{files-internal.d.ts → files.d.ts} +1 -2
  60. package/dist/commands/{files-internal.js → files.js} +40 -90
  61. package/dist/commands/health.d.ts +0 -1
  62. package/dist/commands/health.js +22 -78
  63. package/dist/commands/inbox.d.ts +0 -1
  64. package/dist/commands/inbox.js +22 -78
  65. package/dist/commands/integrations.d.ts +0 -1
  66. package/dist/commands/integrations.js +22 -78
  67. package/dist/commands/item-categories.d.ts +0 -1
  68. package/dist/commands/item-categories.js +22 -78
  69. package/dist/commands/items.d.ts +0 -1
  70. package/dist/commands/items.js +24 -80
  71. package/dist/commands/journal-lines.d.ts +0 -1
  72. package/dist/commands/journal-lines.js +22 -78
  73. package/dist/commands/journals.d.ts +1 -1
  74. package/dist/commands/journals.js +29 -81
  75. package/dist/commands/ledgers.d.ts +0 -1
  76. package/dist/commands/ledgers.js +26 -79
  77. package/dist/commands/nango-integrations.d.ts +39 -0
  78. package/dist/commands/nango-integrations.js +1115 -0
  79. package/dist/commands/office-locations.d.ts +0 -1
  80. package/dist/commands/office-locations.js +22 -78
  81. package/dist/commands/payment-methods.d.ts +0 -1
  82. package/dist/commands/payment-methods.js +22 -78
  83. package/dist/commands/payment-runs.d.ts +0 -1
  84. package/dist/commands/payment-runs.js +24 -79
  85. package/dist/commands/payment-terms.d.ts +0 -1
  86. package/dist/commands/payment-terms.js +22 -78
  87. package/dist/commands/payments.d.ts +0 -1
  88. package/dist/commands/payments.js +22 -78
  89. package/dist/commands/price-lists.d.ts +0 -1
  90. package/dist/commands/price-lists.js +22 -78
  91. package/dist/commands/processor-mappings.d.ts +0 -1
  92. package/dist/commands/processor-mappings.js +22 -78
  93. package/dist/commands/processors.d.ts +0 -1
  94. package/dist/commands/processors.js +22 -78
  95. package/dist/commands/purchase-order-types.d.ts +0 -1
  96. package/dist/commands/purchase-order-types.js +22 -78
  97. package/dist/commands/purchase-orders.d.ts +1 -1
  98. package/dist/commands/purchase-orders.js +35 -84
  99. package/dist/commands/reports.d.ts +0 -1
  100. package/dist/commands/reports.js +23 -79
  101. package/dist/commands/salary-types.d.ts +0 -1
  102. package/dist/commands/salary-types.js +22 -78
  103. package/dist/commands/sales-invoices.d.ts +0 -1
  104. package/dist/commands/sales-invoices.js +38 -89
  105. package/dist/commands/sales-orders.d.ts +0 -1
  106. package/dist/commands/sales-orders.js +26 -82
  107. package/dist/commands/saved-views.d.ts +0 -1
  108. package/dist/commands/saved-views.js +22 -78
  109. package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
  110. package/dist/commands/scheduled-processor-mappings.js +22 -78
  111. package/dist/commands/schedules.d.ts +0 -1
  112. package/dist/commands/schedules.js +22 -78
  113. package/dist/commands/switch.js +14 -7
  114. package/dist/commands/tag-configurations.d.ts +39 -0
  115. package/dist/commands/{documents.js → tag-configurations.js} +40 -96
  116. package/dist/commands/tags.d.ts +0 -1
  117. package/dist/commands/tags.js +22 -78
  118. package/dist/commands/tax-codes.d.ts +0 -1
  119. package/dist/commands/tax-codes.js +22 -78
  120. package/dist/commands/tax-matrix.d.ts +0 -1
  121. package/dist/commands/tax-matrix.js +22 -78
  122. package/dist/commands/tenants.d.ts +0 -1
  123. package/dist/commands/tenants.js +24 -79
  124. package/dist/commands/text-templates.d.ts +0 -1
  125. package/dist/commands/text-templates.js +22 -78
  126. package/dist/commands/tools.d.ts +0 -1
  127. package/dist/commands/tools.js +22 -78
  128. package/dist/commands/vat-declarations.d.ts +1 -1
  129. package/dist/commands/vat-declarations.js +28 -79
  130. package/dist/commands/vendor-invoices.d.ts +0 -1
  131. package/dist/commands/vendor-invoices.js +31 -86
  132. package/dist/commands/webhooks.d.ts +0 -1
  133. package/dist/commands/webhooks.js +22 -78
  134. package/dist/commands/workflows.d.ts +0 -1
  135. package/dist/commands/workflows.js +22 -78
  136. package/dist/lib/api-client.js +8 -6
  137. package/dist/lib/auth/constants.d.ts +11 -0
  138. package/dist/lib/auth/constants.js +11 -0
  139. package/dist/lib/auth/errors.d.ts +17 -0
  140. package/dist/lib/auth/errors.js +22 -0
  141. package/dist/lib/auth/flow.d.ts +27 -0
  142. package/dist/lib/auth/flow.js +163 -0
  143. package/dist/lib/auth/guards.d.ts +5 -0
  144. package/dist/lib/auth/guards.js +14 -0
  145. package/dist/lib/auth/http.d.ts +16 -0
  146. package/dist/lib/auth/http.js +49 -0
  147. package/dist/lib/auth/index.d.ts +49 -0
  148. package/dist/lib/auth/index.js +217 -0
  149. package/dist/lib/auth/keycloak-provider.d.ts +75 -0
  150. package/dist/lib/auth/keycloak-provider.js +215 -0
  151. package/dist/lib/auth/pages.d.ts +25 -0
  152. package/dist/lib/auth/pages.js +123 -0
  153. package/dist/lib/auth/pkce.d.ts +11 -0
  154. package/dist/lib/auth/pkce.js +17 -0
  155. package/dist/lib/auth/tokens.d.ts +54 -0
  156. package/dist/lib/auth/tokens.js +104 -0
  157. package/dist/lib/auth/types.d.ts +57 -0
  158. package/dist/lib/auth/types.js +1 -0
  159. package/dist/lib/auth/validated-url.d.ts +6 -0
  160. package/dist/lib/auth/validated-url.js +53 -0
  161. package/dist/lib/base-api-command.js +6 -4
  162. package/dist/lib/config.d.ts +44 -4
  163. package/dist/lib/config.js +55 -19
  164. package/oclif.manifest.json +1181 -330
  165. package/package.json +3 -2
  166. package/dist/lib/auth.d.ts +0 -35
  167. package/dist/lib/auth.js +0 -566
@@ -31,7 +31,6 @@ export default class AuditLog extends Command {
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
33
  private parseFilterValue;
34
- private filterFields;
35
34
  private sanitizeOutput;
36
35
  private getColumns;
37
36
  private getValue;
@@ -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 AuditLog 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)' }),
@@ -37,7 +37,7 @@ export default class AuditLog extends Command {
37
37
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
38
38
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
39
39
  query: Flags.string({ description: 'Additional query params as JSON' }),
40
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
40
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
41
41
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
42
42
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
43
43
  };
@@ -95,9 +95,9 @@ export default class AuditLog 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;
@@ -109,6 +109,8 @@ export default class AuditLog extends Command {
109
109
  queryParams.search = flags.search;
110
110
  if (flags['sort-by'])
111
111
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
112
+ if (flags.fields)
113
+ queryParams.fields = flags.fields;
112
114
  if (flags.query) {
113
115
  try {
114
116
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -311,11 +313,6 @@ export default class AuditLog extends Command {
311
313
  output = obj[arrayKeys[0]];
312
314
  }
313
315
  }
314
- // Filter fields if --fields flag is provided
315
- if (flags.fields) {
316
- const fieldList = flags.fields.split(',').map(f => f.trim());
317
- output = this.filterFields(output, fieldList);
318
- }
319
316
  // Sanitize output if requested
320
317
  if (flags.sanitize) {
321
318
  output = this.sanitizeOutput(output);
@@ -340,9 +337,9 @@ export default class AuditLog extends Command {
340
337
  async runImport(flags) {
341
338
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
342
339
  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');
340
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
341
+ if (!hasCredentials(context))
342
+ this.error(missingCredentialsMessage(context));
346
343
  // Get data from file, --data, or --set flags
347
344
  let items = [];
348
345
  if (flags.file) {
@@ -552,9 +549,9 @@ export default class AuditLog extends Command {
552
549
  async runExport(flags) {
553
550
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
554
551
  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');
552
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
553
+ if (!hasCredentials(context))
554
+ this.error(missingCredentialsMessage(context));
558
555
  const listEndpoint = ENDPOINTS['list'];
559
556
  if (!listEndpoint)
560
557
  this.error('Export requires a list endpoint');
@@ -570,6 +567,8 @@ export default class AuditLog extends Command {
570
567
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
571
568
  if (flags.include)
572
569
  baseParams.include = flags.include;
570
+ if (flags.fields)
571
+ baseParams.fields = flags.fields;
573
572
  if (flags.query) {
574
573
  try {
575
574
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -624,12 +623,7 @@ export default class AuditLog extends Command {
624
623
  }
625
624
  }
626
625
  process.stderr.write(' done\n');
627
- // Filter fields if specified
628
626
  let output = allItems;
629
- if (flags.fields) {
630
- const fieldList = flags.fields.split(',').map(f => f.trim());
631
- output = this.filterFields(allItems, fieldList);
632
- }
633
627
  // Sanitize output if requested
634
628
  if (flags.sanitize) {
635
629
  output = this.sanitizeOutput(output);
@@ -646,9 +640,9 @@ export default class AuditLog extends Command {
646
640
  async runCamt053Import(flags) {
647
641
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
648
642
  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');
643
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
644
+ if (!hasCredentials(context))
645
+ this.error(missingCredentialsMessage(context));
652
646
  if (!flags.file) {
653
647
  this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
654
648
  }
@@ -736,9 +730,9 @@ export default class AuditLog extends Command {
736
730
  }
737
731
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
738
732
  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');
733
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
734
+ if (!hasCredentials(context))
735
+ this.error(missingCredentialsMessage(context));
742
736
  // Get data from file, --data, or --set flags
743
737
  let items = [];
744
738
  if (flags.file) {
@@ -958,56 +952,6 @@ export default class AuditLog extends Command {
958
952
  return `\$${op}`;
959
953
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
960
954
  }
961
- filterFields(data, fields) {
962
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
963
- const pick = (obj, keys) => {
964
- // Group keys by their first segment so sibling sub-paths
965
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
966
- // and don't clobber each other on merge.
967
- const directKeys = new Set();
968
- const nestedKeys = new Map();
969
- for (const key of keys) {
970
- if (key.split('.').some(isUnsafeKey))
971
- continue;
972
- if (key.includes('.')) {
973
- const [first, ...rest] = key.split('.');
974
- if (isUnsafeKey(first))
975
- continue;
976
- const list = nestedKeys.get(first);
977
- if (list) {
978
- list.push(rest.join('.'));
979
- }
980
- else {
981
- nestedKeys.set(first, [rest.join('.')]);
982
- }
983
- }
984
- else {
985
- directKeys.add(key);
986
- }
987
- }
988
- const result = {};
989
- for (const key of directKeys) {
990
- if (key in obj)
991
- result[key] = obj[key];
992
- }
993
- for (const [first, restKeys] of nestedKeys) {
994
- const val = obj[first];
995
- if (!val || typeof val !== 'object')
996
- continue;
997
- if (Array.isArray(val)) {
998
- result[first] = val.map(item => pick(item, restKeys));
999
- }
1000
- else {
1001
- result[first] = pick(val, restKeys);
1002
- }
1003
- }
1004
- return result;
1005
- };
1006
- if (Array.isArray(data)) {
1007
- return data.map(item => pick(item, fields));
1008
- }
1009
- return pick(data, fields);
1010
- }
1011
955
  sanitizeOutput(data) {
1012
956
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1013
957
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -1,4 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type FlowHooks } from '../../lib/auth/index.js';
2
3
  export default class AuthLogin extends Command {
3
4
  static description: string;
4
5
  static examples: string[];
@@ -6,9 +7,17 @@ export default class AuthLogin extends Command {
6
7
  'client-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
8
  context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'keycloak-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
11
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
12
  new: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
13
  };
12
14
  run(): Promise<void>;
15
+ /** Prompts for a tenant; throws {@link NoTerminalError} without a terminal, which `run()` turns into the hint. */
16
+ private chooseTenant;
17
+ /** Browser-flow hooks: the defaults here, overridden by tests. */
18
+ protected flowHooks(): FlowHooks;
19
+ private printSession;
20
+ /** Validates `--keycloak-url` before the context is stored. */
21
+ private resolveKeycloakBaseUrl;
13
22
  private createNewContext;
14
23
  }
@@ -1,27 +1,33 @@
1
1
  import { input, select } from '@inquirer/prompts';
2
2
  import { Command, Flags } from '@oclif/core';
3
3
  import ora from 'ora';
4
- import { login, testAuth } from '../../lib/auth.js';
5
- import { addContext, DEFAULT_CLIENT_IDS, getContext, getCurrentContext, } from '../../lib/config.js';
4
+ import { completeTenantSelection, login, parseKeycloakBaseUrl, tenantLabel, testAuth, } from '../../lib/auth/index.js';
5
+ import { addContext, DEFAULT_KEYCLOAK_CLIENT_ID, getAuthType, getContext, getCurrentContext, } from '../../lib/config.js';
6
6
  export default class AuthLogin extends Command {
7
7
  static description = 'Login to an Aderis environment using OAuth';
8
8
  static examples = [
9
9
  '<%= config.bin %> auth login',
10
10
  '<%= config.bin %> auth login --context my-client',
11
11
  '<%= config.bin %> auth login --new --name my-client --environment dev',
12
+ '<%= config.bin %> auth login --new --name test-kc --environment test',
12
13
  ];
13
14
  static flags = {
14
15
  'client-id': Flags.string({
15
16
  description: 'OAuth client ID (uses default if not specified)',
16
17
  }),
17
18
  context: Flags.string({
19
+ aliases: ['config'],
18
20
  char: 'c',
19
- description: 'Context to login with (uses current context if not specified)',
21
+ description: 'Context to login with, overriding the current context (alias: --config)',
22
+ env: 'ADERIS_CONTEXT',
20
23
  }),
21
24
  environment: Flags.string({
22
25
  char: 'e',
23
- description: 'Environment (dev, staging, live)',
24
- options: ['dev', 'staging', 'live'],
26
+ description: 'Environment (dev, test, staging, live)',
27
+ options: ['dev', 'test', 'staging', 'live'],
28
+ }),
29
+ 'keycloak-url': Flags.string({
30
+ description: 'Keycloak origin for a new context instead of the default host for the environment (--new only; https, allowlisted)',
25
31
  }),
26
32
  name: Flags.string({
27
33
  description: 'Name for the new context (required with --new)',
@@ -35,6 +41,9 @@ export default class AuthLogin extends Command {
35
41
  async run() {
36
42
  const { flags } = await this.parse(AuthLogin);
37
43
  let context = null;
44
+ if (flags['keycloak-url'] && !flags.new) {
45
+ this.error('--keycloak-url only applies when creating a context with --new; an existing context keeps its stored Keycloak URL.');
46
+ }
38
47
  if (flags.new) {
39
48
  context = await this.createNewContext(flags);
40
49
  }
@@ -51,25 +60,70 @@ export default class AuthLogin extends Command {
51
60
  context = await this.createNewContext(flags);
52
61
  }
53
62
  }
63
+ // API-key contexts have no browser flow; sending them through OAuth would
64
+ // fail deeper in with a less useful error.
65
+ if (getAuthType(context) === 'api_key') {
66
+ this.error(`Context "${context.name}" authenticates with an API key, so there is nothing to log in to.\n` +
67
+ `Set or rotate the key with: aderis config set-api-key --context ${context.name}`);
68
+ }
54
69
  const spinner = ora(`Logging in to ${context.name} (${context.environment})...`).start();
55
70
  try {
56
71
  spinner.text = 'Waiting for browser authentication...';
57
- await login(context);
58
- spinner.text = 'Verifying authentication...';
72
+ await login(context, this.flowHooks());
73
+ // The tenant prompt needs the terminal, so the spinner pauses around it.
74
+ spinner.stop();
75
+ await completeTenantSelection(context.name, (tenants) => this.chooseTenant(tenants));
76
+ spinner.start('Verifying authentication...');
59
77
  const user = await testAuth(context.name);
60
78
  spinner.succeed(`Successfully logged in as ${user.email || user.name || 'user'}`);
61
- this.log(`\nContext: ${context.name}`);
62
- this.log(`Environment: ${context.environment}`);
63
- if (context.tenant_id) {
64
- this.log(`Tenant ID: ${context.tenant_id}`);
79
+ this.printSession(context);
80
+ // `context` predates the stored tokens; re-read it for the tenant they carry.
81
+ const tenantId = getContext(context.name)?.tenant_id;
82
+ if (tenantId) {
83
+ this.log(`Tenant ID: ${tenantId}`);
65
84
  }
66
85
  }
67
86
  catch (error) {
68
- spinner.fail('Login failed');
69
- if (error instanceof Error) {
70
- this.error(error.message);
87
+ if (error instanceof NoTerminalError) {
88
+ // The login is stored and only the tenant pick is deferred; the session
89
+ // check is skipped because a token without a tenant is always rejected.
90
+ spinner.stop();
91
+ this.printSession(context);
92
+ this.warn(`Your user reaches ${error.tenants.length} tenants and none is selected yet. Pick one with: aderis auth tenant switch <tenant-id> --context ${context.name}\n` +
93
+ error.tenants.map((t) => ` ${tenantLabel(t)}`).join('\n'));
94
+ return;
71
95
  }
72
- throw error;
96
+ spinner.fail('Login failed');
97
+ this.error(error instanceof Error ? error.message : String(error));
98
+ }
99
+ }
100
+ /** Prompts for a tenant; throws {@link NoTerminalError} without a terminal, which `run()` turns into the hint. */
101
+ async chooseTenant(tenants) {
102
+ if (!process.stdin.isTTY) {
103
+ throw new NoTerminalError(tenants);
104
+ }
105
+ return select({
106
+ choices: tenants.map((t) => ({ name: tenantLabel(t), value: t.tenantId })),
107
+ message: 'Your user reaches several tenants. Select the one to work in:',
108
+ });
109
+ }
110
+ /** Browser-flow hooks: the defaults here, overridden by tests. */
111
+ flowHooks() {
112
+ return {};
113
+ }
114
+ printSession(context) {
115
+ this.log(`\nContext: ${context.name}`);
116
+ this.log(`Environment: ${context.environment}`);
117
+ }
118
+ /** Validates `--keycloak-url` before the context is stored. */
119
+ resolveKeycloakBaseUrl(flag) {
120
+ if (flag === undefined)
121
+ return undefined;
122
+ try {
123
+ return parseKeycloakBaseUrl(flag);
124
+ }
125
+ catch (error) {
126
+ this.error(`Invalid --keycloak-url: ${error instanceof Error ? error.message : String(error)}`);
73
127
  }
74
128
  }
75
129
  async createNewContext(flags) {
@@ -93,19 +147,18 @@ export default class AuthLogin extends Command {
93
147
  const environment = flags.environment ? flags.environment : (await select({
94
148
  choices: [
95
149
  { description: 'Development environment', name: 'dev', value: 'dev' },
150
+ { description: 'QA environment', name: 'test', value: 'test' },
96
151
  { description: 'Staging environment', name: 'staging', value: 'staging' },
97
152
  { description: 'Production environment', name: 'live', value: 'live' },
98
153
  ],
99
154
  message: 'Environment:',
100
155
  }));
101
- const defaultClientId = DEFAULT_CLIENT_IDS[environment];
102
- const clientId = flags['client-id'] || defaultClientId;
103
- if (!clientId) {
104
- this.error(`No default client ID configured for ${environment} environment. Please provide --client-id`);
105
- }
156
+ const keycloakBaseUrl = this.resolveKeycloakBaseUrl(flags['keycloak-url']);
106
157
  const context = {
107
- client_id: clientId,
158
+ auth_type: 'oauth2',
159
+ client_id: flags['client-id'] || DEFAULT_KEYCLOAK_CLIENT_ID,
108
160
  environment,
161
+ keycloak_base_url: keycloakBaseUrl,
109
162
  name: contextName,
110
163
  };
111
164
  addContext(context);
@@ -113,3 +166,12 @@ export default class AuthLogin extends Command {
113
166
  return context;
114
167
  }
115
168
  }
169
+ /** Thrown by the tenant chooser when there is no terminal; the login stays stored. */
170
+ class NoTerminalError extends Error {
171
+ tenants;
172
+ constructor(tenants) {
173
+ super('No terminal available to select a tenant');
174
+ this.tenants = tenants;
175
+ this.name = 'NoTerminalError';
176
+ }
177
+ }
@@ -1,4 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
+ /**
3
+ * Deletes the stored credential only; the provider's browser session
4
+ * (`end_session_endpoint`) is left in place.
5
+ */
2
6
  export default class AuthLogout extends Command {
3
7
  static description: string;
4
8
  static examples: string[];
@@ -7,4 +11,6 @@ export default class AuthLogout extends Command {
7
11
  context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
12
  };
9
13
  run(): Promise<void>;
14
+ /** Drop whichever credential the context holds — OAuth tokens or an API key. */
15
+ private clearCredentials;
10
16
  }
@@ -1,5 +1,9 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { clearTokens, getContext, getCurrentContext } from '../../lib/config.js';
2
+ import { clearApiKey, clearTokens, getAuthType, getContext, getCurrentContext, hasCredentials, } from '../../lib/config.js';
3
+ /**
4
+ * Deletes the stored credential only; the provider's browser session
5
+ * (`end_session_endpoint`) is left in place.
6
+ */
3
7
  export default class AuthLogout extends Command {
4
8
  static description = 'Logout from the current or specified context';
5
9
  static examples = [
@@ -14,8 +18,10 @@ export default class AuthLogout extends Command {
14
18
  description: 'Logout from all contexts',
15
19
  }),
16
20
  context: Flags.string({
21
+ aliases: ['config'],
17
22
  char: 'c',
18
- description: 'Specific context to logout from',
23
+ description: 'Context to logout from, overriding the current context (alias: --config)',
24
+ env: 'ADERIS_CONTEXT',
19
25
  }),
20
26
  };
21
27
  async run() {
@@ -24,8 +30,8 @@ export default class AuthLogout extends Command {
24
30
  if (flags.all) {
25
31
  const contexts = listContexts();
26
32
  for (const ctx of contexts) {
27
- if (ctx.tokens) {
28
- clearTokens(ctx.name);
33
+ if (hasCredentials(ctx)) {
34
+ this.clearCredentials(ctx.name);
29
35
  this.log(`Logged out from ${ctx.name}`);
30
36
  }
31
37
  }
@@ -48,11 +54,22 @@ export default class AuthLogout extends Command {
48
54
  contextName = current.name;
49
55
  }
50
56
  const context = getContext(contextName);
51
- if (!context?.tokens) {
57
+ if (!context || !hasCredentials(context)) {
52
58
  this.log(`Already logged out from ${contextName}`);
53
59
  return;
54
60
  }
61
+ this.clearCredentials(contextName);
62
+ this.log(getAuthType(context) === 'api_key'
63
+ ? `Removed API key from ${contextName}`
64
+ : `Successfully logged out from ${contextName}`);
65
+ }
66
+ /** Drop whichever credential the context holds — OAuth tokens or an API key. */
67
+ clearCredentials(contextName) {
68
+ const context = getContext(contextName);
69
+ if (context && getAuthType(context) === 'api_key') {
70
+ clearApiKey(contextName);
71
+ return;
72
+ }
55
73
  clearTokens(contextName);
56
- this.log(`Successfully logged out from ${contextName}`);
57
74
  }
58
75
  }
@@ -1,6 +1,6 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { testAuth } from '../../lib/auth.js';
3
- import { getContext, getCurrentContext, listContexts } from '../../lib/config.js';
2
+ import { keycloakBaseUrlOf, testAuth } from '../../lib/auth/index.js';
3
+ import { getAuthType, getContext, getCurrentContext, hasCredentials, listContexts, maskApiKey, missingCredentialsMessage, } from '../../lib/config.js';
4
4
  export default class AuthStatus extends Command {
5
5
  static description = 'Show authentication status for contexts';
6
6
  static examples = [
@@ -15,8 +15,10 @@ export default class AuthStatus extends Command {
15
15
  description: 'Show status for all contexts',
16
16
  }),
17
17
  context: Flags.string({
18
+ aliases: ['config'],
18
19
  char: 'c',
19
- description: 'Specific context to check',
20
+ description: 'Context to check, overriding the current context (alias: --config)',
21
+ env: 'ADERIS_CONTEXT',
20
22
  }),
21
23
  };
22
24
  async run() {
@@ -56,7 +58,10 @@ export default class AuthStatus extends Command {
56
58
  for (const ctx of contexts) {
57
59
  const isCurrent = current?.name === ctx.name;
58
60
  const prefix = isCurrent ? '* ' : ' ';
59
- const status = ctx.tokens ? '✓ logged in' : '✗ not logged in';
61
+ const isApiKey = getAuthType(ctx) === 'api_key';
62
+ const status = hasCredentials(ctx)
63
+ ? (isApiKey ? '✓ api key set' : '✓ logged in')
64
+ : (isApiKey ? '✗ no api key' : '✗ not logged in');
60
65
  this.log(`${prefix}${ctx.name} (${ctx.environment}) - ${status}`);
61
66
  }
62
67
  this.log('\n* = current context');
@@ -68,33 +73,51 @@ export default class AuthStatus extends Command {
68
73
  this.error(`Context "${name}" not found.`);
69
74
  }
70
75
  const isCurrent = current?.name === name;
76
+ const authType = getAuthType(context);
71
77
  this.log(`Context: ${context.name}${isCurrent ? ' (current)' : ''}`);
72
78
  this.log(`Environment: ${context.environment}`);
73
- this.log(`Client ID: ${context.client_id}`);
74
- if (!context.tokens) {
75
- this.log(`Status: Not logged in`);
76
- this.log(`\nRun: aderis auth login${isCurrent ? '' : ` --context ${name}`}`);
77
- return;
79
+ this.log(`Auth: ${authType === 'api_key' ? 'API key (x-api-key)' : 'oauth2'}`);
80
+ if (authType === 'oauth2') {
81
+ this.log(`Keycloak URL: ${keycloakBaseUrlOf(context)}`);
82
+ this.log(`Client ID: ${context.client_id ?? 'N/A'}`);
78
83
  }
79
- this.log(`Tenant ID: ${context.tenant_id || 'N/A'}`);
80
- if (context.tokens.expires_at) {
81
- const expiresAt = new Date(context.tokens.expires_at);
82
- const isExpired = expiresAt < new Date();
83
- this.log(`Token expires: ${expiresAt.toLocaleString()}${isExpired ? ' (EXPIRED)' : ''}`);
84
+ if (!hasCredentials(context)) {
85
+ this.log(`Status: ${authType === 'api_key' ? 'No API key set' : 'Not logged in'}`);
86
+ this.log(`\n${missingCredentialsMessage(context)}`);
87
+ return;
84
88
  }
89
+ // `testAuth` refreshes an access token that is within the expiry buffer, so
90
+ // it runs before the expiry is printed — otherwise a session that was just
91
+ // renewed still shows the stale timestamp, labelled (EXPIRED).
92
+ let checked;
85
93
  if (detailed) {
86
94
  try {
87
- const user = await testAuth(name);
88
- this.log(`\nLogged in as: ${user.email || user.name || 'Unknown'}`);
89
- this.log('Status: ✓ Authenticated');
95
+ checked = await testAuth(name);
90
96
  }
91
97
  catch (error) {
92
- if (error instanceof Error) {
93
- this.log(`\nStatus: ✗ ${error.message}`);
94
- }
95
- else {
96
- this.log('\nStatus: ✗ Authentication error');
97
- }
98
+ checked = error instanceof Error ? error : new Error('Authentication error');
99
+ }
100
+ }
101
+ // Re-read: a refresh during the check replaced the stored tokens.
102
+ const fresh = getContext(name) ?? context;
103
+ if (authType === 'api_key') {
104
+ this.log(`API key: ${maskApiKey(fresh.api_key)}`);
105
+ }
106
+ else {
107
+ this.log(`Tenant ID: ${fresh.tenant_id || 'N/A'}`);
108
+ if (fresh.tokens?.expires_at) {
109
+ const expiresAt = new Date(fresh.tokens.expires_at);
110
+ const isExpired = expiresAt < new Date();
111
+ this.log(`Token expires: ${expiresAt.toLocaleString()}${isExpired ? ' (EXPIRED)' : ''}`);
112
+ }
113
+ }
114
+ if (checked !== undefined) {
115
+ if (checked instanceof Error) {
116
+ this.log(`\nStatus: ✗ ${checked.message}`);
117
+ }
118
+ else {
119
+ this.log(`\nLogged in as: ${checked.email || checked.name || 'Unknown'}`);
120
+ this.log('Status: ✓ Authenticated');
98
121
  }
99
122
  }
100
123
  }
@@ -1,7 +1,7 @@
1
1
  import { select } from '@inquirer/prompts';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
- import { getCurrentTenant, listTenants, switchTenant } from '../../lib/auth.js';
4
- import { getContext, getCurrentContext } from '../../lib/config.js';
3
+ import { getCurrentTenant, listTenants, switchTenant, tenantLabel } from '../../lib/auth/index.js';
4
+ import { getAuthType, getContext, getCurrentContext } from '../../lib/config.js';
5
5
  export default class AuthTenant extends Command {
6
6
  static args = {
7
7
  action: Args.string({
@@ -23,8 +23,10 @@ export default class AuthTenant extends Command {
23
23
  ];
24
24
  static flags = {
25
25
  context: Flags.string({
26
+ aliases: ['config'],
26
27
  char: 'c',
27
- description: 'Context to use',
28
+ description: 'Context to use, overriding the current context (alias: --config)',
29
+ env: 'ADERIS_CONTEXT',
28
30
  }),
29
31
  json: Flags.boolean({
30
32
  default: false,
@@ -38,7 +40,14 @@ export default class AuthTenant extends Command {
38
40
  this.error('No context configured. Run: aderis auth login --new');
39
41
  }
40
42
  const context = getContext(contextName);
41
- if (!context?.tokens) {
43
+ if (!context) {
44
+ this.error(`Context "${contextName}" not found.`);
45
+ }
46
+ // API keys are issued per tenant, so there is nothing to list or switch.
47
+ if (getAuthType(context) === 'api_key') {
48
+ this.error(`Context "${contextName}" authenticates with an API key, which is already scoped to a single tenant. Tenant switching requires OAuth login.`);
49
+ }
50
+ if (!context.tokens) {
42
51
  this.error('Not logged in. Run: aderis auth login');
43
52
  }
44
53
  const action = args.action || 'interactive';
@@ -98,7 +107,7 @@ export default class AuthTenant extends Command {
98
107
  tags.push('default');
99
108
  const tagStr = tags.length > 0 ? ` (${tags.join(', ')})` : '';
100
109
  return {
101
- name: `${t.name}${tagStr}`,
110
+ name: `${tenantLabel(t)}${tagStr}`,
102
111
  value: t.tenantId,
103
112
  };
104
113
  });
@@ -175,10 +184,7 @@ export default class AuthTenant extends Command {
175
184
  this.log(`Successfully switched to tenant ${display}`);
176
185
  }
177
186
  catch (error) {
178
- if (error instanceof Error) {
179
- this.error(`Failed to switch tenant: ${error.message}`);
180
- }
181
- throw error;
187
+ this.error(`Failed to switch tenant: ${error instanceof Error ? error.message : String(error)}`);
182
188
  }
183
189
  }
184
190
  }
@@ -24,7 +24,6 @@ export default class Auth extends Command {
24
24
  table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
25
  };
26
26
  run(): Promise<void>;
27
- private filterFields;
28
27
  private getColumns;
29
28
  private getValue;
30
29
  private printCsv;