mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 14:20:35 +08:00
refactor(authority): complete v0.6-only sql/blob/jobs rollout
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
const DEFAULT_AUTHORITY_BASE_URL = "/api/plugins/authority";
|
||||
const DEFAULT_AUTHORITY_PROBE_INTERVAL_MS = 60000;
|
||||
|
||||
const SQL_FEATURES = ["sql", "sql.query", "sql.page", "sql.pageall", "querysql"];
|
||||
const SQL_MUTATION_FEATURES = ["sql", "sql.mutation", "sql.execute", "sql.write", "sql.transaction"];
|
||||
const TRIVIUM_FEATURES = ["trivium", "trivium.search", "trivium.query", "trivium.filterwhere", "trivium.bulkupsert"];
|
||||
const JOB_FEATURES = ["jobs", "jobs.list", "jobs.wait", "events", "sse"];
|
||||
const BLOB_FEATURES = ["blob", "blob.write", "privatefiles", "private.files", "files.private"];
|
||||
const SQL_FEATURES = ["sql", "sql.query", "sql.querypage", "sql.page", "sql.pageall", "querysql"];
|
||||
const SQL_MUTATION_FEATURES = ["sql", "sql.mutation", "sql.execute", "sql.exec", "sql.write", "sql.transaction"];
|
||||
const TRIVIUM_FEATURES = ["trivium", "trivium.search", "trivium.query", "trivium.filterwhere", "trivium.bulkupsert", "trivium.upsert", "trivium.bulkmutations"];
|
||||
const JOB_FEATURES = ["jobs", "jobs.background", "jobs.list", "jobs.wait", "diagnostics.jobspage", "events", "sse"];
|
||||
const BLOB_FEATURES = ["blob", "blob.write", "storage.blob", "transfers.blob", "transfers.fs", "fs.private", "privatefiles", "private.files", "files.private"];
|
||||
|
||||
function toBoolean(value, fallback = false) {
|
||||
if (typeof value === "boolean") return value;
|
||||
@@ -89,6 +89,189 @@ function readNowMs() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function clonePlain(value, fallbackValue = null) {
|
||||
if (value == null) return fallbackValue;
|
||||
if (typeof globalThis.structuredClone === "function") {
|
||||
try {
|
||||
return globalThis.structuredClone(value);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHeaderName(name = "") {
|
||||
return String(name || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function buildDefaultSessionInitConfig(source = {}) {
|
||||
const config = source && typeof source === "object" && !Array.isArray(source) ? source : {};
|
||||
return {
|
||||
extensionId: String(config.extensionId || "third-party/st-bme"),
|
||||
displayName: String(config.displayName || "ST-BME"),
|
||||
version: String(config.version || "0.0.0"),
|
||||
installType: String(config.installType || "local"),
|
||||
declaredPermissions: clonePlain(config.declaredPermissions, null) || {
|
||||
storage: { kv: true, blob: true },
|
||||
fs: { private: true },
|
||||
sql: { private: true },
|
||||
trivium: { private: true },
|
||||
jobs: { background: true },
|
||||
events: { channels: true },
|
||||
},
|
||||
...(config.uiLabel ? { uiLabel: String(config.uiLabel) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function withJsonHeaders(headers = {}) {
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...(headers || {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function readResponsePayload(response = null) {
|
||||
if (!response) return {};
|
||||
if (typeof response.json === "function") {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (typeof response.text === "function") {
|
||||
try {
|
||||
return { error: await response.text() };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function readPayloadMessage(payload = {}, fallback = "") {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return fallback;
|
||||
return String(payload.error || payload.message || payload.reason || fallback || "");
|
||||
}
|
||||
|
||||
function buildAuthorityPermissionEvaluateRequests(settings = {}, readiness = {}, options = {}) {
|
||||
const requests = [];
|
||||
const sqlTarget = String(options.sqlTarget || settings.sqlTarget || "default");
|
||||
const triviumTarget = String(options.triviumTarget || settings.triviumTarget || "st_bme_vectors");
|
||||
const jobTarget = String(options.jobTarget || settings.jobTarget || "delay");
|
||||
if (readiness.sql || readiness.sqlMutation) {
|
||||
requests.push({ resource: "sql.private", target: sqlTarget, reason: `Probe SQL capability for ${sqlTarget}` });
|
||||
}
|
||||
if (readiness.trivium) {
|
||||
requests.push({ resource: "trivium.private", target: triviumTarget, reason: `Probe Trivium capability for ${triviumTarget}` });
|
||||
}
|
||||
if (readiness.blob) {
|
||||
requests.push({ resource: "fs.private", reason: "Probe private file capability for Authority Blob adapter" });
|
||||
}
|
||||
if (readiness.jobs) {
|
||||
requests.push({ resource: "jobs.background", target: jobTarget, reason: `Probe Jobs capability for ${jobTarget}` });
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
async function verifyAuthorityDataPlane(baseUrl, fetchImpl, headers, settings = {}, readiness = {}, options = {}) {
|
||||
const initHeaders = withJsonHeaders(headers);
|
||||
const initResponse = await fetchImpl(`${baseUrl}/session/init`, {
|
||||
method: "POST",
|
||||
headers: initHeaders,
|
||||
body: JSON.stringify(buildDefaultSessionInitConfig(options.sessionInitConfig || settings)),
|
||||
});
|
||||
const initStatus = Number(initResponse?.status || 0);
|
||||
const initPayload = await readResponsePayload(initResponse);
|
||||
if (!initResponse?.ok) {
|
||||
return {
|
||||
sessionReady: false,
|
||||
permissionReady: false,
|
||||
reason: initStatus === 401 || initStatus === 403 ? "session-init-denied" : "session-init-failed",
|
||||
lastError: readPayloadMessage(initPayload, `HTTP ${initStatus || "unknown"}`),
|
||||
status: initStatus,
|
||||
};
|
||||
}
|
||||
|
||||
const sessionToken = String(initPayload?.sessionToken || initPayload?.token || "");
|
||||
if (!sessionToken) {
|
||||
return {
|
||||
sessionReady: false,
|
||||
permissionReady: false,
|
||||
reason: "session-token-missing",
|
||||
lastError: "session token missing",
|
||||
status: initStatus,
|
||||
};
|
||||
}
|
||||
|
||||
const sessionHeaders = {
|
||||
...withJsonHeaders(headers),
|
||||
...(Object.keys(headers || {}).some((name) => normalizeHeaderName(name) === "x-authority-session-token")
|
||||
? {}
|
||||
: { "x-authority-session-token": sessionToken }),
|
||||
};
|
||||
const currentResponse = await fetchImpl(`${baseUrl}/session/current`, {
|
||||
method: "GET",
|
||||
headers: sessionHeaders,
|
||||
});
|
||||
const currentStatus = Number(currentResponse?.status || 0);
|
||||
const currentPayload = await readResponsePayload(currentResponse);
|
||||
if (!currentResponse?.ok) {
|
||||
return {
|
||||
sessionReady: false,
|
||||
permissionReady: false,
|
||||
reason: currentStatus === 401 || currentStatus === 403 ? "session-invalid" : "session-current-failed",
|
||||
lastError: readPayloadMessage(currentPayload, `HTTP ${currentStatus || "unknown"}`),
|
||||
status: currentStatus,
|
||||
};
|
||||
}
|
||||
|
||||
const requests = buildAuthorityPermissionEvaluateRequests(settings, readiness, options);
|
||||
if (!requests.length) {
|
||||
return {
|
||||
sessionReady: true,
|
||||
permissionReady: true,
|
||||
reason: "ok",
|
||||
lastError: "",
|
||||
status: currentStatus,
|
||||
};
|
||||
}
|
||||
|
||||
const permissionResponse = await fetchImpl(`${baseUrl}/permissions/evaluate-batch`, {
|
||||
method: "POST",
|
||||
headers: sessionHeaders,
|
||||
body: JSON.stringify({ requests }),
|
||||
});
|
||||
const permissionStatus = Number(permissionResponse?.status || 0);
|
||||
const permissionPayload = await readResponsePayload(permissionResponse);
|
||||
if (!permissionResponse?.ok) {
|
||||
return {
|
||||
sessionReady: true,
|
||||
permissionReady: false,
|
||||
reason: permissionStatus === 401 || permissionStatus === 403 ? "permission-denied" : "permission-evaluate-failed",
|
||||
lastError: readPayloadMessage(permissionPayload, `HTTP ${permissionStatus || "unknown"}`),
|
||||
status: permissionStatus,
|
||||
};
|
||||
}
|
||||
|
||||
const results = Array.isArray(permissionPayload?.results) ? permissionPayload.results : [];
|
||||
const permissionReady = results.length === requests.length && results.every((result) => {
|
||||
const decision = String(result?.decision || result?.grant?.status || "").trim().toLowerCase();
|
||||
return decision === "granted";
|
||||
});
|
||||
return {
|
||||
sessionReady: true,
|
||||
permissionReady,
|
||||
reason: permissionReady ? "ok" : "permission-not-ready",
|
||||
lastError: permissionReady ? "" : "required Authority permissions are not granted",
|
||||
status: permissionStatus || currentStatus,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBaseUrl(baseUrl = DEFAULT_AUTHORITY_BASE_URL) {
|
||||
const normalized = String(baseUrl || DEFAULT_AUTHORITY_BASE_URL).trim() || DEFAULT_AUTHORITY_BASE_URL;
|
||||
return normalized.replace(/\/+$/, "");
|
||||
@@ -116,12 +299,7 @@ export function normalizeAuthoritySettings(settings = {}) {
|
||||
|
||||
export function buildAuthorityProbeUrls(baseUrl = DEFAULT_AUTHORITY_BASE_URL) {
|
||||
const normalizedBaseUrl = normalizeAuthorityBaseUrl(baseUrl);
|
||||
return [
|
||||
`${normalizedBaseUrl}/v1/diagnostics/probe`,
|
||||
`${normalizedBaseUrl}/v1/probe`,
|
||||
`${normalizedBaseUrl}/probe`,
|
||||
normalizedBaseUrl,
|
||||
];
|
||||
return [`${normalizedBaseUrl}/probe`];
|
||||
}
|
||||
|
||||
export function collectAuthorityFeatures(payload = {}) {
|
||||
@@ -295,7 +473,7 @@ export async function probeAuthorityCapabilities(options = {}) {
|
||||
for (const endpoint of buildAuthorityProbeUrls(settings.baseUrl)) {
|
||||
const startedAt = readNowMs();
|
||||
try {
|
||||
const response = await fetchImpl(endpoint, { method: "GET", headers });
|
||||
const response = await fetchImpl(endpoint, { method: "POST", headers });
|
||||
const finishedAt = readNowMs();
|
||||
const status = Number(response?.status || 0);
|
||||
lastStatus = status;
|
||||
@@ -340,13 +518,43 @@ export async function probeAuthorityCapabilities(options = {}) {
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
return normalizeAuthorityProbeResponse(payload, {
|
||||
const features = collectAuthorityFeatures(payload);
|
||||
const readiness = createFeatureReadiness(features);
|
||||
const missingFeatures = collectMissingFeatures(readiness);
|
||||
const healthy = payload?.healthy ?? payload?.ok ?? true;
|
||||
let sessionReady = payload?.sessionReady ?? payload?.session?.ready ?? payload?.session?.active;
|
||||
let permissionReady = payload?.permissionReady ?? payload?.permissions?.ready ?? payload?.authorized;
|
||||
let reason = missingFeatures.length ? "missing-required-features" : "ok";
|
||||
let dataPlaneLastError = "";
|
||||
let dataPlaneStatus = status;
|
||||
if (healthy) {
|
||||
const verified = await verifyAuthorityDataPlane(settings.baseUrl, fetchImpl, headers, settings, readiness, options);
|
||||
sessionReady = verified.sessionReady;
|
||||
permissionReady = verified.permissionReady;
|
||||
dataPlaneStatus = Number(verified.status || status || 0);
|
||||
dataPlaneLastError = String(verified.lastError || "");
|
||||
if (verified.reason && verified.reason !== "ok") {
|
||||
reason = verified.reason;
|
||||
}
|
||||
}
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: true,
|
||||
healthy: Boolean(healthy),
|
||||
sessionReady: Boolean(sessionReady),
|
||||
permissionReady: Boolean(permissionReady),
|
||||
features: Array.from(features),
|
||||
missingFeatures,
|
||||
reason,
|
||||
lastError: dataPlaneLastError,
|
||||
endpoint,
|
||||
status: dataPlaneStatus,
|
||||
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
|
||||
lastProbeAt: nowMs,
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
},
|
||||
settings,
|
||||
endpoint,
|
||||
status,
|
||||
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
|
||||
nowMs,
|
||||
});
|
||||
);
|
||||
} catch (error) {
|
||||
lastError = error?.message || String(error);
|
||||
}
|
||||
|
||||
194
runtime/authority-http-client.js
Normal file
194
runtime/authority-http-client.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import { normalizeAuthorityBaseUrl } from "./authority-capabilities.js";
|
||||
|
||||
export const AUTHORITY_PROTOCOL_AUTO = "auto";
|
||||
export const AUTHORITY_PROTOCOL_SERVER_PLUGIN_V06 = "server-plugin-v06";
|
||||
export const AUTHORITY_SESSION_HEADER = "x-authority-session-token";
|
||||
|
||||
function normalizeProtocol(value = AUTHORITY_PROTOCOL_AUTO) {
|
||||
const normalized = String(value || AUTHORITY_PROTOCOL_AUTO).trim().toLowerCase();
|
||||
if (["v06", "v0.6", "server-plugin", AUTHORITY_PROTOCOL_SERVER_PLUGIN_V06].includes(normalized)) {
|
||||
return AUTHORITY_PROTOCOL_SERVER_PLUGIN_V06;
|
||||
}
|
||||
return AUTHORITY_PROTOCOL_SERVER_PLUGIN_V06;
|
||||
}
|
||||
|
||||
function clonePlain(value, fallbackValue = null) {
|
||||
if (value == null) return fallbackValue;
|
||||
if (typeof globalThis.structuredClone === "function") {
|
||||
try {
|
||||
return globalThis.structuredClone(value);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHeaderName(name = "") {
|
||||
return String(name || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function hasSessionHeader(headers = {}) {
|
||||
return Object.keys(headers || {}).some((name) => normalizeHeaderName(name) === AUTHORITY_SESSION_HEADER);
|
||||
}
|
||||
|
||||
function buildDefaultSessionInitConfig(source = {}) {
|
||||
const config = source && typeof source === "object" && !Array.isArray(source) ? source : {};
|
||||
return {
|
||||
extensionId: String(config.extensionId || "third-party/st-bme"),
|
||||
displayName: String(config.displayName || "ST-BME"),
|
||||
version: String(config.version || "0.0.0"),
|
||||
installType: String(config.installType || "local"),
|
||||
declaredPermissions: clonePlain(config.declaredPermissions, null) || {
|
||||
storage: { kv: true, blob: true },
|
||||
fs: { private: true },
|
||||
sql: { private: true },
|
||||
trivium: { private: true },
|
||||
jobs: { background: true },
|
||||
events: { channels: true },
|
||||
},
|
||||
...(config.uiLabel ? { uiLabel: String(config.uiLabel) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function readPayloadErrorMessage(payload = null, fallback = "") {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return fallback;
|
||||
return String(payload.error || payload.message || payload.reason || fallback || "");
|
||||
}
|
||||
|
||||
async function readResponsePayload(response = null) {
|
||||
if (!response) return {};
|
||||
const contentType = String(response.headers?.get?.("content-type") || "").toLowerCase();
|
||||
if (contentType.includes("application/json") && typeof response.json === "function") {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (typeof response.json === "function") {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (typeof response.text === "function") {
|
||||
try {
|
||||
return { error: await response.text() };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export class AuthorityHttpError extends Error {
|
||||
constructor(message, options = {}) {
|
||||
super(message);
|
||||
this.name = "AuthorityHttpError";
|
||||
this.status = Number(options.status || 0);
|
||||
this.code = String(options.code || "");
|
||||
this.category = String(options.category || "");
|
||||
this.payload = clonePlain(options.payload, null);
|
||||
this.path = String(options.path || "");
|
||||
this.protocol = String(options.protocol || "");
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthorityHttpClient {
|
||||
constructor(options = {}) {
|
||||
this.baseUrl = normalizeAuthorityBaseUrl(options.baseUrl);
|
||||
this.fetchImpl = options.fetchImpl || (typeof fetch === "function" ? fetch.bind(globalThis) : null);
|
||||
this.headerProvider = typeof options.headerProvider === "function" ? options.headerProvider : null;
|
||||
this.protocol = normalizeProtocol(options.protocol || options.authorityProtocol);
|
||||
this.sessionToken = String(options.sessionToken || options.authoritySessionToken || "");
|
||||
this.sessionInitConfig = buildDefaultSessionInitConfig(options.sessionInitConfig || options.initConfig || options);
|
||||
this.sessionPromise = null;
|
||||
}
|
||||
|
||||
async buildHeaders({ session = false } = {}) {
|
||||
let provided = {};
|
||||
if (this.headerProvider) {
|
||||
provided = await this.headerProvider() || {};
|
||||
}
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...provided,
|
||||
};
|
||||
if (session && this.sessionToken && !hasSessionHeader(headers)) {
|
||||
headers[AUTHORITY_SESSION_HEADER] = this.sessionToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async ensureSession() {
|
||||
if (this.sessionToken) return this.sessionToken;
|
||||
if (!this.sessionPromise) {
|
||||
this.sessionPromise = this.requestJson("/session/init", {
|
||||
method: "POST",
|
||||
body: this.sessionInitConfig,
|
||||
session: false,
|
||||
protocol: AUTHORITY_PROTOCOL_SERVER_PLUGIN_V06,
|
||||
}).then((payload) => {
|
||||
const token = String(payload?.sessionToken || payload?.token || "");
|
||||
if (!token) {
|
||||
throw new AuthorityHttpError("Authority session init did not return a session token", {
|
||||
status: 0,
|
||||
path: "/session/init",
|
||||
protocol: AUTHORITY_PROTOCOL_SERVER_PLUGIN_V06,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
this.sessionToken = token;
|
||||
return token;
|
||||
}).catch((error) => {
|
||||
this.sessionPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return await this.sessionPromise;
|
||||
}
|
||||
|
||||
async requestJson(path, options = {}) {
|
||||
if (typeof this.fetchImpl !== "function") {
|
||||
throw new AuthorityHttpError("Authority fetch unavailable", {
|
||||
path,
|
||||
protocol: options.protocol || this.protocol,
|
||||
});
|
||||
}
|
||||
const method = String(options.method || "POST").toUpperCase();
|
||||
const session = Boolean(options.session);
|
||||
if (session && !this.sessionToken) {
|
||||
await this.ensureSession();
|
||||
}
|
||||
const headers = await this.buildHeaders({ session });
|
||||
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
...(method === "GET" || options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
|
||||
...(options.signal ? { signal: options.signal } : {}),
|
||||
});
|
||||
const status = Number(response?.status || 0);
|
||||
const payload = await readResponsePayload(response);
|
||||
if (!response?.ok) {
|
||||
const message = readPayloadErrorMessage(payload, `Authority HTTP ${status || "unknown"}`);
|
||||
throw new AuthorityHttpError(message || `Authority HTTP ${status || "unknown"}`, {
|
||||
status,
|
||||
code: payload?.code,
|
||||
category: payload?.category,
|
||||
payload,
|
||||
path,
|
||||
protocol: options.protocol || this.protocol,
|
||||
});
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
export function createAuthorityHttpClient(options = {}) {
|
||||
return new AuthorityHttpClient(options);
|
||||
}
|
||||
Reference in New Issue
Block a user