@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.
@@ -24370,17 +24370,18 @@ function buildTLSOptions(config2) {
24370
24370
  return options;
24371
24371
  }
24372
24372
  function createNetworkModule(config2, _deps) {
24373
+ let currentRequestHandler = (_req, res) => {
24374
+ res.writeHead(200);
24375
+ res.end(config2.tls?.enabled ? "TopGun Server Running (Secure)" : "TopGun Server Running");
24376
+ };
24377
+ const requestDispatcher = (req, res) => {
24378
+ currentRequestHandler(req, res);
24379
+ };
24373
24380
  let httpServer;
24374
24381
  if (config2.tls?.enabled) {
24375
- httpServer = createHttpsServer(buildTLSOptions(config2.tls), (_req, res) => {
24376
- res.writeHead(200);
24377
- res.end("TopGun Server Running (Secure)");
24378
- });
24382
+ httpServer = createHttpsServer(buildTLSOptions(config2.tls), requestDispatcher);
24379
24383
  } else {
24380
- httpServer = createHttpServer((_req, res) => {
24381
- res.writeHead(200);
24382
- res.end("TopGun Server Running");
24383
- });
24384
+ httpServer = createHttpServer(requestDispatcher);
24384
24385
  }
24385
24386
  httpServer.maxConnections = config2.maxConnections ?? 1e4;
24386
24387
  httpServer.timeout = config2.serverTimeout ?? 12e4;
@@ -24418,6 +24419,11 @@ function createNetworkModule(config2, _deps) {
24418
24419
  httpServer.listen(config2.port, () => {
24419
24420
  logger.info({ port: config2.port }, "Server Coordinator listening");
24420
24421
  });
24422
+ },
24423
+ // Deferred wiring: allows ServerFactory to inject the /sync handler
24424
+ // after HttpSyncHandler is assembled
24425
+ setHttpRequestHandler: (handler) => {
24426
+ currentRequestHandler = handler;
24421
24427
  }
24422
24428
  };
24423
24429
  }
@@ -27480,6 +27486,253 @@ var QueryHandler = class {
27480
27486
  }
27481
27487
  };
27482
27488
 
