@wirecat/cli-base 0.40.4 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ # Releases
2
+
3
+ <!-- cspell:ignore flock lslocks OIDC whatsapp zizmor -->
4
+
5
+ ## Scope
6
+
7
+ How a CLI is released: `bin/release`, the release lock, version numbers, the release report and trusted publishing.
8
+
9
+ Rule ids on this page are `WC-REL-<n>`. How to read a rule, how ids work and which rule wins are in the
10
+ [index](README.md). The changelog a release dates is on the [Documents](docs.md) page. The checks a release runs are
11
+ on the [CI](ci.md) page.
12
+
13
+ ## Rules
14
+
15
+ ### WC-REL-1 · Release only with `bin/release`, from a clean `main` that matches `origin/main`
16
+
17
+ - **Why:** the workflow publishes `origin/main`, so a local commit or a dirty tree would not ship what was checked.
18
+ - **Enforced by:** `<app>: bin/release`.
19
+ - **Example:** raise `version` in `package.json` and date the pending changelog section in a pull request. Merge it.
20
+ Then run `bin/release` on `main`. It refuses another branch, uncommitted changes and a `main` ahead of
21
+ `origin/main`.
22
+ - **Exceptions:** none.
23
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
24
+
25
+ ### WC-REL-2 · `main` changes only through a pull request, also during a release
26
+
27
+ - **Why:** the repository ruleset requires it, and every change to `main` then passes the pull request checks.
28
+ - **Enforced by:** review only.
29
+ - **Example:** when `bin/release` renumbers the version or folds the changelog, it opens a pull request, waits for its
30
+ checks and merges it before it publishes. The merge rule itself is
31
+ [WC-CI-12](ci.md#wc-ci-12--merge-to-main-only-through-a-pull-request-whose-checks-all-passed).
32
+ - **Exceptions:** none.
33
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
34
+
35
+ ### WC-REL-3 · `bin/release` takes the machine release lock before it builds or publishes
36
+
37
+ - **Why:** two sessions published at the same time on one machine; one release at a time, across every package.
38
+ - **Enforced by:** `<app>: bin/release`.
39
+ - **Example:**
40
+
41
+ ```sh
42
+ . ./config/wirecat/release-lock.sh
43
+ wirecat_release_lock
44
+ ```
45
+
46
+ The helper comes from the community presets (`adopt-standards.py --only release-lock`). It holds
47
+ `${XDG_RUNTIME_DIR:-/tmp}/leemour-release.lock` with `flock` on file descriptor 9 until the release ends.
48
+ - **Exceptions:** none. cli-testing still opens the lock with its own `flock` line and moves to the helper.
49
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** NEED-854.
50
+
51
+ ### WC-REL-4 · Never break the release lock; wait for the holder and try again
52
+
53
+ - **Why:** a deleted lock file is a new file, so a second release can then run at the same time as the first.
54
+ - **Enforced by:** review only.
55
+ - **Example:** when the lock is held, the helper names the holder on stderr. Let that release finish, then run
56
+ `bin/release` again. Do not delete the lock file and do not kill the holder.
57
+ - **Exceptions:** none. A missing `lslocks` or an old holder label does not allow a release; only `flock` decides.
58
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
59
+
60
+ ### WC-REL-5 · One releaser per version: npm decides which number is free
61
+
62
+ - **Why:** two sessions often prepare the same number; the second must not publish over the first or fail late.
63
+ - **Enforced by:** `tg-cli: bin/release` · `max-cli: bin/release` · `whatsapp-cli: bin/release` ·
64
+ `cli-memo: bin/release`.
65
+ - **Example:** before it publishes, `bin/release` asks npm. When npm already has the version, or a higher one, it
66
+ takes the next free number at the same level (after `x.y.0` the next minor, else the next patch). It lands that
67
+ number through a pull request and publishes it.
68
+ - **Exceptions:** a pre-release version is chosen by hand. drive's and zoom's `bin/release` do not renumber yet: they
69
+ refuse a version npm already has.
70
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
71
+
72
+ ### WC-REL-6 · `package.json` holds the version, and the program reads a synced copy
73
+
74
+ - **Why:** a bundled command has no `package.json` to read, so `--version` reads a source file that must not drift.
75
+ - **Enforced by:** `tg-cli: pnpm version:check` · `max-cli: pnpm version:check` · `whatsapp-cli: pnpm version:check`.
76
+ - **Example:** edit only `package.json`, then run `pnpm version:sync` to update `src/version.ts`. `pnpm version:check`
77
+ fails when the two disagree.
78
+ - **Exceptions:** a shared library that prints no version has no `src/version.ts`. Today tg, max and wa have
79
+ `version:check`; memo, drive and zoom do not.
80
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
81
+
82
+ ### WC-REL-7 · Versions are `x.y.z`; before 1.0.0 the changelog, not the number, marks a breaking change
83
+
84
+ - **Why:** before 1.0.0 any release may change the API, so a caller reads "Changed — may break callers" before an
85
+ upgrade.
86
+ - **Enforced by:** review only.
87
+ - **Example:** raise the minor (`0.40.4` to `0.41.0`) for a release with new features, and the patch for one with only
88
+ fixes. A breaking change, in either kind of release, has an entry under "Changed — may break callers".
89
+ - **Exceptions:** a pre-release version (`0.41.0-rc.1`) is chosen by hand.
90
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
91
+
92
+ ### WC-REL-8 · A messenger CLI waits two hours between releases; a shared library does not wait
93
+
94
+ - **Why:** a broken CLI release must be noticed before the next one lands on top of it. Every CLI waits for a library
95
+ release, so a gap there only delays them.
96
+ - **Enforced by:** `<app>: bin/release`.
97
+ - **Example:** tg-cli, max-cli and whatsapp-cli's `bin/release` refuse to start within two hours of the last version
98
+ on npm. cli-base and cli-core release one after another with no gap.
99
+ - **Exceptions:** memo, drive and zoom have no gap in their `bin/release` today.
100
+ - **Applies to:** messenger CLIs (gap); shared libraries (no gap). **Since:** cli-base 0.41.0. **Rulings:** NEED-854.
101
+
102
+ ### WC-REL-9 · A breaking change to a stable shared-library export ships at most once a week
103
+
104
+ - **Why:** each such change makes every CLI change too; batching them keeps the CLIs on one version.
105
+ - **Enforced by:** review only.
106
+ - **Example:** gather breaking export changes into one release a week. Bump the library in every CLI that uses it on
107
+ the same day.
108
+ - **Exceptions:** none.
109
+ - **Applies to:** shared libraries. **Since:** cli-base 0.41.0. **Rulings:** NEED-473.
110
+
111
+ ### WC-REL-10 · A messenger CLI is released only with a release report the owner signed for that commit
112
+
113
+ - **Why:** the judgement half of a release (changelog wording, docs against the change, live checks) has no other
114
+ record that it ran for this exact commit.
115
+ - **Enforced by:** `max-cli: bin/release`.
116
+ - **Example:** max-cli's `bin/release` refuses unless the report has `Commit: <HEAD>` and `Signed off: <date>`. When
117
+ `bin/release` only renumbers or folds the changelog, it carries the report to the new commit. A fork passes its
118
+ own report with `bin/release --report <path>`.
119
+ - **Exceptions:** none. Today only max-cli's `bin/release` checks the report. tg-cli and whatsapp-cli have the
120
+ release skill, but their `bin/release` does not check a report yet.
121
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
122
+
123
+ ### WC-REL-11 · Publish from GitHub Actions with npm trusted publishing
124
+
125
+ - **Why:** no npm token is stored in GitHub, and npm attaches provenance to the package.
126
+ - **Enforced by:** `release.yml:publish`.
127
+ - **Example:** `bin/release` starts `release.yml` and follows it. Only the `publish` job has the OIDC permission (the
128
+ short-lived identity npm trusts), and it runs no project code: it only runs `npm publish` on the validated file.
129
+ - **Exceptions:** npm trusts the workflow by file name: renaming `release.yml` stops publishing until npm's trusted
130
+ publisher settings are updated.
131
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
132
+
133
+ ### WC-REL-12 · Publish only after full validation and the release security checks pass
134
+
135
+ - **Why:** pull request checks are bounded; the full suite and the heavy scanners run before anything reaches npm.
136
+ - **Enforced by:** `release.yml` · `release-checks.yml`.
137
+ - **Example:** `release.yml` runs full validation and `release-checks.yml` (dependency audit, workflow analysis with
138
+ zizmor, CodeQL). Validation packs the package once, and the `build` job checks that file's SHA-256 before it is
139
+ handed to `publish`.
140
+ - **Exceptions:** none.
141
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
142
+
143
+ ### WC-REL-13 · Tag `v<version>` only after npm shows that version
144
+
145
+ - **Why:** a tag for a version npm does not have yet marks a release that may still fail.
146
+ - **Enforced by:** `release.yml:tag`.
147
+ - **Example:** the `publish` job waits until npm serves the new version; only then does the `tag` job push
148
+ `v<version>`. In the CLIs, the workflow also creates the GitHub release from that version's changelog section.
149
+ - **Exceptions:** none.
150
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
151
+
152
+ ### WC-REL-14 · `bin/release --local` is the fallback, and it never prints the npm token
153
+
154
+ - **Why:** a release must still be possible when GitHub Actions is down, without leaking the token.
155
+ - **Enforced by:** `tg-cli: bin/release` · `max-cli: bin/release` · `whatsapp-cli: bin/release` ·
156
+ `cli-memo: bin/release`.
157
+ - **Example:** `bin/release --local` runs the same checks on this machine and reads the npm token from the keyring
158
+ into the publish command's environment. Neither success nor an error prints it.
159
+ - **Exceptions:** none. drive's and zoom's `bin/release` have no `--local` yet.
160
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
161
+
162
+ ### WC-REL-15 · Fold a late pending section into the unpublished version before publishing
163
+
164
+ - **Why:** a pull request merged after the version was dated adds a pending section above it, and that change must
165
+ ship in the version it lands in.
166
+ - **Enforced by:** review only.
167
+ - **Example:** `bin/release` runs `cli-dev changelog fold --land --version <x.y.z>` while npm does not have the version.
168
+ It folds the pending section into the dated one, and merges any repeated `###` heading, through a pull request.
169
+ - **Exceptions:** none.
170
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
171
+
172
+ ### WC-REL-16 · The package ships only the files it names
173
+
174
+ - **Why:** a stray file in the package can leak local data and grows every install.
175
+ - **Enforced by:** `tg-cli: pnpm release:check` · `max-cli: pnpm release:check` · `whatsapp-cli: pnpm release:check`.
176
+ - **Example:** `package.json` `files` lists what ships. The CLI's `pnpm release:check` runs `packContents` from
177
+ `@wirecat/cli-base/release` and fails on any other path in `npm pack --dry-run`.
178
+ - **Exceptions:** none. tg, max and wa run this check. memo's `release:check` has no package-contents step, drive and
179
+ zoom have no `release:check`, and zoom checks the packed file in a test instead.
180
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
181
+
182
+ ## Sources
183
+
184
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
185
+ holds.
186
+
187
+ - [cli-base `RELEASING.md`](https://github.com/WireCatLabs/cli-base/blob/main/docs/dev/RELEASING.md), the step by step
188
+ for cli-base itself
189
+ - [community standards, "Release locking"](https://github.com/WireCatLabs/community/blob/main/standards/README.md),
190
+ the how-to for installing the lock helper
191
+ - each CLI's `bin/release` and its release skill, which carry out these rules
@@ -0,0 +1,260 @@
1
+ # Security
2
+
3
+ <!-- cspell:words zizmor -->
4
+
5
+ ## Scope
6
+
7
+ How a CLI keeps secrets and private data safe: secrets never in logs, fixtures or argv, the keyring, error text that
8
+ never repeats input, the scanners, workflow hardening, and how a vulnerability is reported.
9
+
10
+ Two words used on this page:
11
+
12
+ - A **secret** is anything that grants access: a session token, a bot token, an API key, a password, a 2FA code, a
13
+ proxy password.
14
+ - **Private data** is what people wrote or who they are: message text, chat titles, names, phone numbers.
15
+
16
+ Rule ids on this page are `WC-SECR-<n>`. How to read a rule, how ids work and which rule wins are in the
17
+ [index](README.md).
18
+
19
+ ## Rules
20
+
21
+ ### WC-SECR-1 · Private data and secrets never reach a log, a fixture or a committed document
22
+
23
+ Command output is the only place where a message the owner asked to read may appear. A log, a run record, a test
24
+ fixture, a document and a commit message never hold message text, a token or a phone number.
25
+
26
+ - **Why:** a log or a repository is copied and kept far longer than anyone remembers; private data in it is a leak.
27
+ - **Enforced by:** `node-ci.yml:secrets`.
28
+ - **Example:** a log line says `{ "event": "send", "outcome": "sent", "count": 1 }`, not the text that was sent.
29
+ - **Exceptions:** none. The scanners catch secrets of known shapes; message text and names are caught only in review.
30
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
31
+
32
+ ### WC-SECR-2 · A log keeps only known attribute keys, and hides content keys
33
+
34
+ A log record writes only the attribute keys in cli-base's list (`LOG_ATTRIBUTE_KEYS`) and the ones a CLI adds by
35
+ name. Any other top-level key is written as `[not allowed]`. A key that names content or a secret (`text`, `title`,
36
+ `token`, `phone`, `chatTitle`, `accessToken` …) is written as `[redacted]`, at any depth.
37
+
38
+ - **Why:** redaction is the second line of defence; the first is never handing a secret to a logger.
39
+ - **Enforced by:** `src/logging.test.ts`.
40
+ - **Example:** `recordLog({ … redact: ["sessionKey"] })` adds a CLI's own secret field to the redacted list.
41
+ - **Exceptions:** none.
42
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
43
+
44
+ ### WC-SECR-3 · Merged into [WC-TEST-8](testing.md#wc-test-8--prove-that-content-does-not-leak-with-a-marker-and-visibly-fake-data)
45
+
46
+ ### WC-SECR-4 · Secrets come from the environment, then the OS keyring, then a private file
47
+
48
+ cli-base's `Credentials` reads the environment variable first (for CI), then the OS keyring, then a file with mode
49
+ `0600`. The keyring service is the command's name. When the config directory comes from the environment, the service
50
+ name is scoped to that directory (`isolated: true`).
51
+
52
+ - **Why:** the keyring is the safest store on the computer, and a throwaway config directory must not overwrite the
53
+ owner's real keyring entry.
54
+ - **Enforced by:** `src/credentials.test.ts`.
55
+ - **Example:** a bot token is read from `MAX_BOT_TOKEN`, then the keyring entry `bot:<profile>`, then the file.
56
+ - **Exceptions:** `auto` storage tries the keyring and, when it fails, warns once on stderr and uses the file.
57
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-295.
58
+
59
+ ### WC-SECR-5 · A secret is never taken on the command line
60
+
61
+ A secret is typed at a terminal, without echo, or piped on stdin. A secret given on the command line is refused with
62
+ `validation_error`. It is stored only after the remote side accepted it.
63
+
64
+ - **Why:** the command line is visible to other processes and kept in shell history; a keyring entry cannot be read
65
+ back to check it.
66
+ - **Enforced by:** review only.
67
+ - **Example:** cli-core's `readSecret` reads one line from a terminal or a pipe, at most 64 KiB, and prompts on stderr.
68
+ `contacts import` reads phone numbers from stdin, never from the command line.
69
+ - **Exceptions:** none.
70
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-203.
71
+
72
+ ### WC-SECR-6 · Error text never repeats a secret or what other people wrote
73
+
74
+ An error message names what failed and the next step. It never quotes a secret, a parameter the caller passed that
75
+ may hold private data, or text from the provider that can quote a chat title or a message. Provider errors are reduced
76
+ to their error name, or replaced with fixed wording.
77
+
78
+ - **Why:** errors reach logs, run records and agents' transcripts.
79
+ - **Enforced by:** review only.
80
+ - **Example:** tg's error mapper keeps only the provider's error name, and turns a name that tells a person nothing
81
+ into a fixed sentence with the next `tg …` command to run.
82
+ - **Exceptions:** the owner's own input that holds no private data, such as a file path, may be named.
83
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
84
+
85
+ ### WC-SECR-7 · A token stays inside the transport
86
+
87
+ The address that holds a token is built in one place and never reaches an error, a trace or a rethrown network error.
88
+ Text that looks like a token is replaced with `[redacted]`. Attachments carry a file id, never a download address
89
+ that contains the token. Proxy credentials are passed as options, never inside a proxy address.
90
+
91
+ - **Why:** the token is the account or the bot; anything that prints it hands it away.
92
+ - **Enforced by:** review only.
93
+ - **Example:** a failed Bot API call reports the method and the API's own description with tokens replaced, never
94
+ the request address.
95
+ - **Exceptions:** none.
96
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
97
+
98
+ ### WC-SECR-8 · Phone numbers are masked unless the owner asks for the whole number
99
+
100
+ Output shows the last four digits (`***1234`). `--show-phone` prints the whole number, on the command line only.
101
+
102
+ - **Why:** a phone number identifies a person and is the login of a messenger account.
103
+ - **Enforced by:** review only.
104
+ - **Example:** `max work account show --show-phone`.
105
+ - **Exceptions:** none.
106
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-209.
107
+
108
+ ### WC-SECR-9 · A command does not send the CLI's own secret files unless the owner says so
109
+
110
+ A command that sends a local file refuses known credential files, browser profiles, the WireCat apps' own folders and
111
+ the message store. The owner adds `--allow-any-file` to send one anyway.
112
+
113
+ - **Why:** an agent talked into "send me your config" must not be able to send the session.
114
+ - **Enforced by:** review only.
115
+ - **Example:** `max work messages send @me --file ~/.config/max-cli/credentials.json` is refused.
116
+ - **Exceptions:** ordinary hidden developer folders, such as `.cache` or `.worktrees`, are not refused.
117
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-274.
118
+
119
+ ### WC-SECR-10 · Private files are written atomically with private modes
120
+
121
+ A file that holds a secret or private state is written with `writeSecurely`: atomically, mode `0600`, in a directory
122
+ created with mode `0700`. A temporary credential file, such as a login QR image, is `0600` and removed in `finally`.
123
+ tg's `doctor` reports a wrong mode and prints the `chmod` fix without applying it.
124
+
125
+ - **Why:** another user on the same computer must not read the session.
126
+ - **Enforced by:** `src/config.test.ts`.
127
+ - **Example:** `writeSecurely(path, JSON.stringify(state), 0o600)`.
128
+ - **Exceptions:** Windows ignores both modes.
129
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
130
+
131
+ ### WC-SECR-11 · Text from outside is made safe before a terminal or an agent sees it
132
+
133
+ Display text and agent text pass through cli-base's sanitizer. It escapes control characters, terminal escapes and
134
+ the characters that reverse text direction.
135
+
136
+ - **Why:** a chat title can hold `\x1b[2K` and overwrite what the tool already printed, or make `invoice‮fdp.exe`
137
+ look like a PDF.
138
+ - **Enforced by:** `src/sanitize.test.ts`.
139
+ - **Example:** a title with an escape sequence prints as visible `\x1b` text.
140
+ - **Exceptions:** machine output (`--json`) keeps the original strings, as a contract with scripts.
141
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
142
+
143
+ ### WC-SECR-12 · Nothing goes to an outside model or service without explicit config
144
+
145
+ Private data goes to an outside AI model only when the owner configured it, through cli-core's one model gateway. A
146
+ feature that sends a chat's text asks once for consent and can be turned off per chat. Embeddings use the local model
147
+ by default. Nothing is downloaded unasked either: attachment text is extracted from a remote file only with
148
+ `--download`, and a meaning search with no model falls back to words and says `meaning: "unavailable"`.
149
+
150
+ - **Why:** messages belong to the people who wrote them; sending them to a third party is the owner's decision.
151
+ - **Enforced by:** review only.
152
+ - **Example:** reply rewording is off until the profile turns it on, and asks once per profile.
153
+ - **Exceptions:** none.
154
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-545, NEED-552, NEED-755, NEED-756, NEED-757, ss:10.
155
+
156
+ ### WC-SECR-13 · A CLI sends no telemetry
157
+
158
+ - **Why:** the tool must not report on its owner; for a messenger CLI, extra traffic also stops it looking like the
159
+ official client.
160
+ - **Enforced by:** review only.
161
+ - **Example:** a pull request that adds a call to a WireCat server, or to an analytics service, is refused in review.
162
+ - **Exceptions:** a messenger CLI may later copy what the official client reports, and nothing more.
163
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-16.
164
+
165
+ ### WC-SECR-14 · Certificate checks stay on, and the computer's trust store is never changed
166
+
167
+ A CLI never turns off TLS verification. A root certificate that a provider needs is trusted inside the process only.
168
+
169
+ - **Why:** changing the computer's trust store weakens every other program on it.
170
+ - **Enforced by:** review only.
171
+ - **Example:** for its Bot API calls, max adds the root certificate it ships to that process alone.
172
+ - **Exceptions:** none.
173
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-293, sc:5.
174
+
175
+ ### WC-SECR-15 · Security work keeps ordinary workflows working
176
+
177
+ Prefer compatibility over guesses about attacks. Fix a confirmed critical issue at once. A new restriction that is not
178
+ critical goes to the owner for review first.
179
+
180
+ - **Why:** users run VPNs, proxies and unusual DNS; a speculative guard that breaks them gets switched off.
181
+ - **Enforced by:** review only.
182
+ - **Example:** attachment downloads use the configured transport and normal redirects, with no private-address block
183
+ list.
184
+ - **Exceptions:** none.
185
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** sc:3, sc:5, sc:6, sc:8.
186
+
187
+ ### WC-SECR-16 · Keep the secret scanners on, and run each one with `--redact`
188
+
189
+ - gitleaks, in the pre-commit hook on staged changes, in the pull request job on the new commits, and on the whole
190
+ history in full validation, at release and every month.
191
+ - Biome's `noSecrets` rule, from the shared preset, off in tests only
192
+ ([WC-TOOL-10](toolchain.md#wc-tool-10--keep-the-secret-lint-on-and-suppress-a-finding-only-with-its-reason)).
193
+ - At release and every month: a dependency audit, zizmor for workflows and CodeQL.
194
+ - **Why:** a secret removed in a later commit is still a leaked secret; `--redact` keeps a finding from leaking it
195
+ again into the log.
196
+ - **Enforced by:** `node-ci.yml:secrets` · `release-checks.yml` · `standards.yml:standards` · `pnpm standards:check`.
197
+ - **Example:** `gitleaks git --staged --no-banner --redact .`. When each scanner runs is on the CI page:
198
+ [WC-CI-1](ci.md#wc-ci-1--run-only-staged-lint-and-a-staged-secret-scan-before-a-commit),
199
+ [WC-CI-3](ci.md#wc-ci-3--run-the-fast-standards-job-on-every-pull-request-and-every-push-to-main),
200
+ [WC-CI-7](ci.md#wc-ci-7--run-full-validation-once-on-each-merge-to-main-on-linux-without-publishing) and
201
+ [WC-CI-10](ci.md#wc-ci-10--run-heavy-security-checks-only-at-release-monthly-or-on-request).
202
+ - **Exceptions:** the pre-commit hook warns, and does not block, when gitleaks is not installed; CI scans anyway.
203
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
204
+
205
+ ### WC-SECR-17 · A gitleaks rule covers each secret shape a CLI handles
206
+
207
+ A CLI adds a gitleaks rule for each token, device id or personal identifier that the default rules do not know. An
208
+ allowlist names synthetic values by value, never a whole file, so the file stays scanned.
209
+
210
+ - **Why:** the default rules know AWS and GitHub keys, not a messenger's session token.
211
+ - **Enforced by:** review only.
212
+ - **Example:** max's `.gitleaks.toml` has `max-session-token`, `max-device-identity` and `phone-number-e164`, and
213
+ allows only `+71234567890` and other sanctioned fakes.
214
+ - **Exceptions:** `pnpm-lock.yaml` may be allowed by path. Today only max and wa have a phone number rule.
215
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
216
+
217
+ ### WC-SECR-18 · Report a vulnerability privately
218
+
219
+ Every repository has a `SECURITY.md`. It names `hello@wirecat.dev` and GitHub's private vulnerability reporting, and
220
+ asks never to send a token, a session or anyone's messages.
221
+
222
+ - **Why:** a public issue tells attackers before users can upgrade.
223
+ - **Enforced by:** review only.
224
+ - **Example:** [max-cli's `SECURITY.md`](https://github.com/WireCatLabs/max-cli/blob/main/SECURITY.md).
225
+ - **Exceptions:** none.
226
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-555.
227
+
228
+ ### WC-SECR-19 · Audit findings stay out of public places
229
+
230
+ An audit follows the [cli-testing security method](https://github.com/WireCatLabs/cli-testing/tree/main/docs/security).
231
+ Its results go to a private repository, not to a public backlog or issue. A fix is described in the changelog in
232
+ general words.
233
+
234
+ - **Why:** a public finding is a guide to attacking users who have not upgraded.
235
+ - **Enforced by:** review only.
236
+ - **Example:** "Fixed: an error could include part of a request" — not the steps to make it happen.
237
+ - **Exceptions:** none.
238
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-271.
239
+
240
+ ### WC-SECR-20 · Pin each third-party action to a commit, and give each workflow read-only defaults
241
+
242
+ - **Why:** a moved tag or a write token in a workflow is how an outside change reaches a release.
243
+ - **Enforced by:** review only.
244
+ - **Example:** `uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1` with
245
+ `persist-credentials: false`, and `permissions: contents: read` at the top of the workflow. A job that needs more
246
+ asks for it on that job only. The `zizmor` scan in `release-checks.yml` reports high findings at release, but does
247
+ not fail on them.
248
+ - **Exceptions:** none.
249
+ - **Applies to:** the ten repositories: the six CLIs, cli-base, cli-core, cli-testing, cli-docs and community.
250
+ **Since:** cli-base 0.41.0. **Rulings:** none.
251
+
252
+ ## Sources
253
+
254
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
255
+ holds. The audit procedure stays in cli-testing; this page links it and does not copy it.
256
+
257
+ - [The security audit method](https://github.com/WireCatLabs/cli-testing/tree/main/docs/security): what an audit
258
+ checks, how a run is done, and where its results go.
259
+ - [The organisation's security policy](https://github.com/WireCatLabs/.github/blob/main/SECURITY.md): how to report a
260
+ problem.
@@ -0,0 +1,177 @@
1
+ # Store
2
+
3
+ ## Scope
4
+
5
+ How local data is kept: one shared store, its areas, account scope, migrations and store versions, what a delete
6
+ and a repair do to the data, and the rule that tests never touch the owner's store.
7
+
8
+ Rule ids on this page are `WC-STORE-<n>`. How to read a rule, how ids work and which rule wins are in the
9
+ [index](README.md).
10
+
11
+ Related pages: which account a read covers, and how a read across accounts is asked for, is in
12
+ [Accounts](accounts.md); the general test sandbox is in [Testing](testing.md); where secrets live is in
13
+ [Security](security.md).
14
+
15
+ ### Words used here
16
+
17
+ - **The store:** the one SQLite file that cli-core opens for every CLI, `wirecat.db`.
18
+ - **Area:** one part of the store for one kind of record, such as `store.messages` or `store.chats`.
19
+ - **Store version:** the number of the newest migration the file has run. It is the `version` field of `MANIFEST`
20
+ in cli-core's `src/store/sqlite/manifest.ts`.
21
+ - **Tombstone:** the mark a deleted message keeps in place of its content.
22
+
23
+ ## Rules
24
+
25
+ ### WC-STORE-1 · Keep local data in the one shared store that cli-core opens
26
+
27
+ - **Why:** one file for every messenger and account lets search, backup and export see everything in one place.
28
+ - **Enforced by:** review only.
29
+ - **Example:** `openStore` opens `wirecat.db` in the WireCat folder (`~/.local/share/WireCat/` on Linux,
30
+ `%LOCALAPPDATA%\WireCat\Data\` on Windows). Rows are keyed by provider and account. `storePath` is the only code
31
+ that reads `MESSAGING_STORE` and `WIRECAT_STATE_DIR`.
32
+ - **Exceptions:** none. Older documents that name `messages.db` describe a store that is no longer used.
33
+ - **Applies to:** CLIs that keep local data through cli-core (tg, max, wa, memo, drive, zoom). **Since:** cli-base 0.41.0.
34
+ **Rulings:** NEED-383.
35
+
36
+ ### WC-STORE-2 · Reach the data through its area, and add a new kind of record as a new area
37
+
38
+ - **Why:** an area keeps one kind of record in one file, so the store does not grow back into one long interface.
39
+ - **Enforced by:** `cli-core: biome noExcessiveLinesPerFile`.
40
+ - **Example:** `store.chats.saveChats(key, …)`, `store.messages.find(…)`, `store.tasks`, `store.mail`. Each area is a
41
+ getter built on first use from its own file (`xStoreOver(context)`). `MessageStore` has no flat methods.
42
+ - **Exceptions:** none. cli-core's `biome.json` caps `src/store/store.ts` at 438 lines so that it does not grow again.
43
+ - **Applies to:** cli-core, and every CLI that calls the store. **Since:** cli-base 0.41.0 (store areas since
44
+ cli-core 0.307.0). **Rulings:** none.
45
+
46
+ ### WC-STORE-3 · A service of one account works through the store bound to that account
47
+
48
+ - **Why:** when no call takes an account, a row of another account cannot be read or written by mistake.
49
+ - **Enforced by:** `cli-core: src/services/account-isolation.test.ts`.
50
+ - **Example:** `accountStore(deps)` returns `store.forAccount(key)`; then `.messages.messages(chatId, …)` takes no
51
+ account, and a row id of another account (an attachment, a mail thread, a meeting) is "not found". A read across
52
+ accounts on purpose asks for the whole store with `crossAccount(deps, reason)`; see [Accounts](accounts.md).
53
+ - **Exceptions:** `forAccount` binds only the account areas (accounts, chats, sync, messages, attachments, roster,
54
+ people, conversations, vectors, counters, tags, mentions). The `chats`, `conversations` and `attachments` services
55
+ use the bound store; the other services still pair `deps.store()` with `deps.account()`. New code uses the bound
56
+ store.
57
+ - **Applies to:** cli-core and every CLI that adds a service. **Since:** cli-base 0.41.0. **Rulings:** none.
58
+
59
+ ### WC-STORE-4 · Reach the store through its public module, never a table module
60
+
61
+ - **Why:** the table modules are the SQL layer; a caller that imports them skips the transaction and the account
62
+ binding.
63
+ - **Enforced by:** review only.
64
+ - **Example:** in cli-core, `biome.json` is meant to refuse an import of `src/store/sqlite/*` from `src/services/`,
65
+ `src/sends/`, `src/mcp/` and `src/cli/`; today it does not, because a later override replaces that rule
66
+ ([WC-ARCH-1](architecture.md#wc-arch-1--call-only-the-layers-below-you-interface-then-service-then-port-then-adapter)). They import from `src/store/store.ts`, which re-exports what they need.
67
+ - **Exceptions:** tests of a table module.
68
+ - **Applies to:** cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
69
+
70
+ ### WC-STORE-5 · A CLI writes no SQL against the shared store
71
+
72
+ - **Why:** the schema belongs to cli-core; SQL in a CLI breaks on the next migration.
73
+ - **Enforced by:** review only.
74
+ - **Example:** a CLI that needs a new query asks cli-core for an area method, and the method lands in cli-core with
75
+ its test. drive and zoom state "no SQL here" in their own architecture pages.
76
+ - **Exceptions:** a provider engine's own file of keys and device state, outside `wirecat.db`
77
+ ([WC-STORE-13](#wc-store-13--keep-keys-and-device-state-out-of-the-shared-store)).
78
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-383.
79
+
80
+ ### WC-STORE-6 · One store method is one whole operation, with no `await` inside a transaction
81
+
82
+ - **Why:** the driver is synchronous; an `await` inside a transaction lets the commit run before the awaited part.
83
+ - **Enforced by:** review only.
84
+ - **Example:** a write goes through `write` and a read of several queries through `read`
85
+ (`src/store/sqlite/transaction.ts`), never Drizzle's `transaction` or a hand-written `BEGIN`. A large write runs
86
+ in bounded batches, because it blocks the event loop while it runs.
87
+ - **Exceptions:** `replaceConversations` writes a new build in several short transactions on purpose. Readers see
88
+ only the current build.
89
+ - **Applies to:** cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
90
+
91
+ ### WC-STORE-7 · Migrations only go forward: add, never edit or rebuild
92
+
93
+ - **Why:** a file that a newer build migrated must still open in the build already installed.
94
+ - **Enforced by:** `cli-core: src/store/sqlite/manifest.test.ts`.
95
+ - **Example:** a change is a new column that is nullable or has a default, or a new table. Triggers, full-text tables
96
+ and data fills go in a custom migration. A mistake in a released migration is fixed by the next version. cli-core's
97
+ manifest tests refuse a rebuilt base table, a gap in the versions and a bundle that differs from what drizzle-kit
98
+ wrote.
99
+ - **Exceptions:** none.
100
+ - **Applies to:** cli-core. **Since:** cli-base 0.41.0. **Rulings:** NEED-383.
101
+
102
+ ### WC-STORE-8 · Take the migration number before you write the migration
103
+
104
+ - **Why:** two branches with the same number leave one migration unapplied on stores that ran the other.
105
+ - **Enforced by:** review only.
106
+ - **Example:** edit the "next free version" line in cli-core `docs/dev/COORDINATION.md` in a pull request of its
107
+ own, and wait for it to merge. Versions are the `version` field of `MANIFEST`; the initial migration is 1.
108
+ - **Exceptions:** none.
109
+ - **Applies to:** cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
110
+
111
+ ### WC-STORE-9 · Ship each migration with its schema spec, an upgrade test and a changelog line
112
+
113
+ - **Why:** the schema page, the code and the files already on disk must agree after every version.
114
+ - **Enforced by:** `cli-core: pnpm schema:check`.
115
+ - **Example:** change `scripts/schema/spec.mjs` first, then `schema.ts` until `pnpm schema:check` passes, then
116
+ `pnpm db:generate` and `pnpm db:bundle`. Add a `src/store/version-<n>.test.ts` that opens a file at version n−1 and
117
+ reads what version n added. The changelog entry names the store version, as in "Chat members in the store (store
118
+ version 7)".
119
+ - **Exceptions:** none.
120
+ - **Applies to:** cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
121
+
122
+ ### WC-STORE-10 · An older build refuses a store it cannot read and says to upgrade
123
+
124
+ - **Why:** an older build that reads a file it does not understand corrupts it or answers wrongly.
125
+ - **Enforced by:** review only.
126
+ - **Example:** each migration keeps `minCompatible`. An additive change leaves it as it is (1 today). A change that
127
+ drops what an older build reads raises it; that older build then refuses the file and asks to be upgraded.
128
+ - **Exceptions:** raising `minCompatible` needs the owner's approval. It is a major version of cli-core, and the CLIs
129
+ ship their upgrade the same day.
130
+ - **Applies to:** cli-core and the CLIs that use the store. **Since:** cli-base 0.41.0. **Rulings:** NEED-867.
131
+
132
+ ### WC-STORE-11 · A deleted message keeps a tombstone and loses its content
133
+
134
+ - **Why:** a delete must remove what the owner wanted gone, while the history still shows that something was there.
135
+ - **Enforced by:** review only.
136
+ - **Example:** a delete keeps a mark in the message row. Triggers erase its text, revisions, transcript, file text and
137
+ tags, so a build that knows nothing about a newer table cannot leave content behind.
138
+ - **Exceptions:** none.
139
+ - **Applies to:** cli-core. **Since:** cli-base 0.41.0. **Rulings:** NEED-393.
140
+
141
+ ### WC-STORE-12 · A repair never deletes data
142
+
143
+ - **Why:** the store is the owner's only copy of much of this data.
144
+ - **Enforced by:** review only.
145
+ - **Example:** `store repair` keeps a misshapen table as a copy, builds the correct one and copies the columns.
146
+ - **Exceptions:** none.
147
+ - **Applies to:** cli-core and the CLIs that expose `store repair`. **Since:** cli-base 0.41.0. **Rulings:** NEED-626.
148
+
149
+ ### WC-STORE-13 · Keep keys and device state out of the shared store
150
+
151
+ - **Why:** a store that holds keys turns every backup and export into a secret.
152
+ - **Enforced by:** review only.
153
+ - **Example:** WhatsApp's linked-device keys live in a separate file per profile, `private.db`, which only the
154
+ provider engine writes. Tokens go to the keyring ([Security](security.md)).
155
+ - **Exceptions:** none.
156
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1395.
157
+
158
+ ### WC-STORE-14 · Tests never open the owner's store
159
+
160
+ - **Why:** migrations only go forward; a test that opened the real file would migrate it, and nothing undoes that.
161
+ - **Enforced by:** review only.
162
+ - **Example:** the vitest `setupFiles` sandbox points `MESSAGING_STORE`, `WIRECAT_STATE_DIR` and each app's own
163
+ directory variables at a temporary directory before any test runs. A test that defines a new app prefix adds it to
164
+ the sandbox. A test that needs its own store is
165
+ [WC-TEST-9](testing.md#wc-test-9--give-each-test-its-own-profile-or-its-own-store). The rest of the sandbox is
166
+ [WC-TEST-1](testing.md#wc-test-1--point-every-path-a-test-could-write-at-a-temporary-directory).
167
+ - **Exceptions:** none.
168
+ - **Applies to:** all CLIs that use the store, and cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
169
+
170
+ ## Sources
171
+
172
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
173
+ holds.
174
+
175
+ - [cli-core `ARCHITECTURE.md`, "The store"](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/ARCHITECTURE.md#the-store)
176
+ - [cli-core `COORDINATION.md`, "Store migrations"](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/COORDINATION.md#store-migrations)
177
+ - [cli-core `TESTING.md`](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/TESTING.md)