mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 06:42:44 +00:00
pairing: enforce strict account-scoped state
This commit is contained in:
@@ -243,7 +243,9 @@ function normalizeAllowEntry(channel: PairingChannel, entry: string): string {
|
||||
|
||||
function normalizeAllowFromList(channel: PairingChannel, store: AllowFromStore): string[] {
|
||||
const list = Array.isArray(store.allowFrom) ? store.allowFrom : [];
|
||||
return list.map((v) => normalizeAllowEntry(channel, String(v))).filter(Boolean);
|
||||
return dedupePreserveOrder(
|
||||
list.map((v) => normalizeAllowEntry(channel, String(v))).filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAllowFromInput(channel: PairingChannel, entry: string | number): string {
|
||||
@@ -268,20 +270,46 @@ async function readAllowFromStateForPath(
|
||||
channel: PairingChannel,
|
||||
filePath: string,
|
||||
): Promise<string[]> {
|
||||
const { value } = await readJsonFile<AllowFromStore>(filePath, {
|
||||
return (await readAllowFromStateForPathWithExists(channel, filePath)).entries;
|
||||
}
|
||||
|
||||
async function readAllowFromStateForPathWithExists(
|
||||
channel: PairingChannel,
|
||||
filePath: string,
|
||||
): Promise<{ entries: string[]; exists: boolean }> {
|
||||
const { value, exists } = await readJsonFile<AllowFromStore>(filePath, {
|
||||
version: 1,
|
||||
allowFrom: [],
|
||||
});
|
||||
return normalizeAllowFromList(channel, value);
|
||||
const entries = normalizeAllowFromList(channel, value);
|
||||
return { entries, exists };
|
||||
}
|
||||
|
||||
function readAllowFromStateForPathSync(channel: PairingChannel, filePath: string): string[] {
|
||||
return readAllowFromStateForPathSyncWithExists(channel, filePath).entries;
|
||||
}
|
||||
|
||||
function readAllowFromStateForPathSyncWithExists(
|
||||
channel: PairingChannel,
|
||||
filePath: string,
|
||||
): { entries: string[]; exists: boolean } {
|
||||
let raw = "";
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, "utf8");
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") {
|
||||
return { entries: [], exists: false };
|
||||
}
|
||||
return { entries: [], exists: false };
|
||||
}
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as AllowFromStore;
|
||||
return normalizeAllowFromList(channel, parsed);
|
||||
const entries = normalizeAllowFromList(channel, parsed);
|
||||
return { entries, exists: true };
|
||||
} catch {
|
||||
return [];
|
||||
// Keep parity with async reads: malformed JSON still means the file exists.
|
||||
return { entries: [], exists: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +334,24 @@ async function writeAllowFromState(filePath: string, allowFrom: string[]): Promi
|
||||
} satisfies AllowFromStore);
|
||||
}
|
||||
|
||||
async function readNonDefaultAccountAllowFrom(params: {
|
||||
channel: PairingChannel;
|
||||
env: NodeJS.ProcessEnv;
|
||||
accountId: string;
|
||||
}): Promise<string[]> {
|
||||
const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
|
||||
return await readAllowFromStateForPath(params.channel, scopedPath);
|
||||
}
|
||||
|
||||
function readNonDefaultAccountAllowFromSync(params: {
|
||||
channel: PairingChannel;
|
||||
env: NodeJS.ProcessEnv;
|
||||
accountId: string;
|
||||
}): string[] {
|
||||
const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
|
||||
return readAllowFromStateForPathSync(params.channel, scopedPath);
|
||||
}
|
||||
|
||||
async function updateAllowFromStoreEntry(params: {
|
||||
channel: PairingChannel;
|
||||
entry: string | number;
|
||||
@@ -348,11 +394,15 @@ export async function readChannelAllowFromStore(
|
||||
return await readAllowFromStateForPath(channel, filePath);
|
||||
}
|
||||
|
||||
if (!shouldIncludeLegacyAllowFromEntries(normalizedAccountId)) {
|
||||
return await readNonDefaultAccountAllowFrom({
|
||||
channel,
|
||||
env,
|
||||
accountId: normalizedAccountId,
|
||||
});
|
||||
}
|
||||
const scopedPath = resolveAllowFromPath(channel, env, accountId);
|
||||
const scopedEntries = await readAllowFromStateForPath(channel, scopedPath);
|
||||
if (!shouldIncludeLegacyAllowFromEntries(normalizedAccountId)) {
|
||||
return scopedEntries;
|
||||
}
|
||||
// Backward compatibility: legacy channel-level allowFrom store was unscoped.
|
||||
// Keep honoring it for default account to prevent re-pair prompts after upgrades.
|
||||
const legacyPath = resolveAllowFromPath(channel, env);
|
||||
@@ -371,11 +421,15 @@ export function readChannelAllowFromStoreSync(
|
||||
return readAllowFromStateForPathSync(channel, filePath);
|
||||
}
|
||||
|
||||
if (!shouldIncludeLegacyAllowFromEntries(normalizedAccountId)) {
|
||||
return readNonDefaultAccountAllowFromSync({
|
||||
channel,
|
||||
env,
|
||||
accountId: normalizedAccountId,
|
||||
});
|
||||
}
|
||||
const scopedPath = resolveAllowFromPath(channel, env, accountId);
|
||||
const scopedEntries = readAllowFromStateForPathSync(channel, scopedPath);
|
||||
if (!shouldIncludeLegacyAllowFromEntries(normalizedAccountId)) {
|
||||
return scopedEntries;
|
||||
}
|
||||
const legacyPath = resolveAllowFromPath(channel, env);
|
||||
const legacyEntries = readAllowFromStateForPathSync(channel, legacyPath);
|
||||
return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
|
||||
@@ -515,11 +569,12 @@ export async function upsertChannelPairingRequest(params: {
|
||||
nowMs,
|
||||
);
|
||||
reqs = prunedExpired;
|
||||
const normalizedMatchingAccountId = normalizePairingAccountId(normalizedAccountId);
|
||||
const existingIdx = reqs.findIndex((r) => {
|
||||
if (r.id !== id) {
|
||||
return false;
|
||||
}
|
||||
return requestMatchesAccountId(r, normalizePairingAccountId(normalizedAccountId));
|
||||
return requestMatchesAccountId(r, normalizedMatchingAccountId);
|
||||
});
|
||||
const existingCodes = new Set(
|
||||
reqs.map((req) =>
|
||||
|
||||
Reference in New Issue
Block a user