44 lines
4.5 KiB
TypeScript
44 lines
4.5 KiB
TypeScript
/**
|
||
* 导入源完整性证据的拥有权与发送屏障。块读取证明允许先显示/编辑合法前缀,
|
||
* 但source SHA必须在可靠Root中封存后才能发送受影响修改;普通内存布尔值不能冒充durable证据。
|
||
*/
|
||
import type {Id} from '../canvas-core/types.ts';
|
||
import {equal,fault} from '../canvas-core/values.ts';
|
||
import {importNeedsSeal,validateImportJob,type ImportJob} from './imports.ts';
|
||
import {validateImportReadProof} from './import-read-proof.ts';
|
||
|
||
/** A derived durable-cut index, never an in-memory permission boolean. */
|
||
/** 从 integrity-v2、带 SHA 且无 integrityError 的 job 派生 fileId→SHA Map,不扫描字节或读取存储。
|
||
* 调用方必须传已持久 cut;函数不证明其来源可靠,也不独立调用 validateImportJob 验摘要。
|
||
* 返回实际可变 Map,若重复 fileId 后项覆盖前项;文件身份唯一性由外围约束保证。 */
|
||
export function importSeals(jobs:ReadonlyMap<Id,ImportJob>|undefined){
|
||
const seals=new Map<Id,string>();for(const job of jobs?.values()??[])if(job.source?.integrityVersion===2&&job.source.sha256&&!job.source.integrityError)seals.set(job.source.fileId,job.source.sha256);return seals;
|
||
}
|
||
/** 整个 job 表的发送闸门:每个非豁免 integrity-v2 源都须无需 seal 且与 seals 中 SHA 相等。
|
||
* 取消且 committedSourceKeys 为空者豁免;不按本次请求挑选 job,也不重验块证明/实际存储事务。
|
||
* 其它 integrityVersion 在此跳过,是否支持/合法由 job 解释器先校验,不能把 true 当所有源都已验证。 */
|
||
export function importsMaySend(jobs:ReadonlyMap<Id,ImportJob>|undefined,seals:ReadonlyMap<Id,string>){
|
||
for(const job of jobs?.values()??[])if(job.source?.integrityVersion===2&&!(job.status==='cancelled'&&!job.committedSourceKeys.length)&&(importNeedsSeal(job)||seals.get(job.source.fileId)!==job.source.sha256))return false;return true;
|
||
}
|
||
/** Only engine-owned publication paths may advance read evidence or source verification. */
|
||
/** 比较前后 job 表保护 integrity-v2 证据,owner 是引擎传入的当前任务 ID,不是外部权限令牌。
|
||
* 同一表引用直接返回,依赖不可变状态合同;v2 job 不得删除或降级,即使 owner 匹配也不例外。
|
||
* 每个 next v2 job 验结构且要求 proof 索引为从 0 开始的连续前缀,比通用 seek reader 更严格。
|
||
* owner 可推进证据/执行事实,仍不能改 fileId/长度/已有 SHA 或丢改旧块;此处不重算字节或检查持久水位。 */
|
||
export function assertImportEvidence(previous:ReadonlyMap<Id,ImportJob>|undefined,next:ReadonlyMap<Id,ImportJob>|undefined,owner:Id|undefined){
|
||
if(previous===next)return;
|
||
for(const[id,job]of previous??[])if(job.source?.integrityVersion===2&&!next?.has(id))throw fault('import_integrity_owned',[id]);
|
||
/** 比较执行事实时仅剔除 job.generation 和当前 source.retirement;previousFiles 等内容仍参与比较。
|
||
* 允许普通保留流程调整这些排除字段,不在这里证明 generation/退休时钟单调,交其它发布校验处理。 */
|
||
const execution=(job:ImportJob|undefined)=>{if(!job)return job;const{generation:_,source,...body}=job;if(!source)return body;const{retirement:__,...identity}=source;return {...body,source:identity};};
|
||
for(const[id,job]of next??[]){const source=job.source,old=previous?.get(id)?.source;if(source?.integrityVersion!==2){if(old?.integrityVersion===2)throw fault('import_integrity_owned',[id]);continue;}
|
||
validateImportJob(job);const proof=validateImportReadProof(source.readProof,source.fileId,source.byteLength);
|
||
// engine的解析进度要求证明为从0开始的连续块前缀,比通用seek reader的证明形状更严格。
|
||
if(proof.blocks.some((block,index)=>block.index!==index))throw fault('invalid_import_read_proof');
|
||
const changed=!equal(old?.readProof,proof)||old?.sha256!==source.sha256||old?.integrityError!==source.integrityError;
|
||
if(owner!==id&&(changed||!equal(execution(previous?.get(id)),execution(job))))throw fault('import_integrity_owned',[id]);
|
||
// 相同fileId/长度和已存在SHA不可改变,旧块摘要也必须原样保留,不能用“重算成功”覆盖冲突源。
|
||
if(old?.integrityVersion===2){if(old.fileId!==source.fileId||old.byteLength!==source.byteLength||old.sha256&&old.sha256!==source.sha256)throw fault('import_source_changed',[id]);const hashes=new Map(proof.blocks.map(block=>[block.index,block.sha256]));for(const block of old.readProof!.blocks)if(hashes.get(block.index)!==block.sha256)throw fault('import_source_changed',[id]);}
|
||
}
|
||
}
|