Files
lindyson-ai/apps/api/migrations/043_secure_generation_asset_references.sql
T
Juno ChowandClaude Fable 5.1 f665082dad chore: 建立产品仓库,完成开源前的敏感信息清理
前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、
迁移脚本、测试与自托管 docker-compose 示例。

开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读
publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为
VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注
Apache-2.0。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-16 02:10:46 +08:00

336 lines
10 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 生成参考素材只能由任务/资产所属用户的活动资产行重建。
-- 清理范围同时覆盖终态任务 config 与已生成资产 generation_meta;
-- 没有 objectKey 的早期外链记录继续保留,但任何未验证 thumbnailKey 都会移除。
-- 不能在供应商仍可能读取任务配置时改写输入。部署前应先排空带参考素材的任务;
-- 若未排空,整条 migration 失败并回滚,不会把任务静默改成无参考生成。
SET LOCAL lock_timeout = '10s';
LOCK TABLE generation_tasks IN SHARE ROW EXCLUSIVE MODE;
LOCK TABLE assets IN SHARE ROW EXCLUSIVE MODE;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM generation_tasks
WHERE status IN ('queued', 'processing')
AND (
jsonb_array_length(
CASE
WHEN jsonb_typeof(config->'referenceAssets') = 'array'
THEN config->'referenceAssets'
ELSE '[]'::jsonb
END
) > 0
OR jsonb_array_length(
CASE
WHEN jsonb_typeof(config->'referenceImages') = 'array'
THEN config->'referenceImages'
ELSE '[]'::jsonb
END
) > 0
)
) THEN
RAISE EXCEPTION '043 migration requires queued/processing reference tasks to be drained first';
END IF;
END;
$$;
-- 两次历史 UPDATE 共用同一份活动资产快照,避免迁移期间资产状态变化造成任务与资产口径漂移。
CREATE TEMP TABLE lindyson_active_reference_assets ON COMMIT DROP AS
SELECT id, user_id, name, object_key, thumbnail_key, type, duration_seconds, size_bytes
FROM assets
WHERE deletion_status = 'active';
CREATE UNIQUE INDEX lindyson_active_reference_assets_owner_object_idx
ON lindyson_active_reference_assets(user_id, object_key);
CREATE OR REPLACE FUNCTION pg_temp.lindyson_clean_generation_reference(
owner_id uuid,
reference_value jsonb,
image_only boolean
)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
requested_object_key text;
reference_asset record;
cleaned jsonb;
role_value text;
BEGIN
IF jsonb_typeof(reference_value) <> 'object' THEN
RETURN NULL;
END IF;
requested_object_key := NULLIF(btrim(reference_value->>'objectKey'), '');
-- 旧版允许只保存外链;它不再具备对象存储 key,因此不能触发服务端签名。
IF requested_object_key IS NULL THEN
cleaned := reference_value - 'objectKey' - 'thumbnailKey';
IF image_only THEN
cleaned := jsonb_set(cleaned, '{type}', '"image"'::jsonb, true);
END IF;
RETURN cleaned;
END IF;
SELECT a.id, a.name, a.type, a.object_key, a.thumbnail_key,
a.duration_seconds, a.size_bytes
INTO reference_asset
FROM pg_temp.lindyson_active_reference_assets a
WHERE a.user_id = owner_id
AND a.object_key = requested_object_key
LIMIT 1;
-- 跨用户、已删除或损坏引用只保留无访问能力的展示信息。
IF NOT FOUND THEN
cleaned := reference_value
- 'objectKey'
- 'thumbnailKey'
- 'publicUrl'
- 'thumbnailUrl'
- 'url';
cleaned := jsonb_set(cleaned, '{thumbnail}', '""'::jsonb, true);
IF image_only THEN
cleaned := jsonb_set(cleaned, '{type}', '"image"'::jsonb, true);
END IF;
RETURN cleaned;
END IF;
-- referenceImages 只能投影真实图片;损坏的跨类型旧记录直接移除。
IF image_only AND reference_asset.type <> 'image' THEN
RETURN NULL;
END IF;
cleaned := jsonb_build_object(
'id', reference_asset.id,
'name', reference_asset.name,
'thumbnail', '',
'objectKey', reference_asset.object_key,
'type', reference_asset.type
);
IF reference_asset.thumbnail_key IS NOT NULL THEN
cleaned := cleaned || jsonb_build_object('thumbnailKey', reference_asset.thumbnail_key);
END IF;
IF reference_asset.duration_seconds IS NOT NULL THEN
cleaned := cleaned || jsonb_build_object('durationSeconds', reference_asset.duration_seconds);
END IF;
IF reference_asset.size_bytes IS NOT NULL THEN
cleaned := cleaned || jsonb_build_object('sizeBytes', reference_asset.size_bytes);
END IF;
role_value := reference_value->>'role';
IF role_value IN ('reference_image', 'reference_video', 'reference_audio', 'first_frame', 'last_frame') THEN
cleaned := cleaned || jsonb_build_object('role', role_value);
END IF;
RETURN cleaned;
END;
$$;
CREATE OR REPLACE FUNCTION pg_temp.lindyson_clean_generation_reference_array(
owner_id uuid,
references_value jsonb,
image_only boolean
)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
item jsonb;
cleaned_item jsonb;
cleaned_items jsonb := '[]'::jsonb;
BEGIN
IF jsonb_typeof(references_value) <> 'array' THEN
RETURN references_value;
END IF;
FOR item IN SELECT value FROM jsonb_array_elements(references_value)
LOOP
cleaned_item := pg_temp.lindyson_clean_generation_reference(owner_id, item, image_only);
IF cleaned_item IS NOT NULL THEN
cleaned_items := cleaned_items || jsonb_build_array(cleaned_item);
END IF;
END LOOP;
RETURN cleaned_items;
END;
$$;
CREATE OR REPLACE FUNCTION pg_temp.lindyson_clean_generation_reference_container(
owner_id uuid,
container_value jsonb
)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
cleaned jsonb := container_value;
BEGIN
IF jsonb_typeof(container_value) <> 'object' THEN
RETURN container_value;
END IF;
IF jsonb_typeof(container_value->'referenceAssets') = 'array' THEN
cleaned := jsonb_set(
cleaned,
'{referenceAssets}',
pg_temp.lindyson_clean_generation_reference_array(
owner_id,
container_value->'referenceAssets',
false
)
);
END IF;
IF jsonb_typeof(container_value->'referenceImages') = 'array' THEN
cleaned := jsonb_set(
cleaned,
'{referenceImages}',
pg_temp.lindyson_clean_generation_reference_array(
owner_id,
container_value->'referenceImages',
true
)
);
END IF;
RETURN cleaned;
END;
$$;
CREATE OR REPLACE FUNCTION pg_temp.lindyson_clean_generation_meta(
owner_id uuid,
generation_value jsonb
)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
cleaned jsonb := generation_value;
BEGIN
IF jsonb_typeof(generation_value) <> 'object' THEN
RETURN generation_value;
END IF;
IF jsonb_typeof(generation_value->'config') = 'object' THEN
cleaned := jsonb_set(
cleaned,
'{config}',
pg_temp.lindyson_clean_generation_reference_container(
owner_id,
generation_value->'config'
)
);
END IF;
RETURN pg_temp.lindyson_clean_generation_reference_container(owner_id, cleaned);
END;
$$;
WITH cleaned_tasks AS (
SELECT id,
pg_temp.lindyson_clean_generation_reference_container(user_id, config) AS next_config
FROM generation_tasks
WHERE jsonb_typeof(config) = 'object'
AND (config ? 'referenceAssets' OR config ? 'referenceImages')
)
UPDATE generation_tasks AS task
SET config = cleaned_tasks.next_config
FROM cleaned_tasks
WHERE task.id = cleaned_tasks.id
AND task.config IS DISTINCT FROM cleaned_tasks.next_config;
WITH cleaned_assets AS (
SELECT id,
pg_temp.lindyson_clean_generation_meta(user_id, generation_meta) AS next_generation_meta
FROM assets
WHERE jsonb_typeof(generation_meta) = 'object'
AND (
generation_meta ? 'referenceAssets'
OR generation_meta ? 'referenceImages'
OR (generation_meta->'config') ? 'referenceAssets'
OR (generation_meta->'config') ? 'referenceImages'
)
)
UPDATE assets AS asset
SET generation_meta = cleaned_assets.next_generation_meta
FROM cleaned_assets
WHERE asset.id = cleaned_assets.id
AND asset.generation_meta IS DISTINCT FROM cleaned_assets.next_generation_meta;
-- 后置不变量:任何仍带 objectKey 的引用都必须精确命中所属用户的活动资产,
-- thumbnailKey 必须与快照一致,且持久化 JSON 中不得残留可过期的访问 URL。
DO $$
DECLARE
invalid_reference_count bigint;
BEGIN
WITH reference_rows AS (
SELECT task.user_id AS owner_id, item.value AS reference_value
FROM generation_tasks task
CROSS JOIN LATERAL (
VALUES (task.config->'referenceAssets'), (task.config->'referenceImages')
) AS arrays(value)
CROSS JOIN LATERAL jsonb_array_elements(
CASE WHEN jsonb_typeof(arrays.value) = 'array' THEN arrays.value ELSE '[]'::jsonb END
) AS item(value)
UNION ALL
SELECT asset.user_id AS owner_id, item.value AS reference_value
FROM assets asset
CROSS JOIN LATERAL (
VALUES
(asset.generation_meta->'referenceAssets'),
(asset.generation_meta->'referenceImages'),
(asset.generation_meta->'config'->'referenceAssets'),
(asset.generation_meta->'config'->'referenceImages')
) AS arrays(value)
CROSS JOIN LATERAL jsonb_array_elements(
CASE WHEN jsonb_typeof(arrays.value) = 'array' THEN arrays.value ELSE '[]'::jsonb END
) AS item(value)
)
SELECT count(*)
INTO invalid_reference_count
FROM reference_rows reference
WHERE jsonb_typeof(reference.reference_value) = 'object'
AND (
(
reference.reference_value ? 'objectKey'
AND NOT EXISTS (
SELECT 1
FROM pg_temp.lindyson_active_reference_assets owned
WHERE owned.user_id = reference.owner_id
AND owned.object_key = reference.reference_value->>'objectKey'
AND COALESCE(owned.thumbnail_key, '') = COALESCE(
reference.reference_value->>'thumbnailKey',
''
)
)
)
OR (
reference.reference_value ? 'thumbnailKey'
AND NOT (reference.reference_value ? 'objectKey')
)
OR (
reference.reference_value ? 'objectKey'
AND (
reference.reference_value ? 'publicUrl'
OR reference.reference_value ? 'thumbnailUrl'
OR reference.reference_value ? 'url'
)
)
);
IF invalid_reference_count > 0 THEN
RAISE EXCEPTION '043 migration left % invalid generation asset references', invalid_reference_count;
END IF;
END;
$$;
DROP FUNCTION pg_temp.lindyson_clean_generation_meta(uuid, jsonb);
DROP FUNCTION pg_temp.lindyson_clean_generation_reference_container(uuid, jsonb);
DROP FUNCTION pg_temp.lindyson_clean_generation_reference_array(uuid, jsonb, boolean);
DROP FUNCTION pg_temp.lindyson_clean_generation_reference(uuid, jsonb, boolean);