Skip to content

feat: add credentials-init, which writes the credentials file - #195

Merged
willkg merged 6 commits into
mainfrom
credentials-init
Sep 23, 2026
Merged

willkg merged 6 commits into
mainfrom
credentials-init

Conversation

@willkg

@willkg willkg commented Sep 23, 2026

Copy link
Copy Markdown
Member

Closes #193.

Adds markfluence credentials-init, which prompts for the site URL, username, and API token, checks them with Confluence, and writes the user's credentials file (~/.config/markfluence/credentials, or under an absolute $XDG_CONFIG_HOME) with mode 0600. It writes only that file and nothing in the working directory; project setup stays with #5's init. The design and its reasoning are in _plans/051_credentials-init.md, including a section on what the code review changed.

What it does

  • Token without echo, through charmbracelet/x/term, which lipgloss already built in. It is now the seventh direct dependency. Ctrl-C at the token prompt restores the terminal.
  • Cloud ID fetched, never asked for, from {site}/_edge/tenant_info, for *.atlassian.net sites only. An unscoped token also works through the gateway, so nobody has to know what kind of token they have. Government and isolated Cloud use a different gateway than the hardcoded api.atlassian.com, so they get no cloud ID.
  • Checked before saving with the request user-info makes. A failure is classified by the response's shape, reusing the markers HTTPError.hint matches. A rejected credential, a site-domain HTML 401, another 401/403, or a non-credential 404 writes nothing. A scope mismatch counts as checked, with a warning. No response or a 5xx asks Save anyway?.
  • A rerun prefills every prompt, so rotating a token is one paste. It first says it will drop comments (Ctrl-C to exit). The token and cloud ID are kept only for the same site, so one site's token is never paired with another's URL.
  • Written atomically: a temp file renamed into place, writing through a symbolic link, then read back and compared.
  • Environment warnings before the first question for any exported CONFLUENCE_* variable that changes what the file does.
  • Refusals: no terminal on stdin or stderr, --json, --env-file, and no home directory are each a plain error, so --json still gets an errorObject.
  • The "missing Confluence …" error now suggests the command when there is a credentials file to write.

Supporting changes

  • internal/client:
    • new credentials.go: CredentialsPath, ReadCredentials (one read returning values, the lines a rewrite drops, and the mode), WriteCredentials, DisplayPath, FetchCloudID;
    • HTTPError.ScopeMismatch and SiteRejectedAuth, which hint now uses;
    • exported setting names, CredentialsDoc, and LooseMode.
  • internal/ui: InfoStderr and SuccessStderr, so all of the command's output sits on stderr beside its prompts.
  • Docs: docs/credentials.md and the README start setup with the command, and the manual steps stay as the alternative. The cloud ID is no longer described as something to leave out for a personal token. docs/confluence/api.md records that the gateway answers a wrong token on /user/current with the same v1 403 as the site.

Checked

  • make check.
  • Live against mozilla-hub, through a pty into a temporary config directory:
    • a good token saves and reports the account, with the file 0600 in a 0700 directory;
    • a wrong token is refused and leaves the existing file unchanged;
    • a rerun keeps the values and announces the dropped comment;
    • user-info works with the written file.
  • By hand: Ctrl-C at the token prompt ends the process with echo restored.

Answers #193: a command that prompts for the credentials, checks them,
and writes the user's credentials file.
The pieces credentials-init (#193) needs, kept in internal/client beside
the reader so one package owns the format:

- CredentialsPath and ReadCredentials, exported, moved to credentials.go.
  ReadCredentials raises no permission warning, since its caller is about
  to rewrite the file 0600; loadDotenv is now readDotenv plus the warning.
- WriteCredentials: temp file and rename at 0600, writing through a
  symbolic link, quoting a value exactly when readDotenv would not read it
  back unchanged, then reading the result back and comparing.
- UnkeptLines, the lines a rewrite would drop, and DisplayPath.
- FetchCloudID: the unauthenticated tenant_info request, outside send,
  which always sets basic auth.
- HTTPError.ScopeMismatch and SiteRejectedAuth, the shapes hint already
  matched, so a caller classifying a failure cannot disagree with it.

The missing-settings error now suggests credentials-init when there is a
credentials file to write, and not in the half-pair forms, where a new
file would fail the same-source rule.
Answers #193. credentials-init prompts for the site URL, username, and
token (read without echo), fetches the cloud ID for an atlassian.net site,
checks the credentials with the request user-info makes, and writes the
user's credentials file 0600. It writes nothing in the working directory.

A failed check is classified by the response's shape: a rejected
credential, a site-domain HTML 401, another 401/403, or a non-credential
404 writes nothing; a scope mismatch, a server error, or no response asks
whether to save anyway. An existing file prefills every prompt.

It refuses without a terminal on stdin or stderr, and refuses --json and
--env-file, each as a plain error so --json still reports it.

charmbracelet/x/term becomes a direct dependency; lipgloss already built
it in. The plan is amended where the implementation found a simpler
shape.
Setup in the README and docs/credentials.md starts with the command, and
the manual steps stay as the alternative. The cloud ID is no longer
described as something to leave out for a personal token, which works
with or without one. api.md records that the gateway answers a wrong
token on /user/current with the same v1 403 as the site.
- The current token and cloud ID are offered only for the same site. A
  kept token after a new URL paired one site's token with another's URL,
  the thing the same-source rule exists to stop.
- A same-site cloud ID is kept when the fetch fails, and for a host
  outside atlassian.net, where it was set by hand; a scoped-token user
  rotating a token no longer loses it.
- The host is lowercased, so a capitalized atlassian.net site still gets
  its cloud ID. A URL's user name and password are dropped with a note,
  and URL errors quote what was typed.
- A scope mismatch is a checked answer: the token authenticated.
- The environment is reported before the first question, not after the
  write.
- A rerun says first that it will drop comments, with "Ctrl-C to exit".
  ReadCredentials returns the values, the dropped-line counts, and the
  mode from one read, replacing UnkeptLines.
- All output goes to stderr beside the prompts (ui.InfoStderr and
  SuccessStderr), so > log cannot hide a status line.
- The site-rejected hint no longer names credentials-init, which cannot
  help in CI.
- The setting names, CredentialsDoc, and LooseMode are exported from
  internal/client instead of copied.

The plan records each change under "Amended after code review".
This clarifies where you can and can't use credentials-init and
simplifies the section about it requiring a terminal.
@willkg
willkg merged commit 4173fd0 into main Sep 23, 2026
1 check passed
@willkg
willkg deleted the credentials-init branch September 23, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a command that writes the credentials file

1 participant