27489
+ // src/coordinator/http-sync-handler.ts
27490
+ import { HLC as HLC8, LWWMap as LWWMap10 } from "@topgunbuild/core";
27491
+ var HttpSyncHandler = class {
27492
+ constructor(config2) {
27493
+ this.config = config2;
27494
+ }
27495
+ /**
27496
+ * Process a complete HTTP sync request and return a response.
27497
+ *
27498
+ * @param request - Parsed and validated HttpSyncRequest body
27499
+ * @param authToken - JWT token from Authorization header
27500
+ * @returns HttpSyncResponse with acks, deltas, query/search results
27501
+ * @throws Error with message starting with '401:' for auth failures
27502
+ * @throws Error with message starting with '403:' for permission failures
27503
+ */
27504
+ async handleSyncRequest(request, authToken) {
27505
+ let principal;
27506
+ try {
27507
+ principal = this.config.authHandler.verifyToken(authToken);
27508
+ } catch (err) {
27509
+ throw new Error(`401: Authentication failed: ${err.message}`);
27510
+ }
27511
+ this.config.hlc.update({
27512
+ millis: request.clientHlc.millis,
27513
+ counter: request.clientHlc.counter,
27514
+ nodeId: request.clientHlc.nodeId
27515
+ });
27516
+ const response = {
27517
+ serverHlc: this.config.hlc.now()
27518
+ };
27519
+ const errors = [];
27520
+ if (request.operations && request.operations.length > 0) {
27521
+ const ackResults = await this.processOperations(request.operations, principal, errors);
27522
+ if (ackResults.processedCount > 0) {
27523
+ response.ack = {
27524
+ lastId: ackResults.lastId,
27525
+ results: ackResults.results.length > 0 ? ackResults.results : void 0
27526
+ };
27527
+ }
27528
+ }
27529
+ if (request.syncMaps && request.syncMaps.length > 0) {
27530
+ response.deltas = await this.computeDeltas(request.syncMaps, principal, errors);
27531
+ }
27532
+ if (request.queries && request.queries.length > 0) {
27533
+ response.queryResults = await this.executeQueries(request.queries, principal, errors);
27534
+ }
27535
+ if (request.searches && request.searches.length > 0) {
27536
+ response.searchResults = await this.executeSearches(request.searches, principal, errors);
27537
+ }
27538
+ if (errors.length > 0) {
27539
+ response.errors = errors;
27540
+ }
27541
+ response.serverHlc = this.config.hlc.now();
27542
+ return response;
27543
+ }
27544
+ /**
27545
+ * Process a batch of client operations.
27546
+ */
27547
+ async processOperations(operations, principal, errors) {
27548
+ let lastId = "";
27549
+ let processedCount = 0;
27550
+ const results = [];
27551
+ for (const op of operations) {
27552
+ const opId = op.id || `http-op-${processedCount}`;
27553
+ const isRemove = op.opType === "REMOVE" || op.record && op.record.value === null;
27554
+ const action = isRemove ? "REMOVE" : "PUT";
27555
+ if (!this.config.securityManager.checkPermission(principal, op.mapName, action)) {
27556
+ errors.push({
27557
+ code: 403,
27558
+ message: "Access denied",
27559
+ context: `Operation on ${op.mapName}/${op.key}`
27560
+ });
27561
+ results.push({
27562
+ opId,
27563
+ success: false,
27564
+ achievedLevel: "FIRE_AND_FORGET",
27565
+ error: "Access denied"
27566
+ });
27567
+ continue;
27568
+ }
27569
+ try {
27570
+ const result = await this.config.operationHandler.applyOpToMap(op);
27571
+ if (result.rejected) {
27572
+ results.push({
27573
+ opId,
27574
+ success: false,
27575
+ achievedLevel: "FIRE_AND_FORGET",
27576
+ error: "Operation rejected by conflict resolver"
27577
+ });
27578
+ } else {
27579
+ results.push({
27580
+ opId,
27581
+ success: true,
27582
+ achievedLevel: "MEMORY"
27583
+ });
27584
+ }
27585
+ processedCount++;
27586
+ lastId = opId;
27587
+ } catch (err) {
27588
+ logger.error({ err, opId }, "HTTP sync operation failed");
27589
+ errors.push({
27590
+ code: 500,
27591
+ message: err.message || "Operation failed",
27592
+ context: `Operation ${opId} on ${op.mapName}/${op.key}`
27593
+ });
27594
+ results.push({
27595
+ opId,
27596
+ success: false,
27597
+ achievedLevel: "FIRE_AND_FORGET",
27598
+ error: err.message || "Operation failed"
27599
+ });
27600
+ }
27601
+ }
27602
+ return { processedCount, lastId, results };
27603
+ }
27604
+ /**
27605
+ * Compute deltas by iterating the in-memory LWWMap and filtering records
27606
+ * newer than the client's lastSyncTimestamp using HLC.compare().
27607
+ */
27608
+ async computeDeltas(syncMaps, principal, errors) {
27609
+ const deltas = [];
27610
+ for (const { mapName, lastSyncTimestamp } of syncMaps) {
27611
+ if (!this.config.securityManager.checkPermission(principal, mapName, "READ")) {
27612
+ errors.push({
27613
+ code: 403,
27614
+ message: "Access denied",
27615
+ context: `Read deltas for ${mapName}`
27616
+ });
27617
+ continue;
27618
+ }
27619
+ try {
27620
+ const map2 = await this.config.storageManager.getMapAsync(mapName);
27621
+ if (!(map2 instanceof LWWMap10)) {
27622
+ errors.push({
27623
+ code: 400,
27624
+ message: "HTTP sync only supports LWWMap deltas",
27625
+ context: `Map ${mapName} is not an LWWMap`
27626
+ });
27627
+ continue;
27628
+ }
27629
+ const records = [];
27630
+ for (const key of map2.allKeys()) {
27631
+ const record2 = map2.getRecord(key);
27632
+ if (!record2) continue;
27633
+ if (HLC8.compare(record2.timestamp, lastSyncTimestamp) > 0) {
27634
+ records.push({
27635
+ key,
27636
+ record: record2,
27637
+ eventType: record2.value === null ? "REMOVE" : "PUT"
27638
+ });
27639
+ }
27640
+ }
27641
+ const serverSyncTimestamp = this.config.hlc.now();
27642
+ deltas.push({
27643
+ mapName,
27644
+ records,
27645
+ serverSyncTimestamp
27646
+ });
27647
+ } catch (err) {
27648
+ logger.error({ err, mapName }, "HTTP sync delta computation failed");
27649
+ errors.push({
27650
+ code: 500,
27651
+ message: err.message || "Delta computation failed",
27652
+ context: `Map ${mapName}`
27653
+ });
27654
+ }
27655
+ }
27656
+ return deltas.length > 0 ? deltas : void 0;
27657
+ }
27658
+ /**
27659
+ * Execute one-shot queries.
27660
+ */
27661
+ async executeQueries(queries, principal, errors) {
27662
+ const results = [];
27663
+ for (const query of queries) {
27664
+ if (!this.config.securityManager.checkPermission(principal, query.mapName, "READ")) {
27665
+ errors.push({
27666
+ code: 403,
27667
+ message: "Access denied",
27668
+ context: `Query ${query.queryId} on ${query.mapName}`
27669
+ });
27670
+ continue;
27671
+ }
27672
+ try {
27673
+ const coreQuery = {
27674
+ where: query.filter,
27675
+ limit: query.limit
27676
+ };
27677
+ const queryResults = await this.config.queryConversionHandler.executeLocalQuery(
27678
+ query.mapName,
27679
+ coreQuery
27680
+ );
27681
+ const hasMore = query.limit ? queryResults.length >= query.limit : false;
27682
+ results.push({
27683
+ queryId: query.queryId,
27684
+ results: queryResults,
27685
+ hasMore
27686
+ });
27687
+ } catch (err) {
27688
+ logger.error({ err, queryId: query.queryId }, "HTTP sync query failed");
27689
+ errors.push({
27690
+ code: 500,
27691
+ message: err.message || "Query failed",
27692
+ context: `Query ${query.queryId} on ${query.mapName}`
27693
+ });
27694
+ }
27695
+ }
27696
+ return results.length > 0 ? results : void 0;
27697
+ }
27698
+ /**
27699
+ * Execute one-shot searches.
27700
+ */
27701
+ async executeSearches(searches, principal, errors) {
27702
+ const results = [];
27703
+ for (const search of searches) {
27704
+ if (!this.config.securityManager.checkPermission(principal, search.mapName, "READ")) {
27705
+ errors.push({
27706
+ code: 403,
27707
+ message: "Access denied",
27708
+ context: `Search ${search.searchId} on ${search.mapName}`
27709
+ });
27710
+ continue;
27711
+ }
27712
+ try {
27713
+ const searchResult = this.config.searchCoordinator.search(
27714
+ search.mapName,
27715
+ search.query,
27716
+ search.options
27717
+ );
27718
+ results.push({
27719
+ searchId: search.searchId,
27720
+ results: searchResult.results || [],
27721
+ totalCount: searchResult.totalCount
27722
+ });
27723
+ } catch (err) {
27724
+ logger.error({ err, searchId: search.searchId }, "HTTP sync search failed");
27725
+ errors.push({
27726
+ code: 500,
27727
+ message: err.message || "Search failed",
27728
+ context: `Search ${search.searchId} on ${search.mapName}`
27729
+ });
27730
+ }
27731
+ }
27732
+ return results.length > 0 ? results : void 0;
27733
+ }
27734
+ };
27735
+
27483
27736
  // src/coordinator/websocket-handler.ts
