@git-stunts/git-warp 10.8.0 → 11.3.3
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 +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +80 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +3 -3
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +707 -0
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +2 -2
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +71 -4
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
- package/src/hooks/post-merge.sh +0 -60
|
@@ -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 }
|
|
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 {
|
|
19
|
-
* @returns {
|
|
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]:
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
131
|
+
const rec = /** @type {Record<string, unknown>} */ (parsed);
|
|
132
|
+
if (rec.type !== 'sync-request') {
|
|
91
133
|
return false;
|
|
92
134
|
}
|
|
93
|
-
if (!
|
|
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:
|
|
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,18 +225,14 @@ function parseBody(body) {
|
|
|
183
225
|
/**
|
|
184
226
|
* Initializes auth service from config if present.
|
|
185
227
|
*
|
|
186
|
-
* @param {
|
|
228
|
+
* @param {z.infer<typeof authSchema>} [auth]
|
|
229
|
+
* @param {string[]} [allowedWriters]
|
|
187
230
|
* @returns {{ auth: SyncAuthService|null, authMode: string|null }}
|
|
188
231
|
* @private
|
|
189
232
|
*/
|
|
190
|
-
function initAuth(auth) {
|
|
191
|
-
if (auth
|
|
192
|
-
|
|
193
|
-
const mode = auth.mode || 'enforce';
|
|
194
|
-
if (!VALID_MODES.has(mode)) {
|
|
195
|
-
throw new Error(`Invalid auth.mode: '${mode}'. Must be 'enforce' or 'log-only'.`);
|
|
196
|
-
}
|
|
197
|
-
return { auth: new SyncAuthService(auth), authMode: mode };
|
|
233
|
+
function initAuth(auth, allowedWriters) {
|
|
234
|
+
if (auth) {
|
|
235
|
+
return { auth: new SyncAuthService({ ...auth, allowedWriters }), authMode: auth.mode };
|
|
198
236
|
}
|
|
199
237
|
return { auth: null, authMode: null };
|
|
200
238
|
}
|
|
@@ -203,85 +241,109 @@ export default class HttpSyncServer {
|
|
|
203
241
|
/**
|
|
204
242
|
* @param {Object} options
|
|
205
243
|
* @param {import('../../ports/HttpServerPort.js').default} options.httpPort - HTTP server port abstraction
|
|
206
|
-
* @param {{ processSyncRequest:
|
|
244
|
+
* @param {{ processSyncRequest: Function }} options.graph - WarpGraph instance (must expose processSyncRequest)
|
|
207
245
|
* @param {string} [options.path='/sync'] - URL path to handle sync requests on
|
|
208
246
|
* @param {string} [options.host='127.0.0.1'] - Host to bind
|
|
209
247
|
* @param {number} [options.maxRequestBytes=4194304] - Maximum request body size in bytes
|
|
210
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
|
|
249
|
+
* @param {string[]} [options.allowedWriters] - Optional whitelist of allowed writer IDs
|
|
211
250
|
*/
|
|
212
|
-
constructor(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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;
|
|
218
269
|
this._server = null;
|
|
219
|
-
const authInit = initAuth(auth);
|
|
270
|
+
const authInit = initAuth(parsed.auth, parsed.allowedWriters);
|
|
220
271
|
this._auth = authInit.auth;
|
|
221
272
|
this._authMode = authInit.authMode;
|
|
222
273
|
}
|
|
223
274
|
|
|
224
275
|
/**
|
|
225
|
-
*
|
|
276
|
+
* Runs auth verification and writer whitelist checks. Returns an error
|
|
277
|
+
* response when enforcement blocks the request, or null to proceed.
|
|
226
278
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* @private
|
|
230
|
-
*/
|
|
231
|
-
/**
|
|
232
|
-
* Runs auth verification if configured. Returns an error response to
|
|
233
|
-
* send, or null if the request should proceed.
|
|
279
|
+
* In log-only mode both checks record metrics/logs but always return
|
|
280
|
+
* null so the request proceeds.
|
|
234
281
|
*
|
|
235
|
-
* @param {
|
|
282
|
+
* @param {{ method: string, url: string, headers: { [x: string]: string }, body: Buffer|undefined }} request
|
|
283
|
+
* @param {Record<string, unknown>} parsed - Parsed sync request body
|
|
236
284
|
* @returns {Promise<{ status: number, headers: Object, body: string }|null>}
|
|
237
285
|
* @private
|
|
238
286
|
*/
|
|
239
|
-
async
|
|
287
|
+
async _authorize(request, parsed) {
|
|
240
288
|
if (!this._auth) {
|
|
241
289
|
return null;
|
|
242
290
|
}
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
|
|
292
|
+
// Signature verification (uses raw request headers + body hash)
|
|
293
|
+
const authResult = await this._auth.verify(request);
|
|
294
|
+
if (!authResult.ok) {
|
|
245
295
|
if (this._authMode === 'enforce') {
|
|
246
|
-
return errorResponse(
|
|
296
|
+
return errorResponse(authResult.status, authResult.reason);
|
|
247
297
|
}
|
|
248
298
|
this._auth.recordLogOnlyPassthrough();
|
|
249
299
|
}
|
|
300
|
+
|
|
301
|
+
// Writer whitelist (uses parsed body for writer IDs)
|
|
302
|
+
if (parsed.patches && typeof parsed.patches === 'object') {
|
|
303
|
+
const writerIds = Object.keys(parsed.patches);
|
|
304
|
+
const writerResult = this._auth.enforceWriters(writerIds);
|
|
305
|
+
if (!writerResult.ok) {
|
|
306
|
+
return errorResponse(writerResult.status, writerResult.reason);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
250
310
|
return null;
|
|
251
311
|
}
|
|
252
312
|
|
|
253
|
-
/** @param {{ method: string, url: string, headers:
|
|
313
|
+
/** @param {{ method: string, url: string, headers: Object, body: Buffer|undefined }} request */
|
|
254
314
|
async _handleRequest(request) {
|
|
255
|
-
|
|
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);
|
|
256
318
|
if (contentTypeError) {
|
|
257
319
|
return contentTypeError;
|
|
258
320
|
}
|
|
259
321
|
|
|
260
|
-
const routeError = validateRoute(
|
|
322
|
+
const routeError = validateRoute(req, this._path, this._host);
|
|
261
323
|
if (routeError) {
|
|
262
324
|
return routeError;
|
|
263
325
|
}
|
|
264
326
|
|
|
265
|
-
const sizeError = checkBodySize(
|
|
327
|
+
const sizeError = checkBodySize(req.body, this._maxRequestBytes);
|
|
266
328
|
if (sizeError) {
|
|
267
329
|
return sizeError;
|
|
268
330
|
}
|
|
269
331
|
|
|
270
|
-
const
|
|
271
|
-
if (authError) {
|
|
272
|
-
return authError;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const { error, parsed } = parseBody(request.body);
|
|
332
|
+
const { error, parsed } = parseBody(req.body);
|
|
276
333
|
if (error) {
|
|
277
334
|
return error;
|
|
278
335
|
}
|
|
279
336
|
|
|
337
|
+
const authError = await this._authorize(req, parsed);
|
|
338
|
+
if (authError) {
|
|
339
|
+
return authError;
|
|
340
|
+
}
|
|
341
|
+
|
|
280
342
|
try {
|
|
281
343
|
const response = await this._graph.processSyncRequest(parsed);
|
|
282
344
|
return jsonResponse(response);
|
|
283
|
-
} catch (err) {
|
|
284
|
-
return errorResponse(500,
|
|
345
|
+
} catch (/** @type {unknown} */ err) {
|
|
346
|
+
return errorResponse(500, err instanceof Error ? err.message : 'Sync failed');
|
|
285
347
|
}
|
|
286
348
|
}
|
|
287
349
|
|
|
@@ -297,11 +359,14 @@ export default class HttpSyncServer {
|
|
|
297
359
|
throw new Error('listen() requires a numeric port');
|
|
298
360
|
}
|
|
299
361
|
|
|
300
|
-
|
|
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
|
+
);
|
|
301
366
|
this._server = server;
|
|
302
367
|
|
|
303
368
|
await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
304
|
-
server.listen(port, this._host, (/** @type {
|
|
369
|
+
server.listen(port, this._host, (/** @type {Error|null} */ err) => {
|
|
305
370
|
if (err) {
|
|
306
371
|
reject(err);
|
|
307
372
|
} else {
|
|
@@ -318,7 +383,7 @@ export default class HttpSyncServer {
|
|
|
318
383
|
url,
|
|
319
384
|
close: () =>
|
|
320
385
|
/** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
321
|
-
server.close((/** @type {
|
|
386
|
+
server.close((/** @type {Error|null} */ err) => {
|
|
322
387
|
if (err) {
|
|
323
388
|
reject(err);
|
|
324
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 {
|
|
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:
|
|
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(
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
9
|
+
* @param {unknown} envelope
|
|
10
10
|
* @param {string} label
|
|
11
11
|
* @private
|
|
12
12
|
*/
|
|
13
13
|
function validateEnvelope(envelope, label) {
|
|
14
|
-
|
|
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 } =
|
|
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) {
|
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
* @typedef {Object} WarpStateV5
|
|
30
30
|
* @property {import('../crdt/ORSet.js').ORSet} nodeAlive - ORSet of alive nodes
|
|
31
31
|
* @property {import('../crdt/ORSet.js').ORSet} edgeAlive - ORSet of alive edges
|
|
32
|
-
* @property {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
32
|
+
* @property {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} prop - Properties with LWW
|
|
33
33
|
* @property {import('../crdt/VersionVector.js').VersionVector} observedFrontier - Observed version vector
|
|
34
34
|
* @property {Map<string, import('../utils/EventId.js').EventId>} edgeBirthEvent - EdgeKey → EventId of most recent EdgeAdd (for clean-slate prop visibility)
|
|
35
35
|
*/
|
|
@@ -81,7 +81,7 @@ export function createEmptyStateV5() {
|
|
|
81
81
|
* @param {string} [op.to] - Target node ID (for EdgeAdd, EdgeRemove)
|
|
82
82
|
* @param {string} [op.label] - Edge label (for EdgeAdd, EdgeRemove)
|
|
83
83
|
* @param {string} [op.key] - Property key (for PropSet)
|
|
84
|
-
* @param {
|
|
84
|
+
* @param {unknown} [op.value] - Property value (for PropSet)
|
|
85
85
|
* @param {import('../utils/EventId.js').EventId} eventId - Event ID for causality tracking
|
|
86
86
|
* @returns {void}
|
|
87
87
|
*/
|
|
@@ -114,7 +114,7 @@ export function applyOpV2(state, op, eventId) {
|
|
|
114
114
|
// Uses EventId-based LWW, same as v4
|
|
115
115
|
const key = encodePropKey(/** @type {string} */ (op.node), /** @type {string} */ (op.key));
|
|
116
116
|
const current = state.prop.get(key);
|
|
117
|
-
state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister
|
|
117
|
+
state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<unknown>} */ (lwwMax(current, lwwSet(eventId, op.value))));
|
|
118
118
|
break;
|
|
119
119
|
}
|
|
120
120
|
default:
|
|
@@ -290,11 +290,11 @@ function edgeRemoveOutcome(orset, op) {
|
|
|
290
290
|
* - `superseded`: An existing value with higher EventId wins
|
|
291
291
|
* - `redundant`: Exact same write (identical EventId)
|
|
292
292
|
*
|
|
293
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
293
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} propMap - The properties map keyed by encoded prop keys
|
|
294
294
|
* @param {Object} op - The PropSet operation
|
|
295
295
|
* @param {string} op.node - Node ID owning the property
|
|
296
296
|
* @param {string} op.key - Property key/name
|
|
297
|
-
* @param {
|
|
297
|
+
* @param {unknown} op.value - Property value to set
|
|
298
298
|
* @param {import('../utils/EventId.js').EventId} eventId - The event ID for this operation, used for LWW comparison
|
|
299
299
|
* @returns {{target: string, result: 'applied'|'superseded'|'redundant', reason?: string}}
|
|
300
300
|
* Outcome with encoded prop key as target; includes reason when superseded
|
|
@@ -347,7 +347,7 @@ function propSetOutcome(propMap, op, eventId) {
|
|
|
347
347
|
* @param {Object} patch - The patch to apply
|
|
348
348
|
* @param {string} patch.writer - Writer ID who created this patch
|
|
349
349
|
* @param {number} patch.lamport - Lamport timestamp of this patch
|
|
350
|
-
* @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?:
|
|
350
|
+
* @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>} patch.ops - Array of operations to apply
|
|
351
351
|
* @param {Map<string, number>|{[x: string]: number}} patch.context - Version vector context (Map or serialized form)
|
|
352
352
|
* @param {string} patchSha - The Git SHA of the patch commit (used for EventId creation)
|
|
353
353
|
* @param {boolean} [collectReceipts=false] - When true, computes and returns receipt data
|
|
@@ -470,16 +470,16 @@ export function joinStates(a, b) {
|
|
|
470
470
|
*
|
|
471
471
|
* This is a pure function that does not mutate its inputs.
|
|
472
472
|
*
|
|
473
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
474
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
475
|
-
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
473
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} a - First property map
|
|
474
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} b - Second property map
|
|
475
|
+
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} New map containing merged properties
|
|
476
476
|
*/
|
|
477
477
|
function mergeProps(a, b) {
|
|
478
478
|
const result = new Map(a);
|
|
479
479
|
|
|
480
480
|
for (const [key, regB] of b) {
|
|
481
481
|
const regA = result.get(key);
|
|
482
|
-
result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister
|
|
482
|
+
result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<unknown>} */ (lwwMax(regA, regB)));
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
return result;
|
|
@@ -530,7 +530,7 @@ function mergeEdgeBirthEvent(a, b) {
|
|
|
530
530
|
* - When `options.receipts` is true, returns a TickReceipt per patch for
|
|
531
531
|
* provenance tracking and debugging.
|
|
532
532
|
*
|
|
533
|
-
* @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?:
|
|
533
|
+
* @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>, context: Map<string, number>|{[x: string]: number}}, sha: string}>} patches - Array of patch objects with their Git SHAs
|
|
534
534
|
* @param {WarpStateV5} [initialState] - Optional starting state (for incremental materialization from checkpoint)
|
|
535
535
|
* @param {Object} [options] - Optional configuration
|
|
536
536
|
* @param {boolean} [options.receipts=false] - When true, collect and return TickReceipts
|
|
@@ -152,7 +152,7 @@ export default class LogicalTraversal {
|
|
|
152
152
|
* @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
|
|
153
153
|
*/
|
|
154
154
|
async _prepare(start, { dir, labelFilter, maxDepth }) {
|
|
155
|
-
const materialized = await /** @type {
|
|
155
|
+
const materialized = await /** @type {{ _materializeGraph: () => Promise<{adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}> }} */ (this._graph)._materializeGraph();
|
|
156
156
|
|
|
157
157
|
if (!(await this._graph.hasNode(start))) {
|
|
158
158
|
throw new TraversalError(`Start node not found: ${start}`, {
|
|
@@ -27,6 +27,7 @@ export const MESSAGE_TITLES = {
|
|
|
27
27
|
patch: 'warp:patch',
|
|
28
28
|
checkpoint: 'warp:checkpoint',
|
|
29
29
|
anchor: 'warp:anchor',
|
|
30
|
+
audit: 'warp:audit',
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -44,6 +45,8 @@ export const TRAILER_KEYS = {
|
|
|
44
45
|
indexOid: 'eg-index-oid',
|
|
45
46
|
schema: 'eg-schema',
|
|
46
47
|
checkpointVersion: 'eg-checkpoint',
|
|
48
|
+
dataCommit: 'eg-data-commit',
|
|
49
|
+
opsDigest: 'eg-ops-digest',
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
/**
|
|
@@ -63,7 +66,7 @@ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
|
63
66
|
// -----------------------------------------------------------------------------
|
|
64
67
|
|
|
65
68
|
// Lazy singleton codec instance
|
|
66
|
-
/** @type {
|
|
69
|
+
/** @type {TrailerCodec|null} */
|
|
67
70
|
let _codec = null;
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -116,7 +116,7 @@ export function assertOpsCompatible(ops, maxSchema) {
|
|
|
116
116
|
* Detects the WARP message kind from a raw commit message.
|
|
117
117
|
*
|
|
118
118
|
* @param {string} message - The raw commit message
|
|
119
|
-
* @returns {'patch'|'checkpoint'|'anchor'|null} The message kind, or null if not a WARP message
|
|
119
|
+
* @returns {'patch'|'checkpoint'|'anchor'|'audit'|null} The message kind, or null if not a WARP message
|
|
120
120
|
*
|
|
121
121
|
* @example
|
|
122
122
|
* const kind = detectMessageKind(message);
|
|
@@ -134,7 +134,7 @@ export function detectMessageKind(message) {
|
|
|
134
134
|
const decoded = codec.decode(message);
|
|
135
135
|
const kind = decoded.trailers[TRAILER_KEYS.kind];
|
|
136
136
|
|
|
137
|
-
if (kind === 'patch' || kind === 'checkpoint' || kind === 'anchor') {
|
|
137
|
+
if (kind === 'patch' || kind === 'checkpoint' || kind === 'anchor' || kind === 'audit') {
|
|
138
138
|
return kind;
|
|
139
139
|
}
|
|
140
140
|
return null;
|
|
@@ -16,7 +16,7 @@ import { createVersionVector, vvIncrement } from '../crdt/VersionVector.js';
|
|
|
16
16
|
* @param {Object} v4State - The V4 materialized state (visible projection)
|
|
17
17
|
* @param {Map<string, {value: boolean}>} v4State.nodeAlive - V4 node alive map
|
|
18
18
|
* @param {Map<string, {value: boolean}>} v4State.edgeAlive - V4 edge alive map
|
|
19
|
-
* @param {Map<string,
|
|
19
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} v4State.prop - V4 property map
|
|
20
20
|
* @param {string} migrationWriterId - Writer ID to use for synthetic dots
|
|
21
21
|
* @returns {import('./JoinReducer.js').WarpStateV5} The migrated V5 state
|
|
22
22
|
*/
|
|
@@ -43,10 +43,10 @@ function matchGlob(pattern, str) {
|
|
|
43
43
|
* - If `expose` is provided and non-empty, only keys in `expose` are included.
|
|
44
44
|
* - If `expose` is absent/empty, all non-redacted keys are included.
|
|
45
45
|
*
|
|
46
|
-
* @param {Map<string,
|
|
46
|
+
* @param {Map<string, unknown>} propsMap - The full properties Map
|
|
47
47
|
* @param {string[]|undefined} expose - Whitelist of property keys to include
|
|
48
48
|
* @param {string[]|undefined} redact - Blacklist of property keys to exclude
|
|
49
|
-
* @returns {Map<string,
|
|
49
|
+
* @returns {Map<string, unknown>} Filtered properties Map
|
|
50
50
|
*/
|
|
51
51
|
function filterProps(propsMap, expose, redact) {
|
|
52
52
|
const redactSet = redact && redact.length > 0 ? new Set(redact) : null;
|
|
@@ -102,7 +102,7 @@ export default class ObserverView {
|
|
|
102
102
|
this._graph = graph;
|
|
103
103
|
|
|
104
104
|
/** @type {LogicalTraversal} */
|
|
105
|
-
this.traverse = new LogicalTraversal(/** @type {
|
|
105
|
+
this.traverse = new LogicalTraversal(/** @type {import('../WarpGraph.js').default} */ (/** @type {unknown} */ (this)));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
@@ -124,11 +124,11 @@ export default class ObserverView {
|
|
|
124
124
|
* Builds a filtered adjacency structure that only includes edges
|
|
125
125
|
* where both endpoints pass the match filter.
|
|
126
126
|
*
|
|
127
|
-
* @returns {Promise<{state:
|
|
127
|
+
* @returns {Promise<{state: unknown, stateHash: string, adjacency: {outgoing: Map<string, unknown[]>, incoming: Map<string, unknown[]>}}>}
|
|
128
128
|
* @private
|
|
129
129
|
*/
|
|
130
130
|
async _materializeGraph() {
|
|
131
|
-
const materialized = await /** @type {
|
|
131
|
+
const materialized = await /** @type {{ _materializeGraph: () => Promise<{state: import('./JoinReducer.js').WarpStateV5, stateHash: string, adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}> }} */ (this._graph)._materializeGraph();
|
|
132
132
|
const { state, stateHash } = materialized;
|
|
133
133
|
|
|
134
134
|
// Build filtered adjacency: only edges where both endpoints match
|
|
@@ -212,7 +212,7 @@ export default class ObserverView {
|
|
|
212
212
|
* the observer pattern.
|
|
213
213
|
*
|
|
214
214
|
* @param {string} nodeId - The node ID to get properties for
|
|
215
|
-
* @returns {Promise<Map<string,
|
|
215
|
+
* @returns {Promise<Map<string, unknown>|null>} Filtered properties Map, or null
|
|
216
216
|
*/
|
|
217
217
|
async getNodeProps(nodeId) {
|
|
218
218
|
if (!matchGlob(this._matchPattern, nodeId)) {
|
|
@@ -234,7 +234,7 @@ export default class ObserverView {
|
|
|
234
234
|
*
|
|
235
235
|
* An edge is visible only when both endpoints match the observer pattern.
|
|
236
236
|
*
|
|
237
|
-
* @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string,
|
|
237
|
+
* @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string, unknown>}>>}
|
|
238
238
|
*/
|
|
239
239
|
async getEdges() {
|
|
240
240
|
const allEdges = await this._graph.getEdges();
|
|
@@ -260,6 +260,6 @@ export default class ObserverView {
|
|
|
260
260
|
* @returns {QueryBuilder} A query builder scoped to this observer
|
|
261
261
|
*/
|
|
262
262
|
query() {
|
|
263
|
-
return new QueryBuilder(/** @type {
|
|
263
|
+
return new QueryBuilder(/** @type {import('../WarpGraph.js').default} */ (/** @type {unknown} */ (this)));
|
|
264
264
|
}
|
|
265
265
|
}
|