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

70 lines
8.1 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>}>();
/** v6 且 loadingRead 存在时提取 resource/binding 的 admission.sourceURL,不执行认证或完整 proof 校验。
* 以源对象与各 proof 引用身份缓存;依赖 runtime/proof 不可变合同,准入校验应由外围先完成。
* 返回实际可变 Map(类型只读),命中时沿用同一表,内部调用方不得修改;否则会污染后续复用。
* 同 ID 若两表均有 proof 后者覆盖,资源命名空间无冲突由其它运行时校验保证。 */
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;
}
/** 仅 v6 校验全部本地 resource/binding 的准入:token/范围、逻辑因果与冻结上传 body 一致性。
* 要求外围已验证 loadingRead/ledger/资源结构;这里不下载字节、重算摘要、请求授权或核验真实服务回执。
* referencedResources 是声明字段的保守引用集(含变化前后),顺序检查不单独证明某条记录当前仍可显示。
* sourceURL 对 upload 只查非空/长度,对 adoption 要与 binding.asset.url 相等,规范来源由受信端口提供。 */
export function validateLoadingAssetAdmissions(runtime:RuntimeState,registry:Registry):void{
const loading=runtime.loading;if(loading?.version!==6)return;
// 每个 upload resource 都要有对应上传意图;stored 存在时 resolution 必须 applied。
// 至少有一个后续 document.change 引用且不允许早于/等于上传序号的引用;不按 ledger phase 过滤。
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]);
}
// 每个 asset.upload 冻结请求必须只对应一个资源意图,协议 2 且 canvas/expectedVersions 为空。
// JSON body 与原 request/resource/operation/digest/长度/名称/MIME 精确匹配;不在此重新编译或修改冻结请求。
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[]>>>();
/** 外围应先将本 intent 应用到逻辑候选 document,再在首次遇有 admission 的资源时比较实际 owner 集合。
* registry+document 身份缓存全表资源→owner 索引,复用依赖不可变文档/定义;统计内容引用,不使用 DOM 可见性。
* 无 proof 或已 seen 的资源跳过,后续意图不重复做首次范围比较;一般资源合法性由其它门禁负责。
* seen 在比较前更新,失败后应丢弃本轮 Set,不能将失败结果当作已验证缓存继续使用。 */
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]);
}
}