Skip to content

feat(reachability): add versioned state and the status command - #203

Closed
Rinse12 wants to merge 4 commits into
masterfrom
feat/reachability-status
Closed

Rinse12 wants to merge 4 commits into
masterfrom
feat/reachability-status

Conversation

@Rinse12

@Rinse12 Rinse12 commented Nov 2, 2026

Copy link
Copy Markdown
Member

Part of #198 (step 3). Based on #202, so its commit shows here until that merges.

Summary

<dataPath>/reachability/
├── config.json           # versioned; written by setup/enable/disable and on IP change
├── runtime-status.json   # daemon's last checks; written only by the daemon
└── caddy-data/           # certificates read by status (Caddy arrives in step 4)

bitsocial reachability status [--json] reports mode, announced addresses, certificate expiry and last checks.

The separate runtime file is not in the issue. A periodic check landing during setup would otherwise overwrite the operator's config with a stale copy.

Evidence

  • Before: no reachability topic.
    After: test/reachability/state.test.ts and test/cli/reachability-status.test.ts pass. npm run test:cli: 53 files, 507 passed, 1 skipped.
status --json, nothing configured   -> { configured: false, addresses: [] }, exit 0, stdout is only JSON
config.json from a newer version    -> error names the fix, exit non-zero, file untouched
half-written .crt or runtime file   -> skipped, status still works

Merge Danger

Door: two-way

The config.json schema becomes a compatibility commitment only once setup writes it (step 5).

Blast Radius: none

Adds a read-only command and a help topic.

computeKuboConfig(kuboConfig, reachabilityConfig, enabled) is the one place that
decides the kubo edits. planKuboConfig also returns a record of what changed,
which tells our entries from identical operator ones and lets disable restore
the touched fields byte for byte.
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 42 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: adbfb69a-b005-4594-9891-f372f913d495
Commits

Reviewing files that changed from the base of the PR and between 6121b54 and 812bb36.

Files selected for processing: 7
  • package.json
  • src/cli/commands/reachability/status.ts
  • src/reachability/kubo-config.ts
  • src/reachability/state.ts
  • test/cli/reachability-status.test.ts
  • test/reachability/kubo-config.test.ts
  • test/reachability/state.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T13:04:40.675626Z 189d3c6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread src/reachability/state.ts
Comment on lines +134 to +150
export async function readIssuedCertificates(dataPath: string, now: Date = new Date()): Promise<CertificateInfo[]> {
const certificatesRoot = path.join(caddyDataDir(dataPath), "certificates");
let entries: { name: string; parentPath: string; isFile(): boolean }[];
try {
entries = await fs.readdir(certificatesRoot, { recursive: true, withFileTypes: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
throw error;
}
const certificates: CertificateInfo[] = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".crt")) continue;
const filePath = path.join(entry.parentPath, entry.name);
try {
const certificate = new X509Certificate(await fs.readFile(filePath));
const notAfter = new Date(certificate.validTo);
certificates.push({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: status lists stale certificates as if they were current.

readIssuedCertificates (

// Caddy stores certificates at <data>/certificates/<ca>/<name>/<name>.crt.
export async function readIssuedCertificates(dataPath: string, now: Date = new Date()): Promise<CertificateInfo[]> {
const certificatesRoot = path.join(caddyDataDir(dataPath), "certificates");
let entries: { name: string; parentPath: string; isFile(): boolean }[];
try {
entries = await fs.readdir(certificatesRoot, { recursive: true, withFileTypes: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
throw error;
}
const certificates: CertificateInfo[] = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".crt")) continue;
const filePath = path.join(entry.parentPath, entry.name);
try {
const certificate = new X509Certificate(await fs.readFile(filePath));
const notAfter = new Date(certificate.validTo);
certificates.push({
issuer: path.relative(certificatesRoot, filePath).split(path.sep)[0]!,
) parses every .crt under caddy-data/certificates/**. It never compares them with the current config (publicIps, domain, staging), and the human-readable output doesn't print issuer.

Normal-use scenarios once Caddy lands (step 4):

  • IP change: the old IP's cert stays in Caddy storage until it expires. The list is sorted by notAfter, so the dead cert comes first with few or negative hours left, and an operator reads that as "my cert is about to expire".
  • Staging then production: both certs are listed under the same name, and text mode gives no way to tell them apart.

Suggested fix: keep only certs whose SANs intersect the current publicIps/domain and whose CA directory matches staging. At the least, print the issuer and mark expired entries.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 189d3c6eaa

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/reachability/state.ts Outdated
// Status must keep working when this file is damaged: it only holds the last check results.
async function readRuntimeStatus(dataPath: string): Promise<ReachabilityRuntimeStatus | null> {
try {
return ((await readJsonIfExists(reachabilityRuntimeStatusPath(dataPath))) as ReachabilityRuntimeStatus | undefined) ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate runtime status before exposing it

When runtime-status.json contains valid JSON with a damaged shape, such as a missing or non-array lastIpCheck.publicIps, this cast accepts it and the human status command subsequently throws on publicIps.join(...). The stated resilience currently covers only JSON parse failures; validate the runtime fields and return null for malformed content so bitsocial reachability status continues working.

Useful? React with 👍 / 👎.

Comment on lines +35 to +38
if (status.lastChecks?.lastIpCheck) {
const { at, publicIps } = status.lastChecks.lastIpCheck;
this.log(`Last IP check: ${at} (${publicIps.join(", ") || "no public IP found"})`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Print domain and Caddy checks in human status

For domain-only and reverse-proxy configurations, the daemon can populate lastChecks.domains and lastChecks.caddy without setting lastIpCheck, but the normal command prints none of that health information even though its description promises the last checks. Only --json exposes these fields, leaving interactive users unable to see domain failures or whether Caddy is running.

Useful? React with 👍 / 👎.

reachability/config.json holds the operator's choices and refuses to load a
newer version rather than drop its fields. The daemon's check results go in a
separate file so they cannot overwrite a setup in progress.
…e status

Caddy keeps certificates for old addresses and other CAs until they expire, so
status now filters to the configured CA and current names. A damaged runtime
status file reads as no checks yet, and the text output prints domain and Caddy
checks.
@Rinse12

Rinse12 commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

Consolidated into #208.

@Rinse12 Rinse12 closed this Nov 2, 2026
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.

1 participant