Skip to content

Latest commit

 

History

History
1633 lines (1508 loc) · 93 KB

File metadata and controls

1633 lines (1508 loc) · 93 KB

Changelog

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.

[Unreleased]

Added

  • 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> wire add_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). update and delete are gated by the #109 confirmation prompt — update replaces the access password (the previous one is unrecoverable) and delete revokes access, so both can lock users out; add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; directory_password is redacted in both sinks. There is no _new_password split — update sends the replacement under the same directory_password key add uses — and update sends only the explicitly-changed --password/--authname (keyed on cobra Changed), so an omitted password keeps the current one. KAS also accepts parallel directory_user/directory_password arrays to create several protected users in one call (hence the directory_user_count_neq_passcount fault); 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> wire add_mailstandardfilter / delete_mailstandardfilter. Both are gated by the #109 confirmation prompt: the KAS API has no update_mailstandardfilter action, so add replaces the configured filter chain wholesale (items previously set but missing from the new --filter list are dropped), which is destructive to recover from without a stored copy. Both honour --dry-run (#132) and emit a #131 audit record. Repeatable --filter items are joined with ; on the wire (the format the captured add_mailstandardfilter request 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. delete takes 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_mailstandardfilter sometimes surfaces an envelope-level SOAP fault (an internal sizeof() PHP error) even when the chain was in fact removed on the server; the fault is surfaced verbatim, and docs/usage/destructive-writes.md documents the verification path (mail accounts get <login>mail_spamfilter).

  • A new shared envelope-level fault fixture testdata/response_failed_internal_server_error.xml captures the generic PHP sizeof() runtime error wrapped in a SOAP-ENV:Server fault. It is exercised by the soap fixture walker and by mailfilter.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> wire add_mailaccount / update_mailaccount / delete_mailaccount. update and delete are gated by the #109 confirmation prompt; add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; the password is redacted in both. add splits the address on the last @ into the local_part / domain_part KAS expects and takes no mail_login — KAS generates the login (e.g. m0000001) and echoes it in ReturnInfo, which the command prints. The Y/N/text toggles and XLIST folder names default to the KAS API's own defaults, so a bare add <address> --password <pw> is a complete create. update sends only the explicitly-set flags (keyed on cobra Changed), adds the --active (is_active) toggle, and its --password maps to mail_new_password (the _new_password split the database/ftpuser/sambauser slices carry) rather than the add-only mail_password. responder is 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 as delete_database).

  • database.InProgressFalse / database.InProgressTrue package 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> wire add_database / update_database / delete_database. update and delete are gated by the #109 confirmation prompt; add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; the password is redacted in both. update sends only the explicitly-set flags (keyed on cobra Changed), so an empty value is a deliberate "clear". add_database takes no database_login — KAS generates it (the login equals the database name on creation, e.g. d0123460) and echoes it in ReturnInfo, which the command prints. The password key is split between actions: --password maps to database_password on add and to database_new_password on update (the same _new_password split 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.

