From 0ee39d2eba081c2f8964f4ece11f380426328f48 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 17:55:47 +0000 Subject: [PATCH 1/6] feat(fork): build with official T3 Connect public config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fork builds compiled T3 Connect out because upstream injects its four public values (Clerk publishable key, JWT template, CLI OAuth client id, relay URL) from release-time CI variables this fork does not have. Track those values — read from the published t3@0.0.30 CLI, public identifiers by upstream's own docs — in a repository-root .env, force-added past .gitignore so every clone and worktree builds at parity with official releases against the hosted relay. .env.local stays gitignored and outranks it for personal or self-hosted-relay overrides. Registered as t3-connect-official-config in the fork manifest, with a guard pinning the .env's key set to exactly the four public values (so secret creep fails CI), asserting the file stays tracked, and exercising upstream's public-config loader against the repo root. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018u3xDVxSAq2CyaDVhhB2sg --- .env | 14 ++++ .fork/customizations.yaml | 40 ++++++++++ .fork/notes/FORK-CUSTOMIZATION-DECISIONS.md | 26 ++++++ .../t3ConnectOfficialConfig.test.ts | 80 +++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 .env create mode 100644 apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts diff --git a/.env b/.env new file mode 100644 index 00000000000..ba98813b92b --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +# fork-owned — see .fork/customizations.yaml#t3-connect-official-config +# +# Official T3 Connect public configuration, identical to what upstream bakes +# into every released artifact (read from the published npm CLI, t3@0.0.30). +# These are public identifiers, not secrets — see docs/cloud/t3-connect-clerk.md. +# Tracked on purpose (force-added past .gitignore's .env* rule) so every clone +# and worktree of this fork builds at T3 Connect parity with official releases. +# +# Personal overrides belong in .env.local, which stays gitignored and wins. +# Never add server-side secrets (CLERK_SECRET_KEY, API tokens) to this file. +T3CODE_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsudDMuY29kZXMk +T3CODE_CLERK_JWT_TEMPLATE=t3-relay +T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=hzxSgY2cH10sDU2r +T3CODE_RELAY_URL=https://relay.t3.codes diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 53d2095d7b2..69227ff5228 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -1181,3 +1181,43 @@ - apps/web/src/components/ComposerPromptEditor.tsx verify: - apps/web/src/__fork_guards__/forkComposerShell.test.ts + +- id: t3-connect-official-config + intent: > + Fork builds carry the same T3 Connect public configuration as official + releases, so the cloud UI, the `t3 connect` CLI group, and the managed + tunnel all work against upstream's hosted relay out of the box. Upstream + injects four public values at release time from CI variables this fork + does not have (release.yml's relay_public_config job); the fork instead + tracks them in a repository-root .env, which scripts/lib/public-config.ts + already reads for every dev run and build — with the right precedence + built in, since process env and .env.local both outrank it, so a personal + or self-hosted-relay override stays possible and stays gitignored. The + file is force-added past .gitignore's .env* rule on purpose: tracked + files are exempt from ignore rules, and upstream never ships a .env, so + a sync can never conflict with it. The values are public identifiers, + not secrets (docs/cloud/t3-connect-clerk.md) — the same ones baked into + every official artifact; they were read from the published npm CLI + (t3@0.0.30). The .env must never grow server-side secrets or tokens; the + guard pins its key set to exactly these four so secret creep fails CI. + Two neighbouring customizations make keyed fork builds safe rather than + accidental: fork-clerk-launch-resilience's keyless skip only engages + when no publishable key is baked and keyed builds keep upstream's Clerk + bridge behavior exactly, and fork-app-identity deliberately keeps the + shared t3code:// scheme, which is the desktop OAuth callback Clerk's + redirect allowlist expects. Relay access itself remains gated by the + user's own Clerk account, which is what authorizes use of the hosted + service. + tier: 1 + files: + - .env + shadows: [] + watch: + # Upstream's loader: the one place the repo-root .env is read and + # projected into VITE_*/EXPO_PUBLIC_* aliases. If upstream renames the + # canonical variables or stops reading .env, this customization dies + # silently — the guard exercises the loader against the real repo root + # so that failure turns CI red instead. + - scripts/lib/public-config.ts + verify: + - apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts diff --git a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md index 4d38b5bac10..f69ec2a9b58 100644 --- a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md +++ b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md @@ -185,3 +185,29 @@ Related deep-dives that predate this file and stay where they are: editor stays unclamped with `overflow-y: hidden` so Geist ink cannot inflate a scroll container. The guard's `not.toContain("data-composer-prompt-scrollable")` is a regression fence against bringing the toggle back, not an unexplained ban. + +## t3-connect-official-config + +- The goal was stated as "parity with official releases on top of the fork's changes", which ruled + out the two heavier options up front: self-hosting the relay (`infra/relay` — own Clerk app, + Cloudflare, PlanetScale; a second production system to operate for zero parity gain) and asking + every clone to hand-author an untracked `.env` (works once, silently absent in the next worktree + or clone, and this fork's contributions largely come from fresh agent worktrees). +- The four values were extracted from the published npm CLI tarball (`t3@0.0.30`), where upstream's + release build inlines them (`VITE_CLERK_CLI_OAUTH_CLIENT_ID` etc. in the bundled web assets). + `app.t3.codes` would have shown the same values but is unreachable from the agent environment's + network policy. If upstream ever rotates them, the fix is re-extracting from the current release + and updating `.env` and the guard's `OFFICIAL_VALUES` together. +- Committing a file that `.gitignore` matches is deliberate, not an oversight: ignore rules bind + only untracked files, so one `git add -f` makes it a normal tracked file forever, upstream can + never conflict with a path it has never shipped, and `.env.local` (still ignored, higher + precedence in `scripts/lib/public-config.ts`) remains the escape hatch for anyone pointing a + checkout at a staging or self-hosted relay. The alternative — carrying the values as code-level + fallbacks — would have meant a Tier-4 fence in an upstream loader that syncs would fight over. +- No client code changes were needed, by construction: `fork-clerk-launch-resilience` only skips + the desktop Clerk bridge when the build is keyless, and `fork-app-identity` explicitly kept the + shared `t3code://` scheme, which is the redirect Clerk's desktop OAuth allowlist expects. The + packaged fork app signing in shares upstream's scheme contention caveat already recorded there. +- What this does not grant: relay access is authorized by the signed-in user's Clerk account + (waitlist/allowlist on upstream's instance), not by the baked values. A fork build with these + values but no approved account gets exactly what an official build gets — the sign-in flow. diff --git a/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts b/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts new file mode 100644 index 00000000000..7ec08ccb18b --- /dev/null +++ b/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts @@ -0,0 +1,80 @@ +// @effect-diagnostics nodeBuiltinImport:off +/** + * Fork guard — see `.fork/customizations.yaml#t3-connect-official-config`. + * + * The fork tracks upstream's official T3 Connect public values in a + * repository-root .env so source builds reach the hosted relay exactly like + * released artifacts do. Three ways this dies silently without a guard: the + * file drops out of the index (it matches .gitignore's .env* rule, so an + * untracked copy looks identical on disk), it grows values it must never + * carry (server-side secrets), or upstream's loader stops reading the + * repo-root .env / renames the canonical variables during a sync. + */ + +import * as NodeChildProcess from "node:child_process"; +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; +import * as NodeURL from "node:url"; +import * as NodeUtil from "node:util"; +import { describe, expect, it } from "vite-plus/test"; + +import { loadRepoEnv } from "../../../../scripts/lib/public-config.ts"; + +const repoRoot = NodePath.resolve( + NodeURL.fileURLToPath(new URL(".", import.meta.url)), + "../../../..", +); + +// The values upstream bakes into every official artifact (read from the +// published npm CLI, t3@0.0.30). Public identifiers, not secrets. +const OFFICIAL_VALUES = { + T3CODE_CLERK_PUBLISHABLE_KEY: "pk_live_Y2xlcmsudDMuY29kZXMk", + T3CODE_CLERK_JWT_TEMPLATE: "t3-relay", + T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: "hzxSgY2cH10sDU2r", + T3CODE_RELAY_URL: "https://relay.t3.codes", +} as const; + +describe("fork guard: t3-connect-official-config", () => { + it("keeps .env tracked, not merely present on disk", () => { + // .env matches .gitignore's .env* rule, so losing the index entry leaves + // a working tree that looks right while every fresh clone and worktree + // silently builds with T3 Connect compiled out. + expect(() => + NodeChildProcess.execSync("git ls-files --error-unmatch .env", { + cwd: repoRoot, + stdio: "pipe", + }), + ).not.toThrow(); + }); + + it("carries exactly the four official public values", () => { + const parsed = NodeUtil.parseEnv( + NodeFS.readFileSync(NodePath.join(repoRoot, ".env"), "utf8"), + ) as Record; + // Exact key-set equality is the secret-creep fence: a CLERK_SECRET_KEY or + // token added to a tracked env file would ship in the repository forever. + expect({ ...parsed }).toEqual(OFFICIAL_VALUES); + }); + + it("resolves through the build loader for every client surface", () => { + // baseEnv: {} isolates from ambient CI variables; a developer's local + // .env.local may override individual values, so this asserts presence + // and projection rather than exact contents. + const env = loadRepoEnv({ baseEnv: {}, repoRoot }); + const projected = [ + "T3CODE_CLERK_PUBLISHABLE_KEY", + "T3CODE_CLERK_JWT_TEMPLATE", + "T3CODE_CLERK_CLI_OAUTH_CLIENT_ID", + "T3CODE_RELAY_URL", + // Aliases the web/desktop and mobile builds consume. + "VITE_CLERK_PUBLISHABLE_KEY", + "VITE_CLERK_JWT_TEMPLATE", + "VITE_CLERK_CLI_OAUTH_CLIENT_ID", + "VITE_T3CODE_RELAY_URL", + "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY", + "EXPO_PUBLIC_CLERK_JWT_TEMPLATE", + ]; + const missing = projected.filter((name) => !env[name]?.trim()); + expect(missing).toEqual([]); + }); +}); From 04c72b51819694f5944b639b6360e0bda9e7dac2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 18:39:21 +0000 Subject: [PATCH 2/6] fix(fork): repair tests and docs for keyed T3 Connect builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tracked .env broke upstream's connectCliAuth tests, which model an unconfigured clone by leaving VITE_CLERK_CLI_OAUTH_CLIENT_ID unstubbed — no longer the ambient default once every build bakes the official id. Two fenced stubs blank it to restore the case the tests assert. Review follow-ups from #35: correct fork-clerk-launch-resilience's now-false premise (fork builds are keyed; the keyless skip remains the net for unconfigured checkouts, and keyed coverage already lives in DesktopClerk.test.ts's fenced key bake); fence a warning into .env.example against `cp .env.example .env` clobbering the tracked file; make the guard docblock honest about what it detects and record upstream value rotation as a known gap; distinguish a missing git binary from the tracking regression in the guard; and record the accepted residual risks (committable .env, production defaults in dev) in the decisions note. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018u3xDVxSAq2CyaDVhhB2sg --- .env.example | 6 +++ .fork/customizations.yaml | 39 +++++++++++++------ .fork/notes/FORK-CUSTOMIZATION-DECISIONS.md | 26 +++++++++++++ .../t3ConnectOfficialConfig.test.ts | 32 +++++++++------ apps/web/src/cloud/connectCliAuth.test.ts | 10 +++++ 5 files changed, 90 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 61cdd66d246..79bf4a5a2be 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ +# fork:begin t3-connect-official-config — see .fork/customizations.yaml#t3-connect-official-config +# FORK NOTE: unlike upstream, this repository TRACKS a repo-root .env carrying +# the official T3 Connect public values, so builds are configured by default. +# Do not `cp .env.example .env` — that clobbers the tracked file. Put personal +# overrides in .env.local instead (gitignored, higher precedence). +# fork:end t3-connect-official-config # Optional: T3 Connect source builds # Leave these unset to disable optional T3 Connect features in local source builds. # Release builds inject their public values at build time. Do not add server-side diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 69227ff5228..425787084e7 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -894,17 +894,26 @@ the app is ready. Fast machines win that race; CI runners lose it deterministically, and a cold local boot is the same dice roll (the v0.1.2 launch isolation gate caught it: the app exited before its server - ever started). Fork builds bake no Clerk publishable key, so the bridge - is guaranteed dead weight — DesktopClerk skips it outright when - desktopClerkFrontendApiHostname is undefined, logging a warning, and - main.ts registers both schemes' privileges synchronously at module load - (guaranteed to precede "ready"; the sole registrar on the skip path; - observed against the bundled Electron 41 that pre-ready re-registration - by a keyed build's bridge replaces the list harmlessly). Keyed builds - keep upstream's behavior exactly: bridge created, initialization and - cleanup failures both loud — the cleanup path's Effect.orDie is - deliberately untouched, because a real Clerk defect in a keyed build - must not hide behind a warning. The singleton-lock behavior in + ever started). A build with no baked Clerk publishable key has a bridge + that is guaranteed dead weight — historically every fork build; since + t3-connect-official-config, only a checkout without the tracked .env or + one that blanks the key via .env.local. DesktopClerk skips it outright + when desktopClerkFrontendApiHostname is undefined, logging a warning, + and main.ts registers both schemes' privileges synchronously at module + load (guaranteed to precede "ready"; the sole registrar on the skip + path; observed against the bundled Electron 41 that pre-ready + re-registration by a keyed build's bridge replaces the list harmlessly). + Packaged fork builds are now keyed, so they take upstream's bridge path + — the same path every official desktop release ships — and keep + upstream's behavior exactly: bridge created, initialization and cleanup + failures both loud — the cleanup path's Effect.orDie is deliberately + untouched, because a real Clerk defect in a keyed build must not hide + behind a warning. Keyed bridge behavior stays unit-covered by upstream's + DesktopClerk.test.ts, whose fenced hunk bakes a key in before import; + the keyless skip is covered by DesktopClerkForkSkip.test.ts; and the + launch race itself is only observable in a real packaged boot, which is + what fork-release.yml's launch isolation gate exercises on every build, + dry runs included. The singleton-lock behavior in DesktopClerk.configure is bridge-independent and preserved on both paths. tier: 4 @@ -1219,5 +1228,13 @@ # silently — the guard exercises the loader against the real repo root # so that failure turns CI red instead. - scripts/lib/public-config.ts + # Carries a fenced note telling developers NOT to `cp .env.example .env` + # in this fork — that documented upstream onboarding move would clobber + # the tracked file. An upstream rewrite of the example must re-seat it. + - .env.example + # Two fenced stubs blank the baked CLI OAuth client id: upstream models + # "unconfigured clone" by not stubbing the variable, which stopped being + # the ambient default once the tracked .env keyed every build. + - apps/web/src/cloud/connectCliAuth.test.ts verify: - apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts diff --git a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md index f69ec2a9b58..576f0ee24e8 100644 --- a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md +++ b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md @@ -211,3 +211,29 @@ Related deep-dives that predate this file and stay where they are: - What this does not grant: relay access is authorized by the signed-in user's Clerk account (waitlist/allowlist on upstream's instance), not by the baked values. A fork build with these values but no approved account gets exactly what an official build gets — the sign-in flow. +- Review follow-ups (PR #35). Accepted residual risk, named rather than implied: tracking `.env` + removes the structural impossibility of committing it, and `.env` is the file most likely to + receive a `CLERK_SECRET_KEY` by habit. The guard's exact-key-set assertion is the in-repo fence, + but it is CI-only and post-push; GitHub push protection / secret scanning on the fork is the + control actually positioned to catch it pre-push, and should be confirmed enabled in repository + settings (not verifiable from a checkout). Second recorded gap: the guard's value assertion + compares the tracked `.env` against a constant maintained in the same commits, so it catches + copy-drift and secret creep, not upstream rotating the values — if rotation happens, both copies + agree, CI stays green, and the failure surfaces as a dead relay handshake at runtime. A live + probe was considered and declined; guards do not do network. +- Identity consequences of parity, stated out loud: dev runs (`vp run dev`, throwaway `test-t3-app` + worktrees) are now keyed against upstream's production Clerk instance and relay by default, where + T3 Connect was previously compiled out; and with `T3CODE_HOSTED_APP_URL` unset the `t3 connect` + CLI flow round-trips through upstream's hosted app, so the consent screen names upstream's + application. Both are exactly what "parity with official releases" means, and both are + overridable per-checkout via `.env.local`. +- Keyed desktop launch path: baking a key moves packaged fork builds off + fork-clerk-launch-resilience's skip path and onto upstream's bridge path — deliberately, since + that is the path every official desktop release ships. main.ts's synchronous pre-ready privilege + registration remains in force on keyed builds too (strictly earlier than upstream's own + bridge-time registration, observed harmless on the bundled Electron 41), so a keyed fork build + is no worse-positioned in the ready race than an official build. Unit coverage of the keyed + bridge already existed before this change in upstream's DesktopClerk.test.ts, whose fenced hunk + bakes a key in before import; the launch race itself is only observable in a packaged boot, + which fork-release.yml's launch isolation gate exercises — run it with dry_run=true to prove a + keyed build before tagging a release. diff --git a/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts b/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts index 7ec08ccb18b..d789c466596 100644 --- a/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts +++ b/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts @@ -4,11 +4,17 @@ * * The fork tracks upstream's official T3 Connect public values in a * repository-root .env so source builds reach the hosted relay exactly like - * released artifacts do. Three ways this dies silently without a guard: the - * file drops out of the index (it matches .gitignore's .env* rule, so an - * untracked copy looks identical on disk), it grows values it must never - * carry (server-side secrets), or upstream's loader stops reading the - * repo-root .env / renames the canonical variables during a sync. + * released artifacts do. What these tests catch: the file dropping out of + * the index (it matches .gitignore's .env* rule, so an untracked copy looks + * identical on disk), secret creep or copy-drift in its contents (the + * key-set assertion compares against a second hand-maintained copy of the + * same fact, so it only fails when the two copies disagree), and upstream's + * loader ceasing to read the repo-root .env or renaming the canonical + * variables during a sync. Known gap, deliberate and recorded in + * .fork/notes/FORK-CUSTOMIZATION-DECISIONS.md: upstream rotating the values + * out from under us is NOT detected here — both copies keep agreeing while + * the relay handshake dies at runtime. Nothing short of a live probe would + * catch that, and guards do not do network. */ import * as NodeChildProcess from "node:child_process"; @@ -38,13 +44,15 @@ describe("fork guard: t3-connect-official-config", () => { it("keeps .env tracked, not merely present on disk", () => { // .env matches .gitignore's .env* rule, so losing the index entry leaves // a working tree that looks right while every fresh clone and worktree - // silently builds with T3 Connect compiled out. - expect(() => - NodeChildProcess.execSync("git ls-files --error-unmatch .env", { - cwd: repoRoot, - stdio: "pipe", - }), - ).not.toThrow(); + // silently builds with T3 Connect compiled out. spawnSync rather than a + // bare execSync throw: a missing git binary must report itself as an + // environment problem, not masquerade as the tracking regression. + const result = NodeChildProcess.spawnSync("git", ["ls-files", "--error-unmatch", ".env"], { + cwd: repoRoot, + encoding: "utf8", + }); + expect(result.error, "git was not runnable in this environment").toBeUndefined(); + expect(result.status, `.env is not tracked by git: ${result.stderr}`).toBe(0); }); it("carries exactly the four official public values", () => { diff --git a/apps/web/src/cloud/connectCliAuth.test.ts b/apps/web/src/cloud/connectCliAuth.test.ts index 61d854eb6ab..46e98f6042b 100644 --- a/apps/web/src/cloud/connectCliAuth.test.ts +++ b/apps/web/src/cloud/connectCliAuth.test.ts @@ -18,6 +18,12 @@ describe("connectCliAuth", () => { vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", TEST_PUBLISHABLE_KEY); vi.stubEnv("VITE_CLERK_JWT_TEMPLATE", "t3-relay"); vi.stubEnv("VITE_T3CODE_RELAY_URL", "https://relay.example.com"); + // fork:begin t3-connect-official-config — see .fork/customizations.yaml#t3-connect-official-config + // The fork's tracked repo-root .env bakes the official CLI OAuth client id + // into every build, so leaving it unstubbed no longer models an + // unconfigured clone; blank it to restore the case this test asserts. + vi.stubEnv("VITE_CLERK_CLI_OAUTH_CLIENT_ID", ""); + // fork:end t3-connect-official-config expect(hasConnectCliAuthConfig()).toBe(false); vi.stubEnv("VITE_CLERK_CLI_OAUTH_CLIENT_ID", "oauthapp_123"); @@ -48,6 +54,10 @@ describe("connectCliAuth", () => { it("returns null when the CLI OAuth client id is not configured", () => { vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", TEST_PUBLISHABLE_KEY); + // fork:begin t3-connect-official-config — see .fork/customizations.yaml#t3-connect-official-config + // Blank the baked client id from the fork's tracked .env; see above. + vi.stubEnv("VITE_CLERK_CLI_OAUTH_CLIENT_ID", ""); + // fork:end t3-connect-official-config expect( buildConnectCliClerkAuthorizeUrl({ state: "state-1", challenge: "challenge-1" }), ).toBeNull(); From 19d7c57f65aa4889bb412c419956ef2be3685644 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 19:37:29 +0000 Subject: [PATCH 3/6] fix(fork): close second-review gaps around the tracked .env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guard the .env.example fork note (an upstream sync could drop the only warning against `cp .env.example .env`, and watch: only reports drift); give the key-set assertion a failure message naming the two legitimate writers that trip it (relay deploy's reconcileRootEnv, optional OTLP values) and pointing them at .env.local; correct every claim that .env.local can turn T3 Connect off — the loader's firstNonEmpty treats blank as unset and falls through, so disabling means temporarily deleting the tracked .env; and record the reconcileRootEnv token-write hazard in the decisions note with deploy.ts drift-watched. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018u3xDVxSAq2CyaDVhhB2sg --- .env | 5 +++- .env.example | 4 +++- .fork/customizations.yaml | 12 +++++++++- .fork/notes/FORK-CUSTOMIZATION-DECISIONS.md | 23 ++++++++++++++++++- .../t3ConnectOfficialConfig.test.ts | 22 +++++++++++++++++- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/.env b/.env index ba98813b92b..b980d233eee 100644 --- a/.env +++ b/.env @@ -6,7 +6,10 @@ # Tracked on purpose (force-added past .gitignore's .env* rule) so every clone # and worktree of this fork builds at T3 Connect parity with official releases. # -# Personal overrides belong in .env.local, which stays gitignored and wins. +# Personal overrides belong in .env.local, which stays gitignored and wins — +# by REPLACING a value, not blanking it: the loader treats empty as unset and +# falls through to this file. To build with T3 Connect off, temporarily delete +# this file in your checkout (do not commit the deletion). # Never add server-side secrets (CLERK_SECRET_KEY, API tokens) to this file. T3CODE_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsudDMuY29kZXMk T3CODE_CLERK_JWT_TEMPLATE=t3-relay diff --git a/.env.example b/.env.example index 79bf4a5a2be..dd221bf43aa 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,9 @@ # FORK NOTE: unlike upstream, this repository TRACKS a repo-root .env carrying # the official T3 Connect public values, so builds are configured by default. # Do not `cp .env.example .env` — that clobbers the tracked file. Put personal -# overrides in .env.local instead (gitignored, higher precedence). +# overrides in .env.local instead (gitignored, higher precedence). Overrides +# REPLACE values; a blank value falls through to the tracked .env. To build +# with T3 Connect off entirely, temporarily delete the tracked .env locally. # fork:end t3-connect-official-config # Optional: T3 Connect source builds # Leave these unset to disable optional T3 Connect features in local source builds. diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 425787084e7..20b60bd0646 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -1201,7 +1201,11 @@ tracks them in a repository-root .env, which scripts/lib/public-config.ts already reads for every dev run and build — with the right precedence built in, since process env and .env.local both outrank it, so a personal - or self-hosted-relay override stays possible and stays gitignored. The + or self-hosted-relay override stays possible and stays gitignored. An + override REPLACES a value; it cannot blank one — the loader resolves + through firstNonEmpty, which treats empty as unset and falls through to + the tracked file. Building with T3 Connect off means temporarily + deleting the tracked .env in that checkout, never committing it. The file is force-added past .gitignore's .env* rule on purpose: tracked files are exempt from ignore rules, and upstream never ships a .env, so a sync can never conflict with it. The values are public identifiers, @@ -1236,5 +1240,11 @@ # "unconfigured clone" by not stubbing the variable, which stopped being # the ambient default once the tracked .env keyed every build. - apps/web/src/cloud/connectCliAuth.test.ts + # reconcileRootEnv writes relay public config — OTLP tracing tokens + # included — into the repo-root .env, which this customization made + # committable. Unfenced (the fork never runs relay deploys), but any + # upstream rework of where that write lands must be re-checked against + # the tracked file. See the decisions note. + - infra/relay/scripts/deploy.ts verify: - apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts diff --git a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md index 576f0ee24e8..fe83648278b 100644 --- a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md +++ b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md @@ -226,7 +226,14 @@ Related deep-dives that predate this file and stay where they are: T3 Connect was previously compiled out; and with `T3CODE_HOSTED_APP_URL` unset the `t3 connect` CLI flow round-trips through upstream's hosted app, so the consent screen names upstream's application. Both are exactly what "parity with official releases" means, and both are - overridable per-checkout via `.env.local`. + overridable per-checkout via `.env.local` — with a real limit the second review caught: + `.env.local` can replace a value, never blank one. `firstNonEmpty` in the loader treats an empty + string as absent and falls through to the tracked `.env`, and the resolved projection is spread + last, so `T3CODE_CLERK_PUBLISHABLE_KEY=` in `.env.local` (or the process env) leaves the build + keyed. Turning T3 Connect off in a checkout means temporarily deleting the tracked `.env` — the + guard suite will complain locally until it is restored, which is the visible reminder not to + commit the deletion. A fenced loader opt-out was considered and declined: it would put a fork + hunk in upstream's build bootstrap to serve a local-experiment case that deletion already covers. - Keyed desktop launch path: baking a key moves packaged fork builds off fork-clerk-launch-resilience's skip path and onto upstream's bridge path — deliberately, since that is the path every official desktop release ships. main.ts's synchronous pre-ready privilege @@ -237,3 +244,17 @@ Related deep-dives that predate this file and stay where they are: bakes a key in before import; the launch race itself is only observable in a packaged boot, which fork-release.yml's launch isolation gate exercises — run it with dry_run=true to prove a keyed build before tagging a release. +- Second review's sharpest catch: `infra/relay/scripts/deploy.ts`'s `reconcileRootEnv` writes + relay public config — including `T3CODE_MOBILE_OTLP_TRACES_TOKEN` and + `T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN`, the latter `::add-mask::`-ed in upstream's own release + workflow — into the repo-root `.env`, which this customization made committable. In-repo + automation of the exact failure the key-set guard fences. Accepted un-fenced because this fork + never runs relay deploys (self-hosting was rejected above) and a fenced redirect to `.env.local` + would also have to carry fenced edits to upstream's `deploy.test.ts`, which asserts the `.env` + target. If the fork ever self-hosts, redirecting that write is the first change to make. The + guard's failure message now names both legitimate writers (deploy output, optional OTLP values) + and points them at `.env.local`, so tripping it reads as "wrong file", not "you leaked a secret". +- The `.env.example` fork note is now asserted by the guard suite (`toContain` on the fence + marker), closing the fourth finding: `watch:` reports drift after a sync merges, but only a + failing test stops `cp .env.example .env` from shipping upstream's placeholders over the live + values via a routine `git commit -a`. diff --git a/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts b/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts index d789c466596..b4999433b00 100644 --- a/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts +++ b/apps/web/src/__fork_guards__/t3ConnectOfficialConfig.test.ts @@ -61,7 +61,27 @@ describe("fork guard: t3-connect-official-config", () => { ) as Record; // Exact key-set equality is the secret-creep fence: a CLERK_SECRET_KEY or // token added to a tracked env file would ship in the repository forever. - expect({ ...parsed }).toEqual(OFFICIAL_VALUES); + // Two known writers trip this legitimately — hand-added optional values + // that .env.example documents (mobile OTLP tracing), and the relay deploy + // script's reconcileRootEnv, which appends tracing tokens here. Both + // belong in .env.local; the message says so because "secret creep" alone + // sent people hunting for a leak they hadn't caused. + expect( + { ...parsed }, + "the tracked .env carries ONLY the four official T3 Connect public values; " + + "move optional or machine-local values (OTLP tracing, relay deploy output) " + + "to .env.local, which is gitignored and takes precedence", + ).toEqual(OFFICIAL_VALUES); + }); + + it("keeps the fork note in .env.example that steers people off `cp .env.example .env`", () => { + // That note is the only thing standing between upstream's documented + // onboarding move and a commit that replaces the four live values with + // upstream's commented-out placeholders. watch: reports drift after the + // fact; this fails the build instead. + const example = NodeFS.readFileSync(NodePath.join(repoRoot, ".env.example"), "utf8"); + expect(example).toContain("fork:begin t3-connect-official-config"); + expect(example).toContain(".env.local"); }); it("resolves through the build loader for every client surface", () => { From d8d2c8c3e62d8b06efbd9640aed71ef105a568c3 Mon Sep 17 00:00:00 2001 From: noah Date: Wed, 29 Jul 2026 22:42:48 -0400 Subject: [PATCH 4/6] docs(fork): release notes no longer claim T3 Connect is unavailable This branch configures fork builds with the official T3 Connect public values, which made the release body's 'cloud features are not configured and will be unavailable' paragraph false. Say what is now true: sign-in and remote access work against upstream's hosted relay, and local use still needs no account. Co-Authored-By: Claude Fable 5 --- .github/workflows/fork-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index 2b20d34a6b1..ba14698f92b 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -195,8 +195,8 @@ jobs: so copy anything you need out of the prompt stash before upgrading. Details: the [v0.1.6 release notes](https://github.com/NoahHendrickson/t3code/releases/tag/v0.1.6). - Cloud features (T3 Connect remote access and sign-in) are not - configured in fork builds and will be unavailable. Everything local - works normally. + T3 Connect (remote access and sign-in) is configured with the same + public values as official T3 Code releases and uses upstream's + hosted relay. Everything local works without signing in, as always. env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 7eedbbb6ccbc475da6e388128a0a358ed0a08502 Mon Sep 17 00:00:00 2001 From: noah Date: Wed, 29 Jul 2026 22:49:31 -0400 Subject: [PATCH 5/6] fix(fork): keyed builds no longer die on the bridge's scheme re-registration The v0.1.7 dry run's launch isolation gate caught the keyed packaged build crashing at launch: @clerk/electron's createClerkBridge unconditionally calls protocol.registerSchemesAsPrivileged, which throws once Electron is ready, and Effect layer construction trails the ready event on a packaged boot. main.ts already registers the identical privilege set for both schemes at module load, so make it the sole registrar on every path: createDesktopClerkBridge no-ops the registrar for the duration of the bridge call and restores it after. Covered by a new fork-owned unit test, pinned by the launch-resilience guard, recorded in the manifest. Co-Authored-By: Claude Fable 5 --- .fork/customizations.yaml | 37 ++++---- apps/desktop/src/app/DesktopClerk.ts | 51 ++++++++--- ...sktopClerkForkRegistrarSuppression.test.ts | 85 +++++++++++++++++++ apps/desktop/src/main.ts | 11 +-- .../forkClerkLaunchResilience.test.ts | 21 ++++- 5 files changed, 169 insertions(+), 36 deletions(-) create mode 100644 apps/desktop/src/app/DesktopClerkForkRegistrarSuppression.test.ts diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 8a47e650d12..50af7c55b4d 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -935,25 +935,30 @@ one that blanks the key via .env.local. DesktopClerk skips it outright when desktopClerkFrontendApiHostname is undefined, logging a warning, and main.ts registers both schemes' privileges synchronously at module - load (guaranteed to precede "ready"; the sole registrar on the skip - path; observed against the bundled Electron 41 that pre-ready - re-registration by a keyed build's bridge replaces the list harmlessly). - Packaged fork builds are now keyed, so they take upstream's bridge path - — the same path every official desktop release ships — and keep - upstream's behavior exactly: bridge created, initialization and cleanup - failures both loud — the cleanup path's Effect.orDie is deliberately - untouched, because a real Clerk defect in a keyed build must not hide - behind a warning. Keyed bridge behavior stays unit-covered by upstream's - DesktopClerk.test.ts, whose fenced hunk bakes a key in before import; - the keyless skip is covered by DesktopClerkForkSkip.test.ts; and the - launch race itself is only observable in a real packaged boot, which is - what fork-release.yml's launch isolation gate exercises on every build, - dry runs included. The singleton-lock behavior in - DesktopClerk.configure is bridge-independent and preserved on both - paths. + load (guaranteed to precede "ready") and is the sole registrar on every + path: createDesktopClerkBridge no-ops the registrar for the duration of + the createClerkBridge call and restores it after, because the bridge + would re-register the identical privilege set and that call throws + post-"ready". The suppression is load-bearing, not hygiene — packaged + fork builds are keyed since t3-connect-official-config, and the v0.1.7 + dry run's launch isolation gate caught the keyed build losing the ready + race and dying on the bridge's own registration (an earlier observation + that pre-ready re-registration is harmless was true but answered the + wrong case). Keyed builds otherwise keep upstream's behavior: bridge + created, initialization and cleanup failures both loud — the cleanup + path's Effect.orDie is deliberately untouched, because a real Clerk + defect in a keyed build must not hide behind a warning. Keyed bridge + behavior stays unit-covered by upstream's DesktopClerk.test.ts, whose + fenced hunk bakes a key in before import; the registrar suppression by + DesktopClerkForkRegistrarSuppression.test.ts; the keyless skip by + DesktopClerkForkSkip.test.ts; and the packaged boot itself by + fork-release.yml's launch isolation gate on every build, dry runs + included. The singleton-lock behavior in DesktopClerk.configure is + bridge-independent and preserved on both paths. tier: 4 files: - apps/desktop/src/app/DesktopClerkForkSkip.test.ts + - apps/desktop/src/app/DesktopClerkForkRegistrarSuppression.test.ts shadows: [] watch: - apps/desktop/src/main.ts diff --git a/apps/desktop/src/app/DesktopClerk.ts b/apps/desktop/src/app/DesktopClerk.ts index acd4c7349f5..0548f96fc55 100644 --- a/apps/desktop/src/app/DesktopClerk.ts +++ b/apps/desktop/src/app/DesktopClerk.ts @@ -1,5 +1,8 @@ import { createClerkBridge } from "@clerk/electron"; import { storage } from "@clerk/electron/storage"; +// fork:begin fork-clerk-launch-resilience — see .fork/customizations.yaml#fork-clerk-launch-resilience +import * as Electron from "electron"; +// fork:end fork-clerk-launch-resilience import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -72,14 +75,38 @@ export const desktopClerkFrontendApiHostname = resolveDesktopClerkFrontendApiHos ); export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolean) { - return createClerkBridge({ - storage: storage({ path: stateDir }), - passkeys: true, - renderer: { - scheme: ElectronProtocol.getDesktopScheme(isDevelopment), - host: ElectronProtocol.DESKTOP_HOST, - }, - }); + // fork:begin fork-clerk-launch-resilience — main.ts is the sole scheme registrar + // main.ts already registered both renderer schemes' privileges at module + // load with the exact set the bridge would ask for, so the bridge's own + // registerSchemesAsPrivileged call adds nothing — and throws once Electron + // is "ready", which layer construction can trail on a packaged boot (the + // v0.1.7 dry run's launch isolation gate died exactly there). No-op the + // registrar for the duration of the bridge call; everything else the + // bridge does (token persistence, OAuth transport, passkeys) is untouched. + // The optional reads are for unit tests, which import this module in plain + // Node where the electron shim exposes no protocol object. + const protocol = Electron.protocol as typeof Electron.protocol | undefined; + const registerSchemesAsPrivileged = protocol?.registerSchemesAsPrivileged; + if (protocol && registerSchemesAsPrivileged) { + protocol.registerSchemesAsPrivileged = () => {}; + } + try { + // fork:end fork-clerk-launch-resilience + return createClerkBridge({ + storage: storage({ path: stateDir }), + passkeys: true, + renderer: { + scheme: ElectronProtocol.getDesktopScheme(isDevelopment), + host: ElectronProtocol.DESKTOP_HOST, + }, + }); + // fork:begin fork-clerk-launch-resilience — restore the real registrar + } finally { + if (protocol && registerSchemesAsPrivileged) { + protocol.registerSchemesAsPrivileged = registerSchemesAsPrivileged; + } + } + // fork:end fork-clerk-launch-resilience } export const make = Effect.gen(function* () { @@ -92,9 +119,11 @@ export const make = Effect.gen(function* () { // deterministically; a cold local boot rolls the same dice). Skip the // bridge outright when there is no key: deterministic, and the renderer's // scheme privileges come from main.ts's synchronous registration, which is - // the sole registrar on this path. Keyed builds keep upstream's behavior - // exactly — including loud initialization AND cleanup failures, which must - // stay fatal there rather than hide behind a warning. The singleton-lock + // the sole registrar on this path. Keyed builds keep upstream's bridge — + // including loud initialization AND cleanup failures, which must stay + // fatal there rather than hide behind a warning — except its redundant + // scheme re-registration, which createDesktopClerkBridge above suppresses + // so a post-"ready" layer build cannot die on it. The singleton-lock // behavior in configure below is bridge-independent either way. if (desktopClerkFrontendApiHostname === undefined) { yield* Effect.logWarning( diff --git a/apps/desktop/src/app/DesktopClerkForkRegistrarSuppression.test.ts b/apps/desktop/src/app/DesktopClerkForkRegistrarSuppression.test.ts new file mode 100644 index 00000000000..7aef53fb1bb --- /dev/null +++ b/apps/desktop/src/app/DesktopClerkForkRegistrarSuppression.test.ts @@ -0,0 +1,85 @@ +/** + * Fork-owned — see `.fork/customizations.yaml#fork-clerk-launch-resilience`. + * + * Keyed builds create the Clerk bridge, and @clerk/electron's + * createClerkBridge unconditionally re-registers the renderer scheme's + * privileges — a call that throws once Electron is "ready", and layer + * construction can trail "ready" on a packaged boot (the v0.1.7 dry run's + * launch isolation gate died there). main.ts registers the same privilege + * set at module load, so createDesktopClerkBridge suppresses the bridge's + * redundant registration for the duration of the call and restores the real + * registrar after. Unlike DesktopClerkForkSkip.test.ts, this file bakes a + * key in; unlike DesktopClerk.test.ts, it mocks electron so the suppression + * has a real registrar to swap out. + */ + +import { assert, describe, it } from "@effect/vitest"; +import { beforeEach, vi } from "vite-plus/test"; + +const { createClerkBridgeMock, registerSchemesAsPrivilegedSpy, storageMock } = vi.hoisted(() => ({ + createClerkBridgeMock: vi.fn(), + registerSchemesAsPrivilegedSpy: vi.fn(), + storageMock: vi.fn(), +})); + +vi.hoisted(() => { + (globalThis as Record).__T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__ = + `pk_test_${btoa("clerk.t3.codes$")}`; +}); + +vi.mock("electron", () => ({ + protocol: { registerSchemesAsPrivileged: registerSchemesAsPrivilegedSpy }, +})); + +vi.mock("@clerk/electron", () => ({ + createClerkBridge: createClerkBridgeMock, +})); + +vi.mock("@clerk/electron/storage", () => ({ + storage: storageMock, +})); + +import * as Electron from "electron"; +import * as DesktopClerk from "./DesktopClerk.ts"; + +describe("DesktopClerk keyed-build registrar suppression", () => { + beforeEach(() => { + createClerkBridgeMock.mockReset(); + registerSchemesAsPrivilegedSpy.mockClear(); + storageMock.mockReset(); + storageMock.mockReturnValue({ + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }); + }); + + it("suppresses the bridge's scheme re-registration, then restores the registrar", () => { + const bridge = { cleanup: vi.fn() }; + createClerkBridgeMock.mockImplementation(() => { + // What @clerk/electron does unconditionally when given a renderer. + // Post-"ready" this throws in a real boot; suppression makes it a + // no-op instead. + Electron.protocol.registerSchemesAsPrivileged([]); + return bridge; + }); + + assert.equal(DesktopClerk.createDesktopClerkBridge("/tmp/t3-state", false), bridge); + assert.equal(registerSchemesAsPrivilegedSpy.mock.calls.length, 0); + + // The real registrar is back once the bridge call returns. + Electron.protocol.registerSchemesAsPrivileged([]); + assert.equal(registerSchemesAsPrivilegedSpy.mock.calls.length, 1); + }); + + it("restores the registrar even when the bridge throws", () => { + createClerkBridgeMock.mockImplementationOnce(() => { + throw new Error("bridge initialization failed"); + }); + + assert.throws(() => DesktopClerk.createDesktopClerkBridge("/tmp/t3-state", false)); + + Electron.protocol.registerSchemesAsPrivileged([]); + assert.equal(registerSchemesAsPrivilegedSpy.mock.calls.length, 1); + }); +}); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index f61f754eda6..abf9b51fb33 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -66,11 +66,12 @@ import * as DesktopWslEnvironment from "./wsl/DesktopWslEnvironment.ts"; // this synchronous module-load registration is the sole registrar on the // fork's own builds, and it is guaranteed to precede Electron's "ready" // event (module evaluation completes before the event loop can emit it). -// For keyed builds the bridge still registers the active scheme pre-ready; -// observed against the bundled Electron 41 framework binary that pre-ready -// re-registration replaces the scheme list rather than throwing (the binary -// carries only the post-ready error string, and browser_init's registration -// overwrites the linked binding's globals), so the overlap is harmless. +// Keyed builds create the Clerk bridge too, but its own registration is +// suppressed (createDesktopClerkBridge in DesktopClerk.ts): layer +// construction can trail "ready" on a packaged boot — the v0.1.7 dry run's +// launch isolation gate died on exactly that — and the bridge's privilege +// set is identical to this one, so this registration is the sole registrar +// on every path. // Both schemes are registered so no env sniffing is needed; the privilege // set mirrors @clerk/electron's. try/catch because this runs before the // Effect runtime and any observability: a throw here would otherwise be a diff --git a/apps/web/src/__fork_guards__/forkClerkLaunchResilience.test.ts b/apps/web/src/__fork_guards__/forkClerkLaunchResilience.test.ts index ae551b14212..443ff26a593 100644 --- a/apps/web/src/__fork_guards__/forkClerkLaunchResilience.test.ts +++ b/apps/web/src/__fork_guards__/forkClerkLaunchResilience.test.ts @@ -7,10 +7,12 @@ * races Electron's "ready" event; registerSchemesAsPrivileged throws once the * app is ready. CI runners lose that race deterministically — the v0.1.2 * launch isolation gate caught the app exiting before its server started. - * Two hunks close it: main.ts registers the scheme privileges synchronously - * at module load (the sole registrar for keyless builds), and DesktopClerk - * skips the bridge entirely when no publishable key is baked in, so keyless - * builds never enter the race and keyed builds keep upstream's loud + * Three hunks close it: main.ts registers the scheme privileges + * synchronously at module load, DesktopClerk skips the bridge entirely when + * no publishable key is baked in, and createDesktopClerkBridge suppresses + * the bridge's redundant re-registration in keyed builds (the v0.1.7 dry + * run's launch isolation gate died on it post-"ready") — so main.ts is the + * sole registrar on every path and keyed builds keep upstream's loud * failures. */ @@ -86,4 +88,15 @@ describe("fork guard: fork-clerk-launch-resilience", () => { // bridge's initialization error. expect(clerk).not.toContain('catchTag("DesktopClerkBridgeInitializationError"'); }); + + it("makes main.ts the sole scheme registrar on keyed builds too", () => { + const clerk = read(DESKTOP_CLERK); + // The bridge's own registration is suppressed for the duration of the + // createClerkBridge call and the real registrar restored in a finally. + // Losing the no-op reintroduces the post-"ready" throw the v0.1.7 dry + // run died on; losing the restore breaks any later registration. + expect(clerk).toContain("protocol.registerSchemesAsPrivileged = () => {}"); + expect(clerk).toContain("} finally {"); + expect(clerk).toContain("protocol.registerSchemesAsPrivileged = registerSchemesAsPrivileged"); + }); }); From 8d9e55a120ed00b89d5f4043253fe00f68e349a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 02:51:40 +0000 Subject: [PATCH 6/6] docs(fork): record the keyed launch-gate incident and correct the stale claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decisions note still argued a keyed fork build was "no worse-positioned in the ready race than an official build" — true about registration order, useless about the crash, and disproven by the first keyed dry-run gate (run 30508846869). Correct that bullet and add the fork-clerk-launch-resilience incident section: what failed, why the registrar no-op in 7eedbbb6 was chosen over an isReady() gate or catching the initialization error, and the known tradeoff if @clerk/electron ever changes its registered privilege set. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018u3xDVxSAq2CyaDVhhB2sg --- .fork/notes/FORK-CUSTOMIZATION-DECISIONS.md | 44 +++++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md index fe83648278b..55d2b96d28a 100644 --- a/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md +++ b/.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md @@ -236,14 +236,17 @@ Related deep-dives that predate this file and stay where they are: hunk in upstream's build bootstrap to serve a local-experiment case that deletion already covers. - Keyed desktop launch path: baking a key moves packaged fork builds off fork-clerk-launch-resilience's skip path and onto upstream's bridge path — deliberately, since - that is the path every official desktop release ships. main.ts's synchronous pre-ready privilege - registration remains in force on keyed builds too (strictly earlier than upstream's own - bridge-time registration, observed harmless on the bundled Electron 41), so a keyed fork build - is no worse-positioned in the ready race than an official build. Unit coverage of the keyed - bridge already existed before this change in upstream's DesktopClerk.test.ts, whose fenced hunk - bakes a key in before import; the launch race itself is only observable in a packaged boot, - which fork-release.yml's launch isolation gate exercises — run it with dry_run=true to prove a - keyed build before tagging a release. + that is the path every official desktop release ships. This entry originally argued a keyed + fork build was "no worse-positioned in the ready race than an official build" because main.ts's + pre-ready registration was strictly earlier than the bridge's own. True, and useless: the + bridge's own post-ready call still throws regardless of what was registered first, official + builds ship that dice roll to end users, and upstream runs no launch gate in CI to see it. The + fork's first keyed dry-run gate saw it immediately and failed the build; the incident and fix + live under `## fork-clerk-launch-resilience` below. Unit coverage of the keyed bridge already + existed in upstream's DesktopClerk.test.ts, whose fenced hunk bakes a key in before import; the + launch race itself is only observable in a packaged boot, which fork-release.yml's launch + isolation gate exercises — run it with dry_run=true to prove a keyed build before tagging a + release. - Second review's sharpest catch: `infra/relay/scripts/deploy.ts`'s `reconcileRootEnv` writes relay public config — including `T3CODE_MOBILE_OTLP_TRACES_TOKEN` and `T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN`, the latter `::add-mask::`-ed in upstream's own release @@ -258,3 +261,28 @@ Related deep-dives that predate this file and stay where they are: marker), closing the fourth finding: `watch:` reports drift after a sync merges, but only a failing test stops `cp .env.example .env` from shipping upstream's placeholders over the live values via a routine `git commit -a`. + +## fork-clerk-launch-resilience + +- The keyed-build story failed the moment it was actually tested. After t3-connect-official-config + keyed every fork build, the manifest was rewritten to say keyed builds keep upstream's bridge + behavior exactly, leaning on "pre-ready re-registration observed harmless on Electron 41". The + first dry-run launch gate on a keyed build (PR #35, run 30508846869) exited before the server + started: the macOS runner's layer construction trailed "ready", `createClerkBridge` called + `registerSchemesAsPrivileged` post-ready, and `DesktopClerkBridgeInitializationError` was + fatally loud — the v0.1.2 failure reproduced on the keyed path. "Observed harmless" had only + ever described boots that won the race; CI was the population of boots that lose it. +- The fix (7eedbbb6) makes main.ts the sole registrar on every path: `createDesktopClerkBridge` + no-ops `protocol.registerSchemesAsPrivileged` for exactly the duration of `createClerkBridge` + and restores it in a `finally`. Chosen over gating on `app.isReady()` — an isReady gate keeps + fast and slow boots on different code paths, and the losing path is the one CI never exercises — + and over catching the initialization error, which would swallow unknown causes along with the + one known one. Safe only because main.ts registers the identical privilege set the bridge would + have; the launch-resilience guard pins that set item by item, and a future @clerk/electron that + registers a different set surfaces as a failing renderer in the launch gate, not silently. + Covered by DesktopClerkForkRegistrarSuppression.test.ts (registration lands on the no-op, the + registrar is restored after). +- Two sessions independently authored this same fix within the hour, differing only in fence + placement and test naming — the shipped one is 7eedbbb6; the duplicate was dropped unpushed. + Recorded because the convergence is itself evidence the no-op-around-bridge-creation shape is + the minimal fix, not one agent's taste.