mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 14:20:35 +08:00
harden(authority): add pre-scale diagnostics and request safety
This commit is contained in:
@@ -61,6 +61,31 @@ export function normalizeAuthorityBlobPath(path = "") {
|
||||
return normalized.replace(/\/+$/g, "");
|
||||
}
|
||||
|
||||
function decodePathForValidation(path = "") {
|
||||
try {
|
||||
return decodeURIComponent(String(path || ""));
|
||||
} catch {
|
||||
return String(path || "");
|
||||
}
|
||||
}
|
||||
|
||||
function assertSafeAuthorityBlobPath(path = "", options = {}) {
|
||||
const normalized = normalizeAuthorityBlobPath(path);
|
||||
if (!normalized) {
|
||||
if (options.allowEmpty) return "";
|
||||
throw new Error("Authority Blob path is required");
|
||||
}
|
||||
const decoded = decodePathForValidation(normalized).replace(/\\/g, "/");
|
||||
if (/^[A-Za-z]:(?:\/|$)/.test(decoded) || decoded.includes(":/")) {
|
||||
throw new Error(`Unsafe Authority Blob path: ${normalized}`);
|
||||
}
|
||||
const segments = decoded.split("/").filter(Boolean);
|
||||
if (segments.some((segment) => segment === "." || segment === "..")) {
|
||||
throw new Error(`Unsafe Authority Blob path: ${normalized}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeBlobPayload(result = null) {
|
||||
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
||||
const source = result.file || result.blob || result.result || result;
|
||||
@@ -197,8 +222,9 @@ export class AuthorityBlobHttpClient {
|
||||
}
|
||||
|
||||
async writeText(payload = {}) {
|
||||
const path = assertSafeAuthorityBlobPath(payload.path || payload.name);
|
||||
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/write-file`, {
|
||||
path: normalizeAuthorityBlobPath(payload.path || payload.name),
|
||||
path,
|
||||
content: String(payload.text ?? payload.data ?? payload.content ?? ""),
|
||||
encoding: "utf8",
|
||||
createParents: true,
|
||||
@@ -206,22 +232,25 @@ export class AuthorityBlobHttpClient {
|
||||
}
|
||||
|
||||
async readJson(payload = {}) {
|
||||
const path = assertSafeAuthorityBlobPath(payload.path || payload.name);
|
||||
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/read-file`, {
|
||||
path: normalizeAuthorityBlobPath(payload.path || payload.name),
|
||||
path,
|
||||
encoding: "utf8",
|
||||
}, { signal: payload.signal });
|
||||
}
|
||||
|
||||
async delete(payload = {}) {
|
||||
const path = assertSafeAuthorityBlobPath(payload.path || payload.name);
|
||||
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/delete`, {
|
||||
path: normalizeAuthorityBlobPath(payload.path || payload.name),
|
||||
path,
|
||||
recursive: false,
|
||||
}, { signal: payload.signal });
|
||||
}
|
||||
|
||||
async stat(payload = {}) {
|
||||
const path = assertSafeAuthorityBlobPath(payload.path || payload.name);
|
||||
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/stat`, {
|
||||
path: normalizeAuthorityBlobPath(payload.path || payload.name),
|
||||
path,
|
||||
}, { signal: payload.signal });
|
||||
}
|
||||
}
|
||||
@@ -271,8 +300,7 @@ export class AuthorityBlobAdapter {
|
||||
|
||||
async writeJson(path, payload = null, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
if (!normalizedPath) throw new Error("Authority Blob path is required");
|
||||
const normalizedPath = assertSafeAuthorityBlobPath(path);
|
||||
const result = await callClient(this.client, ["writeJson", "putJson", "writeFile", "put"], "writeJson", {
|
||||
namespace: options.namespace || this.config.namespace,
|
||||
path: normalizedPath,
|
||||
@@ -287,8 +315,7 @@ export class AuthorityBlobAdapter {
|
||||
|
||||
async writeText(path, text = "", options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
if (!normalizedPath) throw new Error("Authority Blob path is required");
|
||||
const normalizedPath = assertSafeAuthorityBlobPath(path);
|
||||
const result = await callClient(this.client, ["writeText", "writeFile", "putText", "put"], "writeText", {
|
||||
namespace: options.namespace || this.config.namespace,
|
||||
path: normalizedPath,
|
||||
@@ -303,7 +330,7 @@ export class AuthorityBlobAdapter {
|
||||
|
||||
async readJson(path, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
const normalizedPath = assertSafeAuthorityBlobPath(path, { allowEmpty: true });
|
||||
if (!normalizedPath) return normalizeAuthorityBlobReadResult({ exists: false }, "");
|
||||
try {
|
||||
const result = await callClient(this.client, ["readJson", "getJson", "readFile", "get"], "readJson", {
|
||||
@@ -322,7 +349,7 @@ export class AuthorityBlobAdapter {
|
||||
|
||||
async delete(path, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
const normalizedPath = assertSafeAuthorityBlobPath(path, { allowEmpty: true });
|
||||
if (!normalizedPath) return normalizeAuthorityBlobDeleteResult({ exists: false }, "");
|
||||
try {
|
||||
const result = await callClient(this.client, ["delete", "deleteFile", "remove", "unlink"], "delete", {
|
||||
@@ -341,7 +368,7 @@ export class AuthorityBlobAdapter {
|
||||
|
||||
async stat(path, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
const normalizedPath = assertSafeAuthorityBlobPath(path, { allowEmpty: true });
|
||||
if (!normalizedPath) return normalizeAuthorityBlobReadResult({ exists: false }, "");
|
||||
try {
|
||||
const result = await callClient(this.client, ["stat", "head", "metadata"], "stat", {
|
||||
|
||||
@@ -245,6 +245,8 @@ export function normalizeAuthorityJobConfig(settings = {}, overrides = {}) {
|
||||
failOpen: source.authorityFailOpen !== false && source.failOpen !== false,
|
||||
preferStream: source.authorityJobPreferStream !== false && source.jobStreamPreferred !== false,
|
||||
pollIntervalMs: normalizeInteger(source.authorityJobPollIntervalMs ?? source.pollIntervalMs, 1200, 250, 30000),
|
||||
pollMaxIntervalMs: normalizeInteger(source.authorityJobPollMaxIntervalMs ?? source.pollMaxIntervalMs, 5000, 250, 60000),
|
||||
pollBackoffFactor: Math.min(5, Math.max(1, Number(source.authorityJobPollBackoffFactor ?? source.pollBackoffFactor ?? 1.25) || 1.25)),
|
||||
waitTimeoutMs: normalizeInteger(source.authorityJobWaitTimeoutMs ?? source.waitTimeoutMs, 0, 0, 3600000),
|
||||
...overrides,
|
||||
};
|
||||
@@ -439,20 +441,75 @@ export class AuthorityJobAdapter {
|
||||
id,
|
||||
timeoutMs: normalizeInteger(options.timeoutMs, this.config.waitTimeoutMs, 0, 3600000),
|
||||
});
|
||||
return normalizeAuthorityJobRecord(result?.job || result?.result || result);
|
||||
const normalized = normalizeAuthorityJobRecord(result?.job || result?.result || result);
|
||||
return {
|
||||
...normalized,
|
||||
waitDiagnostics: {
|
||||
mode: "client",
|
||||
pollCount: 0,
|
||||
elapsedMs: 0,
|
||||
timeoutMs: normalizeInteger(options.timeoutMs, this.config.waitTimeoutMs, 0, 3600000),
|
||||
terminal: normalized.terminal,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const timeoutMs = normalizeInteger(options.timeoutMs, this.config.waitTimeoutMs, 0, 3600000);
|
||||
const pollIntervalMs = normalizeInteger(options.pollIntervalMs, this.config.pollIntervalMs, 250, 30000);
|
||||
const initialPollIntervalMs = normalizeInteger(options.pollIntervalMs, this.config.pollIntervalMs, 250, 30000);
|
||||
const maxPollIntervalMs = Math.max(
|
||||
initialPollIntervalMs,
|
||||
normalizeInteger(options.pollMaxIntervalMs, this.config.pollMaxIntervalMs, 250, 60000),
|
||||
);
|
||||
const backoffFactor = Math.min(5, Math.max(1, Number(options.pollBackoffFactor ?? this.config.pollBackoffFactor) || 1));
|
||||
let pollIntervalMs = initialPollIntervalMs;
|
||||
let pollCount = 0;
|
||||
let lastJob = normalizeAuthorityJobRecord(null);
|
||||
while (true) {
|
||||
throwIfAborted(options.signal);
|
||||
const job = await this.get(id, options);
|
||||
if (job.terminal) return job;
|
||||
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
|
||||
return { ...job, status: "timeout", terminal: true, success: false, error: "wait timeout" };
|
||||
pollCount += 1;
|
||||
lastJob = job;
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
if (job.terminal) {
|
||||
return {
|
||||
...job,
|
||||
waitDiagnostics: {
|
||||
mode: "poll",
|
||||
pollCount,
|
||||
elapsedMs,
|
||||
timeoutMs,
|
||||
pollIntervalMs: initialPollIntervalMs,
|
||||
maxPollIntervalMs,
|
||||
backoffFactor,
|
||||
terminal: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
await sleep(pollIntervalMs, options.signal);
|
||||
if (timeoutMs > 0 && elapsedMs >= timeoutMs) {
|
||||
return {
|
||||
...job,
|
||||
status: "timeout",
|
||||
terminal: true,
|
||||
success: false,
|
||||
error: "wait timeout",
|
||||
waitDiagnostics: {
|
||||
mode: "poll",
|
||||
pollCount,
|
||||
elapsedMs,
|
||||
timeoutMs,
|
||||
pollIntervalMs: initialPollIntervalMs,
|
||||
maxPollIntervalMs,
|
||||
backoffFactor,
|
||||
terminal: false,
|
||||
lastStatus: lastJob.status,
|
||||
lastProgress: lastJob.progress,
|
||||
},
|
||||
};
|
||||
}
|
||||
const remainingMs = timeoutMs > 0 ? Math.max(0, timeoutMs - elapsedMs) : pollIntervalMs;
|
||||
await sleep(timeoutMs > 0 ? Math.min(pollIntervalMs, remainingMs) : pollIntervalMs, options.signal);
|
||||
pollIntervalMs = Math.min(maxPollIntervalMs, Math.max(initialPollIntervalMs, Math.ceil(pollIntervalMs * backoffFactor)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user