@malloy-publisher/server 0.0.204 → 0.0.205

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/build.ts +10 -1
  2. package/dist/app/api-doc.yaml +133 -4
  3. package/dist/app/assets/{EnvironmentPage-CX06cjOF.js → EnvironmentPage-CAge6UHD.js} +1 -1
  4. package/dist/app/assets/HomePage-DhTe8qpa.js +1 -0
  5. package/dist/app/assets/{MainPage-nUJ9YatG.js → MainPage-CeTxxGex.js} +2 -2
  6. package/dist/app/assets/MaterializationsPage-CpDHB70t.js +1 -0
  7. package/dist/app/assets/ModelPage-D9sSMb75.js +1 -0
  8. package/dist/app/assets/{PackagePage-BaEVdEAG.js → PackagePage-LRqQWrFY.js} +1 -1
  9. package/dist/app/assets/{RouteError-BShQjZio.js → RouteError-xT6kuCNw.js} +1 -1
  10. package/dist/app/assets/{WorkbookPage-CBn6ZjJW.js → WorkbookPage-DsIh9svZ.js} +1 -1
  11. package/dist/app/assets/{core-DECXYL4E.es-OaRfXwuQ.js → core-C2sQrwVu.es-Bjem0hym.js} +1 -1
  12. package/dist/app/assets/{index-BLfPC1gy.js → index-BdOZDcce.js} +1 -1
  13. package/dist/app/assets/{index-Dy3YhAZQ.js → index-DHHAcY5o.js} +1 -1
  14. package/dist/app/assets/{index-DqiJ0bWp.js → index-RX3QOTde.js} +121 -121
  15. package/dist/app/assets/{index.umd-DAN9K8yC.js → index.umd-D2WH3D-f.js} +1 -1
  16. package/dist/app/index.html +1 -1
  17. package/dist/runtime/publisher.js +318 -0
  18. package/dist/server.mjs +567 -194
  19. package/package.json +5 -4
  20. package/scripts/bake-duckdb-extensions.js +104 -0
  21. package/src/controller/watch-mode.controller.ts +176 -46
  22. package/src/errors.spec.ts +21 -0
  23. package/src/mcp/error_messages.spec.ts +35 -0
  24. package/src/mcp/error_messages.ts +14 -1
  25. package/src/mcp/handler_utils.ts +12 -0
  26. package/src/runtime/publisher.js +318 -0
  27. package/src/server.ts +479 -2
  28. package/src/service/authorize_integration.spec.ts +96 -2
  29. package/src/service/compile_authorize.spec.ts +85 -0
  30. package/src/service/environment.ts +63 -5
  31. package/src/service/environment_store.ts +142 -11
  32. package/src/service/model.ts +44 -0
  33. package/src/service/package.ts +17 -6
  34. package/src/storage/duckdb/DuckDBConnection.ts +70 -124
  35. package/tests/fixtures/authorize-compile/model.malloy +9 -0
  36. package/tests/fixtures/authorize-compile/publisher.json +4 -0
  37. package/tests/fixtures/html-pages-nopublic/model.malloy +1 -0
  38. package/tests/fixtures/html-pages-nopublic/publisher.json +5 -0
  39. package/tests/fixtures/html-pages-test/data.csv +3 -0
  40. package/tests/fixtures/html-pages-test/public/assets/app.css +3 -0
  41. package/tests/fixtures/html-pages-test/public/data.json +1 -0
  42. package/tests/fixtures/html-pages-test/public/index.html +9 -0
  43. package/tests/fixtures/html-pages-test/public/sub/page2.html +9 -0
  44. package/tests/fixtures/html-pages-test/publisher.json +5 -0
  45. package/tests/fixtures/html-pages-test/report.malloy +1 -0
  46. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +92 -0
  47. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +138 -0
  48. package/tests/integration/html_pages/html_pages.integration.spec.ts +378 -0
  49. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +421 -0
  50. package/tests/unit/duckdb/attached_databases.test.ts +111 -0
  51. package/tests/unit/duckdb/duckdb_connection.test.ts +181 -0
  52. package/tests/unit/duckdb/repositories.test.ts +208 -0
  53. package/dist/app/assets/HomePage-CNFt_eUU.js +0 -1
  54. package/dist/app/assets/MaterializationsPage-B5goxVXW.js +0 -1
  55. package/dist/app/assets/ModelPage-Ba7Xh4lL.js +0 -1
package/dist/server.mjs CHANGED
@@ -200925,8 +200925,8 @@ var require_uri_all = __commonJS((exports, module) => {
200925
200925
  wsComponents.secure = undefined;
200926
200926
  }
200927
200927
  if (wsComponents.resourceName) {
200928
- var _wsComponents$resourc = wsComponents.resourceName.split("?"), _wsComponents$resourc2 = slicedToArray(_wsComponents$resourc, 2), path10 = _wsComponents$resourc2[0], query = _wsComponents$resourc2[1];
200929
- wsComponents.path = path10 && path10 !== "/" ? path10 : undefined;
200928
+ var _wsComponents$resourc = wsComponents.resourceName.split("?"), _wsComponents$resourc2 = slicedToArray(_wsComponents$resourc, 2), path11 = _wsComponents$resourc2[0], query = _wsComponents$resourc2[1];
200929
+ wsComponents.path = path11 && path11 !== "/" ? path11 : undefined;
200930
200930
  wsComponents.query = query;
200931
200931
  wsComponents.resourceName = undefined;
200932
200932
  }
@@ -201319,12 +201319,12 @@ var require_util12 = __commonJS((exports, module) => {
201319
201319
  return "'" + escapeQuotes(str) + "'";
201320
201320
  }
201321
201321
  function getPathExpr(currentPath, expr, jsonPointers, isNumber2) {
201322
- var path10 = jsonPointers ? "'/' + " + expr + (isNumber2 ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") : isNumber2 ? "'[' + " + expr + " + ']'" : "'[\\'' + " + expr + " + '\\']'";
201323
- return joinPaths(currentPath, path10);
201322
+ var path11 = jsonPointers ? "'/' + " + expr + (isNumber2 ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") : isNumber2 ? "'[' + " + expr + " + ']'" : "'[\\'' + " + expr + " + '\\']'";
201323
+ return joinPaths(currentPath, path11);
201324
201324
  }
201325
201325
  function getPath(currentPath, prop, jsonPointers) {
201326
- var path10 = jsonPointers ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop));
201327
- return joinPaths(currentPath, path10);
201326
+ var path11 = jsonPointers ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop));
201327
+ return joinPaths(currentPath, path11);
201328
201328
  }
201329
201329
  var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/;
201330
201330
  var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/;
