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>
155 lines
4.1 KiB
TypeScript
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);
|
|
}
|