mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
fix: fall back to next port when OAuth callback server can't bind 8080 (#560)
* fix: fall back to next port when OAuth callback server can't bind 8080
On Windows with Hyper-V/WSL2/Docker, port 8080 is often reserved by the
OS (EACCES) or already in use (EADDRINUSE), making sign-in completely
impossible. The app now scans 8080–8089 and binds the first available
port. For DCR providers, a stale registration locked to a blocked port
is detected and cleared so the client re-registers on the new port.
Static-client providers (Google BYOK) keep fixed-port behaviour with a
clear error message instead of a raw Node.js exception.
* fix: keep createAuthServer fixed-port by default, opt-in fallback
Address review feedback:
- Flip createAuthServer default to fixed-port; fallback is now opt-in via
{ fallback: true }. Composio (composio-handler.ts) keeps exact-port
semantics with no code change — only the Rowboat sign-in call site,
which builds its redirect URI from the actual bound port, opts in.
- Wrap post-bind setup (DCR, PKCE, auth URL) in try/catch and close the
server on any failure so the port is released for retries.
* fix: clear stale DCR registration when bound port differs from start port
This commit is contained in:
parent
c4888e2899
commit
0a3fc3736f
3 changed files with 302 additions and 167 deletions
|
|
@ -3,14 +3,21 @@ import fs from 'fs/promises';
|
|||
import path from 'path';
|
||||
import { ClientRegistrationResponse } from './types.js';
|
||||
|
||||
export const DEFAULT_CALLBACK_PORT = 8080;
|
||||
|
||||
export interface IClientRegistrationRepo {
|
||||
getClientRegistration(provider: string): Promise<ClientRegistrationResponse | null>;
|
||||
saveClientRegistration(provider: string, registration: ClientRegistrationResponse): Promise<void>;
|
||||
/** Returns the port that was used when DCR-registering this provider, or DEFAULT_CALLBACK_PORT if not stored. */
|
||||
getRegisteredPort(provider: string): Promise<number>;
|
||||
saveClientRegistration(provider: string, registration: ClientRegistrationResponse, port: number): Promise<void>;
|
||||
clearClientRegistration(provider: string): Promise<void>;
|
||||
}
|
||||
|
||||
// _registeredPort is our private field — stripped by Zod when we parse the RFC response fields
|
||||
type StoredEntry = Record<string, unknown> & { _registeredPort?: number };
|
||||
|
||||
type ClientRegistrationStorage = {
|
||||
[provider: string]: ClientRegistrationResponse;
|
||||
[provider: string]: StoredEntry;
|
||||
};
|
||||
|
||||
export class FSClientRegistrationRepo implements IClientRegistrationRepo {
|
||||
|
|
@ -45,14 +52,14 @@ export class FSClientRegistrationRepo implements IClientRegistrationRepo {
|
|||
|
||||
async getClientRegistration(provider: string): Promise<ClientRegistrationResponse | null> {
|
||||
const config = await this.readConfig();
|
||||
const registration = config[provider];
|
||||
if (!registration) {
|
||||
const entry = config[provider];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate registration structure
|
||||
// Validate registration structure (Zod strips unknown fields like _registeredPort)
|
||||
try {
|
||||
return ClientRegistrationResponse.parse(registration);
|
||||
return ClientRegistrationResponse.parse(entry);
|
||||
} catch {
|
||||
// Invalid registration, remove it
|
||||
await this.clearClientRegistration(provider);
|
||||
|
|
@ -60,9 +67,14 @@ export class FSClientRegistrationRepo implements IClientRegistrationRepo {
|
|||
}
|
||||
}
|
||||
|
||||
async saveClientRegistration(provider: string, registration: ClientRegistrationResponse): Promise<void> {
|
||||
async getRegisteredPort(provider: string): Promise<number> {
|
||||
const config = await this.readConfig();
|
||||
config[provider] = registration;
|
||||
return config[provider]?._registeredPort ?? DEFAULT_CALLBACK_PORT;
|
||||
}
|
||||
|
||||
async saveClientRegistration(provider: string, registration: ClientRegistrationResponse, port: number): Promise<void> {
|
||||
const config = await this.readConfig();
|
||||
config[provider] = { ...registration, _registeredPort: port };
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue