@git-stunts/git-warp 11.2.1 → 11.5.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 (114) hide show
  1. package/README.md +24 -1
  2. package/bin/cli/commands/check.js +2 -2
  3. package/bin/cli/commands/doctor/checks.js +12 -12
  4. package/bin/cli/commands/doctor/index.js +2 -2
  5. package/bin/cli/commands/doctor/types.js +1 -1
  6. package/bin/cli/commands/history.js +12 -5
  7. package/bin/cli/commands/install-hooks.js +5 -5
  8. package/bin/cli/commands/materialize.js +2 -2
  9. package/bin/cli/commands/patch.js +142 -0
  10. package/bin/cli/commands/path.js +4 -4
  11. package/bin/cli/commands/query.js +54 -13
  12. package/bin/cli/commands/registry.js +4 -0
  13. package/bin/cli/commands/seek.js +17 -11
  14. package/bin/cli/commands/tree.js +230 -0
  15. package/bin/cli/commands/trust.js +3 -3
  16. package/bin/cli/commands/verify-audit.js +8 -7
  17. package/bin/cli/commands/view.js +6 -5
  18. package/bin/cli/infrastructure.js +26 -12
  19. package/bin/cli/shared.js +2 -2
  20. package/bin/cli/types.js +19 -8
  21. package/bin/presenters/index.js +35 -9
  22. package/bin/presenters/json.js +14 -12
  23. package/bin/presenters/text.js +155 -33
  24. package/index.d.ts +118 -22
  25. package/index.js +2 -0
  26. package/package.json +5 -3
  27. package/src/domain/WarpGraph.js +4 -1
  28. package/src/domain/crdt/ORSet.js +8 -8
  29. package/src/domain/errors/EmptyMessageError.js +2 -2
  30. package/src/domain/errors/ForkError.js +1 -1
  31. package/src/domain/errors/IndexError.js +1 -1
  32. package/src/domain/errors/OperationAbortedError.js +1 -1
  33. package/src/domain/errors/QueryError.js +1 -1
  34. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  35. package/src/domain/errors/ShardCorruptionError.js +2 -2
  36. package/src/domain/errors/ShardLoadError.js +2 -2
  37. package/src/domain/errors/ShardValidationError.js +4 -4
  38. package/src/domain/errors/StorageError.js +2 -2
  39. package/src/domain/errors/SyncError.js +1 -1
  40. package/src/domain/errors/TraversalError.js +1 -1
  41. package/src/domain/errors/TrustError.js +1 -1
  42. package/src/domain/errors/WarpError.js +2 -2
  43. package/src/domain/errors/WormholeError.js +1 -1
  44. package/src/domain/services/AuditReceiptService.js +6 -6
  45. package/src/domain/services/AuditVerifierService.js +52 -38
  46. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  47. package/src/domain/services/BitmapIndexReader.js +28 -19
  48. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  49. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  50. package/src/domain/services/CheckpointService.js +22 -3
  51. package/src/domain/services/CommitDagTraversalService.js +13 -13
  52. package/src/domain/services/DagPathFinding.js +7 -7
  53. package/src/domain/services/DagTopology.js +1 -1
  54. package/src/domain/services/DagTraversal.js +1 -1
  55. package/src/domain/services/HealthCheckService.js +1 -1
  56. package/src/domain/services/HookInstaller.js +1 -1
  57. package/src/domain/services/HttpSyncServer.js +92 -41
  58. package/src/domain/services/IndexRebuildService.js +7 -7
  59. package/src/domain/services/IndexStalenessChecker.js +4 -3
  60. package/src/domain/services/JoinReducer.js +26 -11
  61. package/src/domain/services/KeyCodec.js +7 -0
  62. package/src/domain/services/LogicalTraversal.js +1 -1
  63. package/src/domain/services/MessageCodecInternal.js +1 -1
  64. package/src/domain/services/MigrationService.js +1 -1
  65. package/src/domain/services/ObserverView.js +8 -8
  66. package/src/domain/services/PatchBuilderV2.js +96 -30
  67. package/src/domain/services/ProvenanceIndex.js +1 -1
  68. package/src/domain/services/ProvenancePayload.js +1 -1
  69. package/src/domain/services/QueryBuilder.js +3 -3
  70. package/src/domain/services/StateDiff.js +14 -11
  71. package/src/domain/services/StateSerializerV5.js +2 -2
  72. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  73. package/src/domain/services/SyncAuthService.js +3 -2
  74. package/src/domain/services/SyncProtocol.js +25 -11
  75. package/src/domain/services/TemporalQuery.js +9 -6
  76. package/src/domain/services/TranslationCost.js +7 -5
  77. package/src/domain/services/WormholeService.js +16 -7
  78. package/src/domain/trust/TrustCanonical.js +3 -3
  79. package/src/domain/trust/TrustEvaluator.js +18 -3
  80. package/src/domain/trust/TrustRecordService.js +30 -23
  81. package/src/domain/trust/TrustStateBuilder.js +21 -8
  82. package/src/domain/trust/canonical.js +6 -6
  83. package/src/domain/types/TickReceipt.js +1 -1
  84. package/src/domain/types/WarpErrors.js +45 -0
  85. package/src/domain/types/WarpOptions.js +29 -0
  86. package/src/domain/types/WarpPersistence.js +41 -0
  87. package/src/domain/types/WarpTypes.js +2 -2
  88. package/src/domain/types/WarpTypesV2.js +2 -2
  89. package/src/domain/utils/MinHeap.js +6 -5
  90. package/src/domain/utils/canonicalStringify.js +5 -4
  91. package/src/domain/utils/roaring.js +31 -5
  92. package/src/domain/warp/PatchSession.js +40 -18
  93. package/src/domain/warp/_wiredMethods.d.ts +199 -45
  94. package/src/domain/warp/checkpoint.methods.js +5 -1
  95. package/src/domain/warp/fork.methods.js +2 -2
  96. package/src/domain/warp/materialize.methods.js +55 -5
  97. package/src/domain/warp/materializeAdvanced.methods.js +15 -4
  98. package/src/domain/warp/patch.methods.js +54 -29
  99. package/src/domain/warp/provenance.methods.js +5 -3
  100. package/src/domain/warp/query.methods.js +89 -6
  101. package/src/domain/warp/sync.methods.js +16 -11
  102. package/src/globals.d.ts +64 -0
  103. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  104. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  105. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  106. package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
  107. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  108. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  109. package/src/visualization/layouts/converters.js +2 -2
  110. package/src/visualization/layouts/elkAdapter.js +1 -1
  111. package/src/visualization/layouts/elkLayout.js +10 -7
  112. package/src/visualization/layouts/index.js +1 -1
  113. package/src/visualization/renderers/ascii/seek.js +16 -6
  114. package/src/visualization/renderers/svg/index.js +1 -1
