mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-03 16:10:31 +00:00
d4d78ac7db
* fix: dynamodb health checks and client recreation (#2789) * wip: no nanoServices groundwork * feat: data clients in new shape * wip: auth and perms in new system * more wip * middlewaters mainly done * wip: fsv2 in new layout * old fs v2 migration * driver system * driver and old fs fixes * ai drivers wip * stream support * metering in ai chat driver * wip: new auth * rate limit and auth routes * captcha and anti csrf * fix: types * auth store * app logic * wip most other dricvers * fs * mostly kill all legacy stuff * fs finish * fix: redis usage * ai controller * driver cleanup * socket io in v2 * broadcast and crudq stuff * subdomains * notifcations and shares * fix bad syntaxes * auth wip Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * extensions * extension setup * more routes * sql migrations and default services * home router * tier 7 * everything else * everything else * remaining missing bits * server health * logs * cleanup * deps * cleanup 2 * more cleanup 2 * boot * fix launch * config fix * move file * fix: tsconfig things * fix: extension loading * launching * fix: drivers * fix: others * fix: icons * fix: file uploads * fs fixes * fix: fs api * fix: dev-center * config * add back telemetry * lint stuff * husky hooks * fix: fs oss * fix: config migration * config migration * migrate scripts + replicate * runner * fix: merge defafult config * fix: default region * fix: api domain * fix paths in readfile * fix fs entry default s3 * NS: Remove Referral && Entri Service * dep cleanups * fix: static assets * fix: kv and perms * fix: driver registrations * fix: home mapping * fix: rao * adding back 500 alarm * fix: build paths * fix: fs and kv shapes * fix: kv shape * more kv coercing and ai chat matching format as prior * fix: private app gates * private app caches * fix: whole bunch of legacy shape issues * update template jsonc * fix caching partial oidc and fs signed paths * more oidc fixes * fix: wip * fix: private apps * admin route fixes * fix: last few things hopefully * claude uploads * fix security for app only routes * fix kv system namespace * stuff * fix: app and kv and suggested apps * fix:open item * fix: FS operations * fix: default app icons * add back token-read and WSL support * metering fixes * fix: fsEntry * perm scanners and implicators * proper download endpoint * fix: download * fix anti csrft on v2 * fix file extensions, app icons * fold in v1 fixes from origin/main into v2 equivalents Re-applies the v1 fixes that landed on origin/main into their v2 counterparts since the v1 files were deleted on DS/wip during the v2 migration. v1 commits referenced below. - SQLBatcher: flush immediately when queue hits maxBatchSize instead of racing the timer (v1 12f48238). - RedisClient: drop maxRetriesPerRequest from 2 to 1 to shrink failure window (v1b6776ab4). - ChatCompletionDriver: default minimumCredits to 1 when unset/zero so zero-cost precheck doesn't auto-pass (v136bd6073). - OpenAiImageProvider: add gpt-image-2 support — open-ended size rules, token-based cost estimator, arbitrary-size normalizer, isGpt prefix broadened to gpt-image- (v1f14f1bf4). models.ts auto-merged via rename detection. - AppStore: bump row cache TTL from 5m to 24h (v16b3196ed). Not ported: v1 app-object Redis cache (bdfa12b5/b886dde3) — v2's #toClient recomputes filetype_associations/created_from_origin per read; adding a second cache layer is a larger change for a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * remoe anti-csrf from auth routes that had not used them * more icon fixes * fix worker functionality * fix: app and subdomain es Co-authored-by: Copilot <copilot@github.com> * fix PUT-761 * fix: PUT-748 * fix: rename fsService * Add security back to WorkerDriver * Migrate worker from fsEntry to fs. Fix cache issue * remove ability to create symlinks * strict webdav acl * require auth for wisp * chore: service renames * Add metering back to puter peer api * fix: PUT-760 PUT-749 * fix: PUT-746 * fix: peer cost Co-authored-by: Copilot <copilot@github.com> * fix: 771 * change order of peer controller * fix: create appdata folder for app on get auth token * fix: align delete site and list sites * delete: putility * fix subdomains * Add support for tilde in subdomains, fix subdomain update * cleanup PeerController.ts and fix billing oversight (#2844) * fix: PUT-786 * fix: bugs * fix: issues with multiple subdomain queries, or permission checks * fix: harden response shapes to not contain uneeded fields * fix: move state to redis * fix: missing kv methods + better sec Co-authored-by: Copilot <copilot@github.com> * fix: subdomainStore limit * fix: missing path resolution Co-authored-by: Copilot <copilot@github.com> * fs fixes * fix: undef error * fix fs + cleanup * fix: npm audit fixes * heal path entries where missing Co-authored-by: Copilot <copilot@github.com> * fix: caching Co-authored-by: Copilot <copilot@github.com> * fix: cache inconsistencies Co-authored-by: Copilot <copilot@github.com> * fix: app driver metadata Co-authored-by: Copilot <copilot@github.com> * remove extraneous comma * fix: associated app icons * fix: bad tool call * Add validation to WorkerDriver#getFilePaths * misc fs and auth issues Co-authored-by: Copilot <copilot@github.com> * fix: oidc errors Co-authored-by: Copilot <copilot@github.com> * fix: PUT-797 * fix: legacy appdata_app Co-authored-by: Copilot <copilot@github.com> * fix: add alert logs Co-authored-by: Copilot <copilot@github.com> * fix: error handling * Disable sharecontroller * fix: remove private user identifier for ai * fix: private app fixes * Add backback signup_server * fix: completionId size Co-authored-by: Copilot <copilot@github.com> * fix: revalidate path for oidc * fix: revalidate path for oidc * fix: email validation Co-authored-by: Copilot <copilot@github.com> * fix: user create query * fix: middleware extensions Co-authored-by: Copilot <copilot@github.com> * use x-forwarded-for for req ip forwarded * fix: missing last_activity ts * feat: add cache broadcast to subdomains * fix: update config typing --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: ProgrammerIn-wonderland <3838shah@gmail.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Nariman Jelveh <nj@puter.com> Co-authored-by: velzie <velzie@velzie.rip>
480 lines
22 KiB
JavaScript
480 lines
22 KiB
JavaScript
// Shared helpers for migrateConfig.mjs / migrateServers.mjs.
|
|
|
|
// ── JSON loading ──────────────────────────────────────────────────────────
|
|
// Old files may be multiple JSON objects glued together with `//` comments.
|
|
// Strip line-comments + trailing commas, then walk brace depth to split.
|
|
|
|
const stripLineComments = (src) => src.replace(/^\s*\/\/.*$/gm, '');
|
|
const stripTrailingCommas = (src) => src.replace(/,(\s*[}\]])/g, '$1');
|
|
|
|
const splitJsonDocs = (src) => {
|
|
const docs = [];
|
|
let depth = 0;
|
|
let start = -1;
|
|
let inStr = false;
|
|
let esc = false;
|
|
for ( let i = 0; i < src.length; i++ ) {
|
|
const c = src[i];
|
|
if ( inStr ) {
|
|
if ( esc ) esc = false;
|
|
else if ( c === '\\' ) esc = true;
|
|
else if ( c === '"' ) inStr = false;
|
|
continue;
|
|
}
|
|
if ( c === '"' ) { inStr = true; continue; }
|
|
if ( c === '{' ) {
|
|
if ( depth === 0 ) start = i;
|
|
depth++;
|
|
} else if ( c === '}' ) {
|
|
depth--;
|
|
if ( depth === 0 && start !== -1 ) {
|
|
docs.push(src.slice(start, i + 1));
|
|
start = -1;
|
|
}
|
|
}
|
|
}
|
|
return docs;
|
|
};
|
|
|
|
// Strip v1 JSON-extension conventions at every depth:
|
|
// • `$`-prefixed keys → v1 directives (`$preserve`, `$requires`, `$version`)
|
|
// • `__`-prefixed keys → v1 comment-out convention (disabled entries)
|
|
// • empty-string keys → trailing-comma placeholder (`"": null,`)
|
|
const stripDollarKeys = (value) => {
|
|
if ( Array.isArray(value) ) return value.map(stripDollarKeys);
|
|
if ( value && typeof value === 'object' ) {
|
|
const out = {};
|
|
for ( const [k, v] of Object.entries(value) ) {
|
|
if ( k === '' || k.startsWith('$') || k.startsWith('__') ) continue;
|
|
out[k] = stripDollarKeys(v);
|
|
}
|
|
return out;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
export const loadDocs = (raw) => {
|
|
const cleaned = stripTrailingCommas(stripLineComments(raw));
|
|
const texts = splitJsonDocs(cleaned);
|
|
return texts.map((text, idx) => {
|
|
try { return stripDollarKeys(JSON.parse(text)); }
|
|
catch ( e ) {
|
|
throw new Error(`Failed to parse JSON document #${idx + 1}: ${e.message}`);
|
|
}
|
|
});
|
|
};
|
|
|
|
// ── Doc classification ───────────────────────────────────────────────────
|
|
|
|
export const pickServersDoc = (docs) => docs.find(d => Array.isArray(d?.servers));
|
|
|
|
export const pickBaseDoc = (docs) => {
|
|
// Prefer prod base (has `services` and no `servers`), then OSS default,
|
|
// then any non-servers doc.
|
|
const prodBase = docs.find(d => d && !Array.isArray(d.servers) && d.services && d.config_name && d.env !== 'dev');
|
|
if ( prodBase ) return prodBase;
|
|
const ossDefault = docs.find(d => d && !Array.isArray(d.servers) && (d.nginx_mode || d.env === 'dev'));
|
|
if ( ossDefault ) return ossDefault;
|
|
return docs.find(d => d && !Array.isArray(d.servers)) ?? null;
|
|
};
|
|
|
|
// ── Deep merge ───────────────────────────────────────────────────────────
|
|
// Plain deep merge: objects recurse, arrays + primitives replace.
|
|
|
|
export const deepMerge = (base, override) => {
|
|
if ( override === undefined ) return base;
|
|
if ( base === undefined ) return override;
|
|
if ( base === null || override === null ) return override;
|
|
if ( typeof base !== 'object' || typeof override !== 'object' ) return override;
|
|
if ( Array.isArray(base) || Array.isArray(override) ) return override;
|
|
const out = { ...base };
|
|
for ( const [k, v] of Object.entries(override) ) {
|
|
out[k] = deepMerge(base[k], v);
|
|
}
|
|
return out;
|
|
};
|
|
|
|
// ── v1 → v2 transformation ───────────────────────────────────────────────
|
|
|
|
const copyIfSet = (src, sk, dst, dk = sk) => {
|
|
if ( src[sk] !== undefined ) dst[dk] = src[sk];
|
|
};
|
|
|
|
export const transformToV2 = (source) => {
|
|
const out = {};
|
|
|
|
// Scalar + renamed top-level keys.
|
|
copyIfSet(source, 'config_name', out);
|
|
copyIfSet(source, 'env', out);
|
|
copyIfSet(source, 'server_id', out, 'serverId');
|
|
copyIfSet(source, 'id', out, 'serverId');
|
|
copyIfSet(source, 'region', out);
|
|
copyIfSet(source, 'domain', out);
|
|
copyIfSet(source, 'protocol', out);
|
|
copyIfSet(source, 'pub_port', out);
|
|
copyIfSet(source, 'cookie_name', out);
|
|
copyIfSet(source, 'jwt_secret', out);
|
|
copyIfSet(source, 'url_signature_secret', out);
|
|
copyIfSet(source, 'blocked_email_domains', out, 'blockedEmailDomains');
|
|
copyIfSet(source, 'enable_public_folders', out);
|
|
copyIfSet(source, 'is_storage_limited', out);
|
|
copyIfSet(source, 'storage_capacity', out);
|
|
copyIfSet(source, 'static_hosting_domain', out);
|
|
copyIfSet(source, 'static_hosting_domain_alt', out);
|
|
copyIfSet(source, 'private_app_hosting_domain', out);
|
|
copyIfSet(source, 'private_app_hosting_domain_alt', out);
|
|
copyIfSet(source, 'min_pass_length', out);
|
|
copyIfSet(source, 'allow_system_login', out);
|
|
copyIfSet(source, 'allow_all_host_values', out);
|
|
copyIfSet(source, 'allow_no_host_header', out);
|
|
copyIfSet(source, 'allow_nipio_domains', out);
|
|
copyIfSet(source, 'custom_domains_enabled', out);
|
|
copyIfSet(source, 'enable_ip_validation', out);
|
|
copyIfSet(source, 'default_user_group', out);
|
|
copyIfSet(source, 'default_temp_group', out);
|
|
copyIfSet(source, 'api_base_url', out);
|
|
copyIfSet(source, 'origin', out);
|
|
copyIfSet(source, 'contact_email', out, 'support_email');
|
|
|
|
// `extensions` in v1 was overloaded — array form = scan dirs, object form
|
|
// = per-extension config bag. JSON.parse keeps only the last declaration
|
|
// per key, so in prod files where both appear we typically only see the
|
|
// object. Promote object-shape entries onto top-level keys (scoped npm
|
|
// names → camelCase: `@heyputer/app-store-and-purchases` → `appStoreAndPurchases`).
|
|
// v1 also had `mod_directories: string[]` with `{repo}/...` placeholders.
|
|
// v2 uses `extensions: string[]` of plain directory paths; post-cutover the
|
|
// only dir that survives is the repo-root `./extensions`, so synthesize that
|
|
// when only the config-bag (object) or mod_directories form is present.
|
|
// Some v1 configs used `extension` (singular) as the per-extension config
|
|
// bag alongside (or instead of) `extensions`. Accept either.
|
|
const extBag = (source.extensions && typeof source.extensions === 'object' && !Array.isArray(source.extensions))
|
|
? source.extensions
|
|
: (source.extension && typeof source.extension === 'object')
|
|
? source.extension
|
|
: null;
|
|
if ( Array.isArray(source.extensions) ) {
|
|
out.extensions = source.extensions;
|
|
} else if ( extBag ) {
|
|
for ( const [k, v] of Object.entries(extBag) ) {
|
|
const bare = k.split('/').pop() ?? k;
|
|
const camel = bare.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
if ( out[camel] === undefined ) out[camel] = v;
|
|
}
|
|
}
|
|
if ( out.extensions === undefined && (
|
|
Array.isArray(source.mod_directories) || extBag
|
|
) ) {
|
|
out.extensions = ['./extensions'];
|
|
}
|
|
|
|
// Port: http_port → port. Drop string "auto" (v2 requires numeric).
|
|
if ( source.http_port !== undefined && source.http_port !== 'auto' ) {
|
|
out.port = source.http_port;
|
|
} else if ( source.port !== undefined ) {
|
|
out.port = source.port;
|
|
}
|
|
|
|
// S3: old flat keys → `s3.s3Config`.
|
|
if ( source.s3_access_key || source.s3_secret_key ) {
|
|
out.s3 = {
|
|
s3Config: {
|
|
endpoint: source.s3_endpoint ?? '',
|
|
accessKeyId: source.s3_access_key,
|
|
secretAccessKey: source.s3_secret_key,
|
|
...(source.s3_region ? { region: source.s3_region } : {}),
|
|
},
|
|
};
|
|
}
|
|
copyIfSet(source, 's3_bucket', out);
|
|
copyIfSet(source, 's3_region', out);
|
|
|
|
// Database: prefer services.database.{primary, engine}; else db_* flat.
|
|
const svc = source.services ?? {};
|
|
if ( svc.database ) {
|
|
const db = {};
|
|
if ( svc.database.engine ) db.engine = svc.database.engine;
|
|
if ( svc.database.primary ) {
|
|
for ( const k of ['host', 'port', 'user', 'password', 'database'] ) {
|
|
if ( svc.database.primary[k] !== undefined ) db[k] = svc.database.primary[k];
|
|
}
|
|
}
|
|
if ( svc.database.path ) db.path = svc.database.path;
|
|
if ( Object.keys(db).length ) out.database = db;
|
|
}
|
|
if ( ! out.database && source.db_host ) {
|
|
out.database = {
|
|
engine: 'mysql',
|
|
host: source.db_host,
|
|
port: source.db_port,
|
|
user: source.db_user,
|
|
password: source.db_password,
|
|
database: source.db_database,
|
|
};
|
|
}
|
|
if ( source.read_replica_db ) {
|
|
out.database = out.database ?? { engine: 'mysql' };
|
|
const r = source.read_replica_db;
|
|
out.database.replica = {
|
|
host: r.host, port: r.port, user: r.user, password: r.password, database: r.database,
|
|
};
|
|
}
|
|
|
|
// Dynamo (services.dynamo → top-level)
|
|
if ( svc.dynamo ) out.dynamo = svc.dynamo;
|
|
|
|
// Email (services.email → email; drop `engine` adapter switch). Fallback
|
|
// to old flat smtp_* fields.
|
|
if ( svc.email ) {
|
|
const { engine: _engine, ...rest } = svc.email;
|
|
out.email = rest;
|
|
} else if ( source.smtp_server || source.smtp_host ) {
|
|
out.email = {
|
|
host: source.smtp_server ?? source.smtp_host,
|
|
port: source.smtp_port ?? source.smpt_port,
|
|
secure: true,
|
|
auth: { user: source.smtp_username, pass: source.smtp_password },
|
|
};
|
|
}
|
|
|
|
// Pager: routing_key → routingKey.
|
|
if ( source.pager?.pagerduty ) {
|
|
const pd = source.pager.pagerduty;
|
|
out.pager = {
|
|
pagerduty: {
|
|
enabled: pd.enabled,
|
|
...(pd.routing_key ? { routingKey: pd.routing_key } : {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
// Captcha
|
|
if ( svc.captcha ) out.captcha = svc.captcha;
|
|
|
|
// Homepage GUI bundle promotion (services.puter-homepage.* → top-level).
|
|
if ( svc['puter-homepage'] ) {
|
|
const h = svc['puter-homepage'];
|
|
copyIfSet(h, 'gui_bundle', out);
|
|
copyIfSet(h, 'gui_puterjs_bundle', out);
|
|
copyIfSet(h, 'gui_css', out);
|
|
}
|
|
|
|
// Legacy billing consolidation (stripe/offerings/__subs-serve → legacyBilling).
|
|
const legacyBilling = {};
|
|
if ( svc.stripe ) {
|
|
if ( svc.stripe.api_secret ) legacyBilling.api_secret = svc.stripe.api_secret;
|
|
if ( svc.stripe.endpoint_secret ) legacyBilling.endpoint_secret = svc.stripe.endpoint_secret;
|
|
}
|
|
if ( svc['__subs-serve']?.stripe_publishable_key ) {
|
|
legacyBilling.stripe_publishable_key = svc['__subs-serve'].stripe_publishable_key;
|
|
}
|
|
if ( svc.offerings?.price_ids ) legacyBilling.price_ids = svc.offerings.price_ids;
|
|
if ( Object.keys(legacyBilling).length ) out.legacyBilling = legacyBilling;
|
|
|
|
// Abuse / clickhouse / cf_file_cache pass through if already top-level.
|
|
if ( source.abuse ) out.abuse = source.abuse;
|
|
if ( source.clickhouse ) out.clickhouse = source.clickhouse;
|
|
if ( source.cf_file_cache ) out.cf_file_cache = source.cf_file_cache;
|
|
|
|
// Redis: v1 shape was `redis.config: [{host,port},…]`; v2 IRedisConfig
|
|
// expects `redis.startupNodes: [{host,port},…]`.
|
|
if ( source.redis && typeof source.redis === 'object' ) {
|
|
const { config: nodes, ...rest } = source.redis;
|
|
out.redis = { ...rest };
|
|
if ( Array.isArray(nodes) ) out.redis.startupNodes = nodes;
|
|
else if ( Array.isArray(source.redis.startupNodes) ) out.redis.startupNodes = source.redis.startupNodes;
|
|
}
|
|
|
|
// v1 services that became top-level IConfig entries (some with renames).
|
|
if ( svc.oidc ) out.oidc = svc.oidc;
|
|
if ( svc.wisp ) out.wisp = svc.wisp;
|
|
if ( svc.peer ) out.peers = svc.peer;
|
|
if ( svc.broadcast ) out.broadcast = svc.broadcast;
|
|
if ( svc['worker-service'] ) out.workers = svc['worker-service'];
|
|
if ( svc['entri-service'] ) out.entri = svc['entri-service'];
|
|
// v1's services.thumbnails wrapped the bucket config in `.bucket` and also
|
|
// carried an unrelated `engine`/`host` pointer to the thumbnail HTTP
|
|
// service. v2's IThumbnailStoreConfig is strictly the bucket — unwrap.
|
|
if ( svc.thumbnails?.bucket ) out.thumbnailStore = svc.thumbnails.bucket;
|
|
|
|
// v2 onlyoffice extension reads `config.onlyoffice` (same field names as
|
|
// v1's `services.onlyoffice-app`), so it's a straight rename.
|
|
if ( svc['onlyoffice-app'] ) out.onlyoffice = svc['onlyoffice-app'];
|
|
|
|
// Cloudflare Turnstile: v2 GUI renders the challenge widget when
|
|
// `gui_params.turnstileSiteKey` is set (see initgui.js / UIWindowSignup).
|
|
// Preserve the full block at `turnstile` for whenever the backend verifier
|
|
// is ported, and surface the site key into gui_params so the widget works.
|
|
if ( svc['cloudflare-turnstile'] ) {
|
|
out.turnstile = svc['cloudflare-turnstile'];
|
|
if ( svc['cloudflare-turnstile'].site_key ) {
|
|
out.gui_params = out.gui_params ?? {};
|
|
out.gui_params.turnstileSiteKey = svc['cloudflare-turnstile'].site_key;
|
|
}
|
|
}
|
|
|
|
// AI / integration providers: v1 kept each under `services.<id>` (plus a
|
|
// top-level `openai` shortcut in some configs). v2 unifies them under
|
|
// `providers[<id>]` and accepts only the canonical camelCase field names
|
|
// on IAIProviderConfig, so we rename the common snake_case aliases here.
|
|
const PROVIDER_IDS = [
|
|
'openai', 'claude', 'gemini', 'mistral', 'groq', 'deepseek',
|
|
'xai', 'openrouter', 'together-ai', 'ollama',
|
|
'elevenlabs', 'aws-polly', 'aws-textract', 'mistral-ocr', 'cloudflare',
|
|
'openai-completion', 'openai-responses',
|
|
'openai-image-generation', 'openai-video-generation',
|
|
'gemini-image-generation', 'gemini-video-generation',
|
|
'together-image-generation', 'together-video-generation',
|
|
'cloudflare-image-generation', 'xai-image-generation',
|
|
'replicate-image-generation',
|
|
];
|
|
const PROVIDER_RENAMES = [
|
|
['api_key', 'apiKey'], ['secret_key', 'apiKey'], ['key', 'apiKey'],
|
|
['api_token', 'apiToken'],
|
|
['api_base_url', 'apiBaseUrl'],
|
|
['account_id', 'accountId'],
|
|
['default_voice_id', 'defaultVoiceId'],
|
|
['speech_to_speech_model_id', 'speechToSpeechModelId'],
|
|
];
|
|
const normalizeProvider = (raw) => {
|
|
if ( ! raw || typeof raw !== 'object' ) return raw;
|
|
const p = { ...raw };
|
|
for ( const [from, to] of PROVIDER_RENAMES ) {
|
|
if ( p[from] !== undefined && p[to] === undefined ) p[to] = p[from];
|
|
delete p[from];
|
|
}
|
|
return p;
|
|
};
|
|
const providers = {};
|
|
for ( const id of PROVIDER_IDS ) {
|
|
if ( svc[id] ) providers[id] = normalizeProvider(svc[id]);
|
|
}
|
|
// v1 AWS aliases: some configs shortened `aws-polly` → `polly`,
|
|
// `aws-textract` → `textract`. v2 provider ids keep the prefix.
|
|
if ( svc.polly && providers['aws-polly'] === undefined ) {
|
|
providers['aws-polly'] = normalizeProvider(svc.polly);
|
|
}
|
|
if ( svc.textract && providers['aws-textract'] === undefined ) {
|
|
providers['aws-textract'] = normalizeProvider(svc.textract);
|
|
}
|
|
if ( source.openai && providers.openai === undefined ) {
|
|
providers.openai = normalizeProvider(source.openai);
|
|
}
|
|
// v1 had a single `services.replicate` that the image driver keyed on;
|
|
// v2 splits providers by capability, so the image one lands at
|
|
// `providers['replicate-image-generation']`.
|
|
if ( svc.replicate && providers['replicate-image-generation'] === undefined ) {
|
|
providers['replicate-image-generation'] = normalizeProvider(svc.replicate);
|
|
}
|
|
// Backward-compat fan-out: v1 had a single `openai` / `gemini` / `together-ai`
|
|
// / `xai` entry used for chat + image + video. v2 drivers look up split ids
|
|
// (e.g. `openai-completion`, `openai-image-generation`), so seed each
|
|
// split id from the base id when the split key isn't already set.
|
|
const FAN_OUT = {
|
|
openai: ['openai-completion', 'openai-responses', 'openai-image-generation', 'openai-video-generation'],
|
|
gemini: ['gemini-image-generation', 'gemini-video-generation'],
|
|
'together-ai': ['together-image-generation', 'together-video-generation'],
|
|
xai: ['xai-image-generation'],
|
|
};
|
|
for ( const [base, splits] of Object.entries(FAN_OUT) ) {
|
|
if ( ! providers[base] ) continue;
|
|
for ( const split of splits ) {
|
|
if ( providers[split] === undefined ) providers[split] = providers[base];
|
|
}
|
|
}
|
|
if ( Object.keys(providers).length ) out.providers = providers;
|
|
|
|
// Anything left in `services` that we didn't claim above is promoted to
|
|
// top-level (v2's IConfig has no `services` bag). Known consumers that
|
|
// live outside `services` in v2 are listed in `consumedServiceKeys` so
|
|
// we don't double-emit; known-dead v1 services are listed in
|
|
// `droppedServiceKeys` so their data is intentionally discarded.
|
|
const consumedServiceKeys = new Set([
|
|
'database', 'dynamo', 'email', 'captcha', 'puter-homepage',
|
|
'stripe', 'offerings', '__subs-serve',
|
|
'oidc', 'wisp', 'peer', 'broadcast', 'worker-service', 'entri-service',
|
|
'thumbnails', 'onlyoffice-app', 'cloudflare-turnstile', 'replicate',
|
|
'polly', 'textract',
|
|
...PROVIDER_IDS,
|
|
]);
|
|
const droppedServiceKeys = new Set([
|
|
// v1 services with no v2 equivalent — data intentionally discarded.
|
|
'heap-monitor', 'file-cache', 'telemetry', 'monitor', 'spending',
|
|
'judge0', 'convert-api',
|
|
// `auth.uuid_fpe_key` — v2 AuthService uses plain session UUIDs
|
|
// (see services/auth/types.ts: "not FPE-encrypted").
|
|
'auth',
|
|
// SNS bounce handler not ported to v2 — no v2 SNSService.
|
|
'sns',
|
|
// v1 config-only orphans: never actually read by v1 backend
|
|
// (confirmed via git grep on puter@main), no v2 consumers.
|
|
'ipgeo', 'newsdata', 'weather', 'user-send-mail',
|
|
// `ai-chat.concurrentRequests` — v1 concurrency limiter not ported
|
|
// yet; see TODO in v2 drivers/ai-chat/ChatCompletionDriver.ts. Config
|
|
// shape should move under `rate_limit.*` in IConfig when reintroduced,
|
|
// so dropping avoids migrating a soon-to-be-renamed shape.
|
|
'ai-chat',
|
|
// v1-only services with no v2 analogue.
|
|
'web-server', // `disable_ip_validate_event` — flag gone
|
|
'puter-kvstore', // v2 uses `dynamo` for system KV
|
|
]);
|
|
for ( const [k, v] of Object.entries(svc) ) {
|
|
if ( consumedServiceKeys.has(k) || droppedServiceKeys.has(k) ) continue;
|
|
if ( out[k] === undefined ) out[k] = v;
|
|
}
|
|
|
|
// Top-level v1 keys that v2 OSS doesn't read and no extension claims.
|
|
// Silently dropped — everything else falls through to the preservation
|
|
// loop below in case it belongs to a prod extension we're not aware of.
|
|
const droppedTopKeys = new Set([
|
|
// `puter_hosted_data.puter_versions` — set in v1 config.js but never
|
|
// read. Planted for a version-check feature that never shipped.
|
|
'puter_hosted_data',
|
|
// v1 dev-only toggles with no v2 equivalent. Dev vs prod behaviour
|
|
// in v2 branches off `env === 'dev'` and `config.abuse.enabled`.
|
|
'disable_abuse_checks',
|
|
'undefined_origin_allowed',
|
|
]);
|
|
for ( const k of droppedTopKeys ) delete out[k];
|
|
|
|
// Preserve any other top-level keys we haven't explicitly translated
|
|
// (custom extension configs etc.).
|
|
const handledTop = new Set([
|
|
'config_name', 'env', 'http_port', 'port', 'pub_port', 'domain', 'protocol',
|
|
'blocked_email_domains', 'toConsole', 'is_storage_limited',
|
|
'legacy_token_migrate', 'forwarded', 'cross_origin_isolation',
|
|
'enable_public_folders', 'cookie_name', 'jwt_secret', 'url_signature_secret',
|
|
'extensions', 'mod_directories',
|
|
'db_host', 'db_port', 'db_user', 'db_password', 'db_database',
|
|
'db_waitForConnections', 'db_connectionLimit', 'db_enableKeepAlive',
|
|
'db_queueLimit', 'db_read_replica_wait', 'read_replica_db',
|
|
's3_access_key', 's3_secret_key', 's3_bucket', 's3_region', 's3_endpoint',
|
|
'mailchimp', 'cloudwatch', 'monitor',
|
|
'smtp_server', 'smtp_host', 'smtp_port', 'smpt_port', 'smtp_username', 'smtp_password',
|
|
'max_subdomains_per_user',
|
|
'storage_capacity',
|
|
'static_hosting_domain', 'static_hosting_domain_alt',
|
|
'private_app_hosting_domain', 'private_app_hosting_domain_alt',
|
|
'openai',
|
|
'pager',
|
|
'defaultjs_asset_path',
|
|
'services',
|
|
'server_id', 'id', 'region', 'host',
|
|
'nginx_mode', 'contact_email',
|
|
'api_base_url', 'origin',
|
|
'min_pass_length', 'allow_system_login', 'allow_all_host_values',
|
|
'allow_no_host_header', 'allow_nipio_domains', 'custom_domains_enabled',
|
|
'enable_ip_validation',
|
|
'default_user_group', 'default_temp_group',
|
|
'abuse', 'clickhouse', 'cf_file_cache', 'legacyBilling',
|
|
'providers', 'thumbnailStore',
|
|
'redis', 'extension',
|
|
...droppedTopKeys,
|
|
]);
|
|
for ( const [k, v] of Object.entries(source) ) {
|
|
if ( handledTop.has(k) || k === '' ) continue;
|
|
out[k] = v;
|
|
}
|
|
|
|
return out;
|
|
};
|