@aderis-erp/cli 1.0.15 → 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 (163) 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 +5 -61
  4. package/dist/commands/account-transaction-allocations.d.ts +0 -1
  5. package/dist/commands/account-transaction-allocations.js +5 -61
  6. package/dist/commands/account-transactions.d.ts +0 -1
  7. package/dist/commands/account-transactions.js +9 -63
  8. package/dist/commands/accounts.d.ts +0 -1
  9. package/dist/commands/accounts.js +5 -61
  10. package/dist/commands/agent-platform.d.ts +0 -1
  11. package/dist/commands/agent-platform.js +5 -61
  12. package/dist/commands/analytics.d.ts +0 -1
  13. package/dist/commands/analytics.js +5 -61
  14. package/dist/commands/api-keys.d.ts +39 -0
  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 +5 -61
  18. package/dist/commands/auth/login.d.ts +9 -0
  19. package/dist/commands/auth/login.js +73 -20
  20. package/dist/commands/auth/logout.d.ts +4 -0
  21. package/dist/commands/auth/logout.js +4 -0
  22. package/dist/commands/auth/status.js +26 -17
  23. package/dist/commands/auth/tenant.js +3 -6
  24. package/dist/commands/auth.d.ts +0 -1
  25. package/dist/commands/auth.js +5 -34
  26. package/dist/commands/bank-accounts.d.ts +0 -1
  27. package/dist/commands/bank-accounts.js +5 -61
  28. package/dist/commands/bank-integrations.d.ts +0 -1
  29. package/dist/commands/bank-integrations.js +5 -61
  30. package/dist/commands/business-partners.d.ts +1 -1
  31. package/dist/commands/business-partners.js +14 -63
  32. package/dist/commands/chat.d.ts +0 -1
  33. package/dist/commands/chat.js +5 -61
  34. package/dist/commands/config/add.d.ts +3 -0
  35. package/dist/commands/config/add.js +34 -16
  36. package/dist/commands/config/current.js +3 -0
  37. package/dist/commands/config/list.js +3 -0
  38. package/dist/commands/credit-card-accounts.d.ts +0 -1
  39. package/dist/commands/credit-card-accounts.js +5 -61
  40. package/dist/commands/delivery-notes.d.ts +0 -1
  41. package/dist/commands/delivery-notes.js +7 -63
  42. package/dist/commands/dunning-configs.d.ts +0 -1
  43. package/dist/commands/dunning-configs.js +5 -61
  44. package/dist/commands/dunning-reminders.d.ts +0 -1
  45. package/dist/commands/dunning-reminders.js +5 -61
  46. package/dist/commands/dunning-runs.d.ts +0 -1
  47. package/dist/commands/dunning-runs.js +5 -61
  48. package/dist/commands/email-transactions.d.ts +0 -1
  49. package/dist/commands/email-transactions.js +5 -61
  50. package/dist/commands/employees.d.ts +0 -1
  51. package/dist/commands/employees.js +5 -61
  52. package/dist/commands/entity-configurations.d.ts +0 -1
  53. package/dist/commands/entity-configurations.js +5 -61
  54. package/dist/commands/event-processor-mappings.d.ts +0 -1
  55. package/dist/commands/event-processor-mappings.js +5 -61
  56. package/dist/commands/files.d.ts +0 -1
  57. package/dist/commands/files.js +5 -61
  58. package/dist/commands/health.d.ts +0 -1
  59. package/dist/commands/health.js +5 -61
  60. package/dist/commands/inbox.d.ts +0 -1
  61. package/dist/commands/inbox.js +5 -61
  62. package/dist/commands/integrations.d.ts +0 -1
  63. package/dist/commands/integrations.js +5 -61
  64. package/dist/commands/item-categories.d.ts +0 -1
  65. package/dist/commands/item-categories.js +5 -61
  66. package/dist/commands/items.d.ts +0 -1
  67. package/dist/commands/items.js +5 -61
  68. package/dist/commands/journal-lines.d.ts +0 -1
  69. package/dist/commands/journal-lines.js +5 -61
  70. package/dist/commands/journals.d.ts +1 -1
  71. package/dist/commands/journals.js +9 -62
  72. package/dist/commands/ledgers.d.ts +0 -1
  73. package/dist/commands/ledgers.js +5 -61
  74. package/dist/commands/nango-integrations.d.ts +39 -0
  75. package/dist/commands/nango-integrations.js +1115 -0
  76. package/dist/commands/office-locations.d.ts +0 -1
  77. package/dist/commands/office-locations.js +5 -61
  78. package/dist/commands/payment-methods.d.ts +0 -1
  79. package/dist/commands/payment-methods.js +5 -61
  80. package/dist/commands/payment-runs.d.ts +0 -1
  81. package/dist/commands/payment-runs.js +5 -61
  82. package/dist/commands/payment-terms.d.ts +0 -1
  83. package/dist/commands/payment-terms.js +5 -61
  84. package/dist/commands/payments.d.ts +0 -1
  85. package/dist/commands/payments.js +5 -61
  86. package/dist/commands/price-lists.d.ts +0 -1
  87. package/dist/commands/price-lists.js +5 -61
  88. package/dist/commands/processor-mappings.d.ts +0 -1
  89. package/dist/commands/processor-mappings.js +5 -61
  90. package/dist/commands/processors.d.ts +0 -1
  91. package/dist/commands/processors.js +5 -61
  92. package/dist/commands/purchase-order-types.d.ts +0 -1
  93. package/dist/commands/purchase-order-types.js +5 -61
  94. package/dist/commands/purchase-orders.d.ts +0 -1
  95. package/dist/commands/purchase-orders.js +7 -63
  96. package/dist/commands/reports.d.ts +0 -1
  97. package/dist/commands/reports.js +6 -62
  98. package/dist/commands/salary-types.d.ts +0 -1
  99. package/dist/commands/salary-types.js +5 -61
  100. package/dist/commands/sales-invoices.d.ts +0 -1
  101. package/dist/commands/sales-invoices.js +12 -65
  102. package/dist/commands/sales-orders.d.ts +0 -1
  103. package/dist/commands/sales-orders.js +7 -63
  104. package/dist/commands/saved-views.d.ts +0 -1
  105. package/dist/commands/saved-views.js +5 -61
  106. package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
  107. package/dist/commands/scheduled-processor-mappings.js +5 -61
  108. package/dist/commands/schedules.d.ts +0 -1
  109. package/dist/commands/schedules.js +5 -61
  110. package/dist/commands/switch.js +4 -4
  111. package/dist/commands/tag-configurations.d.ts +0 -1
  112. package/dist/commands/tag-configurations.js +9 -64
  113. package/dist/commands/tags.d.ts +0 -1
  114. package/dist/commands/tags.js +5 -61
  115. package/dist/commands/tax-codes.d.ts +0 -1
  116. package/dist/commands/tax-codes.js +5 -61
  117. package/dist/commands/tax-matrix.d.ts +0 -1
  118. package/dist/commands/tax-matrix.js +5 -61
  119. package/dist/commands/tenants.d.ts +0 -1
  120. package/dist/commands/tenants.js +7 -62
  121. package/dist/commands/text-templates.d.ts +0 -1
  122. package/dist/commands/text-templates.js +5 -61
  123. package/dist/commands/tools.d.ts +0 -1
  124. package/dist/commands/tools.js +5 -61
  125. package/dist/commands/vat-declarations.d.ts +1 -1
  126. package/dist/commands/vat-declarations.js +11 -62
  127. package/dist/commands/vendor-invoices.d.ts +0 -1
  128. package/dist/commands/vendor-invoices.js +10 -65
  129. package/dist/commands/webhooks.d.ts +0 -1
  130. package/dist/commands/webhooks.js +5 -61
  131. package/dist/commands/workflows.d.ts +0 -1
  132. package/dist/commands/workflows.js +5 -61
  133. package/dist/lib/api-client.js +3 -6
  134. package/dist/lib/auth/constants.d.ts +11 -0
  135. package/dist/lib/auth/constants.js +11 -0
  136. package/dist/lib/auth/errors.d.ts +17 -0
  137. package/dist/lib/auth/errors.js +22 -0
  138. package/dist/lib/auth/flow.d.ts +27 -0
  139. package/dist/lib/auth/flow.js +163 -0
  140. package/dist/lib/auth/guards.d.ts +5 -0
  141. package/dist/lib/auth/guards.js +14 -0
  142. package/dist/lib/auth/http.d.ts +16 -0
  143. package/dist/lib/auth/http.js +49 -0
  144. package/dist/lib/auth/index.d.ts +49 -0
  145. package/dist/lib/auth/index.js +217 -0
  146. package/dist/lib/auth/keycloak-provider.d.ts +75 -0
  147. package/dist/lib/auth/keycloak-provider.js +215 -0
  148. package/dist/lib/auth/pages.d.ts +25 -0
  149. package/dist/lib/auth/pages.js +123 -0
  150. package/dist/lib/auth/pkce.d.ts +11 -0
  151. package/dist/lib/auth/pkce.js +17 -0
  152. package/dist/lib/auth/tokens.d.ts +54 -0
  153. package/dist/lib/auth/tokens.js +104 -0
  154. package/dist/lib/auth/types.d.ts +57 -0
  155. package/dist/lib/auth/types.js +1 -0
  156. package/dist/lib/auth/validated-url.d.ts +6 -0
  157. package/dist/lib/auth/validated-url.js +53 -0
  158. package/dist/lib/config.d.ts +13 -3
  159. package/dist/lib/config.js +24 -19
  160. package/oclif.manifest.json +440 -71
  161. package/package.json +2 -1
  162. package/dist/lib/auth.d.ts +0 -43
  163. 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;
