@aderis-erp/cli 1.0.15 → 1.1.0

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 (182) 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 +54 -78
  4. package/dist/commands/account-transaction-allocations.d.ts +0 -1
  5. package/dist/commands/account-transaction-allocations.js +51 -75
  6. package/dist/commands/account-transactions.d.ts +0 -1
  7. package/dist/commands/account-transactions.js +80 -94
  8. package/dist/commands/accounts.d.ts +0 -1
  9. package/dist/commands/accounts.js +59 -83
  10. package/dist/commands/agent-platform.d.ts +0 -1
  11. package/dist/commands/agent-platform.js +58 -82
  12. package/dist/commands/agent-runs.d.ts +39 -0
  13. package/dist/commands/agent-runs.js +1155 -0
  14. package/dist/commands/analytics.d.ts +0 -1
  15. package/dist/commands/analytics.js +52 -76
  16. package/dist/commands/{text-templates.d.ts → api-keys.d.ts} +1 -2
  17. package/dist/commands/api-keys.js +1150 -0
  18. package/dist/commands/async-jobs.d.ts +39 -0
  19. package/dist/commands/async-jobs.js +1158 -0
  20. package/dist/commands/audit-log.d.ts +0 -1
  21. package/dist/commands/audit-log.js +51 -75
  22. package/dist/commands/auth/login.d.ts +9 -0
  23. package/dist/commands/auth/login.js +73 -20
  24. package/dist/commands/auth/logout.d.ts +4 -0
  25. package/dist/commands/auth/logout.js +4 -0
  26. package/dist/commands/auth/status.js +26 -17
  27. package/dist/commands/auth/tenant.js +3 -6
  28. package/dist/commands/auth.d.ts +0 -1
  29. package/dist/commands/auth.js +5 -34
  30. package/dist/commands/bank-accounts.d.ts +0 -1
  31. package/dist/commands/bank-accounts.js +68 -91
  32. package/dist/commands/bank-integrations.d.ts +1 -1
  33. package/dist/commands/bank-integrations.js +71 -92
  34. package/dist/commands/business-partners.d.ts +1 -1
  35. package/dist/commands/business-partners.js +84 -100
  36. package/dist/commands/chat.d.ts +0 -1
  37. package/dist/commands/chat.js +57 -80
  38. package/dist/commands/config/add.d.ts +3 -0
  39. package/dist/commands/config/add.js +34 -16
  40. package/dist/commands/config/current.js +3 -0
  41. package/dist/commands/config/list.js +3 -0
  42. package/dist/commands/credit-card-accounts.d.ts +1 -1
  43. package/dist/commands/credit-card-accounts.js +72 -90
  44. package/dist/commands/delivery-notes.d.ts +0 -1
  45. package/dist/commands/delivery-notes.js +83 -105
  46. package/dist/commands/dunning-configs.d.ts +0 -1
  47. package/dist/commands/dunning-configs.js +54 -78
  48. package/dist/commands/dunning-reminders.d.ts +0 -1
  49. package/dist/commands/dunning-reminders.js +57 -81
  50. package/dist/commands/dunning-runs.d.ts +0 -1
  51. package/dist/commands/dunning-runs.js +50 -74
  52. package/dist/commands/email-transactions.d.ts +0 -1
  53. package/dist/commands/email-transactions.js +52 -76
  54. package/dist/commands/employees.d.ts +0 -1
  55. package/dist/commands/employees.js +81 -105
  56. package/dist/commands/entity-configurations.d.ts +0 -1
  57. package/dist/commands/entity-configurations.js +52 -76
  58. package/dist/commands/event-processor-mappings.d.ts +0 -1
  59. package/dist/commands/event-processor-mappings.js +54 -78
  60. package/dist/commands/files.d.ts +0 -1
  61. package/dist/commands/files.js +61 -85
  62. package/dist/commands/health.d.ts +0 -1
  63. package/dist/commands/health.js +54 -78
  64. package/dist/commands/imports.d.ts +39 -0
  65. package/dist/commands/imports.js +1151 -0
  66. package/dist/commands/inbox.d.ts +0 -1
  67. package/dist/commands/inbox.js +57 -80
  68. package/dist/commands/integration-identities.d.ts +40 -0
  69. package/dist/commands/integration-identities.js +1151 -0
  70. package/dist/commands/integrations.d.ts +0 -1
  71. package/dist/commands/integrations.js +52 -76
  72. package/dist/commands/item-categories.d.ts +0 -1
  73. package/dist/commands/item-categories.js +54 -78
  74. package/dist/commands/items.d.ts +0 -1
  75. package/dist/commands/items.js +54 -78
  76. package/dist/commands/journal-lines.d.ts +0 -1
  77. package/dist/commands/journal-lines.js +50 -74
  78. package/dist/commands/journals.d.ts +1 -1
  79. package/dist/commands/journals.js +75 -96
  80. package/dist/commands/ledgers.d.ts +3 -2
  81. package/dist/commands/ledgers.js +100 -95
  82. package/dist/commands/my-account.d.ts +39 -0
  83. package/dist/commands/my-account.js +1160 -0
  84. package/dist/commands/office-locations.d.ts +0 -1
  85. package/dist/commands/office-locations.js +54 -78
  86. package/dist/commands/payment-methods.d.ts +0 -1
  87. package/dist/commands/payment-methods.js +59 -83
  88. package/dist/commands/payment-runs.d.ts +0 -1
  89. package/dist/commands/payment-runs.js +66 -90
  90. package/dist/commands/payment-terms.d.ts +0 -1
  91. package/dist/commands/payment-terms.js +55 -79
  92. package/dist/commands/payments.d.ts +0 -1
  93. package/dist/commands/payments.js +56 -80
  94. package/dist/commands/payroll-runs.d.ts +43 -0
  95. package/dist/commands/payroll-runs.js +1188 -0
  96. package/dist/commands/permissions.d.ts +39 -0
  97. package/dist/commands/permissions.js +1146 -0
  98. package/dist/commands/price-lists.d.ts +0 -1
  99. package/dist/commands/price-lists.js +59 -83
  100. package/dist/commands/processor-mappings.d.ts +0 -1
  101. package/dist/commands/processor-mappings.js +53 -77
  102. package/dist/commands/processors.d.ts +0 -1
  103. package/dist/commands/processors.js +60 -85
  104. package/dist/commands/purchase-order-types.d.ts +0 -1
  105. package/dist/commands/purchase-order-types.js +54 -78
  106. package/dist/commands/purchase-orders.d.ts +0 -1
  107. package/dist/commands/purchase-orders.js +74 -97
  108. package/dist/commands/reports.d.ts +0 -1
  109. package/dist/commands/reports.js +54 -78
  110. package/dist/commands/roles.d.ts +39 -0
  111. package/dist/commands/{text-templates.js → roles.js} +64 -90
  112. package/dist/commands/salary-types.d.ts +0 -1
  113. package/dist/commands/salary-types.js +54 -78
  114. package/dist/commands/sales-invoices.d.ts +0 -1
  115. package/dist/commands/sales-invoices.js +90 -110
  116. package/dist/commands/sales-orders.d.ts +0 -1
  117. package/dist/commands/sales-orders.js +83 -105
  118. package/dist/commands/saved-views.d.ts +0 -1
  119. package/dist/commands/saved-views.js +52 -76
  120. package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
  121. package/dist/commands/scheduled-processor-mappings.js +54 -78
  122. package/dist/commands/schedules.d.ts +0 -1
  123. package/dist/commands/schedules.js +58 -82
  124. package/dist/commands/switch.js +4 -4
  125. package/dist/commands/tag-configurations.d.ts +0 -1
  126. package/dist/commands/tag-configurations.js +56 -79
  127. package/dist/commands/tags.d.ts +0 -1
  128. package/dist/commands/tags.js +54 -78
  129. package/dist/commands/tax-codes.d.ts +0 -1
  130. package/dist/commands/tax-codes.js +57 -81
  131. package/dist/commands/tax-matrix.d.ts +0 -1
  132. package/dist/commands/tax-matrix.js +54 -78
  133. package/dist/commands/templates.d.ts +39 -0
  134. package/dist/commands/templates.js +1157 -0
  135. package/dist/commands/tenants.d.ts +0 -1
  136. package/dist/commands/tenants.js +58 -81
  137. package/dist/commands/tools.d.ts +0 -1
  138. package/dist/commands/tools.js +51 -75
  139. package/dist/commands/users.d.ts +42 -0
  140. package/dist/commands/users.js +1174 -0
  141. package/dist/commands/vat-declarations.d.ts +3 -1
  142. package/dist/commands/vat-declarations.js +78 -93
  143. package/dist/commands/vendor-invoices.d.ts +0 -1
  144. package/dist/commands/vendor-invoices.js +87 -110
  145. package/dist/commands/webhooks.d.ts +0 -1
  146. package/dist/commands/webhooks.js +56 -80
  147. package/dist/commands/workflows.d.ts +0 -1
  148. package/dist/commands/workflows.js +52 -76
  149. package/dist/lib/api-client.d.ts +6 -0
  150. package/dist/lib/api-client.js +16 -6
  151. package/dist/lib/auth/constants.d.ts +11 -0
  152. package/dist/lib/auth/constants.js +11 -0
  153. package/dist/lib/auth/errors.d.ts +17 -0
  154. package/dist/lib/auth/errors.js +22 -0
  155. package/dist/lib/auth/flow.d.ts +27 -0
  156. package/dist/lib/auth/flow.js +163 -0
  157. package/dist/lib/auth/guards.d.ts +5 -0
  158. package/dist/lib/auth/guards.js +14 -0
  159. package/dist/lib/auth/http.d.ts +16 -0
  160. package/dist/lib/auth/http.js +49 -0
  161. package/dist/lib/auth/index.d.ts +49 -0
  162. package/dist/lib/auth/index.js +228 -0
  163. package/dist/lib/auth/jwks.d.ts +19 -0
  164. package/dist/lib/auth/jwks.js +112 -0
  165. package/dist/lib/auth/keycloak-provider.d.ts +91 -0
  166. package/dist/lib/auth/keycloak-provider.js +244 -0
  167. package/dist/lib/auth/pages.d.ts +25 -0
  168. package/dist/lib/auth/pages.js +123 -0
  169. package/dist/lib/auth/pkce.d.ts +11 -0
  170. package/dist/lib/auth/pkce.js +17 -0
  171. package/dist/lib/auth/tokens.d.ts +59 -0
  172. package/dist/lib/auth/tokens.js +105 -0
  173. package/dist/lib/auth/types.d.ts +61 -0
  174. package/dist/lib/auth/types.js +1 -0
  175. package/dist/lib/auth/validated-url.d.ts +6 -0
  176. package/dist/lib/auth/validated-url.js +53 -0
  177. package/dist/lib/config.d.ts +13 -3
  178. package/dist/lib/config.js +24 -19
  179. package/oclif.manifest.json +2447 -641
  180. package/package.json +5 -4
  181. package/dist/lib/auth.d.ts +0 -43
  182. package/dist/lib/auth.js +0 -610
