fix(desktop): isolate profile auth schemes - #484
Conversation
Greptile SummaryThe PR gives each desktop profile a distinct OAuth callback scheme and updates the vendored cloud package so hosted-billing return targets accept it.
Confidence Score: 4/5The packaged profile callback path should be fixed before merging because its dynamically generated OAuth scheme is absent from the native protocol declarations. Profile-specific OAuth and billing URLs are generated consistently inside the application, but packaged macOS, Windows, and branded builds still register only their unsuffixed schemes, preventing reliable native callback routing. Files Needing Attention: apps/desktop/src/main/constants.ts and desktop packaging protocol configurations
|
| Filename | Overview |
|---|---|
| apps/desktop/src/main/constants.ts | Introduces profile-specific OAuth schemes without corresponding packaged protocol declarations. |
| apps/desktop/src/main/tests/cloud-hosted-billing.test.ts | Correctly verifies URL generation for a profile, but does not exercise native operating-system protocol registration. |
| apps/desktop/package.json | Consistently upgrades the desktop application to the new vendored cloud package. |
| apps/webview/package.json | Keeps the webview cloud dependency aligned with the desktop application. |
| pnpm-lock.yaml | Consistently records the vendored package upgrade and integrity metadata. |
Sequence Diagram
sequenceDiagram
participant Profile as Packaged profile app
participant Provider as OAuth/Billing provider
participant OS as Operating system
Profile->>Provider: Request callback to linkcode-profile://
Provider->>OS: Open linkcode-profile:// callback
Note over OS: Package declares only linkcode://
OS--xProfile: Profile-specific scheme is not registered
Reviews (1): Last reviewed commit: "fix(desktop): isolate profile auth schem..." | Re-trigger Greptile
| export const CLOUD_AUTH_SCHEME = | ||
| PROFILE === undefined ? CLOUD_AUTH_SCHEME_BASE : `${CLOUD_AUTH_SCHEME_BASE}-${PROFILE}`; |
There was a problem hiding this comment.
Packaged schemes omit profiles
When a packaged release, development-shell, or branded app runs with a profile, CLOUD_AUTH_SCHEME gains a profile suffix while the package metadata still registers only the unsuffixed scheme, causing OAuth sign-in and hosted-billing callbacks to use a native URL scheme the operating system cannot reliably route back to that app.
There was a problem hiding this comment.
Important
On macOS the profile-suffixed scheme cannot be registered at all — Electron can only claim schemes declared in the bundle's Info.plist, and the protocols: blocks are build-time and profile-blind. Worth deciding the intended macOS behavior before merging. There is also a separator collision that reintroduces exactly the clash this PR removes.
Reviewed changes — the single commit 6d6bdf3b, extending the desktop channel × profile identity fork to the OAuth deep-link scheme.
- Profile-suffixed auth scheme —
apps/desktop/src/main/constants.tssplits out a privateCLOUD_AUTH_SCHEME_BASEand exportsCLOUD_AUTH_SCHEMEas`${BASE}-${PROFILE}`when a profile is active. Consumers incloud-auth/client.ts(sign-in?scheme=,electronClientprotocol,setAsDefaultProtocolClient, billingreturnTarget) are unchanged. - Vendored
@linkcode/cloud0.1.0 → 0.1.1 — the only behavioral delta is the return-target validator: an exactSet(['linkcode:', 'linkcode-dev:'])becomes/^linkcode(?:-dev)?(?:-[a-z0-9][a-z0-9-]{0,31})?:$/, whose profile group mirrorsPROFILE_NAME_PATTERNexactly. Tarball sha512 matches the lockfileintegrity, and the 0.1.0 tarball is removed cleanly. - New test —
cloud-hosted-billing.test.tsgains a profile case plusvi.resetModules()/delete process.env.LINKCODE_PROFILEinbeforeEach. I verified it is real coverage: neuteringconstants.tsback to the base scheme makes it fail, and the wholeapps/desktop/src/main/__tests__directory plus the webview billing test stay green with 0.1.1 installed. - Docs —
docs/DEVELOPMENT.mdprofile bullet and shared-across-channels paragraph,docs/ENVIRONMENT.mdLINKCODE_PROFILErow.
⚠️ The cloud-side half of this contract can't be verified from this repo
The vendored @linkcode/cloud bump only widens the client-side URL builder — it proves nothing about what console.linkcode.ai accepts, and the PR body scopes arcboxlabs/linkcodehq#52 to billing. Sign-in is a separate path: cloud-auth/client.ts:33 puts the suffixed scheme on https://linkcode.ai/sign-in?scheme=…, and if that page has its own scheme allowlist, profiled sign-in breaks in a new way the moment this merges ahead of the cloud deploy.
Technical details
# Server-side acceptance of profile-suffixed schemes
## Affected sites
- `apps/desktop/src/main/cloud-auth/client.ts:33` — `?scheme=<base>-<profile>` on the HQ sign-in URL; server-side allowlist unknown from this repo.
- `apps/desktop/src/main/cloud-auth/client.ts:96` — `returnTarget=<base>-<profile>://billing/return`; the vendored `createHostedBillingUrl` validates it locally, but `console.linkcode.ai` re-validates on receipt.
## Required outcome
- Confirm the sign-in page and the billing console both accept the `-<profile>` suffix, not just the vendored client builder.
- Confirm the merge/deploy ordering: this PR must not ship to users before the linkcodehq side is live.
## Open questions for the human
- Does `arcboxlabs/linkcodehq#52` cover the sign-in `scheme` param, or billing only?ℹ️ Nitpicks
apps/desktop/src/main/cloud-auth/client.ts:24-26still describes the scheme as "channel-split so a development build never fights the release app" — the sibling doc comment inconstants.tswas updated to say "channel and profile", this one was not.
Claude Opus | 𝕏
| BRAND_BASE?.authScheme ?? (CHANNEL === 'development' ? 'linkcode-dev' : 'linkcode'); | ||
|
|
||
| export const CLOUD_AUTH_SCHEME = | ||
| PROFILE === undefined ? CLOUD_AUTH_SCHEME_BASE : `${CLOUD_AUTH_SCHEME_BASE}-${PROFILE}`; |
There was a problem hiding this comment.
On macOS this scheme can never be registered. Electron's docs are explicit: "you can only register protocols that have been added to your app's info.plist, which cannot be modified at runtime" — and the protocols: blocks in electron-builder.release.yml / electron-builder.devshell.yml are build-time and profile-blind. So a packaged macOS build launched with a profile emits linkcode-<profile>:// on both the sign-in ?scheme= and the billing returnTarget, Launch Services has no handler, and the callback dead-ends silently — where before it at least reached the base-scheme app. @better-auth/electron's registerProtocolScheme only console.errors the failed registration, which is invisible in a packaged app.
Technical details
# Profile-suffixed deep-link scheme is unregistrable on macOS
## Affected sites
- `apps/desktop/src/main/constants.ts:85-86` — derives `<base>-<profile>` at launch time; nothing declares it to the OS.
- `apps/desktop/electron-builder.release.yml` / `electron-builder.devshell.yml` — `protocols:` lists only `linkcode` / `linkcode-dev`; these are baked at package time and cannot know the runtime `--profile`.
- `apps/desktop/src/main/cloud-auth/client.ts:59-62` (`claimDeepLink`) and `node_modules/@better-auth/electron/dist/client.mjs` (`registerProtocolScheme`) — both call `app.setAsDefaultProtocolClient`; the latter only `console.error`s on a `false` return.
- `docs/DEVELOPMENT.md:325` — the added claim "OAuth callbacks return to the matching app" does not hold on macOS.
## Required outcome
- A packaged macOS build launched with `--profile=<name>` must either receive its OAuth/billing callback, or fail loudly and predictably rather than dead-ending.
- The docs must not claim per-profile callback routing on a platform where it cannot work.
## Suggested approach (optional)
- Confirm the behavior on a packaged macOS build (`pnpm run package:devshell`, launch with a profile, watch the `setAsDefaultProtocolClient` return value) before choosing a fix — Windows registers at runtime and is unaffected, so this is macOS-specific.
- Options, roughly in increasing cost: keep the base scheme on `darwin` and disambiguate the profile inside the callback URL; declare a small reserved set of profile schemes in the `protocols:` blocks; or scope the suffix to the platforms where runtime registration works and document the macOS limitation.
## Open questions for the human
- Is a packaged macOS build with a profile a supported configuration, or is the profile flow dev-shell/Windows only?| const CLOUD_AUTH_SCHEME_BASE = | ||
| BRAND_BASE?.authScheme ?? (CHANNEL === 'development' ? 'linkcode-dev' : 'linkcode'); | ||
|
|
||
| export const CLOUD_AUTH_SCHEME = |
There was a problem hiding this comment.
The - separator is inside the profile charset, so it reintroduces the collision this change exists to remove: release channel + --profile=dev yields linkcode-dev, byte-identical to the development channel's default scheme (--profile=dev-alpha likewise aliases dev-channel-alpha). Both names pass PROFILE_NAME_PATTERN. Every other identity axis picks a separator the profile charset forbids — . for the state dir and APP_ID, (name) for APP_NAME and the keyring service — for exactly this reason.
Technical details
# Profile separator `-` aliases the channel suffix
## Affected sites
- `apps/desktop/src/main/constants.ts:85-86` — `` `${CLOUD_AUTH_SCHEME_BASE}-${PROFILE}` ``; `linkcode` + `dev` == `linkcode-dev`.
- `packages/foundation/schema/src/daemon-runtime.ts:42` — `PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/` admits `dev` and `dev-alpha`.
- `packages/foundation/schema/src/product.ts:52-57` — the existing comment stating that the state dir's dot separator is chosen precisely so `--profile=development` cannot reach the development channel's directory.
- Vendored `@linkcode/cloud` 0.1.1 — `/^linkcode(?:-dev)?(?:-[a-z0-9][a-z0-9-]{0,31})?:$/` is ambiguous the same way, so it cannot disambiguate server-side either.
## Required outcome
- A channel-plus-profile scheme must be unambiguous: no (channel, profile) pair may produce the same scheme string as a different pair.
## Suggested approach (optional)
- RFC 3986 allows `+`, `-`, and `.` in a scheme after the first character, so a `.` separator (`linkcode-dev.alpha`) mirrors the state-dir discipline and is unambiguous because profile names forbid dots. This changes the wire-visible scheme, so it needs a matching `@linkcode/cloud` release and a linkcodehq-side update — worth confirming before the vendored 0.1.1 regex is treated as settled.
Summary
LINKCODE_PROFILE@linkcode/cloud0.1.1 so profile-specific billing return targets remain strictly validatedDepends on arcboxlabs/linkcodehq#52.
Validation
pnpm check:cipnpm exec vitest run apps/desktop/src/main/__tests__/cloud-hosted-billing.test.ts(3 passed)pnpm test: 3002 passed, 5 skipped; 3 pre-existingrelease-artifact.test.tsfailures caused by macOS/varvs/private/varcanonicalization