@@ -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));
@@ -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);
@@ -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);
@@ -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,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;
@@ -57,7 +57,7 @@ export default class BankAccounts extends Command {
57
57
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
58
58
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
59
59
  query: Flags.string({ description: 'Additional query params as JSON' }),
60
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
60
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
61
61
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
62
62
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
63
63
  };
@@ -129,6 +129,8 @@ export default class BankAccounts extends Command {
129
129
  queryParams.search = flags.search;
130
130
  if (flags['sort-by'])
131
131
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
132
+ if (flags.fields)
133
+ queryParams.fields = flags.fields;
132
134
  if (flags.query) {
133
135
  try {
134
136
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -331,11 +333,6 @@ export default class BankAccounts extends Command {
331
333
  output = obj[arrayKeys[0]];
332
334
  }
333
335
  }
334
- // Filter fields if --fields flag is provided
335
- if (flags.fields) {
336
- const fieldList = flags.fields.split(',').map(f => f.trim());
337
- output = this.filterFields(output, fieldList);
338
- }
339
336
  // Sanitize output if requested
340
337
  if (flags.sanitize) {
341
338
  output = this.sanitizeOutput(output);
@@ -590,6 +587,8 @@ export default class BankAccounts extends Command {
590
587
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
591
588
  if (flags.include)
592
589
  baseParams.include = flags.include;
590
+ if (flags.fields)
591
+ baseParams.fields = flags.fields;
593
592
  if (flags.query) {
594
593
  try {
595
594
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -644,12 +643,7 @@ export default class BankAccounts extends Command {
644
643
  }
645
644
  }
646
645
  process.stderr.write(' done\n');
647
- // Filter fields if specified
648
646
  let output = allItems;
649
- if (flags.fields) {
650
- const fieldList = flags.fields.split(',').map(f => f.trim());
651
- output = this.filterFields(allItems, fieldList);
652
- }
653
647
  // Sanitize output if requested
654
648
  if (flags.sanitize) {
655
649
  output = this.sanitizeOutput(output);
@@ -978,56 +972,6 @@ export default class BankAccounts extends Command {
978
972
  return `\$${op}`;
979
973
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
980
974
  }
981
- filterFields(data, fields) {
982
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
983
- const pick = (obj, keys) => {
984
- // Group keys by their first segment so sibling sub-paths
985
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
986
- // and don't clobber each other on merge.
987
- const directKeys = new Set();
988
- const nestedKeys = new Map();
989
- for (const key of keys) {
990
- if (key.split('.').some(isUnsafeKey))
991
- continue;
992
- if (key.includes('.')) {
993
- const [first, ...rest] = key.split('.');
994
- if (isUnsafeKey(first))
995
- continue;
996
- const list = nestedKeys.get(first);
997
- if (list) {
998
- list.push(rest.join('.'));
999
- }
1000
- else {
1001
- nestedKeys.set(first, [rest.join('.')]);
1002
- }
1003
- }
1004
- else {
1005
- directKeys.add(key);
1006
- }
1007
- }
1008
- const result = {};
1009
- for (const key of directKeys) {
1010
- if (key in obj)
1011
- result[key] = obj[key];
1012
- }
1013
- for (const [first, restKeys] of nestedKeys) {
1014
- const val = obj[first];
1015
- if (!val || typeof val !== 'object')
1016
- continue;
1017
- if (Array.isArray(val)) {
1018
- result[first] = val.map(item => pick(item, restKeys));
1019
- }
1020
- else {
1021
- result[first] = pick(val, restKeys);
1022
- }
1023
- }
1024
- return result;
1025
- };
1026
- if (Array.isArray(data)) {
1027
- return data.map(item => pick(item, fields));
1028
- }
1029
- return pick(data, fields);
1030
- }
1031
975
  sanitizeOutput(data) {
1032
976
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1033
977
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -33,7 +33,6 @@ export default class BankIntegrations 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;