@@ -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,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
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;
@@ -8,8 +8,8 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "get"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/audit-logs', summary: 'Get audit log entries', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'get': { method: 'GET', path: '/v1/audit-logs/{id}', summary: 'Get a single audit log entry', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/audit-logs', summary: 'Get audit log entries', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'get': { method: 'GET', path: '/v1/audit-logs/{id}', summary: 'Get a single audit log entry', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
13
  };
14
14
  export default class AuditLog extends Command {
15
15
  static description = 'Manage audit log';
@@ -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
  };
@@ -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));
@@ -256,10 +258,11 @@ export default class AuditLog extends Command {
256
258
  process.stderr.write('\n');
257
259
  }
258
260
  try {
259
- // Binary response: download to file
261
+ // Binary response: to -f / --file, or to stdout when it is redirected.
260
262
  if (endpoint.isBinaryResponse) {
261
- if (!flags.file) {
262
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
263
+ const toStdout = !flags.file;
264
+ if (toStdout && process.stdout.isTTY) {
265
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
263
266
  }
264
267
  const binResponse = await apiRequest(endpoint.method, apiPath, {
265
268
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -267,24 +270,55 @@ export default class AuditLog extends Command {
267
270
  contextName: flags.context,
268
271
  binary: true,
269
272
  });
270
- const fsModule = await import('node:fs');
271
273
  const binData = binResponse.data;
272
274
  const buffer = Buffer.isBuffer(binData)
273
275
  ? binData
274
276
  : Buffer.from(binData);
277
+ if (toStdout) {
278
+ // Nothing else may reach stdout here — it is the file.
279
+ process.stdout.write(buffer);
280
+ return;
281
+ }
282
+ const fsModule = await import('node:fs');
275
283
  fsModule.writeFileSync(flags.file, buffer);
276
284
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
277
285
  return;
278
286
  }
279
- const response = await apiRequest(endpoint.method, apiPath, {
280
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
281
- data,
282
- contextName: flags.context,
283
- acceptAny: true,
284
- });
287
+ let response;
288
+ if (endpoint.hasBinaryVariant) {
289
+ // This route answers JSON or a file depending on the request body, so the
290
+ // bytes are read first and the response's own content-type decides. Asking
291
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
292
+ const raw = await apiRequest(endpoint.method, apiPath, {
293
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
294
+ data,
295
+ contextName: flags.context,
296
+ binary: true,
297
+ });
298
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
299
+ const rawData = raw.data;
300
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
301
+ if (!isTextualMediaType(rawType)) {
302
+ if (process.stdout.isTTY) {
303
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
304
+ }
305
+ // Nothing else may reach stdout here — it is the file.
306
+ process.stdout.write(buffer);
307
+ return;
308
+ }
309
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
310
+ }
311
+ else {
312
+ response = await apiRequest(endpoint.method, apiPath, {
313
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
314
+ data,
315
+ contextName: flags.context,
316
+ acceptAny: true,
317
+ });
318
+ }
285
319
  // Check content-type for non-JSON responses (XML, text, etc.)
286
320
  const contentType = (response.headers['content-type'] || '').toLowerCase();
287
- if (contentType.includes('xml') || contentType.includes('text/')) {
321
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
288
322
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
289
323
  this.log(String(response.data));
290
324
  return;
@@ -311,11 +345,6 @@ export default class AuditLog extends Command {
311
345
  output = obj[arrayKeys[0]];
312
346
  }
313
347
  }
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
348
  // Sanitize output if requested
320
349
  if (flags.sanitize) {
321
350
  output = this.sanitizeOutput(output);
@@ -570,6 +599,8 @@ export default class AuditLog extends Command {
570
599
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
571
600
  if (flags.include)
572
601
  baseParams.include = flags.include;
602
+ if (flags.fields)
603
+ baseParams.fields = flags.fields;
573
604
  if (flags.query) {
574
605
  try {
575
606
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -624,12 +655,7 @@ export default class AuditLog extends Command {
624
655
  }
625
656
  }
626
657
  process.stderr.write(' done\n');
627
- // Filter fields if specified
628
658
  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
659
  // Sanitize output if requested
634
660
  if (flags.sanitize) {
635
661
  output = this.sanitizeOutput(output);
@@ -958,56 +984,6 @@ export default class AuditLog extends Command {
958
984
  return `\$${op}`;
959
985
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
960
986
  }
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
987
  sanitizeOutput(data) {
1012
988
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1013
989
  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,14 +1,15 @@
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, getAuthType, 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({
@@ -22,8 +23,11 @@ export default class AuthLogin extends Command {
22
23
  }),
23
24
  environment: Flags.string({
24
25
  char: 'e',
25
- description: 'Environment (dev, staging, live)',
26
- 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)',
27
31
  }),
28
32
  name: Flags.string({
29
33
  description: 'Name for the new context (required with --new)',
@@ -37,6 +41,9 @@ export default class AuthLogin extends Command {
37
41
  async run() {
38
42
  const { flags } = await this.parse(AuthLogin);
39
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
+ }
40
47
  if (flags.new) {
41
48
  context = await this.createNewContext(flags);
42
49
  }
@@ -62,22 +69,61 @@ export default class AuthLogin extends Command {
62
69
  const spinner = ora(`Logging in to ${context.name} (${context.environment})...`).start();
63
70
  try {
64
71
  spinner.text = 'Waiting for browser authentication...';
65
- await login(context);
66
- 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...');
67
77
  const user = await testAuth(context.name);
68
78
  spinner.succeed(`Successfully logged in as ${user.email || user.name || 'user'}`);
69
- this.log(`\nContext: ${context.name}`);
70
- this.log(`Environment: ${context.environment}`);
71
- if (context.tenant_id) {
72
- 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}`);
73
84
  }
74
85
  }
75
86
  catch (error) {
76
- spinner.fail('Login failed');
77
- if (error instanceof Error) {
78
- 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;
79
95
  }
80
- 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)}`);
81
127
  }
82
128
  }
83
129
  async createNewContext(flags) {
@@ -101,20 +147,18 @@ export default class AuthLogin extends Command {
101
147
  const environment = flags.environment ? flags.environment : (await select({
102
148
  choices: [
103
149
  { description: 'Development environment', name: 'dev', value: 'dev' },
150
+ { description: 'QA environment', name: 'test', value: 'test' },
104
151
  { description: 'Staging environment', name: 'staging', value: 'staging' },
105
152
  { description: 'Production environment', name: 'live', value: 'live' },
106
153
  ],
107
154
  message: 'Environment:',
108
155
  }));
109
- const defaultClientId = DEFAULT_CLIENT_IDS[environment];
110
- const clientId = flags['client-id'] || defaultClientId;
111
- if (!clientId) {
112
- this.error(`No default client ID configured for ${environment} environment. Please provide --client-id`);
113
- }
156
+ const keycloakBaseUrl = this.resolveKeycloakBaseUrl(flags['keycloak-url']);
114
157
  const context = {
115
158
  auth_type: 'oauth2',
116
- client_id: clientId,
159
+ client_id: flags['client-id'] || DEFAULT_KEYCLOAK_CLIENT_ID,
117
160
  environment,
161
+ keycloak_base_url: keycloakBaseUrl,
118
162
  name: contextName,
119
163
  };
120
164
  addContext(context);
@@ -122,3 +166,12 @@ export default class AuthLogin extends Command {
122
166
  return context;
123
167
  }
124
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[];
@@ -1,5 +1,9 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
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 = [
@@ -1,5 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { testAuth } from '../../lib/auth.js';
2
+ import { keycloakBaseUrlOf, testAuth } from '../../lib/auth/index.js';
3
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';
@@ -78,6 +78,7 @@ export default class AuthStatus extends Command {
78
78
  this.log(`Environment: ${context.environment}`);
79
79
  this.log(`Auth: ${authType === 'api_key' ? 'API key (x-api-key)' : 'oauth2'}`);
80
80
  if (authType === 'oauth2') {
81
+ this.log(`Keycloak URL: ${keycloakBaseUrlOf(context)}`);
81
82
  this.log(`Client ID: ${context.client_id ?? 'N/A'}`);
82
83
  }
83
84
  if (!hasCredentials(context)) {
@@ -85,30 +86,38 @@ export default class AuthStatus extends Command {
85
86
  this.log(`\n${missingCredentialsMessage(context)}`);
86
87
  return;
87
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;
93
+ if (detailed) {
94
+ try {
95
+ checked = await testAuth(name);
96
+ }
97
+ catch (error) {
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;
88
103
  if (authType === 'api_key') {
89
- this.log(`API key: ${maskApiKey(context.api_key)}`);
104
+ this.log(`API key: ${maskApiKey(fresh.api_key)}`);
90
105
  }
91
106
  else {
92
- this.log(`Tenant ID: ${context.tenant_id || 'N/A'}`);
93
- if (context.tokens?.expires_at) {
94
- const expiresAt = new Date(context.tokens.expires_at);
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);
95
110
  const isExpired = expiresAt < new Date();
96
111
  this.log(`Token expires: ${expiresAt.toLocaleString()}${isExpired ? ' (EXPIRED)' : ''}`);
97
112
  }
98
113
  }
99
- if (detailed) {
100
- try {
101
- const user = await testAuth(name);
102
- this.log(`\nLogged in as: ${user.email || user.name || 'Unknown'}`);
103
- this.log('Status: ✓ Authenticated');
114
+ if (checked !== undefined) {
115
+ if (checked instanceof Error) {
116
+ this.log(`\nStatus: ✗ ${checked.message}`);
104
117
  }
105
- catch (error) {
106
- if (error instanceof Error) {
107
- this.log(`\nStatus: ✗ ${error.message}`);
108
- }
109
- else {
110
- this.log('\nStatus: ✗ Authentication error');
111
- }
118
+ else {
119
+ this.log(`\nLogged in as: ${checked.email || checked.name || 'Unknown'}`);
120
+ this.log('Status: ✓ Authenticated');
112
121
  }
113
122
  }
114
123
  }
@@ -1,6 +1,6 @@
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';
3
+ import { getCurrentTenant, listTenants, switchTenant, tenantLabel } from '../../lib/auth/index.js';
4
4
  import { getAuthType, getContext, getCurrentContext } from '../../lib/config.js';
5
5
  export default class AuthTenant extends Command {
6
6
  static args = {
@@ -107,7 +107,7 @@ export default class AuthTenant extends Command {
107
107
  tags.push('default');
108
108
  const tagStr = tags.length > 0 ? ` (${tags.join(', ')})` : '';
109
109
  return {
110
- name: `${t.name}${tagStr}`,
110
+ name: `${tenantLabel(t)}${tagStr}`,
111
111
  value: t.tenantId,
112
112
  };
113
113
  });
@@ -184,10 +184,7 @@ export default class AuthTenant extends Command {
184
184
  this.log(`Successfully switched to tenant ${display}`);
185
185
  }
186
186
  catch (error) {
187
- if (error instanceof Error) {
188
- this.error(`Failed to switch tenant: ${error.message}`);
189
- }
190
- throw error;
187
+ this.error(`Failed to switch tenant: ${error instanceof Error ? error.message : String(error)}`);
191
188
  }
192
189
  }
193
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;
@@ -48,7 +48,7 @@ export default class Auth extends Command {
48
48
  }),
49
49
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
50
50
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
51
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
51
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
52
52
  file: Flags.string({ char: 'f', description: 'Read request body from file' }),
53
53
  include: Flags.string({ description: 'Relations to include' }),
54
54
  limit: Flags.string({ description: 'Items per page' }),
@@ -138,6 +138,8 @@ export default class Auth extends Command {
138
138
  queryParams.search = flags.search;
139
139
  if (flags['sort-by'])
140
140
  queryParams.sortBy = flags['sort-by'];
141
+ if (flags.fields)
142
+ queryParams.fields = flags.fields;
141
143
  if (flags.query) {
142
144
  try {
143
145
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -222,11 +224,6 @@ export default class Auth extends Command {
222
224
  if (args.action === 'list' && output && typeof output === 'object' && 'data' in output) {
223
225
  output = output.data;
224
226
  }
225
- // Filter fields if --fields flag is provided
226
- if (flags.fields) {
227
- const fieldList = flags.fields.split(',').map((f) => f.trim());
228
- output = this.filterFields(output, fieldList);
229
- }
230
227
  // Sanitize output if requested
231
228
  if (flags.sanitize) {
232
229
  output = this.sanitizeOutput(output);
@@ -248,29 +245,6 @@ export default class Auth extends Command {
248
245
  throw error;
249
246
  }
250
247
  }
251
- filterFields(data, fields) {
252
- const pick = (obj, keys) => {
253
- const result = {};
254
- for (const key of keys) {
255
- if (key.includes('.')) {
256
- // Handle nested fields like 'customer.name'
257
- const [first, ...rest] = key.split('.');
258
- if (obj[first] && typeof obj[first] === 'object') {
259
- const nested = pick(obj[first], [rest.join('.')]);
260
- result[first] = { ...result[first], ...nested };
261
- }
262
- }
263
- else if (key in obj) {
264
- result[key] = obj[key];
265
- }
266
- }
267
- return result;
268
- };
269
- if (Array.isArray(data)) {
270
- return data.map((item) => pick(item, fields));
271
- }
272
- return pick(data, fields);
273
- }
274
248
  getColumns(data, fields, flatten = false) {
275
249
  const items = Array.isArray(data) ? data : [data];
276
250
  if (items.length === 0)
@@ -475,6 +449,8 @@ export default class Auth extends Command {
475
449
  baseParams.sortBy = flags['sort-by'];
476
450
  if (flags.include)
477
451
  baseParams.include = flags.include;
452
+ if (flags.fields)
453
+ baseParams.fields = flags.fields;
478
454
  if (flags.query) {
479
455
  try {
480
456
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -511,12 +487,7 @@ export default class Auth extends Command {
511
487
  }
512
488
  }
513
489
  process.stderr.write(' done\n');
514
- // Filter fields if specified
515
490
  let output = allItems;
516
- if (flags.fields) {
517
- const fieldList = flags.fields.split(',').map((f) => f.trim());
518
- output = this.filterFields(allItems, fieldList);
519
- }
520
491
  // Sanitize output if requested
521
492
  if (flags.sanitize) {
522
493
  output = this.sanitizeOutput(output);
@@ -33,7 +33,6 @@ export default class BankAccounts extends Command {
33
33
  private runCamt053Import;
34
34
  private runNestedImport;
35
35
  private parseFilterValue;
36
- private filterFields;
37
36
  private sanitizeOutput;
38
37
  private getColumns;
39
38
  private getValue;