dscs-server 0.0.0-stage → 0.1.0
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.
- package/LICENSE +661 -0
- package/README.md +39 -2
- package/dist/blobs.js +50 -0
- package/dist/db.js +333 -0
- package/dist/http.js +1281 -0
- package/dist/index.js +170 -0
- package/dist/leases.js +43 -0
- package/dist/sessions.js +369 -0
- package/dist/shared/batches.js +230 -0
- package/dist/shared/blob-batch.js +85 -0
- package/dist/shared/chain.js +152 -0
- package/dist/shared/crypto.js +70 -0
- package/dist/shared/frames.js +102 -0
- package/dist/shared/index.js +12 -0
- package/dist/shared/protocol.js +14 -0
- package/dist/shared/sealed-frame.js +155 -0
- package/dist/shared/vault.js +210 -0
- package/dist/shared/wire.js +25 -0
- package/dist/vault.js +147 -0
- package/package.json +43 -4
- package/web/admin.html +895 -0
- package/web/setup.html +591 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { sha256 } from './crypto.js';
|
|
2
|
+
import { stableStringify } from './chain.js';
|
|
3
|
+
import { openSealedFrame, sealFrame } from './sealed-frame.js';
|
|
4
|
+
export const HEADER_INDEX = 0;
|
|
5
|
+
export const HEADER_SEQ = -1;
|
|
6
|
+
export const DEFAULT_BATCH_EVENTS = 50;
|
|
7
|
+
export const DEFAULT_BATCH_BYTES = 256 * 1024;
|
|
8
|
+
export function headerMetaOf(header, inheritedEventCount) {
|
|
9
|
+
return {
|
|
10
|
+
formatVersion: header.version,
|
|
11
|
+
createdAt: header.createdAt,
|
|
12
|
+
isSeeded: header.isSeeded,
|
|
13
|
+
delegationDepth: header.delegationDepth ?? 0,
|
|
14
|
+
...header.agentPreset === undefined ? {} : {
|
|
15
|
+
agentPreset: header.agentPreset
|
|
16
|
+
},
|
|
17
|
+
...header.parentSession === undefined ? {} : {
|
|
18
|
+
parentSession: header.parentSession
|
|
19
|
+
},
|
|
20
|
+
...header.origin === undefined ? {} : {
|
|
21
|
+
origin: header.origin
|
|
22
|
+
},
|
|
23
|
+
...header.isSeeded ? {
|
|
24
|
+
inheritedEventCount: inheritedEventCount ?? 0
|
|
25
|
+
} : {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function headerChainHash(meta) {
|
|
29
|
+
return sha256(stableStringify(meta));
|
|
30
|
+
}
|
|
31
|
+
export function sealHeader(meta, sessionId, sessionKey, compression = 'none') {
|
|
32
|
+
const plain = Buffer.from(stableStringify(meta), 'utf8');
|
|
33
|
+
return sealFrame(plain, {
|
|
34
|
+
sessionId,
|
|
35
|
+
index: HEADER_INDEX,
|
|
36
|
+
seqFrom: HEADER_SEQ,
|
|
37
|
+
seqTo: HEADER_SEQ
|
|
38
|
+
}, sessionKey, compression === 'zstd' ? {
|
|
39
|
+
compression: 'zstd'
|
|
40
|
+
} : {});
|
|
41
|
+
}
|
|
42
|
+
export function openHeader(sealed, ref, sessionId, sessionKey) {
|
|
43
|
+
if (ref.index !== HEADER_INDEX || ref.seqFrom !== HEADER_SEQ || ref.seqTo !== HEADER_SEQ) {
|
|
44
|
+
throw new Error('第 0 个链条目不是会话头');
|
|
45
|
+
}
|
|
46
|
+
const plain = openSealedFrame(sealed, {
|
|
47
|
+
sessionId,
|
|
48
|
+
index: ref.index,
|
|
49
|
+
seqFrom: ref.seqFrom,
|
|
50
|
+
seqTo: ref.seqTo,
|
|
51
|
+
chainHash: ref.chainHash,
|
|
52
|
+
...ref.compression === undefined ? {} : {
|
|
53
|
+
compression: ref.compression
|
|
54
|
+
},
|
|
55
|
+
...ref.compressedHash === undefined ? {} : {
|
|
56
|
+
compressedHash: ref.compressedHash
|
|
57
|
+
}
|
|
58
|
+
}, sessionKey);
|
|
59
|
+
return parseHeaderMeta(JSON.parse(plain.toString('utf8')));
|
|
60
|
+
}
|
|
61
|
+
function parseEvents(value) {
|
|
62
|
+
if (!Array.isArray(value)) throw new Error('批次不是事件数组');
|
|
63
|
+
return value.map((entry, position)=>{
|
|
64
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`批次第 ${position} 项不是事件对象`);
|
|
65
|
+
const seq = entry['seq'];
|
|
66
|
+
if (!Number.isSafeInteger(seq) || seq < 0) throw new Error(`批次第 ${position} 项的 seq 非法`);
|
|
67
|
+
return entry;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function parseHeaderMeta(value) {
|
|
71
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('会话头不是对象');
|
|
72
|
+
const record = value;
|
|
73
|
+
const formatVersion = record['formatVersion'];
|
|
74
|
+
const createdAt = record['createdAt'];
|
|
75
|
+
const isSeeded = record['isSeeded'];
|
|
76
|
+
const delegationDepth = record['delegationDepth'];
|
|
77
|
+
if (!Number.isSafeInteger(formatVersion) || formatVersion < 1) throw new Error('会话头 formatVersion 非法');
|
|
78
|
+
if (!Number.isSafeInteger(createdAt)) throw new Error('会话头 createdAt 非法');
|
|
79
|
+
if (typeof isSeeded !== 'boolean') throw new Error('会话头 isSeeded 非法');
|
|
80
|
+
if (!Number.isSafeInteger(delegationDepth) || delegationDepth < 0) throw new Error('会话头 delegationDepth 非法');
|
|
81
|
+
const stringField = (key)=>{
|
|
82
|
+
const field = record[key];
|
|
83
|
+
if (field === undefined) return undefined;
|
|
84
|
+
if (typeof field !== 'string') throw new Error(`会话头 ${key} 非法`);
|
|
85
|
+
return field;
|
|
86
|
+
};
|
|
87
|
+
const inherited = record['inheritedEventCount'];
|
|
88
|
+
if (inherited !== undefined && (!Number.isSafeInteger(inherited) || inherited < 0)) {
|
|
89
|
+
throw new Error('会话头 inheritedEventCount 非法');
|
|
90
|
+
}
|
|
91
|
+
const agentPreset = stringField('agentPreset');
|
|
92
|
+
const parentSession = stringField('parentSession');
|
|
93
|
+
const origin = stringField('origin');
|
|
94
|
+
return {
|
|
95
|
+
formatVersion: formatVersion,
|
|
96
|
+
createdAt: createdAt,
|
|
97
|
+
isSeeded,
|
|
98
|
+
delegationDepth: delegationDepth,
|
|
99
|
+
...agentPreset === undefined ? {} : {
|
|
100
|
+
agentPreset
|
|
101
|
+
},
|
|
102
|
+
...parentSession === undefined ? {} : {
|
|
103
|
+
parentSession
|
|
104
|
+
},
|
|
105
|
+
...origin === undefined ? {} : {
|
|
106
|
+
origin
|
|
107
|
+
},
|
|
108
|
+
...inherited === undefined ? {} : {
|
|
109
|
+
inheritedEventCount: inherited
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function serializeEventBatch(events) {
|
|
114
|
+
return stableStringify(events);
|
|
115
|
+
}
|
|
116
|
+
export function eventBatchHash(events) {
|
|
117
|
+
return sha256(serializeEventBatch(events));
|
|
118
|
+
}
|
|
119
|
+
export function sealEventBatchFromText(text, sessionId, index, seqFrom, seqTo, sessionKey, compression = 'none') {
|
|
120
|
+
if (index < 1) throw new Error('事件批次的链下标必须 >= 1');
|
|
121
|
+
if (seqFrom > seqTo) throw new Error('事件批次的 seq 区间非法');
|
|
122
|
+
return sealFrame(Buffer.from(text, 'utf8'), {
|
|
123
|
+
sessionId,
|
|
124
|
+
index,
|
|
125
|
+
seqFrom,
|
|
126
|
+
seqTo
|
|
127
|
+
}, sessionKey, compression === 'zstd' ? {
|
|
128
|
+
compression: 'zstd'
|
|
129
|
+
} : {});
|
|
130
|
+
}
|
|
131
|
+
export function sealEventBatch(events, sessionId, index, sessionKey, compression = 'none') {
|
|
132
|
+
if (index < 1) throw new Error('事件批次的链下标必须 >= 1');
|
|
133
|
+
if (events.length === 0) throw new Error('不能封空批次');
|
|
134
|
+
const seqFrom = events[0].seq;
|
|
135
|
+
const seqTo = events[events.length - 1].seq;
|
|
136
|
+
assertContiguous(events, seqFrom, seqTo);
|
|
137
|
+
return sealEventBatchFromText(serializeEventBatch(events), sessionId, index, seqFrom, seqTo, sessionKey, compression);
|
|
138
|
+
}
|
|
139
|
+
export function openEventBatch(sealed, ref, sessionId, sessionKey) {
|
|
140
|
+
const plain = openSealedFrame(sealed, {
|
|
141
|
+
sessionId,
|
|
142
|
+
index: ref.index,
|
|
143
|
+
seqFrom: ref.seqFrom,
|
|
144
|
+
seqTo: ref.seqTo,
|
|
145
|
+
chainHash: ref.chainHash,
|
|
146
|
+
...ref.compression === undefined ? {} : {
|
|
147
|
+
compression: ref.compression
|
|
148
|
+
},
|
|
149
|
+
...ref.compressedHash === undefined ? {} : {
|
|
150
|
+
compressedHash: ref.compressedHash
|
|
151
|
+
}
|
|
152
|
+
}, sessionKey);
|
|
153
|
+
const events = parseEvents(JSON.parse(plain.toString('utf8')));
|
|
154
|
+
if (events.length !== ref.seqTo - ref.seqFrom + 1) throw new Error('批次事件数与 seq 区间不符');
|
|
155
|
+
if (events.length === 0 || events[0].seq !== ref.seqFrom || events[events.length - 1].seq !== ref.seqTo) {
|
|
156
|
+
throw new Error('批次首尾 seq 与清单不符');
|
|
157
|
+
}
|
|
158
|
+
assertContiguous(events, ref.seqFrom, ref.seqTo);
|
|
159
|
+
return events;
|
|
160
|
+
}
|
|
161
|
+
function assertContiguous(events, seqFrom, seqTo) {
|
|
162
|
+
if (events.length !== seqTo - seqFrom + 1) throw new Error('事件数与 seq 区间不符');
|
|
163
|
+
events.forEach((event, offset)=>{
|
|
164
|
+
if (event.seq !== seqFrom + offset) throw new Error(`第 ${offset} 个事件的 seq 应为 ${seqFrom + offset},实际是 ${event.seq}`);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export function* iterateEventBatches(events, startAfterSeq, options = {}, includeText = true) {
|
|
168
|
+
const maxEvents = options.maxEvents ?? DEFAULT_BATCH_EVENTS;
|
|
169
|
+
const maxBytes = options.maxBytes ?? DEFAULT_BATCH_BYTES;
|
|
170
|
+
if (!Number.isSafeInteger(maxEvents) || maxEvents < 1) throw new Error('maxEvents 必须是正整数');
|
|
171
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) throw new Error('maxBytes 必须是正整数');
|
|
172
|
+
events.forEach((event, offset)=>{
|
|
173
|
+
if (!Number.isSafeInteger(event.seq) || event.seq < 0) throw new Error(`第 ${offset} 个事件的 seq 非法`);
|
|
174
|
+
if (offset > 0 && events[offset - 1].seq + 1 !== event.seq) throw new Error(`事件 seq 不连续:${events[offset - 1].seq} -> ${event.seq}`);
|
|
175
|
+
});
|
|
176
|
+
const pending = events.filter((event)=>event.seq > startAfterSeq);
|
|
177
|
+
if (pending.length === 0) return;
|
|
178
|
+
if (pending[0].seq !== startAfterSeq + 1) throw new Error(`待传事件应从 ${startAfterSeq + 1} 开始,实际是 ${pending[0].seq}`);
|
|
179
|
+
let current = [];
|
|
180
|
+
let currentTexts = includeText ? [] : undefined;
|
|
181
|
+
let currentBytes = 2;
|
|
182
|
+
for (const event of pending){
|
|
183
|
+
const eventText = stableStringify(event);
|
|
184
|
+
const eventBytes = Buffer.byteLength(eventText, 'utf8');
|
|
185
|
+
const candidateBytes = current.length === 0 ? currentBytes + eventBytes : currentBytes + 1 + eventBytes;
|
|
186
|
+
const candidateLength = current.length + 1;
|
|
187
|
+
if (current.length > 0 && (candidateLength > maxEvents || candidateBytes > maxBytes)) {
|
|
188
|
+
yield {
|
|
189
|
+
seqFrom: current[0].seq,
|
|
190
|
+
seqTo: current[current.length - 1].seq,
|
|
191
|
+
events: current,
|
|
192
|
+
bytes: currentBytes,
|
|
193
|
+
text: currentTexts === undefined ? '' : `[${currentTexts.join(',')}]`
|
|
194
|
+
};
|
|
195
|
+
current = [
|
|
196
|
+
event
|
|
197
|
+
];
|
|
198
|
+
currentTexts = includeText ? [
|
|
199
|
+
eventText
|
|
200
|
+
] : undefined;
|
|
201
|
+
currentBytes = 2 + eventBytes;
|
|
202
|
+
} else {
|
|
203
|
+
current.push(event);
|
|
204
|
+
currentTexts?.push(eventText);
|
|
205
|
+
currentBytes = candidateBytes;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (current.length > 0) {
|
|
209
|
+
yield {
|
|
210
|
+
seqFrom: current[0].seq,
|
|
211
|
+
seqTo: current[current.length - 1].seq,
|
|
212
|
+
events: current,
|
|
213
|
+
bytes: currentBytes,
|
|
214
|
+
text: currentTexts === undefined ? '' : `[${currentTexts.join(',')}]`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function planEventBatches(events, startAfterSeq, options = {}) {
|
|
219
|
+
return [
|
|
220
|
+
...iterateEventBatches(events, startAfterSeq, options, false)
|
|
221
|
+
].map(({ seqFrom, seqTo, events: batchEvents, bytes })=>({
|
|
222
|
+
seqFrom,
|
|
223
|
+
seqTo,
|
|
224
|
+
events: batchEvents,
|
|
225
|
+
bytes
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
//# sourceURL=batches.ts
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { sha256 } from './crypto.js';
|
|
2
|
+
export const BLOB_BATCH_CONTENT_TYPE = 'application/x-dsh-blob-batch';
|
|
3
|
+
export const BLOB_BATCH_HEADER_BYTES = 12;
|
|
4
|
+
export const BLOB_BATCH_ENTRY_HEADER_BYTES = 40;
|
|
5
|
+
export const MAX_BATCH_BLOBS = 64;
|
|
6
|
+
export const MAX_BATCH_BODY_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
export const MAX_BLOB_BYTES = 32 * 1024 * 1024;
|
|
8
|
+
const MAGIC = Buffer.from('DSHB', 'ascii');
|
|
9
|
+
const VERSION = 1;
|
|
10
|
+
const KIND_TO_CODE = {
|
|
11
|
+
frame: 1,
|
|
12
|
+
attachment: 2,
|
|
13
|
+
vault: 3
|
|
14
|
+
};
|
|
15
|
+
const CODE_TO_KIND = {
|
|
16
|
+
1: 'frame',
|
|
17
|
+
2: 'attachment',
|
|
18
|
+
3: 'vault'
|
|
19
|
+
};
|
|
20
|
+
export function encodeBlobBatch(entries) {
|
|
21
|
+
if (entries.length < 1 || entries.length > MAX_BATCH_BLOBS) {
|
|
22
|
+
throw new Error(`批量 blob 条数必须在 1–${MAX_BATCH_BLOBS} 之间`);
|
|
23
|
+
}
|
|
24
|
+
let total = BLOB_BATCH_HEADER_BYTES;
|
|
25
|
+
for (const entry of entries){
|
|
26
|
+
if (entry.data.length === 0) throw new Error('批量 blob 不能包含空内容');
|
|
27
|
+
if (entry.data.length > MAX_BLOB_BYTES) throw new Error(`单个 blob 超过上限 ${MAX_BLOB_BYTES} 字节`);
|
|
28
|
+
total += BLOB_BATCH_ENTRY_HEADER_BYTES + entry.data.length;
|
|
29
|
+
}
|
|
30
|
+
if (total > MAX_BATCH_BODY_BYTES) throw new Error(`批量 blob 超过上限 ${MAX_BATCH_BODY_BYTES} 字节`);
|
|
31
|
+
const output = Buffer.allocUnsafe(total);
|
|
32
|
+
MAGIC.copy(output, 0);
|
|
33
|
+
output.writeUInt8(VERSION, 4);
|
|
34
|
+
output.writeUInt8(0, 5);
|
|
35
|
+
output.writeUInt16BE(0, 6);
|
|
36
|
+
output.writeUInt32BE(entries.length, 8);
|
|
37
|
+
let offset = BLOB_BATCH_HEADER_BYTES;
|
|
38
|
+
for (const entry of entries){
|
|
39
|
+
const digest = sha256(entry.data);
|
|
40
|
+
output.writeUInt8(KIND_TO_CODE[entry.kind], offset);
|
|
41
|
+
output.fill(0, offset + 1, offset + 4);
|
|
42
|
+
output.writeUInt32BE(entry.data.length, offset + 4);
|
|
43
|
+
Buffer.from(digest, 'hex').copy(output, offset + 8);
|
|
44
|
+
entry.data.copy(output, offset + BLOB_BATCH_ENTRY_HEADER_BYTES);
|
|
45
|
+
offset += BLOB_BATCH_ENTRY_HEADER_BYTES + entry.data.length;
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
export function decodeBlobBatch(bytes) {
|
|
50
|
+
if (bytes.length < BLOB_BATCH_HEADER_BYTES) throw new Error('批量 blob 头被截断');
|
|
51
|
+
if (!bytes.subarray(0, 4).equals(MAGIC)) throw new Error('不是 DSHB 批量 blob');
|
|
52
|
+
if (bytes.readUInt8(4) !== VERSION) throw new Error(`不支持的批量 blob 版本:${bytes.readUInt8(4)}`);
|
|
53
|
+
if (bytes.readUInt8(5) !== 0) throw new Error('批量 blob flags 非零');
|
|
54
|
+
if (bytes.readUInt16BE(6) !== 0) throw new Error('批量 blob reserved 非零');
|
|
55
|
+
const count = bytes.readUInt32BE(8);
|
|
56
|
+
if (count < 1 || count > MAX_BATCH_BLOBS) throw new Error(`批量 blob 条数非法:${count}`);
|
|
57
|
+
const entries = [];
|
|
58
|
+
let offset = BLOB_BATCH_HEADER_BYTES;
|
|
59
|
+
for(let index = 0; index < count; index += 1){
|
|
60
|
+
if (offset + BLOB_BATCH_ENTRY_HEADER_BYTES > bytes.length) throw new Error(`批量 blob 第 ${index} 条头被截断`);
|
|
61
|
+
const kind = CODE_TO_KIND[bytes.readUInt8(offset)];
|
|
62
|
+
if (kind === undefined) throw new Error(`批量 blob 第 ${index} 条类别非法`);
|
|
63
|
+
if (bytes.readUInt8(offset + 1) !== 0 || bytes.readUInt8(offset + 2) !== 0 || bytes.readUInt8(offset + 3) !== 0) {
|
|
64
|
+
throw new Error(`批量 blob 第 ${index} 条 reserved 非零`);
|
|
65
|
+
}
|
|
66
|
+
const length = bytes.readUInt32BE(offset + 4);
|
|
67
|
+
if (length === 0) throw new Error(`批量 blob 第 ${index} 条为空`);
|
|
68
|
+
if (length > MAX_BLOB_BYTES) throw new Error(`批量 blob 第 ${index} 条超过单 blob 上限`);
|
|
69
|
+
const start = offset + BLOB_BATCH_ENTRY_HEADER_BYTES;
|
|
70
|
+
const end = start + length;
|
|
71
|
+
if (end > bytes.length) throw new Error(`批量 blob 第 ${index} 条数据被截断`);
|
|
72
|
+
entries.push({
|
|
73
|
+
kind,
|
|
74
|
+
sha256: bytes.subarray(offset + 8, offset + 8 + 32).toString('hex'),
|
|
75
|
+
data: bytes.subarray(start, end)
|
|
76
|
+
});
|
|
77
|
+
offset = end;
|
|
78
|
+
}
|
|
79
|
+
if (offset !== bytes.length) throw new Error('批量 blob 尾部有多余字节');
|
|
80
|
+
if (bytes.length > MAX_BATCH_BODY_BYTES) throw new Error(`批量 blob 超过上限 ${MAX_BATCH_BODY_BYTES} 字节`);
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
//# sourceURL=blob-batch.ts
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
if (frame.compression === undefined) {
|
|
94
|
+
if (frame.sealedVersion !== 1 || frame.compressedHash !== undefined) throw new Error(`manifest 第 ${position} 条缺少压缩元数据`);
|
|
95
|
+
} else {
|
|
96
|
+
if (frame.compression !== 'zstd') throw new Error(`manifest 第 ${position} 条的压缩方式非法`);
|
|
97
|
+
if (frame.sealedVersion < 2) throw new Error(`manifest 第 ${position} 条的压缩帧版本非法`);
|
|
98
|
+
if (frame.compressedHash === undefined || !HASH_PATTERN.test(frame.compressedHash)) {
|
|
99
|
+
throw new Error(`manifest 第 ${position} 条的 compressedHash 非法`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const last = manifest.frames[manifest.frames.length - 1];
|
|
104
|
+
if ((last === undefined ? -1 : last.seqTo) !== manifest.lastSeq) throw new Error('manifest 的 lastSeq 与最后一帧不一致');
|
|
105
|
+
const safe = manifest.safeThroughSeq ?? manifest.lastSeq;
|
|
106
|
+
if (!Number.isSafeInteger(safe) || safe < -1 || safe > manifest.lastSeq) throw new Error('manifest 的 safeThroughSeq 非法');
|
|
107
|
+
if (manifest.workspaceKey !== undefined && !/^[A-Za-z0-9_-]{16,64}$/u.test(manifest.workspaceKey)) {
|
|
108
|
+
throw new Error('manifest 的 workspaceKey 非法');
|
|
109
|
+
}
|
|
110
|
+
if (manifest.tailOpen !== undefined && typeof manifest.tailOpen !== 'boolean') throw new Error('manifest 的 tailOpen 非法');
|
|
111
|
+
}
|
|
112
|
+
function canonical(value) {
|
|
113
|
+
if (Array.isArray(value)) return value.map((entry)=>canonical(entry));
|
|
114
|
+
if (value !== null && typeof value === 'object') {
|
|
115
|
+
const sorted = {};
|
|
116
|
+
for (const key of Object.keys(value).sort())sorted[key] = canonical(value[key]);
|
|
117
|
+
return sorted;
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
export function stableStringify(value) {
|
|
122
|
+
return JSON.stringify(canonical(value));
|
|
123
|
+
}
|
|
124
|
+
export function manifestDigest(manifest) {
|
|
125
|
+
return sha256(stableStringify({
|
|
126
|
+
sessionId: manifest.sessionId,
|
|
127
|
+
formatVersion: manifest.formatVersion,
|
|
128
|
+
createdAt: manifest.createdAt,
|
|
129
|
+
lastSeq: manifest.lastSeq,
|
|
130
|
+
workspaceKey: manifest.workspaceKey ?? null,
|
|
131
|
+
tailOpen: manifest.tailOpen ?? null,
|
|
132
|
+
safeThroughSeq: manifest.safeThroughSeq ?? manifest.lastSeq,
|
|
133
|
+
frames: manifest.frames.map((frame)=>({
|
|
134
|
+
index: frame.index,
|
|
135
|
+
chainHash: frame.chainHash,
|
|
136
|
+
blobSha256: frame.blobSha256,
|
|
137
|
+
bytes: frame.bytes,
|
|
138
|
+
sealedVersion: frame.sealedVersion,
|
|
139
|
+
keyId: frame.keyId,
|
|
140
|
+
seqFrom: frame.seqFrom,
|
|
141
|
+
seqTo: frame.seqTo,
|
|
142
|
+
...frame.compression === undefined ? {} : {
|
|
143
|
+
compression: frame.compression,
|
|
144
|
+
compressedHash: frame.compressedHash ?? null
|
|
145
|
+
}
|
|
146
|
+
})),
|
|
147
|
+
deletedAt: manifest.deletedAt ?? null
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
//# 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,12 @@
|
|
|
1
|
+
export * from './batches.js';
|
|
2
|
+
export * from './blob-batch.js';
|
|
3
|
+
export * from './chain.js';
|
|
4
|
+
export * from './crypto.js';
|
|
5
|
+
export * from './frames.js';
|
|
6
|
+
export * from './protocol.js';
|
|
7
|
+
export * from './sealed-frame.js';
|
|
8
|
+
export * from './vault.js';
|
|
9
|
+
export * from './wire.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
//# sourceURL=index.ts
|