Files
Endoleap/src/lib/canvas-runtime/loading-assets.ts
T

59 lines
6.7 KiB
TypeScript

/**
* 部分加载v6的上传/跨项目资源采纳准入证据。资源存在或URL相同不代表当前源可安全使用,
* 必须绑定 sourceToken、认证canvas范围和对应逻辑操作;原上传body/key也要与资源事实一致。
*/
import type {Document,Id,Registry,OperationIntent} from '../canvas-core/types.ts';
import {equal,fault} from '../canvas-core/values.ts';
import type {RuntimeState} from './contracts.ts';
import {structuralRequestScope} from './loading.ts';
import {referencedResources,type LoadingAssetAdmission} from './resources.ts';
/** Proven local upload identities; never infer authorization from an arbitrary URL. */
// source身份+各proof身份一致时复用资源URL表;不因浏览器缓存有URL就授予准入。
const namespaces=new WeakMap<object,{proofs:Map<Id,LoadingAssetAdmission>;urls:ReadonlyMap<Id,string>}>();
/** 从已记录loadingAdmission派生resource→规范来源URL,只在v6和保留源存在时提供。 */
export function loadingAssetURLs(runtime:RuntimeState):ReadonlyMap<Id,string>{
const source=runtime.loadingRead;if(!source||runtime.loading?.version!==6)return new Map();
const proofs=new Map<Id,LoadingAssetAdmission>();for(const resource of runtime.resources.values())if(resource.loadingAdmission)proofs.set(resource.id,resource.loadingAdmission);
for(const [id,binding]of runtime.assetBindings??[])if(binding.loadingAdmission)proofs.set(id,binding.loadingAdmission);
const previous=namespaces.get(source);if(previous&&previous.proofs.size===proofs.size&&[...proofs].every(([id,proof])=>previous.proofs.get(id)===proof))return previous.urls;
const urls=new Map([...proofs].map(([id,proof])=>[id,proof.sourceURL]));namespaces.set(source,{proofs,urls});return urls;
}
/** 联合验证上传/采纳与源范围、操作顺序、引用和stored证明;最后核对冻结上传协议字段。 */
export function validateLoadingAssetAdmissions(runtime:RuntimeState,registry:Registry):void{
const loading=runtime.loading;if(loading?.version!==6)return;
// upload先分配资源,后续document.change才能引用;早于/同于upload的引用不具备可靠身份因果。
for(const resource of runtime.resources.values()){
const proof=resource.loadingAdmission,record=runtime.ledger.operations.get(resource.operationId);
if(!proof||proof.version!==1||proof.sourceToken!==loading.sourceToken||typeof proof.sourceURL!=='string'||!proof.sourceURL||proof.sourceURL.length>8192||!Array.isArray(proof.canvasIds)||!proof.canvasIds.length||new Set(proof.canvasIds).size!==proof.canvasIds.length||proof.canvasIds.some(id=>typeof id!=='string'||!structuralRequestScope(runtime,runtime.loadingRead!,id))||record?.intent.operationType!=='asset.upload'||record.intent.resourceId!==resource.id||resource.stored&&record.resolution!=='applied')throw fault('invalid_loading_asset',[resource.id]);
if([...runtime.ledger.operations.values()].some(candidate=>candidate.intent.operationType==='document.change'&&candidate.intent.sequence<=record.intent.sequence&&referencedResources(candidate.intent.changes,registry).has(resource.id)))throw fault('invalid_loading_asset',[resource.id]);
if(![...runtime.ledger.operations.values()].some(candidate=>candidate.intent.operationType==='document.change'&&candidate.intent.sequence>record.intent.sequence&&referencedResources(candidate.intent.changes,registry).has(resource.id)))throw fault('invalid_loading_asset',[resource.id]);
}
// adoption绑定自己的文档意图,sourceURL等于已授权asset.url,不凭后来的另一个引用补造准入。
for(const [id,binding]of runtime.assetBindings??[]){
const proof=binding.loadingAdmission,record=runtime.ledger.operations.get(binding.operationId);
if(!proof||proof.version!==1||proof.operationId!==binding.operationId||proof.sourceToken!==loading.sourceToken||proof.sourceURL!==binding.asset.url||!Array.isArray(proof.canvasIds)||!proof.canvasIds.length||new Set(proof.canvasIds).size!==proof.canvasIds.length||proof.canvasIds.some(owner=>!structuralRequestScope(runtime,runtime.loadingRead!,owner))||record?.intent.operationType!=='document.change'||!referencedResources(record.intent.changes,registry).has(id))throw fault('invalid_loading_asset',[id]);
if([...runtime.ledger.operations.values()].some(candidate=>candidate.intent.operationType==='document.change'&&candidate.intent.sequence<record.intent.sequence&&referencedResources(candidate.intent.changes,registry).has(id)))throw fault('invalid_loading_asset',[id]);
}
// 冻结请求必须是原资源/operation/digest/长度/MIME,且不伪造canvas版本参与者。
for(const request of runtime.ledger.requests.values())if(request.kind==='asset.upload'){
const intent=request.operationIds.length===1?runtime.ledger.operations.get(request.operationIds[0])?.intent:undefined,resource=intent?.operationType==='asset.upload'?runtime.resources.get(intent.resourceId):undefined;
if(!resource?.digest||request.protocolVersion!==2||request.canvasIds.length||Object.keys(request.expectedVersions).length)throw fault('invalid_loading_request');
let body:unknown;try{body=JSON.parse(request.body);}catch{throw fault('invalid_loading_request');}
if(!equal(body,{protocol_version:2,request_id:request.id,resource_id:resource.id,operation_id:resource.operationId,sha256:resource.digest,byte_length:resource.byteLength,name:resource.name,mime_type:resource.mimeType}))throw fault('invalid_loading_request',[request.id]);
}
}
// 每registry+文档身份缓存资源的实际owner集合,完整比较首用范围时避免重复全表扫描。
const ownership=new WeakMap<Registry,WeakMap<Document,Map<Id,readonly Id[]>>>();
/** 资源首次出现在逻辑回放中时,实际owner集合须精确匹配准入范围;seen只在本次验证中去重。 */
export function validateLoadingAssetUse(intent:OperationIntent,document:Document,runtime:RuntimeState,registry:Registry,seen:Set<Id>):void{
if(runtime.loading?.version!==6)return;
for(const id of referencedResources(intent.changes,registry)){
const proof=runtime.resources.get(id)?.loadingAdmission??runtime.assetBindings?.get(id)?.loadingAdmission;if(!proof||seen.has(id))continue;seen.add(id);
let documents=ownership.get(registry);if(!documents){documents=new WeakMap();ownership.set(registry,documents);}let index=documents.get(document);
if(!index){const owners=new Map<Id,Set<Id>>();for(const element of document.elements.values())for(const ref of referencedResources([{kind:'element',id:element.id,before:null,after:element}],registry)){const scopes=owners.get(ref)??new Set<Id>();scopes.add(element.ownerCanvasId);owners.set(ref,scopes);}index=new Map([...owners].map(([id,scopes])=>[id,[...scopes].sort()]));documents.set(document,index);}
if(!equal(index.get(id)??[],[...proof.canvasIds].sort()))throw fault('invalid_loading_asset',[id]);
}
}