@knowttl/technitium-axi 0.0.0-stage → 0.1.1

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/dist/reads.js ADDED
@@ -0,0 +1,350 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { ToolError } from './errors.js';
3
+ import { readContracts } from './read-contracts.js';
4
+ import { checkStatus, vendorRequest } from './vendor.js';
5
+ import { validateNode } from './routing.js';
6
+ const invalid = () => { throw new ToolError('UPSTREAM_INVALID', 'Unexpected server response fields or types', 'Check the selected server API version and operation', 1); };
7
+ const usage = (message) => { throw new ToolError('USAGE', message, 'Run the selected command with --help'); };
8
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
9
+ const sensitive = /password|secret|^token$|^privateKey$|^stackTrace$|^innerError/i;
10
+ export function readInputs(contract, options) {
11
+ validateNode(contract.id, options['--node']);
12
+ const form = new URLSearchParams();
13
+ const headers = {};
14
+ for (const parameter of contract.parameters) {
15
+ const value = options[parameter.flag];
16
+ if (value === undefined) {
17
+ if (parameter.required)
18
+ usage(`Missing ${parameter.flag}`);
19
+ continue;
20
+ }
21
+ if (parameter.required && value === '')
22
+ usage(`Missing ${parameter.flag}`);
23
+ if (parameter.type === 'boolean' && !['true', 'false'].includes(value))
24
+ usage(`${parameter.flag} requires true or false`);
25
+ if (parameter.type === 'integer' && (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < (parameter.name === 'limit' ? 0 : 1) || Number(value) > 2147483647))
26
+ usage(`${parameter.flag} requires a bounded integer`);
27
+ if (parameter.values && !parameter.values.includes(value))
28
+ usage(`Invalid value for ${parameter.flag}`);
29
+ if (parameter.type === 'http-date' && (Number.isNaN(Date.parse(value)) || new Date(value).toUTCString() !== value))
30
+ usage(`${parameter.flag} requires an RFC 1123 GMT date`);
31
+ if (parameter.type === 'date' && (!/^\d{4}-\d\d-\d\dT/.test(value) || Number.isNaN(Date.parse(value))))
32
+ usage(`${parameter.flag} requires an ISO 8601 date time`);
33
+ if (parameter.type === 'zone-list' && value && value.split(',').some(zone => !/^(?=.{1,253}$)[\p{L}\p{N}_*-]+(?:\.[\p{L}\p{N}_*-]+)*\.?$/u.test(zone)))
34
+ usage(`${parameter.flag} requires comma-separated zone names`);
35
+ if (contract.id === 'OP-135' && parameter.name === 'fileName' && (/[\\/]/.test(value) || value === '.' || value === '..'))
36
+ usage('--file-name requires a log filename without a path');
37
+ if (/[\x00-\x1f\x7f]/.test(value) || value.length > 65536)
38
+ usage(`Invalid value for ${parameter.flag}`);
39
+ if (parameter.location === 'header')
40
+ headers[parameter.name] = value;
41
+ else
42
+ form.set(parameter.name, value);
43
+ }
44
+ return { form, headers };
45
+ }
46
+ // Fields the contract does not list are dropped; `omitted` collects them (indexes folded to []) so the caller can report incompleteness.
47
+ function project(value, shape, nullable = false, path = 'response', omitted) {
48
+ if (typeof shape === 'string') {
49
+ if (shape === 'unknown')
50
+ return null;
51
+ if (shape === 'json')
52
+ return value;
53
+ if (shape === 'nullable') {
54
+ if (value !== null && typeof value !== 'string')
55
+ invalid();
56
+ }
57
+ else if (shape === 'nullable-number') {
58
+ if (value !== null && typeof value !== 'number')
59
+ invalid();
60
+ }
61
+ else if (shape === 'string-or-number') {
62
+ // Protocol and flags vary between numeric data and enum names across record families.
63
+ if (typeof value !== 'string' && typeof value !== 'number')
64
+ invalid();
65
+ }
66
+ else if (typeof value !== shape)
67
+ invalid();
68
+ return value;
69
+ }
70
+ if ('array' in shape) {
71
+ if (!Array.isArray(value))
72
+ return invalid();
73
+ const itemShape = shape.array;
74
+ return value.map(item => {
75
+ if (record(itemShape) && !('array' in itemShape)) {
76
+ if (!record(item))
77
+ return invalid();
78
+ const identity = ['name', 'rowNumber', 'fileName', 'username', 'section', 'scope', 'keyTag'].find(key => Object.hasOwn(itemShape, key));
79
+ if (identity && !Object.hasOwn(item, identity))
80
+ return invalid();
81
+ }
82
+ return project(item, itemShape, false, `${path}[]`, omitted);
83
+ });
84
+ }
85
+ if (value === null && nullable)
86
+ return null;
87
+ if (!record(value))
88
+ return invalid();
89
+ if (Object.hasOwn(shape, 'canView') && ['canView', 'canModify', 'canDelete'].some(key => !Object.hasOwn(value, key)))
90
+ invalid();
91
+ if (omitted) {
92
+ const dropped = Object.keys(value).filter(key => !Object.hasOwn(shape, key) && !sensitive.test(key));
93
+ if (dropped.length)
94
+ omitted[path] = new Set([...omitted[path] ?? [], ...dropped]);
95
+ }
96
+ return Object.fromEntries(Object.entries(shape).filter(([key]) => !sensitive.test(key) && Object.hasOwn(value, key))
97
+ .map(([key, field]) => [key, project(value[key], field, key === 'proxy', `${path}.${key}`, omitted)]));
98
+ }
99
+ const listKeys = {
100
+ 'OP-020': 'zones', 'OP-021': 'catalogZoneNames', 'OP-037': 'dsRecords', 'OP-038': 'dnssecPrivateKeys',
101
+ 'OP-050': 'records', 'OP-053': 'zones', 'OP-056': 'zones', 'OP-062': 'zones', 'OP-068': 'apps',
102
+ 'OP-069': 'storeApps', 'OP-081': 'tsigKeyNames', 'OP-086': 'leases', 'OP-090': 'scopes',
103
+ 'OP-098': 'sessions', 'OP-101': 'users', 'OP-106': 'groups', 'OP-111': 'permissions',
104
+ 'OP-134': 'logFiles', 'OP-138': 'entries',
105
+ };
106
+ export async function readOperation(profile, command, token, options, full) {
107
+ const contract = readContracts.find(item => item.command === command);
108
+ const { form, headers } = readInputs(contract, options);
109
+ const listKey = listKeys[contract.id];
110
+ const listShape = listKey ? contract.response[listKey] : undefined;
111
+ const rowShape = listShape && typeof listShape === 'object' && 'array' in listShape ? listShape.array : undefined;
112
+ const fieldShape = rowShape && typeof rowShape === 'object' ? rowShape : contract.response;
113
+ const fields = options['--fields']?.split(',');
114
+ if (fields && (new Set(fields).size !== fields.length || fields.some(field => !Object.hasOwn(fieldShape, field) || sensitive.test(field))))
115
+ usage('Unknown or repeated output field');
116
+ const cap = (flag, fallback, maximum) => {
117
+ const raw = options[flag];
118
+ if (raw === undefined)
119
+ return fallback;
120
+ if (!/^[1-9]\d*$/.test(raw) || Number(raw) > maximum)
121
+ usage(`${flag} requires an integer from 1 to ${maximum}`);
122
+ return Number(raw);
123
+ };
124
+ const byteCap = cap('--max-bytes', 16 * 1024 * 1024, 64 * 1024 * 1024);
125
+ const requestCap = cap('--max-requests', 12, 100);
126
+ const pageCap = cap('--max-pages', 10, 100);
127
+ const elapsedCap = cap('--max-elapsed-ms', 30000, 120000);
128
+ const started = Date.now();
129
+ let requests = 0;
130
+ let bytesRead = 0;
131
+ const request = async () => {
132
+ let response;
133
+ for (let attempt = 0; attempt < 2; attempt++) {
134
+ if (++requests > requestCap || Date.now() - started >= elapsedCap)
135
+ throw new ToolError('READ_LIMIT', 'Read request or elapsed cap reached', 'Resume with narrower filters or a larger bounded cap', 1);
136
+ response = await vendorRequest(profile, contract.path, form.toString(), token, headers, { bytes: byteCap - bytesRead, elapsedMs: elapsedCap - (Date.now() - started) });
137
+ bytesRead += response.bytes.length;
138
+ if (![429, 502, 503, 504].includes(response.status) || profile.tokenKind === 'single-use' || attempt || requests === requestCap)
139
+ break;
140
+ // Never ignore server backoff. Retry only immediate transient reads; return longer guidance.
141
+ if (response.retryAfter && response.retryAfter !== '0')
142
+ break;
143
+ }
144
+ if (response.status !== 200) {
145
+ const code = response.status === 401 ? 'AUTH_REQUIRED' : response.status === 403 ? 'PERMISSION_DENIED'
146
+ : response.status === 404 ? 'UNSUPPORTED_OPERATION' : response.status === 429 ? 'RATE_LIMITED' : 'UPSTREAM_HTTP';
147
+ const backoff = response.retryAfter;
148
+ const hint = backoff && /^\d{1,9}$/.test(backoff) ? `Retry after ${backoff} seconds` : 'Check the selected server version, credential and permissions';
149
+ throw new ToolError(code, 'Server rejected the read', hint, 1);
150
+ }
151
+ return response;
152
+ };
153
+ const paged = ['OP-020', 'OP-138'].includes(contract.id);
154
+ const pageSizeField = contract.id === 'OP-020' ? 'zonesPerPage' : 'entriesPerPage';
155
+ if (paged) {
156
+ if (!form.has('pageNumber'))
157
+ form.set('pageNumber', '1');
158
+ if (!form.has(pageSizeField))
159
+ form.set(pageSizeField, '100');
160
+ }
161
+ const response = await request();
162
+ const metadata = { bytes: response.bytes.length, contentType: response.contentType,
163
+ sha256: createHash('sha256').update(response.bytes).digest('hex'),
164
+ ...(contract.id === 'OP-125' ? { lastModified: response.lastModified } : {}) };
165
+ if (contract.kind !== 'json') {
166
+ if (response.contentType === 'application/json' || /^\s*\{\s*"(?:status|errorMessage)"\s*:/.test(response.bytes.toString('utf8', 0, 64))) {
167
+ let value;
168
+ try {
169
+ value = JSON.parse(response.bytes.toString('utf8'));
170
+ }
171
+ catch {
172
+ invalid();
173
+ }
174
+ checkStatus(value);
175
+ invalid();
176
+ }
177
+ if (contract.id === 'OP-125' && (!response.lastModified || Number.isNaN(Date.parse(response.lastModified)) || new Date(response.lastModified).toUTCString() !== response.lastModified))
178
+ invalid();
179
+ const expected = contract.kind === 'zip' ? ['application/zip', 'application/octet-stream']
180
+ : contract.kind === 'csv' ? ['text/csv', 'text/plain', 'application/octet-stream'] : ['text/plain', 'application/octet-stream'];
181
+ if (!expected.includes(response.contentType))
182
+ invalid();
183
+ if (contract.kind === 'zip' && (!response.bytes.subarray(0, 2).equals(Buffer.from('PK')) || response.bytes.length < 22))
184
+ invalid();
185
+ return { bytes: response.bytes, projection: { status: 'ok', operation: contract.id, ...metadata,
186
+ ...(contract.kind === 'prometheus-text' ? { text: response.bytes.toString('utf8').slice(0, full ? 100000 : 1000),
187
+ complete: response.bytes.length <= (full ? 100000 : 1000) } : {}) } };
188
+ }
189
+ const decode = (bytes) => {
190
+ let value;
191
+ try {
192
+ value = JSON.parse(bytes.toString('utf8'));
193
+ }
194
+ catch {
195
+ invalid();
196
+ }
197
+ checkStatus(value);
198
+ if (contract.id === 'OP-078') {
199
+ if (typeof value.server !== 'string')
200
+ return invalid();
201
+ return value;
202
+ }
203
+ if (!record(value.response))
204
+ return invalid();
205
+ const payload = value.response;
206
+ if (contract.id === 'OP-015' && (!record(payload.lifetimeCounters) || typeof payload.uptimeSeconds !== 'number'))
207
+ invalid();
208
+ if (contract.id === 'OP-075' && payload.config !== null && typeof payload.config !== 'string')
209
+ invalid();
210
+ const required = contract.id === 'OP-018' ? ({ TopClients: 'topClients', TopDomains: 'topDomains', TopBlockedDomains: 'topBlockedDomains' }[form.get('statsType')])
211
+ : listKeys[contract.id] ?? Object.keys(contract.response)[0];
212
+ if (required && !Object.hasOwn(payload, required))
213
+ invalid();
214
+ return payload;
215
+ };
216
+ if (response.contentType !== 'application/json')
217
+ invalid();
218
+ const payload = decode(response.bytes);
219
+ if (contract.id === 'OP-075')
220
+ return { bytes: response.bytes, projection: { status: 'ok', operation: contract.id, ...metadata } };
221
+ let pages = 1;
222
+ let complete = true;
223
+ let continuation;
224
+ let stoppedBy;
225
+ if (paged && listKey) {
226
+ const validPage = (value, page) => Array.isArray(value[listKey]) &&
227
+ (value.pageNumber === page || (contract.id === 'OP-020' && value.pageNumber === 0 &&
228
+ value.totalPages === 0 && value.totalZones === 0 && value[listKey].length === 0));
229
+ if (!validPage(payload, Number(form.get('pageNumber'))))
230
+ invalid();
231
+ const identity = (row) => {
232
+ if (!record(row) || (contract.id === 'OP-020' ? typeof row.name !== 'string' : !Number.isSafeInteger(row.rowNumber)))
233
+ return invalid();
234
+ return JSON.stringify(contract.id === 'OP-020' ? row.name : row.rowNumber);
235
+ };
236
+ const rows = [];
237
+ const seen = new Set();
238
+ for (const row of payload[listKey]) {
239
+ const key = identity(row);
240
+ if (!seen.has(key)) {
241
+ seen.add(key);
242
+ rows.push(row);
243
+ }
244
+ }
245
+ payload[listKey] = rows;
246
+ let page = Number(form.get('pageNumber'));
247
+ const hasNext = (value) => {
248
+ if (value.totalPages !== undefined && (!Number.isInteger(value.totalPages) || Number(value.totalPages) < 0))
249
+ invalid();
250
+ return value.totalPages === undefined ? value[listKey].length > 0 : page < Number(value.totalPages);
251
+ };
252
+ let last = payload;
253
+ while (hasNext(last)) {
254
+ continuation = page + 1;
255
+ complete = false;
256
+ if (!full || pages >= pageCap || requests >= requestCap || Date.now() - started >= elapsedCap || bytesRead >= byteCap)
257
+ break;
258
+ form.set('pageNumber', String(++page));
259
+ let next;
260
+ try {
261
+ next = await request();
262
+ }
263
+ catch (error) {
264
+ if (!(error instanceof ToolError) || !['RESPONSE_LIMIT', 'READ_LIMIT', 'READ_TIMEOUT'].includes(error.code))
265
+ throw error;
266
+ stoppedBy = error.code;
267
+ break;
268
+ }
269
+ if (next.contentType !== 'application/json')
270
+ invalid();
271
+ last = decode(next.bytes);
272
+ if (!validPage(last, page))
273
+ invalid();
274
+ for (const row of last[listKey]) {
275
+ const key = identity(row);
276
+ if (!seen.has(key)) {
277
+ seen.add(key);
278
+ rows.push(row);
279
+ }
280
+ }
281
+ pages++;
282
+ complete = true;
283
+ continuation = undefined;
284
+ }
285
+ const total = payload[contract.id === 'OP-020' ? 'totalZones' : 'totalEntries'];
286
+ if (total !== undefined && (!Number.isSafeInteger(total) || Number(total) < 0))
287
+ invalid();
288
+ if (complete && total !== undefined && Number(options['--page-number'] ?? 1) === 1 && rows.length !== total) {
289
+ complete = false;
290
+ stoppedBy = 'PROVIDER_TOTAL_MISMATCH';
291
+ }
292
+ }
293
+ // DNSSEC properties and record-bearing reads must disclose omitted fields before claiming completeness.
294
+ const omitted = ['OP-038', 'OP-050', 'OP-053'].includes(contract.id) ? {} : undefined;
295
+ let projected = project(payload, contract.response, false, 'response', omitted);
296
+ const omittedFields = Object.fromEntries(Object.entries(omitted ?? {}).map(([path, keys]) => [path, [...keys].sort()]));
297
+ if (Object.keys(omittedFields).length)
298
+ complete = false;
299
+ if (fields && !(rowShape && typeof rowShape === 'object')) {
300
+ projected = Object.fromEntries(fields.filter(field => Object.hasOwn(projected, field)).map(field => [field, projected[field]]));
301
+ }
302
+ const bounds = {};
303
+ const truncations = {};
304
+ const bound = (value, path) => {
305
+ const limit = full ? 10000 : 100;
306
+ if (typeof value === 'string') {
307
+ const stringLimit = full ? 1000000 : 1000;
308
+ if (value.length > stringLimit) {
309
+ complete = false;
310
+ truncations[path] = { returned: stringLimit, available: value.length };
311
+ }
312
+ return value.slice(0, stringLimit);
313
+ }
314
+ if (Array.isArray(value)) {
315
+ if (value.length > limit) {
316
+ complete = false;
317
+ truncations[path] = { returned: limit, available: value.length };
318
+ }
319
+ return value.slice(0, limit).map((row, index) => bound(row, `${path}[${index}]`));
320
+ }
321
+ if (record(value))
322
+ return Object.fromEntries(Object.entries(value).map(([key, field]) => [key, bound(field, `${path}.${key}`)]));
323
+ return value;
324
+ };
325
+ for (const [key, value] of Object.entries(projected)) {
326
+ if (!Array.isArray(value))
327
+ continue;
328
+ const limit = full ? 10000 : 100;
329
+ let selected = value.slice(0, limit);
330
+ bounds[key] = { returned: selected.length, available: value.length, complete: value.length <= limit };
331
+ if (value.length > limit)
332
+ complete = false;
333
+ if (value.length > limit && paged && key === listKey)
334
+ continuation = Number(options['--page-number'] ?? 1);
335
+ if (fields && key === listKey && rowShape && typeof rowShape === 'object') {
336
+ selected = selected.map(row => Object.fromEntries(fields.filter(field => record(row) && Object.hasOwn(row, field)).map(field => [field, row[field]])));
337
+ }
338
+ else if (!full) {
339
+ selected = selected.map(row => record(row) ? Object.fromEntries(Object.entries(row).slice(0, 4)) : row);
340
+ }
341
+ projected[key] = selected;
342
+ }
343
+ projected = bound(projected, 'response');
344
+ return { bytes: response.bytes, projection: { status: 'ok', response: projected,
345
+ output: { complete, pages, requests, bytesRead, collections: bounds,
346
+ ...(Object.keys(truncations).length ? { truncations } : {}),
347
+ ...(Object.keys(omittedFields).length ? { omittedFields } : {}),
348
+ ...(stoppedBy ? { stoppedBy } : {}),
349
+ ...(continuation === undefined ? {} : { nextPage: continuation }) } } };
350
+ }
@@ -0,0 +1,14 @@
1
+ import { ToolError } from './errors.js';
2
+ // DnsWebService.GetClusterNodeTypeForPath at vendor revision 0dd592aad21eced4221a9a46204cb1a299972ca0.
3
+ const primary = new Set(['003', '008', '009', '010', '011', '013', '057', '058', '059', '060', '063', '064', '065', '066', '070', '071', '072', '073', '074', '076', '099', '102', '104', '105', '107', '109', '110', '115', '116', '117', '142']);
4
+ const local = new Set(['001', '002', '005', '006', '007']);
5
+ export function routing(id) {
6
+ const number = id.slice(3);
7
+ return { target: primary.has(number) ? 'primary via vendor proxy' : local.has(number) ? 'current node' : 'selected or named node via vendor proxy',
8
+ cluster: ['OP-017', 'OP-018'].includes(id) ? 'aggregate' : ['OP-082', 'OP-083'].includes(id) ? 'asynchronous relay' : 'unsupported',
9
+ ...(id === 'OP-080' ? { replication: 'Listeners and local settings stay local; cluster parameters propagate through the primary' } : {}) };
10
+ }
11
+ export function validateNode(id, node) {
12
+ if (node === 'cluster' && routing(id).cluster === 'unsupported')
13
+ throw new ToolError('USAGE', 'node=cluster is unsupported for this operation', 'Select a named node or omit node to use the configured server');
14
+ }
package/dist/vendor.js ADDED
@@ -0,0 +1,126 @@
1
+ import { request } from 'node:https';
2
+ import { checkServerIdentity } from 'node:tls';
3
+ import { ToolError } from './errors.js';
4
+ import { PrivateFiles } from './private-file.js';
5
+ export class Redactor {
6
+ values = new Set();
7
+ add(value) {
8
+ if (!value)
9
+ return;
10
+ for (const form of [value, JSON.stringify(value).slice(1, -1), encodeURIComponent(value), encodeURI(value), Buffer.from(value).toString('base64')])
11
+ this.values.add(form);
12
+ }
13
+ text(value) {
14
+ for (const secret of [...this.values].sort((a, b) => b.length - a.length))
15
+ value = value.replaceAll(secret, '[REDACTED]');
16
+ return value;
17
+ }
18
+ }
19
+ function responseInvalid() {
20
+ throw new ToolError('UPSTREAM_INVALID', 'Unexpected server response', 'Check the selected server API version and operation', 1);
21
+ }
22
+ function object(value) {
23
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+ export async function vendorRequest(profile, path, body, token, headers, limits) {
26
+ const origin = new URL(profile.origin);
27
+ const ca = profile.caFile ? await new PrivateFiles().read(profile.caFile, false) : undefined;
28
+ return await new Promise((resolve, reject) => {
29
+ const call = request(origin, {
30
+ method: 'POST', path, ca, rejectUnauthorized: true,
31
+ ...(profile.tlsServerName ? { servername: profile.tlsServerName,
32
+ checkServerIdentity: (_hostname, certificate) => checkServerIdentity(profile.tlsServerName, certificate) } : {}),
33
+ headers: { 'content-type': 'application/x-www-form-urlencoded', 'content-length': Buffer.byteLength(body),
34
+ ...headers, ...(token ? { authorization: `Bearer ${token}` } : {}) },
35
+ signal: AbortSignal.timeout(Math.max(1, Math.min(10000, limits.elapsedMs))),
36
+ }, incoming => {
37
+ const chunks = [];
38
+ let size = 0;
39
+ incoming.on('data', (chunk) => {
40
+ size += chunk.length;
41
+ if (size > limits.bytes)
42
+ incoming.destroy(new ToolError('RESPONSE_LIMIT', 'Response exceeded the byte cap', 'Use narrower filters or raise --max-bytes within its bound', 1));
43
+ else
44
+ chunks.push(chunk);
45
+ });
46
+ incoming.once('error', reject);
47
+ incoming.once('end', () => {
48
+ if (!incoming.complete) {
49
+ reject(new ToolError('TRANSFER_INCOMPLETE', 'Server response was interrupted', 'Retry the read; no result file was completed', 1));
50
+ return;
51
+ }
52
+ resolve({ bytes: Buffer.concat(chunks), contentType: incoming.headers['content-type']?.split(';')[0]?.trim() ?? '',
53
+ status: incoming.statusCode ?? 0, ...(incoming.headers['retry-after'] ? { retryAfter: incoming.headers['retry-after'] } : {}),
54
+ ...(incoming.headers['last-modified'] ? { lastModified: incoming.headers['last-modified'] } : {}) });
55
+ });
56
+ });
57
+ call.once('error', reject);
58
+ call.end(body);
59
+ }).catch(error => {
60
+ if (error instanceof ToolError)
61
+ throw error;
62
+ if (error instanceof Error && error.name === 'AbortError')
63
+ throw new ToolError('READ_TIMEOUT', 'Read exceeded its elapsed time cap', 'Retry with narrower filters', 1);
64
+ throw new ToolError('TRANSPORT_FAILED', 'Verified HTTPS request failed', 'Check origin, caFile and tlsServerName; certificate verification is required', 1);
65
+ });
66
+ }
67
+ // Vendor refusal text is shown to the operator: strip control characters and bound the length.
68
+ export function vendorMessage(value) {
69
+ return typeof value.errorMessage === 'string' ? value.errorMessage.replace(/[\x00-\x1f\x7f]+/g, ' ').trim().slice(0, 300) || undefined : undefined;
70
+ }
71
+ export function checkStatus(value) {
72
+ if (!object(value) || typeof value.status !== 'string')
73
+ responseInvalid();
74
+ if (value.status === 'invalid-token')
75
+ throw new ToolError('AUTH_REQUIRED', 'The selected credential has expired or is invalid', 'Supply a valid credential for the same profile', 1);
76
+ if (value.status === '2fa-required')
77
+ throw new ToolError('TWO_FACTOR_REQUIRED', 'The server requires a second authentication factor', 'Supply a previously authenticated credential', 1);
78
+ if (value.status === 'error')
79
+ throw new ToolError('UPSTREAM_ERROR', `Server reported an operation failure${vendorMessage(value) ? `: ${vendorMessage(value)}` : ''}`, 'Check the selected identity and operation permissions', 1);
80
+ if (value.status !== 'ok')
81
+ responseInvalid();
82
+ }
83
+ export async function vendorRead(profile, operation, token) {
84
+ const path = operation === 'status' ? '/api/status' : '/api/user/session/get';
85
+ const body = operation === 'status' ? '' : 'includeInfo=true';
86
+ const response = await vendorRequest(profile, path, body, token, {}, { bytes: 1024 * 1024, elapsedMs: 10000 });
87
+ if (response.status !== 200)
88
+ throw new ToolError(response.status === 401 ? 'AUTH_REQUIRED' : response.status === 403 ? 'PERMISSION_DENIED'
89
+ : response.status === 404 ? 'UNSUPPORTED_OPERATION' : response.status === 429 ? 'RATE_LIMITED' : 'UPSTREAM_HTTP', 'Server rejected the request', 'Check the selected profile and server permissions', 1);
90
+ if (response.contentType !== 'application/json')
91
+ responseInvalid();
92
+ const bytes = response.bytes;
93
+ let value;
94
+ try {
95
+ value = JSON.parse(bytes.toString('utf8'));
96
+ }
97
+ catch {
98
+ responseInvalid();
99
+ }
100
+ checkStatus(value);
101
+ if (operation === 'status') {
102
+ if (typeof value.server !== 'string' || typeof value.hasDefaultCredentials !== 'boolean' || typeof value.ssoEnabled !== 'boolean')
103
+ responseInvalid();
104
+ return { bytes, projection: { status: 'ok', server: value.server, hasDefaultCredentials: value.hasDefaultCredentials, ssoEnabled: value.ssoEnabled } };
105
+ }
106
+ if (typeof value.username !== 'string' || typeof value.token !== 'string' || !object(value.info) || typeof value.info.version !== 'string')
107
+ responseInvalid();
108
+ if ((profile.tokenKind === 'api' || value.tokenName !== undefined) && typeof value.tokenName !== 'string')
109
+ responseInvalid();
110
+ const permissions = value.info.permissions;
111
+ if (!object(permissions))
112
+ responseInvalid();
113
+ if (value.hasDnssecPrivateKeys !== undefined && typeof value.hasDnssecPrivateKeys !== 'boolean')
114
+ responseInvalid();
115
+ for (const permission of Object.values(permissions)) {
116
+ if (!object(permission) || Object.values(permission).some(flag => typeof flag !== 'boolean'))
117
+ responseInvalid();
118
+ }
119
+ return { bytes, projection: { status: 'ok', username: value.username,
120
+ ...(value.tokenName !== undefined ? { tokenName: value.tokenName } : {}),
121
+ ...(value.hasDnssecPrivateKeys !== undefined ? { hasDnssecPrivateKeys: value.hasDnssecPrivateKeys } : {}),
122
+ info: { version: value.info.version, permissions } }, secret: value.token };
123
+ }
124
+ export async function saveResult(path, bytes) {
125
+ await new PrivateFiles().write(path, bytes);
126
+ }