openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
758 lines (757 loc) • 29.3 kB
JavaScript
import { a as asNullableRecord } from "./runtime-doctor-migrations-DJDQaWC5.js";
import { isDeepStrictEqual } from "node:util";
//#region packages/normalization-core/src/string-coerce.ts
/** Trims string input and returns null for non-strings or empty strings. */
function normalizeNullableString(value) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
/** Trims string input and returns undefined for non-strings or empty strings. */
function normalizeOptionalString(value) {
return normalizeNullableString(value) ?? void 0;
}
/** Lowercases a normalized optional string. */
function normalizeOptionalLowercaseString(value) {
return normalizeOptionalString(value)?.toLowerCase();
}
/** Lowercases a normalized string or returns an empty string when absent. */
function normalizeLowercaseStringOrEmpty(value) {
return normalizeOptionalLowercaseString(value) ?? "";
}
//#endregion
//#region packages/normalization-core/src/string-normalization.ts
/** Coerces entries to strings, trims them, and drops empty results. */
function normalizeStringEntries(list) {
return (list ?? []).map((entry) => normalizeOptionalString(String(entry)) ?? "").filter(Boolean);
}
/** Returns first-seen unique values while preserving insertion order. */
function uniqueValues(values) {
return [...new Set(values)];
}
/** Returns first-seen unique strings while preserving insertion order. */
function uniqueStrings(values) {
return uniqueValues(values);
}
//#endregion
//#region src/channels/plugins/dm-access.ts
/**
* Channel DM access helpers.
*
* Reads, writes, migrates, and normalizes direct-message policy and allowFrom fields.
*/
function cloneDm(entry) {
const dm = asNullableRecord(entry.dm);
return dm ? { ...dm } : null;
}
function allowFromListsMatch(left, right) {
if (!Array.isArray(left) || !Array.isArray(right)) return false;
const normalizedLeft = normalizeStringEntries(left);
const normalizedRight = normalizeStringEntries(right);
if (normalizedLeft.length !== normalizedRight.length) return false;
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
}
/**
* Migrates legacy `dm.*` aliases into the canonical DM access fields.
*/
function normalizeLegacyDmAliases(params) {
let changed = false;
let updated = params.entry;
const rawDm = updated.dm;
const dm = cloneDm(updated);
let dmChanged = false;
const topDmPolicy = updated.dmPolicy;
const legacyDmPolicy = dm?.policy;
if (topDmPolicy === void 0 && legacyDmPolicy !== void 0) {
updated = {
...updated,
dmPolicy: legacyDmPolicy
};
changed = true;
if (dm) {
delete dm.policy;
dmChanged = true;
}
params.changes.push(`Moved ${params.pathPrefix}.dm.policy → ${params.pathPrefix}.dmPolicy.`);
} else if (topDmPolicy !== void 0 && legacyDmPolicy !== void 0 && topDmPolicy === legacyDmPolicy) {
if (dm) {
delete dm.policy;
dmChanged = true;
params.changes.push(`Removed ${params.pathPrefix}.dm.policy (dmPolicy already set).`);
}
}
if (params.promoteAllowFrom !== false) {
const topAllowFrom = updated.allowFrom;
const legacyAllowFrom = dm?.allowFrom;
if (topAllowFrom === void 0 && legacyAllowFrom !== void 0) {
updated = {
...updated,
allowFrom: legacyAllowFrom
};
changed = true;
if (dm) {
delete dm.allowFrom;
dmChanged = true;
}
params.changes.push(`Moved ${params.pathPrefix}.dm.allowFrom → ${params.pathPrefix}.allowFrom.`);
} else if (topAllowFrom !== void 0 && legacyAllowFrom !== void 0 && allowFromListsMatch(topAllowFrom, legacyAllowFrom)) {
if (dm) {
delete dm.allowFrom;
dmChanged = true;
params.changes.push(`Removed ${params.pathPrefix}.dm.allowFrom (allowFrom already set).`);
}
}
}
if (dm && asNullableRecord(rawDm) && dmChanged) {
if (Object.keys(dm).length === 0) {
if (updated.dm !== void 0) {
const { dm: _ignored, ...rest } = updated;
updated = rest;
changed = true;
params.changes.push(`Removed empty ${params.pathPrefix}.dm after migration.`);
}
} else {
updated = {
...updated,
dm
};
changed = true;
}
}
return {
entry: updated,
changed
};
}
//#endregion
//#region src/config/channel-compat-normalization.ts
function parseAliasStreamingMode(value) {
if (typeof value !== "string") return null;
const normalized = value.trim().toLowerCase();
return normalized === "off" || normalized === "partial" || normalized === "block" || normalized === "progress" ? normalized : null;
}
/**
* Doctor-only stream mode resolution across nested and legacy alias keys.
*
* Runtime helpers no longer read `streamMode`, so doctor contracts use this to
* preserve legacy intent (nested mode > scalar string > streamMode > scalar
* boolean) while migrating flat aliases into `streaming.mode`.
*/
function resolveLegacyAliasStreamingMode(entry, defaultMode) {
const nestedMode = asNullableRecord(entry.streaming)?.mode;
const parsed = parseAliasStreamingMode(nestedMode ?? entry.streaming) ?? parseAliasStreamingMode(entry.streamMode);
if (parsed) return parsed;
if (typeof entry.streaming === "boolean") return entry.streaming ? "partial" : "off";
return defaultMode;
}
/** Checks whether any account entry still carries a channel-specific legacy alias. */
function hasLegacyAccountStreamingAliases(value, match) {
const accounts = asNullableRecord(value);
if (!accounts) return false;
return Object.values(accounts).some((account) => match(account));
}
function ensureNestedRecord(owner, key) {
const existing = asNullableRecord(owner[key]);
if (existing) return { ...existing };
return {};
}
/**
* Moves legacy flat streaming aliases into the nested `streaming` config shape.
*
* Existing nested values win over legacy aliases, matching doctor migration rules
* that preserve explicit modern config while removing stale compatibility keys.
*/
function normalizeLegacyStreamingAliases(params) {
const beforeStreaming = params.entry.streaming;
const hadLegacyStreamMode = params.entry.streamMode !== void 0;
const hasLegacyFlatFields = params.entry.chunkMode !== void 0 || params.entry.blockStreaming !== void 0 || params.entry.blockStreamingCoalesce !== void 0 || params.includePreviewChunk === true && params.entry.draftChunk !== void 0 || params.entry.nativeStreaming !== void 0;
if (!(hadLegacyStreamMode || typeof beforeStreaming === "boolean" || typeof beforeStreaming === "string" || hasLegacyFlatFields)) return {
entry: params.entry,
changed: false
};
const updated = { ...params.entry };
let changed = false;
const streaming = ensureNestedRecord(updated, "streaming");
const block = ensureNestedRecord(streaming, "block");
const preview = ensureNestedRecord(streaming, "preview");
let movedStreamMode = false;
if ((hadLegacyStreamMode || typeof beforeStreaming === "boolean" || typeof beforeStreaming === "string") && streaming.mode === void 0) {
streaming.mode = params.resolvedMode;
if (hadLegacyStreamMode) {
movedStreamMode = true;
params.changes.push(`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`);
} else if (typeof beforeStreaming === "boolean") params.changes.push(`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`);
else if (typeof beforeStreaming === "string") params.changes.push(`Moved ${params.pathPrefix}.streaming (scalar) → ${params.pathPrefix}.streaming.mode (${params.resolvedMode}).`);
changed = true;
}
if (hadLegacyStreamMode) {
if (!movedStreamMode) params.changes.push(`Removed ${params.pathPrefix}.streamMode (${params.pathPrefix}.streaming.mode already set).`);
delete updated.streamMode;
changed = true;
}
const moveOrRemoveAlias = (flatKey, target, slot, nestedPath) => {
if (updated[flatKey] === void 0) return;
const nested = `${params.pathPrefix}.streaming.${nestedPath}`;
if (target[slot] === void 0) {
target[slot] = updated[flatKey];
params.changes.push(`Moved ${params.pathPrefix}.${flatKey} → ${nested}.`);
} else params.changes.push(`Removed ${params.pathPrefix}.${flatKey} (${nested} already set).`);
delete updated[flatKey];
changed = true;
};
moveOrRemoveAlias("chunkMode", streaming, "chunkMode", "chunkMode");
moveOrRemoveAlias("blockStreaming", block, "enabled", "block.enabled");
if (params.includePreviewChunk === true) moveOrRemoveAlias("draftChunk", preview, "chunk", "preview.chunk");
moveOrRemoveAlias("blockStreamingCoalesce", block, "coalesce", "block.coalesce");
if (updated.nativeStreaming !== void 0 && params.resolvedNativeTransport !== void 0) {
if (streaming.nativeTransport === void 0) {
streaming.nativeTransport = params.resolvedNativeTransport;
params.changes.push(`Moved ${params.pathPrefix}.nativeStreaming → ${params.pathPrefix}.streaming.nativeTransport.`);
} else params.changes.push(`Removed ${params.pathPrefix}.nativeStreaming (${params.pathPrefix}.streaming.nativeTransport already set).`);
delete updated.nativeStreaming;
changed = true;
} else if (typeof beforeStreaming === "boolean" && streaming.nativeTransport === void 0 && params.resolvedNativeTransport !== void 0) {
streaming.nativeTransport = params.resolvedNativeTransport;
params.changes.push(`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.streaming.nativeTransport.`);
changed = true;
}
if (changed && beforeStreaming === void 0 && streaming.mode === void 0 && params.aliasOnlyMode !== void 0) {
streaming.mode = params.aliasOnlyMode;
params.changes.push(`Set ${params.pathPrefix}.streaming.mode (${params.aliasOnlyMode}) to keep the previous default while migrating flat streaming keys.`);
changed = true;
}
if (Object.keys(preview).length > 0) streaming.preview = preview;
if (Object.keys(block).length > 0) streaming.block = block;
updated.streaming = streaming;
return {
entry: updated,
changed
};
}
/**
* Root flat delivery aliases resolved per-key for every account (nested-first,
* flat-fallback), even when the account carried its own `streaming` value that
* replaces the root object wholesale at merge time. Capture them before root
* migration so replace-semantics channels can seed existing account streaming
* objects with the delivery settings those accounts previously inherited.
*/
function buildRootFlatDeliverySeed(entry, includePreviewChunk) {
const seed = {};
if (entry.chunkMode !== void 0) seed.chunkMode = entry.chunkMode;
const block = {};
if (entry.blockStreaming !== void 0) block.enabled = entry.blockStreaming;
if (entry.blockStreamingCoalesce !== void 0) block.coalesce = entry.blockStreamingCoalesce;
if (Object.keys(block).length > 0) seed.block = block;
if (includePreviewChunk === true && entry.draftChunk !== void 0) seed.preview = { chunk: entry.draftChunk };
return Object.keys(seed).length > 0 ? seed : null;
}
/**
* Rebuilds a materialized account streaming object with the per-slot
* precedence the runtime resolvers applied pre-migration. The slots disagree:
* - mode, block.enabled, preview.chunk resolve on the MERGED entry
* (src/channels/streaming.ts nested-first), so the root nested object
* outranked account flat aliases and preview.chunk picks atomically.
* - chunkMode resolves the raw account entry before the root entry
* (resolveChunkModeForProvider in src/auto-reply/chunk.ts), so an account
* flat chunkMode outranked every root spelling.
* - block.coalesce merges the account pick over the root pick per field
* (resolveProviderBlockStreamingCoalesce in
* src/auto-reply/reply/block-streaming.ts).
* One generic deep-fill cannot express that ladder, so seed slot by slot.
* Copying root values freezes inheritance at fix time by design (the change
* message records it); merged-entry channels (mattermost-style resolved
* accounts) would otherwise lose the root values entirely once the account
* owns a streaming object.
*/
function seedMaterializedAccountStreaming(params) {
const { created } = params;
const rootNested = params.rootNestedBefore ?? {};
const rootFlat = params.rootFlat ?? {};
let seeded = fillMissingRecordFields(structuredClone(rootNested), created).value;
seeded = fillMissingRecordFields(seeded, rootFlat).value;
seeded = fillMissingRecordFields(seeded, params.rootAfter).value;
if (created.chunkMode !== void 0) seeded = {
...seeded,
chunkMode: created.chunkMode
};
const createdCoalesce = asNullableRecord(asNullableRecord(created.block)?.coalesce);
if (createdCoalesce) {
const rootCoalesce = asNullableRecord(asNullableRecord(rootNested.block)?.coalesce) ?? asNullableRecord(asNullableRecord(rootFlat.block)?.coalesce);
seeded = {
...seeded,
block: {
...asNullableRecord(seeded.block),
coalesce: {
...structuredClone(rootCoalesce ?? {}),
...structuredClone(createdCoalesce)
}
}
};
}
const rootNestedPreviewChunk = asNullableRecord(rootNested.preview)?.chunk;
if (rootNestedPreviewChunk !== void 0 && asNullableRecord(created.preview)?.chunk !== void 0) seeded = {
...seeded,
preview: {
...asNullableRecord(seeded.preview),
chunk: structuredClone(rootNestedPreviewChunk)
}
};
return seeded;
}
/** Deep-fills record fields missing from target with copies of source values. */
function fillMissingRecordFields(target, source) {
let filled = false;
const value = { ...target };
for (const [key, sourceValue] of Object.entries(source)) {
if (sourceValue === void 0) continue;
const existing = value[key];
if (existing === void 0) {
value[key] = structuredClone(sourceValue);
filled = true;
continue;
}
const existingRecord = asNullableRecord(existing);
const sourceRecord = asNullableRecord(sourceValue);
if (!existingRecord || !sourceRecord) continue;
const merged = fillMissingRecordFields(existingRecord, sourceRecord);
if (merged.filled) {
value[key] = merged.value;
filled = true;
}
}
return {
value,
filled
};
}
/**
* Runs generic channel doctor alias migration for the root entry and accounts.
*
* Channel plugins provide streaming resolution and optional account-specific
* migrations so core can keep one compatibility path for all channel shapes.
*/
function normalizeLegacyChannelAliases(params) {
let updated = params.entry;
let changed = false;
const rootFlatDeliverySeed = params.seedAccountStreamingFromRoot === true ? buildRootFlatDeliverySeed(params.entry, params.resolveStreamingOptions(params.entry).includePreviewChunk) : null;
const rootNestedStreamingBefore = params.seedAccountStreamingFromRoot === true ? asNullableRecord(params.entry.streaming) : null;
if (params.normalizeDm === true) {
const dm = normalizeLegacyDmAliases({
entry: updated,
pathPrefix: params.pathPrefix,
changes: params.changes,
promoteAllowFrom: params.rootDmPromoteAllowFrom
});
updated = dm.entry;
changed = dm.changed;
}
const streaming = normalizeLegacyStreamingAliases({
entry: updated,
pathPrefix: params.pathPrefix,
changes: params.changes,
...params.resolveStreamingOptions(updated)
});
updated = streaming.entry;
changed = changed || streaming.changed;
const rawAccounts = asNullableRecord(updated.accounts);
if (!rawAccounts) return {
entry: updated,
changed
};
const rootStreaming = asNullableRecord(updated.streaming);
let accountsChanged = false;
const accounts = { ...rawAccounts };
for (const [accountId, rawAccount] of Object.entries(rawAccounts)) {
const account = asNullableRecord(rawAccount);
if (!account) continue;
let accountEntry = account;
let accountChanged = false;
const accountPathPrefix = `${params.pathPrefix}.accounts.${accountId}`;
if (params.normalizeAccountDm === true) {
const accountDm = normalizeLegacyDmAliases({
entry: accountEntry,
pathPrefix: accountPathPrefix,
changes: params.changes
});
accountEntry = accountDm.entry;
accountChanged = accountDm.changed;
}
const accountStreamingOptions = { ...params.resolveStreamingOptions(accountEntry) };
if (rootStreaming) delete accountStreamingOptions.aliasOnlyMode;
const beforeAccountStreaming = accountEntry.streaming;
const accountStreaming = normalizeLegacyStreamingAliases({
entry: accountEntry,
pathPrefix: accountPathPrefix,
changes: params.changes,
...accountStreamingOptions
});
accountEntry = accountStreaming.entry;
accountChanged = accountChanged || accountStreaming.changed;
if (params.seedAccountStreamingFromRoot === true && accountStreaming.changed && beforeAccountStreaming === void 0 && rootStreaming) {
const created = asNullableRecord(accountEntry.streaming);
if (created) {
const seeded = seedMaterializedAccountStreaming({
created,
rootNestedBefore: rootNestedStreamingBefore,
rootFlat: rootFlatDeliverySeed,
rootAfter: rootStreaming
});
if (JSON.stringify(seeded) !== JSON.stringify(created)) {
accountEntry = {
...accountEntry,
streaming: seeded
};
params.changes.push(`Copied ${params.pathPrefix}.streaming into ${accountPathPrefix}.streaming to keep inherited settings while migrating flat streaming keys.`);
}
}
} else if (rootFlatDeliverySeed && beforeAccountStreaming !== void 0) {
const accountStreamingObject = asNullableRecord(accountEntry.streaming);
if (accountStreamingObject) {
let seededAccount = accountStreamingObject;
if (rootFlatDeliverySeed.chunkMode !== void 0 && seededAccount.chunkMode === void 0) seededAccount = {
...seededAccount,
chunkMode: rootFlatDeliverySeed.chunkMode
};
const rootFlatBlock = asNullableRecord(rootFlatDeliverySeed.block);
const rootFlatBlockEnabled = rootFlatBlock?.enabled;
if (rootFlatBlockEnabled !== void 0 && asNullableRecord(seededAccount.block)?.enabled === void 0) seededAccount = {
...seededAccount,
block: {
...asNullableRecord(seededAccount.block),
enabled: rootFlatBlockEnabled
}
};
const rootFlatCoalesce = asNullableRecord(rootFlatBlock?.coalesce);
if (rootFlatCoalesce) {
const accountCoalesce = asNullableRecord(asNullableRecord(seededAccount.block)?.coalesce);
const mergedCoalesce = {
...structuredClone(rootFlatCoalesce),
...structuredClone(accountCoalesce ?? {})
};
if (JSON.stringify(mergedCoalesce) !== JSON.stringify(accountCoalesce ?? {})) seededAccount = {
...seededAccount,
block: {
...asNullableRecord(seededAccount.block),
coalesce: mergedCoalesce
}
};
}
const rootFlatPreviewChunk = asNullableRecord(rootFlatDeliverySeed.preview)?.chunk;
if (rootFlatPreviewChunk !== void 0 && asNullableRecord(seededAccount.preview)?.chunk === void 0) seededAccount = {
...seededAccount,
preview: {
...asNullableRecord(seededAccount.preview),
chunk: structuredClone(rootFlatPreviewChunk)
}
};
if (seededAccount !== accountStreamingObject) {
accountEntry = {
...accountEntry,
streaming: seededAccount
};
accountChanged = true;
params.changes.push(`Copied flat ${params.pathPrefix} delivery keys into ${accountPathPrefix}.streaming to keep inherited settings while migrating flat streaming keys.`);
}
}
}
const accountExtra = params.normalizeAccountExtra?.({
account: accountEntry,
accountId,
pathPrefix: accountPathPrefix,
changes: params.changes
});
if (accountExtra) {
accountEntry = accountExtra.entry;
accountChanged = accountChanged || accountExtra.changed;
}
if (accountChanged) {
accounts[accountId] = accountEntry;
accountsChanged = true;
}
}
if (accountsChanged) {
updated = {
...updated,
accounts
};
changed = true;
}
return {
entry: updated,
changed
};
}
/** Detects legacy streaming aliases on one channel or account config entry. */
function hasLegacyStreamingAliases(value, options) {
const entry = asNullableRecord(value);
if (!entry) return false;
return entry.streamMode !== void 0 || typeof entry.streaming === "boolean" || typeof entry.streaming === "string" || entry.chunkMode !== void 0 || entry.blockStreaming !== void 0 || entry.blockStreamingCoalesce !== void 0 || options?.includePreviewChunk === true && entry.draftChunk !== void 0 || options?.includeNativeTransport === true && entry.nativeStreaming !== void 0;
}
//#endregion
//#region src/infra/plain-object.ts
/**
* Config merge/patch accepts only `[object Object]` values, excluding Date/Map/Set/class instances.
* The stricter prototype contract prevents host objects from being merged as authored config.
*/
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
}
//#endregion
//#region src/infra/prototype-keys.ts
const BLOCKED_OBJECT_KEYS = /* @__PURE__ */ new Set([
"__proto__",
"prototype",
"constructor"
]);
/** Return true when assigning `key` could mutate an object prototype. */
function isBlockedObjectKey(key) {
return BLOCKED_OBJECT_KEYS.has(key);
}
//#endregion
//#region src/infra/deep-merge.ts
function sanitizePlainObject(value) {
const sanitized = {};
for (const [key, entry] of Object.entries(value)) {
if (isBlockedObjectKey(key)) continue;
sanitized[key] = isPlainObject(entry) ? sanitizePlainObject(entry) : entry;
}
return sanitized;
}
/** Merge plain objects while preserving OpenClaw's null, undefined, and array policies. */
function mergeDeep(base, override, options = {}) {
const arrays = options.arrays ?? "replace";
const undefinedValues = options.undefinedValues ?? "skip";
if (Array.isArray(base) && Array.isArray(override)) return arrays === "concat" ? [...base, ...override] : override;
if (!isPlainObject(base) || !isPlainObject(override)) return override === void 0 && undefinedValues === "skip" ? base : override;
const merged = sanitizePlainObject(base);
for (const [key, value] of Object.entries(override)) {
if (isBlockedObjectKey(key) || value === void 0 && undefinedValues === "skip") continue;
const current = merged[key];
if (isPlainObject(value)) merged[key] = isPlainObject(current) ? mergeDeep(current, value, options) : sanitizePlainObject(value);
else if (arrays === "concat" && Array.isArray(current) && Array.isArray(value)) merged[key] = [...current, ...value];
else merged[key] = value;
}
return merged;
}
//#endregion
//#region src/config/channel-doctor-helpers.ts
/** Applies one channel-specific doctor migration to every object-shaped account. */
function normalizeChannelAccounts(params) {
const rawAccounts = asNullableRecord(params.entry.accounts);
if (!rawAccounts) return {
entry: params.entry,
changed: false
};
let changed = false;
const accounts = { ...rawAccounts };
for (const [accountId, value] of Object.entries(rawAccounts)) {
const account = asNullableRecord(value);
if (!account) continue;
const normalized = params.normalizeAccount({
account,
accountId,
pathPrefix: `${params.pathPrefix}.accounts.${accountId}`,
changes: params.changes
});
if (normalized.changed) {
accounts[accountId] = normalized.entry;
changed = true;
}
}
return changed ? {
entry: {
...params.entry,
accounts
},
changed: true
} : {
entry: params.entry,
changed: false
};
}
/** Applies the same channel-specific doctor migration at root and account scope. */
function normalizeChannelConfigEntries(params) {
const changes = params.changes ?? [];
const channels = params.cfg.channels;
const entry = asNullableRecord(channels?.[params.channelId]);
if (!entry) return {
config: params.cfg,
changes
};
const channelPath = `channels.${params.channelId}`;
const root = params.normalizeEntry({
entry,
pathPrefix: channelPath,
changes
});
const accounts = normalizeChannelAccounts({
entry: root.entry,
pathPrefix: channelPath,
changes,
normalizeAccount: (accountParams) => params.normalizeEntry({
entry: accountParams.account,
accountId: accountParams.accountId,
pathPrefix: accountParams.pathPrefix,
changes: accountParams.changes
})
});
if (!root.changed && !accounts.changed) return {
config: params.cfg,
changes
};
return {
config: {
...params.cfg,
channels: {
...channels,
[params.channelId]: accounts.entry
}
},
changes
};
}
function stripRetiredKeys(params) {
if (params.recursive && Array.isArray(params.value)) {
let changed = false;
const value = params.value.map((item, index) => {
const stripped = stripRetiredKeys({
...params,
value: item,
pathPrefix: `${params.pathPrefix}[${index}]`
});
changed = changed || stripped.changed;
return stripped.value;
});
return {
value: changed ? value : params.value,
changed
};
}
const record = asNullableRecord(params.value);
if (!record) return {
value: params.value,
changed: false
};
let changed = false;
const value = {};
for (const [key, child] of Object.entries(record)) {
if (params.keys.has(key)) {
params.onRemove?.({
key,
pathPrefix: params.pathPrefix
});
changed = true;
continue;
}
if (!params.recursive) {
value[key] = child;
continue;
}
const stripped = stripRetiredKeys({
...params,
value: child,
pathPrefix: `${params.pathPrefix}.${key}`
});
changed = changed || stripped.changed;
value[key] = stripped.value;
}
return {
value: changed ? value : params.value,
changed
};
}
/** Removes retired keys recursively or from a channel root and its accounts. */
function stripRetiredChannelKeys(params) {
const channels = params.cfg.channels;
const entry = asNullableRecord(channels?.[params.channelId]);
if (!entry) return {
config: params.cfg,
changed: false
};
const channelPath = `channels.${params.channelId}`;
if (params.scope === "recursive") {
const stripped = stripRetiredKeys({
value: entry,
keys: params.keys,
pathPrefix: channelPath,
recursive: true,
onRemove: params.onRemove
});
return stripped.changed ? {
config: {
...params.cfg,
channels: {
...channels,
[params.channelId]: stripped.value
}
},
changed: true
} : {
config: params.cfg,
changed: false
};
}
const normalized = normalizeChannelConfigEntries({
cfg: params.cfg,
channelId: params.channelId,
normalizeEntry: (entryParams) => {
const stripped = stripRetiredKeys({
value: entryParams.entry,
keys: params.keys,
pathPrefix: entryParams.pathPrefix,
recursive: false,
onRemove: params.onRemove
});
return {
entry: stripped.value,
changed: stripped.changed
};
}
});
return {
config: normalized.config,
changed: normalized.config !== params.cfg
};
}
/** Materializes root/default-account inheritance after aliases create streaming. */
function materializeInheritedAccountStreaming(params) {
const channels = params.cfg.channels;
const entry = asNullableRecord(channels?.[params.channelId]);
const accounts = asNullableRecord(entry?.accounts);
if (!entry || !accounts) return params.cfg;
const rootStreaming = asNullableRecord(entry.streaming);
const defaultKey = Object.hasOwn(accounts, "default") ? "default" : Object.keys(accounts).find((key) => key.trim().toLowerCase() === "default");
let changed = false;
const nextAccounts = { ...accounts };
const accountIds = Object.keys(accounts).toSorted((left, right) => left === defaultKey ? -1 : right === defaultKey ? 1 : left.localeCompare(right));
for (const accountId of accountIds) {
if (asNullableRecord(params.accountsBefore?.[accountId])?.streaming !== void 0) continue;
const account = asNullableRecord(nextAccounts[accountId]);
const created = asNullableRecord(account?.streaming);
if (!account || !created) continue;
const defaultStreaming = defaultKey ? asNullableRecord(asNullableRecord(nextAccounts[defaultKey])?.streaming) : null;
const inherited = accountId === defaultKey ? rootStreaming : defaultStreaming ?? rootStreaming;
if (!inherited) continue;
const materialized = asNullableRecord(mergeDeep(inherited, created));
if (!materialized || isDeepStrictEqual(materialized, created)) continue;
nextAccounts[accountId] = {
...account,
streaming: materialized
};
changed = true;
const sourcePath = accountId !== defaultKey && defaultKey && defaultStreaming ? `channels.${params.channelId}.accounts.${defaultKey}.streaming` : `channels.${params.channelId}.streaming`;
params.changes.push(`Copied ${sourcePath} into channels.${params.channelId}.accounts.${accountId}.streaming to keep inherited settings while migrating flat streaming keys.`);
}
return changed ? {
...params.cfg,
channels: {
...channels,
[params.channelId]: {
...entry,
accounts: nextAccounts
}
}
} : params.cfg;
}
//#endregion
export { mergeDeep as a, hasLegacyAccountStreamingAliases as c, resolveLegacyAliasStreamingMode as d, uniqueStrings as f, normalizeOptionalString as h, stripRetiredChannelKeys as i, hasLegacyStreamingAliases as l, normalizeOptionalLowercaseString as m, normalizeChannelAccounts as n, isBlockedObjectKey as o, normalizeLowercaseStringOrEmpty as p, normalizeChannelConfigEntries as r, isPlainObject as s, materializeInheritedAccountStreaming as t, normalizeLegacyChannelAliases as u };