Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions src/auth-providers/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,24 @@ export function extractEnvPairs(content: string): Map<string, string> {
return out;
}

// Refresh stale platform defaults in the user's .env.local. If the user's
// value matches the manifest's literal default AND the platform now has a
// real value (e.g., cloud DATABASE_URL), overwrite the line. User-customized
// values (anything that differs from the manifest default) are preserved.
//
// One extra case beyond "matches the default": the cloud's
// `database-connection-string` endpoint used to return a placeholder URL
// like `postgresql://postgres:********@host/db?sslmode=require` before
// 0.1.72 spliced the real password in. Users linked under that older CLI
// still have the masked URL in their .env.local. The masked value can
// never authenticate to Postgres, so it's clearly stale — detect any
// `:****@` (one or more `*`) password and refresh from the platform.
// Exported for unit testing.
// The cloud's `database-connection-string` endpoint used to return a
// placeholder URL like `postgresql://postgres:********@host/db?sslmode=require`
// before 0.1.72 spliced the real password in. Users linked under that older
// CLI still have the masked URL in their .env.local. The masked value can
// never authenticate to Postgres, so it's clearly stale — any `:****@` (one
// or more `*`) password is refreshed from the platform.
const MASKED_PASSWORD_PATTERN = /:\*+@/;

/**
* Refresh stale platform defaults in the user's .env.local. If the user's
* value matches the manifest's literal default AND the platform now has a
* real value (e.g., cloud DATABASE_URL), overwrite the line. User-customized
* values (anything that differs from the manifest default) are preserved.
* A masked-password URL (see `MASKED_PASSWORD_PATTERN`) is also treated as
* stale even when it differs from the default.
*
* Exported for unit testing.
*/
export function refreshStaleEnvDefaults(
existing: string,
manifestDefaults: Map<string, string>,
Expand Down Expand Up @@ -364,13 +368,8 @@ export async function applyAuthProvider(
await fs.writeFile(envLocalPath, filled + '\n');
result.envLocalWritten = true;
} else {
// Two passes:
// 1. Refresh stale platform defaults in-place. If the user's existing
// value matches the manifest's literal default AND we now have a
// real platform value (e.g., they re-linked against a cloud project
// after the CLI started fetching DATABASE_URL), update the line.
// User-customized values are preserved untouched.
// 2. Append any genuinely new keys at the end.
// Two passes: refresh stale platform defaults in place (see
// refreshStaleEnvDefaults), then append any genuinely new keys.
const manifestDefaults = extractEnvPairs(manifest.envExampleAppend);
const platformValues = extractEnvPairs(filled);
const { updated, refreshed } = refreshStaleEnvDefaults(existingLocal, manifestDefaults, platformValues);
Expand Down
3 changes: 2 additions & 1 deletion src/commands/branch/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const POLL_INTERVAL_MS = 3_000;
// now covers the observed range with headroom.
const POLL_TIMEOUT_MS = 15 * 60 * 1_000;
// Once the control plane says ready, wait for the data plane too. Until this
// passes, every subsequent command against the branch fails.
// passes, every subsequent command against the branch fails. The ready-to-
// serving measurements above come from InsForge/InsForge#1790.
const HEALTH_TIMEOUT_MS = 10 * 60 * 1_000;
const HEALTH_INTERVAL_MS = 5_000;
// Tolerance for clock skew when deciding whether a branch is the one we just
Expand Down
1 change: 0 additions & 1 deletion src/commands/config/apply.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// CLI/src/commands/config/apply.ts
import type { Command } from 'commander';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
Expand Down
1 change: 0 additions & 1 deletion src/commands/config/export.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// CLI/src/commands/config/export.ts
import type { Command } from 'commander';
import { writeFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
Expand Down
1 change: 0 additions & 1 deletion src/commands/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// CLI/src/commands/config/index.ts
import type { Command } from 'commander';
import { registerConfigExportCommand } from './export.js';
import { registerConfigPlanCommand } from './plan.js';
Expand Down
1 change: 0 additions & 1 deletion src/commands/config/plan.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// CLI/src/commands/config/plan.ts
import type { Command } from 'commander';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
Expand Down
5 changes: 4 additions & 1 deletion src/commands/deployments/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ export async function pollDeployment(
// responses other than the retryable ones are terminal. Gateway 5xx
// responses on the status endpoint are transient — the deployment itself
// may still succeed — so keep polling, same as network-level fetch
// failures, which `ossFetch` now tags as such. Anything left unclassified
// failures, which `ossFetch` tags as such. Anything left unclassified
// (a malformed status body failing `.json()`, a missing `status` field)
// is a bug, not an outage: it would fail identically on every retry, so
// it surfaces now instead of at the end of the poll window.
Expand All @@ -327,6 +327,9 @@ export async function pollDeployment(
}
// Transient: keep polling, but remember why the read failed so that an
// outage lasting the whole window is not reported as a plain timeout.
// The `formatFetchError` arm is unreachable since fbffb74: `ossFetch`
// wraps raw fetch rejections into a CLIError, and `isTransientApiError`
// returns false for anything else (see errors.ts).
lastTransientError =
err instanceof CLIError ? err.message : formatFetchError(err, 'the deployment API');
}
Expand Down
3 changes: 1 addition & 2 deletions src/lib/api/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export async function platformFetch(
const fullUrl = `${baseUrl}${path}`;
if (process.env.INSFORGE_DEBUG) {
console.error(`[DEBUG] ${fetchOptions.method ?? 'GET'} ${fullUrl}`);
// Redact the bearer credential. For direct-key sessions it is now the
// Redact the bearer credential. For direct-key sessions it is the
// long-lived, non-rotating uak_ (not a short-lived JWT), so never write it
// in full — even behind the debug flag. Match the auth header in ANY case,
// since a caller-provided lowercase `authorization` survives the spread.
Expand Down Expand Up @@ -253,7 +253,6 @@ export type DiagnosticEventHandler = (event: DiagnosticSSEEvent) => void;

/**
* Stream diagnostic analysis via SSE. Calls `onEvent` for each SSE event.
* Returns the raw Response so the caller can handle errors before streaming.
*/
export async function streamDiagnosticAnalysis(
payload: DiagnosticRequest,
Expand Down
6 changes: 3 additions & 3 deletions src/lib/command-telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { CLIError } from './errors.js';
* Telemetry must never affect command behavior, so everything is wrapped in a
* try/catch and failures are swallowed.
*
* IMPORTANT: only pass non-sensitive metadata as `properties` — counts,
* booleans, enums, exit/status codes. Never SQL, file contents, credentials,
* resource names, or other user-entered free text.
* Only pass non-sensitive metadata as `properties` — counts, booleans, enums,
* exit/status codes. Never SQL, file contents, credentials, resource names, or
* other user-entered free text.
*/
export type CommandTelemetry = Record<
string,
Expand Down
8 changes: 3 additions & 5 deletions src/lib/config-capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// CLI/src/lib/config-capabilities.ts
//
// Capability detection by metadata-shape probing.
//
// InsForge backends evolve independently per project. A user's CLI is always
Expand All @@ -13,9 +11,9 @@
// simply omit the key. The CLI infers support from presence/absence — no
// version handshake, no new server endpoint.
//
// IMPORTANT: probe the RAW JSON, never the Zod-parsed object. Zod's
// `.default([])` on the consumer schema would silently fill in absent fields
// and erase the signal we're checking for.
// Probe the RAW JSON, never the Zod-parsed object. Zod's `.default([])` on
// the consumer schema would silently fill in absent fields and erase the
// signal we're checking for.
//
// Server contract (documented in shared-schemas/metadata.schema.ts): when a
// backend doesn't yet support a TOML-relevant field, its `/api/metadata`
Expand Down
2 changes: 0 additions & 2 deletions src/lib/config-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// CLI/src/lib/config-metadata.ts
//
// Single source of truth for converting /api/metadata's raw JSON response
// into the shapes the rest of the CLI consumes:
// - liveFromMetadata → LiveConfig for the diff layer (apply, plan)
Expand Down
2 changes: 0 additions & 2 deletions src/lib/config-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// CLI/src/lib/config-schema.ts

import { validateSensitiveString } from './config-secrets.js';

/**
Expand Down
2 changes: 0 additions & 2 deletions src/lib/config-secrets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// CLI/src/lib/config-secrets.ts
//
// Sensitive-field validation for insforge.toml.
//
// Sensitive fields (OAuth client_secret, SMTP password, S3 secret key, etc.)
Expand Down
7 changes: 3 additions & 4 deletions src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@ export class ProjectNotLinkedError extends CLIError {
* budget;
* - anything that is not a CLIError at all. That is an UNCLASSIFIED throw —
* a `res.json()` parse failure on a mangled body, or a plain bug — and a
* poll loop must not spin on it for its whole budget. `platformFetch`
* wraps every transport and HTTP failure into a CLIError, so nothing real
* is lost. `ossFetch` does NOT wrap raw fetch rejections, so its callers
* handle that case themselves (see the deployment poller).
* poll loop must not spin on it for its whole budget. `platformFetch` and
* `ossFetch` wrap every transport and HTTP failure into a CLIError, so
* nothing real is lost.
*/
export function isTransientApiError(err: unknown): boolean {
if (!(err instanceof CLIError)) return false;
Expand Down
12 changes: 6 additions & 6 deletions src/lib/flyctl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ function ensureFlyTomlStub(opts: { dir: string; appId: string; region: string; p
* and returns the resulting image ref. Does NOT launch a machine — the cloud
* backend handles that step (it owns DB state and quota enforcement).
*
* IMPORTANT: returns the **digest-pinned** ref `<app>@sha256:<digest>`, not
* the tag-based `<app>:<label>` ref. buildkit's `--build-only --push` lands
* the manifest in the remote builder's namespace, and Fly's registry then
* aliases the user-app namespace to it by content digest. The bare tag does
* not always resolve (and racily fails on Fly's side as MANIFEST_UNKNOWN),
* but the digest always does. The cloud's launchMachine accepts either form.
* Returns the **digest-pinned** ref `<app>@sha256:<digest>`, not the tag-based
* `<app>:<label>` ref. buildkit's `--build-only --push` lands the manifest in
* the remote builder's namespace, and Fly's registry then aliases the user-app
* namespace to it by content digest. The bare tag does not always resolve (and
* racily fails on Fly's side as MANIFEST_UNKNOWN), but the digest always does.
* The cloud's launchMachine accepts either form.
*
* If the user's directory has no fly.toml, we write a stub before invoking
* flyctl and remove it after. flyctl needs the stub on a fresh app with no
Expand Down
Loading