@knowttl/technitium-axi 0.0.0-stage → 0.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/dist/reads.js ADDED
@@ -0,0 +1,330 @@
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
+ const invalid = () => { throw new ToolError('UPSTREAM_INVALID', 'Unexpected server response fields or types', 'Check the selected server API version and operation', 1); };
6
+ const usage = (message) => { throw new ToolError('USAGE', message, 'Run the selected command with --help'); };
7
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ const sensitive = /password|secret|^token$|^privateKey$|^stackTrace$|^innerError/i;
9
+ export function readInputs(contract, options) {
10
+ const form = new URLSearchParams();
11
+ const headers = {};
12
+ for (const parameter of contract.parameters) {
13
+ const value = options[parameter.flag];
14
+ if (value === undefined) {
15
+ if (parameter.required)
16
+ usage(`Missing ${parameter.flag}`);
17
+ continue;
18
+ }
19
+ if (parameter.required && value === '')
20
+ usage(`Missing ${parameter.flag}`);
21
+ if (parameter.type === 'boolean' && !['true', 'false'].includes(value))
22
+ usage(`${parameter.flag} requires true or false`);
23
+ if (parameter.type === 'integer' && (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < (parameter.name === 'limit' ? 0 : 1) || Number(value) > 2147483647))
24
+ usage(`${parameter.flag} requires a bounded integer`);
25
+ if (parameter.values && !parameter.values.includes(value))
26
+ usage(`Invalid value for ${parameter.flag}`);
27
+ if (parameter.type === 'http-date' && (Number.isNaN(Date.parse(value)) || new Date(value).toUTCString() !== value))
28
+ usage(`${parameter.flag} requires an RFC 1123 GMT date`);
29
+ if (parameter.type === 'date' && (!/^\d{4}-\d\d-\d\dT/.test(value) || Number.isNaN(Date.parse(value))))
30
+ usage(`${parameter.flag} requires an ISO 8601 date time`);
31
+ if (parameter.type === 'zone-list' && value && value.split(',').some(zone => !/^(?=.{1,253}$)[\p{L}\p{N}_*-]+(?:\.[\p{L}\p{N}_*-]+)*\.?$/u.test(zone)))
32
+ usage(`${parameter.flag} requires comma-separated zone names`);
33
+ if (contract.id === 'OP-135' && parameter.name === 'fileName' && (/[\\/]/.test(value) || value === '.' || value === '..'))
34
+ usage('--file-name requires a log filename without a path');
35
+ if (/[\x00-\x1f\x7f]/.test(value) || value.length > 65536)
36
+ usage(`Invalid value for ${parameter.flag}`);
37
+ if (parameter.location === 'header')
38
+ headers[parameter.name] = value;
39
+ else
40
+ form.set(parameter.name, value);
41
+ }
42
+ return { form, headers };
43
+ }
44
+ function project(value, shape, nullable = false) {
45
+ if (typeof shape === 'string') {
46
+ if (shape === 'unknown')
47
+ return null;
48
+ if (shape === 'nullable') {
49
+ if (value !== null && typeof value !== 'string')
50
+ invalid();
51
+ }
52
+ else if (shape === 'string-or-number') {
53
+ // DNSKEY encodes the protocol as a number; FWD encodes its transport as a string.
54
+ if (typeof value !== 'string' && typeof value !== 'number')
55
+ invalid();
56
+ }
57
+ else if (typeof value !== shape)
58
+ invalid();
59
+ return value;
60
+ }
61
+ if ('array' in shape) {
62
+ if (!Array.isArray(value))
63
+ return invalid();
64
+ const itemShape = shape.array;
65
+ return value.map(item => {
66
+ if (record(itemShape) && !('array' in itemShape)) {
67
+ if (!record(item))
68
+ return invalid();
69
+ const identity = ['name', 'rowNumber', 'fileName', 'username', 'section', 'scope', 'keyTag'].find(key => Object.hasOwn(itemShape, key));
70
+ if (identity && !Object.hasOwn(item, identity))
71
+ return invalid();
72
+ }
73
+ return project(item, itemShape);
74
+ });
75
+ }
76
+ if (value === null && nullable)
77
+ return null;
78
+ if (!record(value))
79
+ return invalid();
80
+ if (Object.hasOwn(shape, 'canView') && ['canView', 'canModify', 'canDelete'].some(key => !Object.hasOwn(value, key)))
81
+ invalid();
82
+ return Object.fromEntries(Object.entries(shape).filter(([key]) => !sensitive.test(key) && Object.hasOwn(value, key))
83
+ .map(([key, field]) => [key, project(value[key], field, key === 'proxy')]));
84
+ }
85
+ const listKeys = {
86
+ 'OP-020': 'zones', 'OP-021': 'catalogZoneNames', 'OP-037': 'dsRecords', 'OP-038': 'dnssecPrivateKeys',
87
+ 'OP-050': 'records', 'OP-053': 'zones', 'OP-056': 'zones', 'OP-062': 'zones', 'OP-068': 'apps',
88
+ 'OP-069': 'storeApps', 'OP-081': 'tsigKeyNames', 'OP-086': 'leases', 'OP-090': 'scopes',
89
+ 'OP-098': 'sessions', 'OP-101': 'users', 'OP-106': 'groups', 'OP-111': 'permissions',
90
+ 'OP-134': 'logFiles', 'OP-138': 'entries',
91
+ };
92
+ export async function readOperation(profile, command, token, options, full) {
93
+ const contract = readContracts.find(item => item.command === command);
94
+ const { form, headers } = readInputs(contract, options);
95
+ const listKey = listKeys[contract.id];
96
+ const listShape = listKey ? contract.response[listKey] : undefined;
97
+ const rowShape = listShape && typeof listShape === 'object' && 'array' in listShape ? listShape.array : undefined;
98
+ const fieldShape = rowShape && typeof rowShape === 'object' ? rowShape : contract.response;
99
+ const fields = options['--fields']?.split(',');
100
+ if (fields && (new Set(fields).size !== fields.length || fields.some(field => !Object.hasOwn(fieldShape, field) || sensitive.test(field))))
101
+ usage('Unknown or repeated output field');
102
+ const cap = (flag, fallback, maximum) => {
103
+ const raw = options[flag];
104
+ if (raw === undefined)
105
+ return fallback;
106
+ if (!/^[1-9]\d*$/.test(raw) || Number(raw) > maximum)
107
+ usage(`${flag} requires an integer from 1 to ${maximum}`);
108
+ return Number(raw);
109
+ };
110
+ const byteCap = cap('--max-bytes', 16 * 1024 * 1024, 64 * 1024 * 1024);
111
+ const requestCap = cap('--max-requests', 12, 100);
112
+ const pageCap = cap('--max-pages', 10, 100);
113
+ const elapsedCap = cap('--max-elapsed-ms', 30000, 120000);
114
+ const started = Date.now();
115
+ let requests = 0;
116
+ let bytesRead = 0;
117
+ const request = async () => {
118
+ let response;
119
+ for (let attempt = 0; attempt < 2; attempt++) {
120
+ if (++requests > requestCap || Date.now() - started >= elapsedCap)
121
+ throw new ToolError('READ_LIMIT', 'Read request or elapsed cap reached', 'Resume with narrower filters or a larger bounded cap', 1);
122
+ response = await vendorRequest(profile, contract.path, form.toString(), token, headers, { bytes: byteCap - bytesRead, elapsedMs: elapsedCap - (Date.now() - started) });
123
+ bytesRead += response.bytes.length;
124
+ if (![429, 502, 503, 504].includes(response.status) || profile.tokenKind === 'single-use' || attempt || requests === requestCap)
125
+ break;
126
+ // Never ignore server backoff. Retry only immediate transient reads; return longer guidance.
127
+ if (response.retryAfter && response.retryAfter !== '0')
128
+ break;
129
+ }
130
+ if (response.status !== 200) {
131
+ const code = response.status === 401 ? 'AUTH_REQUIRED' : response.status === 403 ? 'PERMISSION_DENIED'
132
+ : response.status === 404 ? 'UNSUPPORTED_OPERATION' : response.status === 429 ? 'RATE_LIMITED' : 'UPSTREAM_HTTP';
133
+ const backoff = response.retryAfter;
134
+ const hint = backoff && /^\d{1,9}$/.test(backoff) ? `Retry after ${backoff} seconds` : 'Check the selected server version, credential and permissions';
135
+ throw new ToolError(code, 'Server rejected the read', hint, 1);
136
+ }
137
+ return response;
138
+ };
139
+ const paged = ['OP-020', 'OP-138'].includes(contract.id);
140
+ const pageSizeField = contract.id === 'OP-020' ? 'zonesPerPage' : 'entriesPerPage';
141
+ if (paged) {
142
+ if (!form.has('pageNumber'))
143
+ form.set('pageNumber', '1');
144
+ if (!form.has(pageSizeField))
145
+ form.set(pageSizeField, '100');
146
+ }
147
+ const response = await request();
148
+ const metadata = { bytes: response.bytes.length, contentType: response.contentType,
149
+ sha256: createHash('sha256').update(response.bytes).digest('hex'),
150
+ ...(contract.id === 'OP-125' ? { lastModified: response.lastModified } : {}) };
151
+ if (contract.kind !== 'json') {
152
+ if (response.contentType === 'application/json' || /^\s*\{\s*"(?:status|errorMessage)"\s*:/.test(response.bytes.toString('utf8', 0, 64))) {
153
+ let value;
154
+ try {
155
+ value = JSON.parse(response.bytes.toString('utf8'));
156
+ }
157
+ catch {
158
+ invalid();
159
+ }
160
+ checkStatus(value);
161
+ invalid();
162
+ }
163
+ if (contract.id === 'OP-125' && (!response.lastModified || Number.isNaN(Date.parse(response.lastModified)) || new Date(response.lastModified).toUTCString() !== response.lastModified))
164
+ invalid();
165
+ const expected = contract.kind === 'zip' ? ['application/zip', 'application/octet-stream']
166
+ : contract.kind === 'csv' ? ['text/csv', 'text/plain', 'application/octet-stream'] : ['text/plain', 'application/octet-stream'];
167
+ if (!expected.includes(response.contentType))
168
+ invalid();
169
+ if (contract.kind === 'zip' && (!response.bytes.subarray(0, 2).equals(Buffer.from('PK')) || response.bytes.length < 22))
170
+ invalid();
171
+ return { bytes: response.bytes, projection: { status: 'ok', operation: contract.id, ...metadata,
172
+ ...(contract.kind === 'prometheus-text' ? { text: response.bytes.toString('utf8').slice(0, full ? 100000 : 1000),
173
+ complete: response.bytes.length <= (full ? 100000 : 1000) } : {}) } };
174
+ }
175
+ const decode = (bytes) => {
176
+ let value;
177
+ try {
178
+ value = JSON.parse(bytes.toString('utf8'));
179
+ }
180
+ catch {
181
+ invalid();
182
+ }
183
+ checkStatus(value);
184
+ if (contract.id === 'OP-078') {
185
+ if (typeof value.server !== 'string')
186
+ return invalid();
187
+ return value;
188
+ }
189
+ if (!record(value.response))
190
+ return invalid();
191
+ const payload = value.response;
192
+ if (contract.id === 'OP-015' && (!record(payload.lifetimeCounters) || typeof payload.uptimeSeconds !== 'number'))
193
+ invalid();
194
+ if (contract.id === 'OP-075' && payload.config !== null && typeof payload.config !== 'string')
195
+ invalid();
196
+ const required = contract.id === 'OP-018' ? ({ TopClients: 'topClients', TopDomains: 'topDomains', TopBlockedDomains: 'topBlockedDomains' }[form.get('statsType')])
197
+ : listKeys[contract.id] ?? Object.keys(contract.response)[0];
198
+ if (required && !Object.hasOwn(payload, required))
199
+ invalid();
200
+ return payload;
201
+ };
202
+ if (response.contentType !== 'application/json')
203
+ invalid();
204
+ const payload = decode(response.bytes);
205
+ if (contract.id === 'OP-075')
206
+ return { bytes: response.bytes, projection: { status: 'ok', operation: contract.id, ...metadata } };
207
+ let pages = 1;
208
+ let complete = true;
209
+ let continuation;
210
+ let stoppedBy;
211
+ if (paged && listKey) {
212
+ const validPage = (value, page) => Array.isArray(value[listKey]) &&
213
+ (value.pageNumber === page || (contract.id === 'OP-020' && value.pageNumber === 0 &&
214
+ value.totalPages === 0 && value.totalZones === 0 && value[listKey].length === 0));
215
+ if (!validPage(payload, Number(form.get('pageNumber'))))
216
+ invalid();
217
+ const identity = (row) => {
218
+ if (!record(row) || (contract.id === 'OP-020' ? typeof row.name !== 'string' : !Number.isSafeInteger(row.rowNumber)))
219
+ return invalid();
220
+ return JSON.stringify(contract.id === 'OP-020' ? row.name : row.rowNumber);
221
+ };
222
+ const rows = [];
223
+ const seen = new Set();
224
+ for (const row of payload[listKey]) {
225
+ const key = identity(row);
226
+ if (!seen.has(key)) {
227
+ seen.add(key);
228
+ rows.push(row);
229
+ }
230
+ }
231
+ payload[listKey] = rows;
232
+ let page = Number(form.get('pageNumber'));
233
+ const hasNext = (value) => {
234
+ if (value.totalPages !== undefined && (!Number.isInteger(value.totalPages) || Number(value.totalPages) < 0))
235
+ invalid();
236
+ return value.totalPages === undefined ? value[listKey].length > 0 : page < Number(value.totalPages);
237
+ };
238
+ let last = payload;
239
+ while (hasNext(last)) {
240
+ continuation = page + 1;
241
+ complete = false;
242
+ if (!full || pages >= pageCap || requests >= requestCap || Date.now() - started >= elapsedCap || bytesRead >= byteCap)
243
+ break;
244
+ form.set('pageNumber', String(++page));
245
+ let next;
246
+ try {
247
+ next = await request();
248
+ }
249
+ catch (error) {
250
+ if (!(error instanceof ToolError) || !['RESPONSE_LIMIT', 'READ_LIMIT', 'READ_TIMEOUT'].includes(error.code))
251
+ throw error;
252
+ stoppedBy = error.code;
253
+ break;
254
+ }
255
+ if (next.contentType !== 'application/json')
256
+ invalid();
257
+ last = decode(next.bytes);
258
+ if (!validPage(last, page))
259
+ invalid();
260
+ for (const row of last[listKey]) {
261
+ const key = identity(row);
262
+ if (!seen.has(key)) {
263
+ seen.add(key);
264
+ rows.push(row);
265
+ }
266
+ }
267
+ pages++;
268
+ complete = true;
269
+ continuation = undefined;
270
+ }
271
+ const total = payload[contract.id === 'OP-020' ? 'totalZones' : 'totalEntries'];
272
+ if (total !== undefined && (!Number.isSafeInteger(total) || Number(total) < 0))
273
+ invalid();
274
+ if (complete && total !== undefined && Number(options['--page-number'] ?? 1) === 1 && rows.length !== total) {
275
+ complete = false;
276
+ stoppedBy = 'PROVIDER_TOTAL_MISMATCH';
277
+ }
278
+ }
279
+ let projected = project(payload, contract.response);
280
+ if (fields && !(rowShape && typeof rowShape === 'object')) {
281
+ projected = Object.fromEntries(fields.filter(field => Object.hasOwn(projected, field)).map(field => [field, projected[field]]));
282
+ }
283
+ const bounds = {};
284
+ const truncations = {};
285
+ const bound = (value, path) => {
286
+ const limit = full ? 10000 : 100;
287
+ if (typeof value === 'string') {
288
+ const stringLimit = full ? 1000000 : 1000;
289
+ if (value.length > stringLimit) {
290
+ complete = false;
291
+ truncations[path] = { returned: stringLimit, available: value.length };
292
+ }
293
+ return value.slice(0, stringLimit);
294
+ }
295
+ if (Array.isArray(value)) {
296
+ if (value.length > limit) {
297
+ complete = false;
298
+ truncations[path] = { returned: limit, available: value.length };
299
+ }
300
+ return value.slice(0, limit).map((row, index) => bound(row, `${path}[${index}]`));
301
+ }
302
+ if (record(value))
303
+ return Object.fromEntries(Object.entries(value).map(([key, field]) => [key, bound(field, `${path}.${key}`)]));
304
+ return value;
305
+ };
306
+ for (const [key, value] of Object.entries(projected)) {
307
+ if (!Array.isArray(value))
308
+ continue;
309
+ const limit = full ? 10000 : 100;
310
+ let selected = value.slice(0, limit);
311
+ bounds[key] = { returned: selected.length, available: value.length, complete: value.length <= limit };
312
+ if (value.length > limit)
313
+ complete = false;
314
+ if (value.length > limit && paged && key === listKey)
315
+ continuation = Number(options['--page-number'] ?? 1);
316
+ if (fields && key === listKey && rowShape && typeof rowShape === 'object') {
317
+ selected = selected.map(row => Object.fromEntries(fields.filter(field => record(row) && Object.hasOwn(row, field)).map(field => [field, row[field]])));
318
+ }
319
+ else if (!full) {
320
+ selected = selected.map(row => record(row) ? Object.fromEntries(Object.entries(row).slice(0, 4)) : row);
321
+ }
322
+ projected[key] = selected;
323
+ }
324
+ projected = bound(projected, 'response');
325
+ return { bytes: response.bytes, projection: { status: 'ok', response: projected,
326
+ output: { complete, pages, requests, bytesRead, collections: bounds,
327
+ ...(Object.keys(truncations).length ? { truncations } : {}),
328
+ ...(stoppedBy ? { stoppedBy } : {}),
329
+ ...(continuation === undefined ? {} : { nextPage: continuation }) } } };
330
+ }
package/dist/vendor.js ADDED
@@ -0,0 +1,122 @@
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)])
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
+ export function checkStatus(value) {
68
+ if (!object(value) || typeof value.status !== 'string')
69
+ responseInvalid();
70
+ if (value.status === 'invalid-token')
71
+ throw new ToolError('AUTH_REQUIRED', 'The selected credential has expired or is invalid', 'Supply a valid credential for the same profile', 1);
72
+ if (value.status === '2fa-required')
73
+ throw new ToolError('TWO_FACTOR_REQUIRED', 'The server requires a second authentication factor', 'Supply a previously authenticated credential', 1);
74
+ if (value.status === 'error')
75
+ throw new ToolError('UPSTREAM_ERROR', 'Server reported an operation failure', 'Check the selected identity and operation permissions', 1);
76
+ if (value.status !== 'ok')
77
+ responseInvalid();
78
+ }
79
+ export async function vendorRead(profile, operation, token) {
80
+ const path = operation === 'status' ? '/api/status' : '/api/user/session/get';
81
+ const body = operation === 'status' ? '' : 'includeInfo=true';
82
+ const response = await vendorRequest(profile, path, body, token, {}, { bytes: 1024 * 1024, elapsedMs: 10000 });
83
+ if (response.status !== 200)
84
+ throw new ToolError(response.status === 401 ? 'AUTH_REQUIRED' : response.status === 403 ? 'PERMISSION_DENIED'
85
+ : response.status === 404 ? 'UNSUPPORTED_OPERATION' : response.status === 429 ? 'RATE_LIMITED' : 'UPSTREAM_HTTP', 'Server rejected the request', 'Check the selected profile and server permissions', 1);
86
+ if (response.contentType !== 'application/json')
87
+ responseInvalid();
88
+ const bytes = response.bytes;
89
+ let value;
90
+ try {
91
+ value = JSON.parse(bytes.toString('utf8'));
92
+ }
93
+ catch {
94
+ responseInvalid();
95
+ }
96
+ checkStatus(value);
97
+ if (operation === 'status') {
98
+ if (typeof value.server !== 'string' || typeof value.hasDefaultCredentials !== 'boolean' || typeof value.ssoEnabled !== 'boolean')
99
+ responseInvalid();
100
+ return { bytes, projection: { status: 'ok', server: value.server, hasDefaultCredentials: value.hasDefaultCredentials, ssoEnabled: value.ssoEnabled } };
101
+ }
102
+ if (typeof value.username !== 'string' || typeof value.token !== 'string' || !object(value.info) || typeof value.info.version !== 'string')
103
+ responseInvalid();
104
+ if ((profile.tokenKind === 'api' || value.tokenName !== undefined) && typeof value.tokenName !== 'string')
105
+ responseInvalid();
106
+ const permissions = value.info.permissions;
107
+ if (!object(permissions))
108
+ responseInvalid();
109
+ if (value.hasDnssecPrivateKeys !== undefined && typeof value.hasDnssecPrivateKeys !== 'boolean')
110
+ responseInvalid();
111
+ for (const permission of Object.values(permissions)) {
112
+ if (!object(permission) || Object.values(permission).some(flag => typeof flag !== 'boolean'))
113
+ responseInvalid();
114
+ }
115
+ return { bytes, projection: { status: 'ok', username: value.username,
116
+ ...(value.tokenName !== undefined ? { tokenName: value.tokenName } : {}),
117
+ ...(value.hasDnssecPrivateKeys !== undefined ? { hasDnssecPrivateKeys: value.hasDnssecPrivateKeys } : {}),
118
+ info: { version: value.info.version, permissions } }, secret: value.token };
119
+ }
120
+ export async function saveResult(path, bytes) {
121
+ await new PrivateFiles().write(path, bytes);
122
+ }
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "@knowttl/technitium-axi",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "publishConfig": { "access": "public" },
4
+ "version": "0.1.0",
5
+ "description": "AXI-standard agent-facing CLI for Technitium DNS Server",
6
+ "type": "module",
7
+ "license": "UNLICENSED",
8
+ "repository": { "type": "git", "url": "https://github.com/knowttl/technitium-axi.git" },
9
+ "bin": { "technitium-axi": "bin/technitium-axi.js" },
10
+ "files": ["bin/", "dist/", "axi-credential.schema.json", "command-inventory.json"],
11
+ "engines": { "node": ">=24.14.0 <25", "npm": ">=11.9.0 <12" },
12
+ "packageManager": "npm@11.9.0",
13
+ "scripts": {
14
+ "build": "tsc && node scripts/inventory.mjs && node scripts/read-matrix.mjs",
15
+ "typecheck": "tsc --noEmit && tsc -p scripts/tsconfig.json",
16
+ "check:generated": "node scripts/inventory.mjs --check && node scripts/read-matrix.mjs --check",
17
+ "check:journey": "node scripts/journey.mjs",
18
+ "test": "npm run check:journey -- --task B1.01 --paths native --offline && npm run check:journey -- --task B1.02 --paths native --offline && npm run check:journey -- --task B2.01 --paths native --offline && npm run check:journey -- --task B2.02 --paths native --offline && npm run check:journey -- --task B2.03 --paths native --offline && npm run check:journey -- --task B2.04 --paths native --offline && npm run check:journey -- --task B2.05 --paths native --offline && npm run check:journey -- --task B2.admin-reads --paths native --offline",
19
+ "test:release": "node --test tests/vendor.test.mjs tests/private-file.test.mjs",
20
+ "prepack": "npm run build && npm run check:generated"
21
+ },
22
+ "dependencies": { "@toon-format/toon": "2.3.1", "ajv": "8.17.1" },
23
+ "devDependencies": { "@types/node": "24.10.0", "typescript": "5.9.3" }
24
+ }