feat(web): add account workspaces and signed content flows

This commit is contained in:
justme committed 2026-10-14 04:06:03 +08:00
1 parent 7b8d61d1ba
commit b5cf3d5958
117 files changed
+2789 -1674

No files matched your search

+9 -3
View File
@@ -3,6 +3,7 @@ import { concat, equalBytes, tryDecodeUtf8, toBase64Url, type Bytes } from './by
import { IntegrityError, type IntegrityReason } from './errors.js';
import { openFrame, parseEnvelope, type Envelope } from './frame.js';
import { sha256 } from './kdf.js';
import { frameManifestHash } from './signatures.js';
import { decodeFirstFrame, type ItemMeta } from './item.js';
import { parseLogRecord, type LogRecord, type RemovedReason } from './log.js';
@@ -29,6 +30,8 @@ export interface ReceivedItem {
complete: boolean;
/** 到第一个缺口为止已收到的内容。大文件请改用 decryptItemStream,不要在这里缓冲。 */
content: Bytes;
/** SHA-256 manifest over the complete ciphertext envelopes in frame-index order. */
ciphertextManifestHash?: Bytes;
}
export type ItemEvent =
@@ -104,7 +107,7 @@ export class ItemReader {
const itemId = toBase64Url(env.itemId);
try {
return await this.#accept(env, itemId, seq);
return await this.#accept(env, itemId, seq, Uint8Array.from(envelope));
} catch (e) {
if (!(e instanceof IntegrityError)) throw e;
this.#open.delete(itemId);
@@ -118,7 +121,7 @@ export class ItemReader {
this.#done.delete(itemId);
}
async #accept(env: Envelope, itemId: string, seq: number | undefined): Promise<ItemEvent[]> {
async #accept(env: Envelope, itemId: string, seq: number | undefined, envelope: Bytes): Promise<ItemEvent[]> {
const isEncFrame = (env.flags & Flags.Enc) !== 0;
// 降级防御:enc 会话只接受认证过的 enc 帧,服务端不能注入明文帧;raw 会话反之
if (isEncFrame !== (this.#ctx.mode === 'enc')) throw new IntegrityError('wrong_mode');
@@ -139,7 +142,7 @@ export class ItemReader {
}
// 重复帧:内容必须逐字节相同,相同则视为重放,不产生事件
const digest = await sha256(env.payload);
const digest = await sha256(envelope);
const dup = state?.frames.get(env.frameIndex);
if (dup) {
if (!equalBytes(dup.digest, digest)) throw new IntegrityError('duplicate_mismatch');
@@ -186,6 +189,9 @@ export class ItemReader {
content: concat(...parts),
};
if (state.meta) item.meta = state.meta;
if (complete) item.ciphertextManifestHash = Uint8Array.from(await frameManifestHash(
Array.from({ length: state.finalIndex! + 1 }, (_, index) => state!.frames.get(index)!.digest),
));
if (complete) {
this.#open.delete(itemId);