@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.
Files changed (108) hide show
  1. package/README.md +6 -3
  2. package/SECURITY.md +89 -1
  3. package/bin/warp-graph.js +574 -208
  4. package/index.d.ts +55 -0
  5. package/index.js +4 -0
  6. package/package.json +8 -4
  7. package/src/domain/WarpGraph.js +334 -161
  8. package/src/domain/crdt/LWW.js +1 -1
  9. package/src/domain/crdt/ORSet.js +10 -6
  10. package/src/domain/crdt/VersionVector.js +5 -1
  11. package/src/domain/errors/EmptyMessageError.js +2 -4
  12. package/src/domain/errors/ForkError.js +4 -0
  13. package/src/domain/errors/IndexError.js +4 -0
  14. package/src/domain/errors/OperationAbortedError.js +4 -0
  15. package/src/domain/errors/QueryError.js +4 -0
  16. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  17. package/src/domain/errors/ShardCorruptionError.js +2 -6
  18. package/src/domain/errors/ShardLoadError.js +2 -6
  19. package/src/domain/errors/ShardValidationError.js +2 -7
  20. package/src/domain/errors/StorageError.js +2 -6
  21. package/src/domain/errors/SyncError.js +4 -0
  22. package/src/domain/errors/TraversalError.js +4 -0
  23. package/src/domain/errors/WarpError.js +2 -4
  24. package/src/domain/errors/WormholeError.js +4 -0
  25. package/src/domain/services/AnchorMessageCodec.js +1 -4
  26. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  27. package/src/domain/services/BitmapIndexReader.js +27 -21
  28. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  29. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  30. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  31. package/src/domain/services/CheckpointService.js +18 -18
  32. package/src/domain/services/CommitDagTraversalService.js +13 -1
  33. package/src/domain/services/DagPathFinding.js +40 -18
  34. package/src/domain/services/DagTopology.js +7 -6
  35. package/src/domain/services/DagTraversal.js +5 -3
  36. package/src/domain/services/Frontier.js +7 -6
  37. package/src/domain/services/HealthCheckService.js +15 -14
  38. package/src/domain/services/HookInstaller.js +64 -13
  39. package/src/domain/services/HttpSyncServer.js +88 -19
  40. package/src/domain/services/IndexRebuildService.js +12 -12
  41. package/src/domain/services/IndexStalenessChecker.js +13 -6
  42. package/src/domain/services/JoinReducer.js +28 -27
  43. package/src/domain/services/LogicalTraversal.js +7 -6
  44. package/src/domain/services/MessageCodecInternal.js +2 -0
  45. package/src/domain/services/ObserverView.js +6 -6
  46. package/src/domain/services/PatchBuilderV2.js +9 -9
  47. package/src/domain/services/PatchMessageCodec.js +1 -7
  48. package/src/domain/services/ProvenanceIndex.js +6 -8
  49. package/src/domain/services/ProvenancePayload.js +1 -2
  50. package/src/domain/services/QueryBuilder.js +29 -23
  51. package/src/domain/services/StateDiff.js +7 -7
  52. package/src/domain/services/StateSerializerV5.js +8 -6
  53. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  54. package/src/domain/services/SyncAuthService.js +396 -0
  55. package/src/domain/services/SyncProtocol.js +23 -26
  56. package/src/domain/services/TemporalQuery.js +4 -3
  57. package/src/domain/services/TranslationCost.js +4 -4
  58. package/src/domain/services/WormholeService.js +19 -15
  59. package/src/domain/types/TickReceipt.js +10 -6
  60. package/src/domain/types/WarpTypesV2.js +2 -3
  61. package/src/domain/utils/CachedValue.js +1 -1
  62. package/src/domain/utils/LRUCache.js +3 -3
  63. package/src/domain/utils/MinHeap.js +2 -2
  64. package/src/domain/utils/RefLayout.js +19 -0
  65. package/src/domain/utils/WriterId.js +2 -2
  66. package/src/domain/utils/defaultCodec.js +9 -2
  67. package/src/domain/utils/defaultCrypto.js +36 -0
  68. package/src/domain/utils/roaring.js +5 -5
  69. package/src/domain/utils/seekCacheKey.js +32 -0
  70. package/src/domain/warp/PatchSession.js +3 -3
  71. package/src/domain/warp/Writer.js +2 -2
  72. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  73. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  74. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  75. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  76. package/src/infrastructure/adapters/GitGraphAdapter.js +25 -83
  77. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
  78. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  79. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  80. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  81. package/src/infrastructure/adapters/adapterValidation.js +90 -0
  82. package/src/infrastructure/codecs/CborCodec.js +16 -8
  83. package/src/ports/BlobPort.js +2 -2
  84. package/src/ports/CodecPort.js +2 -2
  85. package/src/ports/CommitPort.js +8 -21
  86. package/src/ports/ConfigPort.js +3 -3
  87. package/src/ports/CryptoPort.js +7 -7
  88. package/src/ports/GraphPersistencePort.js +12 -14
  89. package/src/ports/HttpServerPort.js +1 -5
  90. package/src/ports/IndexStoragePort.js +1 -0
  91. package/src/ports/LoggerPort.js +9 -9
  92. package/src/ports/RefPort.js +5 -5
  93. package/src/ports/SeekCachePort.js +73 -0
  94. package/src/ports/TreePort.js +3 -3
  95. package/src/visualization/layouts/converters.js +14 -7
  96. package/src/visualization/layouts/elkAdapter.js +17 -4
  97. package/src/visualization/layouts/elkLayout.js +23 -7
  98. package/src/visualization/layouts/index.js +3 -3
  99. package/src/visualization/renderers/ascii/check.js +30 -17
  100. package/src/visualization/renderers/ascii/graph.js +92 -1
  101. package/src/visualization/renderers/ascii/history.js +28 -26
  102. package/src/visualization/renderers/ascii/info.js +9 -7
  103. package/src/visualization/renderers/ascii/materialize.js +20 -16
  104. package/src/visualization/renderers/ascii/opSummary.js +15 -7
  105. package/src/visualization/renderers/ascii/path.js +1 -1
  106. package/src/visualization/renderers/ascii/seek.js +187 -23
  107. package/src/visualization/renderers/ascii/table.js +1 -1
  108. 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
- /** @inheritdoc */
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
- /** @inheritdoc */
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
- /** @inheritdoc */
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
- /** @inheritdoc */
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((err) => {
68
- logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
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
- const args = bindHost !== undefined ? [port, bindHost] : [port];
83
- server.listen(...args, () => {
84
- server.removeListener('error', onError);
85
- if (cb) {
86
- cb(null);
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 {Object<string, string>}
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
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
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 20+.
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
- /** @inheritdoc */
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
- /** @inheritdoc */
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