@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/LICENSE +21 -0
- package/README.md +384 -2
- package/axi-credential.schema.json +37 -0
- package/bin/technitium-axi.js +10 -0
- package/command-inventory.json +8556 -0
- package/dist/archive-contract.js +58 -0
- package/dist/catalogue.js +40 -0
- package/dist/cli.js +157 -0
- package/dist/cluster.js +129 -0
- package/dist/configuration.js +163 -0
- package/dist/credentials.js +178 -0
- package/dist/errors.js +14 -0
- package/dist/private-file.js +137 -0
- package/dist/read-contracts.js +58 -0
- package/dist/reads.js +350 -0
- package/dist/routing.js +14 -0
- package/dist/vendor.js +126 -0
- package/dist/write-contracts.js +154 -0
- package/dist/writes.js +940 -0
- package/package.json +50 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) knowttl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,385 @@
|
|
|
1
|
-
#
|
|
1
|
+
# technitium-axi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Agent-facing CLI for Technitium DNS Server with reads and explicitly granted writes.
|
|
4
|
+
It provides strict credential profiles, offline discovery, protected result files and a durable mutation journal.
|
|
5
|
+
|
|
6
|
+
[Why](#why-this-exists) | [Quickstart](#quickstart) | [Commands](#commands) | [Writes](#writes) | [Safety](#safety) | [Configuration](#configuration) | [Limitations](#limitations) | [Development](#development)
|
|
7
|
+
|
|
8
|
+
## Why this exists
|
|
9
|
+
|
|
10
|
+
Agents need to inspect a DNS server without holding broad credentials or flooding their context.
|
|
11
|
+
`technitium-axi` returns bounded TOON output by default (`--json` serializes the same projection) with next-step hints on errors.
|
|
12
|
+
Collections, strings, pages, requests, time and response bytes all have caps.
|
|
13
|
+
Vendor mutations require explicit grants and preview confirmation, and secrets are only accepted as references, never inline.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
Requires Node `>=24.14.0 <25` (see [.node-version](.node-version)).
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install --global @knowttl/technitium-axi
|
|
21
|
+
technitium-axi --help
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Without a selected profile the tool refuses to run and says how to select one:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
error:
|
|
28
|
+
code: PROFILE_REQUIRED
|
|
29
|
+
message: Select a credential profile
|
|
30
|
+
help[1]: Use --config <file> --profile <name>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Import a config, then make a first authenticated read:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
technitium-axi config import --input-file /absolute/path/config.json
|
|
37
|
+
technitium-axi auth status --profile automation
|
|
38
|
+
technitium-axi zone list --profile automation --fields name,type
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use with your agent
|
|
42
|
+
|
|
43
|
+
No session hooks or agent skill ship with this package yet; both are PENDING.
|
|
44
|
+
Agents can use the CLI directly.
|
|
45
|
+
`technitium-axi --help` and `technitium-axi inventory` describe every command offline.
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
Run `technitium-axi --help` for the full generated list and `technitium-axi <command> --help` for each command's parameters.
|
|
50
|
+
The packaged `command-inventory.json` is the reference; this table lists the groups.
|
|
51
|
+
|
|
52
|
+
| Group | Examples |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| Local | `schema`, `inventory`, `auth status`, `profile list`, `config path`, `config import`, `doctor` |
|
|
55
|
+
| Server | `status`, `user session get`, `user profile get`, `user check-for-update` |
|
|
56
|
+
| Dashboard | `dashboard metrics json`, `dashboard metrics text`, `dashboard stats get`, `dashboard stats get-top` |
|
|
57
|
+
| Zones | `zone list`, `zone catalog list`, `zone export`, `zone options get`, `zone permission get`, `zone dnssec view-ds`, `zone dnssec properties get`, `zone record get` |
|
|
58
|
+
| Cache and lists | `cache list`, `allowed list`, `allowed export`, `blocked list`, `blocked export` |
|
|
59
|
+
| Apps and DNS client | `app list`, `app list-store-apps`, `app config get`, `dns-client health-check` |
|
|
60
|
+
| Settings | `settings get`, `settings get-tsig-key-names`, `settings backup` |
|
|
61
|
+
| DHCP | `dhcp lease list`, `dhcp scope list`, `dhcp scope get` |
|
|
62
|
+
| Admin | `admin session list`, `admin user list`, `admin user get`, `admin group list`, `admin group get`, `admin permission list`, `admin permission get`, `admin sso get`, `admin ldap get`, `admin cluster state`, `admin cluster primary transfer-config` |
|
|
63
|
+
| Logs | `log list`, `log download`, `log query`, `log export` |
|
|
64
|
+
|
|
65
|
+
Read commands share `--config`, `--profile`, `--json`, `--result-file`, `--fields`, `--full`, `--max-bytes`, `--max-requests`, `--max-pages` and `--max-elapsed-ms`.
|
|
66
|
+
Operation-specific flags such as `--node`, `--filter-name` and `--page-number` appear in each command's `--help`.
|
|
67
|
+
Boolean read flags need an explicit value, for example `--list-zone true`; `--full` takes none.
|
|
68
|
+
`--fields` selects top-level or row fields only, so a nested field such as a session's `partialToken` needs `--full`.
|
|
69
|
+
Use `--full` or `--fields rData` to inspect record data from `zone record get` and `cache list`; see [read completeness](docs/reads.md) for omitted-field reporting.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
technitium-axi zone list --profile automation --full
|
|
73
|
+
technitium-axi settings get --profile automation --result-file /absolute/path/settings.json
|
|
74
|
+
technitium-axi settings backup --profile automation --zones true --result-file /absolute/path/zones.zip
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
See [Read contracts and protected results](docs/reads.md) for output bounds, paging, transfer formats and the reviewed archive-entry map.
|
|
78
|
+
|
|
79
|
+
## Writes
|
|
80
|
+
|
|
81
|
+
`technitium-axi inventory --json` is the generated reference for write commands, operation IDs, input fields, required fields, SecretRef fields and record selectors.
|
|
82
|
+
Write inputs use vendor parameter names inside a JSON `parameters` object, supplied through a protected absolute `--input-file` owned by the current user with no group or other access (for example, mode 0600).
|
|
83
|
+
Input and upload files are limited to 16 MiB each.
|
|
84
|
+
See [protected results](docs/reads.md) for result replacement and destination permissions.
|
|
85
|
+
Booleans and integers use JSON types; list fields use arrays of strings.
|
|
86
|
+
Write commands use the flags listed in their `--help`, rather than the read projection and paging flags.
|
|
87
|
+
|
|
88
|
+
Account actions use the same grants, protected input and preview confirmation as other writes.
|
|
89
|
+
`user login` (also `auth login`) explicitly acquires a Standard session with `user`, a `pass` SecretRef and an optional `totp` SecretRef.
|
|
90
|
+
Select a `tokenKind: "session"` profile with a matching `username` and `token: {"stored": true}` to use the issued session on subsequent calls.
|
|
91
|
+
Only the issued session is stored, bound to the profile name, origin, username, token kind and TLS settings; passwords and one-time codes are discarded.
|
|
92
|
+
Ordinary reads never acquire or refresh credentials.
|
|
93
|
+
`user logout` (also `auth logout`) reports remote revocation and bound local removal separately, including when revocation fails.
|
|
94
|
+
|
|
95
|
+
`user create-token` accepts either the selected Standard bearer or an explicit `user` and `pass` SecretRef, with optional `totp`, and never falls back between them.
|
|
96
|
+
`user create-single-use-token`, `user change-password`, `user 2fa init`, `user 2fa enable`, `user 2fa disable` and `user profile set` require a Standard session.
|
|
97
|
+
Token creation (including `user create-single-use-token`) and 2FA initialization require `--result-file` on execution; their confidential response never appears in ordinary output.
|
|
98
|
+
Password changes use `pass` and `newPass` SecretRefs; `totp` is also a SecretRef.
|
|
99
|
+
`user session delete` accepts the exact `partialToken` returned by a profile read.
|
|
100
|
+
`admin user set` supports a protected `newPass` and an ordinary boolean `totpEnabled`.
|
|
101
|
+
`admin session create-token` accepts `user` and `tokenName`, and requires a protected `--result-file` before issuing the API token.
|
|
102
|
+
`admin session delete` selects the exact `partialToken` and optional `node`; it never deletes sessions on other nodes implicitly.
|
|
103
|
+
`admin user create` requires `user` and a `pass` SecretRef, with optional `displayName`.
|
|
104
|
+
`admin user set` can also rename an account with `newUser`, update its display name, disabled state and session timeout, or replace `memberOfGroups`.
|
|
105
|
+
`admin user delete`, `admin group create` and `admin group delete` select one named account or group.
|
|
106
|
+
`admin group set` supports `newGroup`, `description` and replacement `members`, with an exact selected baseline.
|
|
107
|
+
`admin permission set` selects a `section` and optional `node`, with typed `userPermissions` and `groupPermissions` rows and an exact selected baseline.
|
|
108
|
+
Section permission writes require the primary when clustering is enabled; the vendor enforces caller permissions.
|
|
109
|
+
|
|
110
|
+
`admin sso user create` and `admin sso user set` require an initialized secondary profile.
|
|
111
|
+
Preview and execution read the same selected caller's session with `includeInfo=true` and require exactly `info.permissions.Administration.canModify: true` before dispatch.
|
|
112
|
+
Topology comes from that session response, so a Modify-only caller needs no separate Administration View grant.
|
|
113
|
+
The preview names the requested secondary, effective primary and caller.
|
|
114
|
+
The vendor proxy supplies its cluster identity; the tool never acquires that identity.
|
|
115
|
+
Direct primary calls, missing or malformed permissions, and single-use credentials fail before SSO mutation.
|
|
116
|
+
|
|
117
|
+
`admin sso set` and `admin ldap set` update only supplied fields and use the vendor primary proxy when clustered.
|
|
118
|
+
Booleans and LDAP ports use JSON boolean and integer values; `ldapSslOption` is `None`, `StartTLS` or `LDAPS`.
|
|
119
|
+
`ssoScopes` is a string array encoded with pipes on the wire, while `ssoGroupMap` and `ldapGroupMap` are arrays of `{ "remoteGroup": "provider-group", "localGroup": "Operators" }` rows.
|
|
120
|
+
Scopes and group maps replace the selected collection; omitted fields persist.
|
|
121
|
+
`ssoClientSecret` and `ldapBindPassword` require SecretRefs, and masked `************` values are rejected as secret inputs.
|
|
122
|
+
An SSO restart can lose the response after saving: `OUTCOME_UNKNOWN` requires trusted readback and must never trigger a replay.
|
|
123
|
+
`admin ldap test` uses stored connection fields with optional explicit overrides and does not save configuration.
|
|
124
|
+
It performs an outbound bind, so it requires the exact `OP-143` write grant and is denied in read-only mode even though the vendor requires only Administration View.
|
|
125
|
+
A successful configuration read does not prove a successful LDAP bind.
|
|
126
|
+
|
|
127
|
+
Cluster lifecycle commands are available under `admin cluster`, with `primary` and `secondary` subgroups.
|
|
128
|
+
Run `inventory --json` for their exact typed inputs and routing scopes.
|
|
129
|
+
Cluster preflight and verification require Administration View in addition to the vendor's Modify or Delete permission.
|
|
130
|
+
Use an API token or Standard session; single-use tokens cannot support the required reads.
|
|
131
|
+
Previews bind the selected topology, describe replicated effects and identify affected members.
|
|
132
|
+
|
|
133
|
+
`admin cluster init-join` accepts `primaryNodePassword` and optional `primaryNodeTotp` only as SecretRefs.
|
|
134
|
+
`ignoreCertificateErrors` is an explicit vendor join parameter; it does not change this tool's TLS trust.
|
|
135
|
+
Join and update-secondary accept a base64url DER public certificate in `secondaryNodeCertificate`.
|
|
136
|
+
Cluster options use integer intervals: heartbeat refresh and retry accept 10-300 seconds, config refresh and retry accept 30-3600 seconds, and config refresh must exceed heartbeat refresh.
|
|
137
|
+
Omitted timers remain unchanged.
|
|
138
|
+
|
|
139
|
+
Successful lifecycle output verifies only the selected node's topology.
|
|
140
|
+
Notify and resync acknowledge scheduled synchronization; they do not prove every zone has synchronized.
|
|
141
|
+
After a lost response, notify and resync cannot be reconciled from topology alone and remain `OUTCOME_UNKNOWN`.
|
|
142
|
+
Graceful remove asks the secondary to leave; delete-secondary removes primary membership without resetting that secondary.
|
|
143
|
+
Forced leave, delete and promotion can leave remote configuration behind, which requires separate reads through explicitly trusted profiles.
|
|
144
|
+
Changed listeners, URLs and certificates never cause automatic credential forwarding to a new origin.
|
|
145
|
+
After a lost response, retain the audit ID and use the original input and confirmation with `--reconcile` on the original trusted binding.
|
|
146
|
+
For init and init-join, add `--reconcile-profile <name>` to verify through an explicitly configured trusted replacement profile after a listener or identity change.
|
|
147
|
+
The replacement profile is used only for the read and receives no mutation.
|
|
148
|
+
An unreachable or unverifiable target remains `OUTCOME_UNKNOWN` and must not be replayed.
|
|
149
|
+
|
|
150
|
+
Literal `node=cluster` is accepted only for supported dashboard aggregation and blocking relay operations.
|
|
151
|
+
Other commands require a named node or the configured origin.
|
|
152
|
+
|
|
153
|
+
`settings set` accepts all fields listed in the inventory's configuration map, including modern fields and the `preferIPv6` and `reverseProxyNetworkACL` compatibility inputs.
|
|
154
|
+
Omitted fields are preserved; arrays replace collections.
|
|
155
|
+
An empty array clears collections except `dnsServerLocalEndPoints` and `webServiceLocalAddresses`, which reset to wildcard listeners.
|
|
156
|
+
To append, include the existing collection plus the new entries and retain the original collection in the baseline.
|
|
157
|
+
The optional input `encoding` selects `form` (default) or `json`; `--json` controls output only.
|
|
158
|
+
Use the inventory's configuration map for field types and structured row columns.
|
|
159
|
+
`defaultResponsiblePerson` accepts only a plain `local@domain` address in parameters and baselines, preserving its case; an empty string clears the value.
|
|
160
|
+
Display names, comments and angle brackets are rejected locally before any vendor request; full MailAddress syntax is deferred.
|
|
161
|
+
TSIG shared secrets and proxy and certificate passwords require SecretRefs in both changed values and baselines.
|
|
162
|
+
In parameters, a TSIG shared-secret reference resolving to an empty string requests a vendor-generated secret.
|
|
163
|
+
For an absent certificate or proxy password, use a baseline SecretRef resolving to an empty string; an absent proxy username uses an empty string directly.
|
|
164
|
+
Updating a certificate password requires an existing or selected certificate path.
|
|
165
|
+
Proxy updates preserve unselected proxy fields; disabling the proxy clears its configuration.
|
|
166
|
+
Forwarder replacement preserves the current transport unless selected explicitly; clearing the list resets it to UDP.
|
|
167
|
+
Settings comparisons account for case and trailing dots in `dnsServerDomain`, blank web-service headers restoring defaults, and `HttpsJson` normalizing to `Https`.
|
|
168
|
+
Never submit a redacted GET response as a patch.
|
|
169
|
+
Settings are not transactional: a handler error can leave earlier assignments saved and requires inspection before another write.
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"parameters": {"defaultRecordTtl": 300, "forwarders": ["192.0.2.2"], "enableBlocking": false},
|
|
174
|
+
"baseline": {"defaultRecordTtl": 3600, "forwarders": ["192.0.2.1"], "enableBlocking": true},
|
|
175
|
+
"encoding": "json"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`app install`, `app update` and `settings restore` use `upload: {"file": "/absolute/path/package.zip"}` in the input envelope.
|
|
180
|
+
These uploads must be ZIP archives with safe relative entry names and no symbolic links; invalid archives are rejected locally with `UPLOAD_INVALID`.
|
|
181
|
+
The preview binds the exact upload hash, and execution sends one binary file with operation parameters in the query string for vendor proxy compatibility.
|
|
182
|
+
`app download-and-install` and `app download-and-update` take `name` and an HTTPS `url` in `parameters`; the DNS server downloads the package.
|
|
183
|
+
`app uninstall` takes `name` and leaves APP records intact.
|
|
184
|
+
`app config set` takes `name` and `config: {"file": "/absolute/path/dnsApp.config"}` in `parameters`.
|
|
185
|
+
The protected config file is sent as an exact opaque string, including unknown keys and whitespace, and is never printed.
|
|
186
|
+
Restore selection flags are independent booleans and default to false at the vendor.
|
|
187
|
+
Restore is not transactional: a handler error can leave earlier categories restored and requires inspection before another write.
|
|
188
|
+
Use `--result-file` to retain exact settings or restore responses, including returned proxy and TSIG secrets, in a protected file.
|
|
189
|
+
Restore responses can include current DNS settings secrets even when `dnsSettings` is false.
|
|
190
|
+
An unsafe explicit result destination is rejected before write dispatch.
|
|
191
|
+
`settings temporary-disable-blocking` takes a positive integer `minutes` and returns the vendor's expiry time; `settings force-update-block-lists` triggers an asynchronous update.
|
|
192
|
+
`log delete` takes the `fileName` returned by `log list` as its `log` value, without a path, and optional `node`; `log delete-all` accepts optional `node`.
|
|
193
|
+
|
|
194
|
+
DHCP lease removal and conversion require a scope `name` and exactly one of `clientIdentifier` or `hardwareAddress`.
|
|
195
|
+
Scope enable, disable and delete take `name`; reservation removal also requires `hardwareAddress`.
|
|
196
|
+
Reservation addition requires `name`, `hardwareAddress` and `ipAddress`, with optional `hostName` and `comments`, and checks for an existing hardware or address reservation before dispatch.
|
|
197
|
+
Use a single-writer window for this check because the vendor does not atomically reject a concurrent duplicate address.
|
|
198
|
+
`dhcp scope set` updates an existing scope using typed fields from the inventory's `dhcpFields` map.
|
|
199
|
+
Without a `baseline` it creates a missing scope and requires `startingAddress`, `endingAddress` and `subnetMask`; it returns `CONFLICT` if the scope already exists.
|
|
200
|
+
When renaming, `baseline.newName` is the current scope name.
|
|
201
|
+
Omitted fields are preserved, arrays replace collections, and empty arrays clear them.
|
|
202
|
+
Structured rows retain the listed column order when encoded for the vendor, including option bytes, static routes, exclusions and reservations.
|
|
203
|
+
Scope setters and reservation addition need a reusable API or session credential for their preflight reads.
|
|
204
|
+
|
|
205
|
+
Mutations require the selected file profile's `writes.allowWrites` to be `true` and its `writes.operations` to contain the exact capability from the inventory.
|
|
206
|
+
The accepted grant values live in `technitium-axi schema --json`; there are no wildcard grants.
|
|
207
|
+
`TECHNITIUM_AXI_READ_ONLY=true` or `1` overrides all mutation grants; `false` or `0` leaves grants effective.
|
|
208
|
+
`dns-client resolve` without `parameters.import: true` needs no write grant; importing requires `OP-077.import`.
|
|
209
|
+
The resolve response includes the DNS reply and any DNSSEC `warningMessage`.
|
|
210
|
+
When import is requested, a nonempty DNSSEC warning suppresses import and reports `mutation: suppressed`.
|
|
211
|
+
|
|
212
|
+
Cache deletion (`OP-054`) accepts a `domain`; cache flush (`OP-055`) takes an empty `parameters` object.
|
|
213
|
+
Allowed and blocked lists each support `add`, `delete`, `flush` and `import`, with their exact operation grants shown in the inventory.
|
|
214
|
+
Add and delete accept `domain`; flush takes empty parameters.
|
|
215
|
+
Imports accept `{"parameters":{"allowedZones":["a.test","b.test"]}}` or `blockedZones` for the blocked list.
|
|
216
|
+
These arrays are sent as comma-separated form fields, including duplicates or an empty array; imports merge membership and do not use uploads.
|
|
217
|
+
Each import entry must be nonempty and contain no ASCII whitespace, ASCII control characters or commas.
|
|
218
|
+
Dashboard history deletion (`OP-019`) accepts an optional `node` and preserves current query counters.
|
|
219
|
+
|
|
220
|
+
For example, authorize only record additions with this field in the selected profile:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
"writes": { "allowWrites": true, "operations": ["OP-049"] }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
An input file for a record addition can contain:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{"parameters":{"zone":"example.test","domain":"www.example.test","type":"A","ipAddress":"192.0.2.10","ttl":300}}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
First preview, then execute with the exact returned `preview.digest` and the same profile and input:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
technitium-axi zone record add --profile automation --input-file /absolute/path/add.json --json
|
|
236
|
+
technitium-axi zone record add --profile automation --input-file /absolute/path/add.json --execute --confirm '<preview.digest>'
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A preview resolves credentials, operation SecretRefs and uploads locally.
|
|
240
|
+
When an operation can also create other objects, such as a PTR record or reverse zone, or DNS zones through DHCP scope DNS updates, the preview lists them in `sideEffects`.
|
|
241
|
+
Cluster lifecycle and SSO user create/set previews perform the preflight reads described above; previews never dispatch mutations.
|
|
242
|
+
It reports permission, target and upload metadata without secret values.
|
|
243
|
+
Each preview issues a fresh invocation ID within its digest, bound to the selected profile, target and input contents.
|
|
244
|
+
Bearer-authenticated writes also bind the selected credential; explicit identity acquisition uses a private local acquisition key.
|
|
245
|
+
An executed mutation creates an exclusive durable intent before dispatch and retains its receipt; if that confirmation reaches journal creation again, it is refused with `REPLAY_DENIED`.
|
|
246
|
+
A new preview permits a later legitimate operation with identical content.
|
|
247
|
+
|
|
248
|
+
Journal entries default to `$XDG_STATE_HOME/technitium-axi/writes.log.d/<digest>.json`, or `~/.local/state/technitium-axi/writes.log.d/<digest>.json` when no absolute XDG state directory is set.
|
|
249
|
+
Set `TECHNITIUM_AXI_WRITE_LOG` to an absolute base path to use `<base-path>.d/<digest>.json` instead.
|
|
250
|
+
This moves only the journal; stored sessions and the acquisition key stay under the state directory above.
|
|
251
|
+
The journal stores operation identity, state and keyed hashes, plus the preflight topology for cluster lifecycle reconciliation; it never stores secrets.
|
|
252
|
+
Failure to persist an intent prevents dispatch.
|
|
253
|
+
Dispatched mutations never retry automatically.
|
|
254
|
+
An explicit error response is a definitive refusal for every mutation whose handler validates before it changes anything: it returns `UPSTREAM_ERROR` with the server's message and the audit ID, and the journal entry becomes `failed`.
|
|
255
|
+
Examples are a secondary zone whose primary is unreachable, a resync of a non-secondary zone, or a record that does not exist.
|
|
256
|
+
A missing endpoint (HTTP 404) is a definitive `UNSUPPORTED_OPERATION` refusal.
|
|
257
|
+
A lost response, a non-JSON response or an unproven postcondition returns `OUTCOME_UNKNOWN` and an audit ID.
|
|
258
|
+
Mutations that can apply part of a change before failing keep `OUTCOME_UNKNOWN` for an error response, with an audit ID: operations with a baseline, DHCP scope creation, cluster operations, bulk zone deletion, imports (including DNS-client imports), restore, app installation, update, uninstall and configuration changes, multi-field account and configuration setters, SSO user creation with group membership, zone creation with catalog membership, record renames, and record changes that also create a PTR record or reverse zone; `user logout` reports `revoke-failed` when revocation cannot be confirmed.
|
|
259
|
+
Never replay an uncertain write or create a new preview to bypass its journal entry.
|
|
260
|
+
|
|
261
|
+
`zone options set`, `zone permission set`, `settings set`, `dhcp scope set` (when modifying), `admin group set` and `admin permission set` require an additional `baseline` object containing exactly the changed fields, excluding target identifiers.
|
|
262
|
+
Read the selected target first and supply its current values; execution re-reads them and returns `CONFLICT` before dispatch if they differ.
|
|
263
|
+
Zone-option and group lists are explicit replacements, including empty arrays; omitted fields remain unchanged.
|
|
264
|
+
See the settings guidance above for collection reset and coupled-field behavior.
|
|
265
|
+
Null settings collection baselines map to empty arrays, and null catalog/primary-transfer TSIG names map to empty strings.
|
|
266
|
+
Group renames use `baseline.newGroup` for the current group name; settings proxy fields use the getter's nested `proxy` values.
|
|
267
|
+
Update-security-policy baselines accept getter rows or the setter string representation.
|
|
268
|
+
These operations verify selected fields after dispatch, including renamed group and scope targets.
|
|
269
|
+
The vendor has no atomic conditional setter, so maintain a single-writer window between the final read and write.
|
|
270
|
+
For these operations, use `--reconcile <audit-id> --confirm '<original-preview.digest>'` with the original input and profile to prove the postcondition without resending the mutation.
|
|
271
|
+
For cluster lifecycle reconciliation, see the cluster guidance above.
|
|
272
|
+
Other operations, except zone resync as described below, require manual inspection of the exact selected target when their outcome is uncertain.
|
|
273
|
+
Baseline checks, verification and reconciliation account for vendor member ordering and case, TSIG name sets, catalog names, enum case, IPv6 formatting, ACL network prefixes and merged update-policy rows.
|
|
274
|
+
TSIG names, catalog names and update-policy keys and domains compare case-insensitively after trimming leading and trailing dots; other ASCII characters and numeric labels remain literal, so `key/a` differs from `key/b` and `127.1` differs from `127.0.0.1`.
|
|
275
|
+
Permission replacements use arrays of getter-shaped rows: `userPermissions` rows contain `username`, `canView`, `canModify` and `canDelete`; `groupPermissions` rows use `name` instead of `username`.
|
|
276
|
+
An empty user permission array clears that collection; group permission replacements retain the mandatory administrator groups for the selected zone or section with all permissions.
|
|
277
|
+
See the inventory's `permissions` contract for the exact groups.
|
|
278
|
+
An omitted collection remains unchanged.
|
|
279
|
+
Administrators membership replacements retain the authenticated caller.
|
|
280
|
+
|
|
281
|
+
Zone import and Primary or Forwarder zone creation accept `upload: {"file":"/absolute/path/zone.txt"}` alongside `parameters`, without inline `records`.
|
|
282
|
+
The upload is read before preview or dispatch and bound into the confirmation by its bytes, hash and format.
|
|
283
|
+
For imports, `upload.format: "text"` sends the exact file bytes with `Content-Type: text/plain`; the default format is `"multipart"`.
|
|
284
|
+
Creation uploads require multipart format.
|
|
285
|
+
Zone imports also accept inline zone text in `parameters.records` through the form transport.
|
|
286
|
+
Zone creation, cloning, conversion, enable, disable, deletion and resync use the same preview and exact-grant boundary.
|
|
287
|
+
Delete accepts either `parameters.zone` or a nonempty `parameters.zones` array; mixed batch results report `status: "partial"` with the deleted and failed targets.
|
|
288
|
+
Resync requires an API or session credential, reads the original SOA serial, then checks up to three complete zone reads for serial advancement.
|
|
289
|
+
SOA matching trims leading and trailing dots, ignores case and normalizes internationalized zone names to ASCII while preserving distinct ASCII labels.
|
|
290
|
+
If advancement cannot be proven, it returns `OUTCOME_UNKNOWN` with an audit ID and never resends the resync.
|
|
291
|
+
Reconciliation can prove later serial advancement with the original input, confirmation and audit ID.
|
|
292
|
+
An unchanged serial can conservatively remain unknown even after a successful transfer.
|
|
293
|
+
Operation secret fields (including PEM keys, FWD proxy passwords and LDAP bind passwords) accept only `{"env":"VARIABLE"}` or `{"file":"/absolute/path/secret"}` references, not stored references or inline values.
|
|
294
|
+
Resolved operation secrets are limited to 1 MiB and are not trimmed.
|
|
295
|
+
|
|
296
|
+
DNSSEC write parameters accept integer `iterations` from 0 to 50, `saltLength` from 0 to 32 bytes, `keyTag` from 0 to 65535, and `rolloverDays` or `zskRolloverDays` from 0 to 365, where supported by the operation.
|
|
297
|
+
DNSSEC writes report success when the vendor accepts the request, without waiting for key lifecycle progress or verifying the resulting zone state.
|
|
298
|
+
Inspect `zone dnssec properties get`, `zone record get` and, for delegation data, `zone dnssec view-ds` to check the selected zone afterward.
|
|
299
|
+
|
|
300
|
+
Record selectors and replacement fields depend on both family and action; use the inventory's `recordFamilies` instead of copying a read projection.
|
|
301
|
+
Known decimal record types are normalized to names before validation; raw `rdata` selectors apply only to genuinely unknown numeric types.
|
|
302
|
+
SOA uses apex update, while generated DNSSEC records use lifecycle operations.
|
|
303
|
+
DNAME deletion, including numeric type `"39"`, selects only the owner name and type, because the server removes the DNAME record at that owner without comparing its target.
|
|
304
|
+
A `dname` value is therefore refused as an unused selector; read the record first if the target matters.
|
|
305
|
+
|
|
306
|
+
Single-use credentials are never retried and are refused for operations requiring baseline reads.
|
|
307
|
+
After dispatch they are reported as consumed, or consumed-or-uncertain on an unknown outcome.
|
|
308
|
+
|
|
309
|
+
## Safety
|
|
310
|
+
|
|
311
|
+
- Writes follow the [grant, confirmation and journal policy](#writes).
|
|
312
|
+
- Ordinary reads never acquire, refresh or store credentials.
|
|
313
|
+
- `status` and the no-argument view perform one unauthenticated HTTPS status read and send no credential.
|
|
314
|
+
- `user session get` returns a redacted projection.
|
|
315
|
+
`--result-file` additionally saves the exact confidential JSON in an atomic protected file and reports its path, byte count and SHA-256.
|
|
316
|
+
- Config and CA files must be owned by the current user and not writable by group or others.
|
|
317
|
+
File SecretRefs and stored credentials must additionally deny all group and other access.
|
|
318
|
+
These reads reject symlinks and redirected paths before consuming the file.
|
|
319
|
+
- `caFile` and `tlsServerName` select trust and certificate identity while certificate verification stays enabled.
|
|
320
|
+
- `doctor` verifies credential protection and performs one bounded session read.
|
|
321
|
+
|
|
322
|
+
## Configuration
|
|
323
|
+
|
|
324
|
+
A version 1 config holds named profiles.
|
|
325
|
+
Each profile has an HTTPS `origin`, `auth: "token"`, a `tokenKind` and a `token` SecretRef such as `{"env": "MY_TECHNITIUM_TOKEN"}`.
|
|
326
|
+
The accepted credential kinds and optional write policy are defined by the schema below; see [Writes](#writes) for execution requirements.
|
|
327
|
+
`technitium-axi schema --json` describes the accepted profile fields.
|
|
328
|
+
SecretRefs also support protected absolute files and explicitly stored credentials.
|
|
329
|
+
Inline secrets and secret flags are refused.
|
|
330
|
+
|
|
331
|
+
Config selection uses `--config`, then `TECHNITIUM_AXI_CONFIG`, then the platform config directory.
|
|
332
|
+
`technitium-axi config path` shows the selected path.
|
|
333
|
+
|
|
334
|
+
Profile selection uses `--profile`, `TECHNITIUM_AXI_PROFILE`, a complete environment profile, the configured default, or the sole profile.
|
|
335
|
+
A selected file profile ignores competing environment fields.
|
|
336
|
+
Partial environment profiles fail without borrowing fields.
|
|
337
|
+
|
|
338
|
+
The environment profile uses `TECHNITIUM_AXI_ORIGIN`, `TECHNITIUM_AXI_AUTH`, `TECHNITIUM_AXI_TOKEN_KIND` and `TECHNITIUM_AXI_TOKEN`.
|
|
339
|
+
Optional fields use the same prefix: `TECHNITIUM_AXI_USERNAME`, `TECHNITIUM_AXI_CA_FILE` and `TECHNITIUM_AXI_TLS_SERVER_NAME`.
|
|
340
|
+
Environment-only profiles and env SecretRefs work when no config file is present.
|
|
341
|
+
Relative `caFile` paths resolve beside the config file that declares them; environment profiles require an absolute CA path.
|
|
342
|
+
|
|
343
|
+
## Output and exit codes
|
|
344
|
+
|
|
345
|
+
- Output is TOON by default; `--json` gives the same approved projection.
|
|
346
|
+
- Collections default to at most 100 rows with four summary fields per object row.
|
|
347
|
+
Strings are bounded to 1,000 characters, or 1,000,000 with `--full`.
|
|
348
|
+
- Defaults are 10 pages, 12 requests, 30 seconds overall and 16 MiB of response bytes.
|
|
349
|
+
`--max-pages`, `--max-requests`, `--max-elapsed-ms` and `--max-bytes` can lower those caps or raise them within the limits reported by input errors.
|
|
350
|
+
- Usage and input errors print an `error` block with a `code`, a `message` and a `help` hint, and exit with code 2.
|
|
351
|
+
|
|
352
|
+
## Limitations
|
|
353
|
+
|
|
354
|
+
- Native Windows file protection and interruption verification are PENDING.
|
|
355
|
+
Windows file reads and writes are refused until ownership and ACL verification is available.
|
|
356
|
+
- Linux file protection is verified natively.
|
|
357
|
+
- Hosted write admission requires separate release-candidate proof; see [Offline package journeys](docs/offline-checks.md).
|
|
358
|
+
- Session hooks and an agent skill are PENDING.
|
|
359
|
+
|
|
360
|
+
## Development
|
|
361
|
+
|
|
362
|
+
Use the Node version pinned in [.node-version](.node-version) and the npm version in the `packageManager` field of [package.json](package.json).
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
npm ci
|
|
366
|
+
npm run build
|
|
367
|
+
npm run typecheck
|
|
368
|
+
npm run check:generated
|
|
369
|
+
npm run check:journey -- --task B1.01 --paths native --offline
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Native checks run on Linux with `unshare`, `ip` and `openssl` installed.
|
|
373
|
+
See [Offline package journeys](docs/offline-checks.md) for isolation, synthetic harness boundaries, cleanup and the full native and Artium checks.
|
|
374
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for test-quality rules.
|
|
375
|
+
Release automation runs `npm run test:release` and installs the packed package for CLI smoke checks.
|
|
376
|
+
`npm test` also checks the packed file list, offline installation, CLI help/version, and published module imports in a network namespace.
|
|
377
|
+
Run this check alone with `npm run test:contract`.
|
|
378
|
+
|
|
379
|
+
## Security
|
|
380
|
+
|
|
381
|
+
No `SECURITY.md` ships yet; this is PENDING.
|
|
382
|
+
|
|
383
|
+
## License
|
|
384
|
+
|
|
385
|
+
`LICENSE` and `package.json` declare `MIT`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/knowttl/technitium-axi/axi-credential.schema.json",
|
|
4
|
+
"title": "Technitium credential profile",
|
|
5
|
+
"description": "AXI Credential Standard v2 profile. Secret values must be references.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["origin", "auth", "tokenKind"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"origin": { "type": "string", "pattern": "^https://(?:\\[[0-9A-Fa-f:]+\\]|[A-Za-z0-9.-]+)(?::[0-9]{1,5})?$" },
|
|
11
|
+
"auth": { "const": "token", "type": "string" },
|
|
12
|
+
"tokenKind": { "type": "string", "enum": ["api", "session", "single-use"] },
|
|
13
|
+
"token": { "$ref": "#/definitions/secretRef", "x-secret": true, "default": { "env": "TECHNITIUM_AXI_TOKEN" } },
|
|
14
|
+
"username": { "type": "string", "minLength": 1 },
|
|
15
|
+
"caFile": { "type": "string", "minLength": 1 },
|
|
16
|
+
"tlsServerName": { "type": "string", "pattern": "^[A-Za-z0-9.-]+$" },
|
|
17
|
+
"writes": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["allowWrites", "operations"],
|
|
21
|
+
"default": { "allowWrites": false, "operations": [] },
|
|
22
|
+
"properties": {
|
|
23
|
+
"allowWrites": { "type": "boolean" },
|
|
24
|
+
"operations": { "type": "array", "uniqueItems": true, "items": { "type": "string", "enum": ["OP-002", "OP-003", "OP-004", "OP-005", "OP-007", "OP-008", "OP-009", "OP-010", "OP-011", "OP-013", "OP-019", "OP-022", "OP-023", "OP-025", "OP-026", "OP-027", "OP-028", "OP-029", "OP-030", "OP-032", "OP-034", "OP-035", "OP-036", "OP-039", "OP-040", "OP-041", "OP-042", "OP-043", "OP-044", "OP-045", "OP-046", "OP-047", "OP-048", "OP-049", "OP-051", "OP-052", "OP-054", "OP-055", "OP-057", "OP-058", "OP-059", "OP-060", "OP-063", "OP-064", "OP-065", "OP-066", "OP-070", "OP-071", "OP-072", "OP-073", "OP-074", "OP-076", "OP-077.import", "OP-080", "OP-082", "OP-083", "OP-085", "OP-087", "OP-088", "OP-089", "OP-092", "OP-093", "OP-094", "OP-095", "OP-096", "OP-097", "OP-099", "OP-100", "OP-102", "OP-104", "OP-105", "OP-107", "OP-109", "OP-110", "OP-113", "OP-115", "OP-116", "OP-117", "OP-119", "OP-120", "OP-121", "OP-122", "OP-123", "OP-124", "OP-126", "OP-127", "OP-128", "OP-129", "OP-130", "OP-131", "OP-132", "OP-133", "OP-136", "OP-137", "OP-140", "OP-142", "OP-143"] } }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"definitions": {
|
|
29
|
+
"secretRef": {
|
|
30
|
+
"oneOf": [
|
|
31
|
+
{ "type": "object", "additionalProperties": false, "required": ["env"], "properties": { "env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } },
|
|
32
|
+
{ "type": "object", "additionalProperties": false, "required": ["file"], "properties": { "file": { "type": "string", "pattern": "^(?:/|[A-Za-z]:[\\\\/]).+" } } },
|
|
33
|
+
{ "type": "object", "additionalProperties": false, "required": ["stored"], "properties": { "stored": { "const": true, "type": "boolean" } } }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
if (args.length === 1 && ['--version', '-v', '-V'].includes(args[0])) {
|
|
6
|
+
console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
|
|
7
|
+
} else {
|
|
8
|
+
const { main } = await import('../dist/cli.js');
|
|
9
|
+
await main(args);
|
|
10
|
+
}
|