merge: integrate WP5-05 and WP5-06 baseline

# Conflicts:
#	package.json
This commit is contained in:
suyx
2026-08-04 10:54:59 +08:00
33 changed files with 3434 additions and 25 deletions
+150
View File
@@ -0,0 +1,150 @@
import { randomUUID } from "node:crypto";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import { RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
import { createAssetReleaseManifest } from "../../packages/asset-release-manifest/src/index.js";
const now = Date.parse("2026-08-03T10:00:00.000Z");
const releaseVersion = "asset-20260803.1";
const publicId = "7f0c9530-a7d9-4bf1-8c65-0e9298dd04ac";
const previewId = "ab18fd72-60e1-44e3-a9a0-3dfccb12e17c";
const ungrantedPreviewId = "d8fe890d-6df4-46a9-a578-96c1f8361ac0";
const privateId = "e3792605-5252-4d3b-a101-827408ab3515";
const headers = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
const roots: string[] = [];
const registrations: RegistrationService[] = [];
function addUser(registration: RegistrationService, role: "super_admin" | "user") {
const userId = randomUUID();
registration.database.prepare(`INSERT INTO users (
user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at
) VALUES (?, ?, ?, 'active', ?, ?, ?)`).run(
userId,
`${role}-${userId}@example.invalid`,
role,
role === "user" ? 1 : 0,
randomUUID(),
now,
);
if (role === "user") {
registration.database.prepare("INSERT INTO user_profiles (user_id, creator_name, social_id) VALUES (?, 'Asset User', '@asset_user')").run(userId);
registration.database.prepare("INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, 10, 0, ?)").run(userId, now);
} else {
registration.database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(userId);
}
return { session: registration.issueAuthenticatedSession(userId, role === "user" ? "user" : "admin"), userId };
}
function evidence(name: string, value: unknown) {
const directory = process.env.DADA_EVIDENCE_DIR_WP5_RES;
if (!directory) return;
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, name), `${JSON.stringify(value, null, 2)}\n`);
}
function harness() {
const root = mkdtempSync(join(tmpdir(), "dada-wp5-04-api-"));
roots.push(root);
const registration = new RegistrationService({
challengePepper: Buffer.alloc(32, 0x31), clock: () => now,
currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath: join(root, "dada.sqlite3"),
invitePepper: Buffer.alloc(32, 0x32), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x33),
});
registrations.push(registration);
const owner = addUser(registration, "user");
const intruder = addUser(registration, "user");
const admin = addUser(registration, "super_admin");
const assetReleases = createAssetReleaseManifest({
items: [
{ access_class: "public_release_asset", cache_kind: "thumbnail", content: Buffer.from("public"), mime_type: "image/png", relative_path: "public/FLOWER001.png", resource_id: publicId, root_ref: "canonical-assets" },
{ access_class: "internal_preview_asset", content: Buffer.from("preview"), mime_type: "image/webp", relative_path: "preview/FLOWER009.webp", resource_id: previewId, root_ref: "canonical-assets" },
{ access_class: "internal_preview_asset", content: Buffer.from("ungranted-preview"), mime_type: "image/webp", relative_path: "preview/FLOWER010.webp", resource_id: ungrantedPreviewId, root_ref: "canonical-assets" },
{ access_class: "private_user_asset", content: Buffer.from("private"), mime_type: "image/png", owner_id: owner.userId, relative_path: "private/generated.png", resource_id: privateId, root_ref: "managed-assets" },
],
release_version: releaseVersion,
});
let previewGranted = true;
let previewChecks = 0;
const appPromise = createApp({
assetReleases,
browserGate: false,
networkBoundary: { allowTestPort: true },
previewAssetAuthorizer: ({ resourceId, userId }) => {
previewChecks += 1;
return previewGranted && userId === owner.userId && resourceId === previewId;
},
privateAssetAdminAuthorizer: ({ adminUserId, ownerId }) => adminUserId === admin.userId && ownerId === owner.userId,
registration,
});
return { admin, appPromise, intruder, owner, previewChecks: () => previewChecks, revokePreview: () => { previewGranted = false; } };
}
afterEach(() => {
for (const registration of registrations.splice(0)) registration.close();
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TDD-WP5-RES-001 three access classes", () => {
it("separates route projections, per-request authorization, and cache headers", async () => {
const test = harness();
const app = await test.appPromise;
const ownerCookie = `dada_session=${test.owner.session.sessionToken}`;
const intruderCookie = `dada_session=${test.intruder.session.sessionToken}`;
const adminCookie = `dada_admin_session=${test.admin.session.sessionToken}`;
const publicManifest = await app.inject({ headers, method: "GET", url: `/api/v1/assets/public/${releaseVersion}/manifest` });
const publicAsset = await app.inject({ headers, method: "GET", url: `/api/v1/assets/public/${releaseVersion}/${publicId}` });
const previewManifest = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/assets/preview/${releaseVersion}/manifest` });
const previewAsset = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/assets/preview/${releaseVersion}/${previewId}` });
const privateManifest = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/manifest` });
const privateAsset = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/${privateId}` });
const deniedPrivate = await app.inject({ headers: { ...headers, cookie: intruderCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/${privateId}` });
const controlledAdmin = await app.inject({ headers: { ...headers, cookie: adminCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/${privateId}` });
expect(publicManifest.statusCode).toBe(200);
expect(publicManifest.headers["cache-control"]).toBe("public, max-age=31536000, immutable");
expect(publicManifest.json().items).toEqual([expect.objectContaining({ access_class: "public_release_asset", resource_id: publicId })]);
expect(JSON.stringify(publicManifest.json())).not.toMatch(/preview|private|relative_path|root_ref|[A-Z]:\\\\/i);
expect(publicAsset.statusCode).toBe(200);
expect(publicAsset.rawPayload).toEqual(Buffer.from("public"));
expect(publicAsset.headers["cache-control"]).toBe("public, max-age=31536000, immutable");
expect(previewManifest.statusCode).toBe(200);
expect(previewManifest.headers["cache-control"]).toBe("private, no-store");
expect(previewManifest.json().items).toEqual([expect.objectContaining({ resource_id: previewId })]);
expect(JSON.stringify(previewManifest.json())).not.toContain(ungrantedPreviewId);
expect(previewAsset.statusCode).toBe(200);
expect(previewAsset.headers["cache-control"]).toBe("private, no-store");
expect(privateManifest.json().items).toEqual([expect.objectContaining({ access_class: "private_user_asset", resource_id: privateId })]);
expect(privateAsset.rawPayload).toEqual(Buffer.from("private"));
expect(privateAsset.headers["cache-control"]).toBe("private, no-store");
expect(deniedPrivate.statusCode).toBe(404);
expect(controlledAdmin.statusCode).toBe(200);
const publicGuess = await app.inject({ headers, method: "GET", url: `/api/v1/assets/public/${releaseVersion}/${privateId}` });
expect(publicGuess.statusCode).toBe(404);
test.revokePreview();
const revoked = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/assets/preview/${releaseVersion}/${previewId}` });
expect(revoked.statusCode).toBe(404);
expect(test.previewChecks()).toBe(4);
evidence("response.json", {
controlled_admin_status: controlledAdmin.statusCode,
private_intruder_status: deniedPrivate.statusCode,
public_guess_status: publicGuess.statusCode,
revoked_preview_status: revoked.statusCode,
});
evidence("headers.json", {
private: privateAsset.headers["cache-control"],
preview: previewAsset.headers["cache-control"],
public: publicAsset.headers["cache-control"],
});
await app.close();
});
});
+109
View File
@@ -0,0 +1,109 @@
import { createHash, randomUUID } from "node:crypto";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
import { RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
import { StickerReleaseService } from "../../apps/api/src/sticker-releases.js";
const now = Date.parse("2026-08-03T12:00:00.000Z");
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEklEQVQImWO4E6XxHxkzEBQAANIxHF3ECQOzAAAAAElFTkSuQmCC", "base64");
const roots: string[] = [];
const closeables: Array<{ close(): void }> = [];
const baseHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
async function multipart() {
const form = new FormData();
form.append("stable_id", "STK1408");
form.append("part", "25");
form.append("order", "184");
form.append("enabled", "true");
form.append("original_byte_size", String(png.byteLength));
form.append("original_sha256", createHash("sha256").update(png).digest("hex"));
form.append("sticker_file", new Blob([png], { type: "image/png" }), "STK1408.png");
const serialized = new Response(form);
return { contentType: serialized.headers.get("content-type")!, payload: Buffer.from(await serialized.arrayBuffer()) };
}
function evidence(value: unknown) {
const directory = process.env.DADA_EVIDENCE_DIR_WP5_UPL;
if (!directory) return;
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, "response.json"), `${JSON.stringify(value, null, 2)}\n`);
}
afterEach(() => {
for (const value of closeables.splice(0).reverse()) value.close();
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TASK-WP5-05 admin sticker API", () => {
it("requires admin mutation controls, publishes upload, and exposes current and versioned public resources", async () => {
const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp5-05-api-"));
roots.push(dataRoot);
mkdirSync(join(dataRoot, "db"), { recursive: true });
const databasePath = join(dataRoot, "db", "dada.sqlite3");
const registration = new RegistrationService({
challengePepper: Buffer.alloc(32, 0x41), clock: () => now,
currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath,
invitePepper: Buffer.alloc(32, 0x42), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x43),
});
const storage = new ManagedStorage({ dataRoot, databasePath });
const stickers = new StickerReleaseService({ clock: () => now, databasePath, storage });
closeables.push(stickers, storage, registration);
const adminId = randomUUID();
registration.database.prepare(`INSERT INTO users (
user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at
) VALUES (?, 'sticker-admin@example.invalid', 'super_admin', 'active', 0, ?, ?)`).run(adminId, randomUUID(), now);
registration.database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(adminId);
const session = registration.issueAuthenticatedSession(adminId, "admin");
const csrf = registration.issueAdminCsrfToken(session.sessionToken);
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration, stickers });
const uploadKey = `sticker-${randomUUID()}-${randomUUID()}`;
const body = await multipart();
const denied = await app.inject({ headers: { ...baseHeaders, "content-type": body.contentType }, method: "POST", payload: body.payload, url: "/api/v1/admin/assets/static-stickers" });
expect(denied.statusCode).toBe(401);
const acceptedBody = await multipart();
const accepted = await app.inject({
headers: {
...baseHeaders, cookie: `dada_admin_session=${session.sessionToken}`, "content-type": acceptedBody.contentType,
"idempotency-key": uploadKey, "x-csrf-token": csrf,
},
method: "POST", payload: acceptedBody.payload, url: "/api/v1/admin/assets/static-stickers",
});
expect(accepted.statusCode).toBe(201);
expect(accepted.json()).toMatchObject({ item: { stable_id: "STK1408" }, release_version: "asset-20260803.1" });
const replayBody = await multipart();
const replay = await app.inject({
headers: {
...baseHeaders, cookie: `dada_admin_session=${session.sessionToken}`, "content-type": replayBody.contentType,
"idempotency-key": uploadKey, "x-csrf-token": csrf,
},
method: "POST", payload: replayBody.payload, url: "/api/v1/admin/assets/static-stickers",
});
expect(replay.statusCode).toBe(200);
expect(replay.json()).toMatchObject({ created: false, release_version: "asset-20260803.1" });
expect(storage.inspectCounts()).toMatchObject({ managed_files: 2 });
const adminList = await app.inject({ headers: { ...baseHeaders, cookie: `dada_admin_session=${session.sessionToken}` }, method: "GET", url: "/api/v1/admin/assets/static-stickers" });
const publicList = await app.inject({ headers: baseHeaders, method: "GET", url: "/api/v1/static-stickers/current" });
const original = await app.inject({ headers: baseHeaders, method: "GET", url: "/api/v1/assets/public/asset-20260803.1/STK1408" });
const thumbnail = await app.inject({ headers: baseHeaders, method: "GET", url: "/api/v1/assets/public/asset-20260803.1/STK1408?variant=thumbnail" });
expect(adminList.statusCode).toBe(200);
expect(publicList.json()).toMatchObject({ count: 1, items: [{ stable_id: "STK1408" }] });
expect(original.rawPayload).toEqual(png);
expect(thumbnail.statusCode).toBe(200);
expect(thumbnail.headers["content-type"]).toMatch(/^image\/png/);
expect(thumbnail.rawPayload).not.toEqual(png);
evidence({ admin_list_status: adminList.statusCode, public_count: publicList.json().count, upload_status: accepted.statusCode });
await app.close();
});
});
+149
View File
@@ -0,0 +1,149 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { expect, test } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
const releaseVersion = "asset-20260803.1";
const publicId = "7f0c9530-a7d9-4bf1-8c65-0e9298dd04ac";
const previewId = "ab18fd72-60e1-44e3-a9a0-3dfccb12e17c";
const privateId = "e3792605-5252-4d3b-a101-827408ab3515";
let vite: ViteDevServer;
let webUrl: string;
const requestCounts = { preview: 0, private: 0, public: 0 };
function writeEvidence(directory: string | undefined, name: string, value: unknown) {
if (!directory) return;
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, name), `${JSON.stringify(value, null, 2)}\n`);
}
test.beforeAll(async () => {
vite = await createServer({
configFile: false,
plugins: [{
name: "wp5-04-three-resource-classes",
configureServer(server) {
server.middlewares.use((request, response, next) => {
const routes = [
{ access: "public", body: "public-content", id: publicId, prefix: "/api/v1/assets/public/" },
{ access: "preview", body: "preview-content", id: previewId, prefix: "/api/v1/assets/preview/" },
{ access: "private", body: "private-content", id: privateId, prefix: "/api/v1/private-assets/" },
] as const;
const route = routes.find((item) => request.url === `${item.prefix}${releaseVersion}/${item.id}`);
if (!route) return next();
requestCounts[route.access] += 1;
response.statusCode = 200;
response.setHeader("Cache-Control", route.access === "public" ? "public, max-age=31536000, immutable" : "private, no-store");
response.setHeader("Content-Type", "application/octet-stream");
response.end(route.body);
});
},
}],
publicDir: resolve("apps/web/public"),
root: process.cwd(),
server: { host: "127.0.0.1", port: 0 },
});
await vite.listen();
const address = vite.httpServer?.address();
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
webUrl = `http://127.0.0.1:${address.port}`;
});
test.afterAll(async () => vite.close());
test("TDD-WP5-04 enumerates no preview or private client state", async ({ context, page }) => {
requestCounts.preview = 0;
requestCounts.private = 0;
requestCounts.public = 0;
await page.goto(`${webUrl}/tests/e2e/fixtures/public-asset-cache.html`);
await expect(page.locator("#status")).toHaveText("ready");
await page.reload();
await expect(page.locator("#status")).toHaveText("ready");
const online = await page.evaluate(async ({ privateId, previewId, publicId, releaseVersion }) => {
const cache = window.dadaCacheProbe.cache;
await cache.clear();
const cached = await cache.cache({
access_class: "public_release_asset",
cache_kind: "thumbnail",
release_version: releaseVersion,
resource_id: publicId,
});
const rejected = await Promise.all([
cache.cache({ access_class: "internal_preview_asset", cache_kind: "thumbnail", release_version: releaseVersion, resource_id: previewId }),
cache.cache({ access_class: "private_user_asset", cache_kind: "thumbnail", release_version: releaseVersion, resource_id: privateId }),
]);
const preview = await fetch(`/api/v1/assets/preview/${releaseVersion}/${previewId}`);
const privateAsset = await fetch(`/api/v1/private-assets/${releaseVersion}/${privateId}`);
const inspection = await cache.inspect();
const registrations = await navigator.serviceWorker.getRegistrations();
const databases = await indexedDB.databases();
return {
cached,
inspection,
private_bytes: (await privateAsset.arrayBuffer()).byteLength,
private_cache_control: privateAsset.headers.get("cache-control"),
preview_bytes: (await preview.arrayBuffer()).byteLength,
preview_cache_control: preview.headers.get("cache-control"),
rejected,
service_workers: registrations.map((registration) => ({
active: registration.active?.state,
scope: registration.scope,
script_url: registration.active?.scriptURL,
})),
indexed_db_names: databases.map((database) => database.name).filter(Boolean).sort(),
local_storage_keys: Object.keys(localStorage),
session_storage_keys: Object.keys(sessionStorage),
};
}, { privateId, previewId, publicId, releaseVersion });
await context.setOffline(true);
const offline = await page.evaluate(async ({ privateId, previewId, publicId, releaseVersion }) => {
const read = async (url: string) => {
try {
const response = await fetch(url);
return { body: await response.text(), status: response.status };
} catch {
return { body: null, status: "network_error" };
}
};
return {
preview: await read(`/api/v1/assets/preview/${releaseVersion}/${previewId}`),
private: await read(`/api/v1/private-assets/${releaseVersion}/${privateId}`),
public: await read(`/api/v1/assets/public/${releaseVersion}/${publicId}`),
};
}, { privateId, previewId, publicId, releaseVersion });
await context.setOffline(false);
expect(online.cached.status).toBe("cached");
expect(online.rejected).toEqual([
{ status: "rejected_not_allowlisted" },
{ status: "rejected_not_allowlisted" },
]);
expect(online.preview_cache_control).toBe("private, no-store");
expect(online.private_cache_control).toBe("private, no-store");
expect(online.inspection.cache_keys).toHaveLength(1);
expect(online.inspection.cache_names).toEqual(["dada-public-assets-v1"]);
expect(online.inspection.entries).toEqual([expect.objectContaining({ resource_id: publicId })]);
expect(JSON.stringify(online.inspection)).not.toContain(previewId);
expect(JSON.stringify(online.inspection)).not.toContain(privateId);
expect(online.indexed_db_names).toEqual(["dada-public-asset-cache-v1"]);
expect(online.local_storage_keys).toEqual([]);
expect(online.session_storage_keys).toEqual([]);
expect(online.service_workers).toHaveLength(1);
expect(offline.public).toEqual({ body: "public-content", status: 200 });
expect(offline.preview.status).toBe("network_error");
expect(offline.private.status).toBe("network_error");
expect(requestCounts).toEqual({ preview: 1, private: 1, public: 1 });
const cacheEnumeration = {
business_database_calls: 0,
offline,
online,
origin_request_counts: { ...requestCounts },
};
writeEvidence(process.env.DADA_EVIDENCE_DIR_WP5_CACHE, "cache-enumeration.json", cacheEnumeration);
writeEvidence(process.env.DADA_EVIDENCE_DIR_WP5_CACHE, "service-worker.json", { registrations: online.service_workers });
writeEvidence(process.env.DADA_EVIDENCE_DIR_WP5_RES, "cache-enumeration.json", cacheEnumeration);
});
+131
View File
@@ -0,0 +1,131 @@
import { mkdirSync } from "node:fs";
import { resolve } from "node:path";
import { expect, test, type Page } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
let vite: ViteDevServer;
let webUrl: string;
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEklEQVQImWO4E6XxHxkzEBQAANIxHF3ECQOzAAAAAElFTkSuQmCC", "base64");
const adminSession = { csrf_token: "csrf-wp5-05-admin-000000000000000000000000000000000" };
const projectId = "00000000-0000-4000-8000-000000001405";
const userSession = {
audience: "user", authenticated: true, credits: { available_balance: 10, reserved_balance: 0 },
csrf_token: "csrf-wp5-05-user-0000000000000000000000000000000000",
expires_at: "2026-09-03T12:00:00.000Z",
user: { creator_name: "Sticker User", role: "user", social_id: "@sticker", status: "active", user_id: projectId },
};
function asset(stableId = "STK1408") {
return {
enabled: true, file_state: "committed", height: 3, mime: "image/png", mime_type: "image/png", order: 184,
original_byte_size: png.byteLength, original_filename: `${stableId}.png`, original_reference: `/api/v1/assets/public/asset-20260803.1/${stableId}`,
origin: "admin_uploaded", part: 25, relative_path: `managed-assets/stickers/original/${stableId}.png`, resource_version: "asset-20260803.1",
sha256: "0".repeat(64), stable_id: stableId, thumbnail_byte_size: 75,
thumbnail_reference: { media: "thumbnail", resource_id: stableId, resource_version: "asset-20260803.1", url: `/api/v1/assets/public/asset-20260803.1/${stableId}?variant=thumbnail` }, width: 4,
};
}
function assetsResponse(status: "active" | "full" | "unavailable" = "active", items = [asset()]) {
return {
count: items.length, items, release_version: items.length ? "asset-20260803.1" : null,
storage: { capacity_notice_level: status === "active" ? "normal" : "critical", hard_limit_bytes: 5_368_709_120, managed_content_bytes: status === "full" ? 5_368_709_120 : 150, storage_status: status },
};
}
test.beforeAll(async () => {
vite = await createServer({ configFile: resolve("apps/web/vite.config.ts"), root: resolve("apps/web"), server: { host: "127.0.0.1", port: 0 } });
await vite.listen();
const address = vite.httpServer?.address();
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
webUrl = `http://127.0.0.1:${address.port}`;
});
test.afterAll(async () => vite.close());
async function routeAdminSession(page: Page) {
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill({ body: JSON.stringify(adminSession), contentType: "application/json", status: 200 }));
await page.route("**/api/v1/assets/public/**", (route) => route.fulfill({ body: png, contentType: "image/png", status: 200 }));
}
test("TDD-WP5-UPL-001-upload-metering uploads and publishes with stable metadata controls", async ({ page }) => {
await routeAdminSession(page);
let uploaded = false;
let multipartBody = "";
await page.route("**/api/v1/admin/assets/static-stickers", async (route) => {
if (route.request().method() === "POST") {
multipartBody = route.request().postDataBuffer()?.toString("latin1") ?? "";
uploaded = true;
return route.fulfill({ body: JSON.stringify({ created: true, item: asset(), release_version: "asset-20260803.1" }), contentType: "application/json", status: 201 });
}
return route.fulfill({ body: JSON.stringify(assetsResponse("active", uploaded ? [asset()] : [])), contentType: "application/json", status: 200 });
});
await page.goto(`${webUrl}/admin/assets`);
await expect(page.getByRole("heading", { name: "普通贴纸" })).toBeVisible();
await expect(page.getByText("当前没有后台上传的普通贴纸。")).toBeVisible();
await page.getByLabel("贴纸文件").setInputFiles({ buffer: png, mimeType: "image/png", name: "STK1408.png" });
await page.getByRole("button", { name: "上传并发布" }).click();
await expect(page.getByRole("rowheader", { name: /STK1408/ })).toBeVisible();
expect(multipartBody).toContain("STK1408");
expect(multipartBody).toContain("image/png");
expect(multipartBody).toContain("original_sha256");
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_WP5_UPL;
if (evidenceRoot) {
const directory = resolve(evidenceRoot, "screenshots");
mkdirSync(directory, { recursive: true });
await page.screenshot({ fullPage: true, path: resolve(directory, "admin-assets-uploaded.png") });
}
});
test("storage full disables every upload control and load failure exposes retry", async ({ page }) => {
await page.setViewportSize({ height: 844, width: 390 });
await routeAdminSession(page);
await page.route("**/api/v1/admin/assets/static-stickers", (route) => route.fulfill({ body: JSON.stringify(assetsResponse("full")), contentType: "application/json", status: 200 }));
await page.goto(`${webUrl}/admin/assets`);
await expect(page.getByText("当前存储状态禁止新增原图和缩略图。")).toBeVisible();
await expect(page.getByLabel("贴纸文件")).toBeDisabled();
await expect(page.getByRole("button", { name: "上传并发布" })).toBeDisabled();
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_WP5_UPL;
if (evidenceRoot) {
const directory = resolve(evidenceRoot, "screenshots");
mkdirSync(directory, { recursive: true });
await page.screenshot({ fullPage: true, path: resolve(directory, "admin-assets-full-mobile.png") });
}
await page.unroute("**/api/v1/admin/assets/static-stickers");
await page.route("**/api/v1/admin/assets/static-stickers", (route) => route.fulfill({ status: 503 }));
await page.reload();
await expect(page.getByRole("alert")).toContainText("素材状态暂时无法读取");
await expect(page.getByRole("button", { name: "重试" })).toBeVisible();
});
test("the editor merges the current uploaded release and saves its resource version", async ({ page }) => {
let savedResourceVersion = "";
let originalRequests = 0;
await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(userSession), contentType: "application/json", status: 200 }));
await page.route("**/api/v1/static-stickers/current", (route) => route.fulfill({ body: JSON.stringify({ count: 1, items: [asset()], release_version: "asset-20260803.1" }), contentType: "application/json", status: 200 }));
await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify({
canvas_state: { background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: null }, elements: [], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1 },
created_at: "2026-08-03T12:00:00.000Z", current_image_id: null, images: [], name: "上传贴纸", project_id: projectId, ratio: "3:4", state_version: 1,
}), contentType: "application/json", status: 200 }));
await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
const body = route.request().postDataJSON() as { canvas_state: { elements: Array<{ resource_version: string }> } };
savedResourceVersion = body.canvas_state.elements[0]?.resource_version ?? "";
await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: 2 }), contentType: "application/json", status: 200 });
});
await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json", status: 200 }));
await page.route("**/api/v1/assets/public/**", async (route) => {
if (!new URL(route.request().url()).searchParams.has("variant")) originalRequests += 1;
await route.fulfill({ body: png, contentType: "image/png", status: 200 });
});
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
await page.getByRole("button", { name: "普通贴纸", exact: true }).click();
await expect(page.getByText("共 1,408 张", { exact: true })).toBeVisible();
const list = page.getByTestId("static-sticker-list");
await list.evaluate((element) => { element.scrollTop = element.scrollHeight; element.dispatchEvent(new Event("scroll")); });
await list.getByRole("button", { name: "添加贴纸 STK1408" }).click();
await expect.poll(() => savedResourceVersion).toBe("asset-20260803.1");
await expect.poll(() => originalRequests).toBeGreaterThan(0);
});
@@ -0,0 +1,135 @@
import { createHash, randomUUID } from "node:crypto";
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { Readable } from "node:stream";
import { afterEach, describe, expect, it } from "vitest";
import { HARD_LIMIT_BYTES, ManagedStorage, StorageCapacityError, StorageUnavailableError } from "../../apps/api/src/managed-storage.js";
import { StickerReleaseService } from "../../apps/api/src/sticker-releases.js";
const now = Date.parse("2026-08-03T12:00:00.000Z");
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEklEQVQImWO4E6XxHxkzEBQAANIxHF3ECQOzAAAAAElFTkSuQmCC", "base64");
const webp = Buffer.from("UklGRjoAAABXRUJQVlA4IC4AAADQAQCdASoGAAUAAUAmJaACdLoB+AADsAD+9IiH/pNnibPE2fJI/+Uq8Fjc3wAA", "base64");
const roots: string[] = [];
const closeables: Array<{ close(): void }> = [];
function filesBelow(path: string): string[] {
if (!existsSync(path)) return [];
return readdirSync(path, { withFileTypes: true }).flatMap((entry) => {
const child = join(path, entry.name);
return entry.isDirectory() ? filesBelow(child) : [child];
});
}
function evidence(name: string, value: unknown) {
const directory = process.env.DADA_EVIDENCE_DIR_WP5_UPL;
if (!directory) return;
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, name), `${JSON.stringify(value, null, 2)}\n`);
}
function fixture() {
const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp5-05-integration-"));
roots.push(dataRoot);
mkdirSync(join(dataRoot, "db"), { recursive: true });
const databasePath = join(dataRoot, "db", "dada.sqlite3");
const storage = new ManagedStorage({ dataRoot, databasePath });
const stickers = new StickerReleaseService({ clock: () => now, databasePath, storage });
closeables.push(stickers, storage);
return { dataRoot, stickers, storage };
}
async function upload(stickers: StickerReleaseService, stableId: string, order: number, bytes = png, mimeType: "image/png" | "image/webp" = "image/png") {
return stickers.upload({
actorId: randomUUID(),
content: Readable.from(bytes),
enabled: true,
expectedByteSize: bytes.byteLength,
expectedMimeType: mimeType,
expectedSha256: createHash("sha256").update(bytes).digest("hex"),
fileName: mimeType === "image/png" ? `${stableId}.png` : `${stableId}.webp`,
idempotencyKey: `upload-${randomUUID()}-${randomUUID()}`,
order,
part: 25,
stableId,
});
}
afterEach(() => {
for (const value of closeables.splice(0).reverse()) value.close();
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TDD-WP5-UPL-001 upload metering", () => {
it("decodes PNG, publishes an immutable release, meters original and thumbnail, and preserves old release reads", async () => {
const test = fixture();
const before = { files: filesBelow(join(test.dataRoot, "managed-assets")), state: test.storage.getState() };
const published = await upload(test.stickers, "STK1408", 184);
const afterUpload = test.storage.getState();
expect(published.release_version).toBe("asset-20260803.1");
expect(published.item).toMatchObject({ enabled: true, height: 3, mime_type: "image/png", origin: "admin_uploaded", stable_id: "STK1408", width: 4 });
expect(afterUpload.managed_content_bytes).toBe(published.original.byte_size + published.thumbnail.byte_size);
expect(test.storage.inspectCounts()).toMatchObject({ active_reservations: 0, managed_files: 2, pending_cleanup: 0 });
expect(filesBelow(join(test.dataRoot, "managed-assets"))).toHaveLength(2);
expect(test.stickers.listPublic()).toMatchObject({ count: 1, items: [{ stable_id: "STK1408" }] });
const disabled = test.stickers.update({ actorId: randomUUID(), enabled: false, stableId: "STK1408" });
expect(disabled.release_version).toBe("asset-20260803.2");
expect(test.stickers.listPublic().items).toEqual([]);
expect(test.stickers.listPublic(published.release_version).items).toHaveLength(1);
expect(test.stickers.readPublicAsset(published.release_version, "STK1408", "original")?.bytes).toEqual(png);
expect(test.stickers.readPublicAsset(disabled.release_version, "STK1408", "original")).toBeUndefined();
evidence("fs-before.json", before);
evidence("fs-after.json", { files: filesBelow(join(test.dataRoot, "managed-assets")), state: test.storage.getState() });
evidence("db-diff.json", { releases: test.stickers.inspectCounts(), storage: test.storage.inspectCounts() });
});
it("allows exact equality then blocks full and unavailable storage without partial files or rows", async () => {
const sizing = fixture();
const measured = await upload(sizing.stickers, "STK1408", 184);
const writeBytes = measured.original.byte_size + measured.thumbnail.byte_size;
const exact = fixture();
exact.storage.applyControlledMeasurement(HARD_LIMIT_BYTES - writeBytes);
const exactResult = await upload(exact.stickers, "STK1408", 184);
expect(exactResult.original.byte_size + exactResult.thumbnail.byte_size).toBe(writeBytes);
expect(exact.storage.getState().storage_status).toBe("full");
const filesAtFull = filesBelow(join(exact.dataRoot, "managed-assets"));
const countsAtFull = exact.stickers.inspectCounts();
await expect(upload(exact.stickers, "STK1409", 185)).rejects.toBeInstanceOf(StorageCapacityError);
expect(filesBelow(join(exact.dataRoot, "managed-assets"))).toEqual(filesAtFull);
expect(exact.stickers.inspectCounts()).toEqual(countsAtFull);
const unavailable = fixture();
unavailable.storage.setAvailability({ dataRootWritable: false, diskSpaceAvailable: true, sqliteWritable: true });
await expect(upload(unavailable.stickers, "STK1408", 184)).rejects.toBeInstanceOf(StorageUnavailableError);
expect(filesBelow(join(unavailable.dataRoot, "managed-assets"))).toEqual([]);
expect(unavailable.stickers.inspectCounts()).toEqual({ items: 0, releases: 0, upload_receipts: 0 });
});
it("rejects a forged PNG before release or managed-file commit", async () => {
const test = fixture();
const forged = Buffer.concat([png.subarray(0, 8), Buffer.from("not-a-decodable-png")]);
await expect(upload(test.stickers, "STK1408", 184, forged)).rejects.toThrow("content_decode_invalid");
expect(test.storage.inspectCounts()).toMatchObject({ active_reservations: 0, managed_files: 0 });
expect(test.stickers.inspectCounts()).toEqual({ items: 0, releases: 0, upload_receipts: 0 });
expect(filesBelow(join(test.dataRoot, "managed-assets"))).toEqual([]);
});
it("decodes WebP and rejects stable-ID or part-order conflicts without extra writes", async () => {
const test = fixture();
const published = await upload(test.stickers, "STK1408", 184, webp, "image/webp");
expect(published.item).toMatchObject({ height: 5, mime_type: "image/webp", width: 6 });
expect(test.stickers.readPublicAsset(published.release_version, "STK1408", "original")?.bytes).toEqual(webp);
const files = filesBelow(join(test.dataRoot, "managed-assets"));
const counts = test.stickers.inspectCounts();
await expect(upload(test.stickers, "STK1408", 185)).rejects.toMatchObject({ httpStatus: 409, reason: "sticker_stable_id_conflict" });
await expect(upload(test.stickers, "STK1409", 184)).rejects.toMatchObject({ httpStatus: 409, reason: "sticker_order_conflict" });
expect(filesBelow(join(test.dataRoot, "managed-assets"))).toEqual(files);
expect(test.stickers.inspectCounts()).toEqual(counts);
});
});
@@ -0,0 +1,139 @@
import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
import { RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
import { StickerReleaseService } from "../../apps/api/src/sticker-releases.js";
import { ProjectPurgeCleanup } from "../../apps/worker/src/project-purge-cleanup.js";
const roots: string[] = [];
const closeables: Array<{ close(): void }> = [];
function fixture() {
const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp5-06-cleanup-"));
roots.push(dataRoot);
mkdirSync(join(dataRoot, "db"), { recursive: true });
const databasePath = join(dataRoot, "db", "dada.sqlite3");
const registration = new RegistrationService({
challengePepper: Buffer.alloc(32, 0x61),
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
databasePath,
invitePepper: Buffer.alloc(32, 0x62),
resend: new MockResendAdapter(),
sessionPepper: Buffer.alloc(32, 0x63),
});
const storage = new ManagedStorage({ dataRoot, databasePath });
const stickers = new StickerReleaseService({ databasePath, storage });
closeables.push(stickers, storage, registration);
return { dataRoot, databasePath, database: registration.database, storage };
}
function seedAdmin(database: RegistrationService["database"]) {
const adminId = randomUUID();
database.prepare(`
INSERT INTO users (
user_id, normalized_email, role, status, counts_toward_stage_limit,
registration_id, created_at
) VALUES (?, ?, 'super_admin', 'active', 0, ?, ?)
`).run(adminId, `${adminId}@example.invalid`, randomUUID(), Date.now());
database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(adminId);
return adminId;
}
async function seedHistoricalPair(test: ReturnType<typeof fixture>) {
const original = await test.storage.commitBufferFixture("sticker_original", "STK2401.png", Buffer.from("original-history"));
const thumbnail = await test.storage.commitBufferFixture("sticker_thumbnail", "STK2401-thumbnail.png", Buffer.from("thumbnail-history"));
const insert = test.database.prepare(`
INSERT INTO sticker_managed_file_history (
managed_file_id, stable_id, resource_version, file_kind, created_at
) VALUES (?, 'STK2401', 'asset-20260701.1', ?, ?)
`);
insert.run(original.file_id, "original", Date.now());
insert.run(thumbnail.file_id, "thumbnail", Date.now());
return { original, thumbnail };
}
afterEach(() => {
for (const value of closeables.splice(0).reverse()) value.close();
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TDD-WP5-CLN-001 sticker history cleanup", () => {
it("denies the whole batch when a reference appears after the candidate snapshot", async () => {
const test = fixture();
const adminId = seedAdmin(test.database);
const files = await seedHistoricalPair(test);
const candidates = test.storage.listAssetCleanupCandidates();
expect(candidates.items).toEqual(expect.arrayContaining([
expect.objectContaining({ file_id: files.original.file_id, file_kind: "original", reference_count: 0, stable_id: "STK2401" }),
expect.objectContaining({ file_id: files.thumbnail.file_id, file_kind: "thumbnail", reference_count: 0, stable_id: "STK2401" }),
]));
const intent = test.storage.createAssetCleanupIntent({
actorId: adminId,
fileIds: [files.original.file_id, files.thumbnail.file_id],
idempotencyKey: `cleanup-${randomUUID()}-${randomUUID()}`,
snapshotVersion: candidates.candidate_snapshot_version,
});
test.storage.addAssetReference(files.original.file_id, "release");
expect(() => test.storage.confirmAssetCleanupIntent({
actorId: adminId,
confirmationToken: intent.confirmation_token,
requestId: intent.request_id,
})).toThrow("ASSET_HISTORY_REFERENCE_CONFLICT");
expect(test.database.prepare("SELECT status FROM asset_cleanup_requests WHERE request_id = ?").get(intent.request_id)).toEqual({ status: "denied" });
expect(test.database.prepare("SELECT COUNT(*) AS count FROM file_cleanup_queue WHERE status = 'pending'").get()).toEqual({ count: 0 });
expect(test.database.prepare("SELECT COUNT(*) AS count FROM admin_operation_logs WHERE target_ref = ? AND result = 'failed'").get(intent.request_id)).toEqual({ count: 1 });
});
it("requires the same admin, queues without reducing capacity, then remeasures after physical deletion", async () => {
const test = fixture();
const adminId = seedAdmin(test.database);
const otherAdminId = seedAdmin(test.database);
const files = await seedHistoricalPair(test);
const bytesBefore = test.storage.getState().managed_content_bytes;
const candidates = test.storage.listAssetCleanupCandidates();
const intent = test.storage.createAssetCleanupIntent({
actorId: adminId,
fileIds: [files.original.file_id, files.thumbnail.file_id],
idempotencyKey: `cleanup-${randomUUID()}-${randomUUID()}`,
snapshotVersion: candidates.candidate_snapshot_version,
});
expect(() => test.storage.confirmAssetCleanupIntent({
actorId: otherAdminId,
confirmationToken: intent.confirmation_token,
requestId: intent.request_id,
})).toThrow("ASSET_CLEANUP_CANDIDATE_STALE");
const queued = test.storage.confirmAssetCleanupIntent({
actorId: adminId,
confirmationToken: intent.confirmation_token,
requestId: intent.request_id,
});
expect(queued).toMatchObject({ file_count: 2, status: "queued" });
expect(test.storage.getState().managed_content_bytes).toBe(bytesBefore);
expect(existsSync(join(test.dataRoot, files.original.relative_path))).toBe(true);
expect(test.database.prepare("SELECT COUNT(*) AS count FROM file_cleanup_queue WHERE status = 'pending'").get()).toEqual({ count: 2 });
const worker = new ProjectPurgeCleanup({ dataRoot: test.dataRoot, databasePath: test.databasePath });
const result = worker.processFileCleanup();
worker.close();
expect(result).toEqual({ completed: 2, failed: 0 });
expect(existsSync(join(test.dataRoot, files.original.relative_path))).toBe(false);
expect(existsSync(join(test.dataRoot, files.thumbnail.relative_path))).toBe(false);
expect(test.storage.getState()).toMatchObject({ managed_content_bytes: 0, storage_status: "active" });
expect(test.database.prepare("SELECT status FROM asset_cleanup_requests WHERE request_id = ?").get(intent.request_id)).toEqual({ status: "completed" });
expect(test.database.prepare("SELECT operation_type, result FROM admin_operation_logs WHERE target_ref = ? ORDER BY occurred_at").all(intent.request_id)).toEqual([
{ operation_type: "asset_cleanup_requested", result: "succeeded" },
{ operation_type: "asset_cleanup_validated", result: "succeeded" },
{ operation_type: "asset_cleanup_scheduled", result: "succeeded" },
{ operation_type: "asset_cleanup_physical_completed", result: "succeeded" },
]);
});
});
@@ -0,0 +1,101 @@
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { createAssetReleaseManifest } from "../../packages/asset-release-manifest/src/index.js";
const releaseVersion = "asset-20260803.1";
const publicId = "7f0c9530-a7d9-4bf1-8c65-0e9298dd04ac";
const previewId = "ab18fd72-60e1-44e3-a9a0-3dfccb12e17c";
const privateId = "e3792605-5252-4d3b-a101-827408ab3515";
function fixture() {
return createAssetReleaseManifest({
items: [
{
access_class: "public_release_asset",
cache_kind: "thumbnail",
content: Buffer.from("public-content"),
mime_type: "image/png",
relative_path: "public/thumbnails/FLOWER001.png",
resource_id: publicId,
root_ref: "canonical-assets",
},
{
access_class: "internal_preview_asset",
content: Buffer.from("preview-content"),
mime_type: "image/webp",
relative_path: "preview/batch-7/FLOWER009.webp",
resource_id: previewId,
root_ref: "canonical-assets",
},
{
access_class: "private_user_asset",
content: Buffer.from("private-content"),
mime_type: "image/png",
owner_id: "owner-1",
relative_path: "private/owner-1/generated.png",
resource_id: privateId,
root_ref: "managed-assets",
},
],
release_version: releaseVersion,
});
}
describe("TASK-WP5-04 immutable asset release manifest", () => {
it("projects only the selected access class and never exposes source paths", () => {
const manifest = fixture();
const projected = manifest.project("public_release_asset", releaseVersion);
expect(Object.isFrozen(projected)).toBe(true);
expect(projected?.manifest_sha256).toMatch(/^[0-9a-f]{64}$/);
expect(projected?.items).toEqual([expect.objectContaining({
access_class: "public_release_asset",
resource_id: publicId,
sha256: createHash("sha256").update("public-content").digest("hex"),
url: `/api/v1/assets/public/${releaseVersion}/${publicId}`,
})]);
const serialized = JSON.stringify(projected);
expect(serialized).not.toContain(previewId);
expect(serialized).not.toContain(privateId);
expect(serialized).not.toContain("relative_path");
expect(serialized).not.toContain("root_ref");
expect(serialized).not.toMatch(/[A-Z]:\\\\/i);
});
it("keeps resource identifiers opaque, verifies file hashes, and rejects unsafe manifests", () => {
const manifest = fixture();
expect(manifest.read("public_release_asset", releaseVersion, publicId)?.bytes).toEqual(Buffer.from("public-content"));
expect(manifest.read("public_release_asset", releaseVersion, previewId)).toBeUndefined();
expect(manifest.project("private_user_asset", releaseVersion, { ownerId: "owner-1" })?.items)
.toEqual([expect.objectContaining({ resource_id: privateId })]);
expect(manifest.project("private_user_asset", releaseVersion, { ownerId: "owner-2" })?.items).toEqual([]);
expect(() => createAssetReleaseManifest({
items: [{
access_class: "public_release_asset",
cache_kind: "thumbnail",
content: Buffer.from("tampered"),
mime_type: "image/png",
relative_path: "public/tampered.png",
resource_id: publicId,
root_ref: "canonical-assets",
sha256: "0".repeat(64),
}],
release_version: releaseVersion,
})).toThrow(/sha-?256/i);
expect(() => createAssetReleaseManifest({
items: [{
access_class: "public_release_asset",
cache_kind: "thumbnail",
content: Buffer.from("unsafe"),
mime_type: "image/png",
relative_path: "/outside/private.png",
resource_id: publicId,
root_ref: "canonical-assets",
}],
release_version: releaseVersion,
})).toThrow(/relative path/i);
});
});