dscs-server 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ import { sha256 } from './crypto.js';
2
+ const HASH_PATTERN = /^[0-9a-f]{64}$/u;
3
+ export function assertChain(chain) {
4
+ chain.forEach((frame, position)=>{
5
+ if (frame.index !== position) throw new Error(`第 ${position} 帧的下标是 ${frame.index},链不连续`);
6
+ if (!HASH_PATTERN.test(frame.chainHash)) throw new Error(`第 ${position} 帧的 chainHash 不是 64 位十六进制`);
7
+ if (!Number.isSafeInteger(frame.seqFrom) || !Number.isSafeInteger(frame.seqTo)) throw new Error(`第 ${position} 帧的 seq 不是整数`);
8
+ if (position === 0) {
9
+ if (frame.seqFrom !== -1 || frame.seqTo !== -1) throw new Error('header 帧的 seq 必须是 -1');
10
+ return;
11
+ }
12
+ const previous = chain[position - 1];
13
+ if (frame.seqFrom < 0 || frame.seqTo < frame.seqFrom) throw new Error(`第 ${position} 帧的 seq 区间非法`);
14
+ if (previous.seqTo + 1 !== frame.seqFrom) {
15
+ throw new Error(`第 ${position} 帧的 seq 应从 ${previous.seqTo + 1} 开始,实际是 ${frame.seqFrom}`);
16
+ }
17
+ });
18
+ }
19
+ export function commonPrefix(local, remote) {
20
+ let index = 0;
21
+ while(index < local.length && index < remote.length && local[index].chainHash === remote[index].chainHash)index += 1;
22
+ return index;
23
+ }
24
+ export function diffChains(local, remote) {
25
+ assertChain(local);
26
+ assertChain(remote);
27
+ const common = commonPrefix(local, remote);
28
+ const pull = remote.slice(common);
29
+ const push = local.slice(common);
30
+ if (pull.length === 0 && push.length === 0) return {
31
+ status: 'equal',
32
+ common,
33
+ pull,
34
+ push
35
+ };
36
+ if (push.length === 0) return {
37
+ status: 'remote-ahead',
38
+ common,
39
+ pull,
40
+ push
41
+ };
42
+ if (pull.length === 0) return {
43
+ status: 'local-ahead',
44
+ common,
45
+ pull,
46
+ push
47
+ };
48
+ if (pull.length === 1 && push.length === 1 && pull[0].index === push[0].index) {
49
+ return {
50
+ status: 'tail-mismatch',
51
+ common,
52
+ pull,
53
+ push
54
+ };
55
+ }
56
+ return {
57
+ status: 'diverged',
58
+ common,
59
+ pull,
60
+ push
61
+ };
62
+ }
63
+ export function buildManifest(input) {
64
+ input.frames.forEach((frame, position)=>{
65
+ if (frame.index !== position) throw new Error(`manifest 第 ${position} 条的 index 是 ${frame.index}`);
66
+ if (!HASH_PATTERN.test(frame.chainHash) || !HASH_PATTERN.test(frame.blobSha256)) throw new Error(`manifest 第 ${position} 条的哈希非法`);
67
+ if (frame.sealedVersion < 1) throw new Error(`manifest 第 ${position} 条的封帧版本非法`);
68
+ });
69
+ const last = input.frames[input.frames.length - 1];
70
+ const lastSeq = last === undefined ? -1 : last.seqTo;
71
+ return {
72
+ sessionId: input.sessionId,
73
+ formatVersion: input.formatVersion,
74
+ createdAt: input.createdAt,
75
+ lastSeq,
76
+ ...input.workspaceKey === undefined ? {} : {
77
+ workspaceKey: input.workspaceKey
78
+ },
79
+ ...input.tailOpen === undefined ? {} : {
80
+ tailOpen: input.tailOpen
81
+ },
82
+ ...input.safeThroughSeq === undefined ? {} : {
83
+ safeThroughSeq: input.safeThroughSeq
84
+ },
85
+ frames: input.frames,
86
+ updatedAt: input.updatedAt
87
+ };
88
+ }
89
+ export function assertManifest(manifest) {
90
+ manifest.frames.forEach((frame, position)=>{
91
+ if (frame.index !== position) throw new Error(`manifest 第 ${position} 条的 index 是 ${frame.index}`);
92
+ if (!HASH_PATTERN.test(frame.chainHash) || !HASH_PATTERN.test(frame.blobSha256)) throw new Error(`manifest 第 ${position} 条的哈希非法`);
93
+ });
94
+ const last = manifest.frames[manifest.frames.length - 1];
95
+ if ((last === undefined ? -1 : last.seqTo) !== manifest.lastSeq) throw new Error('manifest 的 lastSeq 与最后一帧不一致');
96
+ const safe = manifest.safeThroughSeq ?? manifest.lastSeq;
97
+ if (!Number.isSafeInteger(safe) || safe < -1 || safe > manifest.lastSeq) throw new Error('manifest 的 safeThroughSeq 非法');
98
+ if (manifest.workspaceKey !== undefined && !/^[A-Za-z0-9_-]{16,64}$/u.test(manifest.workspaceKey)) {
99
+ throw new Error('manifest 的 workspaceKey 非法');
100
+ }
101
+ if (manifest.tailOpen !== undefined && typeof manifest.tailOpen !== 'boolean') throw new Error('manifest 的 tailOpen 非法');
102
+ }
103
+ function canonical(value) {
104
+ if (Array.isArray(value)) return value.map((entry)=>canonical(entry));
105
+ if (value !== null && typeof value === 'object') {
106
+ const sorted = {};
107
+ for (const key of Object.keys(value).sort())sorted[key] = canonical(value[key]);
108
+ return sorted;
109
+ }
110
+ return value;
111
+ }
112
+ export function stableStringify(value) {
113
+ return JSON.stringify(canonical(value));
114
+ }
115
+ export function manifestDigest(manifest) {
116
+ return sha256(stableStringify({
117
+ sessionId: manifest.sessionId,
118
+ formatVersion: manifest.formatVersion,
119
+ createdAt: manifest.createdAt,
120
+ lastSeq: manifest.lastSeq,
121
+ workspaceKey: manifest.workspaceKey ?? null,
122
+ tailOpen: manifest.tailOpen ?? null,
123
+ safeThroughSeq: manifest.safeThroughSeq ?? manifest.lastSeq,
124
+ frames: manifest.frames.map((frame)=>({
125
+ index: frame.index,
126
+ chainHash: frame.chainHash,
127
+ blobSha256: frame.blobSha256,
128
+ bytes: frame.bytes,
129
+ sealedVersion: frame.sealedVersion,
130
+ keyId: frame.keyId,
131
+ seqFrom: frame.seqFrom,
132
+ seqTo: frame.seqTo
133
+ })),
134
+ deletedAt: manifest.deletedAt ?? null
135
+ }));
136
+ }
137
+
138
+
139
+ //# sourceURL=chain.ts
@@ -0,0 +1,70 @@
1
+ import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, scrypt, scryptSync } from 'node:crypto';
2
+ export const KEY_BYTES = 32;
3
+ export const NONCE_BYTES = 12;
4
+ export const TAG_BYTES = 16;
5
+ export const SALT_BYTES = 16;
6
+ export const SCRYPT = {
7
+ N: 1 << 15,
8
+ r: 8,
9
+ p: 1
10
+ };
11
+ export function newSalt() {
12
+ return randomBytes(SALT_BYTES);
13
+ }
14
+ export function deriveMasterKey(passphrase, salt) {
15
+ return scryptSync(passphrase, salt, KEY_BYTES, {
16
+ ...SCRYPT,
17
+ maxmem: 128 * 1024 * 1024
18
+ });
19
+ }
20
+ export async function deriveMasterKeyAsync(passphrase, salt) {
21
+ return await new Promise((resolve, reject)=>{
22
+ scrypt(passphrase, salt, KEY_BYTES, {
23
+ ...SCRYPT,
24
+ maxmem: 128 * 1024 * 1024
25
+ }, (error, key)=>{
26
+ if (error !== null) reject(error);
27
+ else resolve(Buffer.from(key));
28
+ });
29
+ });
30
+ }
31
+ export function deriveSubkey(master, info) {
32
+ return Buffer.from(hkdfSync('sha256', master, Buffer.alloc(0), Buffer.from(info, 'utf8'), KEY_BYTES));
33
+ }
34
+ export function seal(key, plaintext, aad) {
35
+ const nonce = randomBytes(NONCE_BYTES);
36
+ const cipher = createCipheriv('aes-256-gcm', key, nonce);
37
+ cipher.setAAD(aad);
38
+ const body = Buffer.concat([
39
+ cipher.update(plaintext),
40
+ cipher.final()
41
+ ]);
42
+ return {
43
+ nonce,
44
+ ciphertext: Buffer.concat([
45
+ body,
46
+ cipher.getAuthTag()
47
+ ])
48
+ };
49
+ }
50
+ export function open(key, sealed, aad) {
51
+ if (sealed.ciphertext.length < TAG_BYTES) throw new Error('封装载荷比认证标签还短');
52
+ const body = sealed.ciphertext.subarray(0, sealed.ciphertext.length - TAG_BYTES);
53
+ const tag = sealed.ciphertext.subarray(sealed.ciphertext.length - TAG_BYTES);
54
+ const decipher = createDecipheriv('aes-256-gcm', key, sealed.nonce);
55
+ decipher.setAAD(aad);
56
+ decipher.setAuthTag(tag);
57
+ return Buffer.concat([
58
+ decipher.update(body),
59
+ decipher.final()
60
+ ]);
61
+ }
62
+ export function sha256(data) {
63
+ return createHash('sha256').update(data).digest('hex');
64
+ }
65
+ export function randomId(bytes = 16) {
66
+ return randomBytes(bytes).toString('hex');
67
+ }
68
+
69
+
70
+ //# sourceURL=crypto.ts
@@ -0,0 +1,102 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { zstdCompressSync, zstdDecompressSync } from 'node:zlib';
3
+ export const ZSTD_MAGIC = Buffer.from([
4
+ 0x28,
5
+ 0xb5,
6
+ 0x2f,
7
+ 0xfd
8
+ ]);
9
+ export function isZstd(buffer) {
10
+ return buffer.length >= 4 && buffer.subarray(0, 4).equals(ZSTD_MAGIC);
11
+ }
12
+ export function zstdFrameEnd(buffer, offset) {
13
+ if (offset + 5 > buffer.length || !buffer.subarray(offset, offset + 4).equals(ZSTD_MAGIC)) {
14
+ throw new Error(`offset ${offset} 处不是 zstd 帧`);
15
+ }
16
+ const descriptor = buffer[offset + 4];
17
+ if ((descriptor & 8) !== 0) throw new Error('zstd 帧声明了保留位');
18
+ const singleSegment = (descriptor & 32) !== 0;
19
+ const contentSizeBytes = [
20
+ singleSegment ? 1 : 0,
21
+ 2,
22
+ 4,
23
+ 8
24
+ ][descriptor >>> 6];
25
+ let cursor = offset + 5 + (singleSegment ? 0 : 1) + [
26
+ 0,
27
+ 1,
28
+ 2,
29
+ 4
30
+ ][descriptor & 3] + contentSizeBytes;
31
+ let last = false;
32
+ while(!last){
33
+ if (cursor + 3 > buffer.length) throw new Error('zstd 块头被截断');
34
+ const header = buffer.readUIntLE(cursor, 3);
35
+ const type = header >>> 1 & 3;
36
+ if (type === 3) throw new Error('zstd 块声明了保留类型');
37
+ last = (header & 1) !== 0;
38
+ cursor += 3 + (type === 1 ? 1 : header >>> 3);
39
+ if (cursor > buffer.length) throw new Error('zstd 块被截断');
40
+ }
41
+ if ((descriptor & 4) !== 0) cursor += 4;
42
+ if (cursor > buffer.length) throw new Error('zstd 校验和被截断');
43
+ return cursor;
44
+ }
45
+ export function splitFrames(buffer) {
46
+ const frames = [];
47
+ let offset = 0;
48
+ while(offset < buffer.length){
49
+ const end = zstdFrameEnd(buffer, offset);
50
+ frames.push(buffer.subarray(offset, end));
51
+ offset = end;
52
+ }
53
+ return frames;
54
+ }
55
+ export function decodeFrames(frames) {
56
+ return frames.map((frame)=>zstdDecompressSync(frame));
57
+ }
58
+ export function decodeLog(buffer) {
59
+ if (!isZstd(buffer)) return buffer.toString('utf8');
60
+ return Buffer.concat(decodeFrames(splitFrames(buffer))).toString('utf8');
61
+ }
62
+ export function splitRows(text) {
63
+ return text.split('\n').map((row)=>row.endsWith('\r') ? row.slice(0, -1) : row).filter((row)=>row.trim() !== '');
64
+ }
65
+ export function encodeLog(text, options = {}) {
66
+ const headerLines = options.headerLines ?? 1;
67
+ const rowsPerFrame = options.rowsPerFrame ?? 1;
68
+ const rows = splitRows(text);
69
+ if (rows.length === 0) return [];
70
+ const head = rows.slice(0, headerLines);
71
+ const frames = [
72
+ zstdCompressSync(Buffer.from(`${head.join('\n')}\n`, 'utf8'))
73
+ ];
74
+ let index = headerLines;
75
+ while(index < rows.length){
76
+ const chunk = rows.slice(index, index + rowsPerFrame);
77
+ frames.push(zstdCompressSync(Buffer.from(`${chunk.join('\n')}\n`, 'utf8')));
78
+ index += rowsPerFrame;
79
+ }
80
+ return frames;
81
+ }
82
+ export function joinFrames(frames) {
83
+ return Buffer.concat(frames);
84
+ }
85
+ export function hashFrame(frame) {
86
+ return createHash('sha256').update(frame).digest('hex');
87
+ }
88
+ export function readHeader(buffer) {
89
+ const text = isZstd(buffer) ? Buffer.concat(decodeFrames([
90
+ splitFrames(buffer)[0]
91
+ ])).toString('utf8') : buffer.toString('utf8');
92
+ const first = splitRows(text)[0];
93
+ if (first === undefined) throw new Error('会话日志为空');
94
+ const parsed = JSON.parse(first);
95
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
96
+ throw new Error('会话日志 header 不是 JSON 对象');
97
+ }
98
+ return parsed;
99
+ }
100
+
101
+
102
+ //# sourceURL=frames.ts
@@ -0,0 +1,11 @@
1
+ export * from './batches.js';
2
+ export * from './chain.js';
3
+ export * from './crypto.js';
4
+ export * from './frames.js';
5
+ export * from './protocol.js';
6
+ export * from './sealed-frame.js';
7
+ export * from './vault.js';
8
+ export * from './wire.js';
9
+
10
+
11
+ //# sourceURL=index.ts
@@ -0,0 +1,14 @@
1
+ export const API_PREFIX = '/v1';
2
+ export const PROTOCOL_VERSION = 2;
3
+ export function errorBody(code, message) {
4
+ return {
5
+ ok: false,
6
+ error: {
7
+ code,
8
+ message
9
+ }
10
+ };
11
+ }
12
+
13
+
14
+ //# sourceURL=protocol.ts
@@ -0,0 +1,118 @@
1
+ import { createCipheriv, createDecipheriv } from 'node:crypto';
2
+ import { deriveSubkey, sha256, TAG_BYTES, KEY_BYTES, NONCE_BYTES } from './crypto.js';
3
+ export const SEALED_MAGIC = Buffer.from('DSHF', 'ascii');
4
+ export const SEALED_VERSION = 1;
5
+ export const KEY_ID_BYTES = 8;
6
+ export const SEALED_HEADER_BYTES = 4 + 1 + 1 + KEY_ID_BYTES + NONCE_BYTES;
7
+ const FIXED_NONCE = Buffer.alloc(NONCE_BYTES);
8
+ export function sessionKeyOf(master, sessionId) {
9
+ return deriveSubkey(master, `v1|session:${sessionId}`);
10
+ }
11
+ export function frameKeyOf(sessionKey, chainHash) {
12
+ return deriveSubkey(sessionKey, `v1|frame:${chainHash}`);
13
+ }
14
+ export function keyIdOf(sessionKey) {
15
+ return deriveSubkey(sessionKey, 'v1|key-id').subarray(0, KEY_ID_BYTES);
16
+ }
17
+ const SEQ_HEADER = 0xffffffff;
18
+ function buildAad(header, position) {
19
+ const session = Buffer.from(position.sessionId, 'utf8');
20
+ if (session.length > 0xffff) throw new Error('会话 id 过长,AAD 无法编码');
21
+ const tail = Buffer.alloc(2 + session.length + 8 + 4 + 4 + 32);
22
+ tail.writeUInt16BE(session.length, 0);
23
+ session.copy(tail, 2);
24
+ let offset = 2 + session.length;
25
+ tail.writeBigUInt64BE(BigInt(position.index), offset);
26
+ offset += 8;
27
+ tail.writeUInt32BE(position.seqFrom < 0 ? SEQ_HEADER : position.seqFrom, offset);
28
+ offset += 4;
29
+ tail.writeUInt32BE(position.seqTo < 0 ? SEQ_HEADER : position.seqTo, offset);
30
+ offset += 4;
31
+ Buffer.from(position.chainHash, 'hex').copy(tail, offset);
32
+ return Buffer.concat([
33
+ header,
34
+ tail
35
+ ]);
36
+ }
37
+ export function sealFrame(plain, position, sessionKey) {
38
+ if (sessionKey.length !== KEY_BYTES) throw new Error(`会话密钥必须是 ${KEY_BYTES} 字节`);
39
+ if (!Number.isSafeInteger(position.index) || position.index < 0) throw new Error('帧下标必须是非负整数');
40
+ const chainHash = sha256(plain);
41
+ const frameKey = frameKeyOf(sessionKey, chainHash);
42
+ const keyId = keyIdOf(sessionKey);
43
+ const header = Buffer.alloc(SEALED_HEADER_BYTES);
44
+ SEALED_MAGIC.copy(header, 0);
45
+ header.writeUInt8(SEALED_VERSION, 4);
46
+ header.writeUInt8(0, 5);
47
+ keyId.copy(header, 6);
48
+ FIXED_NONCE.copy(header, 14);
49
+ const aad = buildAad(header, {
50
+ ...position,
51
+ chainHash
52
+ });
53
+ const cipher = createCipheriv('aes-256-gcm', frameKey, FIXED_NONCE);
54
+ cipher.setAAD(aad);
55
+ const body = Buffer.concat([
56
+ cipher.update(plain),
57
+ cipher.final()
58
+ ]);
59
+ const bytes = Buffer.concat([
60
+ header,
61
+ body,
62
+ cipher.getAuthTag()
63
+ ]);
64
+ return {
65
+ bytes,
66
+ ref: {
67
+ index: position.index,
68
+ chainHash,
69
+ blobSha256: sha256(bytes),
70
+ bytes: bytes.length,
71
+ sealedVersion: SEALED_VERSION,
72
+ keyId: keyId.toString('hex'),
73
+ seqFrom: position.seqFrom,
74
+ seqTo: position.seqTo
75
+ }
76
+ };
77
+ }
78
+ export function parseSealedHeader(sealed) {
79
+ if (sealed.length < SEALED_HEADER_BYTES + TAG_BYTES) throw new Error('封帧字节被截断');
80
+ if (!sealed.subarray(0, 4).equals(SEALED_MAGIC)) throw new Error('不是 DSHF 封帧');
81
+ const version = sealed.readUInt8(4);
82
+ if (version !== SEALED_VERSION) throw new Error(`不支持的封帧版本:${version}`);
83
+ return {
84
+ version,
85
+ flags: sealed.readUInt8(5),
86
+ keyId: sealed.subarray(6, 6 + KEY_ID_BYTES),
87
+ nonce: sealed.subarray(14, 14 + NONCE_BYTES)
88
+ };
89
+ }
90
+ export function openSealedFrame(sealed, position, sessionKey) {
91
+ const header = parseSealedHeader(sealed);
92
+ const expectedKeyId = keyIdOf(sessionKey);
93
+ if (!header.keyId.equals(expectedKeyId)) {
94
+ throw new Error('封帧的 keyId 与当前密钥不符(请确认各设备同步口令、服务端地址与 salt 一致)');
95
+ }
96
+ const headerBytes = sealed.subarray(0, SEALED_HEADER_BYTES);
97
+ const aad = buildAad(headerBytes, position);
98
+ const body = sealed.subarray(SEALED_HEADER_BYTES, sealed.length - TAG_BYTES);
99
+ const tag = sealed.subarray(sealed.length - TAG_BYTES);
100
+ const frameKey = frameKeyOf(sessionKey, position.chainHash);
101
+ const decipher = createDecipheriv('aes-256-gcm', frameKey, header.nonce);
102
+ decipher.setAAD(aad);
103
+ decipher.setAuthTag(tag);
104
+ let plain;
105
+ try {
106
+ plain = Buffer.concat([
107
+ decipher.update(body),
108
+ decipher.final()
109
+ ]);
110
+ } catch {
111
+ throw new Error('封帧认证失败(密文、AAD 或密钥不匹配)');
112
+ }
113
+ if (sha256(plain) !== position.chainHash) throw new Error('明文哈希与清单不符');
114
+ return plain;
115
+ }
116
+
117
+
118
+ //# sourceURL=sealed-frame.ts
@@ -0,0 +1,210 @@
1
+ import { createCipheriv, createDecipheriv, createHmac } from 'node:crypto';
2
+ import { stableStringify } from './chain.js';
3
+ import { deriveSubkey, KEY_BYTES, NONCE_BYTES, sha256, TAG_BYTES } from './crypto.js';
4
+ export const VAULT_SCHEMA = 'dsh-session-cloudsync/vault/1';
5
+ export const VAULT_FORMAT_VERSION = 1;
6
+ export const VAULT_KEY_ID_BYTES = 8;
7
+ export const WORKSPACE_KEY_CHARS = 24;
8
+ export const VAULT_GENESIS_HASH = null;
9
+ const FIXED_NONCE = Buffer.alloc(NONCE_BYTES);
10
+ const HASH_PATTERN = /^[0-9a-f]{64}$/u;
11
+ const KEY_ID_PATTERN = /^[0-9a-f]{16}$/u;
12
+ const KIND_PATTERN = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/u;
13
+ export function vaultKeyOf(master) {
14
+ assertKey(master, 'masterKey');
15
+ return deriveSubkey(master, 'dsh-sync/vault/v1');
16
+ }
17
+ export function vaultKeyIdOf(vaultKey) {
18
+ assertKey(vaultKey, 'vaultKey');
19
+ return deriveSubkey(vaultKey, 'v1|key-id').subarray(0, VAULT_KEY_ID_BYTES).toString('hex');
20
+ }
21
+ export function workspaceKeyOf(vaultKey, canonicalPath) {
22
+ assertKey(vaultKey, 'vaultKey');
23
+ if (canonicalPath.trim() === '') throw new Error('canonical workspace path 不能为空');
24
+ return createHmac('sha256', vaultKey).update(`workspace|${canonicalPath}`, 'utf8').digest('base64url').slice(0, WORKSPACE_KEY_CHARS);
25
+ }
26
+ export function sealVaultRecord(input) {
27
+ assertKey(input.vaultKey, 'vaultKey');
28
+ assertWriter(input.writer);
29
+ assertSeq(input.seq);
30
+ assertPrevHash(input.seq, input.prevHash);
31
+ assertTimestamp(input.createdAt, 'createdAt');
32
+ assertKind(input.kind);
33
+ assertEntity(input.entity);
34
+ assertTimestamp(input.at, 'at');
35
+ if (input.data === undefined) throw new Error('Vault data 不能是 undefined');
36
+ const payload = {
37
+ schema: VAULT_SCHEMA,
38
+ kind: input.kind,
39
+ entity: input.entity,
40
+ at: input.at,
41
+ data: input.data
42
+ };
43
+ const plain = Buffer.from(stableStringify(payload), 'utf8');
44
+ const keyId = vaultKeyIdOf(input.vaultKey);
45
+ const key = recordKeyOf(input.vaultKey, input.writer, input.seq);
46
+ const aad = aadOf(input.writer, input.seq, input.prevHash, VAULT_SCHEMA);
47
+ const cipher = createCipheriv('aes-256-gcm', key, FIXED_NONCE);
48
+ cipher.setAAD(aad);
49
+ const body = Buffer.concat([
50
+ cipher.update(plain),
51
+ cipher.final()
52
+ ]);
53
+ const bytes = Buffer.concat([
54
+ body,
55
+ cipher.getAuthTag()
56
+ ]);
57
+ const sealed = {
58
+ formatVersion: VAULT_FORMAT_VERSION,
59
+ keyId
60
+ };
61
+ const blobSha256 = sha256(bytes);
62
+ const ref = {
63
+ v: VAULT_FORMAT_VERSION,
64
+ writer: input.writer,
65
+ seq: input.seq,
66
+ prevHash: input.prevHash,
67
+ blobSha256,
68
+ bytes: bytes.length,
69
+ createdAt: input.createdAt,
70
+ sealed,
71
+ recordHash: vaultRecordHashOf({
72
+ v: VAULT_FORMAT_VERSION,
73
+ writer: input.writer,
74
+ seq: input.seq,
75
+ prevHash: input.prevHash,
76
+ blobSha256,
77
+ bytes: bytes.length,
78
+ createdAt: input.createdAt,
79
+ sealed
80
+ })
81
+ };
82
+ return {
83
+ ref,
84
+ bytes
85
+ };
86
+ }
87
+ export function openVaultRecord(record, vaultKey) {
88
+ assertKey(vaultKey, 'vaultKey');
89
+ assertVaultRecordRef(record.ref);
90
+ if (!Buffer.isBuffer(record.bytes)) throw new Error('Vault 密文必须是 Buffer');
91
+ if (record.bytes.length !== record.ref.bytes) throw new Error('Vault 密文字节数与索引不一致');
92
+ if (sha256(record.bytes) !== record.ref.blobSha256) throw new Error('Vault 密文哈希与索引不一致');
93
+ if (vaultKeyIdOf(vaultKey) !== record.ref.sealed.keyId) throw new Error('Vault keyId 不匹配');
94
+ const key = recordKeyOf(vaultKey, record.ref.writer, record.ref.seq);
95
+ if (record.bytes.length < TAG_BYTES) throw new Error('Vault 密文过短');
96
+ const body = record.bytes.subarray(0, record.bytes.length - TAG_BYTES);
97
+ const tag = record.bytes.subarray(record.bytes.length - TAG_BYTES);
98
+ const decipher = createDecipheriv('aes-256-gcm', key, FIXED_NONCE);
99
+ decipher.setAAD(aadOf(record.ref.writer, record.ref.seq, record.ref.prevHash, VAULT_SCHEMA));
100
+ decipher.setAuthTag(tag);
101
+ let parsed;
102
+ try {
103
+ const plain = Buffer.concat([
104
+ decipher.update(body),
105
+ decipher.final()
106
+ ]);
107
+ parsed = JSON.parse(plain.toString('utf8'));
108
+ } catch (error) {
109
+ throw new Error(`Vault 记录解密失败:${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Vault 载荷不是对象');
112
+ const payload = parsed;
113
+ if (payload['schema'] !== VAULT_SCHEMA) throw new Error(`未知 Vault schema:${String(payload['schema'])}`);
114
+ if (typeof payload['kind'] !== 'string' || !KIND_PATTERN.test(payload['kind'])) throw new Error('Vault kind 非法');
115
+ if (typeof payload['entity'] !== 'string' || payload['entity'].trim() === '') throw new Error('Vault entity 非法');
116
+ if (!Number.isSafeInteger(payload['at'])) throw new Error('Vault at 非法');
117
+ if (payload['data'] === undefined) throw new Error('Vault data 缺失');
118
+ return {
119
+ schema: payload['schema'],
120
+ kind: payload['kind'],
121
+ entity: payload['entity'],
122
+ at: payload['at'],
123
+ data: payload['data']
124
+ };
125
+ }
126
+ export function assertVaultRecordRef(ref) {
127
+ if (ref.v !== VAULT_FORMAT_VERSION) throw new Error(`Vault 记录版本非法:${String(ref.v)}`);
128
+ assertWriter(ref.writer);
129
+ assertSeq(ref.seq);
130
+ assertPrevHash(ref.seq, ref.prevHash);
131
+ if (!HASH_PATTERN.test(ref.blobSha256)) throw new Error('Vault blobSha256 非法');
132
+ if (!Number.isSafeInteger(ref.bytes) || ref.bytes <= 0) throw new Error('Vault bytes 非法');
133
+ assertTimestamp(ref.createdAt, 'createdAt');
134
+ if (ref.sealed === null || typeof ref.sealed !== 'object') throw new Error('Vault sealed 非法');
135
+ if (ref.sealed.formatVersion !== VAULT_FORMAT_VERSION) throw new Error('Vault sealed.formatVersion 非法');
136
+ if (!KEY_ID_PATTERN.test(ref.sealed.keyId)) throw new Error('Vault sealed.keyId 非法');
137
+ if (!HASH_PATTERN.test(ref.recordHash)) throw new Error('Vault recordHash 非法');
138
+ const expected = vaultRecordHashOf(ref);
139
+ if (expected !== ref.recordHash) throw new Error('Vault recordHash 与索引内容不一致');
140
+ }
141
+ export function verifyVaultChain(records) {
142
+ let previous;
143
+ records.forEach((ref, index)=>{
144
+ const expectedSeq = index + 1;
145
+ if (ref.seq !== expectedSeq) throw new Error(`Vault seq 应为 ${expectedSeq},实际是 ${ref.seq}`);
146
+ assertVaultRecordRef(ref);
147
+ if (previous === undefined) {
148
+ if (ref.prevHash !== VAULT_GENESIS_HASH) throw new Error('Vault 链首条记录的 prevHash 必须为 null');
149
+ } else {
150
+ if (ref.writer !== previous.writer) throw new Error('Vault 链混用了不同 writer');
151
+ if (ref.prevHash !== previous.recordHash) throw new Error(`Vault 第 ${ref.seq} 条的 prevHash 不匹配`);
152
+ }
153
+ previous = ref;
154
+ });
155
+ }
156
+ function recordKeyOf(vaultKey, writer, seq) {
157
+ return deriveSubkey(vaultKey, `v1|vault-record:${writer}:${seq}`);
158
+ }
159
+ function aadOf(writer, seq, prevHash, schema) {
160
+ return Buffer.from(stableStringify({
161
+ v: VAULT_FORMAT_VERSION,
162
+ writer,
163
+ seq,
164
+ prevHash,
165
+ schema
166
+ }), 'utf8');
167
+ }
168
+ export function vaultRecordHashOf(ref) {
169
+ return sha256(stableStringify({
170
+ v: ref.v,
171
+ writer: ref.writer,
172
+ seq: ref.seq,
173
+ prevHash: ref.prevHash,
174
+ blobSha256: ref.blobSha256,
175
+ bytes: ref.bytes,
176
+ createdAt: ref.createdAt,
177
+ sealed: ref.sealed
178
+ }));
179
+ }
180
+ function assertKey(key, name) {
181
+ if (!Buffer.isBuffer(key) || key.length !== KEY_BYTES) throw new Error(`${name} 必须是 ${KEY_BYTES} 字节`);
182
+ }
183
+ function assertWriter(writer) {
184
+ if (typeof writer !== 'string' || writer.length === 0 || writer.length > 128) throw new Error('Vault writer 非法');
185
+ for (const character of writer){
186
+ if (character.charCodeAt(0) < 0x21 || character.charCodeAt(0) > 0x7e) throw new Error('Vault writer 含非法字符');
187
+ }
188
+ }
189
+ function assertSeq(seq) {
190
+ if (!Number.isSafeInteger(seq) || seq < 1) throw new Error('Vault seq 必须是正整数');
191
+ }
192
+ function assertPrevHash(seq, prevHash) {
193
+ if (seq === 1) {
194
+ if (prevHash !== VAULT_GENESIS_HASH) throw new Error('Vault seq=1 的 prevHash 必须为 null');
195
+ return;
196
+ }
197
+ if (prevHash === null || !HASH_PATTERN.test(prevHash)) throw new Error('Vault prevHash 非法');
198
+ }
199
+ function assertTimestamp(value, name) {
200
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Vault ${name} 非法`);
201
+ }
202
+ function assertKind(kind) {
203
+ if (typeof kind !== 'string' || !KIND_PATTERN.test(kind)) throw new Error('Vault kind 非法');
204
+ }
205
+ function assertEntity(entity) {
206
+ if (typeof entity !== 'string' || entity.trim() === '' || entity.length > 512) throw new Error('Vault entity 非法');
207
+ }
208
+
209
+
210
+ //# sourceURL=vault.ts
@@ -0,0 +1,25 @@
1
+ export const ERROR_STATUS = {
2
+ unauthorized: 401,
3
+ 'bad-request': 400,
4
+ 'not-found': 404,
5
+ conflict: 409,
6
+ diverged: 409,
7
+ 'lease-held': 409,
8
+ gone: 410,
9
+ quota: 507,
10
+ 'rate-limited': 429
11
+ };
12
+ export function statusOf(code) {
13
+ return ERROR_STATUS[code];
14
+ }
15
+ export class SyncError extends Error {
16
+ code;
17
+ constructor(code, message){
18
+ super(message);
19
+ this.name = 'SyncError';
20
+ this.code = code;
21
+ }
22
+ }
23
+
24
+
25
+ //# sourceURL=wire.ts