27484
27737
  import * as crypto2 from "crypto";
27485
27738
  import { serialize as serialize5, deserialize, MessageSchema } from "@topgunbuild/core";
@@ -30557,7 +30810,7 @@ function createBootstrapController(config2) {
30557
30810
 
30558
30811
  // src/ServerFactory.ts
30559
30812
  import { createServer as createHttpServer2 } from "http";
30560
- import { PARTITION_COUNT as PARTITION_COUNT7 } from "@topgunbuild/core";
30813
+ import { PARTITION_COUNT as PARTITION_COUNT7, serialize as serialize7, deserialize as deserialize2, HttpSyncRequestSchema } from "@topgunbuild/core";
30561
30814
 
30562
30815
  // src/debug/DebugEndpoints.ts
30563
30816
  import {
@@ -31420,6 +31673,25 @@ var ServerFactory = class _ServerFactory {
31420
31673
  journalSubscriptions
31421
31674
  }
31422
31675
  } = handlers;
31676
+ const httpSyncHandler = new HttpSyncHandler({
31677
+ authHandler,
31678
+ operationHandler,
31679
+ storageManager,
31680
+ queryConversionHandler,
31681
+ searchCoordinator,
31682
+ hlc,
31683
+ securityManager
31684
+ });
31685
+ if (network.setHttpRequestHandler) {
31686
+ network.setHttpRequestHandler((req, res) => {
31687
+ if (req.method === "POST" && req.url === "/sync") {
31688
+ _ServerFactory.handleHttpSync(req, res, httpSyncHandler);
31689
+ return;
31690
+ }
31691
+ res.writeHead(200);
31692
+ res.end(config2.tls?.enabled ? "TopGun Server Running (Secure)" : "TopGun Server Running");
31693
+ });
31694
+ }
31423
31695
  const lifecycle = createLifecycleModule(
31424
31696
  { nodeId: config2.nodeId },
31425
31697
  {
@@ -31522,6 +31794,70 @@ var ServerFactory = class _ServerFactory {
31522
31794
  }
31523
31795
  return coordinator;
31524
31796
  }
31797
+ /**
31798
+ * Handle an HTTP sync request by parsing the body, validating auth,
31799
+ * delegating to HttpSyncHandler, and serializing the response.
31800
+ */
31801
+ static handleHttpSync(req, res, handler) {
31802
+ const authHeader = req.headers["authorization"] || "";
31803
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
31804
+ if (!token) {
31805
+ res.writeHead(401, { "Content-Type": "application/json" });
31806
+ res.end(JSON.stringify({ error: "Missing Authorization header" }));
31807
+ return;
31808
+ }
31809
+ const chunks = [];
31810
+ req.on("data", (chunk) => chunks.push(chunk));
31811
+ req.on("end", async () => {
31812
+ try {
31813
+ const body = Buffer.concat(chunks);
31814
+ const contentType = req.headers["content-type"] || "";
31815
+ const isJson = contentType.includes("application/json");
31816
+ let parsed;
31817
+ if (isJson) {
31818
+ parsed = JSON.parse(body.toString("utf-8"));
31819
+ } else {
31820
+ parsed = deserialize2(body);
31821
+ }
31822
+ const validation = HttpSyncRequestSchema.safeParse(parsed);
31823
+ if (!validation.success) {
31824
+ res.writeHead(400, { "Content-Type": "application/json" });
31825
+ res.end(JSON.stringify({
31826
+ error: "Invalid request body",
31827
+ details: validation.error.issues
31828
+ }));
31829
+ return;
31830
+ }
31831
+ const response = await handler.handleSyncRequest(validation.data, token);
31832
+ if (isJson) {
31833
+ res.writeHead(200, { "Content-Type": "application/json" });
31834
+ res.end(JSON.stringify(response));
31835
+ } else {
31836
+ const responseBytes = serialize7(response);
31837
+ res.writeHead(200, { "Content-Type": "application/x-msgpack" });
31838
+ res.end(Buffer.from(responseBytes));
31839
+ }
31840
+ } catch (err) {
31841
+ const message = err.message || "Internal server error";
31842
+ if (message.startsWith("401:")) {
31843
+ res.writeHead(401, { "Content-Type": "application/json" });
31844
+ res.end(JSON.stringify({ error: message.slice(5).trim() }));
31845
+ } else if (message.startsWith("403:")) {
31846
+ res.writeHead(403, { "Content-Type": "application/json" });
31847
+ res.end(JSON.stringify({ error: message.slice(5).trim() }));
31848
+ } else {
31849
+ logger.error({ err }, "HTTP sync request failed");
31850
+ res.writeHead(500, { "Content-Type": "application/json" });
31851
+ res.end(JSON.stringify({ error: "Internal server error" }));
31852
+ }
31853
+ }
31854
+ });
31855
+ req.on("error", (err) => {
31856
+ logger.error({ err }, "HTTP sync request stream error");
31857
+ res.writeHead(400, { "Content-Type": "application/json" });
31858
+ res.end(JSON.stringify({ error: "Request stream error" }));
31859
+ });
31860
+ }
31525
31861
  static createMetricsServer(bootstrap, settings, debug, metrics) {
31526
31862
  const server = createHttpServer2(async (req, res) => {
31527
31863
  const bootstrapHandled = await bootstrap.handle(req, res);
@@ -31657,4 +31993,4 @@ export {
31657
31993
  createBootstrapController,
31658
31994
  ServerFactory
31659
31995
  };
31660
- //# sourceMappingURL=chunk-IQNKZPW3.mjs.map
31996
+ //# sourceMappingURL=chunk-Z3TONATT.mjs.map