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