@@ -25,7 +25,7 @@ import { createORSet, orsetAdd, orsetCompact } from '../crdt/ORSet.js';
25
25
  import { createDot } from '../crdt/Dot.js';
26
26
  import { createVersionVector } from '../crdt/VersionVector.js';
27
27
  import { cloneStateV5, reduceV5 } from './JoinReducer.js';
28
- import { encodeEdgeKey, encodePropKey } from './KeyCodec.js';
28
+ import { encodeEdgeKey, encodePropKey, CONTENT_PROPERTY_KEY, decodePropKey, isEdgePropKey, decodeEdgePropKey } from './KeyCodec.js';
29
29
  import { ProvenanceIndex } from './ProvenanceIndex.js';
30
30
 
31
31
  // ============================================================================
@@ -132,6 +132,20 @@ export async function createV5({
132
132
  provenanceIndexBlobOid = await persistence.writeBlob(/** @type {Buffer} */ (provenanceIndexBuffer));
133
133
  }
134
134
 
135
+ // 6c. Collect content blob OIDs from state properties for GC anchoring.
136
+ // If patch commits are ever pruned, content blobs remain reachable via
137
+ // the checkpoint tree. Without this, git gc would nuke content blobs
138
+ // whose only anchor was the (now-pruned) patch commit tree.
139
+ const contentOids = new Set();
140
+ for (const [propKey, register] of checkpointState.prop) {
141
+ const { propKey: decodedKey } = isEdgePropKey(propKey)
142
+ ? decodeEdgePropKey(propKey)
143
+ : decodePropKey(propKey);
144
+ if (decodedKey === CONTENT_PROPERTY_KEY && typeof register.value === 'string') {
145
+ contentOids.add(register.value);
146
+ }
147
+ }
148
+
135
149
  // 7. Create tree with sorted entries
136
150
  const treeEntries = [
137
151
  `100644 blob ${appliedVVBlobOid}\tappliedVV.cbor`,
@@ -145,6 +159,11 @@ export async function createV5({
145
159
  treeEntries.push(`100644 blob ${provenanceIndexBlobOid}\tprovenanceIndex.cbor`);
146
160
  }
147
161
 
162
+ // Add content blob anchors
163
+ for (const oid of contentOids) {
164
+ treeEntries.push(`100644 blob ${oid}\t_content_${oid}`);
165
+ }
166
+
148
167
  // Sort entries by filename for deterministic tree (git requires sorted entries by path)
149
168
  treeEntries.sort((a, b) => {
150
169
  const filenameA = a.split('\t')[1];
@@ -196,7 +215,7 @@ export async function createV5({
196
215
  * @returns {Promise<{state: import('./JoinReducer.js').WarpStateV5, frontier: import('./Frontier.js').Frontier, stateHash: string, schema: number, appliedVV: Map<string, number>|null, provenanceIndex?: import('./ProvenanceIndex.js').ProvenanceIndex}>} The loaded checkpoint data
197
216
  * @throws {Error} If checkpoint is schema:1 (migration required)
198
217
  */
199
- export async function loadCheckpoint(persistence, checkpointSha, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
218
+ export async function loadCheckpoint(persistence, checkpointSha, { codec } = {}) {
200
219
  // 1. Read commit message and decode
201
220
  const message = await persistence.showNode(checkpointSha);
202
221
  const decoded = /** @type {{ schema: number, stateHash: string, indexOid: string }} */ (decodeCheckpointMessage(message));
@@ -327,7 +346,7 @@ export async function materializeIncremental({
327
346
  * @param {Object} visibleProjection - The checkpoint's visible projection
328
347
  * @param {string[]} visibleProjection.nodes - Visible node IDs
329
348
  * @param {Array<{from: string, to: string, label: string}>} visibleProjection.edges - Visible edges
330
- * @param {Array<{node: string, key: string, value: *}>} visibleProjection.props - Visible properties
349
+ * @param {Array<{node: string, key: string, value: unknown}>} visibleProjection.props - Visible properties
331
350
  * @returns {import('./JoinReducer.js').WarpStateV5} Reconstructed WarpStateV5
332
351
  * @public
333
352
  */
@@ -39,7 +39,7 @@ export default class CommitDagTraversalService {
39
39
  * @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
40
40
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
41
41
  */
42
- constructor({ indexReader, logger = nullLogger } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
42
+ constructor({ indexReader, logger = nullLogger }) {
43
43
  if (!indexReader) {
44
44
  throw new Error('CommitDagTraversalService requires an indexReader');
45
45
  }
@@ -56,7 +56,7 @@ export default class CommitDagTraversalService {
56
56
 
57
57
  /**
58
58
  * Breadth-first traversal from a starting node.
59
- * @param {*} options
59
+ * @param {Parameters<import('./DagTraversal.js').default['bfs']>[0]} options
60
60
  * @see DagTraversal#bfs
61
61
  */
62
62
  bfs(options) {
@@ -65,7 +65,7 @@ export default class CommitDagTraversalService {
65
65
 
66
66
  /**
67
67
  * Depth-first pre-order traversal from a starting node.
68
- * @param {*} options
68
+ * @param {Parameters<import('./DagTraversal.js').default['dfs']>[0]} options
69
69
  * @see DagTraversal#dfs
70
70
  */
71
71
  dfs(options) {
@@ -74,7 +74,7 @@ export default class CommitDagTraversalService {
74
74
 
75
75
  /**
76
76
  * Yields all ancestors of a node.
77
- * @param {*} options
77
+ * @param {Parameters<import('./DagTraversal.js').default['ancestors']>[0]} options
78
78
  * @see DagTraversal#ancestors
79
79
  */
80
80
  ancestors(options) {
@@ -83,7 +83,7 @@ export default class CommitDagTraversalService {
83
83
 
84
84
  /**
85
85
  * Yields all descendants of a node.
86
- * @param {*} options
86
+ * @param {Parameters<import('./DagTraversal.js').default['descendants']>[0]} options
87
87
  * @see DagTraversal#descendants
88
88
  */
89
89
  descendants(options) {
@@ -92,7 +92,7 @@ export default class CommitDagTraversalService {
92
92
 
93
93
  /**
94
94
  * Checks if there is any path from one node to another.
95
- * @param {*} options
95
+ * @param {Parameters<import('./DagTraversal.js').default['isReachable']>[0]} options
96
96
  * @see DagTraversal#isReachable
97
97
  */
98
98
  isReachable(options) {
@@ -103,7 +103,7 @@ export default class CommitDagTraversalService {
103
103
 
104
104
  /**
105
105
  * Finds ANY path between two nodes using BFS.
106
- * @param {*} options
106
+ * @param {Parameters<import('./DagPathFinding.js').default['findPath']>[0]} options
107
107
  * @see DagPathFinding#findPath
108
108
  */
109
109
  findPath(options) {
@@ -112,7 +112,7 @@ export default class CommitDagTraversalService {
112
112
 
113
113
  /**
114
114
  * Finds the shortest path using bidirectional BFS.
115
- * @param {*} options
115
+ * @param {Parameters<import('./DagPathFinding.js').default['shortestPath']>[0]} options
116
116
  * @see DagPathFinding#shortestPath
117
117
  */
118
118
  shortestPath(options) {
@@ -121,7 +121,7 @@ export default class CommitDagTraversalService {
121
121
 
122
122
  /**
123
123
  * Finds shortest path using Dijkstra's algorithm.
124
- * @param {*} options
124
+ * @param {Parameters<import('./DagPathFinding.js').default['weightedShortestPath']>[0]} options
125
125
  * @see DagPathFinding#weightedShortestPath
126
126
  */
127
127
  weightedShortestPath(options) {
@@ -130,7 +130,7 @@ export default class CommitDagTraversalService {
130
130
 
131
131
  /**
132
132
  * Finds shortest path using A* with heuristic guidance.
133
- * @param {*} options
133
+ * @param {Parameters<import('./DagPathFinding.js').default['aStarSearch']>[0]} options
134
134
  * @see DagPathFinding#aStarSearch
135
135
  */
136
136
  aStarSearch(options) {
@@ -139,7 +139,7 @@ export default class CommitDagTraversalService {
139
139
 
140
140
  /**
141
141
  * Bi-directional A* search.
142
- * @param {*} options
142
+ * @param {Parameters<import('./DagPathFinding.js').default['bidirectionalAStar']>[0]} options
143
143
  * @see DagPathFinding#bidirectionalAStar
144
144
  */
145
145
  bidirectionalAStar(options) {
@@ -150,7 +150,7 @@ export default class CommitDagTraversalService {
150
150
 
151
151
  /**
152
152
  * Finds common ancestors of multiple nodes.
153
- * @param {*} options
153
+ * @param {Parameters<import('./DagTopology.js').default['commonAncestors']>[0]} options
154
154
  * @see DagTopology#commonAncestors
155
155
  */
156
156
  commonAncestors(options) {
@@ -159,7 +159,7 @@ export default class CommitDagTraversalService {
159
159
 
160
160
  /**
161
161
  * Yields nodes in topological order using Kahn's algorithm.
162
- * @param {*} options
162
+ * @param {Parameters<import('./DagTopology.js').default['topologicalSort']>[0]} options
163
163
  * @see DagTopology#topologicalSort
164
164
  */
165
165
  topologicalSort(options) {
@@ -41,7 +41,7 @@ export default class DagPathFinding {
41
41
  * @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
42
42
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
43
43
  */
44
- constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
44
+ constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {{ indexReader: import('./BitmapIndexReader.js').default }} */ ({})) {
45
45
  if (!indexReader) {
46
46
  throw new Error('DagPathFinding requires an indexReader');
47
47
  }
@@ -228,7 +228,7 @@ export default class DagPathFinding {
228
228
  checkAborted(signal, 'weightedShortestPath');
229
229
  }
230
230
 
231
- const current = pq.extractMin();
231
+ const current = /** @type {string} */ (pq.extractMin());
232
232
 
233
233
  if (visited.has(current)) {
234
234
  continue;
@@ -314,7 +314,7 @@ export default class DagPathFinding {
314
314
  checkAborted(signal, 'aStarSearch');
315
315
  }
316
316
 
317
- const current = pq.extractMin();
317
+ const current = /** @type {string} */ (pq.extractMin());
318
318
 
319
319
  if (visited.has(current)) {
320
320
  continue;
@@ -463,7 +463,7 @@ export default class DagPathFinding {
463
463
  * Expands the forward frontier by one node in bidirectional A*.
464
464
  *
465
465
  * @param {Object} state - Forward expansion state
466
- * @param {import('../utils/MinHeap.js').default} state.fwdHeap
466
+ * @param {import('../utils/MinHeap.js').default<string>} state.fwdHeap
467
467
  * @param {Set<string>} state.fwdVisited
468
468
  * @param {Map<string, number>} state.fwdGScore
469
469
  * @param {Map<string, string>} state.fwdPrevious
@@ -483,7 +483,7 @@ export default class DagPathFinding {
483
483
  weightProvider, forwardHeuristic, to,
484
484
  mu: inputMu, meetingPoint: inputMeeting,
485
485
  }) {
486
- const current = fwdHeap.extractMin();
486
+ const current = /** @type {string} */ (fwdHeap.extractMin());
487
487
  let explored = 0;
488
488
  let bestMu = inputMu;
489
489
  let bestMeeting = inputMeeting;
@@ -536,7 +536,7 @@ export default class DagPathFinding {
536
536
  * Expands the backward frontier by one node in bidirectional A*.
537
537
  *
538
538
  * @param {Object} state - Backward expansion state
539
- * @param {import('../utils/MinHeap.js').default} state.bwdHeap
539
+ * @param {import('../utils/MinHeap.js').default<string>} state.bwdHeap
540
540
  * @param {Set<string>} state.bwdVisited
541
541
  * @param {Map<string, number>} state.bwdGScore
542
542
  * @param {Map<string, string>} state.bwdNext
@@ -556,7 +556,7 @@ export default class DagPathFinding {
556
556
  weightProvider, backwardHeuristic, from,
557
557
  mu: inputMu, meetingPoint: inputMeeting,
558
558
  }) {
559
- const current = bwdHeap.extractMin();
559
+ const current = /** @type {string} */ (bwdHeap.extractMin());
560
560
  let explored = 0;
561
561
  let bestMu = inputMu;
562
562
  let bestMeeting = inputMeeting;
@@ -37,7 +37,7 @@ export default class DagTopology {
37
37
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
38
38
  * @param {import('./DagTraversal.js').default} [options.traversal] - Traversal service for ancestor enumeration
39
39
  */
40
- constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default, traversal?: import('./DagTraversal.js').default }} */ { indexReader, logger = nullLogger, traversal } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
40
+ constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default, traversal?: import('./DagTraversal.js').default }} */ { indexReader, logger = nullLogger, traversal } = /** @type {{ indexReader: import('./BitmapIndexReader.js').default }} */ ({})) {
41
41
  if (!indexReader) {
42
42
  throw new Error('DagTopology requires an indexReader');
43
43
  }
@@ -43,7 +43,7 @@ export default class DagTraversal {
43
43
  * @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
44
44
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
45
45
  */
46
- constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
46
+ constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {{ indexReader: import('./BitmapIndexReader.js').default }} */ ({})) {
47
47
  if (!indexReader) {
48
48
  throw new Error('DagTraversal requires an indexReader');
49
49
  }
@@ -191,7 +191,7 @@ export default class HealthCheckService {
191
191
  } catch (err) {
192
192
  this._logger.warn('Repository ping failed', {
193
193
  operation: 'checkRepository',
194
- error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
194
+ error: err instanceof Error ? err.message : String(err),
195
195
  });
196
196
  return {
197
197
  status: /** @type {'healthy'|'unhealthy'} */ (HealthStatus.UNHEALTHY),
@@ -80,7 +80,7 @@ export class HookInstaller {
80
80
  * @param {string} deps.templateDir - Directory containing hook templates
81
81
  * @param {PathUtils} deps.path - Path utilities (join and resolve)
82
82
  */
83
- constructor({ fs, execGitConfig, version, templateDir, path } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
83
+ constructor({ fs, execGitConfig, version, templateDir, path }) {
84
84
  /** @type {FsAdapter} */
85
85
  this._fs = fs;
86
86
  /** @type {(repoPath: string, key: string) => string|null} */
@@ -8,15 +8,56 @@
8
8
  * @module domain/services/HttpSyncServer
9
9
  */
10
10
 
11
+ import { z } from 'zod';
11
12
  import SyncAuthService from './SyncAuthService.js';
12
13
 
13
14
  const DEFAULT_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
15
+ const MAX_REQUEST_BYTES_CEILING = 128 * 1024 * 1024; // 134217728
16
+
17
+ /**
18
+ * Zod schema for HttpSyncServer constructor options.
19
+ * @private
20
+ */
21
+ const authSchema = z.object({
22
+ mode: z.enum(['enforce', 'log-only']).default('enforce'),
23
+ keys: z.record(z.string()).refine(
24
+ (obj) => Object.keys(obj).length > 0,
25
+ 'auth.keys must not be empty',
26
+ ),
27
+ crypto: /** @type {z.ZodType<import('../../ports/CryptoPort.js').default>} */ (z.custom((v) => v === undefined || (typeof v === 'object' && v !== null))).optional(),
28
+ logger: /** @type {z.ZodType<import('../../ports/LoggerPort.js').default>} */ (z.custom((v) => v === undefined || (typeof v === 'object' && v !== null))).optional(),
29
+ wallClockMs: /** @type {z.ZodType<() => number>} */ (z.custom((v) => v === undefined || typeof v === 'function')).optional(),
30
+ }).strict();
31
+
32
+ const optionsSchema = z.object({
33
+ httpPort: /** @type {z.ZodType<import('../../ports/HttpServerPort.js').default>} */ (z.custom(
34
+ (v) => v !== null && v !== undefined && typeof v === 'object',
35
+ 'httpPort must be a non-null object',
36
+ )),
37
+ graph: /** @type {z.ZodType<import('../WarpGraph.js').default>} */ (z.custom(
38
+ (v) => v !== null && v !== undefined && typeof v === 'object',
39
+ 'graph must be a non-null object',
40
+ )),
41
+ maxRequestBytes: z.number().int().positive().max(MAX_REQUEST_BYTES_CEILING).default(DEFAULT_MAX_REQUEST_BYTES),
42
+ path: z.string().startsWith('/').default('/sync'),
43
+ host: z.string().min(1).default('127.0.0.1'),
44
+ auth: authSchema.optional(),
45
+ allowedWriters: z.array(z.string()).optional(),
46
+ }).strict().superRefine((data, ctx) => {
47
+ if (data.allowedWriters && data.allowedWriters.length > 0 && !data.auth) {
48
+ ctx.addIssue({
49
+ code: z.ZodIssueCode.custom,
50
+ message: 'allowedWriters requires auth.keys to be configured',
51
+ path: ['allowedWriters'],
52
+ });
53
+ }
54
+ });
14
55
 
15
56
  /**
16
57
  * Recursively sorts object keys for deterministic JSON output.
17
58
  *
18
- * @param {*} value - Any JSON-serializable value
19
- * @returns {*} The canonicalized value with sorted object keys
59
+ * @param {unknown} value - Any JSON-serializable value
60
+ * @returns {unknown} The canonicalized value with sorted object keys
20
61
  * @private
21
62
  */
22
63
  function canonicalizeJson(value) {
@@ -24,10 +65,10 @@ function canonicalizeJson(value) {
24
65
  return value.map(canonicalizeJson);
25
66
  }
26
67
  if (value && typeof value === 'object') {
27
- /** @type {{ [x: string]: * }} */
68
+ /** @type {{ [x: string]: unknown }} */
28
69
  const sorted = {};
29
70
  for (const key of Object.keys(value).sort()) {
30
- sorted[key] = canonicalizeJson(/** @type {{ [x: string]: * }} */ (value)[key]);
71
+ sorted[key] = canonicalizeJson(/** @type {{ [x: string]: unknown }} */ (value)[key]);
31
72
  }
32
73
  return sorted;
33
74
  }
@@ -37,7 +78,7 @@ function canonicalizeJson(value) {
37
78
  /**
38
79
  * Produces a canonical JSON string with sorted keys.
39
80
  *
40
- * @param {*} value - Any JSON-serializable value
81
+ * @param {unknown} value - Any JSON-serializable value
41
82
  * @returns {string} Canonical JSON string
42
83
  * @private
43
84
  */
@@ -64,7 +105,7 @@ function errorResponse(status, message) {
64
105
  /**
65
106
  * Builds a JSON success response with canonical key ordering.
66
107
  *
67
- * @param {*} data - Response payload
108
+ * @param {unknown} data - Response payload
68
109
  * @returns {{ status: number, headers: Object, body: string }}
69
110
  * @private
70
111
  */
@@ -79,7 +120,7 @@ function jsonResponse(data) {
79
120
  /**
80
121
  * Validates that a sync request object has the expected shape.
81
122
  *
82
- * @param {*} parsed - Parsed JSON body
123
+ * @param {unknown} parsed - Parsed JSON body
83
124
  * @returns {boolean} True if valid
84
125
  * @private
85
126
  */
@@ -87,10 +128,11 @@ function isValidSyncRequest(parsed) {
87
128
  if (!parsed || typeof parsed !== 'object') {
88
129
  return false;
89
130
  }
90
- if (parsed.type !== 'sync-request') {
131
+ const rec = /** @type {Record<string, unknown>} */ (parsed);
132
+ if (rec.type !== 'sync-request') {
91
133
  return false;
92
134
  }
93
- if (!parsed.frontier || typeof parsed.frontier !== 'object' || Array.isArray(parsed.frontier)) {
135
+ if (!rec.frontier || typeof rec.frontier !== 'object' || Array.isArray(rec.frontier)) {
94
136
  return false;
95
137
  }
96
138
  return true;
@@ -160,7 +202,7 @@ function checkBodySize(body, maxBytes) {
160
202
  * Parses and validates the request body as a sync request.
161
203
  *
162
204
  * @param {Buffer|undefined} body
163
- * @returns {{ error: { status: number, headers: Object, body: string }, parsed: null } | { error: null, parsed: Object }}
205
+ * @returns {{ error: { status: number, headers: Object, body: string }, parsed: null } | { error: null, parsed: import('./SyncProtocol.js').SyncRequest }}
164
206
  * @private
165
207
  */
166
208
  function parseBody(body) {
@@ -183,19 +225,14 @@ function parseBody(body) {
183
225
  /**
184
226
  * Initializes auth service from config if present.
185
227
  *
186
- * @param {{ keys: Record<string, string>, mode?: 'enforce'|'log-only', crypto?: *, logger?: *, wallClockMs?: () => number }|undefined} auth
228
+ * @param {z.infer<typeof authSchema>} [auth]
187
229
  * @param {string[]} [allowedWriters]
188
230
  * @returns {{ auth: SyncAuthService|null, authMode: string|null }}
189
231
  * @private
190
232
  */
191
233
  function initAuth(auth, allowedWriters) {
192
- if (auth && auth.keys) {
193
- const VALID_MODES = new Set(['enforce', 'log-only']);
194
- const mode = auth.mode || 'enforce';
195
- if (!VALID_MODES.has(mode)) {
196
- throw new Error(`Invalid auth.mode: '${mode}'. Must be 'enforce' or 'log-only'.`);
197
- }
198
- return { auth: new SyncAuthService({ ...auth, allowedWriters }), authMode: mode };
234
+ if (auth) {
235
+ return { auth: new SyncAuthService({ ...auth, allowedWriters }), authMode: auth.mode };
199
236
  }
200
237
  return { auth: null, authMode: null };
201
238
  }
@@ -204,26 +241,35 @@ export default class HttpSyncServer {
204
241
  /**
205
242
  * @param {Object} options
206
243
  * @param {import('../../ports/HttpServerPort.js').default} options.httpPort - HTTP server port abstraction
207
- * @param {{ processSyncRequest: (request: *) => Promise<*> }} options.graph - WarpGraph instance (must expose processSyncRequest)
244
+ * @param {{ processSyncRequest: Function }} options.graph - WarpGraph instance (must expose processSyncRequest)
208
245
  * @param {string} [options.path='/sync'] - URL path to handle sync requests on
209
246
  * @param {string} [options.host='127.0.0.1'] - Host to bind
210
247
  * @param {number} [options.maxRequestBytes=4194304] - Maximum request body size in bytes
211
248
  * @param {{ keys: Record<string, string>, mode?: 'enforce'|'log-only', crypto?: import('../../ports/CryptoPort.js').default, logger?: import('../../ports/LoggerPort.js').default, wallClockMs?: () => number }} [options.auth] - Auth configuration
212
249
  * @param {string[]} [options.allowedWriters] - Optional whitelist of allowed writer IDs
213
250
  */
214
- constructor({ httpPort, graph, path = '/sync', host = '127.0.0.1', maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES, auth, allowedWriters } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
215
- this._httpPort = httpPort;
216
- this._graph = graph;
217
- this._path = path && path.startsWith('/') ? path : `/${path || 'sync'}`;
218
- this._host = host;
219
- this._maxRequestBytes = maxRequestBytes;
251
+ constructor(options) {
252
+ /** @type {z.infer<typeof optionsSchema>} */
253
+ let parsed;
254
+ try {
255
+ parsed = optionsSchema.parse(options);
256
+ } catch (err) {
257
+ if (err instanceof z.ZodError) {
258
+ const messages = err.issues.map((i) => i.message).join('; ');
259
+ throw new Error(`HttpSyncServer config: ${messages}`);
260
+ }
261
+ throw err;
262
+ }
263
+
264
+ this._httpPort = parsed.httpPort;
265
+ this._graph = parsed.graph;
266
+ this._path = parsed.path;
267
+ this._host = parsed.host;
268
+ this._maxRequestBytes = parsed.maxRequestBytes;
220
269
  this._server = null;
221
- const authInit = initAuth(auth, allowedWriters);
270
+ const authInit = initAuth(parsed.auth, parsed.allowedWriters);
222
271
  this._auth = authInit.auth;
223
272
  this._authMode = authInit.authMode;
224
- if (allowedWriters && !authInit.auth) {
225
- throw new Error('allowedWriters requires auth.keys to be configured');
226
- }
227
273
  }
228
274
 
229
275
  /**
@@ -234,7 +280,7 @@ export default class HttpSyncServer {
234
280
  * null so the request proceeds.
235
281
  *
236
282
  * @param {{ method: string, url: string, headers: { [x: string]: string }, body: Buffer|undefined }} request
237
- * @param {*} parsed - Parsed sync request body
283
+ * @param {Record<string, unknown>} parsed - Parsed sync request body
238
284
  * @returns {Promise<{ status: number, headers: Object, body: string }|null>}
239
285
  * @private
240
286
  */
@@ -264,29 +310,31 @@ export default class HttpSyncServer {
264
310
  return null;
265
311
  }
266
312
 
267
- /** @param {{ method: string, url: string, headers: { [x: string]: string }, body: Buffer|undefined }} request */
313
+ /** @param {{ method: string, url: string, headers: Object, body: Buffer|undefined }} request */
268
314
  async _handleRequest(request) {
269
- const contentTypeError = checkContentType(request.headers);
315
+ /** @type {{ method: string, url: string, headers: Record<string, string>, body: Buffer|undefined }} */
316
+ const req = { ...request, headers: /** @type {Record<string, string>} */ (request.headers) };
317
+ const contentTypeError = checkContentType(req.headers);
270
318
  if (contentTypeError) {
271
319
  return contentTypeError;
272
320
  }
273
321
 
274
- const routeError = validateRoute(request, this._path, this._host);
322
+ const routeError = validateRoute(req, this._path, this._host);
275
323
  if (routeError) {
276
324
  return routeError;
277
325
  }
278
326
 
279
- const sizeError = checkBodySize(request.body, this._maxRequestBytes);
327
+ const sizeError = checkBodySize(req.body, this._maxRequestBytes);
280
328
  if (sizeError) {
281
329
  return sizeError;
282
330
  }
283
331
 
284
- const { error, parsed } = parseBody(request.body);
332
+ const { error, parsed } = parseBody(req.body);
285
333
  if (error) {
286
334
  return error;
287
335
  }
288
336
 
289
- const authError = await this._authorize(request, parsed);
337
+ const authError = await this._authorize(req, parsed);
290
338
  if (authError) {
291
339
  return authError;
292
340
  }
@@ -294,8 +342,8 @@ export default class HttpSyncServer {
294
342
  try {
295
343
  const response = await this._graph.processSyncRequest(parsed);
296
344
  return jsonResponse(response);
297
- } catch (err) {
298
- return errorResponse(500, /** @type {any} */ (err)?.message || 'Sync failed'); // TODO(ts-cleanup): type error
345
+ } catch (/** @type {unknown} */ err) {
346
+ return errorResponse(500, err instanceof Error ? err.message : 'Sync failed');
299
347
  }
300
348
  }
301
349
 
@@ -311,11 +359,14 @@ export default class HttpSyncServer {
311
359
  throw new Error('listen() requires a numeric port');
312
360
  }
313
361
 
314
- const server = this._httpPort.createServer((/** @type {*} */ request) => this._handleRequest(request)); // TODO(ts-cleanup): type http callback
362
+ /** @type {{ listen: Function, close: Function, address: Function }} */
363
+ const server = this._httpPort.createServer(
364
+ (/** @type {{ method: string, url: string, headers: Object, body: Buffer|undefined }} */ request) => this._handleRequest(request),
365
+ );
315
366
  this._server = server;
316
367
 
317
368
  await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
318
- server.listen(port, this._host, (/** @type {*} */ err) => { // TODO(ts-cleanup): type http callback
369
+ server.listen(port, this._host, (/** @type {Error|null} */ err) => {
319
370
  if (err) {
320
371
  reject(err);
321
372
  } else {
@@ -332,7 +383,7 @@ export default class HttpSyncServer {
332
383
  url,
333
384
  close: () =>
334
385
  /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
335
- server.close((/** @type {*} */ err) => { // TODO(ts-cleanup): type http callback
386
+ server.close((/** @type {Error|null} */ err) => {
336
387
  if (err) {
337
388
  reject(err);
338
389
  } else {
@@ -50,7 +50,7 @@ export default class IndexRebuildService {
50
50
  * @throws {Error} If graphService is not provided
51
51
  * @throws {Error} If storage adapter is not provided
52
52
  */
53
- constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
53
+ constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {{ graphService: { iterateNodes: (opts: { ref: string, limit: number }) => AsyncIterable<{ sha: string, parents: string[] }> }, storage: import('../../ports/IndexStoragePort.js').default }} */ ({})) {
54
54
  if (!graphService) {
55
55
  throw new Error('IndexRebuildService requires a graphService');
56
56
  }
@@ -156,7 +156,7 @@ export default class IndexRebuildService {
156
156
  operation: 'rebuild',
157
157
  ref,
158
158
  mode,
159
- error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
159
+ error: err instanceof Error ? err.message : String(err),
160
160
  durationMs,
161
161
  });
162
162
  throw err;
@@ -247,12 +247,12 @@ export default class IndexRebuildService {
247
247
  * @private
248
248
  */
249
249
  async _rebuildStreaming(ref, { limit, maxMemoryBytes, onFlush, onProgress, signal, frontier }) {
250
- const builder = new StreamingBitmapIndexBuilder(/** @type {*} */ ({ // TODO(ts-cleanup): narrow port type
250
+ const builder = new StreamingBitmapIndexBuilder({
251
251
  storage: this.storage,
252
252
  maxMemoryBytes,
253
253
  onFlush,
254
254
  crypto: this._crypto,
255
- }));
255
+ });
256
256
 
257
257
  let processedNodes = 0;
258
258
 
@@ -266,7 +266,7 @@ export default class IndexRebuildService {
266
266
  if (processedNodes % 10000 === 0) {
267
267
  checkAborted(signal, 'rebuild');
268
268
  if (onProgress) {
269
- const stats = /** @type {any} */ (builder).getMemoryStats(); // TODO(ts-cleanup): narrow port type
269
+ const stats = /** @type {{ estimatedBitmapBytes: number }} */ (builder.getMemoryStats());
270
270
  onProgress({
271
271
  processedNodes,
272
272
  currentMemoryBytes: stats.estimatedBitmapBytes,
@@ -275,7 +275,7 @@ export default class IndexRebuildService {
275
275
  }
276
276
  }
277
277
 
278
- return await /** @type {any} */ (builder).finalize({ signal, frontier }); // TODO(ts-cleanup): narrow port type
278
+ return await builder.finalize({ signal, frontier });
279
279
  }
280
280
 
281
281
  /**
@@ -389,7 +389,7 @@ export default class IndexRebuildService {
389
389
 
390
390
  // Staleness check
391
391
  if (currentFrontier) {
392
- const indexFrontier = await loadIndexFrontier(shardOids, /** @type {*} */ (this.storage), { codec: this._codec }); // TODO(ts-cleanup): narrow port type
392
+ const indexFrontier = await loadIndexFrontier(shardOids, /** @type {import('../../ports/IndexStoragePort.js').default & import('../../ports/BlobPort.js').default} */ (this.storage), { codec: this._codec });
393
393
  if (indexFrontier) {
394
394
  const result = checkStaleness(indexFrontier, currentFrontier);
395
395
  if (result.stale) {
@@ -6,12 +6,13 @@
6
6
  import defaultCodec from '../utils/defaultCodec.js';
7
7
 
8
8
  /**
9
- * @param {*} envelope
9
+ * @param {unknown} envelope
10
10
  * @param {string} label
11
11
  * @private
12
12
  */
13
13
  function validateEnvelope(envelope, label) {
14
- if (!envelope || typeof envelope !== 'object' || !envelope.frontier || typeof envelope.frontier !== 'object') {
14
+ const rec = /** @type {Record<string, unknown>} */ (envelope);
15
+ if (!rec || typeof rec !== 'object' || !rec.frontier || typeof rec.frontier !== 'object') {
15
16
  throw new Error(`invalid frontier envelope for ${label}`);
16
17
  }
17
18
  }
@@ -25,7 +26,7 @@ function validateEnvelope(envelope, label) {
25
26
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
26
27
  * @returns {Promise<Map<string, string>|null>} Frontier map, or null if not present (legacy index)
27
28
  */
28
- export async function loadIndexFrontier(shardOids, storage, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
29
+ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
29
30
  const c = codec || defaultCodec;
30
31
  const cborOid = shardOids['frontier.cbor'];
31
32
  if (cborOid) {