fix(POSTV1-06): 恢复贴纸与字体运行时资源链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
This commit is contained in:
@@ -16,7 +16,7 @@ const forbiddenDiagnosticPatterns = [
|
||||
/https?:\/\//i,
|
||||
];
|
||||
const safePauseReasons = new Set([
|
||||
"asset_root_state_missing", "balance_insufficient", "configured_disabled", "contract_blocked",
|
||||
"asset_manifest_invalid", "asset_root_missing", "asset_root_state_missing", "balance_insufficient", "configured_disabled", "contract_blocked",
|
||||
"contract_unverified", "gateway_balance_insufficient", "gateway_paused", "health_check_failed",
|
||||
"model_disabled", "provider_unavailable", "quota_exhausted", "service_state_missing", "unknown",
|
||||
"worker_degraded", "worker_state_missing", "worker_stopped",
|
||||
|
||||
@@ -19,7 +19,7 @@ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:p
|
||||
const require = createRequire(import.meta.url);
|
||||
const Database = require("better-sqlite3") as typeof import("better-sqlite3");
|
||||
|
||||
const assetIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i;
|
||||
const fixedDirectories = [
|
||||
"db",
|
||||
"content/references",
|
||||
@@ -82,13 +82,21 @@ export function readConfiguredLocalDataRoot(configFile = defaultInstanceConfigPa
|
||||
return resolve(candidate);
|
||||
}
|
||||
|
||||
export function readConfiguredAssetRoot(configFile = defaultInstanceConfigPath()) {
|
||||
const configuration = JSON.parse(readFileSync(configFile, "utf8")) as Record<string, unknown>;
|
||||
if (typeof configuration.asset_root !== "string" || !isAbsolute(configuration.asset_root)) {
|
||||
throw new Error("asset_root_configuration_invalid");
|
||||
}
|
||||
return resolve(configuration.asset_root);
|
||||
}
|
||||
|
||||
export interface ValidatedReadOnlyAssetRoot {
|
||||
absolute_root: string;
|
||||
ok: true;
|
||||
root_ref: string;
|
||||
}
|
||||
|
||||
interface PublicAssetEntry {
|
||||
export interface PublicAssetEntry {
|
||||
assetId: string;
|
||||
mimeType: string;
|
||||
relativePath: string;
|
||||
@@ -317,18 +325,19 @@ export function createPublicAssetResolver(input: {
|
||||
const roots = new Map(input.roots.map((root) => [root.root_ref, root.absolute_root]));
|
||||
const entries = new Map<string, PublicAssetEntry>();
|
||||
for (const entry of input.entries) {
|
||||
if (!assetIdPattern.test(entry.assetId) || entries.has(entry.assetId)) throw new Error("asset_id_invalid");
|
||||
const key = `${entry.resourceVersion}\u0000${entry.assetId}`;
|
||||
if (!assetIdPattern.test(entry.assetId) || entries.has(key)) throw new Error("asset_id_invalid");
|
||||
if (!roots.has(entry.rootRef)) throw new Error("asset_root_unvalidated");
|
||||
if (!/^[a-z0-9][a-z0-9._-]{0,79}$/i.test(entry.resourceVersion)) throw new Error("resource_version_invalid");
|
||||
if (!/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType)) throw new Error("mime_type_invalid");
|
||||
entries.set(entry.assetId, { ...entry });
|
||||
entries.set(key, { ...entry });
|
||||
}
|
||||
|
||||
return {
|
||||
read(resourceVersion, assetId) {
|
||||
if (!assetIdPattern.test(assetId)) return undefined;
|
||||
const entry = entries.get(assetId);
|
||||
if (!entry || entry.resourceVersion !== resourceVersion) return undefined;
|
||||
const entry = entries.get(`${resourceVersion}\u0000${assetId}`);
|
||||
if (!entry) return undefined;
|
||||
const root = roots.get(entry.rootRef);
|
||||
if (!root) return undefined;
|
||||
let path: string;
|
||||
|
||||
+13
-1
@@ -5,7 +5,7 @@ import { registrationNotice } from "@dada/shared-contracts";
|
||||
|
||||
import { createApp } from "./app.js";
|
||||
import { readBrowserSupportRelease } from "./browser-support.js";
|
||||
import { defaultInstanceConfigPath, ensureLocalDataRuntimeDirectories, readConfiguredLocalDataRoot } from "./local-data-root.js";
|
||||
import { defaultInstanceConfigPath, ensureLocalDataRuntimeDirectories, readConfiguredLocalDataRoot, type PublicAssetResolver } from "./local-data-root.js";
|
||||
import { ManagedStorage } from "./managed-storage.js";
|
||||
import { LatestExportService } from "./latest-exports.js";
|
||||
import { CreditService } from "./credits.js";
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { MockAmapAdapter, type AmapAdapter } from "./amap-adapter.js";
|
||||
import { StickerReleaseService } from "./sticker-releases.js";
|
||||
import { createAdminDiagnosticsProvider, createAdminServicesStorageProvider } from "./admin-state.js";
|
||||
import { loadConfiguredRuntimeAssets, type RuntimeAssetState } from "./runtime-assets.js";
|
||||
|
||||
const credentialChannelEnabled = process.argv.includes("--dada-credential-stdin");
|
||||
let registration: RegistrationService | undefined;
|
||||
@@ -36,6 +37,8 @@ let models: ModelConfigurationService | undefined;
|
||||
let generations: GenerationSubmissionService | undefined;
|
||||
let recentAssets: RecentAssetService | undefined;
|
||||
let stickers: StickerReleaseService | undefined;
|
||||
let publicAssets: PublicAssetResolver | undefined;
|
||||
let assetRootState: RuntimeAssetState | undefined;
|
||||
let amap: AmapAdapter = new MockAmapAdapter();
|
||||
let localTestAuth = false;
|
||||
const instanceConfigPath = process.env.DADA_INSTANCE_CONFIG_PATH ?? defaultInstanceConfigPath();
|
||||
@@ -49,6 +52,13 @@ if (credentialChannelEnabled) {
|
||||
.digest();
|
||||
const dataRoot = readConfiguredLocalDataRoot(instanceConfigPath);
|
||||
ensureLocalDataRuntimeDirectories(dataRoot);
|
||||
const runtimeAssets = loadConfiguredRuntimeAssets({
|
||||
configFile: instanceConfigPath,
|
||||
dataRoot,
|
||||
trustedManifestPath: resolve("asset-metadata", "manifest.json"),
|
||||
});
|
||||
publicAssets = runtimeAssets.publicAssets;
|
||||
assetRootState = runtimeAssets.state;
|
||||
const databasePath = join(dataRoot, "db", "dada.sqlite3");
|
||||
registration = new RegistrationService({
|
||||
adminAllowlistPepper: Buffer.from(clients.adminAllowlistPepper),
|
||||
@@ -101,6 +111,7 @@ const adminServicesStorage = registration
|
||||
database: registration.database,
|
||||
...(models ? { models } : {}),
|
||||
...(storage ? { storage } : {}),
|
||||
...(assetRootState ? { assetRoot: assetRootState } : {}),
|
||||
})
|
||||
: undefined;
|
||||
const adminDiagnostics = adminServicesStorage
|
||||
@@ -120,6 +131,7 @@ const app = await createApp({
|
||||
...(registration && localTestAuth ? { localTestAuth: true } : {}),
|
||||
...(models ? { models } : {}),
|
||||
...(projects ? { projects } : {}),
|
||||
...(publicAssets ? { publicAssets } : {}),
|
||||
...(registration ? { registration } : {}),
|
||||
...(recentAssets ? { recentAssets } : {}),
|
||||
...(stickers ? { stickers } : {}),
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
|
||||
import {
|
||||
createPublicAssetResolver,
|
||||
readConfiguredAssetRoot,
|
||||
validateReadOnlyAssetRoot,
|
||||
type PublicAssetEntry,
|
||||
type PublicAssetResolver,
|
||||
} from "./local-data-root.js";
|
||||
|
||||
const rootRef = "p0a_runtime_assets";
|
||||
const schemaVersion = "DadaRuntimeAssets/v1";
|
||||
const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i;
|
||||
const releasePattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i;
|
||||
const shaPattern = /^[a-f0-9]{64}$/i;
|
||||
|
||||
export interface RuntimeAssetState {
|
||||
checked_at: string;
|
||||
configured: boolean;
|
||||
pause_reason: "asset_manifest_invalid" | "asset_root_missing" | "asset_root_state_missing" | null;
|
||||
status: "active" | "unavailable";
|
||||
}
|
||||
|
||||
export interface LoadedRuntimeAssets {
|
||||
publicAssets?: PublicAssetResolver;
|
||||
state: RuntimeAssetState;
|
||||
}
|
||||
|
||||
function parseRuntimeManifest(bytes: Buffer): PublicAssetEntry[] {
|
||||
const value = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
|
||||
if (value.schema_version !== schemaVersion || value.source !== "external_read_only" || value.root_ref !== rootRef) {
|
||||
throw new Error("runtime_asset_manifest_invalid");
|
||||
}
|
||||
if (!Array.isArray(value.entries) || value.entries.length === 0) throw new Error("runtime_asset_manifest_invalid");
|
||||
return value.entries.map((candidate) => {
|
||||
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("runtime_asset_manifest_invalid");
|
||||
const entry = candidate as Record<string, unknown>;
|
||||
if (
|
||||
typeof entry.assetId !== "string" || !assetIdPattern.test(entry.assetId)
|
||||
|| typeof entry.mimeType !== "string" || !/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType)
|
||||
|| typeof entry.relativePath !== "string" || entry.relativePath.includes("\\") || entry.relativePath.split("/").includes("..")
|
||||
|| typeof entry.resourceVersion !== "string" || !releasePattern.test(entry.resourceVersion)
|
||||
|| entry.rootRef !== rootRef
|
||||
|| typeof entry.sha256 !== "string" || !shaPattern.test(entry.sha256)
|
||||
) throw new Error("runtime_asset_manifest_invalid");
|
||||
return entry as unknown as PublicAssetEntry;
|
||||
});
|
||||
}
|
||||
|
||||
function unavailable(
|
||||
configured: boolean,
|
||||
pauseReason: Exclude<RuntimeAssetState["pause_reason"], null>,
|
||||
checkedAt: string,
|
||||
): LoadedRuntimeAssets {
|
||||
return { state: { checked_at: checkedAt, configured, pause_reason: pauseReason, status: "unavailable" } };
|
||||
}
|
||||
|
||||
export function loadConfiguredRuntimeAssets(input: {
|
||||
configFile: string;
|
||||
dataRoot: string;
|
||||
trustedManifestPath: string;
|
||||
clock?: () => number;
|
||||
}): LoadedRuntimeAssets {
|
||||
const checkedAt = new Date((input.clock ?? Date.now)()).toISOString();
|
||||
let assetRoot: string;
|
||||
try {
|
||||
assetRoot = readConfiguredAssetRoot(input.configFile);
|
||||
} catch {
|
||||
return unavailable(false, "asset_root_state_missing", checkedAt);
|
||||
}
|
||||
if (!existsSync(input.trustedManifestPath)) return unavailable(true, "asset_manifest_invalid", checkedAt);
|
||||
try {
|
||||
const trustedBytes = readFileSync(input.trustedManifestPath);
|
||||
const entries = parseRuntimeManifest(trustedBytes);
|
||||
const validatedRoot = validateReadOnlyAssetRoot({
|
||||
dataRoot: input.dataRoot,
|
||||
expectedSha256: createHash("sha256").update(trustedBytes).digest("hex"),
|
||||
manifestRelativePath: "manifest.json",
|
||||
root: assetRoot,
|
||||
rootRef,
|
||||
});
|
||||
if (!validatedRoot.ok) {
|
||||
return unavailable(true, validatedRoot.reason === "asset_root_missing" ? "asset_root_missing" : "asset_manifest_invalid", checkedAt);
|
||||
}
|
||||
return {
|
||||
publicAssets: createPublicAssetResolver({ entries, roots: [validatedRoot] }),
|
||||
state: { checked_at: checkedAt, configured: true, pause_reason: null, status: "active" },
|
||||
};
|
||||
} catch {
|
||||
return unavailable(true, "asset_manifest_invalid", checkedAt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user