@@ -207378,11 +207378,11 @@ var require_ast = __commonJS((exports, module) => {
207378
207378
  helperExpression: function helperExpression(node) {
207379
207379
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
207380
207380
  },
207381
- scopedId: function scopedId(path10) {
207382
- return /^\.|this\b/.test(path10.original);
207381
+ scopedId: function scopedId(path11) {
207382
+ return /^\.|this\b/.test(path11.original);
207383
207383
  },
207384
- simpleId: function simpleId(path10) {
207385
- return path10.parts.length === 1 && !AST.helpers.scopedId(path10) && !path10.depth;
207384
+ simpleId: function simpleId(path11) {
207385
+ return path11.parts.length === 1 && !AST.helpers.scopedId(path11) && !path11.depth;
207386
207386
  }
207387
207387
  }
207388
207388
  };
@@ -208442,12 +208442,12 @@ var require_helpers3 = __commonJS((exports) => {
208442
208442
  loc
208443
208443
  };
208444
208444
  }
208445
- function prepareMustache(path10, params, hash, open2, strip, locInfo) {
208445
+ function prepareMustache(path11, params, hash, open2, strip, locInfo) {
208446
208446
  var escapeFlag = open2.charAt(3) || open2.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
208447
208447
  var decorator = /\*/.test(open2);
208448
208448
  return {
208449
208449
  type: decorator ? "Decorator" : "MustacheStatement",
208450
- path: path10,
208450
+ path: path11,
208451
208451
  params,
208452
208452
  hash,
208453
208453
  escaped,
@@ -208711,9 +208711,9 @@ var require_compiler = __commonJS((exports) => {
208711
208711
  },
208712
208712
  DecoratorBlock: function DecoratorBlock(decorator) {
208713
208713
  var program = decorator.program && this.compileProgram(decorator.program);
208714
- var params = this.setupFullMustacheParams(decorator, program, undefined), path10 = decorator.path;
208714
+ var params = this.setupFullMustacheParams(decorator, program, undefined), path11 = decorator.path;
208715
208715
  this.useDecorators = true;
208716
- this.opcode("registerDecorator", params.length, path10.original);
208716
+ this.opcode("registerDecorator", params.length, path11.original);
208717
208717
  },
208718
208718
  PartialStatement: function PartialStatement(partial) {
208719
208719
  this.usePartial = true;
@@ -208776,46 +208776,46 @@ var require_compiler = __commonJS((exports) => {
208776
208776
  }
208777
208777
  },
208778
208778
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
208779
- var path10 = sexpr.path, name = path10.parts[0], isBlock = program != null || inverse != null;
208780
- this.opcode("getContext", path10.depth);
208779
+ var path11 = sexpr.path, name = path11.parts[0], isBlock = program != null || inverse != null;
208780
+ this.opcode("getContext", path11.depth);
208781
208781
  this.opcode("pushProgram", program);
208782
208782
  this.opcode("pushProgram", inverse);
208783
- path10.strict = true;
208784
- this.accept(path10);
208783
+ path11.strict = true;
208784
+ this.accept(path11);
208785
208785
  this.opcode("invokeAmbiguous", name, isBlock);
208786
208786
  },
208787
208787
  simpleSexpr: function simpleSexpr(sexpr) {
208788
- var path10 = sexpr.path;
208789
- path10.strict = true;
208790
- this.accept(path10);
208788
+ var path11 = sexpr.path;
208789
+ path11.strict = true;
208790
+ this.accept(path11);
208791
208791
  this.opcode("resolvePossibleLambda");
208792
208792
  },
208793
208793
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
208794
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path10 = sexpr.path, name = path10.parts[0];
208794
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path11 = sexpr.path, name = path11.parts[0];
208795
208795
  if (this.options.knownHelpers[name]) {
208796
208796
  this.opcode("invokeKnownHelper", params.length, name);
208797
208797
  } else if (this.options.knownHelpersOnly) {
208798
208798
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
208799
208799
  } else {
208800
- path10.strict = true;
208801
- path10.falsy = true;
208802
- this.accept(path10);
208803
- this.opcode("invokeHelper", params.length, path10.original, _ast2["default"].helpers.simpleId(path10));
208800
+ path11.strict = true;
208801
+ path11.falsy = true;
208802
+ this.accept(path11);
208803
+ this.opcode("invokeHelper", params.length, path11.original, _ast2["default"].helpers.simpleId(path11));
208804
208804
  }
208805
208805
  },
208806
- PathExpression: function PathExpression(path10) {
208807
- this.addDepth(path10.depth);
208808
- this.opcode("getContext", path10.depth);
208809
- var name = path10.parts[0], scoped = _ast2["default"].helpers.scopedId(path10), blockParamId = !path10.depth && !scoped && this.blockParamIndex(name);
208806
+ PathExpression: function PathExpression(path11) {
208807
+ this.addDepth(path11.depth);
208808
+ this.opcode("getContext", path11.depth);
208809
+ var name = path11.parts[0], scoped = _ast2["default"].helpers.scopedId(path11), blockParamId = !path11.depth && !scoped && this.blockParamIndex(name);
208810
208810
  if (blockParamId) {
208811
- this.opcode("lookupBlockParam", blockParamId, path10.parts);
208811
+ this.opcode("lookupBlockParam", blockParamId, path11.parts);
208812
208812
  } else if (!name) {
208813
208813
  this.opcode("pushContext");
208814
- } else if (path10.data) {
208814
+ } else if (path11.data) {
208815
208815
  this.options.data = true;
208816
- this.opcode("lookupData", path10.depth, path10.parts, path10.strict);
208816
+ this.opcode("lookupData", path11.depth, path11.parts, path11.strict);
208817
208817
  } else {
208818
- this.opcode("lookupOnContext", path10.parts, path10.falsy, path10.strict, scoped);
208818
+ this.opcode("lookupOnContext", path11.parts, path11.falsy, path11.strict, scoped);
208819
208819
  }
208820
208820
  },
208821
208821
  StringLiteral: function StringLiteral(string2) {
@@ -209159,16 +209159,16 @@ var require_util13 = __commonJS((exports) => {
209159
209159
  }
209160
209160
  exports.urlGenerate = urlGenerate;
209161
209161
  function normalize2(aPath) {
209162
- var path10 = aPath;
209162
+ var path11 = aPath;
209163
209163
  var url2 = urlParse(aPath);
209164
209164
  if (url2) {
209165
209165
  if (!url2.path) {
209166
209166
  return aPath;
209167
209167
  }
209168
- path10 = url2.path;
209168
+ path11 = url2.path;
209169
209169
  }
209170
- var isAbsolute4 = exports.isAbsolute(path10);
209171
- var parts = path10.split(/\/+/);
209170
+ var isAbsolute4 = exports.isAbsolute(path11);
209171
+ var parts = path11.split(/\/+/);
209172
209172
  for (var part, up = 0, i = parts.length - 1;i >= 0; i--) {
209173
209173
  part = parts[i];
209174
209174
  if (part === ".") {
@@ -209185,15 +209185,15 @@ var require_util13 = __commonJS((exports) => {
209185
209185
  }
209186
209186
  }
209187
209187
  }
209188
- path10 = parts.join("/");
209189
- if (path10 === "") {
209190
- path10 = isAbsolute4 ? "/" : ".";
209188
+ path11 = parts.join("/");
209189
+ if (path11 === "") {
209190
+ path11 = isAbsolute4 ? "/" : ".";
209191
209191
  }
209192
209192
  if (url2) {
209193
- url2.path = path10;
209193
+ url2.path = path11;
209194
209194
  return urlGenerate(url2);
209195
209195
  }
209196
- return path10;
209196
+ return path11;
209197
209197
  }
209198
209198
  exports.normalize = normalize2;
209199
209199
  function join9(aRoot, aPath) {
@@ -211750,8 +211750,8 @@ var require_printer = __commonJS((exports) => {
211750
211750
  return this.accept(sexpr.path) + " " + params + hash;
211751
211751
  };
211752
211752
  PrintVisitor.prototype.PathExpression = function(id) {
211753
- var path10 = id.parts.join("/");
211754
- return (id.data ? "@" : "") + "PATH:" + path10;
211753
+ var path11 = id.parts.join("/");
211754
+ return (id.data ? "@" : "") + "PATH:" + path11;
211755
211755
  };
211756
211756
  PrintVisitor.prototype.StringLiteral = function(string2) {
211757
211757
  return '"' + string2.value + '"';
@@ -216953,7 +216953,7 @@ var import_cors = __toESM(require_lib7(), 1);
216953
216953
  var import_express = __toESM(require_express(), 1);
216954
216954
  var import_http_proxy_middleware = __toESM(require_dist4(), 1);
216955
216955
  import * as http2 from "http";
216956
- import * as path10 from "path";
216956
+ import * as path11 from "path";
216957
216957
  import { fileURLToPath as fileURLToPath4 } from "url";
216958
216958
 
216959
216959
  // src/controller/compile.controller.ts
@@ -225341,6 +225341,8 @@ var esm_default = { watch, FSWatcher };
225341
225341
  // src/controller/watch-mode.controller.ts
225342
225342
  init_errors();
225343
225343
  init_logger();
225344
+ import { EventEmitter as EventEmitter4 } from "events";
225345
+ import path10 from "path";
225344
225346
 
225345
225347
  // src/service/environment_store.ts
225346
225348
  var import_client_s32 = __toESM(require_dist_cjs75(), 1);
@@ -229670,11 +229672,13 @@ init_errors();
229670
229672
  init_logger();
229671
229673
 
229672
229674
  // src/storage/duckdb/DuckDBConnection.ts
229673
- import duckdb from "duckdb";
229675
+ import {
229676
+ DuckDBInstance
229677
+ } from "@duckdb/node-api";
229674
229678
  import * as path4 from "path";
229675
229679
 
229676
229680
  class DuckDBConnection2 {
229677
- db = null;
229681
+ instance = null;
229678
229682
  connection = null;
229679
229683
  dbPath;
229680
229684
  mutex = new Mutex;
@@ -229682,68 +229686,42 @@ class DuckDBConnection2 {
229682
229686
  this.dbPath = dbPath || path4.join(process.cwd(), "publisher.db");
229683
229687
  }
229684
229688
  async initialize() {
229685
- return new Promise((resolve4, reject) => {
229686
- this.db = new duckdb.Database(this.dbPath, {}, (err) => {
229687
- if (err) {
229688
- console.error("Failed to create DuckDB database:", err);
229689
- reject(new Error(`Failed to initialize DuckDB: ${err.message}`));
229690
- return;
229691
- }
229692
- this.connection = this.db.connect();
229693
- if (!this.connection) {
229694
- reject(new Error("Failed to create connection object"));
229695
- return;
229696
- }
229697
- this.connection.all("SELECT 42 as answer", (testErr, _rows) => {
229698
- if (testErr) {
229699
- console.error("Connection test failed:", testErr);
229700
- reject(new Error(`Failed to verify DuckDB connection: ${testErr.message}`));
229701
- return;
229702
- }
229703
- resolve4();
229704
- });
229705
- });
229706
- });
229689
+ try {
229690
+ this.instance = await DuckDBInstance.create(this.dbPath);
229691
+ this.connection = await this.instance.connect();
229692
+ await this.connection.run("SELECT 42 as answer");
229693
+ } catch (err) {
229694
+ const message = err instanceof Error ? err.message : String(err);
229695
+ console.error("Failed to create DuckDB database:", err);
229696
+ throw new Error(`Failed to initialize DuckDB: ${message}`);
229697
+ }
229707
229698
  }
229708
229699
  async close() {
229709
- return new Promise((resolve4, reject) => {
229700
+ try {
229710
229701
  if (this.connection) {
229711
- this.connection.close((err) => {
229712
- if (err) {
229713
- reject(new Error(`Failed to close DuckDB connection: ${err.message}`));
229714
- return;
229715
- }
229716
- if (this.db) {
229717
- this.db.close((dbErr) => {
229718
- if (dbErr) {
229719
- reject(new Error(`Failed to close DuckDB: ${dbErr.message}`));
229720
- return;
229721
- }
229722
- console.log("DuckDB connection closed");
229723
- resolve4();
229724
- });
229725
- } else {
229726
- resolve4();
229727
- }
229728
- });
229729
- } else {
229730
- resolve4();
229702
+ this.connection.closeSync();
229703
+ this.connection = null;
229731
229704
  }
229732
- });
229705
+ if (this.instance) {
229706
+ this.instance.closeSync();
229707
+ this.instance = null;
229708
+ }
229709
+ console.log("DuckDB connection closed");
229710
+ } catch (err) {
229711
+ const message = err instanceof Error ? err.message : String(err);
229712
+ throw new Error(`Failed to close DuckDB connection: ${message}`);
229713
+ }
229733
229714
  }
229734
229715
  async isInitialized() {
229735
229716
  if (!this.connection)
229736
229717
  return false;
229737
229718
  return this.mutex.runExclusive(async () => {
229738
- return new Promise((resolve4) => {
229739
- this.connection.all("SELECT name FROM sqlite_master WHERE type='table' AND name='environments'", (err, rows) => {
229740
- if (err) {
229741
- resolve4(false);
229742
- return;
229743
- }
229744
- resolve4(rows && rows.length > 0);
229745
- });
229746
- });
229719
+ try {
229720
+ const reader = await this.connection.runAndReadAll("SELECT name FROM sqlite_master WHERE type='table' AND name='environments'");
229721
+ return reader.getRowObjectsJS().length > 0;
229722
+ } catch {
229723
+ return false;
229724
+ }
229747
229725
  });
229748
229726
  }
229749
229727
  async run(query, params) {
@@ -229751,21 +229729,13 @@ class DuckDBConnection2 {
229751
229729
  throw new Error("Database not initialized");
229752
229730
  }
229753
229731
  return this.mutex.runExclusive(async () => {
229754
- return new Promise((resolve4, reject) => {
229755
- const callback = (err) => {
229756
- if (err) {
229757
- reject(new Error(`Query execution failed: ${err.message}
229758
- Query: ${query}`));
229759
- return;
229760
- }
229761
- resolve4();
229762
- };
229763
- if (params && params.length > 0) {
229764
- this.connection.run(query, ...params, callback);
229765
- } else {
229766
- this.connection.run(query, callback);
229767
- }
229768
- });
229732
+ try {
229733
+ await this.connection.run(query, params);
229734
+ } catch (err) {
229735
+ const message = err instanceof Error ? err.message : String(err);
229736
+ throw new Error(`Query execution failed: ${message}
229737
+ Query: ${query}`);
229738
+ }
229769
229739
  });
229770
229740
  }
229771
229741
  async all(query, params) {
@@ -229773,33 +229743,20 @@ Query: ${query}`));
229773
229743
  throw new Error("Database not initialized");
229774
229744
  }
229775
229745
  return this.mutex.runExclusive(async () => {
229776
- return new Promise((resolve4, reject) => {
229777
- const callback = (err, rows) => {
229778
- if (err) {
229779
- reject(new Error(`Query execution failed: ${err.message}
229780
- Query: ${query}`));
229781
- return;
229782
- }
229783
- resolve4(rows || []);
229784
- };
229785
- if (params && params.length > 0) {
229786
- this.connection.all(query, ...params, callback);
229787
- } else {
229788
- this.connection.all(query, callback);
229789
- }
229790
- });
229746
+ try {
229747
+ const reader = await this.connection.runAndReadAll(query, params);
229748
+ return reader.getRowObjectsJS();
229749
+ } catch (err) {
229750
+ const message = err instanceof Error ? err.message : String(err);
229751
+ throw new Error(`Query execution failed: ${message}
229752
+ Query: ${query}`);
229753
+ }
229791
229754
  });
229792
229755
  }
229793
229756
  async get(query, params) {
229794
229757
  const rows = await this.all(query, params);
229795
229758
  return rows.length > 0 ? rows[0] : null;
229796
229759
  }
229797
- getConnection() {
229798
- if (!this.connection) {
229799
- throw new Error("Database not initialized");
229800
- }
229801
- return this.connection;
229802
- }
229803
229760
  }
229804
229761
 
229805
229762
  // src/storage/duckdb/DuckDBManifestStore.ts
@@ -230869,6 +230826,7 @@ init_logger();
230869
230826
  import crypto4 from "crypto";
230870
230827
  import * as fs6 from "fs";
230871
230828
  import * as path8 from "path";
230829
+ import { pathToFileURL as pathToFileURL2 } from "url";
230872
230830
 
230873
230831
  // src/utils.ts
230874
230832
  import * as fs3 from "fs";
@@ -231480,6 +231438,9 @@ class Model {
231480
231438
  getAuthorize(sourceName) {
231481
231439
  return this.sources?.find((source) => source.name === sourceName)?.authorize ?? [];
231482
231440
  }
231441
+ hasAuthorize() {
231442
+ return this.fileLevelAuthorize.length > 0 || (this.sources?.some((s) => (s.authorize?.length ?? 0) > 0) ?? false);
231443
+ }
231483
231444
  effectiveAuthorizeFor(sourceName) {
231484
231445
  if (sourceName && this.sources?.some((s) => s.name === sourceName)) {
231485
231446
  return this.getAuthorize(sourceName);
@@ -231510,6 +231471,12 @@ class Model {
231510
231471
  if (!passed)
231511
231472
  deny();
231512
231473
  }
231474
+ async assertAuthorizedForText(text, givens) {
231475
+ await this.assertAuthorized(this.extractSourceName(text), givens);
231476
+ }
231477
+ async assertAuthorizedForRunnable(runnable, givens) {
231478
+ await this.assertAuthorized(await this.resolveAuthorizeSourceFromRunnable(runnable), givens);
231479
+ }
231513
231480
  async resolveAuthorizeSourceFromRunnable(runnable) {
231514
231481
  try {
231515
231482
  const prepared = await runnable.getPreparedQuery();
@@ -232196,11 +232163,13 @@ class Package {
232196
232163
  status
232197
232164
  });
232198
232165
  try {
232199
- await fs5.rm(packagePath, {
232200
- recursive: true,
232201
- force: true
232202
- });
232203
- logger.info(`Cleaned up failed package directory: ${packagePath}`);
232166
+ const stat6 = await fs5.lstat(packagePath).catch(() => null);
232167
+ if (stat6?.isSymbolicLink()) {
232168
+ logger.info(`Skipping cleanup of symlinked package path on failure: ${packagePath}`);
232169
+ } else {
232170
+ await fs5.rm(packagePath, { recursive: true, force: true });
232171
+ logger.info(`Cleaned up failed package directory: ${packagePath}`);
232172
+ }
232204
232173
  } catch (cleanupError) {
232205
232174
  logger.warn(`Failed to clean up package directory ${packagePath}`, {
232206
232175
  error: cleanupError
@@ -232495,6 +232464,9 @@ class Environment {
232495
232464
  environmentName;
232496
232465
  metadata;
232497
232466
  memoryGovernor = null;
232467
+ getEnvironmentPath() {
232468
+ return this.environmentPath;
232469
+ }
232498
232470
  constructor(environmentName, environmentPath, malloyConfig, apiConnections) {
232499
232471
  assertSafeEnvironmentPath(environmentPath);
232500
232472
  this.environmentName = environmentName;
@@ -232579,8 +232551,8 @@ class Environment {
232579
232551
  return this.withPackageLock(packageName, async () => {
232580
232552
  const modelPath = safeJoinUnderRoot(this.environmentPath, packageName, modelName);
232581
232553
  const modelDir = path8.dirname(modelPath);
232582
- const virtualUri = `file://${path8.join(modelDir, "__compile_check.malloy")}`;
232583
- const virtualUrl = new URL(virtualUri);
232554
+ const virtualUrl = pathToFileURL2(path8.join(modelDir, "__compile_check.malloy"));
232555
+ const virtualUri = virtualUrl.toString();
232584
232556
  let modelContent = "";
232585
232557
  try {
232586
232558
  modelContent = await fs6.promises.readFile(modelPath, "utf8");
@@ -232596,6 +232568,10 @@ ${source}` : source;
232596
232568
  }
232597
232569
  };
232598
232570
  const pkg = await this._loadOrGetPackageLocked(packageName);
232571
+ const gateModel = pkg.getModel(modelName);
232572
+ if (gateModel) {
232573
+ await gateModel.assertAuthorizedForText(source, givens ?? {});
232574
+ }
232599
232575
  const runtime = new Runtime2({
232600
232576
  urlReader: interceptingReader,
232601
232577
  config: pkg.getMalloyConfig()
@@ -232603,10 +232579,16 @@ ${source}` : source;
232603
232579
  try {
232604
232580
  const modelMaterializer = runtime.loadModel(virtualUrl);
232605
232581
  const model = await modelMaterializer.getModel();
232582
+ let queryMaterializer = null;
232583
+ try {
232584
+ queryMaterializer = modelMaterializer.loadFinalQuery();
232585
+ } catch {}
232586
+ if (queryMaterializer && gateModel?.hasAuthorize()) {
232587
+ await gateModel.assertAuthorizedForRunnable(queryMaterializer, givens ?? {});
232588
+ }
232606
232589
  let sql;
232607
- if (includeSql) {
232590
+ if (includeSql && queryMaterializer) {
232608
232591
  try {
232609
- const queryMaterializer = modelMaterializer.loadFinalQuery();
232610
232592
  sql = await queryMaterializer.getSQL({ givens });
232611
232593
  } catch {}
232612
232594
  }
@@ -233147,6 +233129,16 @@ function validateEnvironmentAzureUrls(environment) {
233147
233129
  }
233148
233130
  }
233149
233131
  }
233132
+ async function clearMountTarget(targetPath) {
233133
+ try {
233134
+ const stats = await fs7.promises.lstat(targetPath);
233135
+ if (stats.isDirectory() && !stats.isSymbolicLink()) {
233136
+ await fs7.promises.rm(targetPath, { recursive: true, force: true });
233137
+ } else {
233138
+ await fs7.promises.unlink(targetPath);
233139
+ }
233140
+ } catch {}
233141
+ }
233150
233142
 
233151
233143
  class EnvironmentStore {
233152
233144
  serverRootPath;
@@ -233161,9 +233153,13 @@ class EnvironmentStore {
233161
233153
  });
233162
233154
  gcsClient;
233163
233155
  memoryGovernor = null;
233156
+ inPlaceEnvs = new Set;
233164
233157
  constructor(serverRootPath) {
233165
233158
  this.serverRootPath = serverRootPath;
233166
233159
  this.gcsClient = new Storage;
233160
+ const watchEnvList = (process.env.PUBLISHER_WATCH || "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
233161
+ for (const envName of watchEnvList)
233162
+ this.inPlaceEnvs.add(envName);
233167
233163
  const storageConfig = {
233168
233164
  type: "duckdb",
233169
233165
  duckdb: {
@@ -233173,6 +233169,12 @@ class EnvironmentStore {
233173
233169
  this.storageManager = new StorageManager(storageConfig);
233174
233170
  this.finishedInitialization = this.initialize();
233175
233171
  }
233172
+ isInPlace(environmentName) {
233173
+ return this.inPlaceEnvs.has(environmentName);
233174
+ }
233175
+ markInPlace(environmentName) {
233176
+ this.inPlaceEnvs.add(environmentName);
233177
+ }
233176
233178
  setMemoryGovernor(governor) {
233177
233179
  this.memoryGovernor = governor;
233178
233180
  for (const env of this.environments.values()) {
@@ -233798,21 +233800,47 @@ class EnvironmentStore {
233798
233800
  }
233799
233801
  } else {
233800
233802
  if (this.isLocalPath(_package.location)) {
233801
- sourcePath = _package.location;
233803
+ sourcePath = path9.isAbsolute(_package.location) ? _package.location : path9.join(this.serverRootPath, _package.location);
233802
233804
  } else {
233803
233805
  sourcePath = safeJoinUnderRoot(tempDownloadPath, groupedLocation);
233804
233806
  }
233805
233807
  }
233806
233808
  const sourceExists = await fs7.promises.access(sourcePath).then(() => true).catch(() => false);
233807
233809
  if (sourceExists) {
233808
- await fs7.promises.mkdir(absolutePackagePath, {
233809
- recursive: true
233810
- });
233811
- await fs7.promises.cp(sourcePath, absolutePackagePath, {
233812
- recursive: true
233813
- });
233814
- logger.info(`Extracted package "${packageDir}" from ${groupedLocation.startsWith("https://github.com/") && _package.location.includes("/tree/") ? "GitHub subdirectory" : "shared download"}`);
233810
+ const isInPlace = this.inPlaceEnvs.has(environmentName) && this.isLocalPath(_package.location);
233811
+ if (isInPlace) {
233812
+ await clearMountTarget(absolutePackagePath);
233813
+ const absoluteSourcePath = path9.resolve(sourcePath);
233814
+ const linkType = process.platform === "win32" ? "junction" : "dir";
233815
+ try {
233816
+ await fs7.promises.symlink(absoluteSourcePath, absolutePackagePath, linkType);
233817
+ logger.info(`In-place mount (watch mode): linked package "${packageDir}" -> "${absoluteSourcePath}"`);
233818
+ } catch (linkError) {
233819
+ const code = linkError?.code ?? String(linkError);
233820
+ logger.warn(`In-place mount failed for package "${packageDir}" (${code}); falling back to a copy. Source-edit live reload is disabled for this package.`);
233821
+ await clearMountTarget(absolutePackagePath);
233822
+ await fs7.promises.mkdir(absolutePackagePath, {
233823
+ recursive: true
233824
+ });
233825
+ await fs7.promises.cp(sourcePath, absolutePackagePath, {
233826
+ recursive: true
233827
+ });
233828
+ }
233829
+ } else {
233830
+ if (this.inPlaceEnvs.has(environmentName) && !this.isLocalPath(_package.location)) {
233831
+ logger.warn(`Watch mode: package "${packageDir}" has remote location "${_package.location}" — falling back to copy. Source-edit live reload won't work for this package; clone the source locally and use a local-dir location to enable it.`);
233832
+ }
233833
+ await clearMountTarget(absolutePackagePath);
233834
+ await fs7.promises.mkdir(absolutePackagePath, {
233835
+ recursive: true
233836
+ });
233837
+ await fs7.promises.cp(sourcePath, absolutePackagePath, {
233838
+ recursive: true
233839
+ });
233840
+ logger.info(`Extracted package "${packageDir}" from ${groupedLocation.startsWith("https://github.com/") && _package.location.includes("/tree/") ? "GitHub subdirectory" : "shared download"}`);
233841
+ }
233815
233842
  } else {
233843
+ await clearMountTarget(absolutePackagePath);
233816
233844
  await fs7.promises.mkdir(absolutePackagePath, {
233817
233845
  recursive: true
233818
233846
  });
@@ -234092,15 +234120,104 @@ class EnvironmentStore {
234092
234120
  }
234093
234121
 
234094
234122
  // src/controller/watch-mode.controller.ts
234123
+ var ASSET_EXTS = new Set([
234124
+ ".html",
234125
+ ".htm",
234126
+ ".css",
234127
+ ".js",
234128
+ ".mjs",
234129
+ ".json",
234130
+ ".png",
234131
+ ".jpg",
234132
+ ".jpeg",
234133
+ ".gif",
234134
+ ".svg",
234135
+ ".webp",
234136
+ ".ico",
234137
+ ".woff",
234138
+ ".woff2"
234139
+ ]);
234140
+ var MODEL_EXTS = new Set([".malloy", ".malloynb", ".md"]);
234141
+
234095
234142
  class WatchModeController {
234096
234143
  environmentStore;
234097
234144
  watchingPath;
234098
234145
  watchingEnvironmentName;
234099
- watcher;
234146
+ watcher = null;
234147
+ setupChain = null;
234148
+ events = new EventEmitter4;
234100
234149
  constructor(environmentStore) {
234101
234150
  this.environmentStore = environmentStore;
234102
234151
  this.watchingPath = null;
234103
234152
  this.watchingEnvironmentName = null;
234153
+ this.events.setMaxListeners(100);
234154
+ }
234155
+ async ensureWatching(environmentName) {
234156
+ if (this.watchingEnvironmentName === environmentName && this.watcher) {
234157
+ return;
234158
+ }
234159
+ const run = (this.setupChain ?? Promise.resolve()).catch(() => {}).then(async () => {
234160
+ if (this.watchingEnvironmentName === environmentName && this.watcher) {
234161
+ return;
234162
+ }
234163
+ const env = await this.environmentStore.getEnvironment(environmentName, false);
234164
+ const watchPath = env.getEnvironmentPath();
234165
+ if (this.watcher) {
234166
+ await this.watcher.close();
234167
+ this.watcher = null;
234168
+ }
234169
+ this.startWatcher(environmentName, watchPath);
234170
+ });
234171
+ this.setupChain = run;
234172
+ await run;
234173
+ }
234174
+ isWatching(environmentName) {
234175
+ return !!this.watcher && this.watchingEnvironmentName === environmentName;
234176
+ }
234177
+ startWatcher(watchName, watchPath) {
234178
+ this.watchingEnvironmentName = watchName;
234179
+ this.watchingPath = watchPath;
234180
+ this.watcher = esm_default.watch(this.watchingPath, {
234181
+ ignored: (filePath, stats) => {
234182
+ if (!stats?.isFile())
234183
+ return false;
234184
+ const ext = path10.extname(filePath).toLowerCase();
234185
+ return !MODEL_EXTS.has(ext) && !ASSET_EXTS.has(ext);
234186
+ },
234187
+ ignoreInitial: true
234188
+ });
234189
+ const reloadPackage = async (pkgName) => {
234190
+ try {
234191
+ const environment = await this.environmentStore.getEnvironment(watchName, false);
234192
+ await environment.getPackage(pkgName, true);
234193
+ logger.info(`Watch: recompiled package "${pkgName}" in environment "${watchName}"`);
234194
+ return true;
234195
+ } catch (error) {
234196
+ logger.error(`Watch: failed to recompile package "${pkgName}" in environment "${watchName}"`, { error });
234197
+ return false;
234198
+ }
234199
+ };
234200
+ const onEvent = (kind) => async (filePath) => {
234201
+ logger.info(`Watch ${kind}: ${filePath}; environment=${watchName}`);
234202
+ const rel = path10.relative(this.watchingPath ?? "", filePath);
234203
+ const segments = rel.split(path10.sep);
234204
+ const pkgName = segments.length > 1 && segments[0] && !segments[0].startsWith("..") ? segments[0] : null;
234205
+ if (!pkgName)
234206
+ return;
234207
+ const ext = path10.extname(filePath).toLowerCase();
234208
+ if (MODEL_EXTS.has(ext)) {
234209
+ const recompiled = await reloadPackage(pkgName);
234210
+ if (!recompiled)
234211
+ return;
234212
+ }
234213
+ this.events.emit(`${watchName}/${pkgName}`, {
234214
+ path: filePath,
234215
+ kind
234216
+ });
234217
+ };
234218
+ this.watcher.on("add", onEvent("add"));
234219
+ this.watcher.on("change", onEvent("change"));
234220
+ this.watcher.on("unlink", onEvent("unlink"));
234104
234221
  }
234105
234222
  getWatchStatus = async (_req, res) => {
234106
234223
  return res.json({
@@ -234120,7 +234237,6 @@ class WatchModeController {
234120
234237
  return;
234121
234238
  }
234122
234239
  const environmentManifest = await EnvironmentStore.reloadEnvironmentManifest(this.environmentStore.serverRootPath);
234123
- this.watchingEnvironmentName = watchName || null;
234124
234240
  const environment = environmentManifest.environments.find((e) => e.name === watchName);
234125
234241
  if (!environment || !environment.packages || environment.packages.length === 0) {
234126
234242
  res.status(404).json({
@@ -234128,32 +234244,21 @@ class WatchModeController {
234128
234244
  });
234129
234245
  return;
234130
234246
  }
234131
- this.watchingPath = safeJoinUnderRoot(this.environmentStore.serverRootPath, watchName);
234132
- this.watcher = esm_default.watch(this.watchingPath, {
234133
- ignored: (path10, stats) => !!stats?.isFile() && !path10.endsWith(".malloy") && !path10.endsWith(".md"),
234134
- ignoreInitial: true
234135
- });
234136
- const reloadEnvironment = async () => {
234137
- const environment2 = await this.environmentStore.getEnvironment(watchName, true);
234138
- await this.environmentStore.addEnvironment(environment2.metadata);
234139
- logger.info(`Reloaded environment ${watchName}`);
234140
- };
234141
- this.watcher.on("add", async (path10) => {
234142
- logger.info(`Detected new file ${path10}, reloading environment ${watchName}`);
234143
- await reloadEnvironment();
234144
- });
234145
- this.watcher.on("unlink", async (path10) => {
234146
- logger.info(`Detected deletion of ${path10}, reloading environment ${watchName}`);
234147
- await reloadEnvironment();
234148
- });
234149
- this.watcher.on("change", async (path10) => {
234150
- logger.info(`Detected change on ${path10}, reloading environment ${watchName}`);
234151
- await reloadEnvironment();
234152
- });
234247
+ try {
234248
+ await this.ensureWatching(watchName);
234249
+ } catch (error) {
234250
+ logger.error(error);
234251
+ const { status } = internalErrorToHttpError(error);
234252
+ res.status(status).json({ error: error.message });
234253
+ return;
234254
+ }
234153
234255
  res.json();
234154
234256
  };
234155
234257
  stopWatchMode = async (_req, res) => {
234156
- this.watcher.close();
234258
+ if (this.watcher) {
234259
+ await this.watcher.close();
234260
+ this.watcher = null;
234261
+ }
234157
234262
  this.watchingPath = null;
234158
234263
  this.watchingEnvironmentName = null;
234159
234264
  res.json();
@@ -237259,7 +237364,14 @@ function getMalloyErrorDetails(operation, modelIdentifier, error) {
237259
237364
  const connectionErrorMatch = error.message.match(/Cannot connect to database/i);
237260
237365
  const fieldNotFoundMatch = error.message.match(/Field '([^']+)' not found in (source|query|view) '([^']+)'/i);
237261
237366
  const invalidRequestMatch = error.message.match(/Invalid query request\\. Query OR queryName must be defined/i);
237262
- if (viewNotFoundMatch) {
237367
+ const accessDeniedMatch = error.message.match(/Access denied for source "([^"]+)"/i);
237368
+ if (accessDeniedMatch) {
237369
+ refined = true;
237370
+ const [, sourceName] = accessDeniedMatch;
237371
+ suggestions = [
237372
+ `Suggestion: Access to source '${sourceName}' is restricted by an #(authorize) gate. Supply the givens its authorize expression requires (e.g. a role/region given) and retry. This is an authorization denial, not a syntax error.`
237373
+ ];
237374
+ } else if (viewNotFoundMatch) {
237263
237375
  refined = true;
237264
237376
  const [, viewName, sourceName] = viewNotFoundMatch;
237265
237377
  suggestions.unshift(`Suggestion: View '${viewName}' was not found in source '${sourceName}'. Check the view name spelling or try requesting the resource details for the source URI (e.g., 'malloy://.../sources/${sourceName}') to see the list of available views. Views are defined within sources like 'source: ${sourceName} is ... extend { view: ${viewName} is { ... } }'.`);
@@ -237377,6 +237489,8 @@ async function getModelForQuery(environmentStore, environmentName, packageName,
237377
237489
  errorDetails = getNotFoundError(`model '${modelPath}' in package '${packageName}' for environment '${environmentName}'`);
237378
237490
  } else if (error instanceof ModelCompilationError) {
237379
237491
  errorDetails = getMalloyErrorDetails("executeQuery (load model)", `${environmentName}/${packageName}/${modelPath}`, error);
237492
+ } else if (error instanceof AccessDeniedError) {
237493
+ errorDetails = getMalloyErrorDetails("executeQuery (load model)", `${environmentName}/${packageName}/${modelPath}`, error);
237380
237494
  } else if (error instanceof ServiceUnavailableError) {
237381
237495
  errorDetails = {
237382
237496
  message: error.message,
@@ -237393,25 +237507,25 @@ async function getModelForQuery(environmentStore, environmentName, packageName,
237393
237507
  }
237394
237508
  }
237395
237509
  function buildMalloyUri(components, fragment) {
237396
- let path10 = "/environment/";
237510
+ let path11 = "/environment/";
237397
237511
  if (components.environment) {
237398
- path10 += encodeURIComponent(components.environment);
237512
+ path11 += encodeURIComponent(components.environment);
237399
237513
  } else {
237400
- path10 += "home";
237514
+ path11 += "home";
237401
237515
  }
237402
237516
  if (components.package) {
237403
- path10 += "/package/" + encodeURIComponent(components.package);
237517
+ path11 += "/package/" + encodeURIComponent(components.package);
237404
237518
  }
237405
237519
  if (components.resourceType) {
237406
- path10 += "/" + components.resourceType;
237520
+ path11 += "/" + components.resourceType;
237407
237521
  if (components.resourceName) {
237408
- path10 += "/" + encodeURIComponent(components.resourceName);
237522
+ path11 += "/" + encodeURIComponent(components.resourceName);
237409
237523
  if (components.subResourceType && components.subResourceName) {
237410
- path10 += "/" + components.subResourceType + "/" + encodeURIComponent(components.subResourceName);
237524
+ path11 += "/" + components.subResourceType + "/" + encodeURIComponent(components.subResourceName);
237411
237525
  }
237412
237526
  }
237413
237527
  }
237414
- let uriString = "malloy:/" + path10;
237528
+ let uriString = "malloy:/" + path11;
237415
237529
  if (fragment) {
237416
237530
  uriString += "#" + fragment;
237417
237531
  }
@@ -239654,6 +239768,10 @@ function parseArgs() {
239654
239768
  i++;
239655
239769
  } else if (arg === "--init") {
239656
239770
  process.env.INITIALIZE_STORAGE = "true";
239771
+ } else if (arg === "--watch-env" && args[i + 1]) {
239772
+ const existing = process.env.PUBLISHER_WATCH || "";
239773
+ process.env.PUBLISHER_WATCH = existing ? `${existing},${args[i + 1]}` : args[i + 1];
239774
+ i++;
239657
239775
  } else if (arg === "--help" || arg === "-h") {
239658
239776
  console.log("Malloy Publisher Server");
239659
239777
  console.log("");
@@ -239668,6 +239786,11 @@ function parseArgs() {
239668
239786
  console.log(" --shutdown_drain_duration_seconds <number> Time in seconds to keep service in draining state before closing servers (default: 0)");
239669
239787
  console.log(" --shutdown_graceful_close_timeout_seconds <number> Time in seconds to wait after closing servers before exit (default: 0)");
239670
239788
  console.log(" --init Initialize the storage (default: false)");
239789
+ console.log(" --watch-env <name> Enable dev-mode watch for the named environment.");
239790
+ console.log(" Mounts local-dir packages in-place (symlink, not");
239791
+ console.log(" copy) so source-edit live reload works. A comma-");
239792
+ console.log(" separated PUBLISHER_WATCH mounts all listed envs in");
239793
+ console.log(" place, but only the first one auto-reloads.");
239671
239794
  console.log(" --help, -h Show this help message");
239672
239795
  process.exit(0);
239673
239796
  }
@@ -239684,8 +239807,8 @@ var MCP_ENDPOINT = "/mcp";
239684
239807
  var SHUTDOWN_DRAIN_DURATION_SECONDS = Number(process.env.SHUTDOWN_DRAIN_DURATION_SECONDS || 0);
239685
239808
  var SHUTDOWN_GRACEFUL_CLOSE_TIMEOUT_SECONDS = Number(process.env.SHUTDOWN_GRACEFUL_CLOSE_TIMEOUT_SECONDS || 0);
239686
239809
  var __filename_esm = fileURLToPath4(import.meta.url);
239687
- var ROOT = path10.join(path10.dirname(__filename_esm), "app");
239688
- var SERVER_ROOT = path10.resolve(process.cwd(), process.env.SERVER_ROOT || ".");
239810
+ var ROOT = path11.join(path11.dirname(__filename_esm), "app");
239811
+ var SERVER_ROOT = path11.resolve(process.cwd(), process.env.SERVER_ROOT || ".");
239689
239812
  var API_PREFIX2 = "/api/v0";
239690
239813
  var isDevelopment = process.env["NODE_ENV"] === "development";
239691
239814
  var app = import_express.default();
@@ -239769,16 +239892,164 @@ mcpApp.all(MCP_ENDPOINT, async (req, res) => {
239769
239892
  }
239770
239893
  }
239771
239894
  });
239895
+ var PUBLISHER_RUNTIME_PATH = path11.join(path11.dirname(__filename_esm), "runtime", "publisher.js");
239896
+ app.get("/sdk/publisher.js", (_req, res) => {
239897
+ res.type("application/javascript");
239898
+ res.setHeader("cache-control", "public, max-age=60");
239899
+ res.setHeader("X-Content-Type-Options", "nosniff");
239900
+ res.sendFile(PUBLISHER_RUNTIME_PATH, (err) => {
239901
+ if (err) {
239902
+ logger.error("Failed to send publisher.js runtime", { error: err });
239903
+ if (!res.headersSent)
239904
+ res.status(500).end();
239905
+ }
239906
+ });
239907
+ });
239908
+ async function serveFromPackage(req, res) {
239909
+ const subPathRaw = req.params["0"] ?? "";
239910
+ try {
239911
+ const environment = await environmentStore.getEnvironment(req.params.environmentName, false);
239912
+ const pkg = await environment.getPackage(req.params.packageName, false);
239913
+ const publicRoot = path11.join(pkg.getPackagePath(), "public");
239914
+ let subPath = subPathRaw;
239915
+ if (subPath === "" || subPath.endsWith("/")) {
239916
+ subPath = subPath + "index.html";
239917
+ }
239918
+ const fullPath = safeJoinUnderRoot(publicRoot, subPath);
239919
+ const fsp = await import("fs/promises");
239920
+ let realPublicRoot;
239921
+ let realFullPath;
239922
+ try {
239923
+ realPublicRoot = await fsp.realpath(publicRoot);
239924
+ realFullPath = await fsp.realpath(fullPath);
239925
+ } catch {
239926
+ if (!res.headersSent) {
239927
+ res.status(404).end();
239928
+ }
239929
+ return;
239930
+ }
239931
+ const rel = path11.relative(realPublicRoot, realFullPath);
239932
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) {
239933
+ res.status(403).end();
239934
+ return;
239935
+ }
239936
+ const ext = path11.extname(realFullPath).toLowerCase();
239937
+ if (ext === ".html" || ext === ".htm") {
239938
+ const frameAncestors = process.env.PUBLISHER_FRAME_ANCESTORS || "*";
239939
+ res.setHeader("Content-Security-Policy", `frame-ancestors ${frameAncestors}`);
239940
+ res.removeHeader("X-Frame-Options");
239941
+ }
239942
+ res.setHeader("X-Content-Type-Options", "nosniff");
239943
+ res.sendFile(realFullPath, (err) => {
239944
+ if (err) {
239945
+ if (!res.headersSent) {
239946
+ res.status(404).end();
239947
+ }
239948
+ }
239949
+ });
239950
+ } catch (e) {
239951
+ if (!res.headersSent) {
239952
+ const { json, status } = internalErrorToHttpError(e);
239953
+ res.status(status).json(json);
239954
+ }
239955
+ }
239956
+ }
239957
+ app.get("/environments/:environmentName/packages/:packageName", (req, res, next) => {
239958
+ if (req.path.endsWith("/"))
239959
+ return next();
239960
+ const canonical = `/environments/${encodeURIComponent(req.params.environmentName)}/packages/${encodeURIComponent(req.params.packageName)}/`;
239961
+ const query = new URLSearchParams;
239962
+ for (const [key, value] of Object.entries(req.query)) {
239963
+ if (Array.isArray(value)) {
239964
+ for (const v of value)
239965
+ query.append(key, String(v));
239966
+ } else if (value !== undefined) {
239967
+ query.append(key, String(value));
239968
+ }
239969
+ }
239970
+ const qs = query.toString();
239971
+ res.redirect(308, qs ? `${canonical}?${qs}` : canonical);
239972
+ });
239973
+ app.get("/environments/:environmentName/packages/:packageName/*", serveFromPackage);
239974
+ var PAGES_DEPTH_CAP = 3;
239975
+ async function listPackagePages(environmentName, packageName, publicRoot) {
239976
+ const fs8 = await import("fs/promises");
239977
+ const out = [];
239978
+ let realPublicRoot;
239979
+ try {
239980
+ realPublicRoot = await fs8.realpath(publicRoot);
239981
+ } catch {
239982
+ return out;
239983
+ }
239984
+ async function walk(dir, depth) {
239985
+ if (depth > PAGES_DEPTH_CAP)
239986
+ return;
239987
+ let entries;
239988
+ try {
239989
+ entries = await fs8.readdir(dir, { withFileTypes: true });
239990
+ } catch {
239991
+ return;
239992
+ }
239993
+ for (const entry of entries) {
239994
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
239995
+ continue;
239996
+ const full = path11.join(dir, entry.name);
239997
+ let realFull;
239998
+ try {
239999
+ realFull = await fs8.realpath(full);
240000
+ } catch {
240001
+ continue;
240002
+ }
240003
+ const contained = path11.relative(realPublicRoot, realFull);
240004
+ if (contained.startsWith("..") || path11.isAbsolute(contained))
240005
+ continue;
240006
+ if (entry.isDirectory()) {
240007
+ await walk(full, depth + 1);
240008
+ } else if (entry.isFile() && (entry.name.endsWith(".html") || entry.name.endsWith(".htm"))) {
240009
+ const rel = path11.relative(publicRoot, full).replace(/\\/g, "/");
240010
+ let title = rel;
240011
+ try {
240012
+ const fh = await fs8.open(full, "r");
240013
+ try {
240014
+ const buf = Buffer.alloc(4096);
240015
+ const { bytesRead } = await fh.read(buf, 0, 4096, 0);
240016
+ const head = buf.slice(0, bytesRead).toString("utf8");
240017
+ const m = head.match(/<title[^>]*>([^<]+)<\/title>/i);
240018
+ if (m)
240019
+ title = m[1].trim();
240020
+ } finally {
240021
+ await fh.close();
240022
+ }
240023
+ } catch {}
240024
+ out.push({
240025
+ resource: `/environments/${environmentName}/packages/${packageName}/${rel}`,
240026
+ packageName,
240027
+ path: rel,
240028
+ title
240029
+ });
240030
+ }
240031
+ }
240032
+ }
240033
+ await walk(publicRoot, 0);
240034
+ out.sort((a, b) => {
240035
+ if (a.path === "index.html")
240036
+ return -1;
240037
+ if (b.path === "index.html")
240038
+ return 1;
240039
+ return a.path.localeCompare(b.path);
240040
+ });
240041
+ return out;
240042
+ }
239772
240043
  if (!isDevelopment) {
239773
240044
  app.use("/", import_express.default.static(ROOT));
239774
- app.use("/api-doc.html", import_express.default.static(path10.join(ROOT, "api-doc.html")));
240045
+ app.use("/api-doc.html", import_express.default.static(path11.join(ROOT, "api-doc.html")));
239775
240046
  } else {
239776
240047
  app.use(`${API_PREFIX2}`, loggerMiddleware);
239777
240048
  app.use(import_http_proxy_middleware.createProxyMiddleware({
239778
240049
  target: "http://localhost:5173",
239779
240050
  changeOrigin: true,
239780
240051
  ws: true,
239781
- pathFilter: (path11) => !path11.startsWith("/api/") && !path11.startsWith("/metrics") && !path11.startsWith("/health")
240052
+ pathFilter: (path12) => !path12.startsWith("/api/") && !path12.startsWith("/metrics") && !path12.startsWith("/health")
239782
240053
  }));
239783
240054
  }
239784
240055
  var setVersionIdError2 = (res) => {
@@ -239799,6 +240070,18 @@ try {
239799
240070
  logger.warn("Failed to register Prometheus metrics endpoint", { error });
239800
240071
  }
239801
240072
  app.use(drainingGuard);
240073
+ app.get(`${API_PREFIX2}/environments/:environmentName/packages/:packageName/pages`, async (req, res) => {
240074
+ try {
240075
+ const environment = await environmentStore.getEnvironment(req.params.environmentName, false);
240076
+ const pkg = await environment.getPackage(req.params.packageName, false);
240077
+ const pages = await listPackagePages(req.params.environmentName, req.params.packageName, path11.join(pkg.getPackagePath(), "public"));
240078
+ res.json(pages);
240079
+ } catch (error) {
240080
+ logger.error("Failed to list package pages", { error });
240081
+ const { json, status } = internalErrorToHttpError(error);
240082
+ res.status(status).json(json);
240083
+ }
240084
+ });
239802
240085
  app.get(`${API_PREFIX2}/status`, async (_req, res) => {
239803
240086
  try {
239804
240087
  const status = await environmentStore.getStatus();
@@ -239812,6 +240095,65 @@ app.get(`${API_PREFIX2}/status`, async (_req, res) => {
239812
240095
  app.get(`${API_PREFIX2}/watch-mode/status`, watchModeController.getWatchStatus);
239813
240096
  app.post(`${API_PREFIX2}/watch-mode/start`, watchModeController.startWatching);
239814
240097
  app.post(`${API_PREFIX2}/watch-mode/stop`, watchModeController.stopWatchMode);
240098
+ var MAX_SSE_CONNECTIONS = 1000;
240099
+ var sseConnectionCount = 0;
240100
+ app.get(`${API_PREFIX2}/environments/:environmentName/packages/:packageName/events`, async (req, res) => {
240101
+ const env = req.params.environmentName;
240102
+ const pkg = req.params.packageName;
240103
+ try {
240104
+ assertSafePackageName(env);
240105
+ assertSafePackageName(pkg);
240106
+ const environment = await environmentStore.getEnvironment(env, false);
240107
+ await environment.getPackage(pkg, false);
240108
+ } catch (error) {
240109
+ const { json, status } = internalErrorToHttpError(error);
240110
+ res.status(status).json(json);
240111
+ return;
240112
+ }
240113
+ if (sseConnectionCount >= MAX_SSE_CONNECTIONS) {
240114
+ res.status(503).json({
240115
+ code: 503,
240116
+ message: "Too many live-reload connections; try again shortly."
240117
+ });
240118
+ return;
240119
+ }
240120
+ sseConnectionCount++;
240121
+ res.set({
240122
+ "content-type": "text/event-stream",
240123
+ "cache-control": "no-cache",
240124
+ connection: "keep-alive",
240125
+ "x-accel-buffering": "no"
240126
+ });
240127
+ res.flushHeaders();
240128
+ const watching = watchModeController.isWatching(env);
240129
+ res.write(`event: hello
240130
+ data: connected
240131
+
240132
+ `);
240133
+ res.write(`event: mode
240134
+ data: ${watching ? "enabled" : "disabled"}
240135
+
240136
+ `);
240137
+ const key = `${env}/${pkg}`;
240138
+ const send = () => {
240139
+ res.write(`event: changed
240140
+ data: changed
240141
+
240142
+ `);
240143
+ };
240144
+ watchModeController.events.on(key, send);
240145
+ const heartbeat = setInterval(() => {
240146
+ res.write(`: heartbeat
240147
+
240148
+ `);
240149
+ }, 25000);
240150
+ const cleanup = () => {
240151
+ clearInterval(heartbeat);
240152
+ watchModeController.events.off(key, send);
240153
+ sseConnectionCount--;
240154
+ };
240155
+ req.on("close", cleanup);
240156
+ });
239815
240157
  app.get(`${API_PREFIX2}/environments`, async (_req, res) => {
239816
240158
  try {
239817
240159
  res.status(200).json(await environmentStore.listEnvironments());
@@ -240381,7 +240723,24 @@ registerLegacyRoutes(app, {
240381
240723
  manifestController
240382
240724
  });
240383
240725
  if (!isDevelopment) {
240384
- app.get("*", (_req, res) => res.sendFile(path10.resolve(ROOT, "index.html")));
240726
+ const SPA_INDEX = path11.resolve(ROOT, "index.html");
240727
+ app.get("*", (req, res) => {
240728
+ res.sendFile(SPA_INDEX, (err) => {
240729
+ if (!err)
240730
+ return;
240731
+ if (res.headersSent)
240732
+ return;
240733
+ res.status(404).type("text/html").send(`<!doctype html><meta charset="utf-8">
240734
+ <title>Publisher</title>
240735
+ <style>body{font:14px/1.4 -apple-system,system-ui,sans-serif;margin:40px;max-width:720px;color:#222}</style>
240736
+ <h1>Publisher is running, but the SPA bundle isn't built.</h1>
240737
+ <p>You requested <code>${req.path.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] ?? c)}</code>.
240738
+ The Publisher API is available at <a href="/api/v0/environments">/api/v0/environments</a>.</p>
240739
+ <p>To get the Publisher web UI, run <code>cd packages/app &amp;&amp; bunx vite build</code>
240740
+ or start the server with <code>NODE_ENV=development</code> after launching Vite on <code>:5173</code>.</p>
240741
+ <p>For in-package HTML data apps, browse to <code>/environments/&lt;env&gt;/packages/&lt;pkg&gt;/&lt;file&gt;</code> directly.</p>`);
240742
+ });
240743
+ });
240385
240744
  }
240386
240745
  app.use((err, _req, res, _next) => {
240387
240746
  logger.error("Unhandled error:", err);
@@ -240396,12 +240755,26 @@ var mainServer = http2.createServer({ maxHeaderSize: 262144 }, app);
240396
240755
  mainServer.timeout = 600000;
240397
240756
  mainServer.keepAliveTimeout = 600000;
240398
240757
  mainServer.headersTimeout = 600000;
240399
- mainServer.listen(PUBLISHER_PORT, PUBLISHER_HOST, () => {
240758
+ mainServer.listen(PUBLISHER_PORT, PUBLISHER_HOST, async () => {
240400
240759
  const address = mainServer.address();
240401
240760
  logger.info(`Publisher server listening at http://${address.address}:${address.port}`);
240402
240761
  if (isDevelopment) {
240403
240762
  logger.info("Running in development mode - proxying to React dev server at http://localhost:5173");
240404
240763
  }
240764
+ const watchEnvList = (process.env.PUBLISHER_WATCH || "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
240765
+ if (watchEnvList.length > 0) {
240766
+ if (watchEnvList.length > 1) {
240767
+ logger.warn(`Multiple watch environments requested (${watchEnvList.join(", ")}); watch mode auto-reloads one at a time. Watching "${watchEnvList[0]}". The others are mounted in place (their source is live) but will not auto-reload. Pass a single --watch-env (or one PUBLISHER_WATCH value) to silence this.`);
240768
+ }
240769
+ const envName = watchEnvList[0];
240770
+ try {
240771
+ await environmentStore.finishedInitialization;
240772
+ await watchModeController.ensureWatching(envName);
240773
+ logger.info(`Watch mode active for environment "${envName}" (in-place mount, source-edit live reload).`);
240774
+ } catch (error) {
240775
+ logger.error(`Failed to start watch mode for environment "${envName}"`, { error });
240776
+ }
240777
+ }
240405
240778
  });
240406
240779
  var mcpServer = mcpApp.listen(MCP_PORT, PUBLISHER_HOST, () => {
240407
240780
  logger.info(`MCP server listening at http://${PUBLISHER_HOST}:${MCP_PORT}`);