All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
Directory protection write endpoints (#123, #13 write slice):
kasapi-cli directoryprotection add <path> <user> --password <pw> [--authname <name>],… update <path> <user> [--password <pw>] [--authname <name>]and… delete <path> <user>wireadd_directoryprotection/update_directoryprotection/delete_directoryprotection. A protection entry is identified by the(path, user)pair taken as two positional arguments (a single path can protect several users).updateanddeleteare gated by the #109 confirmation prompt —updatereplaces the access password (the previous one is unrecoverable) anddeleterevokes access, so both can lock users out;addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record;directory_passwordis redacted in both sinks. There is no_new_passwordsplit —updatesends the replacement under the samedirectory_passwordkeyadduses — andupdatesends only the explicitly-changed--password/--authname(keyed on cobraChanged), so an omitted password keeps the current one. KAS also accepts paralleldirectory_user/directory_passwordarrays to create several protected users in one call (hence thedirectory_user_count_neq_passcountfault); the captured request fixtures only exercise the scalar single-user form, and the array wire-encoding is not captured, so this slice deliberately models one(path, user)protection per call rather than inventing the array shape. -
Mail standard filter write endpoints (#116, #13 write slice):
kasapi-cli mail filters add <mail-login> --filter <item> [--filter <item>...]and… delete <mail-login>wireadd_mailstandardfilter/delete_mailstandardfilter. Both are gated by the #109 confirmation prompt: the KAS API has noupdate_mailstandardfilteraction, soaddreplaces the configured filter chain wholesale (items previously set but missing from the new--filterlist are dropped), which is destructive to recover from without a stored copy. Both honour--dry-run(#132) and emit a #131 audit record. Repeatable--filteritems are joined with;on the wire (the format the capturedadd_mailstandardfilterrequest fixture uses); each item is either a bare filter id (e.g.pdw) or<filter-id>:<option>=<value>(e.g.spamc_move:move=Spam). Items must be non-empty and must not contain;themselves.deletetakes only<mail-login>and removes the whole chain in one shot — the KAS API exposes no per-item delete — so its prompt verb is "remove all standard filters of mail account" rather than the bare "delete" used elsewhere, to make the all-at-once effect explicit. Known API quirk:delete_mailstandardfiltersometimes surfaces an envelope-level SOAP fault (an internalsizeof()PHP error) even when the chain was in fact removed on the server; the fault is surfaced verbatim, anddocs/usage/destructive-writes.mddocuments the verification path (mail accounts get <login>→mail_spamfilter). -
A new shared envelope-level fault fixture
testdata/response_failed_internal_server_error.xmlcaptures the generic PHPsizeof()runtime error wrapped in a SOAP-ENV:Server fault. It is exercised by the soap fixture walker and bymailfilter.Client.Delete's "fault surfaced verbatim" test. -
Mail account write endpoints (#114, #13 write slice):
kasapi-cli mail accounts add <address> --password <pw> [field flags],… update <mail-login> [field flags]and… delete <mail-login>wireadd_mailaccount/update_mailaccount/delete_mailaccount.updateanddeleteare gated by the #109 confirmation prompt;addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record; the password is redacted in both.addsplits the address on the last@into thelocal_part/domain_partKAS expects and takes nomail_login— KAS generates the login (e.g.m0000001) and echoes it inReturnInfo, which the command prints. The Y/N/text toggles and XLIST folder names default to the KAS API's own defaults, so a bareadd <address> --password <pw>is a complete create.updatesends only the explicitly-set flags (keyed on cobraChanged), adds the--active(is_active) toggle, and its--passwordmaps tomail_new_password(the_new_passwordsplit the database/ftpuser/sambauser slices carry) rather than the add-onlymail_password.responderis passed through verbatim ("N", "Y" or a<start>|<end>timestamp range).delete_mailaccount's prompt uses the louder verb "permanently delete" — it drops the mailbox and every message in it (the same data-loss emphasis asdelete_database). -
database.InProgressFalse/database.InProgressTruepackage constants for the literal"FALSE"/"TRUE"strings the KAS API uses to encode the async-write flag, so mapping code and tests share one source of truth rather than re-typing literals. -
Database write endpoints (#122, #13 write slice):
kasapi-cli databases add --password <pw> --comment <text> --allowed-hosts <hosts>,… update <database-login> [flags]and… delete <database-login>wireadd_database/update_database/delete_database.updateanddeleteare gated by the #109 confirmation prompt;addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record; the password is redacted in both.updatesends only the explicitly-set flags (keyed on cobraChanged), so an empty value is a deliberate "clear".add_databasetakes nodatabase_login— KAS generates it (the login equals the database name on creation, e.g.d0123460) and echoes it inReturnInfo, which the command prints. The password key is split between actions:--passwordmaps todatabase_passwordonaddand todatabase_new_passwordonupdate(the same_new_passwordsplit the ftpuser/sambauser slices carry).delete_database's confirmation prompt uses the louder verb "permanently delete" because the action drops the database and every row it contains — the loudest data-loss surface of the v0.2.0 write phase.
-
testdata/cronjob/{add_cronjob_response_success,add_cronjob_response_success_warning,update_cronjob_response_success}.xmlcarry a top-of-file XML comment documenting that KAS itself echoes the notification address undermail_address(double d) in theKasRequestParamsecho block, while the documented request key ismail_adress(single d). The read/write mapping uses the single-d key and never reads the echo, so the captured fixtures keep the typo verbatim instead of being normalised. Closes the last code Nice-to-have from #168. -
Address the post-write-phase nice-to-have bundle from #168:
kaswrite.Callnow prefixes the module label onto the wrappedErrUnexpectedReturnStringmessage, so a regression in e.g.mailforward.Client.Addreadskaswrite: unexpected ReturnString (want TRUE): mailforward add_mailforward got "…"instead of losing the module hint to the canonical sentinel prefix. The sentinel identity is unchanged —errors.Is(err, mailforward.ErrUnexpectedReturnString)keeps working.kasapi-cli mail forwards add --targethelp no longer claims to "replace the full target list" (which is true forupdate, not foradd); the add-side help now reads "repeatable; at least one required".kasapi-cli mail lists update --activenow takes an explicitY|Nargument (string flag), matching themail accounts update --activesurface and removing the slightly non-obvious--active=falseform that previously meant "deactivate".runWriteEresolves the credentials exactly once per write invocation: the post-gate dispatch now reuses the credsrunWriteEalready resolved for the audit login, via the new unexportedbuildAPIClientFromCredshelper. Previously the config + env + flag resolution ran twice — once for the audit, once insideBuildAPIClient— for every real write call.BuildAPIClient(the read-side seam) is unchanged.
-
Cross-module convention alignment, post-#122-review parity sweep:
account.AccountList/mailaccount.MailAccountListnow render theused_*_spacecolumn with a" MB"suffix as part of the value (matching the singular detail row) and use a bareUSEDheader. Consistent with the database slice after #122 followups — all three modules now share a single unit-rendering convention.ddns.DDNSUser.in_progressis no longer markedomitempty, aligning with the majority of read modules. Captured fixtures have always carriedin_progress; the empty-string fallback on an older account is harmless.cronjob.Client.Add,ddns.Client.Add,ftpuser.Client.Add,sambauser.Client.Add,mailinglist.Client.Addandmailforward.Client.Addnow emit per-field validation errors (requires a non-empty <field>) instead of a single combined message ("requires a non-empty X, Y and Z"). A caller hitting the domain validator can now tell which field actually broke, matching the convention introduced by the database slice in #122 followups.
-
docs/usage/destructive-writes.mdrefactored: a single "per-slice baseline" section captures the contract every wired slice carries (gating policy, dry-run/audit/redaction, generated-login printing) and a "per-slice deviations" table calls out only what each slice changes (mailforward's target-list phrasing, database's louder delete verb + optional--allowed-hostswildcard, ddns's no-_new_passwordsplit, …). Replaces eight near-identical paragraphs that had grown to roughly copy-paste. -
kasapi-cli databases addand… updatenow bind disjoint flag sets (mirroring the ddnsuser slice). The flag names are identical on both subcommands, but the help text of each reflects its own action semantics ("initial password" / "required" on add; "replacement password" on update) and cobra rejects an unknown flag at parse time. The regenerateddocs/cli/kasapi-cli_databases_*.mdpages now stop claiming "required for add" on the update help output. -
kasapi-cli databases listrenders theused_database_spacecolumn with a " MB" suffix as part of the value (matching how the singular detail view already rendered it) and drops theUSED_MBheader in favour of a bareUSEDheader. List and singular views now share a single unit-rendering convention. -
The
databasepackage'sDatabase.in_progressJSON/YAML field is no longer markedomitempty, aligning with the majority of read modules (mailaccount,mailinglist,sambauser,ftpuser,account). The KAS API has returnedin_progresson every captured fixture row, so the previous omitempty added drift without shielding callers from a missing key. -
database.Client.Add's domain-level validation now emits per-field errors ("requires a non-empty password" / "comment") instead of a single combined message, so callers who hit the domain validator (rather than the CLI's per-flag required-flag checks) can tell which field actually broke.AllowedHostsis no longer required — an empty value is the KAS API's documented "any host may connect" wildcard, not a missing parameter;kasapi-cli databases add's--allowed-hostsflag is therefore optional and the empty-string wildcard is sent verbatim on the wire. -
(cli.ConfirmAction).Summaryis now exported (wassummary), so tests can pin the rendered prompt (and the per-slice loudness verb) without instantiating a real terminal. -
kasapi-cli ddnsusers addand… updatenow bind disjoint flag sets —addcarries--zone/--label/--target-ip,updatecarries--target-ipv4/--target-ipv6instead — so each subcommand's--helpreflects only the flags the corresponding KAS action consumes and unsupported flags fail at cobra parse time rather than being silently ignored. No behaviour change for any command-line invocation that was already valid; the previously silenced-on-update flags are now a hard parse error there. -
Realigned
testdata/ddns/add_ddnsuser_request.xmlto the values the matchingadd_ddnsuser_response_success.xmlrequest-echo block carries (dyndns_target_ip=127.0.0.1,dyndns_dual_stack=N), and updated theddnswrite-testsampleSpecto match. The two fixture sides now tell the same story; no test depended on the earlier echo divergence. -
Extracted the shared KAS write post-call contract (transport-error passthrough + nil-response guard +
ReturnString="TRUE"check) from the duplicated per-module privatecallmethods inmailforward/mailinglist/sessioninto oneinternal/kaswriteseam, the write-side counterpart ofinternal/kasread. There is now one canonicalerrors.Issentinel (kaswrite.ErrUnexpectedReturnString); the per-moduleErrUnexpectedReturnStringare re-export aliases of it, soerrors.Is(err, <module>.ErrUnexpectedReturnString)keeps working. No behaviour change; the wrapped error message is now uniform (kaswrite: …). -
kasapi-cli databases list/databases getnow decode thein_progressflag the KAS API surfaces on everyget_databasesentry. The list view exposes a newIN_PROGRESScolumn, and the singular detail view appends anin_progressrow when present. Read-test fixtures were captured against an account with a newly- created databased0123460; the existing decode/tabular tests now pin against that snapshot rather than the previousd0123450-based one. -
DDNS-user write endpoints (#121, #13 write slice):
kasapi-cli ddnsusers add --password <pw> --zone <z> --label <l> --target-ip <ip> --comment <text> [--dual-stack],… update <dyndns-login> [flags]and… delete <dyndns-login>wireadd_ddnsuser/update_ddnsuser/delete_ddnsuser.updateanddeleteare gated by the #109 confirmation prompt (update_ddnsuserreplaces every supplied field wholesale);addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record; the password is redacted in both.updatesends only the explicitly-set flags (keyed on cobraChanged), so an empty value is a deliberate "clear".add_ddnsusertakes nodyndns_login— KAS generates it and echoes it inReturnInfo, which the command prints. Unlike the ftpuser/sambauser slices there is no_new_passwordsplit: the password maps todyndns_passwordon both actions, per the fixtures.update_ddnsuseraccepts--target-ipv4/--target-ipv6instead ofadd's legacy--target-ip; the dual-stackdyndns_target_ipv4/dyndns_target_ipv6request keys are undocumented in the public KAS API documentation but verified to work against the live system (observed in the KAS panel's browser network tab), and the capturedupdate_ddnsuserrequest fixture (with its success-response request echo) is the authoritative request-shape contract for the slice. The (dyndns_*prefix) write request keys keep the same wire-side asymmetry as the read path — the action / get filter use theddns*form without they. -
Samba-user write endpoints (#120, #13 write slice):
kasapi-cli sambausers add --password <pw> --comment <c> --path <p>,… update <samba-login> [flags]and… delete <samba-login>wireadd_sambauser/update_sambauser/delete_sambauser.updateanddeleteare gated by the #109 confirmation prompt (update_sambauserreplaces every supplied field wholesale);addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record; the password is redacted in both.updatesends only the explicitly-set flags (keyed on cobraChanged), so an empty value is a deliberate "clear".add_sambausertakes nosamba_login— KAS generates it and echoes it inReturnInfo, which the command prints. The KAS documentation wrongly lists the create password parameter assamba_new_password; the capturedadd_sambauserrequest fixture (and its success- response request echo) confirm the real key issamba_password, so the fixture — the authoritative request-shape contract — was corrected and the code follows it.update_sambauserdoes usesamba_new_password; the CLI maps the single--passwordflag to the correct key per command. -
FTP-user write endpoints (#119, #13 write slice):
kasapi-cli ftpusers add --password <pw> --comment <c> [flags],… update <ftp-login> [flags]and… delete <ftp-login>wireadd_ftpuser/update_ftpuser/delete_ftpuser.updateanddeleteare gated by the #109 confirmation prompt (update_ftpuserreplaces every supplied field wholesale);addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record; the password is redacted in both.updatesends only the explicitly-set flags (keyed on cobraChanged), so an empty value is a deliberate "clear".add_ftpusertakes noftp_login— KAS generates it and echoes it inReturnInfo, which the command prints. The password key differs by action:add_ftpuserusesftp_password,update_ftpuserusesftp_new_password(per the KAS docs and the captured request fixtures); the CLI maps the single--passwordflag to the correct key per command. The plural-vs- singular question from #13 is resolved: the live KAS action isadd_ftpuser(singular) — the documentation'sadd_ftpusersis the internal PHP function name, while the doc example URL, both request fixtures and the success-response request echo all use the singular form; no fixture change was needed. -
Cronjob write endpoints (#118, #13 write slice):
kasapi-cli cronjobs add --url <u> --comment <c> --minute <m> --hour <h> [flags],… update <cronjob-id> [flags]and… delete <cronjob-id>wireadd_cronjob/update_cronjob/delete_cronjob.updateanddeleteare gated by the #109 confirmation prompt (update_cronjobreplaces every supplied field wholesale);addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record;--http-passwordis redacted in both.updatesends only the explicitly-set flags (keyed on cobraChanged), so an empty value is a deliberate "clear". The KAS wire key for the notification address ismail_adress(single 'd', the same quirk the read mapping documents). Theadd/update/deletecronjob fixtures were corrected to the real KAS response shapes beforehand. -
Mailing-list write endpoints (#117, #13 write slice):
kasapi-cli mail lists add <name> --domain <d> --password <pw>,… update <name> [--subscriber …] [--restrict-post …] [--config-file <path>] [--active]and… delete <name>wireadd_mailinglist/update_mailinglist/delete_mailinglist.updateanddeleteare gated by the #109 confirmation prompt (update_mailinglistreplaces the subscriber / restrict-post / config fields wholesale);addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record; the list password is redacted in both.updatesends only the explicitly-set flags (keyed on cobraChanged), so an empty value is a deliberate "clear". Theget_mailinglistsread mapping was corrected to the real KAS schema beforehand (see Fixed). -
Mail-forward write endpoints (#115, first #13 write slice):
kasapi-cli mail forwards add <addr> --target …,… update <addr> --target …and… delete <addr>wireadd_mailforward/update_mailforward/delete_mailforward.deleteandupdateare gated by the #109 confirmation prompt (update_mailforwardreplaces the full target list and is irreversible);addis reversible and not prompted. All three honour--dry-run(#132) and emit a #131 audit record. This is the first command to exercise the gate/audit/dry-run seam end to end, via the sharedcli.runWriteErunner and the new non-gatedcli.ResolveWritecounterpart ofcli.ResolveDestructive. -
--dry-runfor destructive commands (#132, v0.2.0 write-phase prerequisite): the new global--dry-runflag previews the KAS request a destructive command would send (action + redacted parameter map, via--outputtable/json/yaml) and exits 0 without dispatching. It short-circuits the #109 confirmation prompt — even combined with--yesit only previews — and still emits a #131 audit record withoutcome=dry-runso the trace exists. The sharedcli.ResolveDestructiveseam composes the dry-run preview, the confirmation gate and the audit log so every future write command consults one entry point. Documented indocs/usage/destructive-writes.md. Wired by the mail-forward write slice (#115). -
Structured write-action audit log (#131, v0.2.0 write-phase prerequisite):
cli.AuditRecord+cli.WriteAuditemit onelogfmt-style line per dispatched write action to stderr — always on, independent of--verbose— with RFC 3339 timestamp, resolved login, KAS action, target,outcome(success/failure:<kas_code>/failure), and correlating fields. The new global--audit-log <path>flag (andKAS_AUDIT_LOG; the flag wins) additionally appends each record as JSON Lines to a0600file. Secret parameters (auth_data,*password,*token,*secret, …) are redacted in both sinks viacli.RedactParams. Documented indocs/usage/destructive-writes.md. Wired by the mail-forward write slice (#115); read commands are unaffected. -
Destructive-write confirmation gate (#109, v0.2.0 write-phase prerequisite): the global
--yes/-yflag is now wired and advertised again. Newcli.GateDestructiveenforces an explicit[y/N]prompt before any destructive SOAP call — declining or a non-interactive stdin without--yesaborts with exit code 1 (exported sentinelscli.ErrConfirmationDeclined/cli.ErrConfirmationRequired,errors.Is-able).--yesbypasses the prompt for automation. TTY detection is now shared byconfig init/add-profileand the gate. The safety contract is documented indocs/usage/destructive-writes.md. Wired by the mail-forward write slice (#115); read commands are unaffected. -
Exported sentinel
session.ErrUnexpectedReturnString(review follow-up):session.Client.Deletenow wraps it with%wwhendelete_sessionreturns without a SOAP fault butReturnStringis not"TRUE", so callers canerrors.Isthe contract violation apart from a transport or KAS-fault error instead of string-matching the message. -
kasapi-cli sessions deleteinvalidates the resolved profile's cached session token both server-side (KAS APIdelete_session) and in the localsessions.tomlcache. It acts on the currently cached token only and never bootstraps a fresh one just to delete it. The command is idempotent: a missing or already-invalid (unknown_session) session is reported and exits 0; any other transport/KAS error is surfaced with a non-zero exit after the local cache has been cleared (the local cache is the authoritative client-side state). No confirmation prompt — deleting a session merely forces a re-authentication on the next session-mode call.add_sessionis not a separate endpoint: it is the KasAuth credential-token flow already covered byinternal/auth, so it gets no subcommand. The shareddelete_sessionuse case now lives ininternal/session(session.Client);config use-profile's revoke path was refactored to delegate to it. Closes #60. -
kasapi-cli config add-profile <name>/use-profile <name>/list-profilesfor managing multiple profiles inconfig.toml.add-profilereuses theconfig initprompt flow with--forceto overwrite an existing profile;use-profileflipsdefault_profileand, if the outgoing profile had a non-expired cached session token, invokes the KAS APIdelete_sessionaction on it before removing the local entry fromsessions.toml. The server-side revoke is best-effort — a transport orunknown_sessionfault is logged under--verbosebut does not abort the switch, because the local cache is the authoritative client-side state.list-profilesprints one line per profile, marks the default with*, and never writesauth_data. Closes #39. -
The global
--no-colorand--yesflags are now hidden from--helpand the generated CLI docs (review follow-up). They were bound but read nowhere — no colourised output is emitted yet and the destructive-write confirmation gate is tracked in #109 — so advertising them implied behaviour that does not exist. They remain parseable (no breaking removal); the owning features will un-hide them when they are wired. -
Strict period decoding in
get_traffic(re-review follow-up):usage.DecodeTrafficnow decodes the mandatoryyearandmonthfields withsoap.Value.MapIntStrict, so a missing or non-numeric value yields a decode error instead of a silent0that would mislabel the reporting period. Scope is deliberately limited to those two always-present fields:daystays lenient (the monthly summary row legitimately omits it) and thehttp_/ftp_traffic and hit counters stay lenient (KAS returnsxsi:nilfor a bucket with no data, so a strict reading would turn a real no-traffic response into a hard error). This continues the strict-numeric rollout deferred in the first review follow-up. -
Strict numeric decoding for required KAS fields (review follow-up): added
soap.Value.AsIntStrict/MapIntStrict/MapInt64Strict, the error-returning siblings of the lenientAsInt/MapInt/MapInt64accessors — a missing key, empty/xsi:nil, or non-numeric value yields an error instead of being silently coerced to0;MapIntStrictadditionally rejects a value that would overflow the platformint(32-bit targets) rather than truncating it silently. Adopted in the account resource-quota decoder (decodeQuota): a present quota Map whose mandatorymax/reserved/created/used/freeintegers are malformed now failsDecodeAccountResourcesrather than misreporting a0limit. The lenient accessors are unchanged and remain the right choice for genuinely optional fields; broader per-field adoption is deferred until each field's presence is fixture-verified. -
Repo-consistency pass (post-review):
testdata/statistic/renamed totestdata/usage/so the fixture subdirectory matches its module (internal/usage, CLIusage) per the one-subdir-per-module convention;TestServerCmdHelpListsInfomoved out ofaccount_test.gointo its owninternal/cli/server_test.go;internal/cli/gendocs_test.goadded to cover thegen-docshelper;internal/{ssl,chown,symlink}doc comments now state explicitly that the packages are not-yet-implemented placeholders (issue #13); a comment onapi.CodeUnknownActionrecords that"unkown_action"mirrors the KAS API's own misspelling verbatim and must not be "corrected". No behaviour change. -
internal/cli/wire.go: the first-run error fromBuildAPIClientnow appends(run `kasapi-cli config init` to create a profile interactively)when no config file exists at all, so a fresh user who runskasapi-cli accounts listwithout env vars or flags is pointed at the existing bootstrap wizard instead of having to guess at flag combinations. Partial-config cases (file exists but a profile is incomplete) keep the bare validation error becauseconfig initrefuses to overwrite without--forcein that scenario. -
internal/session/store.go+auth/source.go+api/client.go: thread the call'scontext.Contextthroughsession.Store.Load/Save/Deleteand throughapi.Heartbeater.Heartbeat. The lock wait now usesflock.TryLockContextso a userCtrl-Cwhile anotherkasapi-cliprocess holds the sessions-file lock aborts cleanly instead of blocking forever; the synchronous toml/os calls are local-FS only so actx.Err()check at the boundary is sufficient.SessionTokenSource.Invalidatedeliberately usescontext.Backgroundfor the delete so a cancelled run still clears the stale on-disk token, matching the "cleanup is finalisation" pattern. Tests updated; behaviour for non-cancelled calls is unchanged. -
internal/auth/source.go+auth/client.go+api/client.go+transport/client.go: consolidated theslog.New(slog.NewTextHandler(io.Discard, nil))discard-logger pattern. Each of the three packages now has a single package-levelvar discardLoggerbuilt once at init; previously the same expression was inline-duplicated across constructors andlogger()helpers (andtransporthad adiscardLogger()function that re-allocated per call).NewSessionTokenSourcenow seedsLoggerto that discard logger in its constructor, matching the pattern already used byauth.New,api.New, andtransport.New. -
internal/cli/output.go+cli/root.go:formatNamesis now a package-levelvar(built once at init) instead of a function that re-built the[]stringon every call.ParseFormat's error message andjoinFormatsconsume it as a value; both call sites are read-only, so sharing the backing array is safe. -
internal/cli/output.go: tightened theErrTableNotSupportedmessage to a single phrase per Go error-style guidance — same meaning, but easier to skim in logs. -
internal/soap/value.go:KindUnknown's doc now explains why the sentinel value is exactly 255 (max ofKind's underlyinguint8, giving the iota block room to grow without collision). -
internal/cli/wire.go: documented why theauth.New(... soap.AuthPlain, authOpts)call inside theauth_type=sessionbranch hardcodesAuthPlain— KasAuth always bootstraps in plain mode regardless of session mode, and the session token is what subsequent KasApi calls use. Prevents future drive-by "fixes" that would replace the constant withAuthSession. -
internal/transport/client.go: documented whywaitGateruns once before the retry loop, not inside it. 5xx-driven retries do not decode envelopes, so no freshKasFloodDelaycan be recorded between attempts — re-checking the gate would always be a no-op. -
internal/auth/source.go:SessionTokenSourcenow exposes aLoggerfield; the three previously silents.Store.Save/s.Store.Deletecall sites now log aWarnevent when persistence fails so disk-full or permission issues surface in--verboseoutput. The in-memory cache still works in that case and the next invocation re-bootstraps via KasAuth, so behaviour is unchanged for the success path. The CLI wires--verboseinto the new field viainternal/cli/wire.go. -
internal/cli/output.go+internal/cli/root.go: deduplicated theAllFormats → []stringconversion. A new package-internalformatNames()is now the single source of truth used by both the--outputflag help (joined with|) and theParseFormaterror message (joined with,). -
internal/soap/value.go: replaced the magicKind(255)sentinel returned byclassifyTypefor unknownxsi:typevalues with a namedKindUnknown = 255constant. Documented why the constant lives outside the iota block (so future enum additions cannot collide). -
internal/cli/output.go: clarified theErrTableNotSupportedmessage. Every subcommand result type is expected to implementTabular, so this error always indicates a kasapi-cli bug rather than a user choice; the message now says so while keeping the--output=json/--output=yamlworkaround hint. -
internal/soap/envelope.go+internal/auth/codec.go: wrap the SOAP decoder's input reader inio.LimitReader(r, soap.MaxResponseBytes)(16 MB) and setdec.Strict = trueexplicitly. The KAS server is trusted, so this is defense-in-depth against a malformed or hostile response (compromised endpoint, MITM, server bug) — the largest captured fixture is ~70 KB, so the cap is well above any legitimate payload. -
internal/cli/wire.go: renamesessionOpts.any()toisSet()to stop shadowing the Go 1.18 builtin aliasanyin IDE auto-complete and code review. Behaviour and the single call site are unchanged. -
internal/session/store.go: document why the explicittmp.Chmod(0o600)afteros.CreateTempis kept — redundant on Unix but Windows ignores the create-mode bits, so the set is defense-in-depth across platforms. -
test:
internal/account— added unit tests forClient.Settings,Client.Resources, plus the previously-unexercisedTableHeadersonAccountResourcesandAccountSettings(and theTableRowsofAccountSettings). Lifts package coverage from 69.9% to 85.4%. -
test:
internal/cli— added help-output tests for thedns,domains,subdomains,tlds, andmail(incl.accounts/forwards/filters/listssubgroups) command factories, mirroring the pattern already used for the cronjobs/databases/... factories. Lifts package coverage from 60.0% to 69.9%. -
test:
internal/auth— added negative-path and nil-input tests for theIsLoginFailed,IsLoginLocked,IsUnknownSession,IsOTPPinIncorrect,IsCode, andAsErrorsentinel helpers.
-
Whole-codebase re-review follow-up (fourth pass, Med + Low findings):
- The
mail lists update--config-file/--subscriberblobs are now elided from the audit record by key (config/subscriber), independent of the multi-line/oversized shape heuristic: a single-line list config short enough to pass that heuristic could previously reach both audit sinks verbatim, including an embedded cleartext list password —docs/usage/destructive-writes.mdalready promised these blobs never do. kasapi-cli help <nonsense>now exits 1 like every other unknown command: cobra's stock help command returns nil for an unresolvable topic, so a typo'd topic read as success to scripts.help,help <group>, andhelp <group> <subcommand>keep printing help with exit 0.- The last
get_<singular>fixture missed by the third-pass rename sweep,testdata/account/get_account_response_success.xml(embedding the real plural actionget_accounts), was renamed toget_accounts_response_success_single.xml. config show/config path/config list-profilesandsessions deletenow document in their--helpthat their output is plain text and the global--outputformat flag has no effect there, instead of silently ignoring it.- The
mail accounts updateandddnsusers update--helptexts now describe every field flag as a replacement value (the convention the cronjob/ftpuser/sambauser/database update commands already follow), instead of reusingadd's wording on all but the password flag. - The white-box
runWriteEcreated_id test no longer couples to host state — it pinsKAS_AUDIT_LOGand points--configat a nonexistent temp path so it cannot read the developer's real config or append to a real audit log.
- The
-
Whole-codebase re-review follow-up (third pass, High + Med + Low findings):
- Group commands (
mail,accounts,config, ...) invoked with an unknown subcommand no longer print help and exit 0 — a typo'd subcommand read as success to scripts.cli.Finalizenow gives every non-runnable group an explicit unknown-subcommand rejection (exit 1, with cobra's "Did you mean this?" suggestions); a bare group invocation keeps printing help with exit 0. - The lazily-registered
completioncommand is registered before the exit-code walkers run, so its args-validation failures exit 1 (user error) instead of 2. - The success audit record of create actions whose identifier KAS
generates server-side (
add_ftpuser,add_database,add_sambauser,add_ddnsuser,add_cronjob,add_mailaccount,add_mailforward,add_mailinglist) now carries the assigned identifier ascreated_id, so the create correlates with the identifier later update/delete records carry as their target. - A confirmation-prompt I/O failure (neither a yes nor a no was read)
now leaves an audit record with the new outcome
abortedinstead of silently skipping the trace of a blocked destructive attempt. - The transport 5xx fault sniff also recognises a prefix-less
default-namespace
<Fault>element, matching what the SOAP decoder accepts; an end-to-end api-layer test pins that a fault delivered with HTTP 500 surfaces as the same typed*api.Errora 200-wrapped fault produces. sambausers updatebinds its own replacement-flag set instead of sharingadd's (whose--helptexts claimed "required for add") — the same add/update split the cronjob/ftpuser slices got in the second pass.- The remaining
get_<singular>fixtures whose filename encoded a non-existent singular KAS action while embedding the plural one were renamed to<real_action>_<kind>_<variant>.xmlacrosstestdata/{cronjob,ddns,domain,ftpuser,mailaccount,mailforward,mailinglist,sambauser}/(e.g.get_ftpuser_response_success.xml→get_ftpusers_response_success_single.xml), and the convention example inCLAUDE.mdnow uses a conforming name.
- Group commands (
-
Whole-codebase re-review follow-up (second pass, Med + Low findings):
- Bad user input now consistently exits 1: cobra positional-args
failures (e.g. a missing required argument) and an unknown root
subcommand were falling through to the API-error exit 2. The
command tree wraps every
Argsvalidator viacli.MarkArgErrorsAsUserErrorsand the root command classifies unknown subcommands itself. transport.Client.Dono longer blindly retries a 5xx response whose body carries a SOAP fault: the body is passed through to the decoder so the typed-fault path (auth refresh, flood fallback, exit-code classification) applies. Fault-free 5xx bodies stay retryable.testutil.AssertFaultFixturesfails onwantentries that match no fixture file on disk, so a renamed or deleted fixture cannot leave a dead pin behind; the shared top-levelresponse_failed_*.xmlset is now anchored by a dedicated test ininternal/apipinning each fixture to itsapi.Code*constant.config.Resolvewith a nil config and--profilewrapsErrUnknownProfileinstead of returning a string-only error.session.Store.Loadtreats an entry withoutexpires_at(only producible by hand-editing sessions.toml) as expired instead of never-expiring.cronjobs update/ftpusers updatebind their own flag sets instead of sharing the add flags, so--helpno longer advertises add defaults (https,*,default) or "required for add" texts — matching the database/mailaccount/ddnsuser/mailinglist split.- The
--dry-runhelp text says "write command" instead of "destructive command" — the flag covers non-gatedaddwrites too. testdata/domain/get_topleveldomains_request.xmlcarriedkas_action: get_subdomains(mis-captured copy); corrected to the action the filename and the response fixture encode.- Fixture names aligned with the documented convention:
dns/get_dns_settings_{request,response_success}_zone_host_and_record_id.xml(variant after kind) andcronjob/add_cronjob_response_success_warning.xml(a success variant, not a distinct status). internal/{ftpuser,cronjob,mailaccount}/doc.gono longer name non-existent singular get actions; the stale ftpuser "verify against the live API" note now records the #119 verification result. Theinternal/ddnsin_progresscomment no longer claims fixture backing the captured fixtures do not contain.- Docs refreshed to the shipped state: README status/CI/what-it-does
(write slices are live, not "pending"), the destructive-writes
prompt example (single line,
permanently deleteverb, stderr note), the audit-trace scope (thesessions delete/config use-profilesession logout is explicitly outside the pipeline),ROADMAP.md(mail lists get,<ddns-login>placeholder), and the CLAUDE.md repository-state paragraph.
- Bad user input now consistently exits 1: cobra positional-args
failures (e.g. a missing required argument) and an unknown root
subcommand were falling through to the API-error exit 2. The
command tree wraps every
-
Whole-codebase review follow-up (Low findings):
api.TokenSource.Invalidatenow reports whether the next Credentials call can produce fresh credentials;api.Clientskips the auth-failure retry for aStaticTokenSourceinstead of doubling the failing request with identical credentials.transport.Clientno longer classifiescontext.Canceled/DeadlineExceededas retryable — the caller's cancellation is not a transient server condition.- The destructive-write
[y/N]prompt goes to stderr so a redirected stdout cannot swallow the question. - A dispatched write whose audit sink fails keeps its true exit classification: a KAS fault stays exit 2 (audit failure reported as a warning), and a successful write renders its result before the audit error surfaces as exit 1.
- Refused (
outcome=refused, non-TTY without--yes) and declined (outcome=declined, prompt answered no) destructive attempts now leave an audit record; previously only dispatched writes and dry-runs were traced. Documented indocs/usage/destructive-writes.md. auth.DecodeResponsevalidates the credential token shape (40 alphanumeric characters) before it is cached and persisted; the error reports only the length, never the content.auth.EncodeRequestrange-checksLifetimeagainst the documented 1..30000 session_lifetime bound (0 = server default).- New
config.ErrUnknownProfile/config.ErrMissingCredentialssentinels;Resolvefailures areerrors.Is-able instead of string-matchable only. dns/directoryprotection/server/usagenow keep theirCallerunexported (c), matching the other ten modules; the exportedAPIfield was construction surface no caller used.cronjob.FieldIDreplaces three hardcoded"cronjob_id"literals.internal/{softwareinstall,directoryprotection,database}/doc.gono longer name non-existent KAS actions (get_softwareinstalls,get_directoryprotections,get_database); fixtures encoding those non-actions in their filenames were renamed to the real action plus a variant suffix (get_databases_{request,response_success}_single.xml,get_directoryprotection_{request,response_success}_all.xml,get_softwareinstall_{request,response_success}_all.xml).- The captured
testdata/{chown,ssl,symlink}/fault fixtures (#125 placeholders) are now anchored by one-linetestutil.AssertFaultFixturestests instead of sitting unreferenced. - Fixture values
info@example1.organd/example-new.com/replaced with RFC 2606-reserved names (example.org,example.net).
-
Whole-codebase review follow-up (Med findings):
transport.Clientnow enforces the 16-MBsoap.MaxResponseBytescap at the HTTP body read. Previously the cap lived only in the soap decoders, which run on an already fully-buffered body — the memory-exhaustion guard was dead code on the live path. An oversized response fails immediately and is not retried.- Write-command success output now honours
--output: the success line renders through the shared pipeline, so--output=json/--output=yamlemit a{"message": ...}object scripts can parse while the default table output stays the bare line. cli.RedactParamselides multi-line or oversized parameter values (<elided N bytes>). Theupdate_mailinglistconfig / subscriber blobs previously reached the stderr logfmt line, the--audit-logJSON sink, and the--dry-runpreview verbatim — and the list config can carry the list password in cleartext.SessionTokenSource.Heartbeatpersists via the newsession.Store.Refresh, which extends an entry only while the on-disk token still matches. Previously a heartbeat blindly re-saved its process's token and could clobber a newer token another process had persisted in the meantime.internal/mailinglist/write_test.gofault-map keyadd_mailinglist_response_failed_mailinglist_mailinglist_domain_doesnt_exist.xmlhad a doubled prefix and matched no fixture, so themailinglist_domain_doesnt_existcode pin silently never ran.get_server_informationfixtures moved fromtestdata/account/to their owntestdata/server/(one-subdir-per-module convention), and theserverandusagemodules gained the missing fault-fixture leg (*_response_failed_no_auth.xml, the action-independent captured auth fault) plustestutil.AssertFaultFixturescoverage.ROADMAP.mdlistedserver get; the shipped command isserver info.CHANGELOG.md[Unreleased]had duplicated, unordered subsections (### Changed×3,### Fixed×2); consolidated into one block per type in canonical Keep-a-Changelog order.
-
dns listnow exposes the documented optionalrecord_idfilter instead of a non-existentnameserverparameter: the flag is renamed--nameserver→--record-idand the wire parameternameserver→record_id, matching the KASget_dns_settingscontract (zone_hostrequired,record_idoptional) and the captured request fixtures.nameserveris a real KAS key, but forreset_dns_settings, notget_dns_settings; the read slice had carried it over by mistake. -
Audit redaction now also catches the German "passwort" spelling.
redactParammatched only the Englishpassword/passwdsubstrings, so real KAS keys such asftp_passwort/db_passwortwould have reached an audit /--dry-runrecord unredacted once a write slice sends them. No current write endpoint does, so this is a latent fix;"passwort"is now in the substring rule and covered by a test. (Full-project review follow-up.) -
Fault-fixture contract coverage extended to every module. The shared
testutil.AssertFaultFixturesanchor asserts every capturedtestdata/<module>/*_response_failed_*.xmldecodes to a*soap.FaultErrorwith a non-empty code (plus curated per-module documented-code samples); the two write slices were refactored onto it so there is one pattern. Previously onlymailforward/mailinglisthad this; the read modules' ~370 fault fixtures were unreferenced.internal/auth/doc.gonow states its adapter layer so its legitimateinternal/transportdependency is not misread as a layering violation. (Full-project review follow-up.) -
kasapi-cli mailhelp no longer under-describes its subtree: the parentShortsaid only "Inspect …" whileforwardsandlistsnow also add/update/delete. It now reads "Inspect mail accounts and filters; inspect and manage forwards and mailing lists" (regenerated indocs/cli/). StaleNewMailCmdcomment and theupdate_mailinglistfield-constant doc comment corrected; the destructive-writes doc now matches the gatedupdateprompt wording. Added amail lists update --dry-runregression test pinning the cobra-Changedfield assembly (--active→Y/N, omitted-not-sent, repeated--subscribernewline-join). #117 re-review follow-up. -
Audit logfmt records no longer split across physical lines. A write field value containing a newline or carriage return (now reachable via
mail lists update --subscriber …/--config-file) is escaped to the two-character\n/\rinside a quoted value in the stderrlogfmtline, so a logfmt consumer can parse each record atomically. The JSON-Lines--audit-logsink was already correct. (#117 re-review follow-up.) -
get_mailinglistsresponse mapping corrected to the real KAS schema. Themailinglist.MailingListmodel previously carriedmailinglist_admin/mailinglist_url, which the API does not return; it now mapsmailinglist_name,mailinglist_domain,mailinglist_password(surfaced via--output=json|yamlonly, never in table output — mailaccount precedent),mailinglist_is_activeandin_progress, plus the singular-view-onlymailinglist_subscriber/mailinglist_config/mailinglist_restrict_post. Table columns for the list view are nowNAME DOMAIN ACTIVE IN_PROGRESS. Read-only mapping change; no CLI surface change. -
Exit-code classification for
sessions delete(re-review follow-up): a failure to remove the localsessions.tomlentry now exits with the user-error code (1) instead of the API-error code (2). A local cache-removal failure is a client-side problem, not a KAS fault or network failure, so it now matches the same classification thegen-docsfilesystem failures already use. The truthful "could NOT be cleared" message and the non-zero exit are unchanged; only the code differs. -
Monotonic flood-delay gate (review follow-up):
transport.Client.RecordDelaynow extends the gate tonow+donly when that is later than an already-pending deadline, instead of unconditionally overwriting it. Previously a shorterKasFloodDelayarriving while a longer gate was still active reset the gate to the shorter window, which could let the client resume early and trip the server's flood protection. An explicit zero/negative delay still clears the gate unconditionally (unchanged contract). -
Read-path safety and transport cancellation context (review follow-up): the generic
kasread.ListGet.Getaccessor now returns an explicit"<label>: %q matched N entries (expected unique)"error when a singular-variant lookup comes back with more than one entry, instead of silently returning the first — enforcing the documented "single matching entry" contract for every module'sGet. Transport flood-gate and retry-backoff sleeps that are interrupted by context cancellation now wrap the error with the phase (flood-gate wait interrupted/retry backoff interrupted) while preservingerrors.Is(err, context.Canceled). Theinternal/ddnsfield-availability comment was corrected to match the captured fixtures (both the list and singular variants returndyndns_target_ipv4/ipv6). -
Truthful CLI output and exit-code classification (review follow-up):
kasapi-cli sessions deleteandconfig use-profileno longer claim the local session cache was cleared (or that the server-side session was invalidated) when the underlyingstore.Delete/delete_sessioncall actually failed — the message now reflects what really happened, andsessions deletereturns a non-zero exit when the local cache removal fails instead of swallowing it.gen-docslocal filesystem failures (mkdir/ write) now map to the user-error exit code (1) viaUserErrorinstead of falling through as the API-fault code (2). No behaviour change to the success paths. -
internal/session/store.go: serialiseLoad/Save/Deletethrough an advisory file lock (github.com/gofrs/flock) at<sessions.toml>.lock. Previously twokasapi-cliprocesses running in parallel (scripts, CI pipelines) could race on the read-modify- write cycle so a Heartbeat from one silently lost a Save from another. Atomic temp+rename already protected the file against corruption; this fix protects the logical transaction. Worst-case symptom was a lost token causing one extra KasAuth refresh — the cache is self-healing, but surfacing the race is preferable. -
.claude/skills/kasapi-cli-vertical-slice/SKILL.md: corrected two factually wrong claims in the slice anatomy. The mapper naming convention isDecode<Thing>(e.g.DecodeAccounts), notMap<Action>Response; the client accessor lives on the module's own*Clientin the module package (dispatching via a per-packageCallerinterface), not ininternal/api/client.go. The transport- levelKasRequestParamsenvelope is filled centrally ininternal/soap/request.go; module code passes plainmap[string]anytoCaller.Call.CONTRIBUTING.md:57-58was already correct on both points; the skill drifted. -
ROADMAP.md: added a newCLI write safetysection listing the cross-cutting prerequisites that gate every destructive subcommand — the destructive-write confirmation infrastructure (#109), the structured write-action audit log (#131), and--dry-runfor write commands (#132). These are not KAS-API endpoints but block the entire v0.2.0 write phase; tracking them on the roadmap keeps the contributor view of "what is still pending" honest. -
ROADMAP.md: corrected the mail standard filter write entry from the non-existentupdate_mailstandardfilterto the actual KAS actionsadd_mailstandardfilteranddelete_mailstandardfilter(the captured fixtures and issue #116 are authoritative; the KAS API has noupdate_mailstandardfilter). -
CONTRIBUTING.md: roadmap links no longer detour throughREADME.md#roadmap(which is a one-line pointer with no checklist) — they now point directly atROADMAP.md. The CI gate description was expanded to match what actually runs on every PR (gosec,govulncheck,go build,docs sync,goreleaser config check, CodeQL). Thekasapi-cli-vertical-sliceskill is now listed under authoritative references alongsidekasapi-cli-git-workflowandkasapi-cli-code-review. -
README.md: dropped the incorrect "read and write operations" claim from the "What it does" paragraph (writes are still pending), tightened the Status sentence to mention the v0.1.0 read modules instead of "several", and corrected the output-format section (tableis the default; the available formats arejson/yaml/table, not just JSON). -
docs/usage/mail.md: corrected the description ofmail filters list. The previous text claimedget_mailstandardfilterreturns "server-side filter rules (Sieve-style: condition + action)"; the endpoint actually returns the catalog of pre-defined spam/virus filter presets that an account can attach via themail_spamfiltersetting on a mailaccount or forward.
First public pre-release. Scope: the read-only KAS API surface
(accounts, server info, domains/subdomains/TLDs, DNS, mail, databases,
FTP/Samba users, cronjobs, directory protection, software installs,
DDNS users, usage statistics) plus session and plain authentication,
output formatters (table / json / yaml), config-file plumbing,
and the goreleaser pipeline (multi-arch binaries, deb/rpm,
keyless-cosign signatures, SPDX SBOMs). Write endpoints are out of
scope for this alpha and are tracked under v0.2.0 (#13). Expect
breaking changes between this alpha and the stable v0.1.0 tag — CLI
flag names, output structures, and exit-code mappings are not yet
frozen.
.github/workflows/release.ymlcosign pin:sigstore/cosign-installer@v3was pinned tov2.4.1, which cannot read the bundle format that newergoreleaser-action@v7releases ship. The release workflow failed duringgoreleaser-action's self-verification withbundle does not contain cert for verification, please provide public key. The pin is dropped so the installer picks its current default; goreleaser-action keeps in step with it..goreleaser.yamlarchives.filesglob:docs/cli/**/*anddocs/usage/**/*matched nothing (both directories are flat) so the rendered docs were missing from the release archives. Switched todocs/cli/*/docs/usage/*so the docs land alongsideLICENSE,README.md, andCHANGELOG.mdin every tarball/zip.usage traffic --year/--monthrange-validation errors now exit with code 1 (user error) instead of code 2 (API error). ThePreRunEpreviously returned a plainfmt.Errorfwhich fell throughcli.CodeFor's default branch.dns list --domain ""now reportsrequired flag --domain not providedwithout the redundant--domain:prefix and exits with code 1; the validation moved into a dedicatedPreRunEso the body can use the canonicalrunListEshape.
-
account.Client.Listnow returnsaccount.AccountListdirectly, matching every other read module. The CLI no longer needs to convert[]Accountto the named list type, and thekasread.ListGetfield is parameterised onAccountList. -
Collapsed the four remaining stand-alone
Caller interfacedeclarations ininternal/directoryprotection,internal/dns,internal/server, andinternal/usagetotype Caller = kasread.Caller, matching the nine modules that were already aliased after issue #73's PR B. The shape was identical in all four cases; the alias removes the last bit of duplicated interface boilerplate and ensures a future change to theCallercontract is a single-file diff ininternal/kasread.
- Closed the two remaining follow-ups from issue #73's NTH bundle:
verified against the KAS docs that
get_mailstandardfilteraccepts no filter parameter (themailfilter.Clientdoc-comment now links the spec page authoritatively, no code change needed), and addedTestClientGetNotFoundforddns.Client.Getto pin the empty-array fallback that the prior test suite did not exercise.
- Unified the
get-subcommandUse:placeholders to the KAS-wire-parameter rule (filter key with hyphens):<dyndns-login>→<ddns-login>,<subdomain>→<subdomain-name>,<address>→<mail-forward>,<domain>→<domain-name>. The 8 placeholders that already followed the rule (e.g.<ftp-login>,<cronjob-id>,<software-id>) are unchanged.docs/cli/regenerated. Cosmetic finalisation of the cross-module-duplication clean-up bundle tracked in issue #73. - Centralised the singular-record
[]string{"FIELD", "VALUE"}table-header literal — duplicated 13 times across 11 read modules — into the new sharedinternal/tablefmtpackage'sFieldValueHeadersvariable. Each module'sTableHeaders()for the singular view now returns the shared variable; a future rename ("KEY"/"VALUE", localisation, …) is one diff rather than thirteen. Part four of the cross-module-duplication clean-up bundle tracked in issue #73. - Replaced the duplicated
cobra.RunEbodies in 14 CLI files (account, cronjobs, databases, ddnsusers, directoryprotection, domains, ftpusers, mail, sambausers, server, softwareinstalls, subdomains, tlds, usage) with two generic factoriesrunListE[T]/runGetE[T]ininternal/cli/run.go. Each subcommand now passes a one-line closure that owns its module client construction and the actual call; the factory handlesBuildAPIClient,APIError(action)wrapping, andRender. 28 RunE bodies migrated; behaviour and exit codes are unchanged. Part three of the cross-module-duplication clean-up bundle tracked in issue #73. - Replaced the duplicated
Client.List/Client.Getboilerplate in 12 read modules (account, cronjob, database, ddns, domain, ftpuser, mailaccount, mailforward, mailinglist, sambauser, softwareinstall, subdomain) plus theList-only mailfilter with a single generic helperkasread.ListGet[L, E]. Each module now binds the action, label, filter key and decoder once inNewClientand exposesList/Getas one-line delegates; per-moduleCallerinterfaces collapse to type aliases overkasread.Caller. Behaviour and error messages are unchanged. Part two of the cross-module-duplication clean-up bundle tracked in issue #73. - Replaced the duplicated KAS
ArrayofMapdecoder boilerplate in 19 read decoders with a single generic helpersoap.DecodeArray[T]. Each module'sDecode<Foo>snow delegates the kind/item-shape checks to the helper and only declares its per-item mapper; behaviour and error messages are unchanged. Part one of the cross-module-duplication clean-up bundle tracked in issue #73.
-
Vulnerability and security scanning, stage 2 of two: GitHub-native CodeQL workflow (
.github/workflows/codeql.yml) running on PR + push tomain+ weekly cron, with thesecurity-extendedquery pack; OSSF Scorecard workflow (.github/workflows/scorecard.yml) running weekly + on push tomain, publishing the score for the public dashboard at https://securityscorecards.dev;SECURITY.mdat the repo root with the disclosure policy, response expectations, and verification recipe (the short hint inCONTRIBUTING.mdnow links here); SBOMs (SPDX-JSON) generated per release artefact via goreleaser'ssboms:block — Syft is now load-bearing inrelease.yml, so the previouscontinue-on-error: truewas removed. -
Vulnerability and security scanning, stage 1 of two: a new
govulncheckCI job (official Go vulnerability scanner, call-graph aware) on every PR + push tomain;gosecadded to thegolangci-lintlinter set in.golangci.yml; Dependabot configured forgomodandgithub-actionsecosystems via.github/dependabot.yml. Existing call sites that triggered gosec false-positives (test fixture loaders,os.Stdin.Fd()conversion, public-docsMkdirAllmode) were annotated with targeted//nolint:gosecmarkers carrying the rule ID and a one-line reason. CodeQL, OSSF Scorecard,SECURITY.md, and SBOM-in-release land in stage 2.
-
Go toolchain bumped from 1.23 to 1.25 (
go.moddirective plusgo-version: "1.25"inci.ymlandrelease.yml). Required to clear all reachable Go-stdlib CVEs that the newgovulncheckjob surfaced — 18 against 1.23, 7 of which (asn1 / net/url / encoding/pem / crypto/tls / crypto/x509 / os) only have 1.25.x backports, since 1.23 and 1.24 have both dropped out of the security-supported window. No source-level changes were needed for the bump itself. -
Release pipeline (
.goreleaser.yaml+.github/workflows/release.yml) driven bygit tag v*. Builds Linux + Windows ×amd64/arm64binaries withinternal/versionldflags wired up, packages Linux artefacts asdebandrpmvianfpm, ships tarballs (tar.gz) / ZIPs alongside, and signs every artefact plusSHA256SUMSkeylessly withcosignvia GitHub OIDC. A new CI jobgoreleaser config checkvalidates.goreleaser.yamlon every PR. README gains anInstallsection pointing at the Releases page with acosign verify-blobrecipe. Makefile getsrelease-snapshot(local dry run into./dist, skips signing) andrelease-checktargets. AURPKGBUILDand a Homebrew tap are tracked separately as a follow-up. -
CI job
docs sync(.github/workflows/ci.yml) that runsmake docsand fails when the checked-indocs/cli/differs from the regenerated output. Ensures any change to a flag, subcommand registration, or short/long description comes paired with adocs/cli/refresh. -
Per-resource usage docs under
docs/usage/(eight pages —accounts,server,domains,dns,mail,databases,usage,hosting— plus an indexREADME.md). Each page lists the most common invocations, sketches the table / JSON output shape, and links to the matching KAS-API documentation page. -
Auto-generated Markdown CLI reference under
docs/cli/, produced by a new hiddenkasapi-cli gen-docs <out-dir>subcommand wrappingcobra/doc.GenMarkdownTree. The root command'sDisableAutoGenTagis flipped before generation so re-running the generator produces byte-identical output when the CLI surface has not changed. -
Top-level
Makefilewith adocstarget (make docs) that wipesdocs/cli/and regenerates it viago run ./cmd/kasapi-cli gen-docs docs/cli. Other targets (build,test,lint,vet,fmt,clean) wrap the standard Go loop documented inCONTRIBUTING.md. Closes #35.
-
internal/ddnsread module andkasapi-cli ddnsusers list|getsubcommand tree wrappingget_ddnsusers. The list variant decodes the Array of Maps into a typedDDNSUserList;get <dyndns-login>reuses the same endpoint with addns_loginfilter (note: the filter parameter has noy, unlike the response keys which use thedyndns_*prefix; per the KAS docs atget-ddnsusers-inc.html) and unwraps the single-entry result. The list view joinsdyndns_labelanddyndns_zoneinto a singleFQDNcolumn so the table reflects the hostname clients will actually look up; the explicitdyndns_target_ipv4/dyndns_target_ipv6fields surface as separate K/V rows in the singular view when the API populated them.dyndns_passwordis omitted from both table views (still available via--output=json|yaml). The KAS API signals "filter matched no entry" with adyndns_login_not_foundSOAP fault rather than an empty array; that fault propagates as an*api.Errorand is detected byapi.IsNotFound. Mapping tests run againsttestdata/ddns/get_ddnsusers_response_success.xmlandget_ddnsuser_response_success.xml. Refs #11. -
internal/softwareinstallread module andkasapi-cli softwareinstalls list|getsubcommand tree wrappingget_softwareinstall(note: the KAS action name is singular for both variants). The list variant decodes the Array of Maps into a typedSoftwareInstallList;get <software-id>reuses the same endpoint with asoftware_idfilter and unwraps the single-entry result. The list view collapses the PHP and database{from, upto}version pairs into one column each ("8.4", "10.5..12.0"), prefixes the DB column with the engine name, and renders the0.0"not applicable" sentinel as—. The base64imagedata URI is kept on the struct for JSON/YAML round-trip fidelity but stripped from both table views. Mapping tests run againsttestdata/softwareinstall/get_softwareinstalls_response_success.xml(22 entries) andget_softwareinstall_response_success.xml. Refs #11. -
internal/directoryprotectionread module andkasapi-cli directoryprotection list [--path PATH]subcommand wrappingget_directoryprotection. The KAS endpoint returns one entry per(directory_path, directory_user)tuple, so a directory with N users surfaces as N rows; for that reason this slice is exposed as a list with an optional--pathfilter rather than the usual list+get pair (matching thedns list --domainshape).directory_passwordis omitted from the table view but remains available via--output=json|yaml. Mapping tests run againsttestdata/directoryprotection/get_directoryprotections_response_success.xmlandget_directoryprotection_response_success.xml. Refs #11. -
internal/cronjobread module andkasapi-cli cronjobs list|getsubcommand tree wrappingget_cronjobs. The list variant decodes the Array of Maps into a typedCronjobList;get <cronjob-id>reuses the same endpoint with acronjob_idfilter and unwraps the single-entry result, matching the established read-slice pattern. The list view collapses the five schedule fields into a single crontab(5)-styleSCHEDULEcolumn and renders the trigger target as eitherprotocol://http_urlorshell_command; the singular view keeps the raw fields plus the joined schedule.xsi:nilvalues forshell_command/timeoutround-trip cleanly to zero values flagged withomitempty. Mapping tests run againsttestdata/cronjob/get_cronjobs_response_success.xmlandget_cronjob_response_success.xml. Refs #11. -
internal/sambauserread module andkasapi-cli sambausers list|getsubcommand tree wrappingget_sambausers. The list variant decodes the Array of Maps into a typedSambaUserList;get <samba-login>reuses the same endpoint with asamba_loginfilter (per the KAS API docs atget-sambausers-inc.html) and unwraps the single-entry result, matching the mail accounts / accounts / databases pattern. The list view shows login, path, comment, andin_progress; the singular view falls back to a key/value table and omitssamba_password(still available via--output=json|yaml). Mapping tests run againsttestdata/sambauser/get_sambausers_response_success.xmlandget_sambauser_response_success.xml. Refs #11. -
internal/ftpuserread module andkasapi-cli ftpusers list|getsubcommand tree wrappingget_ftpusers. The list variant decodes the Array of Maps into a typedFTPUserList;get <ftp-login>reuses the same endpoint with anftp_loginfilter and unwraps the single-entry result. The list view shows login, path, comment, main-user flag, the three permission flags (R/W/L), the ClamAV scan flag, andin_progress; the singular view falls back to a key/value table and omitsftp_password/ftp_passwort(still available via--output=json|yaml). Mapping tests run againsttestdata/ftpuser/get_ftpusers_response_success.xml,get_ftpuser_response_success.xml, and the empty-list fixture (get_ftpuser_response_success_empty_list.xml). Refs #11. -
internal/databaseread module andkasapi-cli databases list|getsubcommand tree wrappingget_databases. The list variant decodes the Array of Maps into a typedDatabaseList;get <database-login>reuses the same endpoint with adatabase_loginfilter and unwraps the single-entry result, mirroring the mail accounts / accounts pattern. The list view reportsused_database_spacein MB; the singular view uses a key/value table and omitsdatabase_password(still available via--output=json|yaml). Mapping tests run againsttestdata/database/get_databases_response_success.xmlandget_database_response_success.xml. Refs #11. -
account.Client.Get(ctx, login)andkasapi-cli accounts get <account-login>callingget_accountswith anaccount_loginfilter. The result is unwrapped from the single-entry array so the CLI can render a key/value detail view; an empty array surfaces as a not-found error. Mapping test runs againsttestdata/account/get_account_response_success.xml. -
internal/mailinglistread module andkasapi-cli mail lists list|getsubcommand tree wrappingget_mailinglists. The list variant decodes the Array of{mailinglist_name, mailinglist_admin, mailinglist_url, in_progress}Maps into a typedMailingListList;get <name>reuses the same endpoint with amailinglist_namefilter (per the KAS docs atget-mailinglists-inc.html) and unwraps the single-entry result, mirroring the mail-forwards pattern. The singular view falls back to a key/value table so the URL stays readable without truncation. Mapping tests run againsttestdata/mailinglist/get_mailinglists_response_success.xmlandget_mailinglist_response_success.xml. Closes #9. -
internal/mailfilterread module andkasapi-cli mail filters listsubcommand wrappingget_mailstandardfilter. Decodes the Array of{filter, type, title, recommended}Maps into a typedStandardFilterListso callers can resolve the preset filter ids used bymail_spamfilteron accounts/forwards. Mapping test runs againsttestdata/mailfilter/get_mailstandardfilter_response_success.xml. Refs #9. -
internal/mailforwardread module andkasapi-cli mail forwards list|getsubcommand tree wrappingget_mailforwards. The list variant decodes the full Map-of-Maps payload into a typedMailForwardList;get <address>reuses the same endpoint with amail_forwardfilter (the source address) and unwraps the single-entry result, mirroring the mail accounts pattern. Mapping tests run againsttestdata/mailforward/get_mailforwards_response_success.xmlandget_mailforward_response_success.xml. Refs #9. -
internal/mailaccountread module andkasapi-cli mail accounts list|getsubcommand tree wrappingget_mailaccounts. The list variant decodes the full Map-of-Maps payload into a typedMailAccountList;get <mail-login>reuses the same endpoint with amail_loginfilter and unwraps the single-entry result. The--output=tableview shows login, address, used MB, responder flag and active state; the singular view falls back to a key/value table so the wider field set (xlist folders, 2FA flag, quota rule, webmail autologin) stays readable. Mapping tests run againsttestdata/mailaccount/get_mailaccounts_response_success.xmlandget_mailaccount_response_success.xml. Refs #9. -
kasapi-cli subdomains get <name>callsget_subdomainswith asubdomain_namefilter and unwraps the single-entry result, mirroring the existingdomains getflow; the singularSubdomainvalue renders as a key/value table with the SSL cert/key/CSR PEM bodies summarised as<bytes,lines>. -
internal/domain,internal/subdomain, andinternal/dnsread modules with the matching CLI subcommand trees:kasapi-cli domains listanddomains get <name>(get_domains, the latter passing adomain_namefilter and unwrapping the single-entry result),kasapi-cli subdomains list(get_subdomains),kasapi-cli tlds list(get_topleveldomains), andkasapi-cli dns list --domain <d> [--nameserver <ns>](get_dns_settings). Domain typesDomain,SSL,TLD,Subdomain, and DNSRecorddecode the KAS Map/Array payloads into typed Go values; the SSL cert/key/CSR PEM bodies are carried through but summarised as<bytes,lines>in the--output=tableview ofdomains getso the key/value layout stays readable. Mapping tests run against the shippedtestdata/domain/,testdata/subdomain/, andtestdata/dns/fixtures. Closes #8. -
internal/usagepackage andkasapi-cli usagesubcommand tree covering the three KAS read endpoints around webspace and traffic counters:usage space(get_space) lists per-account webspace totals with a usage ratio;usage space-detail [--directory PATH](get_space_usage) reports per-directory file counts and byte sums;usage traffic [--year Y --month M](get_traffic) returns the monthly summary plus per-day rows. The decoder maps the get_traffic Map keyed by0/01..31into a slice (summary first, then days), treatsxsi:nilFTP fields as zero, and parses the xsd:string-encoded byte counts intoint64so 9-digit values survive on 32-bit platforms. Closes #10. -
internal/sessionpersistent session-token cache so a successful KasAuth login (including 2FA via--otp) survives across CLI invocations: a newsessions.tomlnext to the config file (mode0600, atomic temp+rename) stores{token, expires_at, lifetime_seconds, update_lifetime}keyed by login.auth.SessionTokenSourcenow loads the cached entry on first use, reuses it whileexpires_athas not been reached, persists every fresh KasAuth response, and deletes the entry onInvalidate. Lifetime defaults tosession.DefaultLifetime(24 h, matching the KasAuthsession_lifetimedefault) when--session-lifetimewas not set; otherwise it mirrors the flag value. With--session-update-lifetime Y,api.Client.Callnow invokes a new optionalHeartbeaterinterface on the token source after every successful call so the localexpires_atrolls forward in lockstep with the server-side window. Practical effect: rerun a command and no--otpprompt is needed for as long as the session is alive.
-
CLAUDE.md: rewrite stale Repository State paragraph that still described the project as greenfield (nocmd/,internal/,go.mod, no git repository, no build/test runnable). Replace with the current state: read-phase modules wired up,mainprotected with required signatures, CI gate (lint & test+docs sync) green on every push. Fix thetestdata/filename convention to match the real layout (<module>/<kas_action>_ response_<status>[_<variant>].xml, notget_<thing>.xml). Add pointers to thekasapi-cli-git-workflow/kasapi-cli-code-reviewskill files andCONTRIBUTING.mdalongside the existingdocs/go/references. Wire the standard command loop to theMakefiletargets that exist today. -
go.mod: bump dependency pins after a routine audit pass —golang.org/x/termv0.30.0 → v0.34.0 (last release that still builds against the project'sgo 1.23.0baseline; v0.35.0+ requires Go 1.24, v0.41.0+ requires Go 1.25 — out of scope for this loop). Indirect bumps:golang.org/x/sysv0.31.0 → v0.35.0,github.com/spf13/pflagv1.0.9 → v1.0.10,github.com/cpuguy83/go-md2man/v2v2.0.6 → v2.0.7. The generateddocs/cli/is byte-identical after the bump.
-
docs/usage/: replace 404 KAS-API anchor URL (packages/API%20Functions.html, used as a generic placeholder in every page) with per-functionfiles/<kas_action>-inc.htmlURLs. Five referenced KAS actions did not exist — replace with the canonical names captured intestdata/:get_accountusage→get_space,get_accountusagedetail→get_space_usage,get_accounttraffic→get_traffic,get_tlds→get_topleveldomains,get_mailfilter→get_mailstandardfilter. All 24 external doc links in the user-facing markdown set now resolve to HTTP 200. -
--verbose/-vwas bound to aRootOptionsfield but never read anywhere; the flag was effectively a no-op. Plumb a*slog.Logger(text handler on stderr when verbose, discard otherwise) throughBuildAPIClientintotransport.Client,api.Client, andauth.Client. Events emitted: resolved credentials withauth_dataredacted (cli); SOAP action before each request, auth-failure retry,flood_protectionfallback gate, appliedKasFloodDelay(api);KasFloodDelaygate wait, transient-error retry attempt (transport);KasAuthbootstrap and credential-token issued with login + token length only (auth). Stdout stays clean for-o json | jqpipes; logs go to stderr only.
-
README.md: expand with a Configuration section (TOML profile example for bothauth_type=plainandauth_type=session,KAS_LOGIN/KAS_AUTHDATA/KAS_AUTHTYPEenv-var reference, flag/env/profile precedence), a Quick start section with the read commands that exist today (accounts list|get|resources,server info,--outputformats), and a Troubleshooting section coveringKasFloodDelay, theno_auth/unknown_session/kas_session_invalidretry behaviour,--verbose, and a pointer at the signed-commit / branch-protection rules inCONTRIBUTING.md. Closes #14. -
CONTRIBUTING.md: add explicit pointers to thekasapi-cli-git-workflowandkasapi-cli-code-reviewskill files alongside the existing references toAGENTS.mdand thedocs/go/set; absorb the contributor-facing "Repository layout" section that previously lived inREADME.md. -
internal/testutil: extract therepoRoot/decodeFixture/fakeCallerhelpers that every per-module*_test.gocarried as a private copy into a single shared package, and migrate all 20 test files (17 module test suites +internal/{soap,auth,api}'s own root-discovery helpers) to use it.DecodeFixturenow takes a forward-slash-separated path rooted attestdata/(e.g."mailinglist/get_mailinglists_response_success.xml") so the fixture layout is visible at the call site instead of being hidden inside per-moduledecodeFixture(t, name)wrappers. TheFakeCallerstub is exported withResp/Err/GotAction/GotParamsfields so cross-module test code can construct it directly. Net effect: ~910 lines of word-for-word boilerplate removed; behaviour unchanged (the loop ends only aftergo test -race ./...andgolangci-lint runare clean against the migrated suite). -
kasapi-cli mail lists getargument placeholder renamed from<name>to<mailinglist-name>so the help text matches the KAS wire parameter and the placeholder convention used by every othergetsubcommand (<address>,<mail-login>,<dyndns-login>,<domain>,<software-id>, …).TestMailingListSingularTabularwas tightened from a map-lookup over rows (order-insensitive) to an indexed comparison so a future refactor reorderingTableRowscannot slip past the test silently. -
internal/soap: extendValuewith typed Map accessors (MapString,MapInt,MapInt64,MapFloat) and a genericAsIntcoercion so every read module can drop its privategetString/getInt/getInt64/getFloathelper. Migrated 17 read packages to the new accessors (~28 helper copies removed; net –195 lines). Behaviour is unchanged — the new methods replicate the existing nil-safe coercion rules and are pinned byTestValueMapAccessorsininternal/soap/soap_test.goagainst missing-key, cross-kind, and unparseable inputs. The package-localgetBool/getYNininternal/account/decode.goare intentionally left in place; they are only used by one decoder and cover boolean/Y-N coercion that is out of scope for #56. Closes #56. -
kasapi-cli accounts getwas renamed tokasapi-cli accounts settings; the old name now wrapsget_accountswith theaccount_loginfilter (see Added), matching themail accounts list|getpattern. Theaccounts listshort description was tightened to clarify that an unfilteredget_accountsreturns every account visible to the login (every sub-account for a main login, just the login itself for a sub-account).
-
internal/usage: add a(t Traffic) IsSummary() boolhelper so callers no longer rely on theDay == 0magic number to distinguish the monthly summary row from per-day entries; the table renderer is switched over too. Document onSpacethatUsedWebspaceis the sum of the four sub-buckets so future readers do not double-count. -
kasapi-cli usage traffic: pre-validate--year(must be in[2000, currentYear+1]) and--month(must be1..12) instead of forwarding obvious typos to KAS. Closes #45.
-
internal/api/doc.go: stop enumerating the auth-failure code list inline; point toIsAuthFailureas the single source of truth so a future code addition only has to update one place. -
internal/auth/source.go: extend theSessionTokenSourcetype-level doc to describe the snapshot/restore semantics applied duringInvalidate, so readers see the full lifecycle without having to drill into the field block. -
internal/api: addtestdata/response_failed_kas_session_invalid.xmlandTestCallRetriesOnSessionInvalidto pin the full Client + retry composition for the new code; complements the IsAuthFailure table-test row.
-
Session re-authentication now triggers on
kas_session_invalid.IsAuthFailurepreviously covered onlyno_auth,unknown_session,kas_access_forbidden, andgot_no_login_data; KAS also returnskas_session_invalidwhen a server-side session is no longer accepted (e.g. it was created withsession_update_lifetime=Nand the lifetime elapsed). Without this code the auto-retry path in*api.Client.Calldid not fire and the user saw the raw fault. -
internal/auth/source.go: preserve the user-configuredLifetime/UpdateLifetimeacrossInvalidate. When a persisted session was loaded its server-side properties are adopted for the duration of that session's life (so Heartbeat stays consistent), but the wired CLI-flag values are now snapshotted on firstCredentialscall and restored byInvalidate. The fresh session created by the next re-authentication therefore reflects the current run's flags rather than the stale persisted properties — fixing the case where an initial run without--session-update-lifetimewould otherwise pin the persisted entry toupdate_lifetime=falseforever. -
internal/auth/source.go: sharpen theHeartbeatdoc comment. The previous wording claimed Heartbeat was a no-op "when no Store is wired up", but the in-memory rolling window is updated regardless ofStore; the comment now describes the actual conditions (UpdateLifetimefalse or no cached token). Closes #41. -
internal/api/client.go: drop the stale "(issue #5)" reference from theStaticTokenSourcedoc comment. Issue #5 was closed by PR #29 when the KasAuth client landed; the surrounding sentence is kept. -
internal/account/table.go: document theused_account_spaceunit conversion. The KAS phpdoc does not state the unit, but the magnitudes and fractional digits in real responses are consistent with KiB (bytes/1024); a one-line code comment records the derivation so future readers do not rediscover it. -
internal/usage: drop the action name fromDecodeSpace/DecodeSpaceUsage/DecodeTrafficerror strings. The Client wrappers already prependusage: get_space:/usage: get_traffic:etc., so leaving the action in the decoder produced a doubled prefix (usage: get_space: usage: get_space: ReturnInfo[0] is not a Map). Decoders now use"usage: …"only, matching the establishedaccount/serverpattern. -
kasapi-cli config init: rename the local--profileflag to--nameso it no longer shadows the persistent root--profileflag. Previouslykasapi-cli --profile X config initsilently reverted to the local defaultmaininstead of writing profileX. The persistent--profileflag continues to select which profile is used at runtime;--nameselects which profile is written. -
Replace the legacy direct
err == io.EOF/err != io.EOFcomparisons ininternal/cli/confirm.goandinternal/auth/codec.gowitherrors.Is(err, io.EOF)so wrapped EOF values are still recognised. -
internal/transport: drop the manualAccept-Encoding: gziprequest header.net/httponly decompresses gzip responses transparently when the caller has not set that header; the manual set turned automatic decoding off and leaked raw gzip bytes into the XML decoder, surfacing asXML syntax error: invalid character entity &…on the first kasserver response that came back compressed. Removing the header letsnet/httpadd it (and decode the response) itself.
kasapi-cli configsubcommand tree for first-run bootstrap and inspection without hand-writing TOML:config initinteractively prompts forlogin,auth_type(session|plain, defaulting tosession), andauth_data(hidden viagolang.org/x/term.ReadPassword), writes the profile to the resolved config path with mode0600(parent dirs created0700, atomic temp+rename), refuses to overwrite an existing profile unless--force, and offers to setdefault_profilewhen none is configured.--profileselects the profile name (defaultmain). Non-TTY stdin fails fast with a clear error so CI and pipes do not hang.config showprints the resolved effective configuration after the flag/env/profile merge withauth_dataredacted viaCredentials.String.config pathprints the resolved config-file path.config.Saveis the new persistence helper that backsconfig init. (Closes #34.)--otp,--session-lifetime, and--session-update-lifetimepersistent flags on the root command, exposing the optional KasAuth parameters (session_2fa,session_lifetime, andsession_update_lifetime). All three are plumbed throughBuildAPIClientintoauth.Optionssoauth.SessionTokenSourceforwards them on the credential-token bootstrap.--session-lifetimeis range-checked client-side (1..30000 seconds);--session-update-lifetimeacceptsYorNand maps to the tri-state*boolfield. The--auth-typehelp text spells out that these flags are KasAuth-only (the KAS docs do not cover them on directkas_auth_type=plaincalls), so combining any of them withauth_type=plainis rejected up front with a user-error exit code and a message that points toauth_type=session.internal/accountandinternal/serverread modules with the first end-to-end CLI subcommands:kasapi-cli accounts list(get_accounts),kasapi-cli accounts get(get_accountsettings),kasapi-cli accounts resources(get_accountresources), andkasapi-cli server info(get_server_information). Domain typesAccount,AccountSettings(with SSH fingerprints, user_prefs, direct-link flags),AccountResources/ResourceQuota, andService/ServiceListdecode the KAS Map/Array payloads into typed Go values;ResourceQuota.Max == -1is rendered as∞in the table view to match the documented "unlimited" sentinel. Mapping tests run against the shippedtestdata/account/get_*_response_success.xmlfixtures.cli.BuildAPIClient(opts)is the new wiring helper that reads config + env + flags, picksapi.StaticTokenSourceforauth_type=plainandauth.SessionTokenSourceforauth_type=session, and returns an*api.Clientthat subcommands consume. (Closes #7.)internal/cliCLI scaffold built on spf13/cobra:NewRootCmd()returns thekasapi-cliroot command with persistent global flags--config,--profile,--login,--auth-data,--auth-type,--output,--no-color,--verbose,--yes, plus the built-in--helpand--version. Output renderers (json,yaml,table) live behind a singleRender(w, format, v)entry point;--output=tablerequires the value to implement theTabularinterface. AConfirm(in, out, prompt)helper covers the[y/N]prompt for future destructive write commands.ExitError,UserError(...),APIError(...), andCodeFor(err)translate failures to the documented exit codes (0ok,1user error,2API error); flag-parsing errors are routed throughUserError. The binary is intentionally without subcommands until #7 —kasapi-cliprints help andkasapi-cli --versionprints the build banner. Adds thegoccy/go-yaml(active fork replacing the archivedgopkg.in/yaml.v3) andspf13/cobradependencies. (Closes #12.)internal/authKasAuth.php credential-token client: separate codec (tns:KasAuthenvelope, barexsd:stringtoken in<return>),Client.GetCredentialToken(ctx)returning the 40-character token,Options{Lifetime, UpdateLifetime, OTP}for the optionalsession_lifetime,session_update_lifetime, and 2FAsession_2faparameters. Faults surface as typed*Errorwith helpersIsLoginFailed,IsLoginLocked,IsOTPPinIncorrect,IsUnknownSession.SessionTokenSourceadapts the client to theapi.TokenSourceinterface, caching the token and re-fetching onInvalidatesoapi.Clientcan refresh transparently after an auth failure. (Closes #5.)internal/apigeneric KasApi.php call surface composing the soap codec with the http transport:Client.Call(ctx, action, params)encodes, posts, decodes, and feeds the server-reportedKasFloodDelayback to the transport gate. SOAP-ENV:Fault bodies surface as typed*Errorvalues whoseCodeis the stable KAS error string, with predicates (IsAuthFailure,IsFloodProtection,IsNotFound,IsSyntaxError,IsMaxReached,IsInProgress,IsMissingParameter,IsNothingToDo). ATokenSourceinterface plusStaticTokenSourceprovide credentials;no_authandunknown_sessiontrigger one token refresh and retry. (Closes #6.)internal/transportHTTP client wrapping the KAS SOAP endpoints: POST with the SOAP 1.1 content type, version-stamped User-Agent, exponential backoff on 5xx and network errors (4xx and SOAP faults are returned without retry), context-aware cancellation, and a per-clientRecordDelay/gate pair so callers can honour the server-sideKasFloodDelay.Now/Sleepare injectable for deterministic tests viahttptest.Server. (Closes #4.)internal/configprofile-aware credentials loader: TOML config under the OS-specific user-config path (XDG on Linux), multi-profile, with resolution precedence flag > env > profile > default profile. Env fallback viaKAS_LOGIN,KAS_AUTHDATA,KAS_AUTHTYPE. Auth-data is redacted byCredentials.Stringso secrets do not surface in logs or--help. Validatesauth_type(plainorsession) and reports missing required fields. (Closes #2.)internal/soapcodec for the KAS-API envelope:Valuediscriminated union mirroring the Apache xml-soapns2:Mapshape (xsi:type: string/int/float/boolean, ns2:Map, SOAP-ENC:Array),DecodeforKasApiResponse/SOAP-ENV:Faultenvelopes returning*Responseor*FaultError, andEncodeRequestfor the JSON-in-<Params>request envelope. Table-driven tests cover 471 response fixtures plus shape pins and encoder validation. (testdata/session/is left for the KasAuth client in issue #5.)- Bootstrap Go module
github.com/chmmou/kasapi-cli(Go 1.23). cmd/kasapi-clientry point with build-stamped--version.internal/package skeleton mirroring the clean-architecture layering indocs/go/ARCHITECTURE.md: per-resource domain packages (account,server,domain,subdomain,dns,mailaccount,mailforward,mailfilter,mailinglist,database,ftpuser,sambauser,cronjob,ddns,directoryprotection,softwareinstall,ssl,usage,chown,symlink,session) plus inner-/adapter-layer packages (soap,transport,auth,api,config,cli,version)..golangci.ymlmatching the gate set indocs/go/LINTING.md.- GitHub Actions CI workflow running
gofmt,go vet,golangci-lint,go test,go test -race, andgo build ./cmd/kasapi-cli.