mirror of
https://gh-proxy.org/https://github.com/klinsmaya/open-connector.git
synced 2026-11-08 01:16:52 +08:00
## Summary Add OOMOL cloud OAuth as a provider authorization source. Users can save an encrypted project API key, select a cloud provider configuration, authorize named connections, and run their actions and proxy requests through SaaS. Existing connections retain their source when the provider default changes, and third-party credentials remain in SaaS. - Implement project discovery, OAuth request synchronization, remote execution, and persistent cleanup of disconnected cloud accounts. Add SQLite, PostgreSQL, and D1 migrations and storage support, with instance reset and maintenance tooling. - Add Console project and authorization settings, localized errors, disconnect notifications, and styled OAuth completion pages that close after successful authorization. Align local development callback origins and proxy routes. - Improve safe request diagnostics and retry transient project health-check failures once. Execution requests are not automatically replayed. - Group storage implementations by database backend, share guarded HTTP requests with Marketplace, update Zod to 4.6.5, and load provider icons from the public catalog. - Document setup, maintenance, and the manual acceptance checklist. ## Validation - `npm run fix-check` passed. - `npm run build:web` passed. - Latest focused regression run: 346 tests passed across 33 files covering Console, SaaS project/client/routes, OAuth completion, and guarded HTTP requests. - Earlier implementation validation included the broader runtime/storage suites and a real PostgreSQL integration run. Full deployed action/proxy, cleanup recovery, and D1 maintenance acceptance remain documented manual checks; they are not claimed as completed by the unit tests. ## Deployment notes Apply migrations `0014_saas_project` and `0015_saas_cleanup_runtime`, configure encryption and an explicit `OOMOL_CONNECT_ORIGIN`, and enable the relevant cleanup scheduling. The remote SaaS deployment must support the project OAuth and remote execution APIs described in `docs/saas-oauth.md`.
134 lines
5.0 KiB
TypeScript
134 lines
5.0 KiB
TypeScript
import { mkdir } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
import { logger } from "../src/server/logger.ts";
|
|
import { createSecretCodec } from "../src/server/secrets/secret-codec.ts";
|
|
import {
|
|
createNodeRuntimeDatabase,
|
|
migratePostgresRuntimeDatabase,
|
|
sqliteMigrationsNotice,
|
|
} from "../src/server/storage/node-runtime-database.ts";
|
|
|
|
const { positionals, values: options } = parseArgs({
|
|
args: process.argv.slice(2),
|
|
allowPositionals: true,
|
|
options: {
|
|
"data-dir": { type: "string" },
|
|
plain: { type: "boolean" },
|
|
yes: { type: "boolean" },
|
|
},
|
|
strict: true,
|
|
});
|
|
const [command] = positionals;
|
|
|
|
if (
|
|
positionals.length !== 1 ||
|
|
(command !== "migrate" && command !== "reset" && command !== "rotate-key" && command !== "reset-instance")
|
|
) {
|
|
printUsageAndExit();
|
|
}
|
|
|
|
const nextEncryptionKey = process.env.OOMOL_CONNECT_NEW_ENCRYPTION_KEY;
|
|
if (command === "migrate") {
|
|
if (options.plain || options.yes || options["data-dir"]) {
|
|
throw new Error("migrate does not accept --plain, --yes, or --data-dir.");
|
|
}
|
|
} else if (command === "rotate-key") {
|
|
if (options.yes) {
|
|
throw new Error("--yes is only valid with reset or reset-instance.");
|
|
}
|
|
if (!nextEncryptionKey && !options.plain) {
|
|
throw new Error("rotate-key requires OOMOL_CONNECT_NEW_ENCRYPTION_KEY unless --plain is set.");
|
|
}
|
|
} else {
|
|
if (options.plain) {
|
|
throw new Error("--plain is only valid with rotate-key.");
|
|
}
|
|
if (!options.yes) {
|
|
throw new Error("reset and reset-instance require --yes; stop all runtime replicas first.");
|
|
}
|
|
}
|
|
|
|
const databaseUrl = process.env.OOMOL_CONNECT_DATABASE_URL?.trim();
|
|
const dataDir = resolve(options["data-dir"] ?? process.env.OOMOL_CONNECT_DATA_DIR ?? join(process.cwd(), "data"));
|
|
const databasePath = join(dataDir, "connect.sqlite");
|
|
if (command === "migrate") {
|
|
if (!databaseUrl) {
|
|
console.log(sqliteMigrationsNotice);
|
|
} else {
|
|
await migratePostgresRuntimeDatabase({
|
|
connectionString: databaseUrl,
|
|
connectionTimeoutMs: readPositiveIntegerEnv("OOMOL_CONNECT_DATABASE_CONNECT_TIMEOUT_MS", 10_000),
|
|
logger,
|
|
});
|
|
}
|
|
} else {
|
|
const secretCodec = createSecretCodec(process.env.OOMOL_CONNECT_ENCRYPTION_KEY);
|
|
if (!databaseUrl) {
|
|
await mkdir(dataDir, { recursive: true });
|
|
}
|
|
const database = databaseUrl
|
|
? await createNodeRuntimeDatabase({
|
|
backend: "postgresql",
|
|
connectionString: databaseUrl,
|
|
secretCodec,
|
|
poolMax: readPositiveIntegerEnv("OOMOL_CONNECT_DATABASE_POOL_MAX", 10),
|
|
connectionTimeoutMs: readPositiveIntegerEnv("OOMOL_CONNECT_DATABASE_CONNECT_TIMEOUT_MS", 10_000),
|
|
})
|
|
: await createNodeRuntimeDatabase({
|
|
backend: "sqlite",
|
|
path: databasePath,
|
|
secretCodec,
|
|
});
|
|
const target = databaseUrl ? "the PostgreSQL runtime database" : databasePath;
|
|
try {
|
|
if (command === "rotate-key") {
|
|
await database.rotateSecretCodec(createSecretCodec(options.plain ? undefined : nextEncryptionKey));
|
|
console.log(`Rotated runtime secret encryption in ${target}.`);
|
|
} else if (command === "reset-instance") {
|
|
const snapshot = await database.saasProjectStore.inspectInstance();
|
|
console.log(`Offline instance reset in ${target}: ${JSON.stringify(snapshot)}`);
|
|
const newInstanceId = crypto.randomUUID();
|
|
const result = await database.saasProjectStore.resetInstance({
|
|
expectedInstanceId: snapshot.instanceId,
|
|
newInstanceId,
|
|
});
|
|
if (result === "conflict") throw new Error("Instance identity changed. No reset was applied.");
|
|
console.log(
|
|
`Instance identity changed to ${newInstanceId}. Local connections and Marketplace were retained; no SaaS requests were sent.`,
|
|
);
|
|
} else {
|
|
await database.resetRuntimeData();
|
|
console.log(`Reset runtime data in ${target}.`);
|
|
}
|
|
} finally {
|
|
await database.close();
|
|
}
|
|
}
|
|
|
|
function printUsageAndExit(): never {
|
|
console.error(`Usage:
|
|
node scripts/runtime-data.ts migrate
|
|
node scripts/runtime-data.ts reset --yes [--data-dir ./data]
|
|
node scripts/runtime-data.ts reset-instance --yes [--data-dir ./data]
|
|
node scripts/runtime-data.ts rotate-key [--data-dir ./data]
|
|
node scripts/runtime-data.ts rotate-key --plain [--data-dir ./data]
|
|
|
|
Stop all runtime replicas before reset, reset-instance, or rotate-key. These commands do not operate on D1.
|
|
reset preserves the instance identity; reset-instance removes only SaaS data and changes the identity without contacting SaaS.
|
|
Set OOMOL_CONNECT_DATABASE_URL to migrate or maintain a PostgreSQL runtime database.
|
|
Set OOMOL_CONNECT_ENCRYPTION_KEY to read/write encrypted runtime credential records.
|
|
Set OOMOL_CONNECT_NEW_ENCRYPTION_KEY when rotating to a new encryption key.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function readPositiveIntegerEnv(name: string, fallback: number): number {
|
|
const value = process.env[name];
|
|
if (value === undefined) {
|
|
return fallback;
|
|
}
|
|
|
|
const parsed = Number(value);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|