@git-stunts/git-warp 10.3.2 → 10.7.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/README.md +6 -3
- package/SECURITY.md +89 -1
- package/bin/warp-graph.js +574 -208
- package/index.d.ts +55 -0
- package/index.js +4 -0
- package/package.json +8 -4
- package/src/domain/WarpGraph.js +334 -161
- package/src/domain/crdt/LWW.js +1 -1
- package/src/domain/crdt/ORSet.js +10 -6
- package/src/domain/crdt/VersionVector.js +5 -1
- package/src/domain/errors/EmptyMessageError.js +2 -4
- package/src/domain/errors/ForkError.js +4 -0
- package/src/domain/errors/IndexError.js +4 -0
- package/src/domain/errors/OperationAbortedError.js +4 -0
- package/src/domain/errors/QueryError.js +4 -0
- package/src/domain/errors/SchemaUnsupportedError.js +4 -0
- package/src/domain/errors/ShardCorruptionError.js +2 -6
- package/src/domain/errors/ShardLoadError.js +2 -6
- package/src/domain/errors/ShardValidationError.js +2 -7
- package/src/domain/errors/StorageError.js +2 -6
- package/src/domain/errors/SyncError.js +4 -0
- package/src/domain/errors/TraversalError.js +4 -0
- package/src/domain/errors/WarpError.js +2 -4
- package/src/domain/errors/WormholeError.js +4 -0
- package/src/domain/services/AnchorMessageCodec.js +1 -4
- package/src/domain/services/BitmapIndexBuilder.js +10 -6
- package/src/domain/services/BitmapIndexReader.js +27 -21
- package/src/domain/services/BoundaryTransitionRecord.js +22 -15
- package/src/domain/services/CheckpointMessageCodec.js +1 -7
- package/src/domain/services/CheckpointSerializerV5.js +20 -19
- package/src/domain/services/CheckpointService.js +18 -18
- package/src/domain/services/CommitDagTraversalService.js +13 -1
- package/src/domain/services/DagPathFinding.js +40 -18
- package/src/domain/services/DagTopology.js +7 -6
- package/src/domain/services/DagTraversal.js +5 -3
- package/src/domain/services/Frontier.js +7 -6
- package/src/domain/services/HealthCheckService.js +15 -14
- package/src/domain/services/HookInstaller.js +64 -13
- package/src/domain/services/HttpSyncServer.js +88 -19
- package/src/domain/services/IndexRebuildService.js +12 -12
- package/src/domain/services/IndexStalenessChecker.js +13 -6
- package/src/domain/services/JoinReducer.js +28 -27
- package/src/domain/services/LogicalTraversal.js +7 -6
- package/src/domain/services/MessageCodecInternal.js +2 -0
- package/src/domain/services/ObserverView.js +6 -6
- package/src/domain/services/PatchBuilderV2.js +9 -9
- package/src/domain/services/PatchMessageCodec.js +1 -7
- package/src/domain/services/ProvenanceIndex.js +6 -8
- package/src/domain/services/ProvenancePayload.js +1 -2
- package/src/domain/services/QueryBuilder.js +29 -23
- package/src/domain/services/StateDiff.js +7 -7
- package/src/domain/services/StateSerializerV5.js +8 -6
- package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
- package/src/domain/services/SyncAuthService.js +396 -0
- package/src/domain/services/SyncProtocol.js +23 -26
- package/src/domain/services/TemporalQuery.js +4 -3
- package/src/domain/services/TranslationCost.js +4 -4
- package/src/domain/services/WormholeService.js +19 -15
- package/src/domain/types/TickReceipt.js +10 -6
- package/src/domain/types/WarpTypesV2.js +2 -3
- package/src/domain/utils/CachedValue.js +1 -1
- package/src/domain/utils/LRUCache.js +3 -3
- package/src/domain/utils/MinHeap.js +2 -2
- package/src/domain/utils/RefLayout.js +19 -0
- package/src/domain/utils/WriterId.js +2 -2
- package/src/domain/utils/defaultCodec.js +9 -2
- package/src/domain/utils/defaultCrypto.js +36 -0
- package/src/domain/utils/roaring.js +5 -5
- package/src/domain/utils/seekCacheKey.js +32 -0
- package/src/domain/warp/PatchSession.js +3 -3
- package/src/domain/warp/Writer.js +2 -2
- package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
- package/src/infrastructure/adapters/ClockAdapter.js +2 -2
- package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
- package/src/infrastructure/adapters/GitGraphAdapter.js +25 -83
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
- package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
- package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
- package/src/infrastructure/adapters/adapterValidation.js +90 -0
- package/src/infrastructure/codecs/CborCodec.js +16 -8
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/CodecPort.js +2 -2
- package/src/ports/CommitPort.js +8 -21
- package/src/ports/ConfigPort.js +3 -3
- package/src/ports/CryptoPort.js +7 -7
- package/src/ports/GraphPersistencePort.js +12 -14
- package/src/ports/HttpServerPort.js +1 -5
- package/src/ports/IndexStoragePort.js +1 -0
- package/src/ports/LoggerPort.js +9 -9
- package/src/ports/RefPort.js +5 -5
- package/src/ports/SeekCachePort.js +73 -0
- package/src/ports/TreePort.js +3 -3
- package/src/visualization/layouts/converters.js +14 -7
- package/src/visualization/layouts/elkAdapter.js +17 -4
- package/src/visualization/layouts/elkLayout.js +23 -7
- package/src/visualization/layouts/index.js +3 -3
- package/src/visualization/renderers/ascii/check.js +30 -17
- package/src/visualization/renderers/ascii/graph.js +92 -1
- package/src/visualization/renderers/ascii/history.js +28 -26
- package/src/visualization/renderers/ascii/info.js +9 -7
- package/src/visualization/renderers/ascii/materialize.js +20 -16
- package/src/visualization/renderers/ascii/opSummary.js +15 -7
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +187 -23
- package/src/visualization/renderers/ascii/table.js +1 -1
- package/src/visualization/renderers/svg/index.js +5 -1
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await -- all async methods match the port contract */
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview In-memory persistence adapter for WARP graph storage.
|
|
4
|
+
*
|
|
5
|
+
* Implements the same {@link GraphPersistencePort} contract as GitGraphAdapter
|
|
6
|
+
* but stores all data in Maps. Designed for fast unit/integration tests that
|
|
7
|
+
* don't need real Git I/O.
|
|
8
|
+
*
|
|
9
|
+
* SHA computation follows Git's object format so debugging is straightforward,
|
|
10
|
+
* but cross-adapter SHA matching is NOT guaranteed.
|
|
11
|
+
*
|
|
12
|
+
* @module infrastructure/adapters/InMemoryGraphAdapter
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { Readable } from 'node:stream';
|
|
17
|
+
import GraphPersistencePort from '../../ports/GraphPersistencePort.js';
|
|
18
|
+
import { validateOid, validateRef, validateLimit, validateConfigKey } from './adapterValidation.js';
|
|
19
|
+
|
|
20
|
+
/** Well-known SHA for Git's empty tree. */
|
|
21
|
+
const EMPTY_TREE_OID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
22
|
+
|
|
23
|
+
// ── SHA helpers ─────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Computes a Git blob SHA-1: `SHA1("blob " + len + "\0" + content)`.
|
|
27
|
+
* @param {Buffer} content
|
|
28
|
+
* @returns {string} 40-hex SHA
|
|
29
|
+
*/
|
|
30
|
+
function hashBlob(content) {
|
|
31
|
+
const header = Buffer.from(`blob ${content.length}\0`);
|
|
32
|
+
return createHash('sha1').update(header).update(content).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the binary tree buffer in Git's internal format and hashes it.
|
|
37
|
+
*
|
|
38
|
+
* Each entry is: `<mode> <path>\0<20-byte binary OID>`
|
|
39
|
+
* Entries are sorted by path (byte order), matching Git's canonical sort.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array<{mode: string, path: string, oid: string}>} entries
|
|
42
|
+
* @returns {string} 40-hex SHA
|
|
43
|
+
*/
|
|
44
|
+
function hashTree(entries) {
|
|
45
|
+
const sorted = [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
46
|
+
const parts = sorted.map(e => {
|
|
47
|
+
const prefix = Buffer.from(`${e.mode} ${e.path}\0`);
|
|
48
|
+
const oidBin = Buffer.from(e.oid, 'hex');
|
|
49
|
+
return Buffer.concat([prefix, oidBin]);
|
|
50
|
+
});
|
|
51
|
+
const body = Buffer.concat(parts);
|
|
52
|
+
const header = Buffer.from(`tree ${body.length}\0`);
|
|
53
|
+
return createHash('sha1').update(header).update(body).digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds a Git-style commit string and hashes it.
|
|
58
|
+
* @param {{treeOid: string, parents: string[], message: string, author: string, date: string}} opts
|
|
59
|
+
* @returns {string} 40-hex SHA
|
|
60
|
+
*/
|
|
61
|
+
function hashCommit({ treeOid, parents, message, author, date }) {
|
|
62
|
+
const lines = [`tree ${treeOid}`];
|
|
63
|
+
for (const p of parents) {
|
|
64
|
+
lines.push(`parent ${p}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(`author ${author} ${date}`);
|
|
67
|
+
lines.push(`committer ${author} ${date}`);
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(message);
|
|
70
|
+
const body = lines.join('\n');
|
|
71
|
+
const header = `commit ${Buffer.byteLength(body)}\0`;
|
|
72
|
+
return createHash('sha1').update(header).update(body).digest('hex');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Adapter ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* In-memory implementation of {@link GraphPersistencePort}.
|
|
79
|
+
*
|
|
80
|
+
* Data structures:
|
|
81
|
+
* - `_commits` — Map<sha, {treeOid, parents[], message, author, date}>
|
|
82
|
+
* - `_blobs` — Map<oid, Buffer>
|
|
83
|
+
* - `_trees` — Map<oid, Array<{mode, path, oid}>>
|
|
84
|
+
* - `_refs` — Map<refName, sha>
|
|
85
|
+
* - `_config` — Map<key, value>
|
|
86
|
+
*
|
|
87
|
+
* @extends GraphPersistencePort
|
|
88
|
+
*/
|
|
89
|
+
export default class InMemoryGraphAdapter extends GraphPersistencePort {
|
|
90
|
+
/**
|
|
91
|
+
* @param {{ author?: string, clock?: { now: () => number } }} [options]
|
|
92
|
+
*/
|
|
93
|
+
constructor({ author, clock } = {}) {
|
|
94
|
+
super();
|
|
95
|
+
this._author = author || 'InMemory <inmemory@test>';
|
|
96
|
+
this._clock = clock || { now: () => Date.now() };
|
|
97
|
+
|
|
98
|
+
/** @type {Map<string, {treeOid: string, parents: string[], message: string, author: string, date: string}>} */
|
|
99
|
+
this._commits = new Map();
|
|
100
|
+
/** @type {Map<string, Buffer>} */
|
|
101
|
+
this._blobs = new Map();
|
|
102
|
+
/** @type {Map<string, Array<{mode: string, path: string, oid: string}>>} */
|
|
103
|
+
this._trees = new Map();
|
|
104
|
+
/** @type {Map<string, string>} */
|
|
105
|
+
this._refs = new Map();
|
|
106
|
+
/** @type {Map<string, string>} */
|
|
107
|
+
this._config = new Map();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── TreePort ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/** @type {string} */
|
|
113
|
+
get emptyTree() {
|
|
114
|
+
return EMPTY_TREE_OID;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a tree from mktree-formatted entries.
|
|
119
|
+
* @param {string[]} entries - Lines in `"<mode> <type> <oid>\t<path>"` format
|
|
120
|
+
* @returns {Promise<string>}
|
|
121
|
+
*/
|
|
122
|
+
async writeTree(entries) {
|
|
123
|
+
const parsed = entries.map(line => {
|
|
124
|
+
const tabIdx = line.indexOf('\t');
|
|
125
|
+
if (tabIdx === -1) {
|
|
126
|
+
throw new Error(`Invalid mktree entry (missing tab): ${line}`);
|
|
127
|
+
}
|
|
128
|
+
const meta = line.slice(0, tabIdx);
|
|
129
|
+
const path = line.slice(tabIdx + 1);
|
|
130
|
+
const [mode, , oid] = meta.split(' ');
|
|
131
|
+
return { mode, path, oid };
|
|
132
|
+
});
|
|
133
|
+
const oid = hashTree(parsed);
|
|
134
|
+
this._trees.set(oid, parsed);
|
|
135
|
+
return oid;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} treeOid
|
|
140
|
+
* @returns {Promise<Record<string, string>>}
|
|
141
|
+
*/
|
|
142
|
+
async readTreeOids(treeOid) {
|
|
143
|
+
validateOid(treeOid);
|
|
144
|
+
if (treeOid === EMPTY_TREE_OID) {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
const entries = this._trees.get(treeOid);
|
|
148
|
+
if (!entries) {
|
|
149
|
+
throw new Error(`Tree not found: ${treeOid}`);
|
|
150
|
+
}
|
|
151
|
+
/** @type {Record<string, string>} */
|
|
152
|
+
const result = {};
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
result[e.path] = e.oid;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} treeOid
|
|
161
|
+
* @returns {Promise<Record<string, Buffer>>}
|
|
162
|
+
*/
|
|
163
|
+
async readTree(treeOid) {
|
|
164
|
+
const oids = await this.readTreeOids(treeOid);
|
|
165
|
+
/** @type {Record<string, Buffer>} */
|
|
166
|
+
const files = {};
|
|
167
|
+
for (const [path, oid] of Object.entries(oids)) {
|
|
168
|
+
files[path] = await this.readBlob(oid);
|
|
169
|
+
}
|
|
170
|
+
return files;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── BlobPort ────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {Buffer|string} content
|
|
177
|
+
* @returns {Promise<string>}
|
|
178
|
+
*/
|
|
179
|
+
async writeBlob(content) {
|
|
180
|
+
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
181
|
+
const oid = hashBlob(buf);
|
|
182
|
+
this._blobs.set(oid, buf);
|
|
183
|
+
return oid;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {string} oid
|
|
188
|
+
* @returns {Promise<Buffer>}
|
|
189
|
+
*/
|
|
190
|
+
async readBlob(oid) {
|
|
191
|
+
validateOid(oid);
|
|
192
|
+
const buf = this._blobs.get(oid);
|
|
193
|
+
if (!buf) {
|
|
194
|
+
throw new Error(`Blob not found: ${oid}`);
|
|
195
|
+
}
|
|
196
|
+
return buf;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── CommitPort ──────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {{ message: string, parents?: string[], sign?: boolean }} options
|
|
203
|
+
* @returns {Promise<string>}
|
|
204
|
+
*/
|
|
205
|
+
async commitNode({ message, parents = [] }) {
|
|
206
|
+
for (const p of parents) {
|
|
207
|
+
validateOid(p);
|
|
208
|
+
}
|
|
209
|
+
return this._createCommit(EMPTY_TREE_OID, parents, message);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {{ treeOid: string, parents?: string[], message: string, sign?: boolean }} options
|
|
214
|
+
* @returns {Promise<string>}
|
|
215
|
+
*/
|
|
216
|
+
async commitNodeWithTree({ treeOid, parents = [], message }) {
|
|
217
|
+
validateOid(treeOid);
|
|
218
|
+
for (const p of parents) {
|
|
219
|
+
validateOid(p);
|
|
220
|
+
}
|
|
221
|
+
return this._createCommit(treeOid, parents, message);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} sha
|
|
226
|
+
* @returns {Promise<string>}
|
|
227
|
+
*/
|
|
228
|
+
async showNode(sha) {
|
|
229
|
+
validateOid(sha);
|
|
230
|
+
const commit = this._commits.get(sha);
|
|
231
|
+
if (!commit) {
|
|
232
|
+
throw new Error(`Commit not found: ${sha}`);
|
|
233
|
+
}
|
|
234
|
+
return commit.message;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {string} sha
|
|
239
|
+
* @returns {Promise<{sha: string, message: string, author: string, date: string, parents: string[]}>}
|
|
240
|
+
*/
|
|
241
|
+
async getNodeInfo(sha) {
|
|
242
|
+
validateOid(sha);
|
|
243
|
+
const commit = this._commits.get(sha);
|
|
244
|
+
if (!commit) {
|
|
245
|
+
throw new Error(`Commit not found: ${sha}`);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
sha,
|
|
249
|
+
message: commit.message,
|
|
250
|
+
author: commit.author,
|
|
251
|
+
date: commit.date,
|
|
252
|
+
parents: [...commit.parents],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {string} sha
|
|
258
|
+
* @returns {Promise<boolean>}
|
|
259
|
+
*/
|
|
260
|
+
async nodeExists(sha) {
|
|
261
|
+
validateOid(sha);
|
|
262
|
+
return this._commits.has(sha);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {string} ref
|
|
267
|
+
* @returns {Promise<number>}
|
|
268
|
+
*/
|
|
269
|
+
async countNodes(ref) {
|
|
270
|
+
validateRef(ref);
|
|
271
|
+
const tip = this._resolveRef(ref);
|
|
272
|
+
if (!tip) {
|
|
273
|
+
throw new Error(`Ref not found: ${ref}`);
|
|
274
|
+
}
|
|
275
|
+
const visited = new Set();
|
|
276
|
+
const stack = [tip];
|
|
277
|
+
while (stack.length > 0) {
|
|
278
|
+
const sha = /** @type {string} */ (stack.pop());
|
|
279
|
+
if (visited.has(sha)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
visited.add(sha);
|
|
283
|
+
const commit = this._commits.get(sha);
|
|
284
|
+
if (commit) {
|
|
285
|
+
for (const p of commit.parents) {
|
|
286
|
+
stack.push(p);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return visited.size;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {{ ref: string, limit?: number, format?: string }} options
|
|
295
|
+
* @returns {Promise<string>}
|
|
296
|
+
*/
|
|
297
|
+
async logNodes({ ref, limit = 50, format: _format }) {
|
|
298
|
+
validateRef(ref);
|
|
299
|
+
validateLimit(limit);
|
|
300
|
+
const records = this._walkLog(ref, limit);
|
|
301
|
+
// Format param is accepted for port compatibility but always uses
|
|
302
|
+
// the GitLogParser-compatible layout (SHA\nauthor\ndate\nparents\nmessage).
|
|
303
|
+
if (!_format) {
|
|
304
|
+
return records.map(c => `commit ${c.sha}\nAuthor: ${c.author}\nDate: ${c.date}\n\n ${c.message}\n`).join('\n');
|
|
305
|
+
}
|
|
306
|
+
return records.map(c => this._formatCommitRecord(c)).join('\0') + (records.length > 0 ? '\0' : '');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {{ ref: string, limit?: number, format?: string }} options
|
|
311
|
+
* @returns {Promise<Readable>}
|
|
312
|
+
*/
|
|
313
|
+
async logNodesStream({ ref, limit = 1000000, format: _format }) {
|
|
314
|
+
validateRef(ref);
|
|
315
|
+
validateLimit(limit);
|
|
316
|
+
const records = this._walkLog(ref, limit);
|
|
317
|
+
const formatted = records.map(c => this._formatCommitRecord(c)).join('\0') + (records.length > 0 ? '\0' : '');
|
|
318
|
+
return Readable.from([formatted]);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @returns {Promise<{ok: boolean, latencyMs: number}>}
|
|
323
|
+
*/
|
|
324
|
+
async ping() {
|
|
325
|
+
return { ok: true, latencyMs: 0 };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── RefPort ─────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} ref
|
|
332
|
+
* @param {string} oid
|
|
333
|
+
* @returns {Promise<void>}
|
|
334
|
+
*/
|
|
335
|
+
async updateRef(ref, oid) {
|
|
336
|
+
validateRef(ref);
|
|
337
|
+
validateOid(oid);
|
|
338
|
+
this._refs.set(ref, oid);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {string} ref
|
|
343
|
+
* @returns {Promise<string|null>}
|
|
344
|
+
*/
|
|
345
|
+
async readRef(ref) {
|
|
346
|
+
validateRef(ref);
|
|
347
|
+
return this._refs.get(ref) || null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @param {string} ref
|
|
352
|
+
* @returns {Promise<void>}
|
|
353
|
+
*/
|
|
354
|
+
async deleteRef(ref) {
|
|
355
|
+
validateRef(ref);
|
|
356
|
+
this._refs.delete(ref);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {string} prefix
|
|
361
|
+
* @returns {Promise<string[]>}
|
|
362
|
+
*/
|
|
363
|
+
async listRefs(prefix) {
|
|
364
|
+
validateRef(prefix);
|
|
365
|
+
const result = [];
|
|
366
|
+
for (const key of this._refs.keys()) {
|
|
367
|
+
if (key.startsWith(prefix)) {
|
|
368
|
+
result.push(key);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return result.sort();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── ConfigPort ──────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param {string} key
|
|
378
|
+
* @returns {Promise<string|null>}
|
|
379
|
+
*/
|
|
380
|
+
async configGet(key) {
|
|
381
|
+
validateConfigKey(key);
|
|
382
|
+
return this._config.get(key) ?? null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param {string} key
|
|
387
|
+
* @param {string} value
|
|
388
|
+
* @returns {Promise<void>}
|
|
389
|
+
*/
|
|
390
|
+
async configSet(key, value) {
|
|
391
|
+
validateConfigKey(key);
|
|
392
|
+
if (typeof value !== 'string') {
|
|
393
|
+
throw new Error('Config value must be a string');
|
|
394
|
+
}
|
|
395
|
+
this._config.set(key, value);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ── Private helpers ─────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @param {string} treeOid
|
|
402
|
+
* @param {string[]} parents
|
|
403
|
+
* @param {string} message
|
|
404
|
+
* @returns {string}
|
|
405
|
+
*/
|
|
406
|
+
_createCommit(treeOid, parents, message) {
|
|
407
|
+
const date = new Date(this._clock.now()).toISOString();
|
|
408
|
+
const sha = hashCommit({
|
|
409
|
+
treeOid,
|
|
410
|
+
parents,
|
|
411
|
+
message,
|
|
412
|
+
author: this._author,
|
|
413
|
+
date,
|
|
414
|
+
});
|
|
415
|
+
this._commits.set(sha, {
|
|
416
|
+
treeOid,
|
|
417
|
+
parents: [...parents],
|
|
418
|
+
message,
|
|
419
|
+
author: this._author,
|
|
420
|
+
date,
|
|
421
|
+
});
|
|
422
|
+
return sha;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Resolves a ref name to a SHA. If the ref looks like a raw SHA, returns it.
|
|
427
|
+
* @param {string} ref
|
|
428
|
+
* @returns {string|null}
|
|
429
|
+
*/
|
|
430
|
+
_resolveRef(ref) {
|
|
431
|
+
if (this._refs.has(ref)) {
|
|
432
|
+
return /** @type {string} */ (this._refs.get(ref));
|
|
433
|
+
}
|
|
434
|
+
if (this._commits.has(ref)) {
|
|
435
|
+
return ref;
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Walks commit history from a ref, reverse chronological (newest first),
|
|
442
|
+
* up to limit. Matches `git log` default ordering for merge DAGs.
|
|
443
|
+
* @param {string} ref
|
|
444
|
+
* @param {number} limit
|
|
445
|
+
* @returns {Array<{sha: string, message: string, author: string, date: string, parents: string[]}>}
|
|
446
|
+
*/
|
|
447
|
+
_walkLog(ref, limit) {
|
|
448
|
+
const tip = this._resolveRef(ref);
|
|
449
|
+
if (!tip) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
/** @type {Array<{sha: string, message: string, author: string, date: string, parents: string[]}>} */
|
|
453
|
+
const all = [];
|
|
454
|
+
const visited = new Set();
|
|
455
|
+
const queue = [tip];
|
|
456
|
+
let head = 0;
|
|
457
|
+
while (head < queue.length) {
|
|
458
|
+
const sha = /** @type {string} */ (queue[head++]);
|
|
459
|
+
if (visited.has(sha)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
visited.add(sha);
|
|
463
|
+
const commit = this._commits.get(sha);
|
|
464
|
+
if (!commit) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
all.push({ sha, ...commit });
|
|
468
|
+
for (const p of commit.parents) {
|
|
469
|
+
if (!visited.has(p)) {
|
|
470
|
+
queue.push(p);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Sort by date descending (reverse chronological), matching git log
|
|
475
|
+
all.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
476
|
+
return all.slice(0, limit);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Formats a commit record in GitLogParser's expected format:
|
|
481
|
+
* `<SHA>\n<author>\n<date>\n<parents>\n<message>`
|
|
482
|
+
* @param {{sha: string, message: string, author: string, date: string, parents: string[]}} c
|
|
483
|
+
* @returns {string}
|
|
484
|
+
*/
|
|
485
|
+
_formatCommitRecord(c) {
|
|
486
|
+
return `${c.sha}\n${c.author}\n${c.date}\n${c.parents.join(' ')}\n${c.message}`;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -13,19 +13,32 @@ import {
|
|
|
13
13
|
* @extends CryptoPort
|
|
14
14
|
*/
|
|
15
15
|
export default class NodeCryptoAdapter extends CryptoPort {
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} algorithm
|
|
18
|
+
* @param {string|Buffer|Uint8Array} data
|
|
19
|
+
* @returns {Promise<string>}
|
|
20
|
+
*/
|
|
17
21
|
// eslint-disable-next-line @typescript-eslint/require-await -- async ensures sync throws become rejected promises
|
|
18
22
|
async hash(algorithm, data) {
|
|
19
23
|
return createHash(algorithm).update(data).digest('hex');
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} algorithm
|
|
28
|
+
* @param {string|Buffer|Uint8Array} key
|
|
29
|
+
* @param {string|Buffer|Uint8Array} data
|
|
30
|
+
* @returns {Promise<Buffer>}
|
|
31
|
+
*/
|
|
23
32
|
// eslint-disable-next-line @typescript-eslint/require-await -- async ensures sync throws become rejected promises
|
|
24
33
|
async hmac(algorithm, key, data) {
|
|
25
34
|
return createHmac(algorithm, key).update(data).digest();
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* @param {Buffer|Uint8Array} a
|
|
39
|
+
* @param {Buffer|Uint8Array} b
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
29
42
|
timingSafeEqual(a, b) {
|
|
30
43
|
return nodeTimingSafeEqual(a, b);
|
|
31
44
|
}
|
|
@@ -7,6 +7,9 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
|
7
7
|
/**
|
|
8
8
|
* Collects the request body and dispatches to the handler, returning
|
|
9
9
|
* a 500 response if the handler throws.
|
|
10
|
+
* @param {import('node:http').IncomingMessage} req
|
|
11
|
+
* @param {import('node:http').ServerResponse} res
|
|
12
|
+
* @param {{ handler: Function, logger: { error: Function } }} options
|
|
10
13
|
*/
|
|
11
14
|
async function dispatch(req, res, { handler, logger }) {
|
|
12
15
|
try {
|
|
@@ -60,33 +63,52 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
60
63
|
this._logger = logger || noopLogger;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* @param {Function} requestHandler
|
|
68
|
+
* @returns {{ listen: Function, close: Function, address: Function }}
|
|
69
|
+
*/
|
|
64
70
|
createServer(requestHandler) {
|
|
65
71
|
const logger = this._logger;
|
|
66
72
|
const server = createServer((req, res) => {
|
|
67
|
-
dispatch(req, res, { handler: requestHandler, logger }).catch(
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
dispatch(req, res, { handler: requestHandler, logger }).catch(
|
|
74
|
+
/** @param {*} err */ (err) => {
|
|
75
|
+
logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
|
|
76
|
+
});
|
|
70
77
|
});
|
|
71
78
|
|
|
72
79
|
return {
|
|
80
|
+
/**
|
|
81
|
+
* @param {number} port
|
|
82
|
+
* @param {string|Function} [host]
|
|
83
|
+
* @param {Function} [callback]
|
|
84
|
+
*/
|
|
73
85
|
listen(port, host, callback) {
|
|
74
86
|
const cb = typeof host === 'function' ? host : callback;
|
|
75
87
|
const bindHost = typeof host === 'string' ? host : undefined;
|
|
88
|
+
/** @param {*} err */
|
|
76
89
|
const onError = (err) => {
|
|
77
90
|
if (cb) {
|
|
78
91
|
cb(err);
|
|
79
92
|
}
|
|
80
93
|
};
|
|
81
94
|
server.once('error', onError);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
if (bindHost !== undefined) {
|
|
96
|
+
server.listen(port, bindHost, () => {
|
|
97
|
+
server.removeListener('error', onError);
|
|
98
|
+
if (cb) {
|
|
99
|
+
cb(null);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
server.listen(port, () => {
|
|
104
|
+
server.removeListener('error', onError);
|
|
105
|
+
if (cb) {
|
|
106
|
+
cb(null);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
89
110
|
},
|
|
111
|
+
/** @param {((err?: Error) => void)} [callback] */
|
|
90
112
|
close(callback) {
|
|
91
113
|
server.close(callback);
|
|
92
114
|
},
|
|
@@ -2,9 +2,9 @@ import CryptoPort from '../../ports/CryptoPort.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Map of common algorithm names to Web Crypto API algorithm identifiers.
|
|
5
|
-
* @const {
|
|
5
|
+
* @const {Record<string, string>}
|
|
6
6
|
*/
|
|
7
|
-
const ALGO_MAP = {
|
|
7
|
+
const ALGO_MAP = /** @type {Record<string, string>} */ ({
|
|
8
8
|
'sha-1': 'SHA-1',
|
|
9
9
|
'sha1': 'SHA-1',
|
|
10
10
|
'sha-256': 'SHA-256',
|
|
@@ -13,7 +13,7 @@ const ALGO_MAP = {
|
|
|
13
13
|
'sha384': 'SHA-384',
|
|
14
14
|
'sha-512': 'SHA-512',
|
|
15
15
|
'sha512': 'SHA-512',
|
|
16
|
-
};
|
|
16
|
+
});
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Converts a common algorithm name to the Web Crypto API identifier.
|
|
@@ -38,8 +38,9 @@ function toWebCryptoAlgo(algorithm) {
|
|
|
38
38
|
function toUint8Array(data) {
|
|
39
39
|
if (data instanceof Uint8Array) { return data; }
|
|
40
40
|
if (typeof data === 'string') { return new TextEncoder().encode(data); }
|
|
41
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
|
|
42
|
-
|
|
41
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(/** @type {*} */ (data))) { // TODO(ts-cleanup): narrow port type
|
|
42
|
+
const buf = /** @type {Buffer} */ (/** @type {*} */ (data)); // TODO(ts-cleanup): narrow port type
|
|
43
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
43
44
|
}
|
|
44
45
|
throw new Error('WebCryptoAdapter: data must be string, Buffer, or Uint8Array');
|
|
45
46
|
}
|
|
@@ -59,7 +60,7 @@ function bufToHex(buf) {
|
|
|
59
60
|
* Web Crypto API adapter implementing CryptoPort.
|
|
60
61
|
*
|
|
61
62
|
* Uses the standard Web Crypto API (globalThis.crypto.subtle) which is
|
|
62
|
-
* available in browsers, Deno, Bun, and Node.js
|
|
63
|
+
* available in browsers, Deno, Bun, and Node.js 22+.
|
|
63
64
|
*
|
|
64
65
|
* All hash and HMAC operations are async because the Web Crypto API
|
|
65
66
|
* is inherently promise-based.
|
|
@@ -77,26 +78,35 @@ export default class WebCryptoAdapter extends CryptoPort {
|
|
|
77
78
|
this._subtle = subtle || globalThis.crypto.subtle;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} algorithm
|
|
83
|
+
* @param {string|Buffer|Uint8Array} data
|
|
84
|
+
* @returns {Promise<string>}
|
|
85
|
+
*/
|
|
81
86
|
async hash(algorithm, data) {
|
|
82
87
|
const digest = await this._subtle.digest(
|
|
83
88
|
toWebCryptoAlgo(algorithm),
|
|
84
|
-
toUint8Array(data),
|
|
89
|
+
/** @type {BufferSource} */ (toUint8Array(data)),
|
|
85
90
|
);
|
|
86
91
|
return bufToHex(digest);
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} algorithm
|
|
96
|
+
* @param {string|Buffer|Uint8Array} key
|
|
97
|
+
* @param {string|Buffer|Uint8Array} data
|
|
98
|
+
* @returns {Promise<Uint8Array>}
|
|
99
|
+
*/
|
|
90
100
|
async hmac(algorithm, key, data) {
|
|
91
101
|
const keyBytes = toUint8Array(key);
|
|
92
102
|
const cryptoKey = await this._subtle.importKey(
|
|
93
103
|
'raw',
|
|
94
|
-
keyBytes,
|
|
104
|
+
/** @type {BufferSource} */ (keyBytes),
|
|
95
105
|
{ name: 'HMAC', hash: toWebCryptoAlgo(algorithm) },
|
|
96
106
|
false,
|
|
97
107
|
['sign'],
|
|
98
108
|
);
|
|
99
|
-
const signature = await this._subtle.sign('HMAC', cryptoKey, toUint8Array(data));
|
|
109
|
+
const signature = await this._subtle.sign('HMAC', cryptoKey, /** @type {BufferSource} */ (toUint8Array(data)));
|
|
100
110
|
return new Uint8Array(signature);
|
|
101
111
|
}
|
|
102
112
|
|