@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.
- package/README.md +30 -8
- package/dist/commands/access-groups.d.ts +0 -1
- package/dist/commands/access-groups.js +5 -61
- package/dist/commands/account-transaction-allocations.d.ts +0 -1
- package/dist/commands/account-transaction-allocations.js +5 -61
- package/dist/commands/account-transactions.d.ts +0 -1
- package/dist/commands/account-transactions.js +9 -63
- package/dist/commands/accounts.d.ts +0 -1
- package/dist/commands/accounts.js +5 -61
- package/dist/commands/agent-platform.d.ts +0 -1
- package/dist/commands/agent-platform.js +5 -61
- package/dist/commands/analytics.d.ts +0 -1
- package/dist/commands/analytics.js +5 -61
- package/dist/commands/api-keys.d.ts +39 -0
- package/dist/commands/api-keys.js +1117 -0
- package/dist/commands/audit-log.d.ts +0 -1
- package/dist/commands/audit-log.js +5 -61
- package/dist/commands/auth/login.d.ts +9 -0
- package/dist/commands/auth/login.js +73 -20
- package/dist/commands/auth/logout.d.ts +4 -0
- package/dist/commands/auth/logout.js +4 -0
- package/dist/commands/auth/status.js +26 -17
- package/dist/commands/auth/tenant.js +3 -6
- package/dist/commands/auth.d.ts +0 -1
- package/dist/commands/auth.js +5 -34
- package/dist/commands/bank-accounts.d.ts +0 -1
- package/dist/commands/bank-accounts.js +5 -61
- package/dist/commands/bank-integrations.d.ts +0 -1
- package/dist/commands/bank-integrations.js +5 -61
- package/dist/commands/business-partners.d.ts +1 -1
- package/dist/commands/business-partners.js +14 -63
- package/dist/commands/chat.d.ts +0 -1
- package/dist/commands/chat.js +5 -61
- package/dist/commands/config/add.d.ts +3 -0
- package/dist/commands/config/add.js +34 -16
- package/dist/commands/config/current.js +3 -0
- package/dist/commands/config/list.js +3 -0
- package/dist/commands/credit-card-accounts.d.ts +0 -1
- package/dist/commands/credit-card-accounts.js +5 -61
- package/dist/commands/delivery-notes.d.ts +0 -1
- package/dist/commands/delivery-notes.js +7 -63
- package/dist/commands/dunning-configs.d.ts +0 -1
- package/dist/commands/dunning-configs.js +5 -61
- package/dist/commands/dunning-reminders.d.ts +0 -1
- package/dist/commands/dunning-reminders.js +5 -61
- package/dist/commands/dunning-runs.d.ts +0 -1
- package/dist/commands/dunning-runs.js +5 -61
- package/dist/commands/email-transactions.d.ts +0 -1
- package/dist/commands/email-transactions.js +5 -61
- package/dist/commands/employees.d.ts +0 -1
- package/dist/commands/employees.js +5 -61
- package/dist/commands/entity-configurations.d.ts +0 -1
- package/dist/commands/entity-configurations.js +5 -61
- package/dist/commands/event-processor-mappings.d.ts +0 -1
- package/dist/commands/event-processor-mappings.js +5 -61
- package/dist/commands/files.d.ts +0 -1
- package/dist/commands/files.js +5 -61
- package/dist/commands/health.d.ts +0 -1
- package/dist/commands/health.js +5 -61
- package/dist/commands/inbox.d.ts +0 -1
- package/dist/commands/inbox.js +5 -61
- package/dist/commands/integrations.d.ts +0 -1
- package/dist/commands/integrations.js +5 -61
- package/dist/commands/item-categories.d.ts +0 -1
- package/dist/commands/item-categories.js +5 -61
- package/dist/commands/items.d.ts +0 -1
- package/dist/commands/items.js +5 -61
- package/dist/commands/journal-lines.d.ts +0 -1
- package/dist/commands/journal-lines.js +5 -61
- package/dist/commands/journals.d.ts +1 -1
- package/dist/commands/journals.js +9 -62
- package/dist/commands/ledgers.d.ts +0 -1
- package/dist/commands/ledgers.js +5 -61
- package/dist/commands/nango-integrations.d.ts +39 -0
- package/dist/commands/nango-integrations.js +1115 -0
- package/dist/commands/office-locations.d.ts +0 -1
- package/dist/commands/office-locations.js +5 -61
- package/dist/commands/payment-methods.d.ts +0 -1
- package/dist/commands/payment-methods.js +5 -61
- package/dist/commands/payment-runs.d.ts +0 -1
- package/dist/commands/payment-runs.js +5 -61
- package/dist/commands/payment-terms.d.ts +0 -1
- package/dist/commands/payment-terms.js +5 -61
- package/dist/commands/payments.d.ts +0 -1
- package/dist/commands/payments.js +5 -61
- package/dist/commands/price-lists.d.ts +0 -1
- package/dist/commands/price-lists.js +5 -61
- package/dist/commands/processor-mappings.d.ts +0 -1
- package/dist/commands/processor-mappings.js +5 -61
- package/dist/commands/processors.d.ts +0 -1
- package/dist/commands/processors.js +5 -61
- package/dist/commands/purchase-order-types.d.ts +0 -1
- package/dist/commands/purchase-order-types.js +5 -61
- package/dist/commands/purchase-orders.d.ts +0 -1
- package/dist/commands/purchase-orders.js +7 -63
- package/dist/commands/reports.d.ts +0 -1
- package/dist/commands/reports.js +6 -62
- package/dist/commands/salary-types.d.ts +0 -1
- package/dist/commands/salary-types.js +5 -61
- package/dist/commands/sales-invoices.d.ts +0 -1
- package/dist/commands/sales-invoices.js +12 -65
- package/dist/commands/sales-orders.d.ts +0 -1
- package/dist/commands/sales-orders.js +7 -63
- package/dist/commands/saved-views.d.ts +0 -1
- package/dist/commands/saved-views.js +5 -61
- package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
- package/dist/commands/scheduled-processor-mappings.js +5 -61
- package/dist/commands/schedules.d.ts +0 -1
- package/dist/commands/schedules.js +5 -61
- package/dist/commands/switch.js +4 -4
- package/dist/commands/tag-configurations.d.ts +0 -1
- package/dist/commands/tag-configurations.js +9 -64
- package/dist/commands/tags.d.ts +0 -1
- package/dist/commands/tags.js +5 -61
- package/dist/commands/tax-codes.d.ts +0 -1
- package/dist/commands/tax-codes.js +5 -61
- package/dist/commands/tax-matrix.d.ts +0 -1
- package/dist/commands/tax-matrix.js +5 -61
- package/dist/commands/tenants.d.ts +0 -1
- package/dist/commands/tenants.js +7 -62
- package/dist/commands/text-templates.d.ts +0 -1
- package/dist/commands/text-templates.js +5 -61
- package/dist/commands/tools.d.ts +0 -1
- package/dist/commands/tools.js +5 -61
- package/dist/commands/vat-declarations.d.ts +1 -1
- package/dist/commands/vat-declarations.js +11 -62
- package/dist/commands/vendor-invoices.d.ts +0 -1
- package/dist/commands/vendor-invoices.js +10 -65
- package/dist/commands/webhooks.d.ts +0 -1
- package/dist/commands/webhooks.js +5 -61
- package/dist/commands/workflows.d.ts +0 -1
- package/dist/commands/workflows.js +5 -61
- package/dist/lib/api-client.js +3 -6
- package/dist/lib/auth/constants.d.ts +11 -0
- package/dist/lib/auth/constants.js +11 -0
- package/dist/lib/auth/errors.d.ts +17 -0
- package/dist/lib/auth/errors.js +22 -0
- package/dist/lib/auth/flow.d.ts +27 -0
- package/dist/lib/auth/flow.js +163 -0
- package/dist/lib/auth/guards.d.ts +5 -0
- package/dist/lib/auth/guards.js +14 -0
- package/dist/lib/auth/http.d.ts +16 -0
- package/dist/lib/auth/http.js +49 -0
- package/dist/lib/auth/index.d.ts +49 -0
- package/dist/lib/auth/index.js +217 -0
- package/dist/lib/auth/keycloak-provider.d.ts +75 -0
- package/dist/lib/auth/keycloak-provider.js +215 -0
- package/dist/lib/auth/pages.d.ts +25 -0
- package/dist/lib/auth/pages.js +123 -0
- package/dist/lib/auth/pkce.d.ts +11 -0
- package/dist/lib/auth/pkce.js +17 -0
- package/dist/lib/auth/tokens.d.ts +54 -0
- package/dist/lib/auth/tokens.js +104 -0
- package/dist/lib/auth/types.d.ts +57 -0
- package/dist/lib/auth/types.js +1 -0
- package/dist/lib/auth/validated-url.d.ts +6 -0
- package/dist/lib/auth/validated-url.js +53 -0
- package/dist/lib/config.d.ts +13 -3
- package/dist/lib/config.js +24 -19
- package/oclif.manifest.json +440 -71
- package/package.json +2 -1
- package/dist/lib/auth.d.ts +0 -43
- package/dist/lib/auth.js +0 -610
|
@@ -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
|
|
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,
|
|
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
|
|
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.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
104
|
+
this.log(`API key: ${maskApiKey(fresh.api_key)}`);
|
|
90
105
|
}
|
|
91
106
|
else {
|
|
92
|
-
this.log(`Tenant ID: ${
|
|
93
|
-
if (
|
|
94
|
-
const expiresAt = new Date(
|
|
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 (
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/auth.d.ts
CHANGED
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
|
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);
|
|
@@ -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
|
|
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;
|