@aderis-erp/cli 1.0.15 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -8
- package/dist/commands/access-groups.d.ts +0 -1
- package/dist/commands/access-groups.js +54 -78
- package/dist/commands/account-transaction-allocations.d.ts +0 -1
- package/dist/commands/account-transaction-allocations.js +51 -75
- package/dist/commands/account-transactions.d.ts +0 -1
- package/dist/commands/account-transactions.js +80 -94
- package/dist/commands/accounts.d.ts +0 -1
- package/dist/commands/accounts.js +59 -83
- package/dist/commands/agent-platform.d.ts +0 -1
- package/dist/commands/agent-platform.js +58 -82
- package/dist/commands/agent-runs.d.ts +39 -0
- package/dist/commands/agent-runs.js +1155 -0
- package/dist/commands/analytics.d.ts +0 -1
- package/dist/commands/analytics.js +52 -76
- package/dist/commands/{text-templates.d.ts → api-keys.d.ts} +1 -2
- package/dist/commands/api-keys.js +1150 -0
- package/dist/commands/async-jobs.d.ts +39 -0
- package/dist/commands/async-jobs.js +1158 -0
- package/dist/commands/audit-log.d.ts +0 -1
- package/dist/commands/audit-log.js +51 -75
- 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 +68 -91
- package/dist/commands/bank-integrations.d.ts +1 -1
- package/dist/commands/bank-integrations.js +71 -92
- package/dist/commands/business-partners.d.ts +1 -1
- package/dist/commands/business-partners.js +84 -100
- package/dist/commands/chat.d.ts +0 -1
- package/dist/commands/chat.js +57 -80
- 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 +1 -1
- package/dist/commands/credit-card-accounts.js +72 -90
- package/dist/commands/delivery-notes.d.ts +0 -1
- package/dist/commands/delivery-notes.js +83 -105
- package/dist/commands/dunning-configs.d.ts +0 -1
- package/dist/commands/dunning-configs.js +54 -78
- package/dist/commands/dunning-reminders.d.ts +0 -1
- package/dist/commands/dunning-reminders.js +57 -81
- package/dist/commands/dunning-runs.d.ts +0 -1
- package/dist/commands/dunning-runs.js +50 -74
- package/dist/commands/email-transactions.d.ts +0 -1
- package/dist/commands/email-transactions.js +52 -76
- package/dist/commands/employees.d.ts +0 -1
- package/dist/commands/employees.js +81 -105
- package/dist/commands/entity-configurations.d.ts +0 -1
- package/dist/commands/entity-configurations.js +52 -76
- package/dist/commands/event-processor-mappings.d.ts +0 -1
- package/dist/commands/event-processor-mappings.js +54 -78
- package/dist/commands/files.d.ts +0 -1
- package/dist/commands/files.js +61 -85
- package/dist/commands/health.d.ts +0 -1
- package/dist/commands/health.js +54 -78
- package/dist/commands/imports.d.ts +39 -0
- package/dist/commands/imports.js +1151 -0
- package/dist/commands/inbox.d.ts +0 -1
- package/dist/commands/inbox.js +57 -80
- package/dist/commands/integration-identities.d.ts +40 -0
- package/dist/commands/integration-identities.js +1151 -0
- package/dist/commands/integrations.d.ts +0 -1
- package/dist/commands/integrations.js +52 -76
- package/dist/commands/item-categories.d.ts +0 -1
- package/dist/commands/item-categories.js +54 -78
- package/dist/commands/items.d.ts +0 -1
- package/dist/commands/items.js +54 -78
- package/dist/commands/journal-lines.d.ts +0 -1
- package/dist/commands/journal-lines.js +50 -74
- package/dist/commands/journals.d.ts +1 -1
- package/dist/commands/journals.js +75 -96
- package/dist/commands/ledgers.d.ts +3 -2
- package/dist/commands/ledgers.js +100 -95
- package/dist/commands/my-account.d.ts +39 -0
- package/dist/commands/my-account.js +1160 -0
- package/dist/commands/office-locations.d.ts +0 -1
- package/dist/commands/office-locations.js +54 -78
- package/dist/commands/payment-methods.d.ts +0 -1
- package/dist/commands/payment-methods.js +59 -83
- package/dist/commands/payment-runs.d.ts +0 -1
- package/dist/commands/payment-runs.js +66 -90
- package/dist/commands/payment-terms.d.ts +0 -1
- package/dist/commands/payment-terms.js +55 -79
- package/dist/commands/payments.d.ts +0 -1
- package/dist/commands/payments.js +56 -80
- package/dist/commands/payroll-runs.d.ts +43 -0
- package/dist/commands/payroll-runs.js +1188 -0
- package/dist/commands/permissions.d.ts +39 -0
- package/dist/commands/permissions.js +1146 -0
- package/dist/commands/price-lists.d.ts +0 -1
- package/dist/commands/price-lists.js +59 -83
- package/dist/commands/processor-mappings.d.ts +0 -1
- package/dist/commands/processor-mappings.js +53 -77
- package/dist/commands/processors.d.ts +0 -1
- package/dist/commands/processors.js +60 -85
- package/dist/commands/purchase-order-types.d.ts +0 -1
- package/dist/commands/purchase-order-types.js +54 -78
- package/dist/commands/purchase-orders.d.ts +0 -1
- package/dist/commands/purchase-orders.js +74 -97
- package/dist/commands/reports.d.ts +0 -1
- package/dist/commands/reports.js +54 -78
- package/dist/commands/roles.d.ts +39 -0
- package/dist/commands/{text-templates.js → roles.js} +64 -90
- package/dist/commands/salary-types.d.ts +0 -1
- package/dist/commands/salary-types.js +54 -78
- package/dist/commands/sales-invoices.d.ts +0 -1
- package/dist/commands/sales-invoices.js +90 -110
- package/dist/commands/sales-orders.d.ts +0 -1
- package/dist/commands/sales-orders.js +83 -105
- package/dist/commands/saved-views.d.ts +0 -1
- package/dist/commands/saved-views.js +52 -76
- package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
- package/dist/commands/scheduled-processor-mappings.js +54 -78
- package/dist/commands/schedules.d.ts +0 -1
- package/dist/commands/schedules.js +58 -82
- package/dist/commands/switch.js +4 -4
- package/dist/commands/tag-configurations.d.ts +0 -1
- package/dist/commands/tag-configurations.js +56 -79
- package/dist/commands/tags.d.ts +0 -1
- package/dist/commands/tags.js +54 -78
- package/dist/commands/tax-codes.d.ts +0 -1
- package/dist/commands/tax-codes.js +57 -81
- package/dist/commands/tax-matrix.d.ts +0 -1
- package/dist/commands/tax-matrix.js +54 -78
- package/dist/commands/templates.d.ts +39 -0
- package/dist/commands/templates.js +1157 -0
- package/dist/commands/tenants.d.ts +0 -1
- package/dist/commands/tenants.js +58 -81
- package/dist/commands/tools.d.ts +0 -1
- package/dist/commands/tools.js +51 -75
- package/dist/commands/users.d.ts +42 -0
- package/dist/commands/users.js +1174 -0
- package/dist/commands/vat-declarations.d.ts +3 -1
- package/dist/commands/vat-declarations.js +78 -93
- package/dist/commands/vendor-invoices.d.ts +0 -1
- package/dist/commands/vendor-invoices.js +87 -110
- package/dist/commands/webhooks.d.ts +0 -1
- package/dist/commands/webhooks.js +56 -80
- package/dist/commands/workflows.d.ts +0 -1
- package/dist/commands/workflows.js +52 -76
- package/dist/lib/api-client.d.ts +6 -0
- package/dist/lib/api-client.js +16 -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 +228 -0
- package/dist/lib/auth/jwks.d.ts +19 -0
- package/dist/lib/auth/jwks.js +112 -0
- package/dist/lib/auth/keycloak-provider.d.ts +91 -0
- package/dist/lib/auth/keycloak-provider.js +244 -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 +59 -0
- package/dist/lib/auth/tokens.js +105 -0
- package/dist/lib/auth/types.d.ts +61 -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 +2447 -641
- package/package.json +5 -4
- package/dist/lib/auth.d.ts +0 -43
- package/dist/lib/auth.js +0 -610
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
5
|
const HAS_IMPORT = false;
|
|
@@ -8,8 +8,8 @@ const HAS_CAMT053 = false;
|
|
|
8
8
|
const NESTED_IMPORTS = [];
|
|
9
9
|
const SUBCOMMANDS = [...["list", "get"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'list': { method: 'GET', path: '/v1/audit-logs', summary: 'Get audit log entries', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'get': { method: 'GET', path: '/v1/audit-logs/{id}', summary: 'Get a single audit log entry', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'list': { method: 'GET', path: '/v1/audit-logs', summary: 'Get audit log entries', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'get': { method: 'GET', path: '/v1/audit-logs/{id}', summary: 'Get a single audit log entry', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
13
13
|
};
|
|
14
14
|
export default class AuditLog extends Command {
|
|
15
15
|
static description = 'Manage audit log';
|
|
@@ -37,7 +37,7 @@ export default class AuditLog extends Command {
|
|
|
37
37
|
'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
|
|
38
38
|
filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
|
|
39
39
|
query: Flags.string({ description: 'Additional query params as JSON' }),
|
|
40
|
-
fields: Flags.string({ description: 'Comma-separated list of fields to
|
|
40
|
+
fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
|
|
41
41
|
sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
|
|
42
42
|
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
43
43
|
};
|
|
@@ -109,6 +109,8 @@ export default class AuditLog extends Command {
|
|
|
109
109
|
queryParams.search = flags.search;
|
|
110
110
|
if (flags['sort-by'])
|
|
111
111
|
queryParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
112
|
+
if (flags.fields)
|
|
113
|
+
queryParams.fields = flags.fields;
|
|
112
114
|
if (flags.query) {
|
|
113
115
|
try {
|
|
114
116
|
Object.assign(queryParams, JSON.parse(flags.query));
|
|
@@ -256,10 +258,11 @@ export default class AuditLog extends Command {
|
|
|
256
258
|
process.stderr.write('\n');
|
|
257
259
|
}
|
|
258
260
|
try {
|
|
259
|
-
// Binary response:
|
|
261
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
260
262
|
if (endpoint.isBinaryResponse) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
const toStdout = !flags.file;
|
|
264
|
+
if (toStdout && process.stdout.isTTY) {
|
|
265
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
263
266
|
}
|
|
264
267
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
265
268
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -267,24 +270,55 @@ export default class AuditLog extends Command {
|
|
|
267
270
|
contextName: flags.context,
|
|
268
271
|
binary: true,
|
|
269
272
|
});
|
|
270
|
-
const fsModule = await import('node:fs');
|
|
271
273
|
const binData = binResponse.data;
|
|
272
274
|
const buffer = Buffer.isBuffer(binData)
|
|
273
275
|
? binData
|
|
274
276
|
: Buffer.from(binData);
|
|
277
|
+
if (toStdout) {
|
|
278
|
+
// Nothing else may reach stdout here — it is the file.
|
|
279
|
+
process.stdout.write(buffer);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const fsModule = await import('node:fs');
|
|
275
283
|
fsModule.writeFileSync(flags.file, buffer);
|
|
276
284
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
277
285
|
return;
|
|
278
286
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
let response;
|
|
288
|
+
if (endpoint.hasBinaryVariant) {
|
|
289
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
290
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
291
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
292
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
293
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
294
|
+
data,
|
|
295
|
+
contextName: flags.context,
|
|
296
|
+
binary: true,
|
|
297
|
+
});
|
|
298
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
299
|
+
const rawData = raw.data;
|
|
300
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
301
|
+
if (!isTextualMediaType(rawType)) {
|
|
302
|
+
if (process.stdout.isTTY) {
|
|
303
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
304
|
+
}
|
|
305
|
+
// Nothing else may reach stdout here — it is the file.
|
|
306
|
+
process.stdout.write(buffer);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
313
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
314
|
+
data,
|
|
315
|
+
contextName: flags.context,
|
|
316
|
+
acceptAny: true,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
285
319
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
286
320
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
287
|
-
if (contentType
|
|
321
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
288
322
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
289
323
|
this.log(String(response.data));
|
|
290
324
|
return;
|
|
@@ -311,11 +345,6 @@ export default class AuditLog extends Command {
|
|
|
311
345
|
output = obj[arrayKeys[0]];
|
|
312
346
|
}
|
|
313
347
|
}
|
|
314
|
-
// Filter fields if --fields flag is provided
|
|
315
|
-
if (flags.fields) {
|
|
316
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
317
|
-
output = this.filterFields(output, fieldList);
|
|
318
|
-
}
|
|
319
348
|
// Sanitize output if requested
|
|
320
349
|
if (flags.sanitize) {
|
|
321
350
|
output = this.sanitizeOutput(output);
|
|
@@ -570,6 +599,8 @@ export default class AuditLog extends Command {
|
|
|
570
599
|
baseParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
571
600
|
if (flags.include)
|
|
572
601
|
baseParams.include = flags.include;
|
|
602
|
+
if (flags.fields)
|
|
603
|
+
baseParams.fields = flags.fields;
|
|
573
604
|
if (flags.query) {
|
|
574
605
|
try {
|
|
575
606
|
Object.assign(baseParams, JSON.parse(flags.query));
|
|
@@ -624,12 +655,7 @@ export default class AuditLog extends Command {
|
|
|
624
655
|
}
|
|
625
656
|
}
|
|
626
657
|
process.stderr.write(' done\n');
|
|
627
|
-
// Filter fields if specified
|
|
628
658
|
let output = allItems;
|
|
629
|
-
if (flags.fields) {
|
|
630
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
631
|
-
output = this.filterFields(allItems, fieldList);
|
|
632
|
-
}
|
|
633
659
|
// Sanitize output if requested
|
|
634
660
|
if (flags.sanitize) {
|
|
635
661
|
output = this.sanitizeOutput(output);
|
|
@@ -958,56 +984,6 @@ export default class AuditLog extends Command {
|
|
|
958
984
|
return `\$${op}`;
|
|
959
985
|
return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
|
|
960
986
|
}
|
|
961
|
-
filterFields(data, fields) {
|
|
962
|
-
const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
|
|
963
|
-
const pick = (obj, keys) => {
|
|
964
|
-
// Group keys by their first segment so sibling sub-paths
|
|
965
|
-
// (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
|
|
966
|
-
// and don't clobber each other on merge.
|
|
967
|
-
const directKeys = new Set();
|
|
968
|
-
const nestedKeys = new Map();
|
|
969
|
-
for (const key of keys) {
|
|
970
|
-
if (key.split('.').some(isUnsafeKey))
|
|
971
|
-
continue;
|
|
972
|
-
if (key.includes('.')) {
|
|
973
|
-
const [first, ...rest] = key.split('.');
|
|
974
|
-
if (isUnsafeKey(first))
|
|
975
|
-
continue;
|
|
976
|
-
const list = nestedKeys.get(first);
|
|
977
|
-
if (list) {
|
|
978
|
-
list.push(rest.join('.'));
|
|
979
|
-
}
|
|
980
|
-
else {
|
|
981
|
-
nestedKeys.set(first, [rest.join('.')]);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
else {
|
|
985
|
-
directKeys.add(key);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
const result = {};
|
|
989
|
-
for (const key of directKeys) {
|
|
990
|
-
if (key in obj)
|
|
991
|
-
result[key] = obj[key];
|
|
992
|
-
}
|
|
993
|
-
for (const [first, restKeys] of nestedKeys) {
|
|
994
|
-
const val = obj[first];
|
|
995
|
-
if (!val || typeof val !== 'object')
|
|
996
|
-
continue;
|
|
997
|
-
if (Array.isArray(val)) {
|
|
998
|
-
result[first] = val.map(item => pick(item, restKeys));
|
|
999
|
-
}
|
|
1000
|
-
else {
|
|
1001
|
-
result[first] = pick(val, restKeys);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
return result;
|
|
1005
|
-
};
|
|
1006
|
-
if (Array.isArray(data)) {
|
|
1007
|
-
return data.map(item => pick(item, fields));
|
|
1008
|
-
}
|
|
1009
|
-
return pick(data, fields);
|
|
1010
|
-
}
|
|
1011
987
|
sanitizeOutput(data) {
|
|
1012
988
|
const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
1013
989
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import { type FlowHooks } from '../../lib/auth/index.js';
|
|
2
3
|
export default class AuthLogin extends Command {
|
|
3
4
|
static description: string;
|
|
4
5
|
static examples: string[];
|
|
@@ -6,9 +7,17 @@ export default class AuthLogin extends Command {
|
|
|
6
7
|
'client-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'keycloak-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
11
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
new: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
13
|
};
|
|
12
14
|
run(): Promise<void>;
|
|
15
|
+
/** Prompts for a tenant; throws {@link NoTerminalError} without a terminal, which `run()` turns into the hint. */
|
|
16
|
+
private chooseTenant;
|
|
17
|
+
/** Browser-flow hooks: the defaults here, overridden by tests. */
|
|
18
|
+
protected flowHooks(): FlowHooks;
|
|
19
|
+
private printSession;
|
|
20
|
+
/** Validates `--keycloak-url` before the context is stored. */
|
|
21
|
+
private resolveKeycloakBaseUrl;
|
|
13
22
|
private createNewContext;
|
|
14
23
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { input, select } from '@inquirer/prompts';
|
|
2
2
|
import { Command, Flags } from '@oclif/core';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
-
import { login, testAuth } from '../../lib/auth.js';
|
|
5
|
-
import { addContext,
|
|
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);
|