Changed

  • testdata/cronjob/{add_cronjob_response_success,add_cronjob_response_success_warning,update_cronjob_response_success}.xml carry a top-of-file XML comment documenting that KAS itself echoes the notification address under mail_address (double d) in the KasRequestParams echo block, while the documented request key is mail_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.Call now prefixes the module label onto the wrapped ErrUnexpectedReturnString message, so a regression in e.g. mailforward.Client.Add reads kaswrite: 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 --target help no longer claims to "replace the full target list" (which is true for update, not for add); the add-side help now reads "repeatable; at least one required".
    • kasapi-cli mail lists update --active now takes an explicit Y|N argument (string flag), matching the mail accounts update --active surface and removing the slightly non-obvious --active=false form that previously meant "deactivate".
    • runWriteE resolves the credentials exactly once per write invocation: the post-gate dispatch now reuses the creds runWriteE already resolved for the audit login, via the new unexported buildAPIClientFromCreds helper. Previously the config + env + flag resolution ran twice — once for the audit, once inside BuildAPIClient — for every real write call. BuildAPIClient (the read-side seam) is unchanged.
  • Cross-module convention alignment, post-#122-review parity sweep:

    • account.AccountList / mailaccount.MailAccountList now render the used_*_space column with a " MB" suffix as part of the value (matching the singular detail row) and use a bare USED header. Consistent with the database slice after #122 followups — all three modules now share a single unit-rendering convention.
    • ddns.DDNSUser.in_progress is no longer marked omitempty, aligning with the majority of read modules. Captured fixtures have always carried in_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.Add and mailforward.Client.Add now 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.md refactored: 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-hosts wildcard, ddns's no-_new_password split, …). Replaces eight near-identical paragraphs that had grown to roughly copy-paste.

  • kasapi-cli databases add and … update now 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 regenerated docs/cli/kasapi-cli_databases_*.md pages now stop claiming "required for add" on the update help output.

  • kasapi-cli databases list renders the used_database_space column with a " MB" suffix as part of the value (matching how the singular detail view already rendered it) and drops the USED_MB header in favour of a bare USED header. List and singular views now share a single unit-rendering convention.

  • The database package's Database.in_progress JSON/YAML field is no longer marked omitempty, aligning with the majority of read modules (mailaccount, mailinglist, sambauser, ftpuser, account). The KAS API has returned in_progress on 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. AllowedHosts is 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-hosts flag is therefore optional and the empty-string wildcard is sent verbatim on the wire.

  • (cli.ConfirmAction).Summary is now exported (was summary), so tests can pin the rendered prompt (and the per-slice loudness verb) without instantiating a real terminal.

  • kasapi-cli ddnsusers add and … update now bind disjoint flag sets — add carries --zone / --label / --target-ip, update carries --target-ipv4 / --target-ipv6 instead — so each subcommand's --help reflects 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.xml to the values the matching add_ddnsuser_response_success.xml request-echo block carries (dyndns_target_ip=127.0.0.1, dyndns_dual_stack=N), and updated the ddns write-test sampleSpec to 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 private call methods in mailforward / mailinglist / session into one internal/kaswrite seam, the write-side counterpart of internal/kasread. There is now one canonical errors.Is sentinel (kaswrite.ErrUnexpectedReturnString); the per-module ErrUnexpectedReturnString are re-export aliases of it, so errors.Is(err, <module>.ErrUnexpectedReturnString) keeps working. No behaviour change; the wrapped error message is now uniform (kaswrite: …).

  • kasapi-cli databases list/databases get now decode the in_progress flag the KAS API surfaces on every get_databases entry. The list view exposes a new IN_PROGRESS column, and the singular detail view appends an in_progress row when present. Read-test fixtures were captured against an account with a newly- created database d0123460; the existing decode/tabular tests now pin against that snapshot rather than the previous d0123450-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> wire add_ddnsuser / update_ddnsuser / delete_ddnsuser. update and delete are gated by the #109 confirmation prompt (update_ddnsuser replaces every supplied field wholesale); add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; the password is redacted in both. update sends only the explicitly-set flags (keyed on cobra Changed), so an empty value is a deliberate "clear". add_ddnsuser takes no dyndns_login — KAS generates it and echoes it in ReturnInfo, which the command prints. Unlike the ftpuser/sambauser slices there is no _new_password split: the password maps to dyndns_password on both actions, per the fixtures. update_ddnsuser accepts --target-ipv4 / --target-ipv6 instead of add's legacy --target-ip; the dual-stack dyndns_target_ipv4 / dyndns_target_ipv6 request 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 captured update_ddnsuser request 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 the ddns* form without the y.

  • 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> wire add_sambauser / update_sambauser / delete_sambauser. update and delete are gated by the #109 confirmation prompt (update_sambauser replaces every supplied field wholesale); add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; the password is redacted in both. update sends only the explicitly-set flags (keyed on cobra Changed), so an empty value is a deliberate "clear". add_sambauser takes no samba_login — KAS generates it and echoes it in ReturnInfo, which the command prints. The KAS documentation wrongly lists the create password parameter as samba_new_password; the captured add_sambauser request fixture (and its success- response request echo) confirm the real key is samba_password, so the fixture — the authoritative request-shape contract — was corrected and the code follows it. update_sambauser does use samba_new_password; the CLI maps the single --password flag 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> wire add_ftpuser / update_ftpuser / delete_ftpuser. update and delete are gated by the #109 confirmation prompt (update_ftpuser replaces every supplied field wholesale); add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; the password is redacted in both. update sends only the explicitly-set flags (keyed on cobra Changed), so an empty value is a deliberate "clear". add_ftpuser takes no ftp_login — KAS generates it and echoes it in ReturnInfo, which the command prints. The password key differs by action: add_ftpuser uses ftp_password, update_ftpuser uses ftp_new_password (per the KAS docs and the captured request fixtures); the CLI maps the single --password flag to the correct key per command. The plural-vs- singular question from #13 is resolved: the live KAS action is add_ftpuser (singular) — the documentation's add_ftpusers is 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> wire add_cronjob / update_cronjob / delete_cronjob. update and delete are gated by the #109 confirmation prompt (update_cronjob replaces every supplied field wholesale); add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; --http-password is redacted in both. update sends only the explicitly-set flags (keyed on cobra Changed), so an empty value is a deliberate "clear". The KAS wire key for the notification address is mail_adress (single 'd', the same quirk the read mapping documents). The add/update/delete cronjob 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> wire add_mailinglist / update_mailinglist / delete_mailinglist. update and delete are gated by the #109 confirmation prompt (update_mailinglist replaces the subscriber / restrict-post / config fields wholesale); add is reversible and not prompted. All three honour --dry-run (#132) and emit a #131 audit record; the list password is redacted in both. update sends only the explicitly-set flags (keyed on cobra Changed), so an empty value is a deliberate "clear". The get_mailinglists read 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> wire add_mailforward / update_mailforward / delete_mailforward. delete and update are gated by the #109 confirmation prompt (update_mailforward replaces the full target list and is irreversible); add is 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 shared cli.runWriteE runner and the new non-gated cli.ResolveWrite counterpart of cli.ResolveDestructive.

  • --dry-run for destructive commands (#132, v0.2.0 write-phase prerequisite): the new global --dry-run flag previews the KAS request a destructive command would send (action + redacted parameter map, via --output table/json/yaml) and exits 0 without dispatching. It short-circuits the #109 confirmation prompt — even combined with --yes it only previews — and still emits a #131 audit record with outcome=dry-run so the trace exists. The shared cli.ResolveDestructive seam composes the dry-run preview, the confirmation gate and the audit log so every future write command consults one entry point. Documented in docs/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.WriteAudit emit one logfmt-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 (and KAS_AUDIT_LOG; the flag wins) additionally appends each record as JSON Lines to a 0600 file. Secret parameters (auth_data, *password, *token, *secret, …) are redacted in both sinks via cli.RedactParams. Documented in docs/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 / -y flag is now wired and advertised again. New cli.GateDestructive enforces an explicit [y/N] prompt before any destructive SOAP call — declining or a non-interactive stdin without --yes aborts with exit code 1 (exported sentinels cli.ErrConfirmationDeclined / cli.ErrConfirmationRequired, errors.Is-able). --yes bypasses the prompt for automation. TTY detection is now shared by config init/add-profile and the gate. The safety contract is documented in docs/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.Delete now wraps it with %w when delete_session returns without a SOAP fault but ReturnString is not "TRUE", so callers can errors.Is the contract violation apart from a transport or KAS-fault error instead of string-matching the message.

  • kasapi-cli sessions delete invalidates the resolved profile's cached session token both server-side (KAS API delete_session) and in the local sessions.toml cache. 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_session is not a separate endpoint: it is the KasAuth credential-token flow already covered by internal/auth, so it gets no subcommand. The shared delete_session use case now lives in internal/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-profiles for managing multiple profiles in config.toml. add-profile reuses the config init prompt flow with --force to overwrite an existing profile; use-profile flips default_profile and, if the outgoing profile had a non-expired cached session token, invokes the KAS API delete_session action on it before removing the local entry from sessions.toml. The server-side revoke is best-effort — a transport or unknown_session fault is logged under --verbose but does not abort the switch, because the local cache is the authoritative client-side state. list-profiles prints one line per profile, marks the default with * , and never writes auth_data. Closes #39.

  • The global --no-color and --yes flags are now hidden from --help and 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.DecodeTraffic now decodes the mandatory year and month fields with soap.Value.MapIntStrict, so a missing or non-numeric value yields a decode error instead of a silent 0 that would mislabel the reporting period. Scope is deliberately limited to those two always-present fields: day stays lenient (the monthly summary row legitimately omits it) and the http_/ftp_ traffic and hit counters stay lenient (KAS returns xsi:nil for 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 lenient AsInt / MapInt / MapInt64 accessors — a missing key, empty/xsi:nil, or non-numeric value yields an error instead of being silently coerced to 0; MapIntStrict additionally rejects a value that would overflow the platform int (32-bit targets) rather than truncating it silently. Adopted in the account resource-quota decoder (decodeQuota): a present quota Map whose mandatory max/reserved/created/used/ free integers are malformed now fails DecodeAccountResources rather than misreporting a 0 limit. 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 to testdata/usage/ so the fixture subdirectory matches its module (internal/usage, CLI usage) per the one-subdir-per-module convention; TestServerCmdHelpListsInfo moved out of account_test.go into its own internal/cli/server_test.go; internal/cli/gendocs_test.go added to cover the gen-docs helper; internal/{ssl,chown,symlink} doc comments now state explicitly that the packages are not-yet-implemented placeholders (issue #13); a comment on api.CodeUnknownAction records 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 from BuildAPIClient now appends (run `kasapi-cli config init` to create a profile interactively) when no config file exists at all, so a fresh user who runs kasapi-cli accounts list without 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 because config init refuses to overwrite without --force in that scenario.

  • internal/session/store.go + auth/source.go + api/client.go: thread the call's context.Context through session.Store.Load / Save / Delete and through api.Heartbeater.Heartbeat. The lock wait now uses flock.TryLockContext so a user Ctrl-C while another kasapi-cli process holds the sessions-file lock aborts cleanly instead of blocking forever; the synchronous toml/os calls are local-FS only so a ctx.Err() check at the boundary is sufficient. SessionTokenSource.Invalidate deliberately uses context.Background for 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 the slog.New(slog.NewTextHandler(io.Discard, nil)) discard-logger pattern. Each of the three packages now has a single package-level var discardLogger built once at init; previously the same expression was inline-duplicated across constructors and logger() helpers (and transport had a discardLogger() function that re-allocated per call). NewSessionTokenSource now seeds Logger to that discard logger in its constructor, matching the pattern already used by auth.New, api.New, and transport.New.

  • internal/cli/output.go + cli/root.go: formatNames is now a package-level var (built once at init) instead of a function that re-built the []string on every call. ParseFormat's error message and joinFormats consume it as a value; both call sites are read-only, so sharing the backing array is safe.

  • internal/cli/output.go: tightened the ErrTableNotSupported message 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 of Kind's underlying uint8, giving the iota block room to grow without collision).

  • internal/cli/wire.go: documented why the auth.New(... soap.AuthPlain, authOpts) call inside the auth_type=session branch hardcodes AuthPlain — 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 with AuthSession.

  • internal/transport/client.go: documented why waitGate runs once before the retry loop, not inside it. 5xx-driven retries do not decode envelopes, so no fresh KasFloodDelay can be recorded between attempts — re-checking the gate would always be a no-op.

  • internal/auth/source.go: SessionTokenSource now exposes a Logger field; the three previously silent s.Store.Save / s.Store.Delete call sites now log a Warn event when persistence fails so disk-full or permission issues surface in --verbose output. 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 --verbose into the new field via internal/cli/wire.go.

  • internal/cli/output.go + internal/cli/root.go: deduplicated the AllFormats → []string conversion. A new package-internal formatNames() is now the single source of truth used by both the --output flag help (joined with |) and the ParseFormat error message (joined with , ).

  • internal/soap/value.go: replaced the magic Kind(255) sentinel returned by classifyType for unknown xsi:type values with a named KindUnknown = 255 constant. Documented why the constant lives outside the iota block (so future enum additions cannot collide).

  • internal/cli/output.go: clarified the ErrTableNotSupported message. Every subcommand result type is expected to implement Tabular, 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=yaml workaround hint.

  • internal/soap/envelope.go + internal/auth/codec.go: wrap the SOAP decoder's input reader in io.LimitReader(r, soap.MaxResponseBytes) (16 MB) and set dec.Strict = true explicitly. 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: rename sessionOpts.any() to isSet() to stop shadowing the Go 1.18 builtin alias any in IDE auto-complete and code review. Behaviour and the single call site are unchanged.

  • internal/session/store.go: document why the explicit tmp.Chmod(0o600) after os.CreateTemp is 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 for Client.Settings, Client.Resources, plus the previously-unexercised TableHeaders on AccountResources and AccountSettings (and the TableRows of AccountSettings). Lifts package coverage from 69.9% to 85.4%.

  • test: internal/cli — added help-output tests for the dns, domains, subdomains, tlds, and mail (incl. accounts / forwards / filters / lists subgroups) 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 the IsLoginFailed, IsLoginLocked, IsUnknownSession, IsOTPPinIncorrect, IsCode, and AsError sentinel helpers.

Fixed

  • Whole-codebase re-review follow-up (fourth pass, Med + Low findings):

    • The mail lists update --config-file / --subscriber blobs 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.md already 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>, and help <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 action get_accounts), was renamed to get_accounts_response_success_single.xml.
    • config show / config path / config list-profiles and sessions delete now document in their --help that their output is plain text and the global --output format flag has no effect there, instead of silently ignoring it.
    • The mail accounts update and ddnsusers update --help texts now describe every field flag as a replacement value (the convention the cronjob/ftpuser/sambauser/database update commands already follow), instead of reusing add's wording on all but the password flag.
    • The white-box runWriteE created_id test no longer couples to host state — it pins KAS_AUDIT_LOG and points --config at a nonexistent temp path so it cannot read the developer's real config or append to a real audit log.
  • 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.Finalize now 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 completion command 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 as created_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 aborted instead 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.Error a 200-wrapped fault produces.
    • sambausers update binds its own replacement-flag set instead of sharing add's (whose --help texts 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>.xml across testdata/{cronjob,ddns,domain,ftpuser,mailaccount,mailforward,mailinglist,sambauser}/ (e.g. get_ftpuser_response_success.xmlget_ftpusers_response_success_single.xml), and the convention example in CLAUDE.md now uses a conforming name.
  • 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 Args validator via cli.MarkArgErrorsAsUserErrors and the root command classifies unknown subcommands itself.
    • transport.Client.Do no 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.AssertFaultFixtures fails on want entries that match no fixture file on disk, so a renamed or deleted fixture cannot leave a dead pin behind; the shared top-level response_failed_*.xml set is now anchored by a dedicated test in internal/api pinning each fixture to its api.Code* constant.
    • config.Resolve with a nil config and --profile wraps ErrUnknownProfile instead of returning a string-only error.
    • session.Store.Load treats an entry without expires_at (only producible by hand-editing sessions.toml) as expired instead of never-expiring.
    • cronjobs update / ftpusers update bind their own flag sets instead of sharing the add flags, so --help no longer advertises add defaults (https, *, default) or "required for add" texts — matching the database/mailaccount/ddnsuser/mailinglist split.
    • The --dry-run help text says "write command" instead of "destructive command" — the flag covers non-gated add writes too.
    • testdata/domain/get_topleveldomains_request.xml carried kas_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) and cronjob/add_cronjob_response_success_warning.xml (a success variant, not a distinct status).
    • internal/{ftpuser,cronjob,mailaccount}/doc.go no longer name non-existent singular get actions; the stale ftpuser "verify against the live API" note now records the #119 verification result. The internal/ddns in_progress comment 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 delete verb, stderr note), the audit-trace scope (the sessions delete / config use-profile session logout is explicitly outside the pipeline), ROADMAP.md (mail lists get, <ddns-login> placeholder), and the CLAUDE.md repository-state paragraph.
  • Whole-codebase review follow-up (Low findings):

    • api.TokenSource.Invalidate now reports whether the next Credentials call can produce fresh credentials; api.Client skips the auth-failure retry for a StaticTokenSource instead of doubling the failing request with identical credentials.
    • transport.Client no longer classifies context.Canceled / DeadlineExceeded as 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 in docs/usage/destructive-writes.md.
    • auth.DecodeResponse validates the credential token shape (40 alphanumeric characters) before it is cached and persisted; the error reports only the length, never the content.
    • auth.EncodeRequest range-checks Lifetime against the documented 1..30000 session_lifetime bound (0 = server default).
    • New config.ErrUnknownProfile / config.ErrMissingCredentials sentinels; Resolve failures are errors.Is-able instead of string-matchable only.
    • dns / directoryprotection / server / usage now keep their Caller unexported (c), matching the other ten modules; the exported API field was construction surface no caller used.
    • cronjob.FieldID replaces three hardcoded "cronjob_id" literals.
    • internal/{softwareinstall,directoryprotection,database}/doc.go no 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-line testutil.AssertFaultFixtures tests instead of sitting unreferenced.
    • Fixture values info@example1.org and /example-new.com/ replaced with RFC 2606-reserved names (example.org, example.net).
  • Whole-codebase review follow-up (Med findings):

    • transport.Client now enforces the 16-MB soap.MaxResponseBytes cap 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=yaml emit a {"message": ...} object scripts can parse while the default table output stays the bare line.
    • cli.RedactParams elides multi-line or oversized parameter values (<elided N bytes>). The update_mailinglist config / subscriber blobs previously reached the stderr logfmt line, the --audit-log JSON sink, and the --dry-run preview verbatim — and the list config can carry the list password in cleartext.
    • SessionTokenSource.Heartbeat persists via the new session.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.go fault-map key add_mailinglist_response_failed_mailinglist_mailinglist_domain_doesnt_exist.xml had a doubled prefix and matched no fixture, so the mailinglist_domain_doesnt_exist code pin silently never ran.
    • get_server_information fixtures moved from testdata/account/ to their own testdata/server/ (one-subdir-per-module convention), and the server and usage modules gained the missing fault-fixture leg (*_response_failed_no_auth.xml, the action-independent captured auth fault) plus testutil.AssertFaultFixtures coverage.
    • ROADMAP.md listed server get; the shipped command is server 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 list now exposes the documented optional record_id filter instead of a non-existent nameserver parameter: the flag is renamed --nameserver--record-id and the wire parameter nameserverrecord_id, matching the KAS get_dns_settings contract (zone_host required, record_id optional) and the captured request fixtures. nameserver is a real KAS key, but for reset_dns_settings, not get_dns_settings; the read slice had carried it over by mistake.

  • Audit redaction now also catches the German "passwort" spelling. redactParam matched only the English password/passwd substrings, so real KAS keys such as ftp_passwort / db_passwort would have reached an audit / --dry-run record 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.AssertFaultFixtures anchor asserts every captured testdata/<module>/*_response_failed_*.xml decodes to a *soap.FaultError with 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 only mailforward / mailinglist had this; the read modules' ~370 fault fixtures were unreferenced. internal/auth/doc.go now states its adapter layer so its legitimate internal/transport dependency is not misread as a layering violation. (Full-project review follow-up.)

  • kasapi-cli mail help no longer under-describes its subtree: the parent Short said only "Inspect …" while forwards and lists now also add/update/delete. It now reads "Inspect mail accounts and filters; inspect and manage forwards and mailing lists" (regenerated in docs/cli/). Stale NewMailCmd comment and the update_mailinglist field-constant doc comment corrected; the destructive-writes doc now matches the gated update prompt wording. Added a mail lists update --dry-run regression test pinning the cobra-Changed field assembly (--active→Y/N, omitted-not-sent, repeated --subscriber newline-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 / \r inside a quoted value in the stderr logfmt line, so a logfmt consumer can parse each record atomically. The JSON-Lines --audit-log sink was already correct. (#117 re-review follow-up.)

  • get_mailinglists response mapping corrected to the real KAS schema. The mailinglist.MailingList model previously carried mailinglist_admin / mailinglist_url, which the API does not return; it now maps mailinglist_name, mailinglist_domain, mailinglist_password (surfaced via --output=json|yaml only, never in table output — mailaccount precedent), mailinglist_is_active and in_progress, plus the singular-view-only mailinglist_subscriber / mailinglist_config / mailinglist_restrict_post. Table columns for the list view are now NAME 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 local sessions.toml entry 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 the gen-docs filesystem 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.RecordDelay now extends the gate to now+d only when that is later than an already-pending deadline, instead of unconditionally overwriting it. Previously a shorter KasFloodDelay arriving 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.Get accessor 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's Get. 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 preserving errors.Is(err, context.Canceled). The internal/ddns field-availability comment was corrected to match the captured fixtures (both the list and singular variants return dyndns_target_ipv4 / ipv6).

  • Truthful CLI output and exit-code classification (review follow-up): kasapi-cli sessions delete and config use-profile no longer claim the local session cache was cleared (or that the server-side session was invalidated) when the underlying store.Delete / delete_session call actually failed — the message now reflects what really happened, and sessions delete returns a non-zero exit when the local cache removal fails instead of swallowing it. gen-docs local filesystem failures (mkdir / write) now map to the user-error exit code (1) via UserError instead of falling through as the API-fault code (2). No behaviour change to the success paths.

  • internal/session/store.go: serialise Load / Save / Delete through an advisory file lock (github.com/gofrs/flock) at <sessions.toml>.lock. Previously two kasapi-cli processes 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 is Decode<Thing> (e.g. DecodeAccounts), not Map<Action>Response; the client accessor lives on the module's own *Client in the module package (dispatching via a per-package Caller interface), not in internal/api/client.go. The transport- level KasRequestParams envelope is filled centrally in internal/soap/request.go; module code passes plain map[string]any to Caller.Call. CONTRIBUTING.md:57-58 was already correct on both points; the skill drifted.

  • ROADMAP.md: added a new CLI write safety section 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-run for 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-existent update_mailstandardfilter to the actual KAS actions add_mailstandardfilter and delete_mailstandardfilter (the captured fixtures and issue #116 are authoritative; the KAS API has no update_mailstandardfilter).

  • CONTRIBUTING.md: roadmap links no longer detour through README.md#roadmap (which is a one-line pointer with no checklist) — they now point directly at ROADMAP.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). The kasapi-cli-vertical-slice skill is now listed under authoritative references alongside kasapi-cli-git-workflow and kasapi-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 (table is the default; the available formats are json / yaml / table, not just JSON).

  • docs/usage/mail.md: corrected the description of mail filters list. The previous text claimed get_mailstandardfilter returns "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 the mail_spamfilter setting on a mailaccount or forward.

[0.1.0-alpha.1] - 2026-05-10

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.

Fixed

  • .github/workflows/release.yml cosign pin: sigstore/cosign-installer@v3 was pinned to v2.4.1, which cannot read the bundle format that newer goreleaser-action@v7 releases ship. The release workflow failed during goreleaser-action's self-verification with bundle 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.yaml archives.files glob: docs/cli/**/* and docs/usage/**/* matched nothing (both directories are flat) so the rendered docs were missing from the release archives. Switched to docs/cli/* / docs/usage/* so the docs land alongside LICENSE, README.md, and CHANGELOG.md in every tarball/zip.
  • usage traffic --year / --month range-validation errors now exit with code 1 (user error) instead of code 2 (API error). The PreRunE previously returned a plain fmt.Errorf which fell through cli.CodeFor's default branch.
  • dns list --domain "" now reports required flag --domain not provided without the redundant --domain: prefix and exits with code 1; the validation moved into a dedicated PreRunE so the body can use the canonical runListE shape.

Changed

  • account.Client.List now returns account.AccountList directly, matching every other read module. The CLI no longer needs to convert []Account to the named list type, and the kasread.ListGet field is parameterised on AccountList.

  • Collapsed the four remaining stand-alone Caller interface declarations in internal/directoryprotection, internal/dns, internal/server, and internal/usage to type 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 the Caller contract is a single-file diff in internal/kasread.

Documentation

  • Closed the two remaining follow-ups from issue #73's NTH bundle: verified against the KAS docs that get_mailstandardfilter accepts no filter parameter (the mailfilter.Client doc-comment now links the spec page authoritatively, no code change needed), and added TestClientGetNotFound for ddns.Client.Get to pin the empty-array fallback that the prior test suite did not exercise.

Changed

  • Unified the get-subcommand Use: 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 shared internal/tablefmt package's FieldValueHeaders variable. Each module's TableHeaders() 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.RunE bodies in 14 CLI files (account, cronjobs, databases, ddnsusers, directoryprotection, domains, ftpusers, mail, sambausers, server, softwareinstalls, subdomains, tlds, usage) with two generic factories runListE[T] / runGetE[T] in internal/cli/run.go. Each subcommand now passes a one-line closure that owns its module client construction and the actual call; the factory handles BuildAPIClient, APIError(action) wrapping, and Render. 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.Get boilerplate in 12 read modules (account, cronjob, database, ddns, domain, ftpuser, mailaccount, mailforward, mailinglist, sambauser, softwareinstall, subdomain) plus the List-only mailfilter with a single generic helper kasread.ListGet[L, E]. Each module now binds the action, label, filter key and decoder once in NewClient and exposes List / Get as one-line delegates; per-module Caller interfaces collapse to type aliases over kasread.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 Array of Map decoder boilerplate in 19 read decoders with a single generic helper soap.DecodeArray[T]. Each module's Decode<Foo>s now 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.

Added

  • Vulnerability and security scanning, stage 2 of two: GitHub-native CodeQL workflow (.github/workflows/codeql.yml) running on PR + push to main + weekly cron, with the security-extended query pack; OSSF Scorecard workflow (.github/workflows/scorecard.yml) running weekly + on push to main, publishing the score for the public dashboard at https://securityscorecards.dev; SECURITY.md at the repo root with the disclosure policy, response expectations, and verification recipe (the short hint in CONTRIBUTING.md now links here); SBOMs (SPDX-JSON) generated per release artefact via goreleaser's sboms: block — Syft is now load-bearing in release.yml, so the previous continue-on-error: true was removed.

  • Vulnerability and security scanning, stage 1 of two: a new govulncheck CI job (official Go vulnerability scanner, call-graph aware) on every PR + push to main; gosec added to the golangci-lint linter set in .golangci.yml; Dependabot configured for gomod and github-actions ecosystems via .github/dependabot.yml. Existing call sites that triggered gosec false-positives (test fixture loaders, os.Stdin.Fd() conversion, public-docs MkdirAll mode) were annotated with targeted //nolint:gosec markers carrying the rule ID and a one-line reason. CodeQL, OSSF Scorecard, SECURITY.md, and SBOM-in-release land in stage 2.

Changed

  • Go toolchain bumped from 1.23 to 1.25 (go.mod directive plus go-version: "1.25" in ci.yml and release.yml). Required to clear all reachable Go-stdlib CVEs that the new govulncheck job 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 by git tag v*. Builds Linux + Windows × amd64/arm64 binaries with internal/version ldflags wired up, packages Linux artefacts as deb and rpm via nfpm, ships tarballs (tar.gz) / ZIPs alongside, and signs every artefact plus SHA256SUMS keylessly with cosign via GitHub OIDC. A new CI job goreleaser config check validates .goreleaser.yaml on every PR. README gains an Install section pointing at the Releases page with a cosign verify-blob recipe. Makefile gets release-snapshot (local dry run into ./dist, skips signing) and release-check targets. AUR PKGBUILD and a Homebrew tap are tracked separately as a follow-up.

  • CI job docs sync (.github/workflows/ci.yml) that runs make docs and fails when the checked-in docs/cli/ differs from the regenerated output. Ensures any change to a flag, subcommand registration, or short/long description comes paired with a docs/cli/ refresh.

  • Per-resource usage docs under docs/usage/ (eight pages — accounts, server, domains, dns, mail, databases, usage, hosting — plus an index README.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 hidden kasapi-cli gen-docs <out-dir> subcommand wrapping cobra/doc.GenMarkdownTree. The root command's DisableAutoGenTag is flipped before generation so re-running the generator produces byte-identical output when the CLI surface has not changed.

  • Top-level Makefile with a docs target (make docs) that wipes docs/cli/ and regenerates it via go run ./cmd/kasapi-cli gen-docs docs/cli. Other targets (build, test, lint, vet, fmt, clean) wrap the standard Go loop documented in CONTRIBUTING.md. Closes #35.

Added

  • internal/ddns read module and kasapi-cli ddnsusers list|get subcommand tree wrapping get_ddnsusers. The list variant decodes the Array of Maps into a typed DDNSUserList; get <dyndns-login> reuses the same endpoint with a ddns_login filter (note: the filter parameter has no y, unlike the response keys which use the dyndns_* prefix; per the KAS docs at get-ddnsusers-inc.html) and unwraps the single-entry result. The list view joins dyndns_label and dyndns_zone into a single FQDN column so the table reflects the hostname clients will actually look up; the explicit dyndns_target_ipv4 / dyndns_target_ipv6 fields surface as separate K/V rows in the singular view when the API populated them. dyndns_password is omitted from both table views (still available via --output=json|yaml). The KAS API signals "filter matched no entry" with a dyndns_login_not_found SOAP fault rather than an empty array; that fault propagates as an *api.Error and is detected by api.IsNotFound. Mapping tests run against testdata/ddns/get_ddnsusers_response_success.xml and get_ddnsuser_response_success.xml. Refs #11.

  • internal/softwareinstall read module and kasapi-cli softwareinstalls list|get subcommand tree wrapping get_softwareinstall (note: the KAS action name is singular for both variants). The list variant decodes the Array of Maps into a typed SoftwareInstallList; get <software-id> reuses the same endpoint with a software_id filter 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 the 0.0 "not applicable" sentinel as . The base64 image data URI is kept on the struct for JSON/YAML round-trip fidelity but stripped from both table views. Mapping tests run against testdata/softwareinstall/get_softwareinstalls_response_success.xml (22 entries) and get_softwareinstall_response_success.xml. Refs #11.

  • internal/directoryprotection read module and kasapi-cli directoryprotection list [--path PATH] subcommand wrapping get_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 --path filter rather than the usual list+get pair (matching the dns list --domain shape). directory_password is omitted from the table view but remains available via --output=json|yaml. Mapping tests run against testdata/directoryprotection/get_directoryprotections_response_success.xml and get_directoryprotection_response_success.xml. Refs #11.

  • internal/cronjob read module and kasapi-cli cronjobs list|get subcommand tree wrapping get_cronjobs. The list variant decodes the Array of Maps into a typed CronjobList; get <cronjob-id> reuses the same endpoint with a cronjob_id filter 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)-style SCHEDULE column and renders the trigger target as either protocol://http_url or shell_command; the singular view keeps the raw fields plus the joined schedule. xsi:nil values for shell_command / timeout round-trip cleanly to zero values flagged with omitempty. Mapping tests run against testdata/cronjob/get_cronjobs_response_success.xml and get_cronjob_response_success.xml. Refs #11.

  • internal/sambauser read module and kasapi-cli sambausers list|get subcommand tree wrapping get_sambausers. The list variant decodes the Array of Maps into a typed SambaUserList; get <samba-login> reuses the same endpoint with a samba_login filter (per the KAS API docs at get-sambausers-inc.html) and unwraps the single-entry result, matching the mail accounts / accounts / databases pattern. The list view shows login, path, comment, and in_progress; the singular view falls back to a key/value table and omits samba_password (still available via --output=json|yaml). Mapping tests run against testdata/sambauser/get_sambausers_response_success.xml and get_sambauser_response_success.xml. Refs #11.

  • internal/ftpuser read module and kasapi-cli ftpusers list|get subcommand tree wrapping get_ftpusers. The list variant decodes the Array of Maps into a typed FTPUserList; get <ftp-login> reuses the same endpoint with an ftp_login filter 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, and in_progress; the singular view falls back to a key/value table and omits ftp_password / ftp_passwort (still available via --output=json|yaml). Mapping tests run against testdata/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/database read module and kasapi-cli databases list|get subcommand tree wrapping get_databases. The list variant decodes the Array of Maps into a typed DatabaseList; get <database-login> reuses the same endpoint with a database_login filter and unwraps the single-entry result, mirroring the mail accounts / accounts pattern. The list view reports used_database_space in MB; the singular view uses a key/value table and omits database_password (still available via --output=json|yaml). Mapping tests run against testdata/database/get_databases_response_success.xml and get_database_response_success.xml. Refs #11.

  • account.Client.Get(ctx, login) and kasapi-cli accounts get <account-login> calling get_accounts with an account_login filter. 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 against testdata/account/get_account_response_success.xml.

  • internal/mailinglist read module and kasapi-cli mail lists list|get subcommand tree wrapping get_mailinglists. The list variant decodes the Array of {mailinglist_name, mailinglist_admin, mailinglist_url, in_progress} Maps into a typed MailingListList; get <name> reuses the same endpoint with a mailinglist_name filter (per the KAS docs at get-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 against testdata/mailinglist/get_mailinglists_response_success.xml and get_mailinglist_response_success.xml. Closes #9.

  • internal/mailfilter read module and kasapi-cli mail filters list subcommand wrapping get_mailstandardfilter. Decodes the Array of {filter, type, title, recommended} Maps into a typed StandardFilterList so callers can resolve the preset filter ids used by mail_spamfilter on accounts/forwards. Mapping test runs against testdata/mailfilter/get_mailstandardfilter_response_success.xml. Refs #9.

  • internal/mailforward read module and kasapi-cli mail forwards list|get subcommand tree wrapping get_mailforwards. The list variant decodes the full Map-of-Maps payload into a typed MailForwardList; get <address> reuses the same endpoint with a mail_forward filter (the source address) and unwraps the single-entry result, mirroring the mail accounts pattern. Mapping tests run against testdata/mailforward/get_mailforwards_response_success.xml and get_mailforward_response_success.xml. Refs #9.

  • internal/mailaccount read module and kasapi-cli mail accounts list|get subcommand tree wrapping get_mailaccounts. The list variant decodes the full Map-of-Maps payload into a typed MailAccountList; get <mail-login> reuses the same endpoint with a mail_login filter and unwraps the single-entry result. The --output=table view 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 against testdata/mailaccount/get_mailaccounts_response_success.xml and get_mailaccount_response_success.xml. Refs #9.

  • kasapi-cli subdomains get <name> calls get_subdomains with a subdomain_name filter and unwraps the single-entry result, mirroring the existing domains get flow; the singular Subdomain value renders as a key/value table with the SSL cert/key/CSR PEM bodies summarised as <bytes,lines>.

  • internal/domain, internal/subdomain, and internal/dns read modules with the matching CLI subcommand trees: kasapi-cli domains list and domains get <name> (get_domains, the latter passing a domain_name filter and unwrapping the single-entry result), kasapi-cli subdomains list (get_subdomains), kasapi-cli tlds list (get_topleveldomains), and kasapi-cli dns list --domain <d> [--nameserver <ns>] (get_dns_settings). Domain types Domain, SSL, TLD, Subdomain, and DNS Record decode 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=table view of domains get so the key/value layout stays readable. Mapping tests run against the shipped testdata/domain/, testdata/subdomain/, and testdata/dns/ fixtures. Closes #8.

  • internal/usage package and kasapi-cli usage subcommand 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 by 0 / 01..31 into a slice (summary first, then days), treats xsi:nil FTP fields as zero, and parses the xsd:string-encoded byte counts into int64 so 9-digit values survive on 32-bit platforms. Closes #10.

  • internal/session persistent session-token cache so a successful KasAuth login (including 2FA via --otp) survives across CLI invocations: a new sessions.toml next to the config file (mode 0600, atomic temp+rename) stores {token, expires_at, lifetime_seconds, update_lifetime} keyed by login. auth.SessionTokenSource now loads the cached entry on first use, reuses it while expires_at has not been reached, persists every fresh KasAuth response, and deletes the entry on Invalidate. Lifetime defaults to session.DefaultLifetime (24 h, matching the KasAuth session_lifetime default) when --session-lifetime was not set; otherwise it mirrors the flag value. With --session-update-lifetime Y, api.Client.Call now invokes a new optional Heartbeater interface on the token source after every successful call so the local expires_at rolls forward in lockstep with the server-side window. Practical effect: rerun a command and no --otp prompt is needed for as long as the session is alive.

Changed

  • CLAUDE.md: rewrite stale Repository State paragraph that still described the project as greenfield (no cmd/, internal/, go.mod, no git repository, no build/test runnable). Replace with the current state: read-phase modules wired up, main protected with required signatures, CI gate (lint & test + docs sync) green on every push. Fix the testdata/ filename convention to match the real layout (<module>/<kas_action>_ response_<status>[_<variant>].xml, not get_<thing>.xml). Add pointers to the kasapi-cli-git-workflow / kasapi-cli-code-review skill files and CONTRIBUTING.md alongside the existing docs/go/ references. Wire the standard command loop to the Makefile targets that exist today.

  • go.mod: bump dependency pins after a routine audit pass — golang.org/x/term v0.30.0 → v0.34.0 (last release that still builds against the project's go 1.23.0 baseline; 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/sys v0.31.0 → v0.35.0, github.com/spf13/pflag v1.0.9 → v1.0.10, github.com/cpuguy83/go-md2man/v2 v2.0.6 → v2.0.7. The generated docs/cli/ is byte-identical after the bump.

Fixed

  • docs/usage/: replace 404 KAS-API anchor URL (packages/API%20Functions.html, used as a generic placeholder in every page) with per-function files/<kas_action>-inc.html URLs. Five referenced KAS actions did not exist — replace with the canonical names captured in testdata/: get_accountusageget_space, get_accountusagedetailget_space_usage, get_accounttrafficget_traffic, get_tldsget_topleveldomains, get_mailfilterget_mailstandardfilter. All 24 external doc links in the user-facing markdown set now resolve to HTTP 200.

  • --verbose / -v was bound to a RootOptions field but never read anywhere; the flag was effectively a no-op. Plumb a *slog.Logger (text handler on stderr when verbose, discard otherwise) through BuildAPIClient into transport.Client, api.Client, and auth.Client. Events emitted: resolved credentials with auth_data redacted (cli); SOAP action before each request, auth-failure retry, flood_protection fallback gate, applied KasFloodDelay (api); KasFloodDelay gate wait, transient-error retry attempt (transport); KasAuth bootstrap and credential-token issued with login + token length only (auth). Stdout stays clean for -o json | jq pipes; logs go to stderr only.

Changed

  • README.md: expand with a Configuration section (TOML profile example for both auth_type=plain and auth_type=session, KAS_LOGIN/KAS_AUTHDATA/KAS_AUTHTYPE env-var reference, flag/env/profile precedence), a Quick start section with the read commands that exist today (accounts list|get|resources, server info, --output formats), and a Troubleshooting section covering KasFloodDelay, the no_auth/unknown_session/ kas_session_invalid retry behaviour, --verbose, and a pointer at the signed-commit / branch-protection rules in CONTRIBUTING.md. Closes #14.

  • CONTRIBUTING.md: add explicit pointers to the kasapi-cli-git-workflow and kasapi-cli-code-review skill files alongside the existing references to AGENTS.md and the docs/go/ set; absorb the contributor-facing "Repository layout" section that previously lived in README.md.

  • internal/testutil: extract the repoRoot / decodeFixture / fakeCaller helpers that every per-module *_test.go carried 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. DecodeFixture now takes a forward-slash-separated path rooted at testdata/ (e.g. "mailinglist/get_mailinglists_response_success.xml") so the fixture layout is visible at the call site instead of being hidden inside per-module decodeFixture(t, name) wrappers. The FakeCaller stub is exported with Resp / Err / GotAction / GotParams fields 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 after go test -race ./... and golangci-lint run are clean against the migrated suite).

  • kasapi-cli mail lists get argument placeholder renamed from <name> to <mailinglist-name> so the help text matches the KAS wire parameter and the placeholder convention used by every other get subcommand (<address>, <mail-login>, <dyndns-login>, <domain>, <software-id>, …). TestMailingListSingularTabular was tightened from a map-lookup over rows (order-insensitive) to an indexed comparison so a future refactor reordering TableRows cannot slip past the test silently.

  • internal/soap: extend Value with typed Map accessors (MapString, MapInt, MapInt64, MapFloat) and a generic AsInt coercion so every read module can drop its private getString / getInt / getInt64 / getFloat helper. 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 by TestValueMapAccessors in internal/soap/soap_test.go against missing-key, cross-kind, and unparseable inputs. The package-local getBool / getYN in internal/account/decode.go are 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 get was renamed to kasapi-cli accounts settings; the old name now wraps get_accounts with the account_login filter (see Added), matching the mail accounts list|get pattern. The accounts list short description was tightened to clarify that an unfiltered get_accounts returns every account visible to the login (every sub-account for a main login, just the login itself for a sub-account).

Changed

  • internal/usage: add a (t Traffic) IsSummary() bool helper so callers no longer rely on the Day == 0 magic number to distinguish the monthly summary row from per-day entries; the table renderer is switched over too. Document on Space that UsedWebspace is 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 be 1..12) instead of forwarding obvious typos to KAS. Closes #45.

Changed

  • internal/api/doc.go: stop enumerating the auth-failure code list inline; point to IsAuthFailure as the single source of truth so a future code addition only has to update one place.

  • internal/auth/source.go: extend the SessionTokenSource type-level doc to describe the snapshot/restore semantics applied during Invalidate, so readers see the full lifecycle without having to drill into the field block.

  • internal/api: add testdata/response_failed_kas_session_invalid.xml and TestCallRetriesOnSessionInvalid to pin the full Client + retry composition for the new code; complements the IsAuthFailure table-test row.

Fixed

  • Session re-authentication now triggers on kas_session_invalid. IsAuthFailure previously covered only no_auth, unknown_session, kas_access_forbidden, and got_no_login_data; KAS also returns kas_session_invalid when a server-side session is no longer accepted (e.g. it was created with session_update_lifetime=N and the lifetime elapsed). Without this code the auto-retry path in *api.Client.Call did not fire and the user saw the raw fault.

  • internal/auth/source.go: preserve the user-configured Lifetime / UpdateLifetime across Invalidate. 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 first Credentials call and restored by Invalidate. 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-lifetime would otherwise pin the persisted entry to update_lifetime=false forever.

  • internal/auth/source.go: sharpen the Heartbeat doc 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 of Store; the comment now describes the actual conditions (UpdateLifetime false or no cached token). Closes #41.

  • internal/api/client.go: drop the stale "(issue #5)" reference from the StaticTokenSource doc comment. Issue #5 was closed by PR #29 when the KasAuth client landed; the surrounding sentence is kept.

  • internal/account/table.go: document the used_account_space unit 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 from DecodeSpace / DecodeSpaceUsage / DecodeTraffic error strings. The Client wrappers already prepend usage: 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 established account / server pattern.

  • kasapi-cli config init: rename the local --profile flag to --name so it no longer shadows the persistent root --profile flag. Previously kasapi-cli --profile X config init silently reverted to the local default main instead of writing profile X. The persistent --profile flag continues to select which profile is used at runtime; --name selects which profile is written.

  • Replace the legacy direct err == io.EOF / err != io.EOF comparisons in internal/cli/confirm.go and internal/auth/codec.go with errors.Is(err, io.EOF) so wrapped EOF values are still recognised.

  • internal/transport: drop the manual Accept-Encoding: gzip request header. net/http only 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 as XML syntax error: invalid character entity &… on the first kasserver response that came back compressed. Removing the header lets net/http add it (and decode the response) itself.

Added

  • kasapi-cli config subcommand tree for first-run bootstrap and inspection without hand-writing TOML: config init interactively prompts for login, auth_type (session|plain, defaulting to session), and auth_data (hidden via golang.org/x/term.ReadPassword), writes the profile to the resolved config path with mode 0600 (parent dirs created 0700, atomic temp+rename), refuses to overwrite an existing profile unless --force, and offers to set default_profile when none is configured. --profile selects the profile name (default main). Non-TTY stdin fails fast with a clear error so CI and pipes do not hang. config show prints the resolved effective configuration after the flag/env/profile merge with auth_data redacted via Credentials.String. config path prints the resolved config-file path. config.Save is the new persistence helper that backs config init. (Closes #34.)
  • --otp, --session-lifetime, and --session-update-lifetime persistent flags on the root command, exposing the optional KasAuth parameters (session_2fa, session_lifetime, and session_update_lifetime). All three are plumbed through BuildAPIClient into auth.Options so auth.SessionTokenSource forwards them on the credential-token bootstrap. --session-lifetime is range-checked client-side (1..30000 seconds); --session-update-lifetime accepts Y or N and maps to the tri-state *bool field. The --auth-type help text spells out that these flags are KasAuth-only (the KAS docs do not cover them on direct kas_auth_type=plain calls), so combining any of them with auth_type=plain is rejected up front with a user-error exit code and a message that points to auth_type=session.
  • internal/account and internal/server read 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), and kasapi-cli server info (get_server_information). Domain types Account, AccountSettings (with SSH fingerprints, user_prefs, direct-link flags), AccountResources / ResourceQuota, and Service / ServiceList decode the KAS Map/Array payloads into typed Go values; ResourceQuota.Max == -1 is rendered as in the table view to match the documented "unlimited" sentinel. Mapping tests run against the shipped testdata/account/get_*_response_success.xml fixtures. cli.BuildAPIClient(opts) is the new wiring helper that reads config + env + flags, picks api.StaticTokenSource for auth_type=plain and auth.SessionTokenSource for auth_type=session, and returns an *api.Client that subcommands consume. (Closes #7.)
  • internal/cli CLI scaffold built on spf13/cobra: NewRootCmd() returns the kasapi-cli root command with persistent global flags --config, --profile, --login, --auth-data, --auth-type, --output, --no-color, --verbose, --yes, plus the built-in --help and --version. Output renderers (json, yaml, table) live behind a single Render(w, format, v) entry point; --output=table requires the value to implement the Tabular interface. A Confirm(in, out, prompt) helper covers the [y/N] prompt for future destructive write commands. ExitError, UserError(...), APIError(...), and CodeFor(err) translate failures to the documented exit codes (0 ok, 1 user error, 2 API error); flag-parsing errors are routed through UserError. The binary is intentionally without subcommands until #7 — kasapi-cli prints help and kasapi-cli --version prints the build banner. Adds the goccy/go-yaml (active fork replacing the archived gopkg.in/yaml.v3) and spf13/cobra dependencies. (Closes #12.)
  • internal/auth KasAuth.php credential-token client: separate codec (tns:KasAuth envelope, bare xsd:string token in <return>), Client.GetCredentialToken(ctx) returning the 40-character token, Options{Lifetime, UpdateLifetime, OTP} for the optional session_lifetime, session_update_lifetime, and 2FA session_2fa parameters. Faults surface as typed *Error with helpers IsLoginFailed, IsLoginLocked, IsOTPPinIncorrect, IsUnknownSession. SessionTokenSource adapts the client to the api.TokenSource interface, caching the token and re-fetching on Invalidate so api.Client can refresh transparently after an auth failure. (Closes #5.)
  • internal/api generic KasApi.php call surface composing the soap codec with the http transport: Client.Call(ctx, action, params) encodes, posts, decodes, and feeds the server-reported KasFloodDelay back to the transport gate. SOAP-ENV:Fault bodies surface as typed *Error values whose Code is the stable KAS error string, with predicates (IsAuthFailure, IsFloodProtection, IsNotFound, IsSyntaxError, IsMaxReached, IsInProgress, IsMissingParameter, IsNothingToDo). A TokenSource interface plus StaticTokenSource provide credentials; no_auth and unknown_session trigger one token refresh and retry. (Closes #6.)
  • internal/transport HTTP 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-client RecordDelay/gate pair so callers can honour the server-side KasFloodDelay. Now/Sleep are injectable for deterministic tests via httptest.Server. (Closes #4.)
  • internal/config profile-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 via KAS_LOGIN, KAS_AUTHDATA, KAS_AUTHTYPE. Auth-data is redacted by Credentials.String so secrets do not surface in logs or --help. Validates auth_type (plain or session) and reports missing required fields. (Closes #2.)
  • internal/soap codec for the KAS-API envelope: Value discriminated union mirroring the Apache xml-soap ns2:Map shape (xsi:type: string/int/float/boolean, ns2:Map, SOAP-ENC:Array), Decode for KasApiResponse/SOAP-ENV:Fault envelopes returning *Response or *FaultError, and EncodeRequest for 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-cli entry point with build-stamped --version.
  • internal/ package skeleton mirroring the clean-architecture layering in docs/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.yml matching the gate set in docs/go/LINTING.md.
  • GitHub Actions CI workflow running gofmt, go vet, golangci-lint, go test, go test -race, and go build ./cmd/kasapi-cli.