feat: isolate asset release access classes (TASK-WP5-04)
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s

This commit is contained in:
suyx
2026-08-03 19:02:04 +08:00
parent 4d1ec8ba7e
commit f1bebab611
12 changed files with 900 additions and 3 deletions
+1
View File
@@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@dada/asset-release-manifest": "workspace:*",
"@dada/shared-contracts": "workspace:*",
"@fastify/multipart": "10.1.0",
"@fastify/swagger": "9.8.1",
+133 -1
View File
@@ -134,6 +134,7 @@ import {
type RegistrationCompleteRequest,
type RegistrationSendRequest,
} from "@dada/shared-contracts";
import type { AssetReleaseReader } from "@dada/asset-release-manifest";
import swagger from "@fastify/swagger";
import multipart from "@fastify/multipart";
import Fastify, { type FastifyReply } from "fastify";
@@ -192,6 +193,7 @@ const defaultBootstrap: BootstrapResponse = {
export interface CreateAppOptions {
amap?: AmapAdapter;
assetReleases?: AssetReleaseReader;
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
browserGate?: boolean;
browserSupportRelease?: BrowserSupportRelease;
@@ -205,6 +207,17 @@ export interface CreateAppOptions {
publicAssets?: PublicAssetResolver;
recentAssets?: RecentAssetService;
projects?: ProjectService;
previewAssetAuthorizer?: (input: {
releaseVersion: string;
resourceId: string;
userId: string;
}) => boolean | Promise<boolean>;
privateAssetAdminAuthorizer?: (input: {
adminUserId: string;
ownerId: string;
releaseVersion: string;
resourceId: string;
}) => boolean | Promise<boolean>;
registration?: RegistrationService;
}
@@ -816,13 +829,27 @@ export async function createApp(options: CreateAppOptions = {}) {
status: "ready",
}));
app.get(
"/api/v1/assets/public/:resourceVersion/manifest",
{ schema: { hide: true } },
async (request, reply) => {
const { resourceVersion } = request.params as { resourceVersion: string };
const manifest = options.assetReleases?.project("public_release_asset", resourceVersion);
if (!manifest) return reply.code(404).send();
reply.header("Cache-Control", "public, max-age=31536000, immutable");
reply.header("ETag", `"sha256-${manifest.manifest_sha256}"`);
return manifest;
},
);
app.get(
"/api/v1/assets/public/:resourceVersion/:assetId",
{ schema: { hide: true } },
async (request, reply) => {
const { assetId, resourceVersion } = request.params as { assetId?: string; resourceVersion?: string };
const resource = assetId && resourceVersion
? options.publicAssets?.read(resourceVersion, assetId)
? options.assetReleases?.read("public_release_asset", resourceVersion, assetId)
?? options.publicAssets?.read(resourceVersion, assetId)
: undefined;
if (!resource) return reply.code(404).send();
reply.type(resource.mimeType);
@@ -833,6 +860,111 @@ export async function createApp(options: CreateAppOptions = {}) {
},
);
app.get(
"/api/v1/assets/preview/:resourceVersion/manifest",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const session = token ? options.registration.readUserSession(token) : undefined;
if (!session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
const { resourceVersion } = request.params as { resourceVersion: string };
const available = options.assetReleases?.project("internal_preview_asset", resourceVersion);
if (!available || !options.previewAssetAuthorizer) return reply.code(404).send();
const authorizedIds: string[] = [];
for (const item of available.items) {
if (await options.previewAssetAuthorizer({
releaseVersion: resourceVersion,
resourceId: item.resource_id,
userId: session.userId,
})) authorizedIds.push(item.resource_id);
}
if (authorizedIds.length === 0) return reply.code(404).send();
const manifest = options.assetReleases?.project("internal_preview_asset", resourceVersion, { resourceIds: authorizedIds });
if (!manifest) return reply.code(404).send();
reply.header("Cache-Control", "private, no-store");
reply.header("Vary", "Cookie");
return manifest;
},
);
app.get(
"/api/v1/assets/preview/:resourceVersion/:assetId",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const session = token ? options.registration.readUserSession(token) : undefined;
if (!session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
const { assetId, resourceVersion } = request.params as { assetId: string; resourceVersion: string };
const authorized = await options.previewAssetAuthorizer?.({ resourceId: assetId, releaseVersion: resourceVersion, userId: session.userId });
const resource = authorized ? options.assetReleases?.read("internal_preview_asset", resourceVersion, assetId) : undefined;
if (!resource) return reply.code(404).send();
reply.type(resource.mimeType);
reply.header("Cache-Control", "private, no-store");
reply.header("Content-Disposition", "inline");
reply.header("Vary", "Cookie");
return resource.bytes;
},
);
app.get(
"/api/v1/private-assets/:resourceVersion/manifest",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const session = token ? options.registration.readUserSession(token) : undefined;
if (!session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
const { resourceVersion } = request.params as { resourceVersion: string };
const manifest = options.assetReleases?.project("private_user_asset", resourceVersion, { ownerId: session.userId });
if (!manifest) return reply.code(404).send();
reply.header("Cache-Control", "private, no-store");
reply.header("Vary", "Cookie");
return manifest;
},
);
app.get(
"/api/v1/private-assets/:resourceVersion/:assetId",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const userToken = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const userSession = userToken ? options.registration.readUserSession(userToken) : undefined;
const adminToken = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const adminSession = adminToken ? options.registration.readAdminSession(adminToken) : undefined;
if (!userSession && !adminSession) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
const { assetId, resourceVersion } = request.params as { assetId: string; resourceVersion: string };
const resource = options.assetReleases?.read("private_user_asset", resourceVersion, assetId);
if (!resource?.ownerId) return reply.code(404).send();
const controlledAdmin = adminSession
? await options.privateAssetAdminAuthorizer?.({
adminUserId: adminSession.user_id,
ownerId: resource.ownerId,
releaseVersion: resource.releaseVersion,
resourceId: resource.resourceId,
})
: false;
if (userSession?.userId !== resource.ownerId && !controlledAdmin) return reply.code(404).send();
reply.type(resource.mimeType);
reply.header("Cache-Control", "private, no-store");
reply.header("Content-Disposition", "inline");
reply.header("Vary", "Cookie");
return resource.bytes;
},
);
app.get(
"/api/v1/assets/recent",
{