/** * 部分加载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;urls:ReadonlyMap}>(); /** v6 且 loadingRead 存在时提取 resource/binding 的 admission.sourceURL,不执行认证或完整 proof 校验。 * 以源对象与各 proof 引用身份缓存;依赖 runtime/proof 不可变合同,准入校验应由外围先完成。 * 返回实际可变 Map(类型只读),命中时沿用同一表,内部调用方不得修改;否则会污染后续复用。 * 同 ID 若两表均有 proof 后者覆盖,资源命名空间无冲突由其它运行时校验保证。 */ export function loadingAssetURLs(runtime:RuntimeState):ReadonlyMap{ const source=runtime.loadingRead;if(!source||runtime.loading?.version!==6)return new Map(); const proofs=new Map();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>>(); /** 外围应先将本 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):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>();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();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]); } }