singularity-forge/src/web/web-auth-storage.ts
Mikael Hugo b24f426f2b batch: snapshot of in-flight v2 work
This commit captures uncommitted modifications that accumulated in the
working tree across multiple in-progress workstreams. It is a snapshot
to clear the deck before sf v3 work begins; individual workstreams
should land separately on top of this.

Notable additions:
- trace-collector.ts, traces.ts, src/tests/trace-export.test.ts —
  trace export plumbing
- biome.json — Biome linter configuration
- .gitignore — exclude native/npm/**/*.node compiled binaries

The bulk of the diff is across src/resources/extensions/sf/ (301 files)
and src/resources/extensions/sf/tests/ (277 files), reflecting the
ongoing sf extension work. Specific feature commits should follow this
snapshot rather than being archaeology'd out of it.

The 76MB native/npm/linux-x64-gnu/forge_engine.node compiled binary
was left out of the commit — it's now gitignored and built locally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 12:42:31 +02:00

155 lines
4.1 KiB
TypeScript

import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { dirname } from "node:path";
import {
getOAuthProvider,
getOAuthProviders,
type OAuthCredentials,
type OAuthLoginCallbacks,
type OAuthProviderInterface,
} from "../../packages/pi-ai/dist/oauth.js";
import { getEnvApiKey } from "../../packages/pi-ai/src/web-runtime-env-api-keys.ts";
export type ApiKeyCredential = {
type: "api_key";
key: string;
};
export type OAuthCredential = {
type: "oauth";
} & OAuthCredentials;
export type StoredCredential = ApiKeyCredential | OAuthCredential;
export type StoredCredentialEntry = StoredCredential | StoredCredential[];
export type StoredCredentialData = Record<string, StoredCredentialEntry>;
export interface OnboardingAuthStorage {
reload(): void;
set(provider: string, credential: StoredCredential): void;
getCredentialsForProvider(provider: string): StoredCredential[];
hasAuth(provider: string): boolean;
getOAuthProviders(): OAuthProviderInterface[];
login(providerId: string, callbacks: OAuthLoginCallbacks): Promise<void>;
logout(providerId: string): void;
}
function ensureAuthFile(authPath: string): void {
const parentDir = dirname(authPath);
if (!existsSync(parentDir)) {
mkdirSync(parentDir, { recursive: true, mode: 0o700 });
}
if (!existsSync(authPath)) {
writeFileSync(authPath, "{}", "utf-8");
chmodSync(authPath, 0o600);
}
}
function parseStoredCredentialData(
content: string | undefined,
): StoredCredentialData {
if (!content || !content.trim()) {
return {};
}
try {
const parsed = JSON.parse(content) as StoredCredentialData;
return typeof parsed === "object" && parsed !== null ? parsed : {};
} catch {
return {};
}
}
export class FileOnboardingAuthStorage implements OnboardingAuthStorage {
private data: StoredCredentialData = {};
private readonly authPath: string;
constructor(authPath: string) {
this.authPath = authPath;
this.reload();
}
reload(): void {
ensureAuthFile(this.authPath);
this.data = parseStoredCredentialData(readFileSync(this.authPath, "utf-8"));
}
getCredentialsForProvider(provider: string): StoredCredential[] {
const entry = this.data[provider];
if (!entry) return [];
return Array.isArray(entry) ? entry : [entry];
}
set(provider: string, credential: StoredCredential): void {
const existing = this.getCredentialsForProvider(provider);
const next =
credential.type === "api_key"
? this.mergeApiKeyCredentials(existing, credential)
: this.mergeOAuthCredential(existing, credential);
this.data[provider] = next.length === 1 ? next[0] : next;
writeFileSync(this.authPath, JSON.stringify(this.data, null, 2), "utf-8");
chmodSync(this.authPath, 0o600);
}
hasAuth(provider: string): boolean {
if (this.getCredentialsForProvider(provider).length > 0) {
return true;
}
return Boolean(getEnvApiKey(provider));
}
getOAuthProviders(): OAuthProviderInterface[] {
return getOAuthProviders();
}
async login(
providerId: string,
callbacks: OAuthLoginCallbacks,
): Promise<void> {
const provider = getOAuthProvider(providerId);
if (!provider) {
throw new Error(`Unknown OAuth provider: ${providerId}`);
}
const credentials = await provider.login(callbacks);
this.set(providerId, { type: "oauth", ...credentials });
}
logout(providerId: string): void {
delete this.data[providerId];
writeFileSync(this.authPath, JSON.stringify(this.data, null, 2), "utf-8");
chmodSync(this.authPath, 0o600);
}
private mergeApiKeyCredentials(
existing: StoredCredential[],
credential: ApiKeyCredential,
): StoredCredential[] {
const alreadyStored = existing.some(
(entry) => entry.type === "api_key" && entry.key === credential.key,
);
if (alreadyStored) {
return existing;
}
return [...existing, credential];
}
private mergeOAuthCredential(
existing: StoredCredential[],
credential: OAuthCredential,
): StoredCredential[] {
const apiKeys = existing.filter((entry) => entry.type === "api_key");
return [...apiKeys, credential];
}
}
export function createOnboardingAuthStorage(
authPath: string,
): OnboardingAuthStorage {
return new FileOnboardingAuthStorage(authPath);
}