@topgunbuild/server 0.10.1 → 0.11.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.
@@ -462,7 +462,7 @@ var require_serialize = __commonJS({
462
462
  "../../node_modules/.pnpm/better-sqlite3@11.10.0/node_modules/better-sqlite3/lib/methods/serialize.js"(exports2, module2) {
463
463
  "use strict";
464
464
  var { cppdb } = require_util();
465
- module2.exports = function serialize7(options) {
465
+ module2.exports = function serialize8(options) {
466
466
  if (options == null) options = {};
467
467
  if (typeof options !== "object") throw new TypeError("Expected first argument to be an options object");
468
468
  const attachedName = "attached" in options ? options.attached : "main";
@@ -805,7 +805,7 @@ var require_lib = __commonJS({
805
805
 
806
806
  // src/ServerFactory.ts
807
807
  var import_http = require("http");
808
- var import_core41 = require("@topgunbuild/core");
808
+ var import_core42 = require("@topgunbuild/core");
809
809
 
810
810
  // src/ServerCoordinator.ts
811
811
  var import_core = require("@topgunbuild/core");
@@ -8938,17 +8938,18 @@ function buildTLSOptions(config2) {
8938
8938
  return options;
8939
8939
  }
8940
8940
  function createNetworkModule(config2, _deps) {
8941
+ let currentRequestHandler = (_req, res) => {
8942
+ res.writeHead(200);
8943
+ res.end(config2.tls?.enabled ? "TopGun Server Running (Secure)" : "TopGun Server Running");
8944
+ };
8945
+ const requestDispatcher = (req, res) => {
8946
+ currentRequestHandler(req, res);
8947
+ };
8941
8948
  let httpServer;
8942
8949
  if (config2.tls?.enabled) {
8943
- httpServer = (0, import_node_https.createServer)(buildTLSOptions(config2.tls), (_req, res) => {
8944
- res.writeHead(200);
8945
- res.end("TopGun Server Running (Secure)");
8946
- });
8950
+ httpServer = (0, import_node_https.createServer)(buildTLSOptions(config2.tls), requestDispatcher);
8947
8951
  } else {
8948
- httpServer = (0, import_node_http.createServer)((_req, res) => {
8949
- res.writeHead(200);
8950
- res.end("TopGun Server Running");
8951
- });
8952
+ httpServer = (0, import_node_http.createServer)(requestDispatcher);
8952
8953
  }
8953
8954
  httpServer.maxConnections = config2.maxConnections ?? 1e4;
8954
8955
  httpServer.timeout = config2.serverTimeout ?? 12e4;
@@ -8986,13 +8987,18 @@ function createNetworkModule(config2, _deps) {
8986
8987
  httpServer.listen(config2.port, () => {
8987
8988
  logger.info({ port: config2.port }, "Server Coordinator listening");
8988
8989
  });
8990
+ },
8991
+ // Deferred wiring: allows ServerFactory to inject the /sync handler
8992
+ // after HttpSyncHandler is assembled
8993
+ setHttpRequestHandler: (handler) => {
8994
+ currentRequestHandler = handler;
8989
8995
  }
8990
8996
  };
8991
8997
  }
8992
8998
 
8993
8999
  // src/modules/handlers-module.ts
8994
9000
  var import_ws9 = require("ws");
8995
- var import_core37 = require("@topgunbuild/core");
9001
+ var import_core38 = require("@topgunbuild/core");
8996
9002
 
8997
9003
  // src/coordinator/auth-handler.ts
8998
9004
  var jwt = __toESM(require("jsonwebtoken"));
@@ -12048,13 +12054,260 @@ var QueryHandler = class {
12048
12054
  }
12049
12055
  };
12050
12056
 
12057
+ // src/coordinator/http-sync-handler.ts
12058
+ var import_core25 = require("@topgunbuild/core");
12059
+ var HttpSyncHandler = class {
12060
+ constructor(config2) {
12061
+ this.config = config2;
12062
+ }
12063
+ /**
12064
+ * Process a complete HTTP sync request and return a response.
12065
+ *
12066
+ * @param request - Parsed and validated HttpSyncRequest body
12067
+ * @param authToken - JWT token from Authorization header
12068
+ * @returns HttpSyncResponse with acks, deltas, query/search results
12069
+ * @throws Error with message starting with '401:' for auth failures
12070
+ * @throws Error with message starting with '403:' for permission failures
12071
+ */
12072
+ async handleSyncRequest(request, authToken) {
12073
+ let principal;
12074
+ try {
12075
+ principal = this.config.authHandler.verifyToken(authToken);
12076
+ } catch (err) {
12077
+ throw new Error(`401: Authentication failed: ${err.message}`);
12078
+ }
12079
+ this.config.hlc.update({
12080
+ millis: request.clientHlc.millis,
12081
+ counter: request.clientHlc.counter,
12082
+ nodeId: request.clientHlc.nodeId
12083
+ });
12084
+ const response = {
12085
+ serverHlc: this.config.hlc.now()
12086
+ };
12087
+ const errors = [];
12088
+ if (request.operations && request.operations.length > 0) {
12089
+ const ackResults = await this.processOperations(request.operations, principal, errors);
12090
+ if (ackResults.processedCount > 0) {
12091
+ response.ack = {
12092
+ lastId: ackResults.lastId,
12093
+ results: ackResults.results.length > 0 ? ackResults.results : void 0
12094
+ };
12095
+ }
12096
+ }
12097
+ if (request.syncMaps && request.syncMaps.length > 0) {
12098
+ response.deltas = await this.computeDeltas(request.syncMaps, principal, errors);
12099
+ }
12100
+ if (request.queries && request.queries.length > 0) {
12101
+ response.queryResults = await this.executeQueries(request.queries, principal, errors);
12102
+ }
12103
+ if (request.searches && request.searches.length > 0) {
12104
+ response.searchResults = await this.executeSearches(request.searches, principal, errors);
12105
+ }
12106
+ if (errors.length > 0) {
12107
+ response.errors = errors;
12108
+ }
12109
+ response.serverHlc = this.config.hlc.now();
12110
+ return response;
12111
+ }
12112
+ /**
12113
+ * Process a batch of client operations.
12114
+ */
12115
+ async processOperations(operations, principal, errors) {
12116
+ let lastId = "";
12117
+ let processedCount = 0;
12118
+ const results = [];
12119
+ for (const op of operations) {
12120
+ const opId = op.id || `http-op-${processedCount}`;
12121
+ const isRemove = op.opType === "REMOVE" || op.record && op.record.value === null;
12122
+ const action = isRemove ? "REMOVE" : "PUT";
12123
+ if (!this.config.securityManager.checkPermission(principal, op.mapName, action)) {
12124
+ errors.push({
12125
+ code: 403,
12126
+ message: "Access denied",
12127
+ context: `Operation on ${op.mapName}/${op.key}`
12128
+ });
12129
+ results.push({
12130
+ opId,
12131
+ success: false,
12132
+ achievedLevel: "FIRE_AND_FORGET",
12133
+ error: "Access denied"
12134
+ });
12135
+ continue;
12136
+ }
12137
+ try {
12138
+ const result = await this.config.operationHandler.applyOpToMap(op);
12139
+ if (result.rejected) {
12140
+ results.push({
12141
+ opId,
12142
+ success: false,
12143
+ achievedLevel: "FIRE_AND_FORGET",
12144
+ error: "Operation rejected by conflict resolver"
12145
+ });
12146
+ } else {
12147
+ results.push({
12148
+ opId,
12149
+ success: true,
12150
+ achievedLevel: "MEMORY"
12151
+ });
12152
+ }
12153
+ processedCount++;
12154
+ lastId = opId;
12155
+ } catch (err) {
12156
+ logger.error({ err, opId }, "HTTP sync operation failed");
12157
+ errors.push({
12158
+ code: 500,
12159
+ message: err.message || "Operation failed",
12160
+ context: `Operation ${opId} on ${op.mapName}/${op.key}`
12161
+ });
12162
+ results.push({
12163
+ opId,
12164
+ success: false,
12165
+ achievedLevel: "FIRE_AND_FORGET",
12166
+ error: err.message || "Operation failed"
12167
+ });
12168
+ }
12169
+ }
12170
+ return { processedCount, lastId, results };
12171
+ }
12172
+ /**
12173
+ * Compute deltas by iterating the in-memory LWWMap and filtering records
12174
+ * newer than the client's lastSyncTimestamp using HLC.compare().
12175
+ */
12176
+ async computeDeltas(syncMaps, principal, errors) {
12177
+ const deltas = [];
12178
+ for (const { mapName, lastSyncTimestamp } of syncMaps) {
12179
+ if (!this.config.securityManager.checkPermission(principal, mapName, "READ")) {
12180
+ errors.push({
12181
+ code: 403,
12182
+ message: "Access denied",
12183
+ context: `Read deltas for ${mapName}`
12184
+ });
12185
+ continue;
12186
+ }
12187
+ try {
12188
+ const map2 = await this.config.storageManager.getMapAsync(mapName);
12189
+ if (!(map2 instanceof import_core25.LWWMap)) {
12190
+ errors.push({
12191
+ code: 400,
12192
+ message: "HTTP sync only supports LWWMap deltas",
12193
+ context: `Map ${mapName} is not an LWWMap`
12194
+ });
12195
+ continue;
12196
+ }
12197
+ const records = [];
12198
+ for (const key of map2.allKeys()) {
12199
+ const record2 = map2.getRecord(key);
12200
+ if (!record2) continue;
12201
+ if (import_core25.HLC.compare(record2.timestamp, lastSyncTimestamp) > 0) {
12202
+ records.push({
12203
+ key,
12204
+ record: record2,
12205
+ eventType: record2.value === null ? "REMOVE" : "PUT"
12206
+ });
12207
+ }
12208
+ }
12209
+ const serverSyncTimestamp = this.config.hlc.now();
12210
+ deltas.push({
12211
+ mapName,
12212
+ records,
12213
+ serverSyncTimestamp
12214
+ });
12215
+ } catch (err) {
12216
+ logger.error({ err, mapName }, "HTTP sync delta computation failed");
12217
+ errors.push({
12218
+ code: 500,
12219
+ message: err.message || "Delta computation failed",
12220
+ context: `Map ${mapName}`
12221
+ });
12222
+ }
12223
+ }
12224
+ return deltas.length > 0 ? deltas : void 0;
12225
+ }
12226
+ /**
12227
+ * Execute one-shot queries.
12228
+ */
12229
+ async executeQueries(queries, principal, errors) {
12230
+ const results = [];
12231
+ for (const query of queries) {
12232
+ if (!this.config.securityManager.checkPermission(principal, query.mapName, "READ")) {
12233
+ errors.push({
12234
+ code: 403,
12235
+ message: "Access denied",
12236
+ context: `Query ${query.queryId} on ${query.mapName}`
12237
+ });
12238
+ continue;
12239
+ }
12240
+ try {
12241
+ const coreQuery = {
12242
+ where: query.filter,
12243
+ limit: query.limit
12244
+ };
12245
+ const queryResults = await this.config.queryConversionHandler.executeLocalQuery(
12246
+ query.mapName,
12247
+ coreQuery
12248
+ );
12249
+ const hasMore = query.limit ? queryResults.length >= query.limit : false;
12250
+ results.push({
12251
+ queryId: query.queryId,
12252
+ results: queryResults,
12253
+ hasMore
12254
+ });
12255
+ } catch (err) {
12256
+ logger.error({ err, queryId: query.queryId }, "HTTP sync query failed");
12257
+ errors.push({
12258
+ code: 500,
12259
+ message: err.message || "Query failed",
12260
+ context: `Query ${query.queryId} on ${query.mapName}`
12261
+ });
12262
+ }
12263
+ }
12264
+ return results.length > 0 ? results : void 0;
12265
+ }
12266
+ /**
12267
+ * Execute one-shot searches.
12268
+ */
12269
+ async executeSearches(searches, principal, errors) {
12270
+ const results = [];
12271
+ for (const search of searches) {
12272
+ if (!this.config.securityManager.checkPermission(principal, search.mapName, "READ")) {
12273
+ errors.push({
12274
+ code: 403,
12275
+ message: "Access denied",
12276
+ context: `Search ${search.searchId} on ${search.mapName}`
12277
+ });
12278
+ continue;
12279
+ }
12280
+ try {
12281
+ const searchResult = this.config.searchCoordinator.search(
12282
+ search.mapName,
12283
+ search.query,
12284
+ search.options
12285
+ );
12286
+ results.push({
12287
+ searchId: search.searchId,
12288
+ results: searchResult.results || [],
12289
+ totalCount: searchResult.totalCount
12290
+ });
12291
+ } catch (err) {
12292
+ logger.error({ err, searchId: search.searchId }, "HTTP sync search failed");
12293
+ errors.push({
12294
+ code: 500,
12295
+ message: err.message || "Search failed",
12296
+ context: `Search ${search.searchId} on ${search.mapName}`
12297
+ });
12298
+ }
12299
+ }
12300
+ return results.length > 0 ? results : void 0;
12301
+ }
12302
+ };
12303
+
12051
12304
  // src/coordinator/websocket-handler.ts
12052
12305
  var crypto2 = __toESM(require("crypto"));
12053
- var import_core26 = require("@topgunbuild/core");
12306
+ var import_core27 = require("@topgunbuild/core");
12054
12307
 
12055
12308
  // src/utils/CoalescingWriter.ts
12056
12309
  var import_ws7 = require("ws");
12057
- var import_core25 = require("@topgunbuild/core");
12310
+ var import_core26 = require("@topgunbuild/core");
12058
12311
  var DEFAULT_OPTIONS = {
12059
12312
  maxBatchSize: 100,
12060
12313
  maxDelayMs: 5,
@@ -12095,7 +12348,7 @@ var CoalescingWriter = class {
12095
12348
  if (this.closed) {
12096
12349
  return;
12097
12350
  }
12098
- const data = (0, import_core25.serialize)(message);
12351
+ const data = (0, import_core26.serialize)(message);
12099
12352
  this.writeRaw(data, urgent);
12100
12353
  }
12101
12354
  /**
@@ -12279,7 +12532,7 @@ var CoalescingWriter = class {
12279
12532
  offset += msg.data.length;
12280
12533
  }
12281
12534
  const usedBatch = batch.subarray(0, totalSize);
12282
- const batchEnvelope = (0, import_core25.serialize)({
12535
+ const batchEnvelope = (0, import_core26.serialize)({
12283
12536
  type: "BATCH",
12284
12537
  count: messages.length,
12285
12538
  data: usedBatch
@@ -12362,7 +12615,7 @@ var WebSocketHandler = class {
12362
12615
  buf = Buffer.from(message);
12363
12616
  }
12364
12617
  try {
12365
- data = (0, import_core26.deserialize)(buf);
12618
+ data = (0, import_core27.deserialize)(buf);
12366
12619
  } catch (e) {
12367
12620
  try {
12368
12621
  const text = Buffer.isBuffer(buf) ? buf.toString() : new TextDecoder().decode(buf);
@@ -12380,14 +12633,14 @@ var WebSocketHandler = class {
12380
12633
  ws.on("close", () => {
12381
12634
  this.handleDisconnect(connection);
12382
12635
  });
12383
- ws.send((0, import_core26.serialize)({ type: "AUTH_REQUIRED" }));
12636
+ ws.send((0, import_core27.serialize)({ type: "AUTH_REQUIRED" }));
12384
12637
  }
12385
12638
  /**
12386
12639
  * Handle incoming message from client.
12387
12640
  * Validates message, handles auth, and routes to appropriate handler.
12388
12641
  */
12389
12642
  async handleMessage(client, rawMessage) {
12390
- const parseResult = import_core26.MessageSchema.safeParse(rawMessage);
12643
+ const parseResult = import_core27.MessageSchema.safeParse(rawMessage);
12391
12644
  if (!parseResult.success) {
12392
12645
  this.config.rateLimitedLogger.error(
12393
12646
  `invalid-message:${client.id}`,
@@ -12481,7 +12734,7 @@ var WebSocketHandler = class {
12481
12734
  };
12482
12735
 
12483
12736
  // src/coordinator/lifecycle-manager.ts
12484
- var import_core27 = require("@topgunbuild/core");
12737
+ var import_core28 = require("@topgunbuild/core");
12485
12738
  var LifecycleManager = class {
12486
12739
  constructor(config2) {
12487
12740
  this.config = config2;
@@ -12527,7 +12780,7 @@ var LifecycleManager = class {
12527
12780
  this.config.metricsService.destroy();
12528
12781
  this.config.wss.close();
12529
12782
  logger.info(`Closing ${this.config.connectionManager.getClientCount()} client connections...`);
12530
- const shutdownMsg = (0, import_core27.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
12783
+ const shutdownMsg = (0, import_core28.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
12531
12784
  for (const client of this.config.connectionManager.getClients().values()) {
12532
12785
  try {
12533
12786
  if (client.socket.readyState === 1) {
@@ -12679,7 +12932,7 @@ var LifecycleManager = class {
12679
12932
  };
12680
12933
 
12681
12934
  // src/handlers/CounterHandler.ts
12682
- var import_core28 = require("@topgunbuild/core");
12935
+ var import_core29 = require("@topgunbuild/core");
12683
12936
  var CounterHandler = class {
12684
12937
  // counterName -> Set<clientId>
12685
12938
  constructor(nodeId = "server") {
@@ -12693,7 +12946,7 @@ var CounterHandler = class {
12693
12946
  getOrCreateCounter(name) {
12694
12947
  let counter = this.counters.get(name);
12695
12948
  if (!counter) {
12696
- counter = new import_core28.PNCounterImpl({ nodeId: this.nodeId });
12949
+ counter = new import_core29.PNCounterImpl({ nodeId: this.nodeId });
12697
12950
  this.counters.set(name, counter);
12698
12951
  logger.debug({ name }, "Created new counter");
12699
12952
  }
@@ -12827,10 +13080,10 @@ var CounterHandler = class {
12827
13080
  };
12828
13081
 
12829
13082
  // src/handlers/EntryProcessorHandler.ts
12830
- var import_core30 = require("@topgunbuild/core");
13083
+ var import_core31 = require("@topgunbuild/core");
12831
13084
 
12832
13085
  // src/ProcessorSandbox.ts
12833
- var import_core29 = require("@topgunbuild/core");
13086
+ var import_core30 = require("@topgunbuild/core");
12834
13087
  var ivm = null;
12835
13088
  try {
12836
13089
  ivm = require("isolated-vm");
@@ -12874,7 +13127,7 @@ var ProcessorSandbox = class {
12874
13127
  };
12875
13128
  }
12876
13129
  if (this.config.strictValidation) {
12877
- const validation = (0, import_core29.validateProcessorCode)(processor.code);
13130
+ const validation = (0, import_core30.validateProcessorCode)(processor.code);
12878
13131
  if (!validation.valid) {
12879
13132
  return {
12880
13133
  success: false,
@@ -13089,7 +13342,7 @@ var EntryProcessorHandler = class {
13089
13342
  * @returns Result with success status, processor result, and new value
13090
13343
  */
13091
13344
  async executeOnKey(map2, key, processorDef) {
13092
- const parseResult = import_core30.EntryProcessorDefSchema.safeParse(processorDef);
13345
+ const parseResult = import_core31.EntryProcessorDefSchema.safeParse(processorDef);
13093
13346
  if (!parseResult.success) {
13094
13347
  logger.warn(
13095
13348
  { key, error: parseResult.error.message },
@@ -13155,7 +13408,7 @@ var EntryProcessorHandler = class {
13155
13408
  async executeOnKeys(map2, keys, processorDef) {
13156
13409
  const results = /* @__PURE__ */ new Map();
13157
13410
  const timestamps = /* @__PURE__ */ new Map();
13158
- const parseResult = import_core30.EntryProcessorDefSchema.safeParse(processorDef);
13411
+ const parseResult = import_core31.EntryProcessorDefSchema.safeParse(processorDef);
13159
13412
  if (!parseResult.success) {
13160
13413
  const errorResult = {
13161
13414
  success: false,
@@ -13192,7 +13445,7 @@ var EntryProcessorHandler = class {
13192
13445
  async executeOnEntries(map2, processorDef, predicateCode) {
13193
13446
  const results = /* @__PURE__ */ new Map();
13194
13447
  const timestamps = /* @__PURE__ */ new Map();
13195
- const parseResult = import_core30.EntryProcessorDefSchema.safeParse(processorDef);
13448
+ const parseResult = import_core31.EntryProcessorDefSchema.safeParse(processorDef);
13196
13449
  if (!parseResult.success) {
13197
13450
  return { results, timestamps };
13198
13451
  }
@@ -13251,7 +13504,7 @@ var EntryProcessorHandler = class {
13251
13504
  };
13252
13505
 
13253
13506
  // src/ConflictResolverService.ts
13254
- var import_core31 = require("@topgunbuild/core");
13507
+ var import_core32 = require("@topgunbuild/core");
13255
13508
  var DEFAULT_CONFLICT_RESOLVER_CONFIG = {
13256
13509
  maxResolversPerMap: 100,
13257
13510
  enableSandboxedResolvers: true,
@@ -13282,7 +13535,7 @@ var ConflictResolverService = class {
13282
13535
  throw new Error("ConflictResolverService has been disposed");
13283
13536
  }
13284
13537
  if (resolver.code) {
13285
- const parsed = import_core31.ConflictResolverDefSchema.safeParse({
13538
+ const parsed = import_core32.ConflictResolverDefSchema.safeParse({
13286
13539
  name: resolver.name,
13287
13540
  code: resolver.code,
13288
13541
  priority: resolver.priority,
@@ -13291,7 +13544,7 @@ var ConflictResolverService = class {
13291
13544
  if (!parsed.success) {
13292
13545
  throw new Error(`Invalid resolver definition: ${parsed.error.message}`);
13293
13546
  }
13294
- const validation = (0, import_core31.validateResolverCode)(resolver.code);
13547
+ const validation = (0, import_core32.validateResolverCode)(resolver.code);
13295
13548
  if (!validation.valid) {
13296
13549
  throw new Error(`Invalid resolver code: ${validation.error}`);
13297
13550
  }
@@ -13353,7 +13606,7 @@ var ConflictResolverService = class {
13353
13606
  const entries = this.resolvers.get(context.mapName) ?? [];
13354
13607
  const allEntries = [
13355
13608
  ...entries,
13356
- { resolver: import_core31.BuiltInResolvers.LWW() }
13609
+ { resolver: import_core32.BuiltInResolvers.LWW() }
13357
13610
  ];
13358
13611
  for (const entry of allEntries) {
13359
13612
  const { resolver } = entry;
@@ -13809,7 +14062,7 @@ var TopicManager = class {
13809
14062
 
13810
14063
  // src/search/SearchCoordinator.ts
13811
14064
  var import_events13 = require("events");
13812
- var import_core32 = require("@topgunbuild/core");
14065
+ var import_core33 = require("@topgunbuild/core");
13813
14066
  var SearchCoordinator = class extends import_events13.EventEmitter {
13814
14067
  constructor() {
13815
14068
  super();
@@ -13879,7 +14132,7 @@ var SearchCoordinator = class extends import_events13.EventEmitter {
13879
14132
  logger.warn({ mapName }, "FTS already enabled for map, replacing index");
13880
14133
  this.indexes.delete(mapName);
13881
14134
  }
13882
- const index = new import_core32.FullTextIndex(config2);
14135
+ const index = new import_core33.FullTextIndex(config2);
13883
14136
  this.indexes.set(mapName, index);
13884
14137
  this.configs.set(mapName, config2);
13885
14138
  logger.info({ mapName, fields: config2.fields }, "FTS enabled for map");
@@ -14511,7 +14764,7 @@ var SearchCoordinator = class extends import_events13.EventEmitter {
14511
14764
 
14512
14765
  // src/search/ClusterSearchCoordinator.ts
14513
14766
  var import_events14 = require("events");
14514
- var import_core33 = require("@topgunbuild/core");
14767
+ var import_core34 = require("@topgunbuild/core");
14515
14768
  var DEFAULT_CONFIG6 = {
14516
14769
  rrfK: 60,
14517
14770
  defaultTimeoutMs: 5e3,
@@ -14526,7 +14779,7 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14526
14779
  this.partitionService = partitionService;
14527
14780
  this.localSearchCoordinator = localSearchCoordinator;
14528
14781
  this.config = { ...DEFAULT_CONFIG6, ...config2 };
14529
- this.rrf = new import_core33.ReciprocalRankFusion({ k: this.config.rrfK });
14782
+ this.rrf = new import_core34.ReciprocalRankFusion({ k: this.config.rrfK });
14530
14783
  this.metricsService = metricsService;
14531
14784
  this.clusterManager.on("message", this.handleClusterMessage.bind(this));
14532
14785
  }
@@ -14550,8 +14803,8 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14550
14803
  const perNodeLimit = this.calculatePerNodeLimit(options.limit, options.cursor);
14551
14804
  let cursorData = null;
14552
14805
  if (options.cursor) {
14553
- cursorData = import_core33.SearchCursor.decode(options.cursor);
14554
- if (cursorData && !import_core33.SearchCursor.isValid(cursorData, query)) {
14806
+ cursorData = import_core34.SearchCursor.decode(options.cursor);
14807
+ if (cursorData && !import_core34.SearchCursor.isValid(cursorData, query)) {
14555
14808
  cursorData = null;
14556
14809
  logger.warn({ requestId }, "Invalid or expired cursor, ignoring");
14557
14810
  }
@@ -14617,7 +14870,7 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14617
14870
  async handleSearchRequest(senderId, rawPayload) {
14618
14871
  const startTime = performance.now();
14619
14872
  const myNodeId = this.clusterManager.config.nodeId;
14620
- const parsed = import_core33.ClusterSearchReqPayloadSchema.safeParse(rawPayload);
14873
+ const parsed = import_core34.ClusterSearchReqPayloadSchema.safeParse(rawPayload);
14621
14874
  if (!parsed.success) {
14622
14875
  logger.warn(
14623
14876
  { senderId, error: parsed.error.message },
@@ -14684,7 +14937,7 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14684
14937
  * Handle search response from a node.
14685
14938
  */
14686
14939
  handleSearchResponse(_senderId, rawPayload) {
14687
- const parsed = import_core33.ClusterSearchRespPayloadSchema.safeParse(rawPayload);
14940
+ const parsed = import_core34.ClusterSearchRespPayloadSchema.safeParse(rawPayload);
14688
14941
  if (!parsed.success) {
14689
14942
  logger.warn(
14690
14943
  { error: parsed.error.message },
@@ -14770,7 +15023,7 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14770
15023
  }
14771
15024
  let nextCursor;
14772
15025
  if (merged.length > limit && cursorResults.length > 0) {
14773
- nextCursor = import_core33.SearchCursor.fromResults(cursorResults, pending.query);
15026
+ nextCursor = import_core34.SearchCursor.fromResults(cursorResults, pending.query);
14774
15027
  }
14775
15028
  const executionTimeMs = performance.now() - pending.startTime;
14776
15029
  if (this.metricsService) {
@@ -14836,7 +15089,7 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14836
15089
  });
14837
15090
  let results = localResult.results;
14838
15091
  if (cursorData) {
14839
- const position = import_core33.SearchCursor.getNodePosition(cursorData, myNodeId);
15092
+ const position = import_core34.SearchCursor.getNodePosition(cursorData, myNodeId);
14840
15093
  if (position) {
14841
15094
  results = results.filter((r) => {
14842
15095
  if (r.score < position.afterScore) {
@@ -14885,9 +15138,9 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14885
15138
  });
14886
15139
  let results = localResult.results;
14887
15140
  if (options.cursor) {
14888
- const cursorData = import_core33.SearchCursor.decode(options.cursor);
14889
- if (cursorData && import_core33.SearchCursor.isValid(cursorData, query)) {
14890
- const position = import_core33.SearchCursor.getNodePosition(cursorData, myNodeId);
15141
+ const cursorData = import_core34.SearchCursor.decode(options.cursor);
15142
+ if (cursorData && import_core34.SearchCursor.isValid(cursorData, query)) {
15143
+ const position = import_core34.SearchCursor.getNodePosition(cursorData, myNodeId);
14891
15144
  if (position) {
14892
15145
  results = results.filter((r) => {
14893
15146
  if (r.score < position.afterScore) {
@@ -14906,7 +15159,7 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14906
15159
  let nextCursor;
14907
15160
  if (totalCount > options.limit && results.length > 0) {
14908
15161
  const lastResult = results[results.length - 1];
14909
- nextCursor = import_core33.SearchCursor.fromResults(
15162
+ nextCursor = import_core34.SearchCursor.fromResults(
14910
15163
  [{ key: lastResult.key, score: lastResult.score, nodeId: myNodeId }],
14911
15164
  query
14912
15165
  );
@@ -14964,10 +15217,10 @@ var ClusterSearchCoordinator = class extends import_events14.EventEmitter {
14964
15217
 
14965
15218
  // src/subscriptions/DistributedSubscriptionCoordinator.ts
14966
15219
  var import_events16 = require("events");
14967
- var import_core35 = require("@topgunbuild/core");
15220
+ var import_core36 = require("@topgunbuild/core");
14968
15221
 
14969
15222
  // src/subscriptions/DistributedSearchCoordinator.ts
14970
- var import_core34 = require("@topgunbuild/core");
15223
+ var import_core35 = require("@topgunbuild/core");
14971
15224
 
14972
15225
  // src/subscriptions/DistributedSubscriptionBase.ts
14973
15226
  var import_events15 = require("events");
@@ -15291,7 +15544,7 @@ var DistributedSearchCoordinator = class extends DistributedSubscriptionBase {
15291
15544
  constructor(clusterManager, searchCoordinator, config2, metricsService, options) {
15292
15545
  super(clusterManager, config2, metricsService, options);
15293
15546
  this.localSearchCoordinator = searchCoordinator;
15294
- this.rrf = new import_core34.ReciprocalRankFusion({ k: this.config.rrfK });
15547
+ this.rrf = new import_core35.ReciprocalRankFusion({ k: this.config.rrfK });
15295
15548
  this.localSearchCoordinator.on("distributedUpdate", this.handleLocalSearchUpdate.bind(this));
15296
15549
  logger.debug("DistributedSearchCoordinator initialized");
15297
15550
  }
@@ -15823,7 +16076,7 @@ var DistributedSubscriptionCoordinator = class extends import_events16.EventEmit
15823
16076
  handleClusterMessage(msg) {
15824
16077
  switch (msg.type) {
15825
16078
  case "CLUSTER_SUB_REGISTER": {
15826
- const parsed = import_core35.ClusterSubRegisterPayloadSchema.safeParse(msg.payload);
16079
+ const parsed = import_core36.ClusterSubRegisterPayloadSchema.safeParse(msg.payload);
15827
16080
  if (!parsed.success) {
15828
16081
  logger.warn(
15829
16082
  { senderId: msg.senderId, error: parsed.error.message },
@@ -15835,7 +16088,7 @@ var DistributedSubscriptionCoordinator = class extends import_events16.EventEmit
15835
16088
  break;
15836
16089
  }
15837
16090
  case "CLUSTER_SUB_ACK": {
15838
- const parsed = import_core35.ClusterSubAckPayloadSchema.safeParse(msg.payload);
16091
+ const parsed = import_core36.ClusterSubAckPayloadSchema.safeParse(msg.payload);
15839
16092
  if (!parsed.success) {
15840
16093
  logger.warn(
15841
16094
  { senderId: msg.senderId, error: parsed.error.message },
@@ -15847,7 +16100,7 @@ var DistributedSubscriptionCoordinator = class extends import_events16.EventEmit
15847
16100
  break;
15848
16101
  }
15849
16102
  case "CLUSTER_SUB_UPDATE": {
15850
- const parsed = import_core35.ClusterSubUpdatePayloadSchema.safeParse(msg.payload);
16103
+ const parsed = import_core36.ClusterSubUpdatePayloadSchema.safeParse(msg.payload);
15851
16104
  if (!parsed.success) {
15852
16105
  logger.warn(
15853
16106
  { senderId: msg.senderId, error: parsed.error.message },
@@ -15859,7 +16112,7 @@ var DistributedSubscriptionCoordinator = class extends import_events16.EventEmit
15859
16112
  break;
15860
16113
  }
15861
16114
  case "CLUSTER_SUB_UNREGISTER": {
15862
- const parsed = import_core35.ClusterSubUnregisterPayloadSchema.safeParse(msg.payload);
16115
+ const parsed = import_core36.ClusterSubUnregisterPayloadSchema.safeParse(msg.payload);
15863
16116
  if (!parsed.success) {
15864
16117
  logger.warn(
15865
16118
  { senderId: msg.senderId, error: parsed.error.message },
@@ -15969,9 +16222,9 @@ var DistributedSubscriptionCoordinator = class extends import_events16.EventEmit
15969
16222
  };
15970
16223
 
15971
16224
  // src/EventJournalService.ts
15972
- var import_core36 = require("@topgunbuild/core");
16225
+ var import_core37 = require("@topgunbuild/core");
15973
16226
  var DEFAULT_JOURNAL_SERVICE_CONFIG = {
15974
- ...import_core36.DEFAULT_EVENT_JOURNAL_CONFIG,
16227
+ ...import_core37.DEFAULT_EVENT_JOURNAL_CONFIG,
15975
16228
  tableName: "event_journal",
15976
16229
  persistBatchSize: 100,
15977
16230
  persistIntervalMs: 1e3
@@ -15984,7 +16237,7 @@ function validateTableName(name) {
15984
16237
  );
15985
16238
  }
15986
16239
  }
15987
- var EventJournalService = class extends import_core36.EventJournalImpl {
16240
+ var EventJournalService = class extends import_core37.EventJournalImpl {
15988
16241
  constructor(config2) {
15989
16242
  super(config2);
15990
16243
  this.pendingPersist = [];
@@ -16631,7 +16884,7 @@ function createQueryHandlers(config2, deps, internal) {
16631
16884
  finalizeClusterQuery: (reqId, timeout) => queryConversionHandler.finalizeClusterQuery(reqId, timeout),
16632
16885
  pendingClusterQueries: internal.pendingClusterQueries,
16633
16886
  readReplicaHandler: deps.cluster.readReplicaHandler,
16634
- ConsistencyLevel: { EVENTUAL: import_core37.ConsistencyLevel.EVENTUAL }
16887
+ ConsistencyLevel: { EVENTUAL: import_core38.ConsistencyLevel.EVENTUAL }
16635
16888
  });
16636
16889
  return { queryHandler, queryConversionHandler };
16637
16890
  }
@@ -17028,7 +17281,7 @@ function createLifecycleModule(config2, deps) {
17028
17281
  }
17029
17282
 
17030
17283
  // src/debug/DebugEndpoints.ts
17031
- var import_core38 = require("@topgunbuild/core");
17284
+ var import_core39 = require("@topgunbuild/core");
17032
17285
  var DebugEndpoints = class {
17033
17286
  constructor(config2) {
17034
17287
  this.config = config2;
@@ -17116,7 +17369,7 @@ var DebugEndpoints = class {
17116
17369
  async handleCrdtExport(req, res) {
17117
17370
  try {
17118
17371
  const body = await this.parseBody(req);
17119
- const debugger_ = (0, import_core38.getCRDTDebugger)();
17372
+ const debugger_ = (0, import_core39.getCRDTDebugger)();
17120
17373
  const format = body.format || "json";
17121
17374
  const data = debugger_.exportHistory(format);
17122
17375
  const contentType = format === "csv" ? "text/csv" : format === "ndjson" ? "application/x-ndjson" : "application/json";
@@ -17130,7 +17383,7 @@ var DebugEndpoints = class {
17130
17383
  async handleCrdtStats(req, res) {
17131
17384
  try {
17132
17385
  const body = await this.parseBody(req);
17133
- const debugger_ = (0, import_core38.getCRDTDebugger)();
17386
+ const debugger_ = (0, import_core39.getCRDTDebugger)();
17134
17387
  const stats = debugger_.getStatistics(body.mapId);
17135
17388
  res.setHeader("Content-Type", "application/json");
17136
17389
  res.end(JSON.stringify(stats, null, 2));
@@ -17142,7 +17395,7 @@ var DebugEndpoints = class {
17142
17395
  async handleCrdtConflicts(req, res) {
17143
17396
  try {
17144
17397
  const body = await this.parseBody(req);
17145
- const debugger_ = (0, import_core38.getCRDTDebugger)();
17398
+ const debugger_ = (0, import_core39.getCRDTDebugger)();
17146
17399
  const conflicts = debugger_.getConflicts(body.mapId);
17147
17400
  res.setHeader("Content-Type", "application/json");
17148
17401
  res.end(JSON.stringify(conflicts, null, 2));
@@ -17154,7 +17407,7 @@ var DebugEndpoints = class {
17154
17407
  async handleCrdtOperations(req, res) {
17155
17408
  try {
17156
17409
  const body = await this.parseBody(req);
17157
- const debugger_ = (0, import_core38.getCRDTDebugger)();
17410
+ const debugger_ = (0, import_core39.getCRDTDebugger)();
17158
17411
  const operations = debugger_.getOperations({
17159
17412
  mapId: body.mapId,
17160
17413
  nodeId: body.nodeId,
@@ -17171,7 +17424,7 @@ var DebugEndpoints = class {
17171
17424
  async handleCrdtTimeline(req, res) {
17172
17425
  try {
17173
17426
  const body = await this.parseBody(req);
17174
- const debugger_ = (0, import_core38.getCRDTDebugger)();
17427
+ const debugger_ = (0, import_core39.getCRDTDebugger)();
17175
17428
  const intervalMs = body.intervalMs || 1e3;
17176
17429
  const timeline = debugger_.getTimeline(
17177
17430
  intervalMs,
@@ -17190,7 +17443,7 @@ var DebugEndpoints = class {
17190
17443
  async handleSearchExplain(req, res) {
17191
17444
  try {
17192
17445
  const body = await this.parseBody(req);
17193
- const debugger_ = (0, import_core38.getSearchDebugger)();
17446
+ const debugger_ = (0, import_core39.getSearchDebugger)();
17194
17447
  if (body.query) {
17195
17448
  const lastQuery2 = debugger_.getLastQuery();
17196
17449
  if (lastQuery2 && lastQuery2.query === body.query) {
@@ -17222,7 +17475,7 @@ var DebugEndpoints = class {
17222
17475
  }
17223
17476
  handleSearchStats(res) {
17224
17477
  try {
17225
- const debugger_ = (0, import_core38.getSearchDebugger)();
17478
+ const debugger_ = (0, import_core39.getSearchDebugger)();
17226
17479
  const stats = debugger_.getSearchStats();
17227
17480
  res.setHeader("Content-Type", "application/json");
17228
17481
  res.end(JSON.stringify(stats, null, 2));
@@ -17234,7 +17487,7 @@ var DebugEndpoints = class {
17234
17487
  async handleSearchHistory(req, res) {
17235
17488
  try {
17236
17489
  const body = await this.parseBody(req);
17237
- const debugger_ = (0, import_core38.getSearchDebugger)();
17490
+ const debugger_ = (0, import_core39.getSearchDebugger)();
17238
17491
  let history;
17239
17492
  if (body.mapId) {
17240
17493
  history = debugger_.getHistoryByMap(body.mapId);
@@ -17302,7 +17555,7 @@ var fs = __toESM(require("fs"));
17302
17555
  var path = __toESM(require("path"));
17303
17556
  var crypto3 = __toESM(require("crypto"));
17304
17557
  var jwt2 = __toESM(require("jsonwebtoken"));
17305
- var import_core39 = require("@topgunbuild/core");
17558
+ var import_core40 = require("@topgunbuild/core");
17306
17559
  var ENV_KEYS = {
17307
17560
  AUTO_SETUP: "TOPGUN_AUTO_SETUP",
17308
17561
  AUTO_SETUP_STRICT: "TOPGUN_AUTO_SETUP_STRICT",
@@ -17663,7 +17916,7 @@ var BootstrapController = class {
17663
17916
  const status = this.getClusterStatus();
17664
17917
  const transformedStatus = {
17665
17918
  ...status,
17666
- totalPartitions: import_core39.PARTITION_COUNT,
17919
+ totalPartitions: import_core40.PARTITION_COUNT,
17667
17920
  // Fixed partition count (see PHASE_14D_AUTH_SECURITY.md)
17668
17921
  nodes: status.nodes.map((node) => ({
17669
17922
  nodeId: node.id,
@@ -17680,7 +17933,7 @@ var BootstrapController = class {
17680
17933
  this.sendJson(res, 200, {
17681
17934
  nodes: [],
17682
17935
  partitions: [],
17683
- totalPartitions: import_core39.PARTITION_COUNT,
17936
+ totalPartitions: import_core40.PARTITION_COUNT,
17684
17937
  isRebalancing: false
17685
17938
  });
17686
17939
  }
@@ -17982,7 +18235,7 @@ function createBootstrapController(config2) {
17982
18235
  // src/settings/SettingsController.ts
17983
18236
  var fs2 = __toESM(require("fs"));
17984
18237
  var jwt3 = __toESM(require("jsonwebtoken"));
17985
- var import_core40 = require("@topgunbuild/core");
18238
+ var import_core41 = require("@topgunbuild/core");
17986
18239
  var HOT_RELOADABLE = /* @__PURE__ */ new Set([
17987
18240
  "logLevel",
17988
18241
  "metricsEnabled",
@@ -18095,7 +18348,7 @@ var SettingsController = class {
18095
18348
  mode: config2?.deploymentMode || process.env.TOPGUN_DEPLOYMENT_MODE || "standalone",
18096
18349
  nodeId: process.env.TOPGUN_NODE_ID || "node-1",
18097
18350
  peers: [],
18098
- partitionCount: import_core40.PARTITION_COUNT
18351
+ partitionCount: import_core41.PARTITION_COUNT
18099
18352
  },
18100
18353
  rateLimits: {
18101
18354
  connections: this.runtimeSettings.rateLimits.connections,
@@ -18567,6 +18820,25 @@ var ServerFactory = class _ServerFactory {
18567
18820
  journalSubscriptions
18568
18821
  }
18569
18822
  } = handlers;
18823
+ const httpSyncHandler = new HttpSyncHandler({
18824
+ authHandler,
18825
+ operationHandler,
18826
+ storageManager,
18827
+ queryConversionHandler,
18828
+ searchCoordinator,
18829
+ hlc,
18830
+ securityManager
18831
+ });
18832
+ if (network.setHttpRequestHandler) {
18833
+ network.setHttpRequestHandler((req, res) => {
18834
+ if (req.method === "POST" && req.url === "/sync") {
18835
+ _ServerFactory.handleHttpSync(req, res, httpSyncHandler);
18836
+ return;
18837
+ }
18838
+ res.writeHead(200);
18839
+ res.end(config2.tls?.enabled ? "TopGun Server Running (Secure)" : "TopGun Server Running");
18840
+ });
18841
+ }
18570
18842
  const lifecycle = createLifecycleModule(
18571
18843
  { nodeId: config2.nodeId },
18572
18844
  {
@@ -18669,6 +18941,70 @@ var ServerFactory = class _ServerFactory {
18669
18941
  }
18670
18942
  return coordinator;
18671
18943
  }
18944
+ /**
18945
+ * Handle an HTTP sync request by parsing the body, validating auth,
18946
+ * delegating to HttpSyncHandler, and serializing the response.
18947
+ */
18948
+ static handleHttpSync(req, res, handler) {
18949
+ const authHeader = req.headers["authorization"] || "";
18950
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
18951
+ if (!token) {
18952
+ res.writeHead(401, { "Content-Type": "application/json" });
18953
+ res.end(JSON.stringify({ error: "Missing Authorization header" }));
18954
+ return;
18955
+ }
18956
+ const chunks = [];
18957
+ req.on("data", (chunk) => chunks.push(chunk));
18958
+ req.on("end", async () => {
18959
+ try {
18960
+ const body = Buffer.concat(chunks);
18961
+ const contentType = req.headers["content-type"] || "";
18962
+ const isJson = contentType.includes("application/json");
18963
+ let parsed;
18964
+ if (isJson) {
18965
+ parsed = JSON.parse(body.toString("utf-8"));
18966
+ } else {
18967
+ parsed = (0, import_core42.deserialize)(body);
18968
+ }
18969
+ const validation = import_core42.HttpSyncRequestSchema.safeParse(parsed);
18970
+ if (!validation.success) {
18971
+ res.writeHead(400, { "Content-Type": "application/json" });
18972
+ res.end(JSON.stringify({
18973
+ error: "Invalid request body",
18974
+ details: validation.error.issues
18975
+ }));
18976
+ return;
18977
+ }
18978
+ const response = await handler.handleSyncRequest(validation.data, token);
18979
+ if (isJson) {
18980
+ res.writeHead(200, { "Content-Type": "application/json" });
18981
+ res.end(JSON.stringify(response));
18982
+ } else {
18983
+ const responseBytes = (0, import_core42.serialize)(response);
18984
+ res.writeHead(200, { "Content-Type": "application/x-msgpack" });
18985
+ res.end(Buffer.from(responseBytes));
18986
+ }
18987
+ } catch (err) {
18988
+ const message = err.message || "Internal server error";
18989
+ if (message.startsWith("401:")) {
18990
+ res.writeHead(401, { "Content-Type": "application/json" });
18991
+ res.end(JSON.stringify({ error: message.slice(5).trim() }));
18992
+ } else if (message.startsWith("403:")) {
18993
+ res.writeHead(403, { "Content-Type": "application/json" });
18994
+ res.end(JSON.stringify({ error: message.slice(5).trim() }));
18995
+ } else {
18996
+ logger.error({ err }, "HTTP sync request failed");
18997
+ res.writeHead(500, { "Content-Type": "application/json" });
18998
+ res.end(JSON.stringify({ error: "Internal server error" }));
18999
+ }
19000
+ }
19001
+ });
19002
+ req.on("error", (err) => {
19003
+ logger.error({ err }, "HTTP sync request stream error");
19004
+ res.writeHead(400, { "Content-Type": "application/json" });
19005
+ res.end(JSON.stringify({ error: "Request stream error" }));
19006
+ });
19007
+ }
18672
19008
  static createMetricsServer(bootstrap, settings, debug, metrics) {
18673
19009
  const server = (0, import_http.createServer)(async (req, res) => {
18674
19010
  const bootstrapHandled = await bootstrap.handle(req, res);
@@ -18699,7 +19035,7 @@ var ServerFactory = class _ServerFactory {
18699
19035
  const memberIds = cluster.getMembers();
18700
19036
  const nodes = memberIds.map((nodeId) => {
18701
19037
  let partitionCount = 0;
18702
- for (let i = 0; i < import_core41.PARTITION_COUNT; i++) {
19038
+ for (let i = 0; i < import_core42.PARTITION_COUNT; i++) {
18703
19039
  if (partitionService.getPartitionOwner(i) === nodeId) partitionCount++;
18704
19040
  }
18705
19041
  return {
@@ -18713,7 +19049,7 @@ var ServerFactory = class _ServerFactory {
18713
19049
  };
18714
19050
  });
18715
19051
  const partitions = [];
18716
- for (let i = 0; i < import_core41.PARTITION_COUNT; i++) {
19052
+ for (let i = 0; i < import_core42.PARTITION_COUNT; i++) {
18717
19053
  const owner = partitionService.getPartitionOwner(i);
18718
19054
  const backups = partitionService.getBackups(i);
18719
19055
  partitions.push({